From b50f85ceb251850ff290249f1904597750003059 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 15 Jul 2026 13:19:28 -0600 Subject: [PATCH] fix: serialize DataView subviews with correct byte offset and length A DataView subview (created with a byteOffset/byteLength) was serialized incorrectly in two code paths: - uneval used a nonexistent `startOffset` property, which is `undefined` and coerces to 0 on revival, silently placing the view at the wrong offset. - stringify reused the typed-array branch and emitted `.length`, which is `undefined` for a DataView, producing invalid JSON that fails to parse. Both now emit `byteOffset,byteLength`, matching the `new DataView(buffer, byteOffset, byteLength)` constructor. Adds regression fixtures for both an inline and a named (deduplicated) DataView subview. --- .changeset/dataview-subview.md | 5 +++++ src/stringify.js | 17 ++++++++++++++--- src/uneval.js | 2 +- test/index.test.js | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 .changeset/dataview-subview.md diff --git a/.changeset/dataview-subview.md b/.changeset/dataview-subview.md new file mode 100644 index 00000000..dfc1d3b0 --- /dev/null +++ b/.changeset/dataview-subview.md @@ -0,0 +1,5 @@ +--- +"devalue": patch +--- + +fix: serialize DataView subviews with the correct byte offset and length diff --git a/src/stringify.js b/src/stringify.js index 35dadffb..ce8b1150 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -289,15 +289,13 @@ function run(async, value, reducers) { case 'Float32Array': case 'Float64Array': case 'BigInt64Array': - case 'BigUint64Array': - case 'DataView': { + case 'BigUint64Array': { /** @type {import("./types.js").TypedArray} */ const typedArray = thing; str = '["' + type + '",' + flatten(typedArray.buffer); // handle subarrays if (typedArray.byteLength !== typedArray.buffer.byteLength) { - // to be used with `new TypedArray(buffer, byteOffset, length)` str += `,${typedArray.byteOffset},${typedArray.length}`; } @@ -305,6 +303,19 @@ function run(async, value, reducers) { break; } + case 'DataView': { + /** @type {DataView} */ + const view = thing; + str = '["' + type + '",' + flatten(view.buffer); + + if (view.byteLength !== view.buffer.byteLength) { + str += `,${view.byteOffset},${view.byteLength}`; + } + + str += ']'; + break; + } + case 'ArrayBuffer': { /** @type {ArrayBuffer} */ const arraybuffer = thing; diff --git a/src/uneval.js b/src/uneval.js index 5c1f39e3..af2afbb3 100644 --- a/src/uneval.js +++ b/src/uneval.js @@ -318,7 +318,7 @@ export function uneval(value, replacer) { // handle subviews if (thing.byteLength !== thing.buffer.byteLength) { - str += `,${thing.startOffset},${thing.byteLength}`; + str += `,${thing.byteOffset},${thing.byteLength}`; } return str + ')'; diff --git a/test/index.test.js b/test/index.test.js index a80125a8..52bac4e7 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -311,6 +311,12 @@ const fixtures = { js: 'new DataView(new Uint8Array([1,2,3]).buffer)', json: '[["DataView",1],["ArrayBuffer","AQID"]]' }, + { + name: 'DataView subview', + value: new DataView(new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).buffer, 2, 4), + js: 'new DataView(new Uint8Array([0,1,2,3,4,5,6,7,8,9]).buffer,2,4)', + json: '[["DataView",1,2,4],["ArrayBuffer","AAECAwQFBgcICQ=="]]' + }, { name: 'URL', value: new URL('https://user:password@example.com/