Skip to content
This repository was archived by the owner on Oct 28, 2025. It is now read-only.

Commit 66fa8c8

Browse files
authored
write data up until error in setFromX methods (#58)
1 parent 58eaa4c commit 66fa8c8

File tree

3 files changed

+130
-63
lines changed

3 files changed

+130
-63
lines changed

playground/polyfill-core.mjs

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function skipAsciiWhitespace(string, index) {
125125

126126
function fromBase64(string, alphabet, lastChunkHandling, maxLength) {
127127
if (maxLength === 0) {
128-
return { read: 0, bytes: [] };
128+
return { read: 0, bytes: [], error: null };
129129
}
130130

131131
let read = 0;
@@ -138,62 +138,69 @@ function fromBase64(string, alphabet, lastChunkHandling, maxLength) {
138138
if (index === string.length) {
139139
if (chunk.length > 0) {
140140
if (lastChunkHandling === 'stop-before-partial') {
141-
return { bytes, read };
141+
return { bytes, read, error: null };
142142
} else if (lastChunkHandling === 'loose') {
143143
if (chunk.length === 1) {
144-
throw new SyntaxError('malformed padding: exactly one additional character');
144+
let error = new SyntaxError('malformed padding: exactly one additional character');
145+
return { bytes, read, error };
145146
}
146147
bytes.push(...decodeBase64Chunk(chunk, false));
147148
} else {
148149
assert(lastChunkHandling === 'strict');
149-
throw new SyntaxError('missing padding');
150+
let error = new SyntaxError('missing padding');
151+
return { bytes, read, error };
150152
}
151153
}
152-
return { bytes, read: string.length };
154+
return { bytes, read: string.length, error: null };
153155
}
154156
let char = string[index];
155157
++index;
156158
if (char === '=') {
157159
if (chunk.length < 2) {
158-
throw new SyntaxError('padding is too early');
160+
let error = new SyntaxError('padding is too early');
161+
return { bytes, read, error };
159162
}
160163
index = skipAsciiWhitespace(string, index);
161164
if (chunk.length === 2) {
162165
if (index === string.length) {
163166
if (lastChunkHandling === 'stop-before-partial') {
164167
// two characters then `=` then EOS: this is, technically, a partial chunk
165-
return { bytes, read };
168+
return { bytes, read, error: null };
166169
}
167-
throw new SyntaxError('malformed padding - only one =');
170+
let error = new SyntaxError('malformed padding - only one =');
171+
return { bytes, read, error };
168172
}
169173
if (string[index] === '=') {
170174
++index;
171175
index = skipAsciiWhitespace(string, index);
172176
}
173177
}
174178
if (index < string.length) {
175-
throw new SyntaxError('unexpected character after padding');
179+
let error = new SyntaxError('unexpected character after padding');
180+
return { bytes, read, error };
176181
}
177182
bytes.push(...decodeBase64Chunk(chunk, lastChunkHandling === 'strict'));
178183
assert(bytes.length <= maxLength);
179-
return { bytes, read: string.length };
184+
return { bytes, read: string.length, error: null };
180185
}
181186
if (alphabet === 'base64url') {
182187
if (char === '+' || char === '/') {
183-
throw new SyntaxError(`unexpected character ${JSON.stringify(char)}`);
188+
let error = new SyntaxError(`unexpected character ${JSON.stringify(char)}`);
189+
return { bytes, read, error };
184190
} else if (char === '-') {
185191
char = '+';
186192
} else if (char === '_') {
187193
char = '/';
188194
}
189195
}
190196
if (!base64Characters.includes(char)) {
191-
throw new SyntaxError(`unexpected character ${JSON.stringify(char)}`);
197+
let error = new SyntaxError(`unexpected character ${JSON.stringify(char)}`);
198+
return { bytes, read, error };
192199
}
193200
let remainingBytes = maxLength - bytes.length;
194201
if (remainingBytes === 1 && chunk.length === 2 || remainingBytes === 2 && chunk.length === 3) {
195202
// special case: we can fit exactly the number of bytes currently represented by chunk, so we were just checking for `=`
196-
return { bytes, read };
203+
return { bytes, read, error: null };
197204
}
198205

199206
chunk += char;
@@ -203,7 +210,7 @@ function fromBase64(string, alphabet, lastChunkHandling, maxLength) {
203210
read = index;
204211
assert(bytes.length <= maxLength);
205212
if (bytes.length === maxLength) {
206-
return { bytes, read };
213+
return { bytes, read, error: null };
207214
}
208215
}
209216
}
@@ -231,14 +238,21 @@ export function base64ToUint8Array(string, options, into) {
231238

232239
let maxLength = into ? into.length : 2 ** 53 - 1;
233240

234-
let { bytes, read } = fromBase64(string, alphabet, lastChunkHandling, maxLength);
241+
let { bytes, read, error } = fromBase64(string, alphabet, lastChunkHandling, maxLength);
242+
if (error && !into) {
243+
throw error;
244+
}
235245

236246
bytes = new Uint8Array(bytes);
237247
if (into && bytes.length > 0) {
238248
assert(bytes.length <= into.length);
239249
into.set(bytes);
240250
}
241251

252+
if (error) {
253+
throw error;
254+
}
255+
242256
return { read, bytes };
243257
}
244258

@@ -254,6 +268,26 @@ export function uint8ArrayToHex(arr) {
254268
return out;
255269
}
256270

271+
function fromHex(string, maxLength) {
272+
let bytes = [];
273+
let read = 0;
274+
if (maxLength > 0) {
275+
while (read < string.length) {
276+
let hexits = string.slice(read, read + 2);
277+
if (/[^0-9a-fA-F]/.test(hexits)) {
278+
let error = new SyntaxError('string should only contain hex characters');
279+
return { read, bytes, error }
280+
}
281+
bytes.push(parseInt(hexits, 16));
282+
read += 2;
283+
if (bytes.length === maxLength) {
284+
break;
285+
}
286+
}
287+
}
288+
return { read, bytes, error: null }
289+
}
290+
257291
export function hexToUint8Array(string, into) {
258292
if (typeof string !== 'string') {
259293
throw new TypeError('expected string to be a string');
@@ -266,23 +300,9 @@ export function hexToUint8Array(string, into) {
266300
}
267301

268302
let maxLength = into ? into.length : 2 ** 53 - 1;
269-
270-
// TODO should hex allow whitespace?
271-
// TODO should hex support lastChunkHandling? (only 'strict' or 'stop-before-partial')
272-
let bytes = [];
273-
let index = 0;
274-
if (maxLength > 0) {
275-
while (index < string.length) {
276-
let hexits = string.slice(index, index + 2);
277-
if (/[^0-9a-fA-F]/.test(hexits)) {
278-
throw new SyntaxError('string should only contain hex characters');
279-
}
280-
bytes.push(parseInt(hexits, 16));
281-
index += 2;
282-
if (bytes.length === maxLength) {
283-
break;
284-
}
285-
}
303+
let { read, bytes, error } = fromHex(string, maxLength);
304+
if (error && !into) {
305+
throw error;
286306
}
287307

288308
bytes = new Uint8Array(bytes);
@@ -291,5 +311,9 @@ export function hexToUint8Array(string, into) {
291311
into.set(bytes);
292312
}
293313

294-
return { read: index, bytes };
314+
if (error) {
315+
throw error;
316+
}
317+
318+
return { read, bytes };
295319
}

spec.html

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ <h1>Uint8Array.fromBase64 ( _string_ [ , _options_ ] )</h1>
6060
1. Let _lastChunkHandling_ be ? Get(_opts_, *"lastChunkHandling"*).
6161
1. If _lastChunkHandling_ is *undefined*, set _lastChunkHandling_ to *"loose"*.
6262
1. If _lastChunkHandling_ is not one of *"loose"*, *"strict"*, or *"stop-before-partial"*, throw a *TypeError* exception.
63-
1. Let _result_ be ? FromBase64(_string_, _alphabet_, _lastChunkHandling_).
63+
1. Let _result_ be FromBase64(_string_, _alphabet_, _lastChunkHandling_).
64+
1. If _result_.[[Error]] is not ~none~, then
65+
1. Throw _result_.[[Error]].
6466
1. Let _resultLength_ be the length of _result_.[[Bytes]].
6567
1. Let _ta_ be ? <emu-meta suppress-effects="user-code">AllocateTypedArray(*"Uint8Array"*, %Uint8Array%, *"%Uint8Array.prototype%"*, _resultLength_)</emu-meta>.
6668
1. Set the value at each index of _ta_.[[ViewedArrayBuffer]].[[ArrayBufferData]] to the value at the corresponding index of _result_.[[Bytes]].
@@ -84,12 +86,14 @@ <h1>Uint8Array.prototype.setFromBase64 ( _string_ [ , _options_ ] )</h1>
8486
1. Let _taRecord_ be MakeTypedArrayWithBufferWitnessRecord(_into_, ~seq-cst~).
8587
1. If IsTypedArrayOutOfBounds(_taRecord_) is *true*, throw a *TypeError* exception.
8688
1. Let _byteLength_ be TypedArrayLength(_taRecord_).
87-
1. Let _result_ be ? FromBase64(_string_, _alphabet_, _lastChunkHandling_, _byteLength_).
89+
1. Let _result_ be FromBase64(_string_, _alphabet_, _lastChunkHandling_, _byteLength_).
8890
1. Let _bytes_ be _result_.[[Bytes]].
8991
1. Let _written_ be the length of _bytes_.
9092
1. NOTE: FromBase64 does not invoke any user code, so the ArrayBuffer backing _into_ cannot have been detached or shrunk.
9193
1. Assert: _written__byteLength_.
9294
1. Perform SetUint8ArrayBytes(_into_, _bytes_).
95+
1. If _result_.[[Error]] is not ~none~, then
96+
1. Throw _result_.[[Error]].
9397
1. Let _resultObject_ be OrdinaryObjectCreate(%Object.prototype%).
9498
1. Perform ! CreateDataPropertyOrThrow(_resultObject_, *"read"*, 𝔽(_result_.[[Read]])).
9599
1. Perform ! CreateDataPropertyOrThrow(_resultObject_, *"written"*, 𝔽(_written_)).
@@ -101,7 +105,9 @@ <h1>Uint8Array.prototype.setFromBase64 ( _string_ [ , _options_ ] )</h1>
101105
<h1>Uint8Array.fromHex ( _string_ )</h1>
102106
<emu-alg>
103107
1. If _string_ is not a String, throw a *TypeError* exception.
104-
1. Let _result_ be ? FromHex(_string_).
108+
1. Let _result_ be FromHex(_string_).
109+
1. If _result_.[[Error]] is not ~none~, then
110+
1. Throw _result_.[[Error]].
105111
1. Let _resultLength_ be the length of _result_.[[Bytes]].
106112
1. Let _ta_ be ? <emu-meta suppress-effects="user-code">AllocateTypedArray(*"Uint8Array"*, %Uint8Array%, *"%Uint8Array.prototype%"*, _resultLength_)</emu-meta>.
107113
1. Set the value at each index of _ta_.[[ViewedArrayBuffer]].[[ArrayBufferData]] to the value at the corresponding index of _result_.[[Bytes]].
@@ -118,12 +124,14 @@ <h1>Uint8Array.prototype.setFromHex ( _string_ )</h1>
118124
1. Let _taRecord_ be MakeTypedArrayWithBufferWitnessRecord(_into_, ~seq-cst~).
119125
1. If IsTypedArrayOutOfBounds(_taRecord_) is *true*, throw a *TypeError* exception.
120126
1. Let _byteLength_ be TypedArrayLength(_taRecord_).
121-
1. Let _result_ be ? FromHex(_string_, _byteLength_).
127+
1. Let _result_ be FromHex(_string_, _byteLength_).
122128
1. Let _bytes_ be _result_.[[Bytes]].
123129
1. Let _written_ be the length of _bytes_.
124130
1. NOTE: FromHex does not invoke any user code, so the ArrayBuffer backing _into_ cannot have been detached or shrunk.
125131
1. Assert: _written__byteLength_.
126132
1. Perform SetUint8ArrayBytes(_into_, _bytes_).
133+
1. If _result_.[[Error]] is not ~none~, then
134+
1. Throw _result_.[[Error]].
127135
1. Let _resultObject_ be OrdinaryObjectCreate(%Object.prototype%).
128136
1. Perform ! CreateDataPropertyOrThrow(_resultObject_, *"read"*, 𝔽(_result_.[[Read]])).
129137
1. Perform ! CreateDataPropertyOrThrow(_resultObject_, *"written"*, 𝔽(_written_)).
@@ -254,7 +262,7 @@ <h1>
254262
_alphabet_: *"base64"* or *"base64url"*,
255263
_lastChunkHandling_: *"loose"*, *"strict"*, or *"stop-before-partial"*,
256264
optional _maxLength_: a non-negative integer,
257-
): either a normal completion containing a Record with fields [[Read]] (an integral Number) and [[Bytes]] (a List of byte values), or a throw completion
265+
): a Record with fields [[Read]] (an integral Number), [[Bytes]] (a List of byte values), and [[Error]] (either ~none~ or a throw completion)
258266
</h1>
259267
<dl class="header">
260268
</dl>
@@ -264,7 +272,7 @@ <h1>
264272
1. NOTE: Because the input is a string, the length of strings is limited to 2<sup>53</sup> - 1 characters, and the output requires no more bytes than the input has characters, this limit can never be reached. However, it is editorially convenient to use a finite value here.
265273
1. NOTE: The order of validation and decoding in the algorithm below is not observable. Implementations are encouraged to perform them in whatever order is most efficient, possibly interleaving validation with decoding, as long as the behaviour is observably equivalent.
266274
1. If _maxLength_ is 0, then
267-
1. Return the Record { [[Read]]: 0, [[Bytes]]: « » }.
275+
1. Return the Record { [[Read]]: 0, [[Bytes]]: « », [[Error]]: ~none~ }.
268276
1. Let _read_ be 0.
269277
1. Let _bytes_ be « ».
270278
1. Let _chunk_ be the empty String.
@@ -276,43 +284,58 @@ <h1>
276284
1. If _index_ = _length_, then
277285
1. If _chunkLength_ > 0, then
278286
1. If _lastChunkHandling_ is *"stop-before-partial"*, then
279-
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_ }.
287+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: ~none~ }.
280288
1. Else if _lastChunkHandling_ is *"loose"*, then
281289
1. If _chunkLength_ is 1, then
282-
1. Throw a *SyntaxError* exception.
290+
1. Let _error_ be a new *SyntaxError* exception.
291+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
283292
1. Set _bytes_ to the list-concatenation of _bytes_ and ! DecodeBase64Chunk(_chunk_, *false*).
284293
1. Else,
285294
1. Assert: _lastChunkHandling_ is *"strict"*.
286-
1. Throw a *SyntaxError* exception.
287-
1. Return the Record { [[Read]]: _length_, [[Bytes]]: _bytes_ }.
295+
1. Let _error_ be a new *SyntaxError* exception.
296+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
297+
1. Return the Record { [[Read]]: _length_, [[Bytes]]: _bytes_, [[Error]]: ~none~ }.
288298
1. Let _char_ be the substring of _string_ from _index_ to _index_ + 1.
289299
1. Set _index_ to _index_ + 1.
290300
1. If _char_ is *"="*, then
291301
1. If _chunkLength_ < 2, then
292-
1. Throw a *SyntaxError* exception.
302+
1. Let _error_ be a new *SyntaxError* exception.
303+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
293304
1. Set _index_ to SkipAsciiWhitespace(_string_, _index_).
294305
1. If _chunkLength_ = 2, then
295306
1. If _index_ = _length_, then
296307
1. If _lastChunkHandling_ is *"stop-before-partial"*, then
297-
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_ }.
298-
1. Throw a *SyntaxError* exception.
308+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: ~none~ }.
309+
1. Let _error_ be a new *SyntaxError* exception.
310+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
299311
1. Set _char_ to the substring of _string_ from _index_ to _index_ + 1.
300312
1. If _char_ is *"="*, then
301313
1. Set _index_ to SkipAsciiWhitespace(_string_, _index_ + 1).
302314
1. If _index_ < _length_, then
303-
1. Throw a *SyntaxError* exception.
315+
1. Let _error_ be a new *SyntaxError* exception.
316+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
304317
1. If _lastChunkHandling_ is *"strict"*, let _throwOnExtraBits_ be *true*.
305318
1. Else, let _throwOnExtraBits_ be *false*.
306-
1. Set _bytes_ to the list-concatenation of _bytes_ and ? DecodeBase64Chunk(_chunk_, _throwOnExtraBits_).
307-
1. Return the Record { [[Read]]: _length_, [[Bytes]]: _bytes_ }.
319+
1. Let _decodeResult_ be Completion(DecodeBase64Chunk(_chunk_, _throwOnExtraBits_)).
320+
1. If _decodeResult_ is an abrupt completion, then
321+
1. Let _error_ be _decodeResult_.[[Value]].
322+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
323+
1. Set _bytes_ to the list-concatenation of _bytes_ and ! _decodeResult_.
324+
1. Return the Record { [[Read]]: _length_, [[Bytes]]: _bytes_, [[Error]]: ~none~ }.
308325
1. If _alphabet_ is *"base64url"*, then
309-
1. If _char_ is either *"+"* or *"/"*, throw a *SyntaxError* exception.
310-
1. Else if _char_ is *"-"*, set _char_ to *"+"*.
311-
1. Else if _char_ is *"_"*, set _char_ to *"/"*.
312-
1. If the sole code unit of _char_ is not an element of the standard base64 alphabet, throw a *SyntaxError* exception.
326+
1. If _char_ is either *"+"* or *"/"*, then
327+
1. Let _error_ be a new *SyntaxError* exception.
328+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
329+
1. Else if _char_ is *"-"*, then
330+
1. Set _char_ to *"+"*.
331+
1. Else if _char_ is *"_"*, then
332+
1. Set _char_ to *"/"*.
333+
1. If the sole code unit of _char_ is not an element of the standard base64 alphabet, then
334+
1. Let _error_ be a new *SyntaxError* exception.
335+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
313336
1. Let _remaining_ be _maxLength_ - the length of _bytes_.
314337
1. If _remaining_ = 1 and _chunkLength_ = 2, or if _remaining_ = 2 and _chunkLength_ = 3, then
315-
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_ }.
338+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: ~none~ }.
316339
1. Set _chunk_ to the string-concatenation of _chunk_ and _char_.
317340
1. Set _chunkLength_ to the length of _chunk_.
318341
1. If _chunkLength_ = 4, then
@@ -321,7 +344,7 @@ <h1>
321344
1. Set _chunkLength_ to 0.
322345
1. Set _read_ to _index_.
323346
1. If the length of _bytes_ = _maxLength_, then
324-
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_ }.
347+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: ~none~ }.
325348
</emu-alg>
326349
</emu-clause>
327350

