@zenfs/core 2.7.5 → 2.7.7

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 (72) hide show
  1. package/dist/backends/backend.js +0 -2
  2. package/dist/backends/cow.d.ts +2 -2
  3. package/dist/backends/cow.js +6 -14
  4. package/dist/backends/passthrough.d.ts +0 -54
  5. package/dist/backends/passthrough.js +0 -54
  6. package/dist/backends/port.d.ts +2 -6
  7. package/dist/backends/port.js +3 -11
  8. package/dist/backends/store/fs.d.ts +0 -9
  9. package/dist/backends/store/fs.js +2 -24
  10. package/dist/backends/store/store.d.ts +1 -1
  11. package/dist/backends/store/store.js +2 -8
  12. package/dist/config.d.ts +7 -0
  13. package/dist/config.js +9 -3
  14. package/dist/internal/contexts.d.ts +1 -2
  15. package/dist/internal/credentials.d.ts +0 -3
  16. package/dist/internal/error.d.ts +5 -0
  17. package/dist/internal/error.js +10 -0
  18. package/dist/internal/file_index.js +0 -1
  19. package/dist/internal/index_fs.d.ts +2 -2
  20. package/dist/internal/index_fs.js +2 -2
  21. package/dist/internal/inode.d.ts +46 -43
  22. package/dist/internal/inode.js +75 -10
  23. package/dist/internal/rpc.d.ts +16 -7
  24. package/dist/internal/rpc.js +2 -11
  25. package/dist/mixins/async.js +22 -24
  26. package/dist/mixins/mutexed.d.ts +6 -41
  27. package/dist/mixins/mutexed.js +36 -109
  28. package/dist/mixins/sync.js +0 -2
  29. package/dist/node/async.js +2 -12
  30. package/dist/node/index.d.ts +1 -1
  31. package/dist/node/promises.d.ts +1 -6
  32. package/dist/node/promises.js +18 -22
  33. package/dist/node/stats.d.ts +25 -99
  34. package/dist/node/stats.js +22 -56
  35. package/dist/node/streams.d.ts +1 -37
  36. package/dist/node/sync.d.ts +1 -6
  37. package/dist/node/sync.js +16 -20
  38. package/dist/polyfills.js +25 -0
  39. package/dist/utils.d.ts +0 -4
  40. package/dist/vfs/acl.d.ts +1 -3
  41. package/dist/vfs/acl.js +1 -3
  42. package/dist/vfs/async.js +77 -69
  43. package/dist/vfs/config.d.ts +10 -1
  44. package/dist/vfs/config.js +5 -2
  45. package/dist/vfs/dir.d.ts +3 -0
  46. package/dist/vfs/dir.js +4 -1
  47. package/dist/vfs/file.d.ts +1 -8
  48. package/dist/vfs/file.js +11 -7
  49. package/dist/vfs/ioctl.js +2 -2
  50. package/dist/vfs/shared.d.ts +2 -0
  51. package/dist/vfs/shared.js +3 -4
  52. package/dist/vfs/sync.js +77 -61
  53. package/dist/vfs/vcache.d.ts +14 -14
  54. package/dist/vfs/vcache.js +65 -58
  55. package/dist/vfs/watchers.d.ts +1 -0
  56. package/dist/vfs/watchers.js +2 -4
  57. package/dist/vfs/xattr.js +18 -17
  58. package/package.json +2 -4
  59. package/scripts/make-index.js +1 -1
  60. package/tests/common/async.test.ts +35 -0
  61. package/tests/common/config.test.ts +1 -1
  62. package/tests/common/context.test.ts +1 -1
  63. package/tests/common/file_index.test.ts +1 -1
  64. package/tests/common/mounts.test.ts +12 -0
  65. package/tests/common/vcache.test.ts +13 -0
  66. package/tests/fs/concurrency.test.ts +3 -3
  67. package/tests/fs/links.test.ts +76 -0
  68. package/tests/fs/permissions.test.ts +232 -0
  69. package/tests/fs/stat.test.ts +1 -1
  70. package/tests/fs/streams.test.ts +1 -1
  71. package/tests/fs/xattr.test.ts +10 -0
  72. package/tests/common/mutex.test.ts +0 -61
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.7.5",
3
+ "version": "2.7.7",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -23,7 +23,6 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "tests",
26
- "types",
27
26
  "license.md",
28
27
  "eslint.shared.js"
29
28
  ],
@@ -68,7 +67,7 @@
68
67
  "spdx:fix": "npx lice -aw src tests/*.ts tests/**/*.test.ts",
69
68
  "lint": "eslint src tests",
70
69
  "test": "npx -s zenfs-test --clean; npx -s zenfs-test -abcpn; tests/fetch/run.sh; npx -s zenfs-test --report-only",
