Skip to content

Commit e46f4c8

Browse files
authored
fix: handle repeated array buffers and subarrays (#105)
* handle repeated array buffers * handle subarrays * changeset
1 parent e28031b commit e46f4c8

File tree

5 files changed

+141
-75
lines changed

5 files changed

+141
-75
lines changed

.changeset/afraid-rats-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'devalue': patch
3+
---
4+
5+
fix: handle repeated array buffers and subarrays

src/parse.js

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,34 @@ export function unflatten(parsed, revivers) {
102102
}
103103
break;
104104

105-
case "Int8Array":
106-
case "Uint8Array":
107-
case "Uint8ClampedArray":
108-
case "Int16Array":
109-
case "Uint16Array":
110-
case "Int32Array":
111-
case "Uint32Array":
112-
case "Float32Array":
113-
case "Float64Array":
114-
case "BigInt64Array":
115-
case "BigUint64Array": {
116-
const TypedArrayConstructor = globalThis[type];
117-
const base64 = value[1];
118-
const arraybuffer = decode64(base64);
119-
const typedArray = new TypedArrayConstructor(arraybuffer);
120-
hydrated[index] = typedArray;
121-
break;
122-
}
123-
124-
case "ArrayBuffer": {
125-
const base64 = value[1];
126-
const arraybuffer = decode64(base64);
127-
hydrated[index] = arraybuffer;
128-
break;
129-
}
105+
case 'Int8Array':
106+
case 'Uint8Array':
107+
case 'Uint8ClampedArray':
108+
case 'Int16Array':
109+
case 'Uint16Array':
110+
case 'Int32Array':
111+
case 'Uint32Array':
112+
case 'Float32Array':
113+
case 'Float64Array':
114+
case 'BigInt64Array':
115+
case 'BigUint64Array': {
116+
const TypedArrayConstructor = globalThis[type];
117+
const typedArray = new TypedArrayConstructor(hydrate(value[1]));
118+
119+
hydrated[index] =
120+
value[2] !== undefined
121+
? typedArray.subarray(value[2], value[3])
122+
: typedArray;
123+
124+
break;
125+
}
126+
127+
case 'ArrayBuffer': {
128+
const base64 = value[1];
129+
const arraybuffer = decode64(base64);
130+
hydrated[index] = arraybuffer;
131+
break;
132+
}
130133

131134
default:
132135
throw new Error(`Unknown type ${type}`);

src/stringify.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -140,33 +140,43 @@ export function stringify(value, reducers) {
140140
str += ']';
141141
break;
142142

143-
case "Int8Array":
144-
case "Uint8Array":
145-
case "Uint8ClampedArray":
146-
case "Int16Array":
147-
case "Uint16Array":
148-
case "Int32Array":
149-
case "Uint32Array":
150-
case "Float32Array":
151-
case "Float64Array":
152-
case "BigInt64Array":
153-
case "BigUint64Array": {
143+
case 'Int8Array':
144+
case 'Uint8Array':
145+
case 'Uint8ClampedArray':
146+
case 'Int16Array':
147+
case 'Uint16Array':
148+
case 'Int32Array':
149+
case 'Uint32Array':
150+
case 'Float32Array':
151+
case 'Float64Array':
152+
case 'BigInt64Array':
153+
case 'BigUint64Array': {
154154
/** @type {import("./types.js").TypedArray} */
155155
const typedArray = thing;
156-
const base64 = encode64(typedArray.buffer);
157-
str = '["' + type + '","' + base64 + '"]';
156+
str = '["' + type + '",' + flatten(typedArray.buffer);
157+
158+
const a = thing.byteOffset;
159+
const b = a + thing.byteLength;
160+
161+
// handle subarrays
162+
if (a > 0 || b !== typedArray.buffer.byteLength) {
163+
const m = +/(\d+)/.exec(type)[1] / 8;
164+
str += `,${a / m},${b / m}`;
165+
}
166+
167+
str += ']';
158168
break;
159169
}
160-
161-
case "ArrayBuffer": {
170+
171+
case 'ArrayBuffer': {
162172
/** @type {ArrayBuffer} */
163173
const arraybuffer = thing;
164174
const base64 = encode64(arraybuffer);
165-
175+
166176
str = `["ArrayBuffer","${base64}"]`;
167177
break;
168178
}
169-
179+
170180
default:
171181
if (!is_plain_object(thing)) {
172182
throw new DevalueError(

src/uneval.js

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,22 @@ export function uneval(value, replacer) {
8282
keys.pop();
8383
}
8484
break;
85-
86-
case "Int8Array":
87-
case "Uint8Array":
88-
case "Uint8ClampedArray":
89-
case "Int16Array":
90-
case "Uint16Array":
91-
case "Int32Array":
92-
case "Uint32Array":
93-
case "Float32Array":
94-
case "Float64Array":
95-
case "BigInt64Array":
96-
case "BigUint64Array":
85+
86+
case 'Int8Array':
87+
case 'Uint8Array':
88+
case 'Uint8ClampedArray':
89+
case 'Int16Array':
90+
case 'Uint16Array':
91+
case 'Int32Array':
92+
case 'Uint32Array':
93+
case 'Float32Array':
94+
case 'Float64Array':
95+
case 'BigInt64Array':
96+
case 'BigUint64Array':
97+
walk(thing.buffer);
9798
return;
98-
99-
case "ArrayBuffer":
99+
100+
case 'ArrayBuffer':
100101
return;
101102

102103
default:
@@ -177,24 +178,40 @@ export function uneval(value, replacer) {
177178
case 'Set':
178179
case 'Map':
179180
return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`;
180-
181-
case "Int8Array":
182-
case "Uint8Array":
183-
case "Uint8ClampedArray":
184-
case "Int16Array":
185-
case "Uint16Array":
186-
case "Int32Array":
187-
case "Uint32Array":
188-
case "Float32Array":
189-
case "Float64Array":
190-
case "BigInt64Array":
191-
case "BigUint64Array": {
192-
/** @type {import("./types.js").TypedArray} */
193-
const typedArray = thing;
194-
return `new ${type}([${typedArray.toString()}])`;
181+
182+
case 'Int8Array':
183+
case 'Uint8Array':
184+
case 'Uint8ClampedArray':
185+
case 'Int16Array':
186+
case 'Uint16Array':
187+
case 'Int32Array':
188+
case 'Uint32Array':
189+
case 'Float32Array':
190+
case 'Float64Array':
191+
case 'BigInt64Array':
192+
case 'BigUint64Array': {
193+
let str = `new ${type}`;
194+
195+
if (counts.get(thing.buffer) === 1) {
196+
const array = new thing.constructor(thing.buffer);
197+
str += `([${array}])`;
198+
} else {
199+
str += `([${stringify(thing.buffer)}])`;
200+
}
201+
202+
const a = thing.byteOffset;
203+
const b = a + thing.byteLength;
204+
205+
// handle subarrays
206+
if (a > 0 || b !== thing.buffer.byteLength) {
207+
const m = +/(\d+)/.exec(type)[1] / 8;
208+
str += `.subarray(${a / m},${b / m})`;
209+
}
210+
211+
return str;
195212
}
196-
197-
case "ArrayBuffer": {
213+
214+
case 'ArrayBuffer': {
198215
const ui8 = new Uint8Array(thing);
199216
return `new Uint8Array([${ui8.toString()}]).buffer`;
200217
}
@@ -281,6 +298,12 @@ export function uneval(value, replacer) {
281298
);
282299
break;
283300

301+
case 'ArrayBuffer':
302+
values.push(
303+
`new Uint8Array([${new Uint8Array(thing).join(',')}]).buffer`
304+
);
305+
break;
306+
284307
default:
285308
values.push(
286309
Object.getPrototypeOf(thing) === null ? 'Object.create(null)' : '{}'

test/test.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,19 @@ const fixtures = {
170170
name: 'Uint8Array',
171171
value: new Uint8Array([1, 2, 3]),
172172
js: 'new Uint8Array([1,2,3])',
173-
json: '[["Uint8Array","AQID"]]'
173+
json: '[["Uint8Array",1],["ArrayBuffer","AQID"]]'
174174
},
175175
{
176176
name: 'ArrayBuffer',
177177
value: new Uint8Array([1, 2, 3]).buffer,
178178
js: 'new Uint8Array([1,2,3]).buffer',
179179
json: '[["ArrayBuffer","AQID"]]'
180+
},
181+
{
182+
name: 'Sliced typed array',
183+
value: new Uint16Array([10, 20, 30, 40]).subarray(1, 3),
184+
js: 'new Uint16Array([10,20,30,40]).subarray(1,3)',
185+
json: '[["Uint16Array",1,1,3],["ArrayBuffer","CgAUAB4AKAA="]]'
180186
}
181187
],
182188

@@ -378,7 +384,26 @@ const fixtures = {
378384
js: '(function(a){return [a,a]}({}))',
379385
json: '[[1,1],{}]'
380386
};
381-
})({})
387+
})({}),
388+
389+
{
390+
name: 'Array buffer (repetition)',
391+
value: (() => {
392+
const uint8 = new Uint8Array(10);
393+
const uint16 = new Uint16Array(uint8.buffer);
394+
395+
for (let i = 0; i < uint8.length; i += 1) {
396+
uint8[i] = i;
397+
}
398+
399+
return [uint8, uint16];
400+
})(),
401+
js: '(function(a){return [new Uint8Array([a]),new Uint16Array([a])]}(new Uint8Array([0,1,2,3,4,5,6,7,8,9]).buffer))',
402+
json: '[[1,3],["Uint8Array",2],["ArrayBuffer","AAECAwQFBgcICQ=="],["Uint16Array",2]]',
403+
validate: ([uint8, uint16]) => {
404+
return uint8.buffer === uint16.buffer;
405+
}
406+
}
382407
],
383408

384409
XSS: [

0 commit comments

Comments
 (0)