diff --git a/playground/index-raw.html b/playground/index-raw.html index f4ca6f9..7cca8c2 100644 --- a/playground/index-raw.html +++ b/playground/index-raw.html @@ -48,8 +48,8 @@
This page documents an early-stage proposal for native base64 support in JavaScript, and includes a non-production polyfill you can experiment with in the browser's console.
-The proposal would provide methods for encoding and decoding Uint8Arrays as base64 strings.
+This page documents an early-stage proposal for native base64 and hex encoding and decoding for binary data in JavaScript, and includes a non-production, slightly inaccurate polyfill you can experiment with in the browser's console. Some details of the polyfill, particularly around coercion and order of observable effects, are not identical to the proposed spec text.
+The proposal would provide methods for encoding and decoding Uint8Arrays as base64 and hex strings.
Feedback on the proposal's repository is appreciated.
Encoding a Uint8Array to a hex string:
+
+let arr = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
+console.log(arr.toHex());
+// '48656c6c6f20576f726c64='
+
+
+Decoding a hex string to a Uint8Array:
+
+let string = '48656c6c6f20576f726c64';
+console.log(Uint8Array.fromHex(string));
+// Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100])
+
+
Both methods take an optional options bag which allows specifying the alphabet as either "base64" (the default) or "base64url" (the URL-safe variant).
+The base64 methods take an optional options bag which allows specifying the alphabet as either "base64" (the default) or "base64url" (the URL-safe variant).
In the future this may allow specifying arbitrary alphabets.
In later versions of this proposal the options bag may also allow additional options, such as specifying whether to generate / enforce padding characters and how to handle whitespace.
+The hex methods do not have any options.
let array = new Uint8Array([251, 255, 191]);
@@ -84,6 +99,7 @@ Options
Streaming
Two additional methods, toPartialBase64
and fromPartialBase64
, allow encoding and decoding chunks of base64. This requires managing state, which is handled by returning a { result, extra }
pair. The options bag for these methods takes two additional arguments, one which specifies whether more data is expected and one which specifies any extra values returned by a previous call.
These methods are intended for lower-level use and are less convenient to use.
+Streaming versions of the hex APIs are not included since they are straightforward to do manually.
Streaming an ArrayBuffer into chunks of base64 strings:
diff --git a/playground/polyfill-core.mjs b/playground/polyfill-core.mjs
index 40d5c03..5756f0f 100644
--- a/playground/polyfill-core.mjs
+++ b/playground/polyfill-core.mjs
@@ -174,3 +174,28 @@ export function base64ToUint8Array(str, alphabetIdentifier = 'base64', more = fa
};
}
+export function uint8ArrayToHex(arr) {
+ checkUint8Array(arr);
+ let out = '';
+ for (let i = 0; i < arr.length; ++i) {
+ out += arr[i].toString(16).padStart(2, '0');
+ }
+ return out;
+}
+
+export function hexToUint8Array(str) {
+ if (typeof str !== 'string') {
+ throw new TypeError('expected str to be a string');
+ }
+ if (str.length % 2 !== 0) {
+ throw new SyntaxError('str should be an even number of characters');
+ }
+ if (/[^0-9a-zA-Z]/.test(str)) {
+ throw new SyntaxError('str should only contain hex characters');
+ }
+ let out = new Uint8Array(str.length / 2);
+ for (let i = 0; i < out.length; ++i) {
+ out[i] = parseInt(str.slice(i * 2, i * 2 + 2), 16);
+ }
+ return out;
+}
diff --git a/playground/polyfill-install.mjs b/playground/polyfill-install.mjs
index 6b2b1c5..703f632 100644
--- a/playground/polyfill-install.mjs
+++ b/playground/polyfill-install.mjs
@@ -1,4 +1,4 @@
-import { checkUint8Array, uint8ArrayToBase64, base64ToUint8Array } from './polyfill-core.mjs';
+import { checkUint8Array, uint8ArrayToBase64, base64ToUint8Array, uint8ArrayToHex, hexToUint8Array } from './polyfill-core.mjs';
Uint8Array.prototype.toBase64 = function (opts) {
checkUint8Array(this);
@@ -39,3 +39,15 @@ Uint8Array.fromPartialBase64 = function (string, opts) {
}
return base64ToUint8Array(string, alphabet, more, extra);
};
+
+Uint8Array.prototype.toHex = function () {
+ checkUint8Array(this);
+ return uint8ArrayToHex(this);
+};
+
+Uint8Array.fromHex = function (string) {
+ if (typeof string !== 'string') {
+ throw new Error('expected argument to be a string');
+ }
+ return hexToUint8Array(string);
+};
diff --git a/spec.html b/spec.html
index b4f6ba5..b71e4f6 100644
--- a/spec.html
+++ b/spec.html
@@ -34,7 +34,7 @@ Uint8Array.prototype.toBase64 ( [ _options_ ] )
-
+
Uint8Array.prototype.toPartialBase64 ( [ _options_ ] )
1. Let _O_ be the *this* value.
@@ -72,10 +72,24 @@ Uint8Array.prototype.toPartialBase64 ( [ _options_ ] )
+
+ Uint8Array.prototype.toHex ( )
+
+ 1. Let _O_ be the *this* value.
+ 1. Let _toEncode_ be ? GetUint8ArrayBytes(_O_).
+ 1. Let _out_ be the empty String.
+ 1. For each byte _byte_ of _toEncode_, do
+ 1. Let _str_ be Number::toString(_byte_, 16).
+ 1. If the length of _str_ is 1, set _str_ to the string-concatenation of *"0"* and _str_.
+ 1. Set _out_ to the string-concatenation of _out_ and _str_.
+ 1. Return _out_.
+
+
+
Uint8Array.fromBase64 ( _string_ [ , _options_ ] )
- 1. Set _string_ to ? GetStringForBase64(_string_).
+ 1. Set _string_ to ? GetStringForBinaryEncoding(_string_).
1. Set _options_ to ? GetOptionsObject(_options_).
1. Let _alphabet_ be ? Get(_options_, *"alphabet"*).
1. If _alphabet_ is *undefined*, set _alphabet_ to *"base64"*.
@@ -93,7 +107,7 @@ Uint8Array.fromBase64 ( _string_ [ , _options_ ] )
1. If _characters_ cannot result from applying the base64url encoding specified in section 5 of RFC 4648 to some sequence of bytes, throw a *SyntaxError* exception.
1. Let _bytes_ be the unique sequence of bytes such that applying the base64url encoding specified in section 5 of RFC 4648 to that sequence would produce _characters_.
1. Let _resultLength_ be the number of bytes in _bytes_.
- 1. Let _result_ be ? AllocateTypedArray(*"Uint8Array*", %Uint8Array%, %Uint8Array.prototype%, _resultLength_).
+ 1. Let _result_ be ? AllocateTypedArray(*"Uint8Array"*, %Uint8Array%, %Uint8Array.prototype%, _resultLength_).
1. Set the value at each index of _result_.[[ViewedArrayBuffer]].[[ArrayBufferData]] to the value at the corresponding index of _bytes_.
1. Return _result_.
@@ -102,7 +116,7 @@ Uint8Array.fromBase64 ( _string_ [ , _options_ ] )
Uint8Array.fromPartialBase64 ( _string_ [ , _options_ ] )
- 1. Set _string_ to ? GetStringForBase64(_string_).
+ 1. Set _string_ to ? GetStringForBinaryEncoding(_string_).
1. Set _options_ to ? GetOptionsObject(_options_).
1. Let _alphabet_ be ? Get(_options_, *"alphabet"*).
1. If _alphabet_ is *undefined*, set _alphabet_ to *"base64"*.
@@ -111,7 +125,7 @@ Uint8Array.fromPartialBase64 ( _string_ [ , _options_ ] )
1. Let _more_ be ToBoolean(? Get(_options_, *"more"*)).
1. Let _extra_ be ? Get(_options_, *"extra"*).
1. If _extra_ is neither *undefined* nor *null*, then
- 1. Let _extraString_ be ? GetStringForBase64(_extra_).
+ 1. Let _extraString_ be ? GetStringForBinaryEncoding(_extra_).
1. Set _string_ to the list-concatenation of _extraString_ and _string_.
1. If _more_ is *true*, then
1. TODO: think about handling of padding on _string_ / _extra_ in this case. This currently assumes no padding on either.
@@ -134,7 +148,7 @@ Uint8Array.fromPartialBase64 ( _string_ [ , _options_ ] )
1. If _characters_ cannot result from applying the base64url encoding specified in section 5 of RFC 4648 to some sequence of bytes, throw a *SyntaxError* exception.
1. Let _bytes_ be the unique sequence of bytes such that applying the base64url encoding specified in section 5 of RFC 4648 to that sequence would produce _characters_.
1. Let _resultLength_ be the number of bytes in _bytes_.
- 1. Let _result_ be ? AllocateTypedArray(*"Uint8Array*", %Uint8Array%, %Uint8Array.prototype%, _resultLength_).
+ 1. Let _result_ be ? AllocateTypedArray(*"Uint8Array"*, %Uint8Array%, %Uint8Array.prototype%, _resultLength_).
1. Set the value at each index of _result_.[[ViewedArrayBuffer]].[[ArrayBufferData]] to the value at the corresponding index of _bytes_.
1. Let _obj_ be OrdinaryObjectCreate(%Object.prototype%).
1. Perform ! CreateDataPropertyOrThrow(_obj_, *"result"*, _result_).
@@ -143,6 +157,27 @@ Uint8Array.fromPartialBase64 ( _string_ [ , _options_ ] )
+
+ Uint8Array.fromHex ( _string_ )
+
+ 1. Set _string_ to ? GetStringForBinaryEncoding(_string_).
+ 1. TODO: consider stripping whitespace here.
+ 1. Let _stringLen_ be the length of _string_.
+ 1. If _stringLen_ modulo 2 is not 0, throw a *SyntaxError* exception.
+ 1. If _string_ contains any code units which are not in *"0123456789abcdefABCDEF"*, throw a *SyntaxError* exception.
+ 1. Let _resultLength_ be _stringLen_ / 2.
+ 1. Let _result_ be ? AllocateTypedArray(*"Uint8Array"*, %Uint8Array%, %Uint8Array.prototype%, _resultLength_).
+ 1. Let _index_ be 0.
+ 1. Repeat, while _index_ < _resultLength_,
+ 1. Let _stringIndex_ be _index_ * 2.
+ 1. Let _hexits_ be the substring of _string_ from _stringIndex_ to _stringIndex_ + 2.
+ 1. Let _byte_ be the integer value represented by _hexits_ in base-16 notation, using the letters A-F and a-f for digits with values 10 through 15.
+ 1. Perform ! IntegerIndexedElementSet(_result_, 𝔽(_index_), 𝔽(_byte_)).
+ 1. Set _index_ to _index_ + 1.
+ 1. Return _result_.
+
+
+
GetUint8ArrayBytes (
@@ -162,18 +197,18 @@
-
+
- GetStringForBase64 (
+ GetStringForBinaryEncoding (
_arg_: an ECMAScript language value,
): either a normal completion containing a String or a throw completion
1. If _arg_ is an Object, return ? ToString(_arg_).
- 1. NOTE: Because `[` is not a valid base64 character, the Strings returned by %Object.prototype.toString% will produce a SyntaxError below. Implementations are encouraged to provide an informative error message in that situations.
+ 1. NOTE: Because `[` is not a valid base64 or hex character, the Strings returned by %Object.prototype.toString% will produce a SyntaxError during encoding. Implementations are encouraged to provide an informative error message in that situations.
1. Else if _arg_ is not a String, throw a TypeError exception.
- 1. NOTE: The above step is included to prevent errors such as accidentally passing `null` and receiving a Uint8Array containing the bytes « 0x9e, 0xe9, 0x65 ».
+ 1. NOTE: The above step is included to prevent errors such as accidentally passing `null` to `fromBase64` and receiving a Uint8Array containing the bytes « 0x9e, 0xe9, 0x65 ».
1. Return _arg_.