71
- "test:native": "npx -s zenfs-test tests/native-setup.ts --verbose",
70
+ "test:native": "npx -s zenfs-test -n tests/native-setup.ts --verbose",
72
71
  "test:perf": "zbench",
73
72
  "build": "tsc --build",
74
73
  "dev": "tsc --build --watch",
@@ -98,7 +97,6 @@
98
97
  "prettier": {
99
98
  "singleQuote": true,
100
99
  "useTabs": true,
101
- "trailingComma": "es5",
102
100
  "tabWidth": 4,
103
101
  "printWidth": 150,
104
102
  "arrowParens": "avoid",
@@ -94,7 +94,7 @@ function computeEntries(path) {
94
94
  else if (!seenDevs.has(stats.dev))
95
95
  if (options.xdev) {
96
96
  console.warn(
97
- `${styleText('yellowBright', `--xdev: Adding entries from device ${stats.dev} (${path}). You may get duplicate inos which MUST be de-duplicated manually.`)}`
97
+ `${styleText('yellowBright', `--xdev: Adding entries from device ${stats.dev} (${path}). You may get duplicate inos which MUST be de-duplicated manually.`)}`,
98
98
  );
99
99
  seenDevs.add(stats.dev);
100
100
  } else {
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { Async, InMemory, InMemoryStore, StoreFS } from '@zenfs/core';
3
+ import assert from 'node:assert/strict';
4
+ import { suite, test } from 'node:test';
5
+
6
+ class AsyncMemory extends Async(StoreFS) {
7
+ _sync = InMemory.create({ label: 'sync' });
8
+ }
9
+
10
+ suite('Async()', () => {
11
+ test('sync operations are applied to the sync cache once', async () => {
12
+ const fs = new AsyncMemory(new InMemoryStore());
13
+ fs.checkRootSync();
14
+ await fs.ready();
15
+
16
+ const calls: string[] = [];
17
+ for (const key of ['mkdirSync', 'rmdirSync', 'createFileSync', 'unlinkSync'] as const) {
18
+ const original = (fs._sync[key] as (...args: unknown[]) => unknown).bind(fs._sync);
19
+ (fs._sync as any)[key] = (...args: unknown[]) => {
20
+ calls.push(key);
21
+ return original(...args);
22
+ };
23
+ }
24
+
25
+ const options = { mode: 0o755, uid: 0, gid: 0 };
26
+ fs.mkdirSync('/dir', options);
27
+ fs.rmdirSync('/dir');
28
+ fs.createFileSync('/file', options);
29
+ fs.unlinkSync('/file');
30
+ await new Promise(resolve => setTimeout(resolve));
31
+
32
+ assert.deepEqual(calls, ['mkdirSync', 'rmdirSync', 'createFileSync', 'unlinkSync']);
33
+ assert.deepEqual(await fs.readdir('/'), []);
34
+ });
35
+ });
@@ -43,7 +43,7 @@ suite('Sync configuration', () => {
43
43
  configureSync({
44
44
  mounts: { '/': { backend: AsyncBackend } },
45
45
  }),
46
- { errno: Errno.EAGAIN }
46
+ { errno: Errno.EAGAIN },
47
47
  );
48
48
  });
49
49
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
- import { bindContext, configure, fs, InMemory, type BoundContext } from '@zenfs/core';
2
+ import { bindContext, configure, fs, InMemory } from '@zenfs/core';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
5
  import { canary } from 'utilium';
@@ -45,7 +45,7 @@ suite('Index', () => {
45
45
  const index = buildIndex(60_000);
46
46
  assert.equal(
47
47
  atDepth(1000, () => index._alloc()),
48
- 120_002
48
+ 120_002,
49
49
  );
50
50
  });
51
51
  });
@@ -19,6 +19,18 @@ suite('Mounts', () => {
19
19
  fs.rmSync('/nested', { recursive: true, force: true });
20
20
  });
21
21
 
