@poe-platform/safe-fs 0.1.720 → 0.1.722

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 (55) hide show
  1. package/README.md +64 -18
  2. package/dist/safe-fs/bridge/filesystem.d.ts +2 -0
  3. package/dist/safe-fs/bridge/filesystem.js +17 -7
  4. package/dist/safe-fs/bridge/types.d.ts +2 -0
  5. package/dist/safe-fs/contracts/descriptor.d.ts +5 -0
  6. package/dist/safe-fs/contracts/filesystem.d.ts +12 -0
  7. package/dist/safe-fs/contracts/io.d.ts +3 -1
  8. package/dist/safe-fs/contracts/io.js +48 -16
  9. package/dist/safe-fs/core.d.ts +2 -1
  10. package/dist/safe-fs/core.js +1 -1
  11. package/dist/safe-fs/fs/capabilities.js +6 -3
  12. package/dist/safe-fs/fs/descriptor.js +9 -2
  13. package/dist/safe-fs/fs/devices/index.d.ts +1 -0
  14. package/dist/safe-fs/fs/devices/index.js +24 -6
  15. package/dist/safe-fs/fs/memory/index.d.ts +1 -0
  16. package/dist/safe-fs/fs/memory/index.js +75 -3
  17. package/dist/safe-fs/fs/memory/limits.js +6 -4
  18. package/dist/safe-fs/fs/mount/index.d.ts +4 -0
  19. package/dist/safe-fs/fs/mount/index.js +74 -11
  20. package/dist/safe-fs/fs/object-publication/index.js +6 -6
  21. package/dist/safe-fs/fs/overlay/index.d.ts +4 -2
  22. package/dist/safe-fs/fs/overlay/index.js +92 -36
  23. package/dist/safe-fs/fs/quota/index.js +18 -16
  24. package/dist/safe-fs/fs/readonly/index.js +5 -3
  25. package/dist/safe-fs/fs/real/index.d.ts +21 -1
  26. package/dist/safe-fs/fs/real/index.js +266 -22
  27. package/dist/safe-fs/fs/s3/filesystem.d.ts +8 -0
  28. package/dist/safe-fs/fs/s3/filesystem.js +58 -12
  29. package/dist/safe-fs/fs/s3/http/request.d.ts +1 -1
  30. package/dist/safe-fs/fs/s3/http/request.js +1 -1
  31. package/dist/safe-fs/fs/s3/http/transport.js +8 -6
  32. package/dist/safe-fs/fs/s3/http/xml.d.ts +4 -1
  33. package/dist/safe-fs/fs/s3/http/xml.js +6 -2
  34. package/dist/safe-fs/fs/s3/namespace.js +12 -10
  35. package/dist/safe-fs/fs/scoped.d.ts +1 -0
  36. package/dist/safe-fs/fs/scoped.js +54 -35
  37. package/dist/safe-fs/fs/webdav/webdav.d.ts +4 -1
  38. package/dist/safe-fs/fs/webdav/webdav.js +30 -22
  39. package/dist/safe-fs/node/filesystem.d.ts +4 -1
  40. package/dist/safe-fs/node/filesystem.js +7 -1
  41. package/dist/safe-fs/node/host.d.ts +3 -0
  42. package/dist/safe-fs/node/host.js +78 -0
  43. package/dist/safe-fs/node/index.d.ts +2 -1
  44. package/dist/safe-fs/node/index.js +2 -1
  45. package/dist/safe-fs/platform/browser.d.ts +1 -0
  46. package/dist/safe-fs/platform/browser.js +1 -0
  47. package/dist/safe-fs/platform/node.d.ts +1 -0
  48. package/dist/safe-fs/platform/node.js +1 -0
  49. package/dist/safe-fs/python/emscripten.d.ts +1 -0
  50. package/dist/safe-fs/python/emscripten.js +5 -5
  51. package/dist/safe-fs/python/filesystem.js +5 -5
  52. package/dist/safe-fs/python/native.d.ts +1 -0
  53. package/dist/safe-fs/python/native.js +3 -2
  54. package/dist/safe-fs/xml.js +32 -21
  55. package/package.json +1 -1
