Skip to content

Commit 6c56c97

Browse files
KhafraDevruyadorno
authored andcommitted
buffer: introduce File
PR-URL: #45139 Fixes: #39015 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Minwoo Jung <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 7c6281a commit 6c56c97

16 files changed

+804
-0
lines changed

benchmark/blob/file.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
const common = require('../common.js');
3+
const { File } = require('buffer');
4+
5+
const bench = common.createBenchmark(main, {
6+
bytes: [128, 1024, 1024 ** 2],
7+
n: [1e6],
8+
operation: ['text', 'arrayBuffer']
9+
});
10+
11+
const options = {
12+
lastModified: Date.now() - 1e6,
13+
};
14+
15+
async function run(n, bytes, operation) {
16+
const buff = Buffer.allocUnsafe(bytes);
17+
const source = new File(buff, 'dummy.txt', options);
18+
bench.start();
19+
for (let i = 0; i < n; i++) {
20+
switch (operation) {
21+
case 'text':
22+
await source.text();
23+
break;
24+
case 'arrayBuffer':
25+
await source.arrayBuffer();
26+
break;
27+
}
28+
}
29+
bench.end(n);
30+
}
31+
32+
function main(conf) {
33+
run(conf.n, conf.bytes, conf.operation).catch(console.log);
34+
}

doc/api/buffer.md

+51
Original file line numberDiff line numberDiff line change
@@ -5013,6 +5013,56 @@ changes:
50135013

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

5016+
## Class: `File`
5017+
5018+
<!-- YAML
5019+
added: REPLACEME
5020+
-->
5021+
5022+
> Stability: 1 - Experimental
5023+
5024+
* Extends: {Blob}
5025+
5026+
A [`File`][] provides information about files.
5027+
5028+
### `new buffer.File(sources, fileName[, options])`
5029+
5030+
<!-- YAML
5031+
added: REPLACEME
5032+
-->
5033+
5034+
* `sources` {string\[]|ArrayBuffer\[]|TypedArray\[]|DataView\[]|Blob\[]|File\[]}
5035+
An array of string, {ArrayBuffer}, {TypedArray}, {DataView}, {File}, or {Blob}
5036+
objects, or any mix of such objects, that will be stored within the `File`.
5037+
* `fileName` {string} The name of the file.
5038+
* `options` {Object}
5039+
* `endings` {string} One of either `'transparent'` or `'native'`. When set
5040+
to `'native'`, line endings in string source parts will be converted to
5041+
the platform native line-ending as specified by `require('node:os').EOL`.
5042+
* `type` {string} The File content-type.
5043+
* `lastModified` {number} The last modified date of the file.
5044+
**Default:** `Date.now()`.
5045+
5046+
### `file.name`
5047+
5048+
<!-- YAML
5049+
added: REPLACEME
5050+
-->
5051+
5052+
* Type: {string}
5053+
5054+
The name of the `File`.
5055+
5056+
### `file.lastModified`
5057+
5058+
<!-- YAML
5059+
added: REPLACEME
5060+
-->
5061+
5062+
* Type: {number}
5063+
5064+
The last modified date of the `File`.
5065+
50165066
## `node:buffer` module APIs
50175067

