Skip to content

Commit 2cd5aac

Browse files
divybotlittledivy
andauthored
fix(ext/node): reject structuredClone for file-backed Blobs (#34075)
`parallel/test-blob-file-backed.js` in the Node compatibility suite expects `structuredClone(await fs.openAsBlob(path))` to throw a `DOMException`/`DataCloneError`. Deno currently implements `node:fs.openAsBlob()` by reading the file into an in-memory `Blob`, so the Blob is treated as serializable and `structuredClone()` succeeds. That diverges from Node behavior for file-backed Blobs. ## Summary - mark `node:fs.openAsBlob()` results as file-backed in the Blob internals - propagate that marker through Blob construction and slicing - reject structured clone serialization for marked Blobs so `structuredClone()` surfaces `DataCloneError` - add a node fs regression covering direct and sliced openAsBlob Blobs Co-authored-by: divybot <divybot@users.noreply.github.com> Co-authored-by: Divy Srivastava <me@littledivy.com>
1 parent 2aeca4a commit 2cd5aac

3 files changed

Lines changed: 60 additions & 1 deletion

File tree

ext/node/polyfills/fs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ const {
200200
const { isArrayBufferView } = core.loadExtScript(
201201
"ext:deno_node/internal/util/types.ts",
202202
);
203+
const { Blob, markFileBackedBlob } = core.loadExtScript(
204+
"ext:deno_web/09_file.js",
205+
);
203206
// Re-exported under both names for tests.
204207
const _toUnixTimestamp = toUnixTimestamp;
205208

@@ -2360,7 +2363,7 @@ function openAsBlob(
23602363
path = getValidatedPath(path);
23612364
return PromisePrototypeThen(
23622365
op_fs_read_file_async(path as string, undefined, 0),
2363-
(data: Uint8Array) => new Blob([data], { type }),
2366+
(data: Uint8Array) => markFileBackedBlob(new Blob([data], { type })),
23642367
);
23652368
}
23662369

ext/web/09_file.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,26 @@ function getParts(blob, bag = []) {
218218
const _type = Symbol("Type");
219219
const _size = Symbol("Size");
220220
const _parts = Symbol("Parts");
221+
const _fileBacked = Symbol("FileBacked");
222+
223+
/** @param {(BlobReference | Blob)[]} parts */
224+
function hasFileBackedPart(parts) {
225+
for (let i = 0; i < parts.length; ++i) {
226+
const part = parts[i];
227+
if (
228+
ObjectPrototypeIsPrototypeOf(BlobPrototype, part) && part[_fileBacked]
229+
) {
230+
return true;
231+
}
232+
}
233+
return false;
234+
}
221235

222236
class Blob {
223237
[_type] = "";
224238
[_size] = 0;
225239
[_parts];
240+
[_fileBacked] = false;
226241

227242
/**
228243
* @param {BlobPart[]} blobParts
@@ -251,6 +266,7 @@ class Blob {
251266
this[_parts] = parts;
252267
this[_size] = size;
253268
this[_type] = normalizeType(options.type);
269+
this[_fileBacked] = hasFileBackedPart(parts);
254270
}
255271

256272
/** @returns {number} */
@@ -360,6 +376,7 @@ class Blob {
360376
const blob = new Blob([], { type: relativeContentType });
361377
blob[_parts] = blobParts;
362378
blob[_size] = span;
379+
blob[_fileBacked] = this[_fileBacked];
363380
return blob;
364381
}
365382

@@ -684,6 +701,9 @@ function getPartRefs(blob, bag = []) {
684701
* @returns {{ uuid: string, size: number }[]}
685702
*/
686703
function cloneBlobParts(blob) {
704+
if (blob[_fileBacked]) {
705+
throw new TypeError("Invalid state: File-backed Blobs are not cloneable");
706+
}
687707
const refs = getPartRefs(blob);
688708
const cloned = [];
689709
for (let i = 0; i < refs.length; ++i) {
@@ -720,6 +740,18 @@ core.registerCloneableResource("Blob", (data) => {
720740
return blob;
721741
});
722742

743+
/**
744+
* Mark a Blob as backed by file storage. File-backed Blobs are intentionally
745+
* rejected by the structured clone serializer, matching Node's behavior.
746+
* @param {Blob} blob
747+
* @returns {Blob}
748+
*/
749+
function markFileBackedBlob(blob) {
750+
webidl.assertBranded(blob, BlobPrototype);
751+
blob[_fileBacked] = true;
752+
return blob;
753+
}
754+
723755
ObjectDefineProperty(File.prototype, core.hostObjectBrand, {
724756
__proto__: null,
725757
value: function () {
@@ -834,5 +866,6 @@ return {
834866
FilePrototype,
835867
getParts,
836868
isBlob,
869+
markFileBackedBlob,
837870
};
838871
})();

tests/unit_node/fs_test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,29 @@ Deno.test(
614614
},
615615
);
616616

617+
Deno.test(
618+
"[node/fs openAsBlob] structuredClone throws DataCloneError",
619+
async () => {
620+
const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
621+
writeFileSync(filename, "content");
622+
623+
const blob = await openAsBlob(filename);
624+
const err = assertThrows(
625+
() => structuredClone(blob),
626+
DOMException,
627+
"Invalid state: File-backed Blobs are not cloneable",
628+
);
629+
assertEquals(err.name, "DataCloneError");
630+
631+
const sliceErr = assertThrows(
632+
() => structuredClone(blob.slice(0, 1)),
633+
DOMException,
634+
"Invalid state: File-backed Blobs are not cloneable",
635+
);
636+
assertEquals(sliceErr.name, "DataCloneError");
637+
},
638+
);
639+
617640
Deno.test(
618641
"[node/fs openAsBlob] rejects for non-existent file",
619642
async () => {

0 commit comments

Comments
 (0)