Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

buffer: introduce File #45139

Merged
merged 15 commits into from Nov 10, 2022
34 changes: 34 additions & 0 deletions 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);
}
51 changes: 51 additions & 0 deletions doc/api/buffer.md
Expand Up @@ -5013,6 +5013,56 @@ changes:

See [`Buffer.from(string[, encoding])`][`Buffer.from(string)`].

## Class: `File`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

* Extends: {Blob}

A [`File`][] provides information about files.

### `new buffer.File(sources, fileName[, options])`

<!-- YAML
added: REPLACEME
-->

* `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.
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
**Default:** `Date.now()`.

### `file.name`

<!-- YAML
added: REPLACEME
-->

* Type: {string}

The name of the `File`.

### `file.lastModified`

<!-- YAML
added: REPLACEME
-->

* 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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/buffer.js
Expand Up @@ -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);
Expand Down Expand Up @@ -1320,6 +1324,7 @@ function atob(input) {

module.exports = {
Blob,
File,
resolveObjectURL,
Buffer,
SlowBuffer,
Expand Down
113 changes: 113 additions & 0 deletions 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);
anonrig marked this conversation as resolved.
Show resolved Hide resolved

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,
};
155 changes: 155 additions & 0 deletions 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');