@@ -15,13 +15,14 @@ const deviceCapabilities = Object.freeze({
15
15
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
16
16
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
17
17
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
18
- open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
18
+ open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
19
19
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
20
20
  });
21
21
  function globalCapabilities(filesystem) {
22
22
  const capabilities = { readOnly: false };
23
23
  const optional = {
24
24
  open: ["open"],
25
+ trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
25
26
  atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
26
27
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
27
28
  streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
@@ -101,7 +102,16 @@ export class DeviceFileSystem {
101
102
  throw new FsError("EBUSY", { path });
102
103
  }
103
104
  async #writeTarget(path, options) {
104
- const resolved = await this.#resolve(path, options);
105
+ let resolved;
106
+ try {
107
+ resolved = await this.#resolve(path, options);
108
+ }
109
+ catch (error) {
110
+ options.signal?.throwIfAborted();
111
+ if ((options.flag === "wx" || options.flag === "ax") && isFsError(error, "ELOOP"))
112
+ return false;
113
+ throw error;
114
+ }
105
115
  if (resolved === nullPath) {
106
116
  if (options.flag === "wx" || options.flag === "ax")
107
117
  throw new FsError("EEXIST", { path });
@@ -122,7 +132,7 @@ export class DeviceFileSystem {
122
132
  let resolved;
123
133
  let selected;
124
134
  try {
125
- resolved = await this.#resolve(path, options, true, options.create);
135
+ resolved = await this.#resolve(path, options, options.create !== undefined || options.creation !== "exclusive", options.create);
126
136
  }
127
137
  catch (error) {
128
138
  options.signal?.throwIfAborted();
@@ -261,11 +271,11 @@ export class DeviceFileSystem {
261
271
  options?.signal?.throwIfAborted();
262
272
  if (!options || typeof options !== "object")
263
273
  throw new FsError("EINVAL", { syscall: "open", path });
264
- const resolved = await this.#resolve(path, options, options.creation !== "exclusive");
274
+ const resolved = await this.#resolve(path, options, !options.noFollow && options.creation !== "exclusive");
265
275
  options.signal?.throwIfAborted();
266
276
  if (resolved === nullPath)
267
277
  return openFileDescriptor(path, options, {
268
- position: true, positionedRead: true, positionedWrite: true, positionedAppendWrite: true,
278
+ noFollow: true, position: true, positionedRead: true, positionedWrite: true, positionedAppendWrite: true,
269
279
  openTruncate: true, delegateZeroLengthWrite: true, truncate: false, synchronization: "none",
270
280
  }, async (admitted) => {
271
281
  if (admitted.creation === "exclusive")
@@ -289,7 +299,7 @@ export class DeviceFileSystem {
289
299
  throw new FsError("ENOTSUP", { syscall: "open", path });
290
300
  const query = this.#filesystem.capabilitiesFor;
291
301
  options.signal?.throwIfAborted();
292
- const capabilities = query && options.creation !== "exclusive" ? await Reflect.apply(query, this.#filesystem, [path, options]) : this.#filesystem.capabilities;
302
+ const capabilities = query && !options.noFollow && options.creation !== "exclusive" ? await Reflect.apply(query, this.#filesystem, [path, options]) : this.#filesystem.capabilities;
293
303
  options.signal?.throwIfAborted();
294
304
  const open = this.#filesystem.open;
295
305
  options.signal?.throwIfAborted();
@@ -572,6 +582,14 @@ export class DeviceFileSystem {
572
582
  throw new FsError("ENOTSUP", { path });
573
583
  await this.#filesystem.symlink(target, path, options);
574
584
  }
585
+ async confineExtraction(roots, options = {}) {
586
+ options.signal?.throwIfAborted();
587
+ if (!this.#filesystem.confineExtraction)
588
+ throw new FsError("ENOTSUP", { syscall: "confineExtraction" });
589
+ // Extraction uses only the backing namespace; synthetic devices must never
590
+ // become archive publication destinations.
591
+ return this.#filesystem.confineExtraction(roots, options);
592
+ }
575
593
  async link(existingPath, newPath, options = {}) {
576
594
  await this.#mutable(existingPath, options, false);
577
595
  await this.#mutable(newPath, options, false);
@@ -53,6 +53,7 @@ export declare class MemoryFileSystem implements FileSystem {
53
53
  constructor(options?: MemoryFileSystemOptions);
54
54
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
55
55
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
56
+ confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
56
57
  private metadata;
57
58
  private directory;
58
59
  private addNode;
@@ -117,6 +117,68 @@ export class MemoryFileSystem {
117
117
  this.validatePath(path, "realpath");
118
118
  return resolveMissingTarget(owner.root, path || ".", options.signal);
119
119
  }
120
+ async confineExtraction(roots, options = {}) {
121
+ options.signal?.throwIfAborted();
122
+ const retained = new Map();
123
+ for (const root of roots) {
124
+ this.validatePath(root, "confineExtraction");
125
+ let path = "";
126
+ for (const component of root.split("/").filter(Boolean)) {
127
+ if (component === "." || component === "..")
128
+ this.fail("EINVAL", "confineExtraction", root);
129
+ path += `/${component}`;
130
+ const node = this.entry(path, "confineExtraction").node;
131
+ if (node.type !== "directory")
132
+ this.fail("ENOTDIR", "confineExtraction", path);
133
+ retained.set(path, node);
134
+ }
135
+ retained.set("/", this.root);
136
+ }
137
+ const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream"]);
138
+ const reads = new Set(["access", "capabilitiesFor", "compareEntry", "lstat", "stat", "readFile", "readStream", "readdir", "readlink", "realpath"]);
139
+ const check = (path, followFinal) => {
140
+ if (!roots.some(root => root === "/" || path === root || path.startsWith(`${root}/`)))
141
+ this.fail("EPERM", "confineExtraction", path);
142
+ const components = path.split("/").filter(Boolean);
143
+ if (components.some(component => component === "." || component === ".."))
144
+ this.fail("EINVAL", "confineExtraction", path);
145
+ let current = "";
146
+ for (const [index, component] of components.entries()) {
147
+ current += `/${component}`;
148
+ const node = this.entry(current, "confineExtraction", true).node;
149
+ if (retained.has(current) && node !== retained.get(current))
150
+ this.fail("EAGAIN", "confineExtraction", current);
151
+ if (index < components.length - 1 && node?.type !== "directory")
152
+ this.fail("ENOTDIR", "confineExtraction", current);
153
+ if (followFinal && index === components.length - 1 && node?.type === "symlink")
154
+ this.fail("ELOOP", "confineExtraction", current);
155
+ }
156
+ };
157
+ return new Proxy(this, {
158
+ get: (target, property) => {
159
+ if (property === "objects" || property === "confineExtraction")
160
+ return undefined;
161
+ const value = Reflect.get(target, property);
162
+ if (typeof value !== "function")
163
+ return value;
164
+ if (reads.has(String(property)))
165
+ return value.bind(target);
166
+ return (...args) => {
167
+ if (!allowed.has(String(property)))
168
+ throw new FsError("ENOTSUP", { syscall: String(property) });
169
+ // Stock mutations resolve and commit synchronously; writeStream retains
170
+ // its file node before its first await, so later chunks cannot redirect.
171
+ const paths = property === "symlink" ? [args[1]] : property === "link" || property === "rename" ? args.slice(0, 2) : [args[0]];
172
+ for (const path of paths) {
173
+ if (typeof path !== "string")
174
+ throw new FsError("EINVAL");
175
+ check(path, ["chmod", "utimes", "appendFile", "writeFile", "writeStream", "link"].includes(String(property)));
176
+ }
177
+ return Reflect.apply(value, target, args);
178
+ };
179
+ },
180
+ });
181
+ }
120
182
  metadata(mode) {
121
183
  const now = Date.now();
122
184
  return {
@@ -201,6 +263,10 @@ export class MemoryFileSystem {
201
263
  }
202
264
  resolve(path, syscall, options = {}) {
203
265
  this.validatePath(path, syscall);
266
+ // Bound all component arrays allocated by this resolution, including cycles.
267
+ let remainingPathUnits = 65_536 - path.length;
268
+ if (remainingPathUnits < 0)
269
+ this.fail("ENAMETOOLONG", syscall, path);
204
270
  const pending = path.split("/").filter(Boolean);
205
271
  if (path.endsWith("/"))
206
272
  pending.push("");
@@ -239,6 +305,9 @@ export class MemoryFileSystem {
239
305
  if (node.type === "symlink" && (options.followFinal !== false || pending.length > 0)) {
240
306
  if (++links > 40)
241
307
  this.fail("ELOOP", syscall, path);
308
+ if (node.target.length > remainingPathUnits)
309
+ this.fail("ENAMETOOLONG", syscall, path);
310
+ remainingPathUnits -= node.target.length;
242
311
  const target = node.target.split("/").filter(Boolean);
243
312
  if (node.target.endsWith("/"))
244
313
  target.push("");
@@ -278,6 +347,7 @@ export class MemoryFileSystem {
278
347
  snapshot(node) {
279
348
  return {
280
349
  type: node.type,
350
+ filesystemType: "memory",
281
351
  ioBlockSize: preferredIoBlockSize,
282
352
  ...(Number.isSafeInteger(node.revision) ? { revision: node.revision } : {}),
283
353
  preferredIoBlockSize: 4096,
@@ -314,15 +384,17 @@ export class MemoryFileSystem {
314
384
  }
315
385
  open(path, options) {
316
386
  return openFileDescriptor(path, options, {
317
- positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
387
+ noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
318
388
  }, async (admitted) => {
319
389
  const location = this.resolve(path, "open", {
320
- followFinal: admitted.creation !== "exclusive", allowMissing: admitted.creation !== "never",
390
+ followFinal: !admitted.noFollow && admitted.creation !== "exclusive", allowMissing: admitted.creation !== "never",
321
391
  });
322
392
  let node = location.node;
323
393
  if (node) {
324
394
  if (admitted.creation === "exclusive")
325
395
  this.fail("EEXIST", "open", path);
396
+ if (node.type === "symlink" && admitted.noFollow)
397
+ this.fail("ELOOP", "open", path);
326
398
  if (node.type !== "file")
327
399
  this.fail("EISDIR", "open", path);
328
400
  this.permission(node, admitted.access === "read" ? 4 : admitted.access === "write" ? 2 : 6, "open", path);
@@ -1212,5 +1284,5 @@ function stockRetainedResize(filesystem) {
1212
1284
  return true;
1213
1285
  }
1214
1286
  export function createMemoryFileSystem(options = {}) {
1215
- return new MemoryFileSystem(normalizeMemoryFileSystemLimits(options));
1287
+ return new MemoryFileSystem(options);
1216
1288
  }
@@ -1,8 +1,8 @@
1
1
  import { readConfigRecord } from "../../config.js";
2
2
  export const defaultMemoryFileSystemLimits = Object.freeze({
3
- maxFileBytes: 16 * 1024 * 1024,
4
- maxRetainedBytes: 64 * 1024 * 1024,
5
- maxMetadataUnits: 10_000,
3
+ maxFileBytes: Infinity,
4
+ maxRetainedBytes: Infinity,
5
+ maxMetadataUnits: Infinity,
6
6
  });
7
7
  export function normalizeMemoryFileSystemLimits(options) {
8
8
  const keys = ["maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
@@ -15,7 +15,9 @@ export function normalizeMemoryFileSystemLimits(options) {
15
15
  limits.maxBytes = record.maxBytes;
16
16
  }
17
17
  for (const key of keys) {
18
- const value = Object.hasOwn(record, key) ? record[key] : limits[key];
18
+ if (!Object.hasOwn(record, key))
19
+ continue;
20
+ const value = record[key];
19
21
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (key === "maxMetadataUnits" ? 1 : 0)) {
20
22
  throw new RangeError(`${key} must be a ${key === "maxMetadataUnits" ? "positive" : "nonnegative"} safe integer`);
21
23
  }
@@ -8,6 +8,9 @@ export interface MountFileSystemOptions {
8
8
  export declare class MountFileSystem implements FileSystem {
9
9
  readonly capabilities: FileSystemCapabilities;
10
10
  private readonly mounts;
11
+ private activeNamespaceOperations;
12
+ private namespaceMutation;
13
+ private readonly namespaceWaiters;
11
14
  constructor(options: MountFileSystemOptions);
12
15
  private select;
13
16
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
@@ -16,6 +19,7 @@ export declare class MountFileSystem implements FileSystem {
16
19
  resizeFile(path: string, operation: FileResizeOperation, options?: FileResizeOptions): Promise<void>;
17
20
  openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
18
21
  private error;
22
+ private acquireNamespace;
19
23
  private operation;
20
24
  private components;
21
25
  private lookup;
@@ -22,9 +22,10 @@ function fail(code) {
22
22
  throw new FsError(code);
23
23
  }
24
24
  function snapshotStat(stat) {
25
- const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
25
+ const { type, filesystemType, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
26
26
  return {
27
27
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
28
+ ...(filesystemType === undefined ? {} : { filesystemType }),
28
29
  ...(revision === undefined ? {} : { revision }),
29
30
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
30
31
  ...(ioBlockSize === undefined ? {} : { ioBlockSize }),
@@ -45,6 +46,9 @@ function snapshotStat(stat) {
45
46
  export class MountFileSystem {
46
47
  capabilities;
47
48
  mounts;
49
+ activeNamespaceOperations = 0;
50
+ namespaceMutation = false;
51
+ namespaceWaiters = new Set();
48
52
  constructor(options) {
49
53
  const mounts = [];
50
54
  const paths = new Set();
@@ -98,6 +102,7 @@ export class MountFileSystem {
98
102
  const common = (capability) => {
99
103
  const optional = {
100
104
  open: ["open"],
105
+ trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
101
106
  atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
102
107
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
103
108
  descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
@@ -116,7 +121,7 @@ export class MountFileSystem {
116
121
  return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
117
122
  };
118
123
  const semantics = Object.fromEntries([
119
- "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
124
+ "trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
120
125
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
121
126
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
122
127
  "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "atomicResize", "symlinks", "hardlinks", "permissions", "timestamps",
@@ -139,6 +144,7 @@ export class MountFileSystem {
139
144
  return this.operation("capabilitiesFor", path, options, async () => {
140
145
  const location = await this.resolve(path, options, {
141
146
  allowMissing: options.create ?? true,
147
+ followFinal: options.create !== undefined || options.creation !== "exclusive",
142
148
  ...(options.create === undefined ? {} : { resizeCreate: options.create }),
143
149
  });
144
150
  const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
@@ -168,7 +174,9 @@ export class MountFileSystem {
168
174
  return path === "/" || this.mounts.some((mount) => within(path, mount.path));
169
175
  }
170
176
  async openReadFile(path, options = {}) {
177
+ let release;
171
178
  try {
179
+ release = await this.acquireNamespace(options);
172
180
  options.signal?.throwIfAborted();
173
181
  const location = await this.resolve(path, options);
174
182
  if (location.synthetic)
@@ -178,6 +186,9 @@ export class MountFileSystem {
178
186
  catch (error) {
179
187
  throw error ? this.error(error, "openReadFile", path, options) : error;
180
188
  }
189
+ finally {
190
+ release?.();
191
+ }
181
192
  }
182
193
  async resizeFile(path, operation, options = {}) {
183
194
  return this.operation("resizeFile", path, options, async () => {
@@ -197,7 +208,9 @@ export class MountFileSystem {
197
208
  });
198
209
  }
199
210
  async openResizeFile(path, options = {}) {
211
+ let release;
200
212
  try {
213
+ release = await this.acquireNamespace(options);
201
214
  options.signal?.throwIfAborted();
202
215
  const resolution = this.resolve(path, options, { allowMissing: options.create === true, resizeCreate: options.create ?? false });
203
216
  const signal = options.signal;
@@ -216,17 +229,56 @@ export class MountFileSystem {
216
229
  catch (error) {
217
230
  throw error ? this.error(error, "openResizeFile", path, options) : error;
218
231
  }
232
+ finally {
233
+ release?.();
234
+ }
219
235
  }
220
236
  error(error, syscall, path, options, dest) {
221
- if (options.signal?.aborted && error === options.signal.reason)
237
+ if (options.signal?.aborted && Object.is(error, options.signal.reason))
222
238
  return error;
223
239
  return new FsError(toFsError(error).code, {
224
240
  cause: error, syscall, path: globalPath(path), ...(dest === undefined ? {} : { dest: globalPath(dest) }),
225
241
  });
226
242
  }
243
+ // Keep guest namespace changes outside every path admission/action interval.
244
+ // Backend references held outside this view still require backend confinement.
245
+ async acquireNamespace(options, mutation = false) {
246
+ const signal = options.signal;
247
+ signal?.throwIfAborted();
248
+ while (this.namespaceMutation || mutation && this.activeNamespaceOperations > 0) {
249
+ await new Promise((resolve, reject) => {
250
+ const wake = () => { cleanup(); resolve(); };
251
+ const abort = () => { cleanup(); reject(signal.reason); };
252
+ const cleanup = () => {
253
+ this.namespaceWaiters.delete(wake);
254
+ signal?.removeEventListener("abort", abort);
255
+ };
256
+ this.namespaceWaiters.add(wake);
257
+ signal?.addEventListener("abort", abort, { once: true });
258
+ if (signal?.aborted)
259
+ abort();
260
+ });
261
+ signal?.throwIfAborted();
262
+ }
263
+ this.activeNamespaceOperations++;
264
+ if (mutation)
265
+ this.namespaceMutation = true;
266
+ return () => {
267
+ this.activeNamespaceOperations--;
268
+ if (mutation)
269
+ this.namespaceMutation = false;
270
+ for (const wake of this.namespaceWaiters)
271
+ wake();
272
+ };
273
+ }
227
274
  async operation(syscall, path, options, action, dest, preserveReceipt = false) {
275
+ let release;
228
276
  try {
229
- options.signal?.throwIfAborted();
277
+ release = await this.acquireNamespace(options, [
278
+ "rename", "symlink", "link", "unlink", "rm", "rmdir",
279
+ "removeEntryConditional", "removeFileConditional", "removeTreeConditional",
280
+ "publishStagedFile", "removeStagedFile",
281
+ ].includes(syscall));
230
282
  const result = await action();
231
283
  if (!preserveReceipt)
232
284
  options.signal?.throwIfAborted();
@@ -235,6 +287,9 @@ export class MountFileSystem {
235
287
  catch (error) {
236
288
  throw this.error(error, syscall, path, options, dest);
237
289
  }
290
+ finally {
291
+ release?.();
292
+ }
238
293
  }
239
294
  components(path) {
240
295
  validatePath(path);
@@ -423,18 +478,21 @@ export class MountFileSystem {
423
478
  }
424
479
  open(path, options) {
425
480
  return openFileDescriptor(globalPath(path), options, {
426
- positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
481
+ noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
427
482
  }, async (admitted) => {
483
+ let release;
428
484
  try {
485
+ release = await this.acquireNamespace(admitted);
429
486
  const location = await this.resolve(path, admitted, {
430
- allowMissing: admitted.creation !== "never", followFinal: admitted.creation !== "exclusive",
487
+ allowMissing: admitted.creation !== "never", followFinal: !admitted.noFollow && admitted.creation !== "exclusive",
431
488
  });
432
489
  if (location.synthetic)
433
490
  fail("EISDIR");
434
491
  if (admitted.access !== "read" || admitted.creation !== "never" || admitted.truncate || admitted.append)
435
492
  this.mutable(location);
436
493
  const backend = location.mount.backend;
437
- const capabilities = await backend.capabilitiesFor?.(location.local, admitted) ?? backend.capabilities;
494
+ const capabilities = admitted.noFollow ? backend.capabilities
495
+ : await backend.capabilitiesFor?.(location.local, admitted) ?? backend.capabilities;
438
496
  if (!backend.open || capabilities.open === false)
439
497
  fail("ENOTSUP");
440
498
  admitted.signal?.throwIfAborted();
@@ -451,6 +509,9 @@ export class MountFileSystem {
451
509
  admitted.signal?.throwIfAborted();
452
510
  throw this.error(error, "open", path, admitted);
453
511
  }
512
+ finally {
513
+ release?.();
514
+ }
454
515
  });
455
516
  }
456
517
  writeFile(path, data, options = {}) {
@@ -776,10 +837,7 @@ export class MountFileSystem {
776
837
  }
777
838
  }
778
839
  else {
779
- const maxBytes = 64 * 1024 * 1024;
780
- const data = await reader.readFile(origin.local, { ...options, maxBytes });
781
- if (data.byteLength > maxBytes)
782
- fail("EFBIG");
840
+ const data = await reader.readFile(origin.local, options);
783
841
  options.signal?.throwIfAborted();
784
842
  await writer.writeFile(target.local, data, writeOptions);
785
843
  }
@@ -856,7 +914,9 @@ export class MountFileSystem {
856
914
  });
857
915
  }
858
916
  async *readStream(path, options = {}) {
917
+ let release;
859
918
  try {
919
+ release = await this.acquireNamespace(options);
860
920
  options.signal?.throwIfAborted();
861
921
  const location = await this.resolve(path, options);
862
922
  if (location.synthetic)
@@ -871,6 +931,9 @@ export class MountFileSystem {
871
931
  catch (error) {
872
932
  throw this.error(error, "readStream", path, options);
873
933
  }
934
+ finally {
935
+ release?.();
936
+ }
874
937
  }
875
938
  writeStream(path, source, options = {}) {
876
939
  return this.operation("writeStream", path, options, async () => {
@@ -5,12 +5,12 @@ import { validatePath } from "../../contracts/virtual-path.js";
5
5
  import { openFileDescriptor } from "../descriptor.js";
6
6
  export function withObjectFileDescriptors(filesystem, store, options = {}) {
7
7
  const chunkBytes = options.chunkBytes ?? 65536;
8
- const maxStagedBytes = options.maxStagedBytes ?? 8 * 1024 * 1024;
9
- const maxStagedPages = options.maxStagedPages ?? 4096;
10
- const maxFileBytes = options.maxFileBytes ?? 256 * 1024 * 1024;
11
- const maxOpenFiles = options.maxOpenFiles ?? 64;
12
- if (![chunkBytes, maxStagedBytes, maxStagedPages, maxFileBytes, maxOpenFiles].every(value => Number.isSafeInteger(value) && value > 0)
13
- || chunkBytes > 1048576 || typeof store.acquire !== "function"
8
+ const maxStagedBytes = options.maxStagedBytes ?? Infinity;
9
+ const maxStagedPages = options.maxStagedPages ?? Infinity;
10
+ const maxFileBytes = options.maxFileBytes ?? Infinity;
11
+ const maxOpenFiles = options.maxOpenFiles ?? Infinity;
12
+ if (![chunkBytes, options.maxStagedBytes, options.maxStagedPages, options.maxFileBytes, options.maxOpenFiles].every(value => value === undefined || Number.isSafeInteger(value) && value > 0)
13
+ || typeof store.acquire !== "function"
14
14
  || store.publish !== undefined && typeof store.publish !== "function"
15
15
  || store.createStaging !== undefined && typeof store.createStaging !== "function")
16
16
  throw new TypeError("Invalid object descriptor configuration");
@@ -1,6 +1,6 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
2
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
3
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
4
4
  export interface OverlayFileSystemOptions {
5
5
  readonly upper: FileSystem;
6
6
  readonly lower: FileSystem;
@@ -21,7 +21,9 @@ export declare class OverlayFileSystem implements FileSystem {
21
21
  open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
22
22
  private run;
23
23
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
24
- openReadFile(path: string, options?: OpenReadFileOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
24
+ openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
25
+ private pinRead;
26
+ private handleBytes;
25
27
  private writable;
26
28
  private permission;
27
29
  private maybeStat;