50185068
While, the `Buffer` object is available as a global, there are additional
@@ -5359,6 +5409,7 @@ introducing security vulnerabilities into an application.
53595409
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
53605410
[`ERR_INVALID_BUFFER_SIZE`]: errors.md#err_invalid_buffer_size
53615411
[`ERR_OUT_OF_RANGE`]: errors.md#err_out_of_range
5412+
[`File`]: https://developer.mozilla.org/en-US/docs/Web/API/File
53625413
[`JSON.stringify()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
53635414
[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
53645415
[`String.prototype.indexOf()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf

lib/buffer.js

+5
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ const {
126126
resolveObjectURL,
127127
} = require('internal/blob');
128128

129+
const {
130+
File,
131+
} = require('internal/file');
132+
129133
FastBuffer.prototype.constructor = Buffer;
130134
Buffer.prototype = FastBuffer.prototype;
131135
addBufferPrototypeMethods(Buffer.prototype);
@@ -1320,6 +1324,7 @@ function atob(input) {
13201324

13211325
module.exports = {
13221326
Blob,
1327+
File,
13231328
resolveObjectURL,
13241329
Buffer,
13251330
SlowBuffer,

lib/internal/file.js

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
'use strict';
2+
3+
const {
4+
DateNow,
5+
NumberIsNaN,
6+
ObjectDefineProperties,
7+
SymbolToStringTag,
8+
} = primordials;
9+
10+
const {
11+
Blob,
12+
} = require('internal/blob');
13+
14+
const {
15+
customInspectSymbol: kInspect,
16+
emitExperimentalWarning,
17+
kEnumerableProperty,
18+
kEmptyObject,
19+
toUSVString,
20+
} = require('internal/util');
21+
22+
const {
23+
codes: {
24+
ERR_INVALID_THIS,
25+
ERR_MISSING_ARGS,
26+
},
27+
} = require('internal/errors');
28+
29+
const {
30+
inspect,
31+
} = require('internal/util/inspect');
32+
33+
class File extends Blob {
34+
/** @type {string} */
35+
#name;
36+
37+
/** @type {number} */
38+
#lastModified;
39+
40+
constructor(fileBits, fileName, options = kEmptyObject) {
41+
emitExperimentalWarning('buffer.File');
42+
43+
if (arguments.length < 2) {
44+
throw new ERR_MISSING_ARGS('fileBits', 'fileName');
45+
}
46+
47+
super(fileBits, options);
48+
49+
let { lastModified } = options ?? kEmptyObject;
50+
51+
if (lastModified !== undefined) {
52+
// Using Number(...) will not throw an error for bigints.
53+
lastModified = +lastModified;
54+
55+
if (NumberIsNaN(lastModified)) {
56+
lastModified = 0;
57+
}
58+
} else {
59+
lastModified = DateNow();
60+
}
61+
62+
this.#name = toUSVString(fileName);
63+
this.#lastModified = lastModified;
64+
}
65+
66+
get name() {
67+
if (!this || !(#name in this)) {
68+
throw new ERR_INVALID_THIS('File');
69+
}
70+
71+
return this.#name;
72+
}
73+
74+
get lastModified() {
75+
if (!this || !(#name in this)) {
76+
throw new ERR_INVALID_THIS('File');
77+
}
78+
79+
return this.#lastModified;
80+
}
81+
82+
[kInspect](depth, options) {
83+
if (depth < 0) {
84+
return this;
85+
}
86+
87+
const opts = {
88+
...options,
89+
depth: options.depth == null ? null : options.depth - 1,
90+
};
91+
92+
return `File ${inspect({
93+
size: this.size,
94+
type: this.type,
95+
name: this.#name,
96+
lastModified: this.#lastModified,
97+
}, opts)}`;
98+
}
99+
}
100+
101+
ObjectDefineProperties(File.prototype, {
102+
name: kEnumerableProperty,
103+
lastModified: kEnumerableProperty,
104+
[SymbolToStringTag]: {
105+
__proto__: null,
106+
configurable: true,
107+
value: 'File',
108+
}
109+
});
110+
111+
module.exports = {
112+
File,
113+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// META: title=File constructor
2+
3+
const to_string_obj = { toString: () => 'a string' };
4+
const to_string_throws = { toString: () => { throw new Error('expected'); } };
5+
6+
test(function() {
7+
assert_true("File" in globalThis, "globalThis should have a File property.");
8+
}, "File interface object exists");
9+
10+
test(t => {
11+
assert_throws_js(TypeError, () => new File(),
12+
'Bits argument is required');
13+
assert_throws_js(TypeError, () => new File([]),
14+
'Name argument is required');
15+
}, 'Required arguments');
16+
17+
function test_first_argument(arg1, expectedSize, testName) {
18+
test(function() {
19+
var file = new File(arg1, "dummy");
20+
assert_true(file instanceof File);
21+
assert_equals(file.name, "dummy");
22+
assert_equals(file.size, expectedSize);
23+
assert_equals(file.type, "");
24+
// assert_false(file.isClosed); XXX: File.isClosed doesn't seem to be implemented
25+
assert_not_equals(file.lastModified, "");
26+
}, testName);
27+
}
28+
29+
test_first_argument([], 0, "empty fileBits");
30+
test_first_argument(["bits"], 4, "DOMString fileBits");
31+
test_first_argument(["𝓽𝓮𝔁𝓽"], 16, "Unicode DOMString fileBits");
32+
test_first_argument([new String('string object')], 13, "String object fileBits");
33+
test_first_argument([new Blob()], 0, "Empty Blob fileBits");
34+
test_first_argument([new Blob(["bits"])], 4, "Blob fileBits");
35+
test_first_argument([new File([], 'world.txt')], 0, "Empty File fileBits");
36+
test_first_argument([new File(["bits"], 'world.txt')], 4, "File fileBits");
37+
test_first_argument([new ArrayBuffer(8)], 8, "ArrayBuffer fileBits");
38+
test_first_argument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4, "Typed array fileBits");
39+
test_first_argument(["bits", new Blob(["bits"]), new Blob(), new Uint8Array([0x50, 0x41]),
40+
new Uint16Array([0x5353]), new Uint32Array([0x53534150])], 16, "Various fileBits");
41+
test_first_argument([12], 2, "Number in fileBits");
42+
test_first_argument([[1,2,3]], 5, "Array in fileBits");
43+
test_first_argument([{}], 15, "Object in fileBits"); // "[object Object]"
44+
if (globalThis.document !== undefined) {
45+
test_first_argument([document.body], 24, "HTMLBodyElement in fileBits"); // "[object HTMLBodyElement]"
46+
}
47+
test_first_argument([to_string_obj], 8, "Object with toString in fileBits");
48+
test_first_argument({[Symbol.iterator]() {
49+
let i = 0;
50+
return {next: () => [
51+
{done:false, value:'ab'},
52+
{done:false, value:'cde'},
53+
{done:true}
54+
][i++]};
55+
}}, 5, 'Custom @@iterator');
56+
57+
[
58+
'hello',
59+
0,
60+
null
61+
].forEach(arg => {
62+
test(t => {
63+
assert_throws_js(TypeError, () => new File(arg, 'world.html'),
64+
'Constructor should throw for invalid bits argument');
65+
}, `Invalid bits argument: ${JSON.stringify(arg)}`);
66+
});
67+
68+
test(t => {
69+
assert_throws_js(Error, () => new File([to_string_throws], 'name.txt'),
70+
'Constructor should propagate exceptions');
71+
}, 'Bits argument: object that throws');
72+
73+
74+
function test_second_argument(arg2, expectedFileName, testName) {
75+
test(function() {
76+
var file = new File(["bits"], arg2);
77+
assert_true(file instanceof File);
78+
assert_equals(file.name, expectedFileName);
79+
}, testName);
80+
}
81+
82+
test_second_argument("dummy", "dummy", "Using fileName");
83+
test_second_argument("dummy/foo", "dummy/foo",
84+
"No replacement when using special character in fileName");
85+
test_second_argument(null, "null", "Using null fileName");
86+
test_second_argument(1, "1", "Using number fileName");
87+
test_second_argument('', '', "Using empty string fileName");
88+
if (globalThis.document !== undefined) {
89+
test_second_argument(document.body, '[object HTMLBodyElement]', "Using object fileName");
90+
}
91+
92+
// testing the third argument
93+
[
94+
{type: 'text/plain', expected: 'text/plain'},
95+
{type: 'text/plain;charset=UTF-8', expected: 'text/plain;charset=utf-8'},
96+
{type: 'TEXT/PLAIN', expected: 'text/plain'},
97+
{type: '𝓽𝓮𝔁𝓽/𝔭𝔩𝔞𝔦𝔫', expected: ''},
98+
{type: 'ascii/nonprintable\u001F', expected: ''},
99+
{type: 'ascii/nonprintable\u007F', expected: ''},
100+
{type: 'nonascii\u00EE', expected: ''},
101+
{type: 'nonascii\u1234', expected: ''},
102+
{type: 'nonparsable', expected: 'nonparsable'}
103+
].forEach(testCase => {
104+
test(t => {
105+
var file = new File(["bits"], "dummy", { type: testCase.type});
106+
assert_true(file instanceof File);
107+
assert_equals(file.type, testCase.expected);
108+
}, `Using type in File constructor: ${testCase.type}`);
109+
});
110+
test(function() {
111+
var file = new File(["bits"], "dummy", { lastModified: 42 });
112+
assert_true(file instanceof File);
113+
assert_equals(file.lastModified, 42);
114+
}, "Using lastModified");
115+
test(function() {
116+
var file = new File(["bits"], "dummy", { name: "foo" });
117+
assert_true(file instanceof File);
118+
assert_equals(file.name, "dummy");
119+
}, "Misusing name");
120+
test(function() {
121+
var file = new File(["bits"], "dummy", { unknownKey: "value" });
122+
assert_true(file instanceof File);
123+
assert_equals(file.name, "dummy");
124+
}, "Unknown properties are ignored");
125+
126+
[
127+
123,
128+
123.4,
129+
true,
130+
'abc'
131+
].forEach(arg => {
132+
test(t => {
133+
assert_throws_js(TypeError, () => new File(['bits'], 'name.txt', arg),
134+
'Constructor should throw for invalid property bag type');
135+
}, `Invalid property bag: ${JSON.stringify(arg)}`);
136+
});
137+
138+
[
139+
null,
140+
undefined,
141+
[1,2,3],
142+
/regex/,
143+
function() {}
144+
].forEach(arg => {
145+
test(t => {
146+
assert_equals(new File(['bits'], 'name.txt', arg).size, 4,
147+
'Constructor should accept object-ish property bag type');
148+
}, `Unusual but valid property bag: ${arg}`);
149+
});
150+
151+
test(t => {
152+
assert_throws_js(Error,
153+
() => new File(['bits'], 'name.txt', {type: to_string_throws}),
154+
'Constructor should propagate exceptions');
155+
}, 'Property bag propagates exceptions');

0 commit comments

Comments
 (0)