22
+ test('lstat of a mount point is the mounted root #326', async () => {
23
+ await configure({ mounts: { '/mounted': InMemory } });
24
+
25
+ await fs.promises.utimes('/mounted', 1, 1);
26
+ assert.equal(fs.statSync('/mounted').mtimeMs, 1000);
27
+ assert.equal(fs.lstatSync('/mounted').mtimeMs, 1000);
28
+ assert.equal((await fs.promises.lstat('/mounted')).mtimeMs, 1000);
29
+
30
+ fs.umount('/mounted');
31
+ fs.rmdirSync('/mounted');
32
+ });
33
+
22
34
  test('Race conditions', async () => {
23
35
  await configure({
24
36
  mounts: {
@@ -0,0 +1,13 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { InMemory } from '@zenfs/core';
3
+ import { withExceptionContext } from '@zenfs/core/internal/error';
4
+ import { cacheOf } from '@zenfs/core/vfs/vcache';
5
+ import assert from 'node:assert/strict';
6
+ import { suite, test } from 'node:test';
7
+
8
+ suite('VCache', () => {
9
+ test('does not keep the exception context of the first caller', () => {
10
+ const fs = InMemory.create({ label: 'vcache' });
11
+ assert.equal(cacheOf(withExceptionContext(fs, { path: '/first' })).fs, fs);
12
+ });
13
+ });
@@ -67,7 +67,7 @@ suite('Concurrency', config('write', 'async'), () => {
67
67
  const rejected = results.filter(r => r.status === 'rejected');
68
68
  assert.deepEqual(
69
69
  rejected.map(r => r.reason.code ?? r.reason.message),
70
- []
70
+ [],
71
71
  );
72
72
 
73
73
  assert.equal(await fs.promises.readFile('/issue-303.txt', 'utf8'), contents);
@@ -96,7 +96,7 @@ suite('Concurrency', config('write', 'async'), () => {
96
96
  const rejected = results.filter(r => r.status == 'rejected');
97
97
  assert.deepEqual(
98
98
  rejected.map(r => r.reason.code ?? r.reason.message),
99
- []
99
+ [],
100
100
  );
101
101
 
102
102
  for (const path of paths) assert((await fs.promises.stat(path)).isDirectory());
@@ -107,7 +107,7 @@ suite('Concurrency', config('write', 'async'), () => {
107
107
 
108
108
  assert.deepEqual(
109
109
  results.filter(result => result !== undefined),
110
- ['/308-same']
110
+ ['/308-same'],
111
111
  );
112
112
  });
113
113
  });
@@ -18,6 +18,16 @@ suite('Links', config('symlinks'), () => {
18
18
  assert(stats.isSymbolicLink());
19
19
  });
20
20
 
21
+ test('symlinks have mode 0o777 #326', async () => {
22
+ assert.equal((await fs.promises.lstat(symlink)).mode & 0o7777, 0o777);
23
+
24
+ fs.symlinkSync(target, 'symlink-sync.js');
25
+ const stats = fs.lstatSync('symlink-sync.js');
26
+ assert(stats.isSymbolicLink());
27
+ assert.equal(stats.mode & 0o7777, 0o777);
28
+ fs.unlinkSync('symlink-sync.js');
29
+ });
30
+
21
31
  test('lstat file inside symlinked directory #241', async () => {
22
32
  await fs.promises.mkdir('/a');
23
33
  await fs.promises.writeFile('/a/hello.txt', 'hello world');
@@ -187,6 +197,72 @@ suite('Links', config('symlinks'), () => {
187
197
  assert(fs.lstatSync('/wfS-dangling').isSymbolicLink());
188
198
  });
189
199
 
200
+ test('operations follow symlinks in parent directories', config('async'), async () => {
201
+ await fs.promises.mkdir('/parent-real');
202
+ await fs.promises.symlink('/parent-real', '/parent-link');
203
+ await fs.promises.writeFile('/parent-real/unlink', 'x');
204
+ await fs.promises.writeFile('/parent-real/rename', 'x');
205
+ await fs.promises.mkdir('/parent-real/rmdir');
206
+ await fs.promises.symlink('/parent-real/renamed', '/parent-real/symlink');
207
+ await fs.promises.symlink('/parent-real/rmdir', '/parent-real/dir-link');
208
+
209
+ await fs.promises.unlink('/parent-link/unlink');
210
+ await fs.promises.rename('/parent-link/rename', '/parent-link/renamed');
211
+ assert.equal(await fs.promises.readlink('/parent-link/symlink'), '/parent-real/renamed');
212
+ await fs.promises.lutimes('/parent-link/symlink', 1, 1);
213
+ assert.equal((await fs.promises.lstat('/parent-real/symlink')).mtimeMs, 1000);
214
+
215
+ await assert.rejects(fs.promises.rmdir('/parent-link/dir-link'), { code: 'ENOTDIR' });
216
+ await fs.promises.unlink('/parent-link/dir-link');
217
+ await fs.promises.rmdir('/parent-link/rmdir');
218
+
219
+ assert.deepEqual((await fs.promises.readdir('/parent-real')).sort(), ['renamed', 'symlink']);
220
+ });
221
+
222
+ test('operations follow symlinks in parent directories (sync)', config('sync'), () => {
223
+ fs.mkdirSync('/parentS-real');
224
+ fs.symlinkSync('/parentS-real', '/parentS-link');
225
+ fs.writeFileSync('/parentS-real/unlink', 'x');
226
+ fs.writeFileSync('/parentS-real/rename', 'x');
227
+ fs.mkdirSync('/parentS-real/rmdir');
228
+ fs.symlinkSync('/parentS-real/renamed', '/parentS-real/symlink');
229
+ fs.symlinkSync('/parentS-real/rmdir', '/parentS-real/dir-link');
230
+
231
+ fs.unlinkSync('/parentS-link/unlink');
232
+ fs.renameSync('/parentS-link/rename', '/parentS-link/renamed');
233
+ assert.equal(fs.readlinkSync('/parentS-link/symlink'), '/parentS-real/renamed');
234
+ fs.lutimesSync('/parentS-link/symlink', 1, 1);
235
+ assert.equal(fs.lstatSync('/parentS-real/symlink').mtimeMs, 1000);
236
+
237
+ assert.throws(() => fs.rmdirSync('/parentS-link/dir-link'), { code: 'ENOTDIR' });
238
+ fs.unlinkSync('/parentS-link/dir-link');
239
+ fs.rmdirSync('/parentS-link/rmdir');
240
+
241
+ assert.deepEqual(fs.readdirSync('/parentS-real').sort(), ['renamed', 'symlink']);
242
+ });
243
+
244
+ test('link follows symlinks in parent directories', config('links'), async () => {
245
+ await fs.promises.link('/parent-link/renamed', '/parent-link/hard');
246
+ assert.equal(await fs.promises.readFile('/parent-real/hard', 'utf8'), 'x');
247
+ fs.linkSync('/parentS-link/renamed', '/parentS-link/hard');
248
+ assert.equal(fs.readFileSync('/parentS-real/hard', 'utf8'), 'x');
249
+ });
250
+
251
+ test('xattr follows symlinks unless noFollow is set', config('xattr'), async () => {
252
+ await fs.xattr.set('/parent-link/renamed', 'user.parent', 'async');
253
+ assert.equal(await fs.xattr.get('/parent-real/renamed', 'user.parent', { encoding: 'utf8' }), 'async');
254
+ fs.xattr.setSync('/parentS-link/renamed', 'user.parent', 'sync');
255
+ assert.equal(fs.xattr.getSync('/parentS-real/renamed', 'user.parent', { encoding: 'utf8' }), 'sync');
256
+
257
+ await fs.xattr.set('/parent-link/symlink', 'user.link', 'on link', { noFollow: true });
258
+ assert.equal(await fs.xattr.get('/parent-real/symlink', 'user.link', { encoding: 'utf8', noFollow: true }), 'on link');
259
+ await assert.rejects(fs.xattr.get('/parent-real/renamed', 'user.link'), { code: 'ENODATA' });
260
+ assert.deepEqual(await fs.xattr.list('/parent-link/symlink'), ['user.parent']);
261
+
262
+ await fs.xattr.remove('/parent-link/renamed', 'user.parent');
263
+ assert.deepEqual(fs.xattr.listSync('/parent-real/renamed'), []);
264
+ });
265
+
190
266
  test('cp preserves a symlink by default #304', async () => {
191
267
  await fs.promises.mkdir('/cp-src');
192
268
  await fs.promises.writeFile('/cp-src/real.txt', 'contents');
@@ -1,8 +1,10 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { bindContext, fs as rootFS } from '@zenfs/core';
2
3
  import { R_OK, W_OK, X_OK } from '@zenfs/core/constants';
3
4
  import { defaultContext } from '@zenfs/core/internal/contexts';
4
5
  import { hasAccess } from '@zenfs/core/internal/inode';
5
6
  import { join } from '@zenfs/core/path';
7
+ import * as vfsConfig from '@zenfs/core/vfs/config';
6
8
  import type { Exception } from 'kerium';
7
9
  import assert from 'node:assert/strict';
8
10
  import { suite, test } from 'node:test';
@@ -84,6 +86,236 @@ suite('Permissions', config('permissions'), () => {
84
86
  assert(hasAccess(defaultContext, stats, W_OK));
85
87
  }
86
88
 
89
+ test('unprivileged users can read files they do not own #326', async () => {
90
+ await rootFS.promises.writeFile('/root-owned.txt', 'shared', { mode: 0o644 });
91
+ const user = bindContext({ credentials: { uid: 1000, gid: 1000 } });
92
+
93
+ assert.equal(user.fs.readFileSync('/root-owned.txt', 'utf8'), 'shared');
94
+ assert.equal(await user.fs.promises.readFile('/root-owned.txt', 'utf8'), 'shared');
95
+
96
+ assert.throws(() => user.fs.writeFileSync('/root-owned.txt', 'nope'), { code: 'EACCES' });
97
+ await assert.rejects(user.fs.promises.writeFile('/root-owned.txt', 'nope'), { code: 'EACCES' });
98
+ });
99
+
100
+ test('access checks follow Linux #326', () => {
101
+ const as = (uid: number, gid: number, euid = uid, egid = gid, groups: number[] = []) =>
102
+ bindContext({ credentials: { uid, gid, euid, egid, groups } });
103
+ const file = (mode: number, uid = 1000, gid = 1000) => ({ mode: 0o100000 | mode, uid, gid });
104
+
105
+ assert(!hasAccess(as(1000, 0), file(0o600, 0, 0), R_OK));
106
+ assert(hasAccess(as(0, 1000), file(0o000, 5, 5), R_OK | W_OK));
107
+
108
+ assert(hasAccess(as(1000, 1000, 2000, 1000), file(0o600, 2000, 5), R_OK | W_OK));
109
+ assert(!hasAccess(as(2000, 1000, 1000, 1000), file(0o600, 2000, 5), R_OK));
110
+ assert(!hasAccess(as(1000, 0, 1000, 1000), file(0o060, 2000, 0), R_OK));
111
+
112
+ assert(!hasAccess(as(1000, 1000), file(0o077), R_OK));
113
+ assert(!hasAccess(as(1000, 1000), file(0o407, 2000), R_OK));
114
+ assert(hasAccess(as(1000, 3000), file(0o004, 2000, 2000), R_OK));
115
+ assert(hasAccess(as(1000, 3000, 1000, 3000, [2000]), file(0o040, 2000, 2000), R_OK));
116
+ assert(hasAccess(as(1000, 2000), file(0o021, 0, 2000), W_OK));
117
+ assert(!hasAccess(as(1000, 2000), file(0o021, 0, 2000), X_OK));
118
+ });
119
+
120
+ test('stat needs no permission on the file itself #326', async () => {
121
+ await rootFS.promises.writeFile('/secret-stat.txt', 'x', { mode: 0o000 });
122
+ const user = bindContext({ credentials: { uid: 1000, gid: 1000 } });
123
+
124
+ assert.equal(user.fs.statSync('/secret-stat.txt').size, 1);
125
+ assert.equal((await user.fs.promises.lstat('/secret-stat.txt')).size, 1);
126
+ assert.throws(() => user.fs.readFileSync('/secret-stat.txt'), { code: 'EACCES' });
127
+ });
128
+
129
+ test('removing an entry needs write and search permission on its parent #326', async () => {
130
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
131
+
132
+ await rootFS.promises.mkdir('/removal-root', { mode: 0o755 });
133
+ await rootFS.promises.writeFile('/removal-root/file', 'x', { mode: 0o666 });
134
+ await rootFS.promises.mkdir('/removal-root/dir', { mode: 0o777 });
135
+ await rootFS.promises.mkdir('/removal-own', { mode: 0o755 });
136
+ await rootFS.promises.lchown('/removal-own', 1000, 1000);
137
+ await rootFS.promises.writeFile('/removal-own/readonly', 'x', { mode: 0o444 });
138
+ await rootFS.promises.mkdir('/removal-own/readonly-dir', { mode: 0o555 });
139
+ await rootFS.promises.mkdir('/removal-unreadable', { mode: 0o300 });
140
+ await rootFS.promises.lchown('/removal-unreadable', 1000, 1000);
141
+ await rootFS.promises.writeFile('/removal-unreadable/file', 'x');
142
+
143
+ await assert.rejects(alice.fs.promises.unlink('/removal-root/file'), { code: 'EACCES' });
144
+ assert.throws(() => alice.fs.unlinkSync('/removal-root/file'), { code: 'EACCES' });
145
+ await assert.rejects(alice.fs.promises.rmdir('/removal-root/dir'), { code: 'EACCES' });
146
+ assert.throws(() => alice.fs.rmdirSync('/removal-root/dir'), { code: 'EACCES' });
147
+ assert.throws(() => alice.fs.renameSync('/removal-root/file', '/removal-own/file'), { code: 'EACCES' });
148
+ await assert.rejects(alice.fs.promises.rename('/removal-own/readonly', '/removal-root/file'), { code: 'EACCES' });
149
+
150
+ await alice.fs.promises.unlink('/removal-own/readonly');
151
+ alice.fs.rmdirSync('/removal-own/readonly-dir');
152
+ assert.deepEqual(rootFS.readdirSync('/removal-own'), []);
153
+
154
+ await alice.fs.promises.rename('/removal-unreadable/file', '/removal-unreadable/renamed');
155
+ alice.fs.renameSync('/removal-unreadable/renamed', '/removal-unreadable/file');
156
+ });
157
+
158
+ test('sticky directories only let owners remove entries #326', async () => {
159
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
160
+ const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
161
+
162
+ await rootFS.promises.mkdir('/sticky', { mode: 0o1777 });
163
+ await alice.fs.promises.writeFile('/sticky/alice', 'a', { mode: 0o666 });
164
+ await alice.fs.promises.mkdir('/sticky/alice-dir', { mode: 0o777 });
165
+ await bob.fs.promises.writeFile('/sticky/bob', 'b');
166
+
167
+ await assert.rejects(bob.fs.promises.unlink('/sticky/alice'), { code: 'EPERM' });
168
+ assert.throws(() => bob.fs.unlinkSync('/sticky/alice'), { code: 'EPERM' });
169
+ await assert.rejects(bob.fs.promises.rmdir('/sticky/alice-dir'), { code: 'EPERM' });
170
+ assert.throws(() => bob.fs.rmdirSync('/sticky/alice-dir'), { code: 'EPERM' });
171
+ assert.throws(() => bob.fs.renameSync('/sticky/alice', '/sticky/stolen'), { code: 'EPERM' });
172
+ await assert.rejects(alice.fs.promises.rename('/sticky/alice', '/sticky/bob'), { code: 'EPERM' });
173
+
174
+ await alice.fs.promises.unlink('/sticky/alice');
175
+ alice.fs.rmdirSync('/sticky/alice-dir');
176
+ await rootFS.promises.unlink('/sticky/bob');
177
+ assert.deepEqual(rootFS.readdirSync('/sticky'), []);
178
+
179
+ await rootFS.promises.mkdir('/sticky-alice', { mode: 0o1777 });
180
+ await rootFS.promises.lchown('/sticky-alice', 1000, 1000);
181
+ await bob.fs.promises.writeFile('/sticky-alice/bob', 'b');
182
+ await alice.fs.promises.unlink('/sticky-alice/bob');
183
+ });
184
+
185
+ test('only owners may change the mode, owner, or times of a file #326', async () => {
186
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000, groups: [3000] } });
187
+ const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
188
+
189
+ await rootFS.promises.writeFile('/owned', 'x', { mode: 0o666 });
190
+ rootFS.chownSync('/owned', 1000, 1000);
191
+
192
+ assert.throws(() => bob.fs.chmodSync('/owned', 0o600), { code: 'EPERM' });
193
+ await assert.rejects(bob.fs.promises.chmod('/owned', 0o600), { code: 'EPERM' });
194
+ assert.throws(() => bob.fs.utimesSync('/owned', 1, 2), { code: 'EPERM' });
195
+ await assert.rejects(bob.fs.promises.utimes('/owned', 1, 2), { code: 'EPERM' });
196
+ assert.throws(() => bob.fs.chownSync('/owned', -1, 2000), { code: 'EPERM' });
197
+ assert.throws(() => bob.fs.chownSync('/owned', 1000, -1), { code: 'EPERM' });
198
+ bob.fs.chownSync('/owned', -1, -1);
199
+
200
+ alice.fs.chmodSync('/owned', 0o644);
201
+ await alice.fs.promises.utimes('/owned', 1, 2);
202
+ alice.fs.chownSync('/owned', -1, 3000);
203
+ await alice.fs.promises.chown('/owned', 1000, 1000);
204
+ assert.throws(() => alice.fs.chownSync('/owned', -1, 4000), { code: 'EPERM' });
205
+ assert.throws(() => alice.fs.chownSync('/owned', 2000, -1), { code: 'EPERM' });
206
+
207
+ rootFS.chownSync('/owned', 2000, 2000);
208
+ const stats = rootFS.statSync('/owned');
209
+ assert.equal(stats.mode & 0o777, 0o644);
210
+ assert.equal(stats.mtimeMs, 2000);
211
+ assert.equal(stats.uid, 2000);
212
+ });
213
+
214
+ test('moving a directory to another parent needs write permission on it', async () => {
215
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
216
+
217
+ await rootFS.promises.mkdir('/move-a', { mode: 0o777 });
218
+ await rootFS.promises.mkdir('/move-b', { mode: 0o777 });
219
+ await rootFS.promises.mkdir('/move-a/dir', { mode: 0o755 });
220
+ await rootFS.promises.writeFile('/move-a/file', 'x', { mode: 0o644 });
221
+
222
+ assert.throws(() => alice.fs.renameSync('/move-a/dir', '/move-b/dir'), { code: 'EACCES' });
223
+ await assert.rejects(alice.fs.promises.rename('/move-a/dir', '/move-b/dir'), { code: 'EACCES' });
224
+ await alice.fs.promises.rename('/move-a/dir', '/move-a/renamed');
225
+ await alice.fs.promises.rename('/move-a/file', '/move-b/file');
226
+ });
227
+
228
+ test('chmod and chown clear setuid and setgid like Linux', async () => {
229
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000, groups: [3000] } });
230
+ const modeOf = (path: string) => rootFS.statSync(path).mode & 0o7777;
231
+
232
+ await rootFS.promises.mkdir('/sid', { mode: 0o777 });
233
+ await alice.fs.promises.writeFile('/sid/file', 'x');
234
+ await alice.fs.promises.mkdir('/sid/dir');
235
+
236
+ alice.fs.chmodSync('/sid/file', 0o6755);
237
+ assert.equal(modeOf('/sid/file'), 0o6755);
238
+ alice.fs.chownSync('/sid/file', -1, -1);
239
+ assert.equal(modeOf('/sid/file'), 0o755);
240
+
241
+ alice.fs.chmodSync('/sid/file', 0o2745);
242
+ alice.fs.chownSync('/sid/file', -1, 3000);
243
+ assert.equal(modeOf('/sid/file'), 0o2745);
244
+
245
+ rootFS.chmodSync('/sid/file', 0o4755);
246
+ rootFS.chownSync('/sid/file', 1000, 0);
247
+ assert.equal(modeOf('/sid/file'), 0o755);
248
+
249
+ alice.fs.chmodSync('/sid/file', 0o2755);
250
+ assert.equal(modeOf('/sid/file'), 0o755);
251
+
252
+ await alice.fs.promises.lchmod('/sid/dir', 0o6755);
253
+ await alice.fs.promises.lchown('/sid/dir', -1, -1);
254
+ assert.equal(modeOf('/sid/dir'), 0o6755);
255
+ });
256
+
257
+ test('access checks see unsynced changes to an open file', async () => {
258
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
259
+
260
+ await rootFS.promises.writeFile('/unsynced', 'x', { mode: 0o644 });
261
+ const fd = rootFS.openSync('/unsynced', 'r');
262
+ rootFS.fchmodSync(fd, 0o600);
263
+
264
+ assert.throws(() => alice.fs.readFileSync('/unsynced'), { code: 'EACCES' });
265
+ await assert.rejects(alice.fs.promises.readFile('/unsynced'), { code: 'EACCES' });
266
+ rootFS.closeSync(fd);
267
+ });
268
+
269
+ test('reaching a path needs search permission on every directory leading to it #326', async () => {
270
+ const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
271
+ const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
272
+
273
+ await rootFS.promises.mkdir('/private', { mode: 0o700 });
274
+ await rootFS.promises.lchown('/private', 1000, 1000);
275
+ await rootFS.promises.mkdir('/private/inner', { mode: 0o755 });
276
+ await rootFS.promises.writeFile('/private/inner/readable.txt', 'secret', { mode: 0o644 });
277
+ await rootFS.promises.symlink('/private', '/private-link');
278
+ await rootFS.promises.mkdir('/search-only', { mode: 0o711 });
279
+ await rootFS.promises.writeFile('/search-only/known.txt', 'known', { mode: 0o644 });
280
+ await rootFS.promises.mkdir('/no-search', { mode: 0o722 });
281
+
282
+ assert.throws(() => bob.fs.readFileSync('/private-link/inner/readable.txt'), { code: 'EACCES' });
283
+ await assert.rejects(bob.fs.promises.readFile('/private-link/inner/readable.txt'), { code: 'EACCES' });
284
+ assert.throws(() => bob.fs.lstatSync('/private/inner'), { code: 'EACCES' });
285
+ assert.throws(() => bob.fs.mkdirSync('/no-search/dir'), { code: 'EACCES' });
286
+ assert.throws(() => bob.fs.writeFileSync('/no-search/file', 'x'), { code: 'EACCES' });
287
+
288
+ const { checkAccess, resolveFullWalk } = vfsConfig;
289
+ vfsConfig._setVFSConfig({ checkAccess, resolveFullWalk: true });
290
+ try {
291
+ assert.equal(alice.fs.readFileSync('/private/inner/readable.txt', 'utf8'), 'secret');
292
+ assert.equal(rootFS.readFileSync('/private/inner/readable.txt', 'utf8'), 'secret');
293
+
294
+ assert.throws(() => bob.fs.readFileSync('/private/inner/readable.txt'), { code: 'EACCES' });
295
+ await assert.rejects(bob.fs.promises.readFile('/private/inner/readable.txt'), { code: 'EACCES' });
296
+ assert.throws(() => bob.fs.statSync('/private/inner/readable.txt'), { code: 'EACCES' });
297
+ await assert.rejects(bob.fs.promises.stat('/private/inner/readable.txt'), { code: 'EACCES' });
298
+ assert.throws(() => bob.fs.readdirSync('/private/inner'), { code: 'EACCES' });
299
+ assert.throws(() => bob.fs.writeFileSync('/private/inner/new.txt', 'x'), { code: 'EACCES' });
300
+ assert.throws(() => bob.fs.unlinkSync('/private/inner/readable.txt'), { code: 'EACCES' });
301
+ assert.throws(() => bob.fs.mkdirSync('/private/inner/dir'), { code: 'EACCES' });
302
+ assert.throws(() => bob.fs.renameSync('/private/inner/readable.txt', '/stolen.txt'), { code: 'EACCES' });
303
+ assert(rootFS.existsSync('/private/inner/readable.txt'));
304
+
305
+ assert.equal(bob.fs.statSync('/private').mode & 0o777, 0o700);
306
+ assert.throws(() => bob.fs.readdirSync('/private'), { code: 'EACCES' });
307
+
308
+ assert.equal(bob.fs.readFileSync('/search-only/known.txt', 'utf8'), 'known');
309
+ assert.throws(() => bob.fs.readdirSync('/search-only'), { code: 'EACCES' });
310
+
311
+ assert.equal(bob.fs.existsSync('/private/inner/readable.txt'), false);
312
+ assert.equal(await bob.fs.promises.exists('/private/inner/readable.txt'), false);
313
+ assert.equal(bob.fs.existsSync('/search-only/known.txt'), true);
314
+ } finally {
315
+ vfsConfig._setVFSConfig({ checkAccess, resolveFullWalk });
316
+ }
317
+ });
318
+
87
319
  const copy = { ...defaultContext.credentials };
88
320
  Object.assign(defaultContext.credentials, { uid: 1000, gid: 1000, euid: 1000, egid: 1000 });
89
321
  test('Access controls: /', () => test_item('/'));
@@ -63,7 +63,7 @@ suite('Stats', () => {
63
63
  assert.equal(hasAccess(defaultContext, stat, fs.constants.X_OK), false);
64
64
  // changing group
65
65
 
66
- Object.assign(defaultContext.credentials, { ...nonRootCredentials, gid: 44 });
66
+ Object.assign(defaultContext.credentials, { ...nonRootCredentials, gid: 44, egid: 44 });
67
67
 
68
68
  assert.equal(hasAccess(defaultContext, stat, fs.constants.R_OK), false);
69
69
  assert.equal(hasAccess(defaultContext, stat, fs.constants.W_OK), false);
@@ -167,7 +167,7 @@ suite('Streams', config('streams'), () => {
167
167
  assert.deepEqual(sizes.slice(0, -1), Array(sizes.length - 1).fill(highWaterMark));
168
168
  assert.equal(
169
169
  sizes.reduce((a, b) => a + b, 0),
170
- large.length
170
+ large.length,
171
171
  );
172
172
  });
173
173
 
@@ -22,6 +22,16 @@ suite('Extended Attributes', config('xattr'), () => {
22
22
  assert.equal(value, testValue);
23
23
  });
24
24
 
25
+ test('set is kept when an open handle with unsynced changes is synced', async () => {
26
+ await using handle = await fs.promises.open(testFile, 'r');
27
+ await handle.chmod(0o640);
28
+ await fs.xattr.set(testFile, 'user.unsynced', 'value');
29
+ await handle.sync();
30
+
31
+ assert.equal(await fs.xattr.get(testFile, 'user.unsynced', { encoding: 'utf8' }), 'value');
32
+ assert.equal((await fs.promises.stat(testFile)).mode & 0o777, 0o640);
33
+ });
34
+
25
35
  test('get attributes with buffer encoding', async () => {
26
36
  await fs.xattr.set(testFile, 'user.buffer', 'buffer value');
27
37
  const buffer = await fs.xattr.get(testFile, 'user.buffer', { encoding: 'buffer' });
@@ -1,61 +0,0 @@
1
- // SPDX-License-Identifier: LGPL-3.0-or-later
2
- import { InMemoryStore, Mutexed, StoreFS } from '@zenfs/core';
3
- import assert from 'node:assert/strict';
4
- import { suite, test } from 'node:test';
5
- import { wait } from 'utilium';
6
-
7
- suite('Mutexed FS', () => {
8
- const fs = new (Mutexed(StoreFS))(new InMemoryStore(0x10000, 'test'));
9
- fs._fs.checkRootSync();
10
-
11
- test('lock/unlock', () => {
12
- const lock = fs.lockSync();
13
- assert(fs.isLocked);
14
- lock.unlock();
15
- assert(!fs.isLocked);
16
- });
17
-
18
- test('queueing multiple locks', async () => {
19
- let lock1Resolved = false;
20
- let lock2Resolved = false;
21
-
22
- const lock1 = fs.lock(100).then(lock => {
23
- lock1Resolved = true;
24
- lock.unlock();
25
- });
26
- const lock2 = fs.lock(100).then(lock => {
27
- lock2Resolved = true;
28
- lock.unlock();
29
- });
30
-
31
- // Both locks are queued, so neither should be resolved initially
32
- assert(!lock1Resolved);
33
- assert(!lock2Resolved);
34
-
35
- // Wait for the first lock to be resolved
36
- await lock1;
37
-
38
- assert(lock1Resolved);
39
- assert(!lock2Resolved);
40
-
41
- // Wait for the second lock to be resolved
42
- await lock2;
43
-
44
- assert(lock1Resolved);
45
- assert(lock2Resolved);
46
- });
47
-
48
- test('test race conditions', async () => {
49
- let x = 1;
50
-
51
- async function foo() {
52
- const lock = await fs.lock(100);
53
- await wait(25);
54
- x++;
55
- lock.unlock();
56
- }
57
-
58
- await Promise.all([foo(), foo(), foo()]);
59
- assert.equal(x, 4);
60
- });
61
- });