Files
node/test/parallel/test-blob-file-backed.js
Paul Bouchon 05447419fb src: report libuv error when openAsBlob cannot stat
`FdEntry::Create()` returned nullptr for any failed `uv_fs_stat()`,
discarding the status, and `BlobFromFilePath()` turned that into
`ERR_INVALID_ARG_VALUE: Unable to open file as blob`. A missing file is
not a malformed argument, and the resulting `TypeError` carried no
`errno`, `syscall`, or `path`, so ENOENT could not be told apart from
any other reason the path was unusable.

Thread the libuv status out of `CreateFdEntry()` and throw a
`UVException` instead, so `fs.openAsBlob()` reports the same error
`fs.stat()` does for the same path.

Fixes: https://github.com/nodejs/node/issues/65514
Signed-off-by: Paul Bouchon <mail@bitpshr.net>
PR-URL: https://github.com/nodejs/node/pull/65517
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2026-08-28 13:31:48 +00:00

155 lines
4.6 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('assert');
const { TextDecoder } = require('util');
const {
writeFileSync,
openAsBlob,
} = require('fs');
const {
unlink
} = require('fs/promises');
const { Blob } = require('buffer');
const tmpdir = require('../common/tmpdir');
const testfile = tmpdir.resolve('test-file-backed-blob.txt');
const testfile2 = tmpdir.resolve('test-file-backed-blob2.txt');
const testfile3 = tmpdir.resolve('test-file-backed-blob3.txt');
const testfile4 = tmpdir.resolve('test-file-backed-blob4.txt');
const testfile5 = tmpdir.resolve('test-file-backed-blob5.txt');
tmpdir.refresh();
const data = `${'a'.repeat(1000)}${'b'.repeat(2000)}`;
writeFileSync(testfile, data);
writeFileSync(testfile2, data);
writeFileSync(testfile3, data.repeat(100));
writeFileSync(testfile4, '');
writeFileSync(testfile5, '');
(async () => {
const blob = await openAsBlob(testfile);
const ab = await blob.arrayBuffer();
const dec = new TextDecoder();
assert.strictEqual(dec.decode(new Uint8Array(ab)), data);
assert.strictEqual(await blob.text(), data);
// Can be read multiple times
let stream = blob.stream();
let check = '';
for await (const chunk of stream)
check = dec.decode(chunk);
assert.strictEqual(check, data);
// Can be combined with other Blob's and read
const combined = new Blob(['hello', blob, 'world']);
const ab2 = await combined.arrayBuffer();
assert.strictEqual(dec.decode(ab2.slice(0, 5)), 'hello');
assert.strictEqual(dec.decode(ab2.slice(5, -5)), data);
assert.strictEqual(dec.decode(ab2.slice(-5)), 'world');
// If the file is modified tho, the stream errors.
writeFileSync(testfile, data + 'abc');
stream = blob.stream();
const read = async () => {
// eslint-disable-next-line no-unused-vars, no-empty
for await (const _ of stream) {}
};
await assert.rejects(read(), { name: 'NotReadableError' });
await unlink(testfile);
})().then(common.mustCall());
(async () => {
// Refs: https://github.com/nodejs/node/issues/47683
const blob = await openAsBlob(testfile2);
const res = blob.slice(10, 20);
const ab = await res.arrayBuffer();
assert.strictEqual(res.size, ab.byteLength);
let length = 0;
const stream = await res.stream();
for await (const chunk of stream)
length += chunk.length;
assert.strictEqual(res.size, length);
const res1 = blob.slice(995, 1005);
assert.strictEqual(await res1.text(), data.slice(995, 1005));
// Refs: https://github.com/nodejs/node/issues/53908
for (const res2 of [
blob.slice(995, 1005).slice(),
blob.slice(995).slice(0, 10),
blob.slice(0, 1005).slice(995),
]) {
assert.strictEqual(await res2.text(), data.slice(995, 1005));
}
await unlink(testfile2);
})().then(common.mustCall());
(async () => {
const blob = await openAsBlob(testfile3);
const stream = blob.stream();
const read = async () => {
// eslint-disable-next-line no-unused-vars
for await (const _ of stream) {
writeFileSync(testfile3, data + 'abc');
}
};
await assert.rejects(read(), { name: 'NotReadableError' });
await unlink(testfile3);
})().then(common.mustCall());
(async () => {
const blob = await openAsBlob(testfile4);
assert.strictEqual(blob.size, 0);
assert.strictEqual(await blob.text(), '');
writeFileSync(testfile4, 'abc');
await assert.rejects(blob.text(), { name: 'NotReadableError' });
await unlink(testfile4);
})().then(common.mustCall());
(async () => {
const blob = await openAsBlob(testfile5);
assert.strictEqual(blob.size, 0);
writeFileSync(testfile5, 'abc');
const stream = blob.stream();
const reader = stream.getReader();
await assert.rejects(() => reader.read(), { name: 'NotReadableError' });
await unlink(testfile5);
})().then(common.mustCall());
(async () => {
// A path that cannot be stat'd reports the underlying libuv error rather
// than a generic argument error, so ENOENT can be told apart from any other
// reason the file could not be used.
// Refs: https://github.com/nodejs/node/issues/65514
const missing = tmpdir.resolve('does-not-exist.txt');
await assert.rejects(async () => openAsBlob(missing), {
code: 'ENOENT',
syscall: 'stat',
path: missing,
});
})().then(common.mustCall());
(async () => {
// We currently do not allow File-backed blobs to be cloned or transferred
// across worker threads. This is largely because the underlying FdEntry
// is bound to the Environment/Realm under which is was created.
const blob = await openAsBlob(__filename);
assert.throws(() => structuredClone(blob), {
code: 'ERR_INVALID_STATE',
message: 'Invalid state: File-backed Blobs are not cloneable'
});
})().then(common.mustCall());