From b6b9262fdf3afad22fab0c6da8e224cbd8e84dae Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 18 Apr 2023 21:14:02 -0400 Subject: [PATCH 1/5] buffer: make File cloneable Fixes: https://github.com/nodejs/node/issues/47612 --- lib/internal/file.js | 64 +++++++++++++++++++++++++++++++------- test/parallel/test-file.js | 16 ++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/lib/internal/file.js b/lib/internal/file.js index e35eb7bf70931f..1bff962e918c47 100644 --- a/lib/internal/file.js +++ b/lib/internal/file.js @@ -4,7 +4,9 @@ const { DateNow, NumberIsNaN, ObjectDefineProperties, + ReflectConstruct, StringPrototypeToWellFormed, + Symbol, SymbolToStringTag, } = primordials; @@ -21,6 +23,7 @@ const { const { codes: { ERR_MISSING_ARGS, + ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -28,12 +31,20 @@ const { inspect, } = require('internal/util/inspect'); -class File extends Blob { - /** @type {string} */ - #name; +const { + makeTransferable, + kClone, + kDeserialize, +} = require('internal/worker/js_transferable'); - /** @type {number} */ - #lastModified; +const kState = Symbol('state'); + +function isFile(object) { + return object?.[kState] !== undefined; +} + +class File extends Blob { + [kState] = { __proto__: null }; constructor(fileBits, fileName, options = kEmptyObject) { if (arguments.length < 2) { @@ -55,16 +66,24 @@ class File extends Blob { lastModified = DateNow(); } - this.#name = StringPrototypeToWellFormed(`${fileName}`); - this.#lastModified = lastModified; + this[kState].name = StringPrototypeToWellFormed(`${fileName}`); + this[kState].lastModified = lastModified; + + makeTransferable(this); } get name() { - return this.#name; + if (!isFile(this)) + throw new ERR_INVALID_THIS('File'); + + return this[kState].name; } get lastModified() { - return this.#lastModified; + if (!isFile(this)) + throw new ERR_INVALID_THIS('File'); + + return this[kState].lastModified; } [kInspect](depth, options) { @@ -80,11 +99,33 @@ class File extends Blob { return `File ${inspect({ size: this.size, type: this.type, - name: this.#name, - lastModified: this.#lastModified, + name: this[kState].name, + lastModified: this[kState].lastModified, }, opts)}`; } + + [kClone]() { + return { + data: { ...super[kClone]().data, ...this[kState] }, + deserializeInfo: 'internal/file:ClonedFile', + }; + } + + [kDeserialize](data) { + super[kDeserialize](data); + + this[kState] = { + __proto__: null, + name: data.name, + lastModified: data.lastModified, + }; + } +} + +function ClonedFile() { + return makeTransferable(ReflectConstruct(function() {}, [], File)); } +ClonedFile.prototype[kDeserialize] = () => {}; ObjectDefineProperties(File.prototype, { name: kEnumerableProperty, @@ -98,4 +139,5 @@ ObjectDefineProperties(File.prototype, { module.exports = { File, + ClonedFile, }; diff --git a/test/parallel/test-file.js b/test/parallel/test-file.js index bfc4548421be23..10d8dcba4158cc 100644 --- a/test/parallel/test-file.js +++ b/test/parallel/test-file.js @@ -158,3 +158,19 @@ const { inspect } = require('util'); ); }); } + +(async () => { + // File should be cloneable via structuredClone. + // Refs: https://github.com/nodejs/node/issues/47612 + + const body = ['hello, ', 'world']; + const lastModified = Date.now() - 10_000; + const name = 'hello_world.txt'; + + const file = new File(body, name, { lastModified }); + const clonedFile = structuredClone(file); + + assert.deepStrictEqual(await file.text(), await clonedFile.text()); + assert.deepStrictEqual(file.lastModified, clonedFile.lastModified); + assert.deepStrictEqual(file.name, clonedFile.name); +})().then(common.mustCall()); From 7a0a20bf853966fe404666736257f7f86810cc95 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 18 Apr 2023 23:51:13 -0400 Subject: [PATCH 2/5] test: mark test as passing --- test/wpt/status/html/webappapis/structured-clone.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/wpt/status/html/webappapis/structured-clone.json b/test/wpt/status/html/webappapis/structured-clone.json index 873f2f9b46eb03..0967ef424bce67 100644 --- a/test/wpt/status/html/webappapis/structured-clone.json +++ b/test/wpt/status/html/webappapis/structured-clone.json @@ -1,7 +1 @@ -{ - "structured-clone.any.js": { - "fail": { - "expected": ["File basic"] - } - } -} +{} From ee9d3527b99e1d2287ff4f5536061745d773d249 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 22 Apr 2023 09:24:08 -0400 Subject: [PATCH 3/5] test,doc: apply suggestions --- doc/api/buffer.md | 3 +++ test/parallel/test-file.js | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/api/buffer.md b/doc/api/buffer.md index fbbcc04083ef51..bc16808f1e1105 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -5094,6 +5094,9 @@ added: - v19.2.0 - v18.13.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/47613 + description: Makes File instances cloneable. - version: v20.0.0 pr-url: https://github.com/nodejs/node/pull/47153 description: No longer experimental. diff --git a/test/parallel/test-file.js b/test/parallel/test-file.js index 10d8dcba4158cc..5f0cd2f4a035f8 100644 --- a/test/parallel/test-file.js +++ b/test/parallel/test-file.js @@ -170,7 +170,13 @@ const { inspect } = require('util'); const file = new File(body, name, { lastModified }); const clonedFile = structuredClone(file); - assert.deepStrictEqual(await file.text(), await clonedFile.text()); - assert.deepStrictEqual(file.lastModified, clonedFile.lastModified); - assert.deepStrictEqual(file.name, clonedFile.name); + assert.deepStrictEqual(await clonedFile.text(), await file.text()); + assert.deepStrictEqual(clonedFile.lastModified, file.lastModified); + assert.deepStrictEqual(clonedFile.name, file.name); + + const clonedFile2 = structuredClone(clonedFile); + + assert.deepStrictEqual(await clonedFile2.text(), await clonedFile.text()); + assert.deepStrictEqual(clonedFile2.lastModified, clonedFile.lastModified); + assert.deepStrictEqual(clonedFile2.name, clonedFile.name); })().then(common.mustCall()); From ff96083ed00c26089ff3a383f29cb2df1fa6a0e7 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 25 Sep 2024 15:07:28 -0400 Subject: [PATCH 4/5] fixup --- lib/internal/blob.js | 1 + lib/internal/file.js | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 4ff2b0e1e7051b..81dd627a1754ae 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -497,4 +497,5 @@ module.exports = { isBlob, kHandle, resolveObjectURL, + TransferableBlob, }; diff --git a/lib/internal/file.js b/lib/internal/file.js index 1bff962e918c47..beb39b43cc4ae9 100644 --- a/lib/internal/file.js +++ b/lib/internal/file.js @@ -2,9 +2,10 @@ const { DateNow, + FunctionPrototypeApply, NumberIsNaN, ObjectDefineProperties, - ReflectConstruct, + ObjectSetPrototypeOf, StringPrototypeToWellFormed, Symbol, SymbolToStringTag, @@ -12,6 +13,7 @@ const { const { Blob, + TransferableBlob, } = require('internal/blob'); const { @@ -22,8 +24,8 @@ const { const { codes: { - ERR_MISSING_ARGS, ERR_INVALID_THIS, + ERR_MISSING_ARGS, }, } = require('internal/errors'); @@ -32,7 +34,6 @@ const { } = require('internal/util/inspect'); const { - makeTransferable, kClone, kDeserialize, } = require('internal/worker/js_transferable'); @@ -68,8 +69,6 @@ class File extends Blob { this[kState].name = StringPrototypeToWellFormed(`${fileName}`); this[kState].lastModified = lastModified; - - makeTransferable(this); } get name() { @@ -107,7 +106,7 @@ class File extends Blob { [kClone]() { return { data: { ...super[kClone]().data, ...this[kState] }, - deserializeInfo: 'internal/file:ClonedFile', + deserializeInfo: 'internal/file:TransferableFile', }; } @@ -122,10 +121,12 @@ class File extends Blob { } } -function ClonedFile() { - return makeTransferable(ReflectConstruct(function() {}, [], File)); +function TransferableFile(handle, length, type = '') { + FunctionPrototypeApply(TransferableBlob, this, [handle, length, type]); } -ClonedFile.prototype[kDeserialize] = () => {}; + +ObjectSetPrototypeOf(TransferableFile.prototype, File.prototype); +ObjectSetPrototypeOf(TransferableFile, File); ObjectDefineProperties(File.prototype, { name: kEnumerableProperty, @@ -139,5 +140,5 @@ ObjectDefineProperties(File.prototype, { module.exports = { File, - ClonedFile, + TransferableFile, }; From 00fd45901175eee3af82c0a248fa3fd479b369c2 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 25 Sep 2024 15:27:14 -0400 Subject: [PATCH 5/5] fixup! use FileState class --- lib/internal/file.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/internal/file.js b/lib/internal/file.js index beb39b43cc4ae9..65fec6dfd0c627 100644 --- a/lib/internal/file.js +++ b/lib/internal/file.js @@ -44,9 +44,21 @@ function isFile(object) { return object?.[kState] !== undefined; } -class File extends Blob { - [kState] = { __proto__: null }; +class FileState { + name; + lastModified; + + /** + * @param {string} name + * @param {number} lastModified + */ + constructor(name, lastModified) { + this.name = name; + this.lastModified = lastModified; + } +} +class File extends Blob { constructor(fileBits, fileName, options = kEmptyObject) { if (arguments.length < 2) { throw new ERR_MISSING_ARGS('fileBits', 'fileName'); @@ -67,8 +79,7 @@ class File extends Blob { lastModified = DateNow(); } - this[kState].name = StringPrototypeToWellFormed(`${fileName}`); - this[kState].lastModified = lastModified; + this[kState] = new FileState(StringPrototypeToWellFormed(`${fileName}`), lastModified); } get name() { @@ -113,11 +124,7 @@ class File extends Blob { [kDeserialize](data) { super[kDeserialize](data); - this[kState] = { - __proto__: null, - name: data.name, - lastModified: data.lastModified, - }; + this[kState] = new FileState(data.name, data.lastModified); } }