@@ -330,23 +353,27 @@ <h1>
330353
FromHex (
331354
_string_: a string,
332355
optional _maxLength_: a non-negative integer,
333-
): either a normal completion containing a Record with fields [[Read]] (an integral Number) and [[Bytes]] (a List of byte values), or a throw completion
356+
): a Record with fields [[Read]] (an integral Number), [[Bytes]] (a List of byte values), and [[Error]] (either ~none~ or a throw completion)
334357
</h1>
335358
<dl class="header">
336359
</dl>
337360
<emu-alg>
338361
1. If _maxLength_ is not present, let _maxLength_ be 2<sup>53</sup> - 1.
339362
1. Let _length_ be the length of _string_.
340-
1. If _length_ modulo 2 is not 0, throw a *SyntaxError* exception.
341363
1. Let _bytes_ be « ».
342-
1. Let _index_ be 0.
343-
1. Repeat, while _index_ &lt; _length_ and the length of _bytes_ &lt; _maxLength_,
344-
1. Let _hexits_ be the substring of _string_ from _index_ to _index_ + 2.
345-
1. If _hexits_ contains any code units which are not in *"0123456789abcdefABCDEF"*, throw a *SyntaxError* exception.
346-
1. Set _index_ to _index_ + 2.
364+
1. Let _read_ be 0.
365+
1. If _length_ modulo 2 is not 0, then
366+
1. Let _error_ be a new *SyntaxError* exception.
367+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
368+
1. Repeat, while _read_ &lt; _length_ and the length of _bytes_ &lt; _maxLength_,
369+
1. Let _hexits_ be the substring of _string_ from _read_ to _read_ + 2.
370+
1. If _hexits_ contains any code units which are not in *"0123456789abcdefABCDEF"*, then
371+
1. Let _error_ be a new *SyntaxError* exception.
372+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: _error_ }.
373+
1. Set _read_ to _read_ + 2.
347374
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.
348375
1. Append _byte_ to _bytes_.
349-
1. Return the Record { [[Read]]: _index_, [[Bytes]]: _bytes_ }.
376+
1. Return the Record { [[Read]]: _read_, [[Bytes]]: _bytes_, [[Error]]: ~none~ }.
350377
</emu-alg>
351378
</emu-clause>
352379

0 commit comments

Comments
 (0)