From 6c56c9722b25829a452b6e5217e31eaba16ae72a Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:52:45 -0500 Subject: [PATCH] buffer: introduce File PR-URL: https://github.com/nodejs/node/pull/45139 Fixes: https://github.com/nodejs/node/issues/39015 Reviewed-By: Yagiz Nizipli Reviewed-By: Minwoo Jung Reviewed-By: Antoine du Hamel --- benchmark/blob/file.js | 34 ++++ doc/api/buffer.md | 51 ++++++ lib/buffer.js | 5 + lib/internal/file.js | 113 +++++++++++++ .../wpt/FileAPI/file/File-constructor.any.js | 155 ++++++++++++++++++ .../file/send-file-formdata-controls.any.js | 69 ++++++++ .../send-file-formdata-punctuation.any.js | 144 ++++++++++++++++ .../file/send-file-formdata-utf-8.any.js | 33 ++++ .../FileAPI/file/send-file-formdata.any.js | 8 + test/fixtures/wpt/README.md | 1 + test/fixtures/wpt/versions.json | 4 + test/parallel/test-bootstrap-modules.js | 1 + test/parallel/test-file.js | 155 ++++++++++++++++++ test/wpt/status/FileAPI/file.json | 17 ++ test/wpt/test-file.js | 13 ++ tools/doc/type-parser.mjs | 1 + 16 files changed, 804 insertions(+) create mode 100644 benchmark/blob/file.js create mode 100644 lib/internal/file.js create mode 100644 test/fixtures/wpt/FileAPI/file/File-constructor.any.js create mode 100644 test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.any.js create mode 100644 test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.any.js create mode 100644 test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.any.js create mode 100644 test/fixtures/wpt/FileAPI/file/send-file-formdata.any.js create mode 100644 test/parallel/test-file.js create mode 100644 test/wpt/status/FileAPI/file.json create mode 100644 test/wpt/test-file.js diff --git a/benchmark/blob/file.js b/benchmark/blob/file.js new file mode 100644 index 00000000000000..42f866b1ad8ce8 --- /dev/null +++ b/benchmark/blob/file.js @@ -0,0 +1,34 @@ +'use strict'; +const common = require('../common.js'); +const { File } = require('buffer'); + +const bench = common.createBenchmark(main, { + bytes: [128, 1024, 1024 ** 2], + n: [1e6], + operation: ['text', 'arrayBuffer'] +}); + +const options = { + lastModified: Date.now() - 1e6, +}; + +async function run(n, bytes, operation) { + const buff = Buffer.allocUnsafe(bytes); + const source = new File(buff, 'dummy.txt', options); + bench.start(); + for (let i = 0; i < n; i++) { + switch (operation) { + case 'text': + await source.text(); + break; + case 'arrayBuffer': + await source.arrayBuffer(); + break; + } + } + bench.end(n); +} + +function main(conf) { + run(conf.n, conf.bytes, conf.operation).catch(console.log); +} diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 8b46553ec4941a..b0522f688f6ad7 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -5013,6 +5013,56 @@ changes: See [`Buffer.from(string[, encoding])`][`Buffer.from(string)`]. +## Class: `File` + + + +> Stability: 1 - Experimental + +* Extends: {Blob} + +A [`File`][] provides information about files. + +### `new buffer.File(sources, fileName[, options])` + + + +* `sources` {string\[]|ArrayBuffer\[]|TypedArray\[]|DataView\[]|Blob\[]|File\[]} + An array of string, {ArrayBuffer}, {TypedArray}, {DataView}, {File}, or {Blob} + objects, or any mix of such objects, that will be stored within the `File`. +* `fileName` {string} The name of the file. +* `options` {Object} + * `endings` {string} One of either `'transparent'` or `'native'`. When set + to `'native'`, line endings in string source parts will be converted to + the platform native line-ending as specified by `require('node:os').EOL`. + * `type` {string} The File content-type. + * `lastModified` {number} The last modified date of the file. + **Default:** `Date.now()`. + +### `file.name` + + + +* Type: {string} + +The name of the `File`. + +### `file.lastModified` + + + +* Type: {number} + +The last modified date of the `File`. + ## `node:buffer` module APIs While, the `Buffer` object is available as a global, there are additional @@ -5359,6 +5409,7 @@ introducing security vulnerabilities into an application. [`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value [`ERR_INVALID_BUFFER_SIZE`]: errors.md#err_invalid_buffer_size [`ERR_OUT_OF_RANGE`]: errors.md#err_out_of_range +[`File`]: https://developer.mozilla.org/en-US/docs/Web/API/File [`JSON.stringify()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify [`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer [`String.prototype.indexOf()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf diff --git a/lib/buffer.js b/lib/buffer.js index 7c0bbbc81c6398..898bc5032e5f8d 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -126,6 +126,10 @@ const { resolveObjectURL, } = require('internal/blob'); +const { + File, +} = require('internal/file'); + FastBuffer.prototype.constructor = Buffer; Buffer.prototype = FastBuffer.prototype; addBufferPrototypeMethods(Buffer.prototype); @@ -1320,6 +1324,7 @@ function atob(input) { module.exports = { Blob, + File, resolveObjectURL, Buffer, SlowBuffer, diff --git a/lib/internal/file.js b/lib/internal/file.js new file mode 100644 index 00000000000000..df9966532cdc24 --- /dev/null +++ b/lib/internal/file.js @@ -0,0 +1,113 @@ +'use strict'; + +const { + DateNow, + NumberIsNaN, + ObjectDefineProperties, + SymbolToStringTag, +} = primordials; + +const { + Blob, +} = require('internal/blob'); + +const { + customInspectSymbol: kInspect, + emitExperimentalWarning, + kEnumerableProperty, + kEmptyObject, + toUSVString, +} = require('internal/util'); + +const { + codes: { + ERR_INVALID_THIS, + ERR_MISSING_ARGS, + }, +} = require('internal/errors'); + +const { + inspect, +} = require('internal/util/inspect'); + +class File extends Blob { + /** @type {string} */ + #name; + + /** @type {number} */ + #lastModified; + + constructor(fileBits, fileName, options = kEmptyObject) { + emitExperimentalWarning('buffer.File'); + + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS('fileBits', 'fileName'); + } + + super(fileBits, options); + + let { lastModified } = options ?? kEmptyObject; + + if (lastModified !== undefined) { + // Using Number(...) will not throw an error for bigints. + lastModified = +lastModified; + + if (NumberIsNaN(lastModified)) { + lastModified = 0; + } + } else { + lastModified = DateNow(); + } + + this.#name = toUSVString(fileName); + this.#lastModified = lastModified; + } + + get name() { + if (!this || !(#name in this)) { + throw new ERR_INVALID_THIS('File'); + } + + return this.#name; + } + + get lastModified() { + if (!this || !(#name in this)) { + throw new ERR_INVALID_THIS('File'); + } + + return this.#lastModified; + } + + [kInspect](depth, options) { + if (depth < 0) { + return this; + } + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `File ${inspect({ + size: this.size, + type: this.type, + name: this.#name, + lastModified: this.#lastModified, + }, opts)}`; + } +} + +ObjectDefineProperties(File.prototype, { + name: kEnumerableProperty, + lastModified: kEnumerableProperty, + [SymbolToStringTag]: { + __proto__: null, + configurable: true, + value: 'File', + } +}); + +module.exports = { + File, +}; diff --git a/test/fixtures/wpt/FileAPI/file/File-constructor.any.js b/test/fixtures/wpt/FileAPI/file/File-constructor.any.js new file mode 100644 index 00000000000000..0b0185c40bf83c --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/File-constructor.any.js @@ -0,0 +1,155 @@ +// META: title=File constructor + +const to_string_obj = { toString: () => 'a string' }; +const to_string_throws = { toString: () => { throw new Error('expected'); } }; + +test(function() { + assert_true("File" in globalThis, "globalThis should have a File property."); +}, "File interface object exists"); + +test(t => { + assert_throws_js(TypeError, () => new File(), + 'Bits argument is required'); + assert_throws_js(TypeError, () => new File([]), + 'Name argument is required'); +}, 'Required arguments'); + +function test_first_argument(arg1, expectedSize, testName) { + test(function() { + var file = new File(arg1, "dummy"); + assert_true(file instanceof File); + assert_equals(file.name, "dummy"); + assert_equals(file.size, expectedSize); + assert_equals(file.type, ""); + // assert_false(file.isClosed); XXX: File.isClosed doesn't seem to be implemented + assert_not_equals(file.lastModified, ""); + }, testName); +} + +test_first_argument([], 0, "empty fileBits"); +test_first_argument(["bits"], 4, "DOMString fileBits"); +test_first_argument(["๐“ฝ๐“ฎ๐”๐“ฝ"], 16, "Unicode DOMString fileBits"); +test_first_argument([new String('string object')], 13, "String object fileBits"); +test_first_argument([new Blob()], 0, "Empty Blob fileBits"); +test_first_argument([new Blob(["bits"])], 4, "Blob fileBits"); +test_first_argument([new File([], 'world.txt')], 0, "Empty File fileBits"); +test_first_argument([new File(["bits"], 'world.txt')], 4, "File fileBits"); +test_first_argument([new ArrayBuffer(8)], 8, "ArrayBuffer fileBits"); +test_first_argument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4, "Typed array fileBits"); +test_first_argument(["bits", new Blob(["bits"]), new Blob(), new Uint8Array([0x50, 0x41]), + new Uint16Array([0x5353]), new Uint32Array([0x53534150])], 16, "Various fileBits"); +test_first_argument([12], 2, "Number in fileBits"); +test_first_argument([[1,2,3]], 5, "Array in fileBits"); +test_first_argument([{}], 15, "Object in fileBits"); // "[object Object]" +if (globalThis.document !== undefined) { + test_first_argument([document.body], 24, "HTMLBodyElement in fileBits"); // "[object HTMLBodyElement]" +} +test_first_argument([to_string_obj], 8, "Object with toString in fileBits"); +test_first_argument({[Symbol.iterator]() { + let i = 0; + return {next: () => [ + {done:false, value:'ab'}, + {done:false, value:'cde'}, + {done:true} + ][i++]}; +}}, 5, 'Custom @@iterator'); + +[ + 'hello', + 0, + null +].forEach(arg => { + test(t => { + assert_throws_js(TypeError, () => new File(arg, 'world.html'), + 'Constructor should throw for invalid bits argument'); + }, `Invalid bits argument: ${JSON.stringify(arg)}`); +}); + +test(t => { + assert_throws_js(Error, () => new File([to_string_throws], 'name.txt'), + 'Constructor should propagate exceptions'); +}, 'Bits argument: object that throws'); + + +function test_second_argument(arg2, expectedFileName, testName) { + test(function() { + var file = new File(["bits"], arg2); + assert_true(file instanceof File); + assert_equals(file.name, expectedFileName); + }, testName); +} + +test_second_argument("dummy", "dummy", "Using fileName"); +test_second_argument("dummy/foo", "dummy/foo", + "No replacement when using special character in fileName"); +test_second_argument(null, "null", "Using null fileName"); +test_second_argument(1, "1", "Using number fileName"); +test_second_argument('', '', "Using empty string fileName"); +if (globalThis.document !== undefined) { + test_second_argument(document.body, '[object HTMLBodyElement]', "Using object fileName"); +} + +// testing the third argument +[ + {type: 'text/plain', expected: 'text/plain'}, + {type: 'text/plain;charset=UTF-8', expected: 'text/plain;charset=utf-8'}, + {type: 'TEXT/PLAIN', expected: 'text/plain'}, + {type: '๐“ฝ๐“ฎ๐”๐“ฝ/๐”ญ๐”ฉ๐”ž๐”ฆ๐”ซ', expected: ''}, + {type: 'ascii/nonprintable\u001F', expected: ''}, + {type: 'ascii/nonprintable\u007F', expected: ''}, + {type: 'nonascii\u00EE', expected: ''}, + {type: 'nonascii\u1234', expected: ''}, + {type: 'nonparsable', expected: 'nonparsable'} +].forEach(testCase => { + test(t => { + var file = new File(["bits"], "dummy", { type: testCase.type}); + assert_true(file instanceof File); + assert_equals(file.type, testCase.expected); + }, `Using type in File constructor: ${testCase.type}`); +}); +test(function() { + var file = new File(["bits"], "dummy", { lastModified: 42 }); + assert_true(file instanceof File); + assert_equals(file.lastModified, 42); +}, "Using lastModified"); +test(function() { + var file = new File(["bits"], "dummy", { name: "foo" }); + assert_true(file instanceof File); + assert_equals(file.name, "dummy"); +}, "Misusing name"); +test(function() { + var file = new File(["bits"], "dummy", { unknownKey: "value" }); + assert_true(file instanceof File); + assert_equals(file.name, "dummy"); +}, "Unknown properties are ignored"); + +[ + 123, + 123.4, + true, + 'abc' +].forEach(arg => { + test(t => { + assert_throws_js(TypeError, () => new File(['bits'], 'name.txt', arg), + 'Constructor should throw for invalid property bag type'); + }, `Invalid property bag: ${JSON.stringify(arg)}`); +}); + +[ + null, + undefined, + [1,2,3], + /regex/, + function() {} +].forEach(arg => { + test(t => { + assert_equals(new File(['bits'], 'name.txt', arg).size, 4, + 'Constructor should accept object-ish property bag type'); + }, `Unusual but valid property bag: ${arg}`); +}); + +test(t => { + assert_throws_js(Error, + () => new File(['bits'], 'name.txt', {type: to_string_throws}), + 'Constructor should propagate exceptions'); +}, 'Property bag propagates exceptions'); diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.any.js b/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.any.js new file mode 100644 index 00000000000000..e95d3aada4421f --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.any.js @@ -0,0 +1,69 @@ +// META: title=FormData: FormData: Upload files named using controls +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-NUL-[\0].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-BS-[\b].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-VT-[\v].txt", + }); + + // These have characters that undergo processing in name=, + // filename=, and/or value; formDataPostFileUploadTest postprocesses + // expectedEncodedBaseName for these internally. + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LF-[\n].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LF-CR-[\n\r].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-CR-[\r].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-CR-LF-[\r\n].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-HT-[\t].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-FF-[\f].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-DEL-[\x7F].txt", + }); + + // The rest should be passed through unmodified: + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-ESC-[\x1B].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-SPACE-[ ].txt", + }); diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.any.js b/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.any.js new file mode 100644 index 00000000000000..987dba39aff3a1 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.any.js @@ -0,0 +1,144 @@ +// META: title=FormData: FormData: Upload files named using punctuation +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + // These have characters that undergo processing in name=, + // filename=, and/or value; formDataPostFileUploadTest postprocesses + // expectedEncodedBaseName for these internally. + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-QUOTATION-MARK-[\x22].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: '"file-for-upload-in-form-double-quoted.txt"', + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-REVERSE-SOLIDUS-[\\].txt", + }); + + // The rest should be passed through unmodified: + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-EXCLAMATION-MARK-[!].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-DOLLAR-SIGN-[$].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-PERCENT-SIGN-[%].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-AMPERSAND-[&].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-APOSTROPHE-['].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LEFT-PARENTHESIS-[(].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-RIGHT-PARENTHESIS-[)].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-ASTERISK-[*].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-PLUS-SIGN-[+].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-COMMA-[,].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-FULL-STOP-[.].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-SOLIDUS-[/].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-COLON-[:].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-SEMICOLON-[;].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-EQUALS-SIGN-[=].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-QUESTION-MARK-[?].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-CIRCUMFLEX-ACCENT-[^].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LEFT-SQUARE-BRACKET-[[].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-RIGHT-SQUARE-BRACKET-[]].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LEFT-CURLY-BRACKET-[{].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-VERTICAL-LINE-[|].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-RIGHT-CURLY-BRACKET-[}].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-TILDE-[~].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "'file-for-upload-in-form-single-quoted.txt'", + }); diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.any.js b/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.any.js new file mode 100644 index 00000000000000..b8bd74c717a1b3 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.any.js @@ -0,0 +1,33 @@ +// META: title=FormData: FormData: Upload files in UTF-8 fetch() +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "x-user-defined", + fileBaseName: "file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "windows-1252", + fileBaseName: "file-for-upload-in-form-รขหœยบรฐลธหœโ€š.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "JIS X 0201 and JIS X 0208", + fileBaseName: "file-for-upload-in-form-โ˜…ๆ˜Ÿโ˜….txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "Unicode", + fileBaseName: "file-for-upload-in-form-โ˜บ๐Ÿ˜‚.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "Unicode", + fileBaseName: `file-for-upload-in-form-${kTestChars}.txt`, + }); diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata.any.js b/test/fixtures/wpt/FileAPI/file/send-file-formdata.any.js new file mode 100644 index 00000000000000..e13a34828a0ebe --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata.any.js @@ -0,0 +1,8 @@ +// META: title=FormData: Upload ASCII-named file in UTF-8 form +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form.txt", + }); diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index b4b0e9c151f1a3..6e2ce815a454fd 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -17,6 +17,7 @@ Last update: - encoding: https://github.com/web-platform-tests/wpt/tree/c1b24fce6e/encoding - fetch/data-urls/resources: https://github.com/web-platform-tests/wpt/tree/7c79d998ff/fetch/data-urls/resources - FileAPI: https://github.com/web-platform-tests/wpt/tree/3b279420d4/FileAPI +- FileAPI/file: https://github.com/web-platform-tests/wpt/tree/c01f637cca/FileAPI/file - hr-time: https://github.com/web-platform-tests/wpt/tree/34cafd797e/hr-time - html/webappapis/atob: https://github.com/web-platform-tests/wpt/tree/f267e1dca6/html/webappapis/atob - html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/2c5c3c4c27/html/webappapis/microtask-queuing diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 374fce9d0dca89..2b6ae3401d3645 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -27,6 +27,10 @@ "commit": "3b279420d40afea32506e823f9ac005448f4f3d8", "path": "FileAPI" }, + "FileAPI/file": { + "commit": "c01f637cca43f0e08ce8e4269121dcd89ccbdd82", + "path": "FileAPI/file" + }, "hr-time": { "commit": "34cafd797e58dad280d20040eee012d49ccfa91f", "path": "hr-time" diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index c0698fe1a96b5c..53da8b1af1492d 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -165,6 +165,7 @@ const expectedModules = new Set([ 'NativeModule internal/worker/js_transferable', 'Internal Binding blob', 'NativeModule internal/blob', + 'NativeModule internal/file', 'NativeModule async_hooks', 'NativeModule net', 'NativeModule path', diff --git a/test/parallel/test-file.js b/test/parallel/test-file.js new file mode 100644 index 00000000000000..64a83f77ef919d --- /dev/null +++ b/test/parallel/test-file.js @@ -0,0 +1,155 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Blob, File } = require('buffer'); +const { inspect } = require('util'); + +{ + // ensure File extends Blob + assert.deepStrictEqual(Object.getPrototypeOf(File.prototype), Blob.prototype); +} + +{ + assert.throws(() => new File(), TypeError); + assert.throws(() => new File([]), TypeError); +} + +{ + const properties = ['name', 'lastModified']; + + for (const prop of properties) { + const desc = Object.getOwnPropertyDescriptor(File.prototype, prop); + assert.notStrictEqual(desc, undefined); + // Ensure these properties are getters. + assert.strictEqual(desc.get?.name, `get ${prop}`); + assert.strictEqual(desc.set, undefined); + assert.strictEqual(desc.enumerable, true); + assert.strictEqual(desc.configurable, true); + } +} + +{ + const file = new File([], ''); + assert.strictEqual(file[Symbol.toStringTag], 'File'); + assert.strictEqual(File.prototype[Symbol.toStringTag], 'File'); +} + +{ + assert.throws(() => File.prototype.name, TypeError); + assert.throws(() => File.prototype.lastModified, TypeError); +} + +{ + const keys = Object.keys(File.prototype).sort(); + assert.deepStrictEqual(keys, ['lastModified', 'name']); +} + +{ + const file = new File([], 'dummy.txt.exe'); + assert.strictEqual(file.name, 'dummy.txt.exe'); + assert.strictEqual(file.size, 0); + assert.strictEqual(typeof file.lastModified, 'number'); + assert(file.lastModified <= Date.now()); +} + +{ + const emptyFile = new File([], 'empty.txt'); + const blob = new Blob(['hello world']); + + emptyFile.text.call(blob).then(common.mustCall((text) => { + assert.strictEqual(text, 'hello world'); + })); +} + +{ + const toPrimitive = { + [Symbol.toPrimitive]() { + return 'NaN'; + } + }; + + const invalidLastModified = [ + null, + 'string', + false, + toPrimitive, + ]; + + for (const lastModified of invalidLastModified) { + const file = new File([], '', { lastModified }); + assert.strictEqual(file.lastModified, 0); + } +} + +{ + const file = new File([], '', { lastModified: undefined }); + assert.notStrictEqual(file.lastModified, 0); +} + +{ + const toPrimitive = { + [Symbol.toPrimitive]() { + throw new TypeError('boom'); + } + }; + + const throwValues = [ + BigInt(3n), + toPrimitive, + ]; + + for (const lastModified of throwValues) { + assert.throws(() => new File([], '', { lastModified }), TypeError); + } +} + +{ + const valid = [ + { + [Symbol.toPrimitive]() { + return 10; + } + }, + new Number(10), + 10, + ]; + + for (const lastModified of valid) { + assert.strictEqual(new File([], '', { lastModified }).lastModified, 10); + } +} + +{ + const file = new File([], ''); + assert(inspect(file).startsWith('File { size: 0, type: \'\', name: \'\', lastModified:')); +} + +{ + function MyClass() {} + MyClass.prototype.lastModified = 10; + + const file = new File([], '', new MyClass()); + assert.strictEqual(file.lastModified, 10); +} + +{ + let counter = 0; + new File([], '', { + get lastModified() { + counter++; + return 10; + } + }); + assert.strictEqual(counter, 1); +} + +{ + const getter = Object.getOwnPropertyDescriptor(File.prototype, 'name').get; + assert.throws( + () => getter.call(undefined), // eslint-disable-line no-useless-call + { + code: 'ERR_INVALID_THIS', + } + ); +} diff --git a/test/wpt/status/FileAPI/file.json b/test/wpt/status/FileAPI/file.json new file mode 100644 index 00000000000000..6b50bcec1539e3 --- /dev/null +++ b/test/wpt/status/FileAPI/file.json @@ -0,0 +1,17 @@ +{ + "Worker-read-file-constructor.worker.js": { + "skip": true + }, + "send-file-formdata-punctuation.any.js": { + "skip": true + }, + "send-file-formdata-utf-8.any.js": { + "skip": true + }, + "send-file-formdata.any.js": { + "skip": true + }, + "send-file-formdata-controls.any.js": { + "skip": true + } +} diff --git a/test/wpt/test-file.js b/test/wpt/test-file.js new file mode 100644 index 00000000000000..71e8c179de95e2 --- /dev/null +++ b/test/wpt/test-file.js @@ -0,0 +1,13 @@ +'use strict'; + +require('../common'); +const { WPTRunner } = require('../common/wpt'); + +const runner = new WPTRunner('FileAPI/file'); + +runner.setInitScript(` + const { File } = require('buffer'); + globalThis.File = File; +`); + +runner.runJsTests(); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 0053b62b31f33a..64d499d182f484 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -43,6 +43,7 @@ const customTypesMap = { `${jsDocPrefix}Reference/Global_Objects/WebAssembly/Instance`, 'Blob': 'buffer.html#class-blob', + 'File': 'buffer.html#class-file', 'BroadcastChannel': 'worker_threads.html#class-broadcastchannel-' +