Skip to content

Commit

Permalink
buffer: introduce File
Browse files Browse the repository at this point in the history
PR-URL: #45139
Fixes: #39015
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
KhafraDev authored and ruyadorno committed Nov 21, 2022
1 parent 7c6281a commit 6c56c97
Show file tree
Hide file tree
Showing 16 changed files with 804 additions and 0 deletions.
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.
**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);

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');

0 comments on commit 6c56c97

Please sign in to comment.