@zenfs/core 2.7.7 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +5 -0
  2. package/dist/backends/index.d.ts +3 -1
  3. package/dist/backends/index.js +3 -1
  4. package/dist/backends/single_buffer/backend.d.ts +42 -0
  5. package/dist/backends/single_buffer/backend.js +204 -0
  6. package/dist/backends/{single_buffer.d.ts → single_buffer/v1.d.ts} +24 -34
  7. package/dist/backends/{single_buffer.js → single_buffer/v1.js} +10 -26
  8. package/dist/backends/single_buffer/v2.d.ts +257 -0
  9. package/dist/backends/single_buffer/v2.js +1283 -0
  10. package/dist/backends/store/fs.d.ts +1 -1
  11. package/dist/backends/store/fs.js +34 -15
  12. package/dist/backends/store/map.d.ts +6 -6
  13. package/dist/backends/store/map.js +6 -6
  14. package/dist/backends/store/store.d.ts +18 -14
  15. package/dist/backends/store/store.js +11 -11
  16. package/dist/internal/contexts.d.ts +5 -0
  17. package/dist/internal/contexts.js +7 -1
  18. package/dist/mixins/async.js +26 -7
  19. package/dist/node/promises.js +6 -103
  20. package/dist/node/sync.js +6 -33
  21. package/dist/node/types.d.ts +6 -1
  22. package/dist/vfs/async.d.ts +4 -1
  23. package/dist/vfs/async.js +72 -5
  24. package/dist/vfs/sync.d.ts +4 -1
  25. package/dist/vfs/sync.js +65 -4
  26. package/package.json +3 -3
  27. package/tests/backend/single-buffer-rotate.worker.js +1 -1
  28. package/tests/backend/single-buffer-v2.test.ts +342 -0
  29. package/tests/backend/single-buffer-v2.worker.js +51 -0
  30. package/tests/backend/single-buffer.test.ts +6 -6
  31. package/tests/backend/single-buffer.worker.js +1 -0
  32. package/tests/common/async.test.ts +11 -0
  33. package/tests/common/context.test.ts +30 -0
  34. package/tests/fs/permissions.test.ts +27 -6
  35. package/tests/fs/watch.test.ts +14 -0
  36. package/tests/perf/config.json +31 -1
  37. package/tests/perf/single-buffer-random.ts +38 -0
  38. package/tests/perf/single-buffer-read.ts +28 -0
  39. package/tests/perf/single-buffer-write.ts +41 -0
  40. package/tests/setup/single-buffer-v2.ts +10 -0
  41. package/tests/setup/single-buffer.ts +1 -0
@@ -5,7 +5,7 @@ import { suite, test } from 'node:test';
5
5
  import { Worker } from 'worker_threads';
6
6
  import { sizeof } from 'memium';
7
7
  import { fs, mount, resolveMountConfig, SingleBuffer, vfs } from '@zenfs/core';
8
- import { MetadataBlock, SuperBlock } from '@zenfs/core/backends/single_buffer';
8
+ import { MetadataBlock, SuperBlock } from '@zenfs/core/backends/single_buffer/v1';
9
9
  import { setupLogs } from '../logs.js';
10
10
 
11
11
  setupLogs();
