Skip to content

Commit 29d3406

Browse files
pipobscureaduh95
authored andcommitted
vfs: apply open(2) effects to ZipProvider handles
A ZipProvider handle keeps its content in memory and adds the entry to the archive when it is closed, and only if something was written. The effects a real `open(2)` has at open time are therefore lost, and so is the metadata the entry already carried: * `open(path, 'w')` followed by `close()` neither truncates an existing entry nor creates a missing one; the same holds for "a" on a missing file. Tools that touch or truncate by open-then-close do nothing. * Rewriting an entry (append, or an in-place write through "r+") re-adds it with the `mode` argument `open()` received (fs's default 0o666), not the mode the entry had, so a 0o755 script silently loses its executable bit. * `fstat` on a handle reports that same `open()` mode and the current time instead of the entry's mode and modification time. * Renaming a file onto an existing directory succeeds and leaves a name that is both a file and a directory; real file systems refuse with EISDIR. This adds a test for each of these against a mounted ZipBuffer, stating the real-fs outcome as the expectation. Proposed solution: mark the handle dirty at open time when the flags imply creation or truncation, so close always commits; carry the existing entry's mode and modification time on the handle, use them for `fstat` and for the re-added entry, and only fall back to the `open()` mode for a newly created entry; and reject `rename` onto an existing directory with EISDIR before touching the archive. Signed-off-by: Philipp Dunkel <pip@pipobscure.com> PR-URL: #65853 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent ae29cc9 commit 29d3406

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Flags: --experimental-vfs
2+
'use strict';
3+
4+
// A ZipProvider handle commits its content to the archive when it is closed.
5+
// The effects `open(2)` has at open time, and the metadata an entry already
6+
// carries, must survive that model: opening with "w" creates or truncates
7+
// even without a write, rewriting an entry keeps its mode, fstat reports the
8+
// entry's mode, and a file cannot be renamed onto a directory. Each case
9+
// states the real-fs outcome as the expectation. Cases are independent so
10+
// the runner reports each one.
11+
12+
require('../common');
13+
const assert = require('assert');
14+
const fs = require('fs');
15+
const path = require('path');
16+
const zlib = require('zlib');
17+
const vfs = require('node:vfs');
18+
const { test } = require('node:test');
19+
20+
// Builds a writable in-memory archive from [name, content, options] triples
21+
// and mounts it, returning the mount point.
22+
function mountZip(entries) {
23+
const list = entries.map(({ 0: name, 1: content, 2: options }) =>
24+
zlib.ZipEntry.createSync(name, Buffer.from(content), options));
25+
const chunks = [];
26+
for (const chunk of zlib.createZipArchiveSync(list)) chunks.push(chunk);
27+
const provider = new vfs.ZipProvider(new zlib.ZipBuffer(Buffer.concat(chunks)));
28+
return vfs.create(provider).mount();
29+
}
30+
31+
test('opening an existing file with "w" truncates it even without a write', () => {
32+
const file = path.join(mountZip([['f.txt', 'hello']]), 'f.txt');
33+
fs.closeSync(fs.openSync(file, 'w'));
34+
assert.strictEqual(fs.readFileSync(file, 'utf8'), '');
35+
});
36+
37+
test('opening a new file with "w" creates it even without a write', () => {
38+
const file = path.join(mountZip([['f.txt', 'hello']]), 'new.txt');
39+
fs.closeSync(fs.openSync(file, 'w'));
40+
assert.strictEqual(fs.existsSync(file), true);
41+
});
42+
43+
test('opening a new file with "a" creates it even without a write', () => {
44+
const file = path.join(mountZip([['f.txt', 'hello']]), 'log.txt');
45+
fs.closeSync(fs.openSync(file, 'a'));
46+
assert.strictEqual(fs.existsSync(file), true);
47+
});
48+
49+
test('appending keeps the entry mode', () => {
50+
const file = path.join(mountZip([['x.sh', 'a', { mode: 0o755 }]]), 'x.sh');
51+
assert.strictEqual(fs.statSync(file).mode & 0o777, 0o755);
52+
fs.appendFileSync(file, 'b');
53+
assert.strictEqual(fs.statSync(file).mode & 0o777, 0o755);
54+
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'ab');
55+
});
56+
57+
test('an in-place write keeps the entry mode', () => {
58+
const file = path.join(mountZip([['x.sh', 'abc', { mode: 0o755 }]]), 'x.sh');
59+
const fd = fs.openSync(file, 'r+');
60+
fs.writeSync(fd, Buffer.from('Z'), 0, 1, 0);
61+
fs.closeSync(fd);
62+
assert.strictEqual(fs.statSync(file).mode & 0o777, 0o755);
63+
});
64+
65+
test('a new file gets the mode passed to open', () => {
66+
const file = path.join(mountZip([['f.txt', 'hello']]), 'new.sh');
67+
const fd = fs.openSync(file, 'w', 0o700);
68+
fs.writeSync(fd, Buffer.from('#!'));
69+
fs.closeSync(fd);
70+
assert.strictEqual(fs.statSync(file).mode & 0o777, 0o700);
71+
});
72+
73+
test('fstat reports the entry mode, not the open() mode argument', () => {
74+
const file = path.join(mountZip([['x.sh', 'a', { mode: 0o755 }]]), 'x.sh');
75+
const fd = fs.openSync(file, 'r');
76+
try {
77+
assert.strictEqual(fs.fstatSync(fd).mode & 0o777, 0o755);
78+
} finally {
79+
fs.closeSync(fd);
80+
}
81+
});
82+
83+
test('fstat reports the entry modification time', () => {
84+
const modified = new Date('2020-01-02T03:04:05Z');
85+
const file = path.join(mountZip([['f.txt', 'a', { modified }]]), 'f.txt');
86+
const fd = fs.openSync(file, 'r');
87+
try {
88+
// ZIP timestamps have two-second resolution, so compare at that grain.
89+
assert.strictEqual(Math.floor(fs.fstatSync(fd).mtimeMs / 2000),
90+
Math.floor(modified.getTime() / 2000));
91+
} finally {
92+
fs.closeSync(fd);
93+
}
94+
});
95+
96+
test('renaming a file onto an existing directory fails with EISDIR', () => {
97+
const mount = mountZip([['dir/', ''], ['f', 'x']]);
98+
assert.throws(() => fs.renameSync(path.join(mount, 'f'), path.join(mount, 'dir')),
99+
{ code: 'EISDIR' });
100+
assert.strictEqual(fs.statSync(path.join(mount, 'dir')).isDirectory(), true);
101+
assert.strictEqual(fs.readFileSync(path.join(mount, 'f'), 'utf8'), 'x');
102+
});

0 commit comments

Comments
 (0)