@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/dist/vfs/sync.js CHANGED
@@ -55,11 +55,11 @@ import { decodeUTF8 } from 'utilium';
55
55
  import * as constants from '../constants.js';
56
56
  import { contextOf } from '../internal/contexts.js';
57
57
  import { wrap } from '../internal/error.js';
58
- import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
58
+ import { assertRemovable, assertSearchable, hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
59
59
  import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
60
60
  import { normalizeMode, normalizePath } from '../utils.js';
61
- import { cacheOf, lockPathSync } from './vcache.js';
62
- import { checkAccess } from './config.js';
61
+ import { cacheOf } from './vcache.js';
62
+ import { checkAccess, resolveFullWalk } from './config.js';
63
63
  import { Dirent, ifToDt } from './dir.js';
64
64
  import { Handle } from './file.js';
65
65
  import * as flags from './flags.js';
@@ -75,35 +75,45 @@ export function resolve($, path, preserveSymlinks, extra) {
75
75
  path = resolvePath.call($, path);
76
76
  /* Try to resolve it directly. If this works,
77
77
  that means we don't need to perform any resolution for parent directories. */
78
- try {
79
- const resolved = resolveMount(path, $);
80
- // Stat it to make sure it exists. The vnode cache takes precedence since it may have unsynced changes
81
- const stats = cacheOf(resolved.fs).get(resolved.path)?.inode ?? resolved.fs.statSync(resolved.path);
82
- if (!isSymbolicLink(stats) || preserveSymlinks) {
83
- return { ...resolved, fullPath: path, stats };
78
+ if (!resolveFullWalk)
79
+ try {
80
+ const resolved = resolveMount(path, $);
81
+ // Stat it to make sure it exists. The vnode cache takes precedence since it may have unsynced changes
82
+ const stats = resolved.cache.statSync(resolved.path);
83
+ if (!isSymbolicLink(stats) || preserveSymlinks) {
84
+ return { ...resolved, fullPath: path, stats };
85
+ }
86
+ const target = resolvePath.call($, dirname(path), readlink.call($, path));
87
+ return resolve($, target, preserveSymlinks, extra);
88
+ }
89
+ catch {
90
+ // Go the long way
84
91
  }
85
- const target = resolvePath.call($, dirname(path), readlink.call($, path));
86
- return resolve($, target, preserveSymlinks, extra);
92
+ const { base, dir } = parse(path);
93
+ const $ex = { syscall: 'stat', path, ...extra };
94
+ let realDir = '/';
95
+ if (dir != '/') {
96
+ const parent = resolve($, dir, false, extra);
97
+ if (parent.stats && isDirectory(parent.stats))
98
+ assertSearchable($, parent.stats, $ex);
99
+ realDir = parent.fullPath;
87
100
  }
88
- catch (e) {
89
- setUVMessage(Object.assign(e, { syscall: 'stat', path, ...extra }));
90
- if (preserveSymlinks)
91
- throw e;
101
+ else if (base && checkAccess) {
102
+ const root = resolveMount('/', $);
103
+ assertSearchable($, root.cache.statSync(root.path), $ex);
92
104
  }
93
- const { base, dir } = parse(path);
94
- const realDir = dir == '/' ? '/' : resolve($, dir, false, extra).fullPath;
95
105
  const maybePath = join(realDir, base);
96
106
  const resolved = resolveMount(maybePath, $);
97
107
  let stats;
98
108
  try {
99
- stats = cacheOf(resolved.fs).get(resolved.path)?.inode ?? resolved.fs.statSync(resolved.path);
109
+ stats = resolved.cache.statSync(resolved.path);
100
110
  }
101
111
  catch (e) {
102
112
  if (e.code === 'ENOENT')
103
113
  return { ...resolved, fullPath: maybePath };
104
114
  throw setUVMessage(Object.assign(e, { syscall: 'stat', path: maybePath, ...extra }));
105
115
  }
106
- if (!isSymbolicLink(stats)) {
116
+ if (!isSymbolicLink(stats) || preserveSymlinks) {
107
117
  return { ...resolved, fullPath: maybePath, stats };
108
118
  }
109
119
  const target = resolvePath.call($, realDir, readlink.call($, maybePath));
@@ -116,11 +126,11 @@ export function resolve($, path, preserveSymlinks, extra) {
116
126
  export function open($, path, opt) {
117
127
  path = normalizePath.call($, path);
118
128
  const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag);
119
- path = opt.preserveSymlinks ? path : resolve($, path).fullPath;
120
- const { fs, path: resolved } = resolveMount(path, $);
129
+ path = resolve($, path, opt.preserveSymlinks).fullPath;
130
+ const { fs, cache, path: resolved } = resolveMount(path, $);
121
131
  let stats;
122
132
  try {
123
- stats = fs.statSync(resolved);
133
+ stats = cache.statSync(resolved);
124
134
  }
125
135
  catch {
126
136
  // nothing
@@ -133,8 +143,8 @@ export function open($, path, opt) {
133
143
  }
134
144
  // Create the file
135
145
  const parentPath = dirname(resolved);
136
- const parentStats = fs.statSync(parentPath);
137
- if (checkAccess && !hasAccess($, parentStats, constants.W_OK)) {
146
+ const parentStats = cache.statSync(parentPath, { syscall: 'open', path });
147
+ if (checkAccess && !hasAccess($, parentStats, constants.W_OK | constants.X_OK)) {
138
148
  throw UV('EACCES', 'open', path);
139
149
  }
140
150
  if (!isDirectory(parentStats)) {
@@ -143,7 +153,7 @@ export function open($, path, opt) {
143
153
  if (!opt.allowDirectory && isDirectory({ mode }))
144
154
  throw UV('EISDIR', 'open', path);
145
155
  // Serialize entry creation with other operations on the parent directory
146
- const _ = __addDisposableResource(env_1, lockPathSync(fs, parentPath, 'rw', parentStats), false);
156
+ const _ = __addDisposableResource(env_1, cache.lockSync(parentPath, 'rw', parentStats), false);
147
157
  const { euid: uid, egid: gid } = contextOf($).credentials;
148
158
  const inode = fs.createFileSync(resolved, {
149
159
  mode,
@@ -152,7 +162,7 @@ export function open($, path, opt) {
152
162
  });
153
163
  // A new entry in the parent directory, which is a 'rename' event
154
164
  emitChange($, 'rename', path);
155
- return new Handle($, path, resolved, flag, cacheOf(fs).ref(resolved, inode));
165
+ return new Handle($, path, resolved, flag, cache.ref(resolved, inode));
156
166
  }
157
167
  catch (e_1) {
158
168
  env_1.error = e_1;
@@ -162,14 +172,14 @@ export function open($, path, opt) {
162
172
  __disposeResources(env_1);
163
173
  }
164
174
  }
165
- if (checkAccess && (!hasAccess($, stats, mode) || !hasAccess($, stats, flags.toMode(flag)))) {
175
+ if (checkAccess && !hasAccess($, stats, flags.toMode(flag))) {
166
176
  throw UV('EACCES', 'open', path);
167
177
  }
168
178
  if (flag & constants.O_EXCL)
169
179
  throw UV('EEXIST', 'open', path);
170
180
  if (!opt.allowDirectory && isDirectory(stats))
171
181
  throw UV('EISDIR', 'open', path);
172
- const file = new Handle($, path, resolved, flag, cacheOf(fs).ref(resolved, stats));
182
+ const file = new Handle($, path, resolved, flag, cache.ref(resolved, stats));
173
183
  if (flag & constants.O_TRUNC)
174
184
  file.truncateSync(0);
175
185
  return file;
@@ -192,7 +202,7 @@ export function mkdir(path, options = {}) {
192
202
  const original = normalizePath.call(this, path);
193
203
  const $ex = { syscall: 'mkdir', path: original };
194
204
  const { fullPath: realParent } = resolve(this, dirname(original), false, $ex);
195
- const { fs, path: target } = resolveMount(join(realParent, basename(original)), this, $ex);
205
+ const { fs, cache, path: target } = resolveMount(join(realParent, basename(original)), this, $ex);
196
206
  const followed = realParent != dirname(original);
197
207
  const { euid: uid, egid: gid } = contextOf(this).credentials;
198
208
  const { mode = 0o777, recursive } = options;
@@ -201,11 +211,11 @@ export function mkdir(path, options = {}) {
201
211
  const env_2 = { stack: [], error: void 0, hasError: false };
202
212
  try {
203
213
  const parentPath = dirname(resolved);
204
- const parent = recursive && parentPath != '/' ? __create(dirname(path), parentPath) : fs.statSync(parentPath);
214
+ const parent = recursive && parentPath != '/' ? __create(dirname(path), parentPath) : cache.statSync(parentPath, $ex);
205
215
  if (recursive) {
206
216
  let existing;
207
217
  try {
208
- existing = cacheOf(fs).get(resolved)?.inode ?? fs.statSync(resolved);
218
+ existing = cache.statSync(resolved);
209
219
  }
210
220
  catch {
211
221
  // It doesn't exist yet
@@ -221,9 +231,9 @@ export function mkdir(path, options = {}) {
221
231
  if (followed && fs.existsSync(join(parentPath, basename(path))))
222
232
  throw UV('ENOENT', $ex);
223
233
  }
224
- if (checkAccess && !hasAccess(this, parent, constants.W_OK))
234
+ if (checkAccess && !hasAccess(this, parent, constants.W_OK | constants.X_OK))
225
235
  throw UV('EACCES', 'mkdir', path);
226
- const _ = __addDisposableResource(env_2, lockPathSync(fs, parentPath, 'rw', parent), false);
236
+ const _ = __addDisposableResource(env_2, cache.lockSync(parentPath, 'rw', parent), false);
227
237
  const inode = wrap(fs, 'mkdirSync', { path, syscall: 'mkdir' })(resolved, {
228
238
  mode,
229
239
  uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
@@ -247,9 +257,9 @@ export function mkdir(path, options = {}) {
247
257
  }
248
258
  export function readdir(path, options = {}) {
249
259
  path = normalizePath.call(this, path);
250
- const { fs, path: resolved } = resolve(this, path);
260
+ const { fs, cache, path: resolved } = resolve(this, path);
251
261
  // Node reports `readdir` failures as `scandir`
252
- const stats = cacheOf(fs).get(resolved)?.inode ?? wrap(fs, 'statSync', { path, syscall: 'scandir' })(resolved);
262
+ const stats = cache.statSync(resolved, { path, syscall: 'scandir' });
253
263
  if (checkAccess && !hasAccess(this, stats, constants.R_OK))
254
264
  throw UV('EACCES', 'scandir', path);
255
265
  if (!isDirectory(stats))
@@ -258,7 +268,7 @@ export function readdir(path, options = {}) {
258
268
  {
259
269
  const env_3 = { stack: [], error: void 0, hasError: false };
260
270
  try {
261
- const _ = __addDisposableResource(env_3, lockPathSync(fs, resolved, 'ro', stats), false);
271
+ const _ = __addDisposableResource(env_3, cache.lockSync(resolved, 'ro', stats), false);
262
272
  entries = fs.readdirSync(resolved);
263
273
  }
264
274
  catch (e_3) {
@@ -269,12 +279,11 @@ export function readdir(path, options = {}) {
269
279
  __disposeResources(env_3);
270
280
  }
271
281
  }
272
- // Iterate over entries and handle recursive case if needed
273
282
  const values = [];
274
283
  const addEntry = (entry) => {
275
284
  let entryStat;
276
285
  try {
277
- entryStat = fs.statSync(join(resolved, entry));
286
+ entryStat = cache.statSync(join(resolved, entry), { syscall: 'scandir', path });
278
287
  }
279
288
  catch (e) {
280
289
  if (e.code == 'ENOENT')
@@ -304,7 +313,7 @@ export function rename(oldPath, newPath) {
304
313
  newPath = normalizePath.call(this, newPath);
305
314
  const $ex = { syscall: 'rename', path: oldPath, dest: newPath };
306
315
  const src = resolve(this, oldPath, true, $ex);
307
- const dst = resolveMount(newPath, this, $ex);
316
+ const dst = resolve(this, newPath, true, $ex);
308
317
  if (src.fs.uuid !== dst.fs.uuid)
309
318
  throw UV('EXDEV', $ex);
310
319
  // A directory can not be moved inside itself
@@ -312,25 +321,29 @@ export function rename(oldPath, newPath) {
312
321
  throw UV('EINVAL', $ex);
313
322
  if (!src.stats)
314
323
  throw UV('ENOENT', $ex);
315
- const fs = src.fs;
316
- const oldParent = fs.statSync(dirname(src.path));
317
- const newParent = fs.statSync(dirname(dst.path));
324
+ const srcDir = dirname(src.path);
325
+ const dstDir = dirname(dst.path);
326
+ const oldParent = src.cache.statSync(srcDir, $ex);
327
+ const newParent = src.cache.statSync(dstDir, $ex);
318
328
  let newStats;
319
329
  try {
320
- newStats = fs.statSync(dst.path);
330
+ newStats = src.cache.statSync(dst.path, $ex);
321
331
  }
322
332
  catch (e) {
323
333
  if (e.code != 'ENOENT')
324
334
  throw e;
325
335
  }
326
- if (checkAccess && (!hasAccess(this, oldParent, constants.R_OK) || !hasAccess(this, newParent, constants.W_OK)))
336
+ assertRemovable(this, oldParent, src.stats, $ex);
337
+ if (newStats)
338
+ assertRemovable(this, newParent, newStats, $ex);
339
+ if (checkAccess && !hasAccess(this, newParent, constants.W_OK | constants.X_OK))
340
+ throw UV('EACCES', $ex);
341
+ if (checkAccess && isDirectory(src.stats) && srcDir != dstDir && !hasAccess(this, src.stats, constants.W_OK))
327
342
  throw UV('EACCES', $ex);
328
343
  if (newStats && !isDirectory(src.stats) && isDirectory(newStats))
329
344
  throw UV('EISDIR', $ex);
330
345
  if (newStats && isDirectory(src.stats) && !isDirectory(newStats))
331
346
  throw UV('ENOTDIR', $ex);
332
- const srcDir = dirname(src.path);
333
- const dstDir = dirname(dst.path);
334
347
  // Lock both parent directories, ordered by inode number to avoid ABBA deadlocks (like Linux's `lock_two_nondirectories`)
335
348
  const parents = [
336
349
  [srcDir, oldParent],
@@ -338,10 +351,10 @@ export function rename(oldPath, newPath) {
338
351
  ];
339
352
  if (oldParent.ino > newParent.ino)
340
353
  parents.reverse();
341
- const _first = __addDisposableResource(env_4, lockPathSync(fs, parents[0][0], 'rw', parents[0][1]), false);
342
- const _second = __addDisposableResource(env_4, oldParent.ino == newParent.ino ? null : lockPathSync(fs, parents[1][0], 'rw', parents[1][1]), false);
354
+ const _first = __addDisposableResource(env_4, src.cache.lockSync(parents[0][0], 'rw', parents[0][1]), false);
355
+ const _second = __addDisposableResource(env_4, oldParent.ino == newParent.ino ? null : src.cache.lockSync(parents[1][0], 'rw', parents[1][1]), false);
343
356
  src.fs.renameSync(src.path, dst.path);
344
- cacheOf(fs).rename(src.path, dst.path);
357
+ src.cache.rename(src.path, dst.path);
345
358
  // Both names change which entries exist, so both are 'rename' events
346
359
  emitChange(this, 'rename', oldPath);
347
360
  emitChange(this, 'rename', newPath);
@@ -360,24 +373,24 @@ export function link(target, link) {
360
373
  target = normalizePath.call(this, target);
361
374
  link = normalizePath.call(this, link);
362
375
  const $ex = { syscall: 'link', path: target, dest: link };
363
- const { fs, path: resolved } = resolveMount(target, this, $ex);
364
- const dst = resolveMount(link, this, $ex);
376
+ const { fs, cache, path: resolved } = resolve(this, target, true, $ex);
377
+ const dst = resolve(this, link, true, $ex);
365
378
  if (fs.uuid !== dst.fs.uuid)
366
379
  throw UV('EXDEV', $ex);
367
- const stats = fs.statSync(resolved);
380
+ const stats = cache.statSync(resolved, $ex);
368
381
  if (checkAccess) {
369
382
  if (!hasAccess(this, stats, constants.R_OK))
370
383
  throw UV('EACCES', $ex);
371
- const dirStats = fs.statSync(dirname(resolved));
384
+ const dirStats = cache.statSync(dirname(resolved), $ex);
372
385
  if (!hasAccess(this, dirStats, constants.R_OK))
373
386
  throw UV('EACCES', $ex);
374
- const destStats = fs.statSync(dirname(dst.path));
387
+ const destStats = cache.statSync(dirname(dst.path), $ex);
375
388
  if (!hasAccess(this, destStats, constants.W_OK))
376
389
  throw UV('EACCES', $ex);
377
390
  }
378
- const _ = __addDisposableResource(env_5, lockPathSync(fs, dirname(dst.path), 'rw'), false);
391
+ const _ = __addDisposableResource(env_5, cache.lockSync(dirname(dst.path), 'rw'), false);
379
392
  fs.linkSync(resolved, dst.path);
380
- cacheOf(fs).link(resolved, dst.path);
393
+ cache.link(resolved, dst.path);
381
394
  }
382
395
  catch (e_5) {
383
396
  env_5.error = e_5;
@@ -395,10 +408,15 @@ export function stat(path, lstat) {
395
408
  stats = resolve(this, path, false, extra).stats;
396
409
  else {
397
410
  const { base, dir } = parse(path);
398
- const { fs, path: parent } = resolve(this, dir, false, extra);
411
+ const parent = resolve(this, dir, false, extra);
412
+ if (parent.stats && isDirectory(parent.stats))
413
+ assertSearchable(this, parent.stats, extra);
414
+ const { root, mounts } = contextOf(this);
415
+ const mounted = base && mounts.get(join(root, parent.fullPath, base));
416
+ const fs = mounted || parent.fs;
417
+ const target = mounted ? '/' : join(parent.path, base);
399
418
  try {
400
- const target = base ? join(parent, base) : parent;
401
- stats = cacheOf(fs).get(target)?.inode ?? fs.statSync(target);
419
+ stats = cacheOf(fs).statSync(target);
402
420
  }
403
421
  catch (e) {
404
422
  setUVMessage(Object.assign(e, extra));
@@ -407,7 +425,5 @@ export function stat(path, lstat) {
407
425
  }
408
426
  if (!stats)
409
427
  throw UV('ENOENT', extra);
410
- if (checkAccess && !hasAccess(this, stats, constants.R_OK))
411
- throw UV('EACCES', extra);
412
428
  return stats;
413
429
  }
@@ -1,6 +1,8 @@
1
1
  import type { LockMode, LockRelease } from 'kerium/locks';
2
+ import { type ExceptionExtra } from 'kerium';
2
3
  import type { FileSystem } from '../internal/filesystem.js';
3
4
  import type { InodeLike } from '../internal/inode.js';
5
+ import '../polyfills.js';
4
6
  import { VNode } from './vnode.js';
5
7
  /**
6
8
  * The VFS-level cache of vnodes for a single file system. Modeled after Linux's dcache/icache.
@@ -35,6 +37,18 @@ export declare class VCache {
35
37
  sync(): Promise<void>;
36
38
  /** Sync all dirty vnodes to the backend synchronously */
37
39
  syncSync(): void;
40
+ /** Get the inode for a path, preferring the cached vnode since it may have unsynced changes */
41
+ stat(path: string, extra?: ExceptionExtra): Promise<InodeLike>;
42
+ /** Synchronous version of {@link stat} */
43
+ statSync(path: string, extra?: ExceptionExtra): InodeLike;
44
+ /**
45
+ * Ref and lock the vnode for a path, usually a parent directory during a namespace operation.
46
+ * The returned release function unlocks and unrefs the vnode.
47
+ * If `inode` is not provided and the vnode is not cached, the backend is `stat`ed.
48
+ */
49
+ lock(path: string, mode: LockMode, inode?: InodeLike): Promise<LockRelease>;
50
+ /** Synchronous version of {@link lock} */
51
+ lockSync(path: string, mode: LockMode, inode?: InodeLike): LockRelease;
38
52
  }
39
53
  /**
40
54
  * All vnode caches, keyed by file system UUID.
@@ -50,17 +64,3 @@ export declare const caches: Map<`${string}-${string}-${string}-${string}-${stri
50
64
  * @internal
51
65
  */
52
66
  export declare function cacheOf(fs: FileSystem): VCache;
53
- /**
54
- * Ref and lock the vnode for a path, usually a parent directory during a namespace operation.
55
- * The returned release function unlocks and unrefs the vnode.
56
- * If `inode` is not provided and the vnode is not cached, the backend is `stat`ed.
57
- * @category VFS
58
- * @internal
59
- */
60
- export declare function lockPath(fs: FileSystem, path: string, mode: LockMode, inode?: InodeLike): Promise<LockRelease>;
61
- /**
62
- * Synchronous version of `lockPath`.
63
- * @category VFS
64
- * @internal
65
- */
66
- export declare function lockPathSync(fs: FileSystem, path: string, mode: LockMode, inode?: InodeLike): LockRelease;
@@ -50,7 +50,10 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
+ import { setUVMessage } from 'kerium';
53
54
  import { err } from 'kerium/log';
55
+ import { withoutExceptionContext } from '../internal/error.js';
56
+ import '../polyfills.js';
54
57
  import { VNode } from './vnode.js';
55
58
  /**
56
59
  * The VFS-level cache of vnodes for a single file system. Modeled after Linux's dcache/icache.
@@ -85,7 +88,6 @@ export class VCache {
85
88
  }
86
89
  else {
87
90
  node.paths.add(path);
88
- // The vnode's inode is authoritative when dirty; otherwise refresh it with the newer stats
89
91
  if (!node.dirty && inode !== node.inode)
90
92
  Object.assign(node.inode, inode);
91
93
  }
@@ -177,6 +179,67 @@ export class VCache {
177
179
  }
178
180
  }
179
181
  }
182
+ /** Get the inode for a path, preferring the cached vnode since it may have unsynced changes */
183
+ async stat(path, extra) {
184
+ try {
185
+ return this.get(path)?.inode ?? (await this.fs.stat(path));
186
+ }
187
+ catch (e) {
188
+ throw extra ? setUVMessage(Object.assign(e, extra)) : e;
189
+ }
190
+ }
191
+ /** Synchronous version of {@link stat} */
192
+ statSync(path, extra) {
193
+ try {
194
+ return this.get(path)?.inode ?? this.fs.statSync(path);
195
+ }
196
+ catch (e) {
197
+ throw extra ? setUVMessage(Object.assign(e, extra)) : e;
198
+ }
199
+ }
200
+ /**
201
+ * Ref and lock the vnode for a path, usually a parent directory during a namespace operation.
202
+ * The returned release function unlocks and unrefs the vnode.
203
+ * If `inode` is not provided and the vnode is not cached, the backend is `stat`ed.
204
+ */
205
+ async lock(path, mode, inode) {
206
+ inode ??= await this.stat(path);
207
+ const node = this.ref(path, inode);
208
+ const unlock = await node.lock(mode);
209
+ let released = false;
210
+ const release = () => {
211
+ if (released)
212
+ return;
213
+ released = true;
214
+ unlock();
215
+ this.unref(node);
216
+ };
217
+ release[Symbol.dispose] = release;
218
+ return release;
219
+ }
220
+ /** Synchronous version of {@link lock} */
221
+ lockSync(path, mode, inode) {
222
+ inode ??= this.statSync(path);
223
+ const node = this.ref(path, inode);
224
+ let unlock;
225
+ try {
226
+ unlock = node.lockSync(mode);
227
+ }
228
+ catch (e) {
229
+ this.unref(node);
230
+ throw e;
231
+ }
232
+ let released = false;
233
+ const release = () => {
234
+ if (released)
235
+ return;
236
+ released = true;
237
+ unlock();
238
+ this.unref(node);
239
+ };
240
+ release[Symbol.dispose] = release;
241
+ return release;
242
+ }
180
243
  }
181
244
  /**
182
245
  * All vnode caches, keyed by file system UUID.
@@ -192,61 +255,5 @@ export const caches = new Map();
192
255
  * @internal
193
256
  */
194
257
  export function cacheOf(fs) {
195
- let cache = caches.get(fs.uuid);
196
- if (!cache) {
197
- cache = new VCache(fs);
198
- caches.set(fs.uuid, cache);
199
- }
200
- return cache;
201
- }
202
- /**
203
- * Ref and lock the vnode for a path, usually a parent directory during a namespace operation.
204
- * The returned release function unlocks and unrefs the vnode.
205
- * If `inode` is not provided and the vnode is not cached, the backend is `stat`ed.
206
- * @category VFS
207
- * @internal
208
- */
209
- export async function lockPath(fs, path, mode, inode) {
210
- const cache = cacheOf(fs);
211
- inode ??= cache.get(path)?.inode ?? (await fs.stat(path));
212
- const node = cache.ref(path, inode);
213
- const unlock = await node.lock(mode);
214
- let released = false;
215
- const release = () => {
216
- if (released)
217
- return;
218
- released = true;
219
- unlock();
220
- cache.unref(node);
221
- };
222
- release[Symbol.dispose] = release;
223
- return release;
224
- }
225
- /**
226
- * Synchronous version of `lockPath`.
227
- * @category VFS
228
- * @internal
229
- */
230
- export function lockPathSync(fs, path, mode, inode) {
231
- const cache = cacheOf(fs);
232
- inode ??= cache.get(path)?.inode ?? fs.statSync(path);
233
- const node = cache.ref(path, inode);
234
- let unlock;
235
- try {
236
- unlock = node.lockSync(mode);
237
- }
238
- catch (e) {
239
- cache.unref(node);
240
- throw e;
241
- }
242
- let released = false;
243
- const release = () => {
244
- if (released)
245
- return;
246
- released = true;
247
- unlock();
248
- cache.unref(node);
249
- };
250
- release[Symbol.dispose] = release;
251
- return release;
258
+ return caches.getOrInsertComputed(fs.uuid, () => new VCache(withoutExceptionContext(fs)));
252
259
  }
@@ -3,6 +3,7 @@ import type * as fs from 'node:fs';
3
3
  import type { V_Context } from '../context.js';
4
4
  import { EventEmitter } from 'eventemitter3';
5
5
  import { type Stats } from '../node/stats.js';
6
+ import '../polyfills.js';
6
7
  /**
7
8
  * Base class for file system watchers.
8
9
  * Provides event handling capabilities for watching file system changes.
@@ -4,6 +4,7 @@ import { contextOf } from '../internal/contexts.js';
4
4
  import { isStatsEqual } from '../node/stats.js';
5
5
  import { statSync } from '../node/sync.js';
6
6
  import { basename, dirname, join, relative } from '../path.js';
7
+ import '../polyfills.js';
7
8
  import { normalizePath } from '../utils.js';
8
9
  /** Alias so `.call` resolves to the non-bigint overload */
9
10
  const _statSync = statSync;
@@ -130,10 +131,7 @@ export class StatWatcher extends Watcher {
130
131
  const watchers = new Map();
131
132
  export function addWatcher(path, watcher) {
132
133
  const normalizedPath = normalizePath(path);
133
- if (!watchers.has(normalizedPath)) {
134
- watchers.set(normalizedPath, new Set());
135
- }
136
- watchers.get(normalizedPath).add(watcher);
134
+ watchers.getOrInsertComputed(normalizedPath, () => new Set()).add(watcher);
137
135
  }
138
136
  export function removeWatcher(path, watcher) {
139
137
  const normalizedPath = normalizePath(path);
package/dist/vfs/xattr.js CHANGED
@@ -5,7 +5,8 @@ import { Attributes, hasAccess } from '../internal/inode.js';
5
5
  import { normalizePath } from '../utils.js';
6
6
  import { checkAccess } from './config.js';
7
7
  import { R_OK, W_OK } from '../constants.js';
8
- import { resolveMount } from './shared.js';
8
+ import { resolve as resolveAsync } from './async.js';
9
+ import { resolve as resolveSync } from './sync.js';
9
10
  /** Attributes outside the `user` namespace that are supported. */
10
11
  export const allowedRestrictedNames = new Set();
11
12
  /**
@@ -19,9 +20,9 @@ function checkName($, name, path, syscall) {
19
20
  }
20
21
  export async function get(path, name, opt = {}) {
21
22
  path = normalizePath.call(this, path);
22
- const { fs, path: resolved } = resolveMount(path, this);
23
+ const { cache, path: resolved } = await resolveAsync(this, path, opt.noFollow);
23
24
  checkName(this, name, path, 'xattr.get');
24
- const inode = await fs.stat(resolved).catch(rethrow('xattr.get', path));
25
+ const inode = await cache.stat(resolved).catch(rethrow('xattr.get', path));
25
26
  if (checkAccess && !hasAccess(this, inode, R_OK))
26
27
  throw UV('EACCES', 'xattr.get', path);
27
28
  inode.attributes ??= new Attributes();
@@ -34,10 +35,10 @@ export async function get(path, name, opt = {}) {
34
35
  export function getSync(path, name, opt = {}) {
35
36
  path = normalizePath.call(this, path);
36
37
  checkName(this, name, path, 'xattr.get');
37
- const { fs, path: resolved } = resolveMount(path, this);
38
+ const { cache, path: resolved } = resolveSync(this, path, opt.noFollow);
38
39
  let inode;
39
40
  try {
40
- inode = fs.statSync(resolved);
41
+ inode = cache.statSync(resolved);
41
42
  }
42
43
  catch (e) {
43
44
  throw setUVMessage(Object.assign(e, { path }));
@@ -61,9 +62,9 @@ export function getSync(path, name, opt = {}) {
61
62
  */
62
63
  export async function set(path, name, value, opt = {}) {
63
64
  path = normalizePath.call(this, path);
64
- const { fs, path: resolved } = resolveMount(path, this);
65
+ const { fs, cache, path: resolved } = await resolveAsync(this, path, opt.noFollow);
65
66
  checkName(this, name, path, 'xattr.set');
66
- const inode = await fs.stat(resolved).catch(rethrow('xattr.set', path));
67
+ const inode = await cache.stat(resolved).catch(rethrow('xattr.set', path));
67
68
  if (checkAccess && !hasAccess(this, inode, W_OK))
68
69
  throw UV('EACCES', 'xattr.set', path);
69
70
  inode.attributes ??= new Attributes();
@@ -85,11 +86,11 @@ export async function set(path, name, value, opt = {}) {
85
86
  */
86
87
  export function setSync(path, name, value, opt = {}) {
87
88
  path = normalizePath.call(this, path);
88
- const { fs, path: resolved } = resolveMount(path, this);
89
+ const { fs, cache, path: resolved } = resolveSync(this, path, opt.noFollow);
89
90
  checkName(this, name, path, 'xattr.set');
90
91
  let inode;
91
92
  try {
92
- inode = fs.statSync(resolved);
93
+ inode = cache.statSync(resolved);
93
94
  }
94
95
  catch (e) {
95
96
  throw setUVMessage(Object.assign(e, { path }));
@@ -118,9 +119,9 @@ export function setSync(path, name, value, opt = {}) {
118
119
  */
119
120
  export async function remove(path, name) {
120
121
  path = normalizePath.call(this, path);
121
- const { fs, path: resolved } = resolveMount(path, this);
122
+ const { fs, cache, path: resolved } = await resolveAsync(this, path);
122
123
  checkName(this, name, path, 'xattr.remove');
123
- const inode = await fs.stat(resolved).catch(rethrow('xattr.remove', path));
124
+ const inode = await cache.stat(resolved).catch(rethrow('xattr.remove', path));
124
125
  if (checkAccess && !hasAccess(this, inode, W_OK))
125
126
  throw UV('EACCES', 'xattr.remove', path);
126
127
  inode.attributes ??= new Attributes();
@@ -138,11 +139,11 @@ export async function remove(path, name) {
138
139
  */
139
140
  export function removeSync(path, name) {
140
141
  path = normalizePath.call(this, path);
141
- const { fs, path: resolved } = resolveMount(path, this);
142
+ const { fs, cache, path: resolved } = resolveSync(this, path);
142
143
  checkName(this, name, path, 'xattr.remove');
143
144
  let inode;
144
145
  try {
145
- inode = fs.statSync(resolved);
146
+ inode = cache.statSync(resolved);
146
147
  }
147
148
  catch (e) {
148
149
  throw setUVMessage(Object.assign(e, { path }));
@@ -169,8 +170,8 @@ export function removeSync(path, name) {
169
170
  */
170
171
  export async function list(path) {
171
172
  path = normalizePath.call(this, path);
172
- const { fs, path: resolved } = resolveMount(path, this);
173
- const inode = await fs.stat(resolved).catch(rethrow('xattr.list', path));
173
+ const { cache, path: resolved } = await resolveAsync(this, path);
174
+ const inode = await cache.stat(resolved).catch(rethrow('xattr.list', path));
174
175
  if (!inode.attributes)
175
176
  return [];
176
177
  return [...inode.attributes.keys()];
@@ -183,10 +184,10 @@ export async function list(path) {
183
184
  */
184
185
  export function listSync(path) {
185
186
  path = normalizePath.call(this, path);
186
- const { fs, path: resolved } = resolveMount(path, this);
187
+ const { cache, path: resolved } = resolveSync(this, path);
187
188
  let inode;
188
189
  try {
189
- inode = fs.statSync(resolved);
190
+ inode = cache.statSync(resolved);
190
191
  }
191
192
  catch (e) {
192
193
  throw setUVMessage(Object.assign(e, { path }));