@@ -14,13 +14,13 @@ await suite('SingleBuffer', () => {
14
14
  test('filesystem restoration from original buffer (with same metadata)', async () => {
15
15
  const buffer = new ArrayBuffer(0x100000);
16
16
 
17
- const writable = await resolveMountConfig({ backend: SingleBuffer, buffer });
17
+ const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, v2: false });
18
18
  mount('/mnt', writable);
19
19
 
20
20
  fs.writeFileSync('/mnt/example.ts', 'console.log("hello world")', 'utf-8');
21
21
  const stats = fs.statSync('/mnt/example.ts');
22
22
 
23
- const snapshot = await resolveMountConfig({ backend: SingleBuffer, buffer });
23
+ const snapshot = await resolveMountConfig({ backend: SingleBuffer, buffer, v2: false });
24
24
  mount('/snapshot', snapshot);
25
25
 
26
26
  const snapshotStats = fs.statSync('/snapshot/example.ts');
@@ -31,7 +31,7 @@ await suite('SingleBuffer', () => {
31
31
  test('cross-thread SharedArrayBuffer', async () => {
32
32
  const sharedBuffer = new SharedArrayBuffer(0x100000);
33
33
 
34
- const writable = await resolveMountConfig({ backend: SingleBuffer, buffer: sharedBuffer });
34
+ const writable = await resolveMountConfig({ backend: SingleBuffer, buffer: sharedBuffer, v2: false });
35
35
  fs.mkdirSync('/shared');
36
36
  mount('/shared', writable);
37
37
 
@@ -141,7 +141,7 @@ await suite('SingleBuffer', () => {
141
141
  const mountPoint = '/sbfs-reliability';
142
142
  const verifyMountPoint = '/sbfs-verify';
143
143
  const buffer = new ArrayBuffer(0x400000);
144
- const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'reliability' });
144
+ const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'reliability', v2: false });
145
145
  mount(mountPoint, writable);
146
146
 
147
147
  const filePath = `${mountPoint}/payload.bin`;
@@ -186,7 +186,7 @@ await suite('SingleBuffer', () => {
186
186
  test('keeps metadata aligned when files have uneven sizes #309', async () => {
187
187
  const mountPoint = '/sbfs-rotation';
188
188
  const buffer = new ArrayBuffer(0x400000);
189
- const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'rotation' });
189
+ const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'rotation', v2: false });
190
190
  mount(mountPoint, writable);
191
191
 
192
192
  // Uneven writes leave used_bytes between alignment boundaries when the metadata block fills up.
@@ -16,6 +16,7 @@ try {
16
16
  await configureSingle({
17
17
  backend: SingleBuffer,
18
18
  buffer: workerData,
19
+ v2: false,
19
20
  });
20
21
 
21
22
  fs.writeFileSync('/worker-file.ts', content, 'utf-8');
@@ -32,4 +32,15 @@ suite('Async()', () => {
32
32
  assert.deepEqual(calls, ['mkdirSync', 'rmdirSync', 'createFileSync', 'unlinkSync']);
33
33
  assert.deepEqual(await fs.readdir('/'), []);
34
34
  });
35
+
36
+ test('async operations see queued sync operations', async () => {
37
+ const fs = new AsyncMemory(new InMemoryStore());
38
+ fs.checkRootSync();
39
+ await fs.ready();
40
+
41
+ fs.mkdirSync('/dir', { mode: 0o755, uid: 0, gid: 0 });
42
+ assert.equal((await fs.stat('/dir')).mode & 0o777, 0o755);
43
+ fs.rmdirSync('/dir');
44
+ assert.equal(await fs.exists('/dir'), false);
45
+ });
35
46
  });
@@ -134,4 +134,34 @@ suite('Context', () => {
134
134
 
135
135
  assert.doesNotThrow(() => ctx.bind({ credentials: { uid: 500, gid: 500 } }));
136
136
  });
137
+
138
+ test('umask is inherited by children', () => {
139
+ const parent = bindContext({ umask: 0o027 });
140
+ assert.equal(parent.bind({}).umask, 0o027);
141
+ assert.equal(parent.bind({ umask: 0o077 }).umask, 0o077);
142
+ });
143
+
144
+ test('umask applies to created files and directories', async () => {
145
+ fs.mkdirSync('/masked');
146
+ const masked = bindContext({ root: '/masked', umask: 0o027 });
147
+
148
+ masked.fs.writeFileSync('/sync.txt', '', { mode: 0o666 });
149
+ assert.equal(masked.fs.statSync('/sync.txt').mode & 0o7777, 0o640);
150
+
151
+ await masked.fs.promises.writeFile('/async.txt', '', { mode: 0o666 });
152
+ assert.equal(masked.fs.statSync('/async.txt').mode & 0o7777, 0o640);
153
+
154
+ masked.fs.mkdirSync('/a/b', { recursive: true, mode: 0o777 });
155
+ assert.equal(masked.fs.statSync('/a').mode & 0o7777, 0o750);
156
+ assert.equal(masked.fs.statSync('/a/b').mode & 0o7777, 0o750);
157
+
158
+ await masked.fs.promises.mkdir('/c', 0o1777);
159
+ assert.equal(masked.fs.statSync('/c').mode & 0o7777, 0o1750);
160
+
161
+ masked.fs.symlinkSync('/sync.txt', '/link');
162
+ assert.equal(masked.fs.lstatSync('/link').mode & 0o7777, 0o777);
163
+
164
+ masked.fs.chmodSync('/sync.txt', 0o666);
165
+ assert.equal(masked.fs.statSync('/sync.txt').mode & 0o7777, 0o666);
166
+ });
137
167
  });
@@ -133,11 +133,11 @@ suite('Permissions', config('permissions'), () => {
133
133
  await rootFS.promises.writeFile('/removal-root/file', 'x', { mode: 0o666 });
134
134
  await rootFS.promises.mkdir('/removal-root/dir', { mode: 0o777 });
135
135
  await rootFS.promises.mkdir('/removal-own', { mode: 0o755 });
136
- await rootFS.promises.lchown('/removal-own', 1000, 1000);
136
+ await rootFS.promises.chown('/removal-own', 1000, 1000);
137
137
  await rootFS.promises.writeFile('/removal-own/readonly', 'x', { mode: 0o444 });
138
138
  await rootFS.promises.mkdir('/removal-own/readonly-dir', { mode: 0o555 });
139
139
  await rootFS.promises.mkdir('/removal-unreadable', { mode: 0o300 });
140
- await rootFS.promises.lchown('/removal-unreadable', 1000, 1000);
140
+ await rootFS.promises.chown('/removal-unreadable', 1000, 1000);
141
141
  await rootFS.promises.writeFile('/removal-unreadable/file', 'x');
142
142
 
143
143
  await assert.rejects(alice.fs.promises.unlink('/removal-root/file'), { code: 'EACCES' });
@@ -177,7 +177,7 @@ suite('Permissions', config('permissions'), () => {
177
177
  assert.deepEqual(rootFS.readdirSync('/sticky'), []);
178
178
 
179
179
  await rootFS.promises.mkdir('/sticky-alice', { mode: 0o1777 });
180
- await rootFS.promises.lchown('/sticky-alice', 1000, 1000);
180
+ await rootFS.promises.chown('/sticky-alice', 1000, 1000);
181
181
  await bob.fs.promises.writeFile('/sticky-alice/bob', 'b');
182
182
  await alice.fs.promises.unlink('/sticky-alice/bob');
183
183
  });
@@ -211,6 +211,27 @@ suite('Permissions', config('permissions'), () => {
211
211
  assert.equal(stats.uid, 2000);
212
212
  });
213
213
 
214
+ test('changing metadata needs no read or write access, even on directories #327', async () => {
215
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
216
+
217
+ await rootFS.promises.writeFile('/metadata-file', 'x', { mode: 0o000 });
218
+ await rootFS.promises.mkdir('/metadata-dir', { mode: 0o000 });
219
+
220
+ for (const path of ['/metadata-file', '/metadata-dir']) {
221
+ rootFS.chownSync(path, 1000, 1000);
222
+ alice.fs.chmodSync(path, 0o500);
223
+ await alice.fs.promises.chmod(path, 0o400);
224
+ alice.fs.utimesSync(path, 1, 2);
225
+ await alice.fs.promises.utimes(path, 3, 4);
226
+ alice.fs.chownSync(path, -1, -1);
227
+ await alice.fs.promises.chown(path, -1, -1);
228
+
229
+ const stats = rootFS.statSync(path);
230
+ assert.equal(stats.mode & 0o777, 0o400);
231
+ assert.equal(stats.mtimeMs, 4000);
232
+ }
233
+ });
234
+
214
235
  test('moving a directory to another parent needs write permission on it', async () => {
215
236
  const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
216
237
 
@@ -249,8 +270,8 @@ suite('Permissions', config('permissions'), () => {
249
270
  alice.fs.chmodSync('/sid/file', 0o2755);
250
271
  assert.equal(modeOf('/sid/file'), 0o755);
251
272
 
252
- await alice.fs.promises.lchmod('/sid/dir', 0o6755);
253
- await alice.fs.promises.lchown('/sid/dir', -1, -1);
273
+ await alice.fs.promises.chmod('/sid/dir', 0o6755);
274
+ await alice.fs.promises.chown('/sid/dir', -1, -1);
254
275
  assert.equal(modeOf('/sid/dir'), 0o6755);
255
276
  });
256
277
 
@@ -271,7 +292,7 @@ suite('Permissions', config('permissions'), () => {
271
292
  const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
272
293
 
273
294
  await rootFS.promises.mkdir('/private', { mode: 0o700 });
274
- await rootFS.promises.lchown('/private', 1000, 1000);
295
+ await rootFS.promises.chown('/private', 1000, 1000);
275
296
  await rootFS.promises.mkdir('/private/inner', { mode: 0o755 });
276
297
  await rootFS.promises.writeFile('/private/inner/readable.txt', 'secret', { mode: 0o644 });
277
298
  await rootFS.promises.symlink('/private', '/private-link');
@@ -41,6 +41,20 @@ suite('Watch', config('watch'), () => {
41
41
  assert.equal(filename, 'test.txt');
42
42
  });
43
43
 
44
+ test('Events emitted on metadata change', async () => {
45
+ const { promise, resolve } = Promise.withResolvers<[string, string]>();
46
+
47
+ using watcher = fs.watch(testFile, (eventType, filename) => {
48
+ resolve([eventType, filename]);
49
+ });
50
+
51
+ fs.chmodSync(testFile, 0o600);
52
+
53
+ const [eventType, filename] = await withTimeout(promise);
54
+ assert.equal(eventType, 'change');
55
+ assert.equal(filename, 'test.txt');
56
+ });
57
+
44
58
  test('Events are emitted on delete', async () => {
45
59
  const { promise, resolve } = Promise.withResolvers<string>();
46
60
 
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "flags": {
3
- "prep": ["writeFile", "stream"]
3
+ "prep": ["writeFile", "stream"],
4
+ "sbfs_v2": [true, false]
4
5
  },
5
6
  "tests": [
6
7
  {
@@ -79,6 +80,35 @@
79
80
  { "name": "2000 appends", "value": { "writes": 2000 } },
80
81
  { "name": "8000 appends", "cpu": 1, "value": { "writes": 8000 } }
81
82
  ]
83
+ },
84
+ {
85
+ "path": "single-buffer-read.ts",
86
+ "name": "Random 4 KB reads from one file in a SingleBuffer",
87
+ "flags": ["sbfs_v2"],
88
+ "measure": {
89
+ "bytes": { "throughput": "s", "unit": "MB" },
90
+ "reads": { "cost": true, "unit": "read" }
91
+ },
92
+ "configurations": [
93
+ { "name": "1 MiB", "value": { "size": 1048576, "count": 256 } },
94
+ { "name": "8 MiB", "value": { "size": 8388608, "count": 1024 } },
95
+ { "name": "32 MiB", "cpu": 1, "value": { "size": 33554432, "count": 1024 } }
96
+ ]
97
+ },
98
+ {
99
+ "path": "single-buffer-write.ts",
100
+ "name": "Random 4 KB writes to one file in a SingleBuffer",
101
+ "flags": ["sbfs_v2"],
102
+ "measure": {
103
+ "bytes": { "throughput": "s", "unit": "MB" },
104
+ "writes": { "cost": true, "unit": "write" }
105
+ },
106
+ "configurations": [
107
+ { "name": "1 MiB, flushed", "value": { "size": 1048576, "count": 256, "flush": 1, "sync": 0 } },
108
+ { "name": "1 MiB, O_SYNC", "value": { "size": 1048576, "count": 256, "flush": 0, "sync": 1 } },
109
+ { "name": "8 MiB, flushed", "cpu": 1, "value": { "size": 8388608, "count": 1024, "flush": 1, "sync": 0 } },
110
+ { "name": "8 MiB, O_SYNC", "cpu": 1, "value": { "size": 8388608, "count": 1024, "flush": 0, "sync": 1 } }
111
+ ]
82
112
  }
83
113
  ]
84
114
  }
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // Helpers for random access to one file in a SingleBuffer.
3
+ import { configureSingle, SingleBuffer } from '@zenfs/core';
4
+ import { fs, KB, MB } from './common.ts';
5
+
6
+ export { fs };
7
+
8
+ export const block = 4 * KB;
9
+
10
+ export interface Config {
11
+ /** Size of the file */
12
+ size: number;
13
+
14
+ /** Number of blocks accessed, each once */
15
+ count: number;
16
+
17
+ sbfs_v2: boolean;
18
+ }
19
+
20
+ /** Mounts a SingleBuffer with room for the file twice over, then creates the file */
21
+ export async function mountWithFile(config: Config): Promise<void> {
22
+ await configureSingle({ backend: SingleBuffer, buffer: new ArrayBuffer(config.size * 2 + 4 * MB), v2: config.sbfs_v2 });
23
+ fs.writeFileSync('/f', new Uint8Array(config.size).fill(0x5a));
24
+ }
25
+
26
+ /** Offsets of `count` distinct blocks of the file, in a shuffled order that is the same on every run */
27
+ export function offsets(config: Config): number[] {
28
+ const blocks = Array.from({ length: config.size / block }, (_, i) => i);
29
+
30
+ let seed = 0x2545f491;
31
+ for (let i = blocks.length - 1; i > 0; i--) {
32
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
33
+ const j = seed % (i + 1);
34
+ [blocks[i], blocks[j]] = [blocks[j], blocks[i]];
35
+ }
36
+
37
+ return blocks.slice(0, config.count).map(i => i * block);
38
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // 4 KB reads at random offsets in one file in a SingleBuffer.
3
+ // The file is opened fresh for each iteration, so every read misses the vnode cache and reaches the store.
4
+ import { block, fs, mountWithFile, offsets, type Config } from './single-buffer-random.ts';
5
+
6
+ interface State {
7
+ offsets: number[];
8
+ target: Uint8Array;
9
+ fd?: number;
10
+ }
11
+
12
+ export async function setup(config: Config): Promise<State> {
13
+ await mountWithFile(config);
14
+ return { offsets: offsets(config), target: new Uint8Array(block) };
15
+ }
16
+
17
+ export function before(config: Config, state: State): void {
18
+ state.fd = fs.openSync('/f', 'r');
19
+ }
20
+
21
+ export function test(config: Config, state: State) {
22
+ for (const offset of state.offsets) fs.readSync(state.fd!, state.target, 0, block, offset);
23
+ return { reads: state.offsets.length, bytes: state.offsets.length * block };
24
+ }
25
+
26
+ export function after(config: Config, state: State): void {
27
+ fs.closeSync(state.fd!);
28
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ // 4 KB writes at random offsets in one file in a SingleBuffer.
3
+ // By default, only the writes are timed, and they stay in the vnode cache until the untimed close.
4
+ // With `flush`, the close is timed too, which writes the cached data to the store.
5
+ // With `osync`, the file is opened with O_SYNC, so every write reaches the store.
6
+ import { block, fs, mountWithFile, offsets, type Config as BaseConfig } from './single-buffer-random.ts';
7
+
8
+ interface Config extends BaseConfig {
9
+ flush: number;
10
+ sync: number;
11
+ }
12
+
13
+ interface State {
14
+ offsets: number[];
15
+ data: Uint8Array;
16
+ fd?: number;
17
+ }
18
+
19
+ export async function setup(config: Config): Promise<State> {
20
+ await mountWithFile(config);
21
+ return { offsets: offsets(config), data: new Uint8Array(block).fill(0xa5) };
22
+ }
23
+
24
+ export function before(config: Config, state: State): void {
25
+ state.fd = fs.openSync('/f', config.sync ? 'rs+' : 'r+');
26
+ }
27
+
28
+ export function test(config: Config, state: State) {
29
+ for (const offset of state.offsets) fs.writeSync(state.fd!, state.data, 0, block, offset);
30
+
31
+ if (config.flush) {
32
+ fs.closeSync(state.fd!);
33
+ state.fd = undefined;
34
+ }
35
+
36
+ return { writes: state.offsets.length, bytes: state.offsets.length * block };
37
+ }
38
+
39
+ export function after(config: Config, state: State): void {
40
+ if (state.fd !== undefined) fs.closeSync(state.fd);
41
+ }
@@ -0,0 +1,10 @@
1
+ import { SingleBuffer, configureSingle } from '@zenfs/core';
2
+ import { copySync, data } from '../setup.ts';
3
+
4
+ await configureSingle({
5
+ backend: SingleBuffer,
6
+ buffer: new ArrayBuffer(0x1100000),
7
+ v2: true,
8
+ });
9
+
10
+ copySync(data);
@@ -4,6 +4,7 @@ import { copySync, data } from '../setup.ts';
4
4
  await configureSingle({
5
5
  backend: SingleBuffer,
6
6
  buffer: new ArrayBuffer(0x1100000),
7
+ v2: false,
7
8
  });
8
9
 
9
10
  copySync(data);