mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
Enable node:vfs by default. --no-experimental-vfs disables node:vfs exposure and --vfs-load. Assisted-by: pi Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: https://github.com/nodejs/node/pull/66318 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
312 lines
14 KiB
JavaScript
312 lines
14 KiB
JavaScript
// Flags: --disable-warning=ExperimentalWarning
|
|
'use strict';
|
|
|
|
// Exercises ZipProvider (node:vfs backed by node:zlib's ZipBuffer/
|
|
// ZipFile): construction validation, readonly reflecting the archive's own
|
|
// writability, stat/readdir over explicit and implicit directories, and the
|
|
// full async and synchronous CRUD surface, against both a ZipBuffer and a
|
|
// ZipFile (opened both via open() and openSync()) source.
|
|
|
|
const common = require('../common');
|
|
const tmpdir = require('../common/tmpdir');
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const fsPromises = require('fs/promises');
|
|
const zlib = require('zlib');
|
|
const vfs = require('node:vfs');
|
|
|
|
tmpdir.refresh();
|
|
|
|
async function buildArchive(entries, comment) {
|
|
const chunks = [];
|
|
for await (const chunk of zlib.createZipArchive(entries, comment)) chunks.push(chunk);
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
(async () => {
|
|
// Construction validation.
|
|
assert.throws(() => new vfs.ZipProvider({}), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
assert.throws(() => new vfs.ZipProvider(null), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
|
|
// --- ZipBuffer-backed: always writable ------------------------------------
|
|
{
|
|
const archive = await buildArchive([
|
|
await zlib.ZipEntry.create('a.txt', Buffer.from('hello')),
|
|
await zlib.ZipEntry.create('dir/b.txt', Buffer.from('nested')),
|
|
await zlib.ZipEntry.create('empty-dir/', Buffer.alloc(0)),
|
|
]);
|
|
const zip = new zlib.ZipBuffer(archive);
|
|
const provider = new vfs.ZipProvider(zip);
|
|
assert.strictEqual(provider.readonly, false);
|
|
assert.strictEqual(provider.supportsSymlinks, false);
|
|
assert.strictEqual(provider.supportsWatch, false);
|
|
|
|
const archiveVfs = vfs.create(provider);
|
|
|
|
// stat: file, implicit directory, explicit directory, root.
|
|
const fileStat = await archiveVfs.promises.stat('/a.txt');
|
|
assert.strictEqual(fileStat.isFile(), true);
|
|
assert.strictEqual(fileStat.size, 5);
|
|
|
|
const implicitDirStat = await archiveVfs.promises.stat('/dir');
|
|
assert.strictEqual(implicitDirStat.isDirectory(), true);
|
|
|
|
const explicitDirStat = await archiveVfs.promises.stat('/empty-dir');
|
|
assert.strictEqual(explicitDirStat.isDirectory(), true);
|
|
|
|
const rootStat = await archiveVfs.promises.stat('/');
|
|
assert.strictEqual(rootStat.isDirectory(), true);
|
|
|
|
await assert.rejects(archiveVfs.promises.stat('/missing.txt'), { code: 'ENOENT' });
|
|
|
|
// readdir: root lists both files and directories, deduped.
|
|
const rootEntries = await archiveVfs.promises.readdir('/');
|
|
assert.deepStrictEqual(rootEntries.sort(), ['a.txt', 'dir', 'empty-dir']);
|
|
|
|
const dirEntries = await archiveVfs.promises.readdir('/dir');
|
|
assert.deepStrictEqual(dirEntries, ['b.txt']);
|
|
|
|
const withTypes = await archiveVfs.promises.readdir('/', { withFileTypes: true });
|
|
const byName = new Map(withTypes.map((d) => [d.name, d]));
|
|
assert.strictEqual(byName.get('a.txt').isFile(), true);
|
|
assert.strictEqual(byName.get('dir').isDirectory(), true);
|
|
|
|
await assert.rejects(archiveVfs.promises.readdir('/a.txt'), { code: 'ENOTDIR' });
|
|
const recursiveEntries = await archiveVfs.promises.readdir('/', { recursive: true });
|
|
assert.deepStrictEqual(recursiveEntries.sort(),
|
|
['a.txt', 'dir', 'dir/b.txt', 'empty-dir']);
|
|
|
|
// readFile / writeFile round trip (new file).
|
|
assert.strictEqual(await archiveVfs.promises.readFile('/a.txt', 'utf8'), 'hello');
|
|
await archiveVfs.promises.writeFile('/new.txt', 'brand new');
|
|
assert.strictEqual(await archiveVfs.promises.readFile('/new.txt', 'utf8'), 'brand new');
|
|
assert.strictEqual(zip.has('new.txt'), true);
|
|
|
|
// Entries cannot be created beneath a file.
|
|
await assert.rejects(
|
|
archiveVfs.promises.writeFile('/a.txt/child.txt', 'child'),
|
|
{ code: 'ENOTDIR' },
|
|
);
|
|
await assert.rejects(
|
|
archiveVfs.promises.mkdir('/a.txt/child'),
|
|
{ code: 'ENOTDIR' },
|
|
);
|
|
await assert.rejects(
|
|
archiveVfs.promises.rename('/new.txt', '/a.txt/renamed.txt'),
|
|
{ code: 'ENOTDIR' },
|
|
);
|
|
assert.strictEqual(zip.has('a.txt/child.txt'), false);
|
|
assert.strictEqual(zip.has('a.txt/child/'), false);
|
|
assert.strictEqual(zip.has('a.txt/renamed.txt'), false);
|
|
assert.strictEqual(zip.has('new.txt'), true);
|
|
|
|
// Overwriting an existing file.
|
|
await archiveVfs.promises.writeFile('/a.txt', 'overwritten');
|
|
assert.strictEqual(await archiveVfs.promises.readFile('/a.txt', 'utf8'), 'overwritten');
|
|
|
|
// appendFile.
|
|
await archiveVfs.promises.writeFile('/append.txt', 'ab');
|
|
await archiveVfs.promises.appendFile('/append.txt', 'cd');
|
|
assert.strictEqual(await archiveVfs.promises.readFile('/append.txt', 'utf8'), 'abcd');
|
|
|
|
// mkdir + rmdir.
|
|
await archiveVfs.promises.mkdir('/newdir');
|
|
assert.strictEqual((await archiveVfs.promises.stat('/newdir')).isDirectory(), true);
|
|
await assert.rejects(archiveVfs.promises.mkdir('/newdir'), { code: 'EEXIST' });
|
|
await archiveVfs.promises.mkdir('/newdir', { recursive: true }); // No throw, already exists
|
|
await archiveVfs.promises.rmdir('/newdir');
|
|
await assert.rejects(archiveVfs.promises.stat('/newdir'), { code: 'ENOENT' });
|
|
|
|
// Rmdir refuses a non-empty directory.
|
|
await assert.rejects(archiveVfs.promises.rmdir('/dir'), { code: 'ENOTEMPTY' });
|
|
|
|
// unlink.
|
|
await archiveVfs.promises.unlink('/append.txt');
|
|
await assert.rejects(archiveVfs.promises.stat('/append.txt'), { code: 'ENOENT' });
|
|
await assert.rejects(archiveVfs.promises.unlink('/missing.txt'), { code: 'ENOENT' });
|
|
await assert.rejects(archiveVfs.promises.unlink('/dir'), { code: 'EISDIR' });
|
|
|
|
// rename.
|
|
await archiveVfs.promises.writeFile('/rename-me.txt', 'content');
|
|
await archiveVfs.promises.rename('/rename-me.txt', '/renamed.txt');
|
|
assert.strictEqual(zip.has('rename-me.txt'), false);
|
|
assert.strictEqual(await archiveVfs.promises.readFile('/renamed.txt', 'utf8'), 'content');
|
|
|
|
// open() flag semantics.
|
|
await assert.rejects(archiveVfs.promises.open('/does-not-exist.txt', 'r'), { code: 'ENOENT' });
|
|
await assert.rejects(archiveVfs.promises.open('/a.txt', 'wx'), { code: 'EEXIST' });
|
|
await assert.rejects(archiveVfs.promises.open('/dir', 'r'), { code: 'EISDIR' });
|
|
|
|
await archiveVfs.promises.rename('/dir', '/renamed-dir');
|
|
await assert.rejects(archiveVfs.promises.stat('/dir'), { code: 'ENOENT' });
|
|
assert.strictEqual(await archiveVfs.promises.readFile('/renamed-dir/b.txt', 'utf8'), 'nested');
|
|
}
|
|
|
|
// Renaming an explicit directory also renames its children.
|
|
{
|
|
const archive = await buildArchive([
|
|
await zlib.ZipEntry.create('async-dir/', Buffer.alloc(0)),
|
|
await zlib.ZipEntry.create('async-dir/child.txt', Buffer.from('async')),
|
|
await zlib.ZipEntry.create('sync-dir/', Buffer.alloc(0)),
|
|
await zlib.ZipEntry.create('sync-dir/child.txt', Buffer.from('sync')),
|
|
]);
|
|
const zip = new zlib.ZipBuffer(archive);
|
|
const archiveVfs = vfs.create(new vfs.ZipProvider(zip));
|
|
|
|
await archiveVfs.promises.rename('/async-dir', '/renamed-async-dir');
|
|
assert.strictEqual(zip.has('async-dir/'), false);
|
|
assert.strictEqual(zip.has('async-dir/child.txt'), false);
|
|
assert.strictEqual(zip.has('renamed-async-dir/'), true);
|
|
assert.strictEqual(
|
|
await archiveVfs.promises.readFile('/renamed-async-dir/child.txt', 'utf8'),
|
|
'async',
|
|
);
|
|
|
|
archiveVfs.renameSync('/sync-dir', '/renamed-sync-dir');
|
|
assert.strictEqual(zip.has('sync-dir/'), false);
|
|
assert.strictEqual(zip.has('sync-dir/child.txt'), false);
|
|
assert.strictEqual(zip.has('renamed-sync-dir/'), true);
|
|
assert.strictEqual(archiveVfs.readFileSync('/renamed-sync-dir/child.txt', 'utf8'), 'sync');
|
|
}
|
|
|
|
// --- ZipFile-backed, read-only: writes rejected with EROFS ----------------
|
|
{
|
|
const archive = await buildArchive([await zlib.ZipEntry.create('a.txt', Buffer.from('x'))]);
|
|
const filePath = path.join(tmpdir.path, 'vfs-archive-readonly.zip');
|
|
await fsPromises.writeFile(filePath, archive);
|
|
const zip = await zlib.ZipFile.open(filePath);
|
|
const provider = new vfs.ZipProvider(zip);
|
|
assert.strictEqual(provider.readonly, true);
|
|
const archiveVfs = vfs.create(provider);
|
|
|
|
assert.strictEqual(await archiveVfs.promises.readFile('/a.txt', 'utf8'), 'x');
|
|
await assert.rejects(archiveVfs.promises.writeFile('/new.txt', 'y'), { code: 'EROFS' });
|
|
await assert.rejects(archiveVfs.promises.unlink('/a.txt'), { code: 'EROFS' });
|
|
await assert.rejects(archiveVfs.promises.mkdir('/newdir'), { code: 'EROFS' });
|
|
await assert.rejects(archiveVfs.promises.rmdir('/newdir'), { code: 'EROFS' });
|
|
await assert.rejects(archiveVfs.promises.rename('/a.txt', '/b.txt'), { code: 'EROFS' });
|
|
|
|
// The synchronous surface rejects the same way.
|
|
assert.strictEqual(archiveVfs.readFileSync('/a.txt', 'utf8'), 'x');
|
|
assert.throws(() => archiveVfs.writeFileSync('/new.txt', 'y'), { code: 'EROFS' });
|
|
assert.throws(() => archiveVfs.unlinkSync('/a.txt'), { code: 'EROFS' });
|
|
assert.throws(() => archiveVfs.mkdirSync('/newdir'), { code: 'EROFS' });
|
|
assert.throws(() => archiveVfs.rmdirSync('/newdir'), { code: 'EROFS' });
|
|
assert.throws(() => archiveVfs.renameSync('/a.txt', '/b.txt'), { code: 'EROFS' });
|
|
|
|
await zip.close();
|
|
}
|
|
|
|
// --- ZipFile-backed, opened writable: mutations persist to disk ----------
|
|
{
|
|
const archive = await buildArchive([await zlib.ZipEntry.create('a.txt', Buffer.from('x'))]);
|
|
const filePath = path.join(tmpdir.path, 'vfs-archive-writable.zip');
|
|
await fsPromises.writeFile(filePath, archive);
|
|
const zip = await zlib.ZipFile.open(filePath, { writable: true });
|
|
const provider = new vfs.ZipProvider(zip);
|
|
assert.strictEqual(provider.readonly, false);
|
|
const archiveVfs = vfs.create(provider);
|
|
|
|
await archiveVfs.promises.writeFile('/b.txt', 'new content');
|
|
await zip.close();
|
|
|
|
const reopened = await zlib.ZipFile.open(filePath);
|
|
assert.strictEqual((await (await reopened.get('b.txt')).content()).toString(), 'new content');
|
|
await reopened.close();
|
|
}
|
|
|
|
// --- ZipBuffer-backed, fully synchronous CRUD ------------------------------
|
|
{
|
|
const archive = await buildArchive([
|
|
await zlib.ZipEntry.create('a.txt', Buffer.from('hello')),
|
|
await zlib.ZipEntry.create('dir/b.txt', Buffer.from('nested')),
|
|
]);
|
|
const zip = new zlib.ZipBuffer(archive);
|
|
const provider = new vfs.ZipProvider(zip);
|
|
const archiveVfs = vfs.create(provider);
|
|
|
|
// stat/readdir.
|
|
assert.strictEqual(archiveVfs.statSync('/a.txt').isFile(), true);
|
|
assert.strictEqual(archiveVfs.statSync('/dir').isDirectory(), true);
|
|
assert.throws(() => archiveVfs.statSync('/missing.txt'), { code: 'ENOENT' });
|
|
assert.deepStrictEqual(archiveVfs.readdirSync('/').sort(), ['a.txt', 'dir']);
|
|
assert.throws(() => archiveVfs.readdirSync('/a.txt'), { code: 'ENOTDIR' });
|
|
assert.deepStrictEqual(archiveVfs.readdirSync('/', { recursive: true }).sort(),
|
|
['a.txt', 'dir', 'dir/b.txt']);
|
|
|
|
// readFile/writeFile/appendFile round trip.
|
|
assert.strictEqual(archiveVfs.readFileSync('/a.txt', 'utf8'), 'hello');
|
|
archiveVfs.writeFileSync('/new.txt', 'brand new');
|
|
assert.strictEqual(archiveVfs.readFileSync('/new.txt', 'utf8'), 'brand new');
|
|
archiveVfs.appendFileSync('/new.txt', '!');
|
|
assert.strictEqual(archiveVfs.readFileSync('/new.txt', 'utf8'), 'brand new!');
|
|
|
|
// Entries cannot be created beneath a file.
|
|
assert.throws(
|
|
() => archiveVfs.writeFileSync('/a.txt/child.txt', 'child'),
|
|
{ code: 'ENOTDIR' },
|
|
);
|
|
assert.throws(
|
|
() => archiveVfs.mkdirSync('/a.txt/child'),
|
|
{ code: 'ENOTDIR' },
|
|
);
|
|
assert.throws(
|
|
() => archiveVfs.renameSync('/new.txt', '/a.txt/renamed.txt'),
|
|
{ code: 'ENOTDIR' },
|
|
);
|
|
assert.strictEqual(zip.has('a.txt/child.txt'), false);
|
|
assert.strictEqual(zip.has('a.txt/child/'), false);
|
|
assert.strictEqual(zip.has('a.txt/renamed.txt'), false);
|
|
assert.strictEqual(zip.has('new.txt'), true);
|
|
|
|
// mkdir/rmdir.
|
|
archiveVfs.mkdirSync('/newdir');
|
|
assert.strictEqual(archiveVfs.statSync('/newdir').isDirectory(), true);
|
|
assert.throws(() => archiveVfs.mkdirSync('/newdir'), { code: 'EEXIST' });
|
|
archiveVfs.mkdirSync('/newdir', { recursive: true }); // No throw, already exists.
|
|
archiveVfs.rmdirSync('/newdir');
|
|
assert.throws(() => archiveVfs.statSync('/newdir'), { code: 'ENOENT' });
|
|
assert.throws(() => archiveVfs.rmdirSync('/dir'), { code: 'ENOTEMPTY' });
|
|
|
|
// unlink.
|
|
archiveVfs.unlinkSync('/new.txt');
|
|
assert.throws(() => archiveVfs.statSync('/new.txt'), { code: 'ENOENT' });
|
|
assert.throws(() => archiveVfs.unlinkSync('/missing.txt'), { code: 'ENOENT' });
|
|
assert.throws(() => archiveVfs.unlinkSync('/dir'), { code: 'EISDIR' });
|
|
|
|
// rename.
|
|
archiveVfs.writeFileSync('/rename-me.txt', 'content');
|
|
archiveVfs.renameSync('/rename-me.txt', '/renamed.txt');
|
|
assert.strictEqual(zip.has('rename-me.txt'), false);
|
|
assert.strictEqual(archiveVfs.readFileSync('/renamed.txt', 'utf8'), 'content');
|
|
|
|
// open() flag semantics.
|
|
assert.throws(() => archiveVfs.openSync('/does-not-exist.txt', 'r'), { code: 'ENOENT' });
|
|
assert.throws(() => archiveVfs.openSync('/a.txt', 'wx'), { code: 'EEXIST' });
|
|
assert.throws(() => archiveVfs.openSync('/dir', 'r'), { code: 'EISDIR' });
|
|
|
|
archiveVfs.renameSync('/dir', '/renamed-dir');
|
|
assert.throws(() => archiveVfs.statSync('/dir'), { code: 'ENOENT' });
|
|
assert.strictEqual(archiveVfs.readFileSync('/renamed-dir/b.txt', 'utf8'), 'nested');
|
|
}
|
|
|
|
// --- ZipFile-backed via openSync: sync-only round trip on disk -----------
|
|
{
|
|
const archive = await buildArchive([await zlib.ZipEntry.create('a.txt', Buffer.from('x'))]);
|
|
const filePath = path.join(tmpdir.path, 'vfs-archive-opensync.zip');
|
|
await fsPromises.writeFile(filePath, archive);
|
|
const zip = zlib.ZipFile.openSync(filePath, { writable: true });
|
|
const provider = new vfs.ZipProvider(zip);
|
|
assert.strictEqual(provider.readonly, false);
|
|
const archiveVfs = vfs.create(provider);
|
|
|
|
assert.strictEqual(archiveVfs.readFileSync('/a.txt', 'utf8'), 'x');
|
|
archiveVfs.writeFileSync('/b.txt', 'new content');
|
|
zip.closeSync();
|
|
|
|
const reopened = zlib.ZipFile.openSync(filePath);
|
|
assert.strictEqual(reopened.getSync('b.txt').contentSync().toString(), 'new content');
|
|
reopened.closeSync();
|
|
}
|
|
})().then(common.mustCall());
|