Skip to content

Commit

Permalink
Normative: move 'into' methods onto prototype and rename (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
bakkot authored Feb 7, 2024
1 parent 4202a5e commit 826e009
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 47 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ The hex methods do not take any options.

## Writing to an existing Uint8Array

The `Uint8Array.fromBase64Into` method allows writing to an existing Uint8Array. Like the [TextEncoder `encodeInto` method](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto), it returns a `{ read, written }` pair.
The `Uint8Array.prototype.setFromBase64` method allows writing to an existing Uint8Array. Like the [TextEncoder `encodeInto` method](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto), it returns a `{ read, written }` pair.

```js
let target = new Uint8Array(8);
let { read, written } = Uint8Array.fromBase64Into('Zm9vYmFy', target);
let { read, written } = target.setFromBase64('Zm9vYmFy');
assert.deepStrictEqual([...target], [102, 111, 111, 98, 97, 114, 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 8, written: 6 });
```
Expand All @@ -55,7 +55,7 @@ This method takes an optional final options bag with the same options as above.

As with `encodeInto`, there is not explicit support for writing to specified offset of the target, but you can accomplish that by creating a subarray.

`Uint8Array.fromHexInto` is the same except for hex.
`Uint8Array.prototype.setFromHex` is the same except for hex.

## Streaming

Expand Down
8 changes: 4 additions & 4 deletions playground/index-raw.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,22 @@ <h3>Options</h3>
</code></pre>

<h3>Writing to an existing Uint8Array</h3>
<p>The <code>Uint8Array.fromBase64Into</code> method allows writing to an existing Uint8Array. Like the <a href="https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto">TextEncoder <code>encodeInto</code> method</a>, it returns a <code>{ read, written }</code> pair.</p>
<p>The <code>Uint8Array.prototype.setFromBase64</code> method allows writing to an existing Uint8Array. Like the <a href="https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto">TextEncoder <code>encodeInto</code> method</a>, it returns a <code>{ read, written }</code> pair.</p>

<p>This method takes an optional final options bag with the same options as above.</p>

<pre class="language-js"><code class="language-js">
let target = new Uint8Array(7);
let { read, written } = Uint8Array.fromBase64Into('Zm9vYmFy', target);
let { read, written } = target.setFromBase64('Zm9vYmFy');
console.log({ target, read, written });
// { target: Uint8Array([102, 111, 111, 98, 97, 114, 0]), read: 8, written: 6 }
</code></pre>

<p><code>Uint8Array.fromHexInto</code> is the same except for hex.</p>
<p><code>Uint8Array.prototype.setFromHex</code> is the same except for hex.</p>

<pre class="language-js"><code class="language-js">
let target = new Uint8Array(6);
let { read, written } = Uint8Array.fromHexInto('deadbeef', target);
let { read, written } = target.setFromHex('deadbeef');
console.log({ target, read, written });
// { target: Uint8Array([222, 173, 190, 239, 0, 0]), read: 8, written: 4 }
</code></pre>
Expand Down
41 changes: 22 additions & 19 deletions playground/polyfill-install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ Object.defineProperty(Uint8Array, 'fromBase64', { enumerable: false });
Object.defineProperty(Uint8Array.fromBase64, 'length', { value: 1 });
Object.defineProperty(Uint8Array.fromBase64, 'name', { value: 'fromBase64' });

Uint8Array.fromBase64Into = (string, into, options) => {
if (typeof string !== 'string') {
throw new TypeError('expected input to be a string');
// method shenanigans to make a non-constructor which can refer to "this"
Uint8Array.prototype.setFromBase64 = {
setFromBase64(string, options) {
checkUint8Array(this);
if (typeof string !== 'string') {
throw new TypeError('expected input to be a string');
}
let { read, bytes } = base64ToUint8Array(string, options, this);
return { read, written: bytes.length };
}
checkUint8Array(into);
let { read, bytes } = base64ToUint8Array(string, options, into);
return { read, written: bytes.length };
};
Object.defineProperty(Uint8Array, 'fromBase64Into', { enumerable: false });
Object.defineProperty(Uint8Array.fromBase64Into, 'length', { value: 2 });
Object.defineProperty(Uint8Array.fromBase64Into, 'name', { value: 'fromBase64Into' });
}.setFromBase64;
Object.defineProperty(Uint8Array.prototype, 'setFromBase64', { enumerable: false });
Object.defineProperty(Uint8Array.prototype.setFromBase64, 'length', { value: 1 });

Uint8Array.prototype.toHex = {
toHex() {
Expand All @@ -47,13 +49,14 @@ Uint8Array.fromHex = (string) => {
Object.defineProperty(Uint8Array, 'fromHex', { enumerable: false });
Object.defineProperty(Uint8Array.fromHex, 'name', { value: 'fromHex' });

Uint8Array.fromHexInto = (string, into) => {
if (typeof string !== 'string') {
throw new TypeError('expected input to be a string');
Uint8Array.prototype.setFromHex = {
setFromHex(string) {
checkUint8Array(this);
if (typeof string !== 'string') {
throw new TypeError('expected input to be a string');
}
let { read, bytes } = hexToUint8Array(string, this);
return { read, written: bytes.length };
}
checkUint8Array(into);
let { read, bytes } = hexToUint8Array(string, into);
return { read, written: bytes.length };
};
Object.defineProperty(Uint8Array, 'fromHexInto', { enumerable: false });
Object.defineProperty(Uint8Array.fromHexInto, 'name', { value: 'fromHexInto' });
}.setFromHex;
Object.defineProperty(Uint8Array.prototype, 'setFromHex', { enumerable: false });
14 changes: 8 additions & 6 deletions spec.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ <h1>Uint8Array.fromBase64 ( _string_ [ , _options_ ] )</h1>
</emu-alg>
</emu-clause>

<emu-clause id="sec-uint8array.frombase64into">
<h1>Uint8Array.fromBase64Into ( _string_, _into_ [ , _options_ ] )</h1>
<emu-clause id="sec-uint8array.prototype.setfrombase64">
<h1>Uint8Array.prototype.setFromBase64 ( _string_ [ , _options_ ] )</h1>
<emu-alg>
1. If _string_ is not a String, throw a *TypeError* exception.
1. Let _into_ be the *this* value.
1. Perform ? ValidateUint8Array(_into_).
1. If _string_ is not a String, throw a *TypeError* exception.
1. Let _opts_ be ? GetOptionsObject(_options_).
1. Let _alphabet_ be ? Get(_opts_, *"alphabet"*).
1. If _alphabet_ is *undefined*, set _alphabet_ to *"base64"*.
Expand Down Expand Up @@ -111,11 +112,12 @@ <h1>Uint8Array.fromHex ( _string_ )</h1>
</emu-alg>
</emu-clause>

<emu-clause id="sec-uint8array.fromhexinto">
<h1>Uint8Array.fromHexInto ( _string_, _into_ )</h1>
<emu-clause id="sec-uint8array.prototype.setfromhex">
<h1>Uint8Array.prototype.setFromHex ( _string_ )</h1>
<emu-alg>
1. If _string_ is not a String, throw a *TypeError* exception.
1. Let _into_ be the *this* value.
1. Perform ? ValidateUint8Array(_into_).
1. If _string_ is not a String, throw a *TypeError* exception.
1. Let _taRecord_ be MakeTypedArrayWithBufferWitnessRecord(_into_, ~seq-cst~).
1. If IsTypedArrayOutOfBounds(_taRecord_) is *true*, throw a *TypeError* exception.
1. Let _byteLength_ be TypedArrayByteLength(_taRecord_).
Expand Down
2 changes: 1 addition & 1 deletion stream.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Base64Decoder {
// but may be too much if there is whitespace
// if you're really concerned about memory, a TextDecoder style API is a bad choice
let buffer = new Uint8Array(Math.ceil(chunk.length * 3 / 4));
let { read, written } = Uint8Array.fromBase64Into(chunk, buffer, opts);
let { read, written } = buffer.setFromBase64(chunk, opts);
buffer = buffer.subarray(0, written);
this.#extra = chunk.slice(read);
return buffer;
Expand Down
28 changes: 14 additions & 14 deletions test-polyfill.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,70 +96,70 @@ test('writing to an existing buffer', async t => {

await t.test('buffer exact', () => {
let output = new Uint8Array(6);
let { read, written } = Uint8Array.fromBase64Into(foobarInput, output);
let { read, written } = output.setFromBase64(foobarInput);
assert.deepStrictEqual([...output], foobarOutput);
assert.deepStrictEqual({ read, written }, { read: 8, written: 6 });
});

await t.test('buffer too large', () => {
let output = new Uint8Array(8);
let { read, written } = Uint8Array.fromBase64Into(foobarInput, output);
let { read, written } = output.setFromBase64(foobarInput);
assert.deepStrictEqual([...output], [...foobarOutput, 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 8, written: 6 });
});

await t.test('buffer too small', () => {
let output = new Uint8Array(5);
let { read, written } = Uint8Array.fromBase64Into(foobarInput, output);
let { read, written } = output.setFromBase64(foobarInput);
assert.deepStrictEqual([...output], [...foobarOutput.slice(0, 3), 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 4, written: 3 });
});

await t.test('buffer exact, padded', () => {
let output = new Uint8Array(5);
let { read, written } = Uint8Array.fromBase64Into(foobaInput + '=', output);
let { read, written } = output.setFromBase64(foobaInput + '=');
assert.deepStrictEqual([...output], foobarOutput.slice(0, 5));
assert.deepStrictEqual({ read, written }, { read: 8, written: 5 });
});

await t.test('buffer exact, not padded', () => {
let output = new Uint8Array(5);
let { read, written } = Uint8Array.fromBase64Into(foobaInput, output);
let { read, written } = output.setFromBase64(foobaInput);
assert.deepStrictEqual([...output], foobarOutput.slice(0, 5));
assert.deepStrictEqual({ read, written }, { read: 7, written: 5 });
});

await t.test('buffer exact, padded, stop-before-partial', () => {
let output = new Uint8Array(5);
let { read, written } = Uint8Array.fromBase64Into(foobaInput + '=', output, { lastChunkHandling: 'stop-before-partial' });
let { read, written } = output.setFromBase64(foobaInput + '=', { lastChunkHandling: 'stop-before-partial' });
assert.deepStrictEqual([...output], foobarOutput.slice(0, 5));
assert.deepStrictEqual({ read, written }, { read: 8, written: 5 });
});

await t.test('buffer exact, not padded, stop-before-partial', () => {
let output = new Uint8Array(5);
let { read, written } = Uint8Array.fromBase64Into(foobaInput, output, { lastChunkHandling: 'stop-before-partial' });
let { read, written } = output.setFromBase64(foobaInput, { lastChunkHandling: 'stop-before-partial' });
assert.deepStrictEqual([...output], [...foobarOutput.slice(0, 3), 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 4, written: 3 });
});

await t.test('buffer too small, padded', () => {
let output = new Uint8Array(4);
let { read, written } = Uint8Array.fromBase64Into(foobaInput + '=', output);
let { read, written } = output.setFromBase64(foobaInput + '=');
assert.deepStrictEqual([...output], [...foobarOutput.slice(0, 3), 0]);
assert.deepStrictEqual({ read, written }, { read: 4, written: 3 });
});

await t.test('buffer too large, trailing whitespace', () => {
let output = new Uint8Array(8);
let { read, written } = Uint8Array.fromBase64Into(foobarInput + ' '.repeat(10), output);
let { read, written } = output.setFromBase64(foobarInput + ' '.repeat(10));
assert.deepStrictEqual([...output], [...foobarOutput, 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 18, written: 6 });
});

await t.test('buffer too large, not padded, trailing whitespace', () => {
let output = new Uint8Array(8);
let { read, written } = Uint8Array.fromBase64Into(foobaInput + ' '.repeat(10), output);
let { read, written } = output.setFromBase64(foobaInput + ' '.repeat(10));
assert.deepStrictEqual([...output], [...foobarOutput.slice(0, 5), 0, 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 17, written: 5 });
});
Expand All @@ -181,7 +181,7 @@ test('stop-before-partial', async t => {

await t.test('no padding, trailing whitespace', () => {
let output = new Uint8Array(8);
let { read, written } = Uint8Array.fromBase64Into(foobaInput + ' '.repeat(10), output, { lastChunkHandling: 'stop-before-partial' });
let { read, written } = output.setFromBase64(foobaInput + ' '.repeat(10), { lastChunkHandling: 'stop-before-partial' });
assert.deepStrictEqual([...output], [...foobarOutput.slice(0, 3), 0, 0, 0, 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 4, written: 3 });
});
Expand All @@ -197,21 +197,21 @@ test('hex', async t => {

await t.test('decode into, exact', () => {
let output = new Uint8Array(4);
let { read, written } = Uint8Array.fromHexInto(encoded, output);
let { read, written } = output.setFromHex(encoded);
assert.deepStrictEqual([...output], decoded);
assert.deepStrictEqual({ read, written }, { read: 8, written: 4 });
});

await t.test('decode into, buffer too large', () => {
let output = new Uint8Array(6);
let { read, written } = Uint8Array.fromHexInto(encoded, output);
let { read, written } = output.setFromHex(encoded);
assert.deepStrictEqual([...output], [...decoded, 0, 0]);
assert.deepStrictEqual({ read, written }, { read: 8, written: 4 });
});

await t.test('decode into, buffer too small', () => {
let output = new Uint8Array(3);
let { read, written } = Uint8Array.fromHexInto(encoded, output);
let { read, written } = output.setFromHex(encoded);
assert.deepStrictEqual([...output], decoded.slice(0, 3));
assert.deepStrictEqual({ read, written }, { read: 6, written: 3 });
});
Expand Down

0 comments on commit 826e009

Please sign in to comment.