@poe-platform/safe-fs 0.1.726 → 0.1.728

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 (57) hide show
  1. package/README.md +8 -1
  2. package/dist/safe-fs/contracts/errors.js +8 -1
  3. package/dist/safe-fs/contracts/filesystem.d.ts +68 -2
  4. package/dist/safe-fs/contracts/io.js +183 -27
  5. package/dist/safe-fs/contracts/virtual-path.d.ts +3 -1
  6. package/dist/safe-fs/contracts/virtual-path.js +20 -1
  7. package/dist/safe-fs/core.d.ts +1 -0
  8. package/dist/safe-fs/core.js +1 -0
  9. package/dist/safe-fs/fs/capabilities.d.ts +1 -1
  10. package/dist/safe-fs/fs/capabilities.js +59 -21
  11. package/dist/safe-fs/fs/conditional-chmod.d.ts +7 -0
  12. package/dist/safe-fs/fs/conditional-chmod.js +19 -0
  13. package/dist/safe-fs/fs/descriptor.js +51 -25
  14. package/dist/safe-fs/fs/devices/index.d.ts +7 -2
  15. package/dist/safe-fs/fs/devices/index.js +210 -29
  16. package/dist/safe-fs/fs/devices/path.d.ts +3 -1
  17. package/dist/safe-fs/fs/devices/path.js +70 -12
  18. package/dist/safe-fs/fs/memory/index.d.ts +34 -48
  19. package/dist/safe-fs/fs/memory/index.js +1150 -235
  20. package/dist/safe-fs/fs/memory/ledger.d.ts +2 -0
  21. package/dist/safe-fs/fs/memory/ledger.js +15 -3
  22. package/dist/safe-fs/fs/memory/missing-target.js +21 -1
  23. package/dist/safe-fs/fs/mount/index.d.ts +11 -3
  24. package/dist/safe-fs/fs/mount/index.js +326 -15
  25. package/dist/safe-fs/fs/object-publication/index.js +79 -3
  26. package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
  27. package/dist/safe-fs/fs/overlay/index.js +22 -2
  28. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +2 -1
  29. package/dist/safe-fs/fs/overlay/memory-publication.js +39 -2
  30. package/dist/safe-fs/fs/quota/index.js +1 -1
  31. package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
  32. package/dist/safe-fs/fs/readonly/index.js +13 -0
  33. package/dist/safe-fs/fs/real/index.d.ts +6 -2
  34. package/dist/safe-fs/fs/real/index.js +78 -8
  35. package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
  36. package/dist/safe-fs/fs/s3/filesystem.js +48 -16
  37. package/dist/safe-fs/fs/s3/http/transport.js +20 -3
  38. package/dist/safe-fs/fs/s3/mock.js +19 -2
  39. package/dist/safe-fs/fs/s3/namespace.js +8 -2
  40. package/dist/safe-fs/fs/s3/registry.js +2 -0
  41. package/dist/safe-fs/fs/scoped.d.ts +1 -0
  42. package/dist/safe-fs/fs/scoped.js +261 -74
  43. package/dist/safe-fs/fs/staging-ancestry.d.ts +8 -0
  44. package/dist/safe-fs/fs/staging-ancestry.js +115 -0
  45. package/dist/safe-fs/fs/staging-cleanup.d.ts +5 -0
  46. package/dist/safe-fs/fs/staging-cleanup.js +63 -0
  47. package/dist/safe-fs/platform/browser.d.ts +5 -0
  48. package/dist/safe-fs/platform/browser.js +17 -0
  49. package/dist/safe-fs/platform/node.d.ts +5 -0
  50. package/dist/safe-fs/platform/node.js +28 -0
  51. package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
  52. package/dist/safe-fs/platform/transport-budget.js +4 -0
  53. package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
  54. package/dist/safe-fs/testing/object-io-metrics.js +29 -0
  55. package/dist/safe-fs/testing/object-publication.d.ts +3 -1
  56. package/dist/safe-fs/testing/object-publication.js +2 -2
  57. package/package.json +1 -1
@@ -45,6 +45,37 @@ function integer(value, syscall, path) {
45
45
  if (!Number.isSafeInteger(value) || value < 0)
46
46
  throw new FsError("EINVAL", { syscall, path });
47
47
  }
48
+ const OPEN_FILE_OPTION_KEYS = new Set(["access", "creation", "truncate", "append", "mode", "exactMode", "noFollow", "synchronization", "signal"]);
49
+ const managedCapabilitiesCache = new WeakMap();
50
+ function getManagedCapabilities(capabilities, access, append) {
51
+ let byKey = managedCapabilitiesCache.get(capabilities);
52
+ const key = `${access}:${append ? 1 : 0}`;
53
+ if (byKey) {
54
+ const cached = byKey.get(key);
55
+ if (cached)
56
+ return cached;
57
+ }
58
+ else {
59
+ byKey = new Map();
60
+ managedCapabilitiesCache.set(capabilities, byKey);
61
+ }
62
+ const positionedAppendWrite = capabilities.positionedAppendWrite === true && capabilities.positionedWrite && access !== "read";
63
+ const frozen = Object.freeze({
64
+ ...(capabilities.noFollow === undefined ? {} : { noFollow: capabilities.noFollow }),
65
+ ...(capabilities.publication === undefined ? {} : { publication: capabilities.publication }),
66
+ ...(capabilities.position === undefined ? {} : { position: capabilities.position }),
67
+ ...(capabilities.readObservation === undefined ? {} : { readObservation: capabilities.readObservation }),
68
+ ...(capabilities.openTruncate === undefined ? {} : { openTruncate: capabilities.openTruncate }),
69
+ positionedRead: capabilities.positionedRead && access !== "write",
70
+ positionedWrite: capabilities.positionedWrite && access !== "read" && (!append || positionedAppendWrite),
71
+ ...(capabilities.positionedAppendWrite === undefined ? {} : { positionedAppendWrite }),
72
+ ...(capabilities.delegateZeroLengthWrite === undefined ? {} : { delegateZeroLengthWrite: capabilities.delegateZeroLengthWrite && access !== "read" }),
73
+ truncate: capabilities.truncate && access !== "read",
74
+ synchronization: capabilities.synchronization,
75
+ });
76
+ byKey.set(key, frozen);
77
+ return frozen;
78
+ }
48
79
  class ManagedFileDescriptor {
49
80
  capabilities;
50
81
  #path;
@@ -62,26 +93,13 @@ class ManagedFileDescriptor {
62
93
  const getPosition = capabilities.position === true ? backend.getPosition : undefined;
63
94
  if (capabilities.position === true && typeof getPosition !== "function")
64
95
  throw new FsError("ENOTSUP", { syscall: "getPosition", path });
65
- this.#getPosition = getPosition?.bind(backend);
96
+ this.#getPosition = getPosition;
66
97
  const probeRead = capabilities.readObservation === true ? backend.probeRead : undefined;
67
98
  if (capabilities.readObservation === true && typeof probeRead !== "function")
68
99
  throw new FsError("ENOTSUP", { syscall: "probeRead", path });
69
- this.#probeRead = probeRead?.bind(backend);
100
+ this.#probeRead = probeRead;
70
101
  this.#backend = backend;
71
- const positionedAppendWrite = capabilities.positionedAppendWrite === true && capabilities.positionedWrite && options.access !== "read";
72
- this.capabilities = Object.freeze({
73
- ...(capabilities.noFollow === undefined ? {} : { noFollow: capabilities.noFollow }),
74
- ...(capabilities.publication === undefined ? {} : { publication: capabilities.publication }),
75
- ...(capabilities.position === undefined ? {} : { position: capabilities.position }),
76
- ...(capabilities.readObservation === undefined ? {} : { readObservation: capabilities.readObservation }),
77
- ...(capabilities.openTruncate === undefined ? {} : { openTruncate: capabilities.openTruncate }),
78
- positionedRead: capabilities.positionedRead && options.access !== "write",
79
- positionedWrite: capabilities.positionedWrite && options.access !== "read" && (!options.append || positionedAppendWrite),
80
- ...(capabilities.positionedAppendWrite === undefined ? {} : { positionedAppendWrite }),
81
- ...(capabilities.delegateZeroLengthWrite === undefined ? {} : { delegateZeroLengthWrite: capabilities.delegateZeroLengthWrite && options.access !== "read" }),
82
- truncate: capabilities.truncate && options.access !== "read",
83
- synchronization: capabilities.synchronization,
84
- });
102
+ this.capabilities = getManagedCapabilities(capabilities, options.access, options.append);
85
103
  }
86
104
  #run(syscall, options, action) {
87
105
  const signal = options.signal;
@@ -128,7 +146,7 @@ class ManagedFileDescriptor {
128
146
  return this.#run("getPosition", options, async (backend, forwarded) => {
129
147
  if (!this.capabilities.position || !this.#getPosition)
130
148
  throw new FsError("ENOTSUP", { syscall: "getPosition", path: this.#path });
131
- const position = await this.#getPosition(backend.resource, forwarded);
149
+ const position = await this.#getPosition.call(backend, backend.resource, forwarded);
132
150
  if (!Number.isSafeInteger(position) || position < 0)
133
151
  throw new FsError("EIO", { syscall: "getPosition", path: this.#path });
134
152
  return position;
@@ -138,7 +156,7 @@ class ManagedFileDescriptor {
138
156
  return this.#run("probeRead", options, async (backend, forwarded) => {
139
157
  if (!this.capabilities.readObservation || !this.#probeRead)
140
158
  throw new FsError("ENOTSUP", { syscall: "probeRead", path: this.#path });
141
- const readiness = await this.#probeRead(backend.resource, forwarded);
159
+ const readiness = await this.#probeRead.call(backend, backend.resource, forwarded);
142
160
  if (readiness !== "ready" && readiness !== "blocked" && readiness !== "unknown")
143
161
  throw new FsError("EIO", { syscall: "probeRead", path: this.#path });
144
162
  return readiness;
@@ -209,11 +227,17 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
209
227
  throw new FsError("EINVAL", { syscall: "open", path });
210
228
  const signal = options.signal;
211
229
  signal?.throwIfAborted();
212
- const keys = ["access", "creation", "truncate", "append", "mode", "exactMode", "noFollow", "synchronization", "signal"];
213
230
  const { access, creation = "never", truncate = false, append = false, mode = 0o666, synchronization } = options;
214
- if (Object.keys(options).some(key => !keys.includes(key))
215
- || !["read", "write", "readwrite"].includes(access)
216
- || !["never", "ifMissing", "exclusive"].includes(creation)
231
+ let invalidKey = false;
232
+ for (const key in options) {
233
+ if (Object.prototype.hasOwnProperty.call(options, key) && !OPEN_FILE_OPTION_KEYS.has(key)) {
234
+ invalidKey = true;
235
+ break;
236
+ }
237
+ }
238
+ if (invalidKey
239
+ || (access !== "read" && access !== "write" && access !== "readwrite")
240
+ || (creation !== "never" && creation !== "ifMissing" && creation !== "exclusive")
217
241
  || options.noFollow !== undefined && typeof options.noFollow !== "boolean"
218
242
  || options.exactMode !== undefined && typeof options.exactMode !== "boolean"
219
243
  || typeof truncate !== "boolean" || typeof append !== "boolean"
@@ -226,7 +250,7 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
226
250
  ...(options.noFollow === undefined ? {} : { noFollow: options.noFollow }),
227
251
  ...(options.exactMode === undefined ? {} : { exactMode: options.exactMode }),
228
252
  ...(signal === undefined ? {} : { signal }), ...(synchronization === undefined ? {} : { synchronization }) });
229
- const admittedCapabilities = Object.freeze({ ...capabilities });
253
+ const admittedCapabilities = Object.isFrozen(capabilities) ? capabilities : Object.freeze({ ...capabilities });
230
254
  admitCapabilities(path, admitted, admittedCapabilities);
231
255
  let backend;
232
256
  try {
@@ -238,9 +262,11 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
238
262
  }
239
263
  try {
240
264
  signal?.throwIfAborted();
241
- const selected = Object.freeze({ ...(backend.capabilities ?? admittedCapabilities) });
265
+ const rawBackendCapabilities = backend.capabilities;
266
+ const selected = rawBackendCapabilities ? Object.freeze({ ...rawBackendCapabilities }) : admittedCapabilities;
242
267
  signal?.throwIfAborted();
243
- admitCapabilities(path, admitted, selected);
268
+ if (rawBackendCapabilities)
269
+ admitCapabilities(path, admitted, selected);
244
270
  const descriptor = new ManagedFileDescriptor(path, admitted, selected, backend);
245
271
  signal?.throwIfAborted();
246
272
  return descriptor;
@@ -1,5 +1,5 @@
1
1
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
3
3
  import type { ByteSource } from "../../contracts/io.js";
4
4
  export declare class DeviceFileSystem implements FileSystem {
5
5
  #private;
@@ -27,11 +27,16 @@ export declare class DeviceFileSystem implements FileSystem {
27
27
  rmdir(path: string, options?: FsOptions): Promise<void>;
28
28
  publishFileConditional(path: string, source: ByteSource, options: ConditionalFilePublicationOptions): Promise<FileStat>;
29
29
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
30
- removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
30
+ removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions & {
31
+ readonly returnRemainingStat?: false | undefined;
32
+ }): Promise<void>;
33
+ removeEntryConditional(path: string, options: ConditionalRemoveEntryReceiptOptions): Promise<void | FileStat>;
31
34
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
32
35
  removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
33
36
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
34
37
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
38
+ prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
39
+ prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
35
40
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
36
41
  prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
37
42
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
@@ -1,29 +1,37 @@
1
+ import { snapshotStagingCreation } from "../staging-cleanup.js";
1
2
  import { FsError, isFsError } from "../../contracts/errors.js";
2
3
  import { finishCleanup } from "../../contracts/cleanup.js";
3
4
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
4
5
  import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
5
6
  import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
7
+ import { tryResolveMemoryDevicePath } from "../memory/index.js";
6
8
  import { createDeviceYield, deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
7
9
  import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
8
10
  import { pathNamespace } from "../path-namespace.js";
11
+ import { runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "../staging-ancestry.js";
9
12
  import { openFileDescriptor } from "../descriptor.js";
13
+ import { normalizePath } from "../../contracts/virtual-path.js";
10
14
  const views = new WeakMap();
11
15
  const deviceCapabilities = Object.freeze({
16
+ synchronousStagingResolution: false,
12
17
  readOnly: false, read: true, stat: true, realpath: true, access: true, readdir: false,
13
18
  write: true, append: true, exclusiveCreate: true, streamingRead: true, retainedRead: true,
14
19
  streamingWrite: true, streamingAppend: true, independentWriteStreams: true, copy: true, exclusiveCopy: true,
15
20
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
16
21
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
17
22
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
18
- open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
23
+ open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
19
24
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
20
25
  });
21
26
  function globalCapabilities(filesystem) {
22
27
  const capabilities = { readOnly: false };
23
28
  const optional = {
24
29
  open: ["open"],
30
+ retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
31
+ synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
32
+ synchronousStagingResolution: ["prepareStagingResolution"],
25
33
  trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
26
- atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
34
+ atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
27
35
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
28
36
  streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
29
37
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
@@ -36,6 +44,8 @@ function globalCapabilities(filesystem) {
36
44
  if (key === "copy" || key === "exclusiveCopy")
37
45
  return undefined;
38
46
  let declared = filesystem.capabilities[key];
47
+ if (key === "atomicEntryRemovalReceipt")
48
+ declared = ownedMutationCapabilities(filesystem).atomicEntryRemovalReceipt;
39
49
  if (declared === true && optional[key]?.some(method => typeof filesystem[method] !== "function"))
40
50
  declared = false;
41
51
  if (key === "retainedResize" && filesystem.capabilities.readOnly === true)
@@ -94,8 +104,16 @@ export class DeviceFileSystem {
94
104
  registerEntryAuthority(this, async () => "unknown");
95
105
  return view;
96
106
  }
97
- #resolve(path, options, followFinal = true, resizeCreate) {
98
- return resolveDevicePath(this.#filesystem, path, options, followFinal, resizeCreate);
107
+ #resolve(path, options, followFinal = true, resizeCreate, traversal) {
108
+ return resolveDevicePath(this.#filesystem, path, options, followFinal, resizeCreate, traversal);
109
+ }
110
+ #tryResolveSync(path, options) {
111
+ if (options.signal?.aborted)
112
+ return undefined;
113
+ if (Reflect.get(this.#filesystem, pathNamespace) === undefined) {
114
+ return tryResolveMemoryDevicePath(this.#filesystem, path);
115
+ }
116
+ return undefined;
99
117
  }
100
118
  async #mutable(path, options, followFinal = true) {
101
119
  if (reserved(await this.#resolve(path, options, followFinal)))
@@ -129,10 +147,14 @@ export class DeviceFileSystem {
129
147
  return reserved(resolved) ? resolved : this.#filesystem.canonicalizeMissingTarget?.(path, options);
130
148
  }
131
149
  async capabilitiesFor(path, options = {}) {
150
+ options.signal?.throwIfAborted();
151
+ if (options.stagingResolution === true && (normalizePath(path) !== path || !path.startsWith("/")))
152
+ throw new FsError("EINVAL", { path });
132
153
  let resolved;
133
154
  let selected;
155
+ const traversal = options.stagingResolution === true ? { virtual: false } : undefined;
134
156
  try {
135
- resolved = await this.#resolve(path, options, options.create !== undefined || options.creation !== "exclusive", options.create);
157
+ resolved = await this.#resolve(path, options, options.stagingResolution !== true && (options.create !== undefined || options.creation !== "exclusive"), options.create, traversal);
136
158
  }
137
159
  catch (error) {
138
160
  options.signal?.throwIfAborted();
@@ -160,6 +182,10 @@ export class DeviceFileSystem {
160
182
  return { ...deviceCapabilities, open: false, readdir: true, write: false, append: false,
161
183
  exclusiveCreate: false, streamingWrite: false, streamingAppend: false, independentWriteStreams: false, descriptorWriteStream: false,
162
184
  retainedRead: false, retainedResize: false, streamingRead: false, copy: false, exclusiveCopy: false };
185
+ // This intent cannot bind virtual traversal. Do not infer other path
186
+ // capabilities by resolving the shadowed entry in the backing filesystem.
187
+ if (traversal?.virtual)
188
+ return Object.freeze({ synchronousStagingResolution: false });
163
189
  if (selected === undefined) {
164
190
  const query = this.#filesystem.capabilitiesFor;
165
191
  options.signal?.throwIfAborted();
@@ -169,26 +195,58 @@ export class DeviceFileSystem {
169
195
  const observed = ownedMutationCapabilities(this.#filesystem, selected ?? this.#filesystem.capabilities);
170
196
  options.signal?.throwIfAborted();
171
197
  const capabilities = observed.retainedResize === true ? retainedResizeCapabilities(this.#filesystem, observed) : observed;
172
- const unavailable = {};
173
- if (typeof this.#filesystem.open !== "function")
174
- unavailable.open = false;
175
- if (typeof this.#filesystem.readStream !== "function")
176
- unavailable.streamingRead = false;
198
+ let unavailable;
199
+ if (options.stagingResolution === true && observed.synchronousStagingResolution === true) {
200
+ try {
201
+ const resolution = snapshotStagingResolution(await this.#filesystem.prepareStagingResolution(path, options));
202
+ options.signal?.throwIfAborted();
203
+ if (resolution.path !== resolved)
204
+ throw new FsError("EAGAIN", { path });
205
+ if ([resolution.path, ...resolution.traversed.map(entry => entry.path)].some(entry => entry === deviceDirectory || entry.startsWith(`${deviceDirectory}/`))) {
206
+ (unavailable ??= {}).synchronousStagingResolution = false;
207
+ }
208
+ runStagingGuard(resolution.validate);
209
+ }
210
+ catch (error) {
211
+ options.signal?.throwIfAborted();
212
+ if (!isFsError(error, "ENOTSUP"))
213
+ throw error;
214
+ (unavailable ??= {}).synchronousStagingResolution = false;
215
+ }
216
+ }
217
+ if (resolved.startsWith(`${deviceDirectory}/`)) {
218
+ const u = unavailable ??= {};
219
+ u.atomicStagingAncestry = false;
220
+ u.synchronousDirectoryValidation = false;
221
+ u.synchronousStagingResolution = false;
222
+ }
223
+ if (typeof this.#filesystem.open !== "function" && capabilities.open !== false)
224
+ (unavailable ??= {}).open = false;
225
+ if (typeof this.#filesystem.readStream !== "function" && capabilities.streamingRead !== false)
226
+ (unavailable ??= {}).streamingRead = false;
177
227
  if (typeof this.#filesystem.writeStream !== "function") {
178
- unavailable.streamingWrite = false;
179
- unavailable.streamingAppend = false;
180
- unavailable.descriptorWriteStream = false;
181
- }
182
- if (typeof this.#filesystem.openReadFile !== "function")
183
- unavailable.retainedRead = false;
184
- if (observed.atomicResize === true && (typeof this.#filesystem.resizeFile !== "function" || observed.readOnly === true))
185
- unavailable.atomicResize = false;
186
- const result = Object.keys(unavailable).some(key => capabilities[key] !== false)
187
- ? { ...capabilities, ...unavailable } : capabilities;
228
+ if (capabilities.streamingWrite !== false)
229
+ (unavailable ??= {}).streamingWrite = false;
230
+ if (capabilities.streamingAppend !== false)
231
+ (unavailable ??= {}).streamingAppend = false;
232
+ if (capabilities.descriptorWriteStream !== false)
233
+ (unavailable ??= {}).descriptorWriteStream = false;
234
+ }
235
+ if (typeof this.#filesystem.openReadFile !== "function" && capabilities.retainedRead !== false)
236
+ (unavailable ??= {}).retainedRead = false;
237
+ if (observed.atomicResize === true && (typeof this.#filesystem.resizeFile !== "function" || observed.readOnly === true) && capabilities.atomicResize !== false)
238
+ (unavailable ??= {}).atomicResize = false;
239
+ const result = unavailable ? { ...capabilities, ...unavailable } : capabilities;
188
240
  options.signal?.throwIfAborted();
189
241
  return result;
190
242
  }
191
- async stat(path, options = {}) {
243
+ stat(path, options = {}) {
244
+ if (this.#tryResolveSync(path, options) !== undefined) {
245
+ return this.#filesystem.stat(path, options);
246
+ }
247
+ return this.#statSlow(path, options);
248
+ }
249
+ async #statSlow(path, options) {
192
250
  const resolved = await this.#resolve(path, options);
193
251
  options.signal?.throwIfAborted();
194
252
  if (resolved === nullPath)
@@ -201,7 +259,13 @@ export class DeviceFileSystem {
201
259
  options.signal?.throwIfAborted();
202
260
  return result;
203
261
  }
204
- async lstat(path, options = {}) {
262
+ lstat(path, options = {}) {
263
+ if (this.#tryResolveSync(path, options) !== undefined) {
264
+ return this.#filesystem.lstat(path, options);
265
+ }
266
+ return this.#lstatSlow(path, options);
267
+ }
268
+ async #lstatSlow(path, options) {
205
269
  const resolved = await this.#resolve(path, options, false);
206
270
  options.signal?.throwIfAborted();
207
271
  if (resolved === nullPath)
@@ -217,11 +281,23 @@ export class DeviceFileSystem {
217
281
  compareEntry(path, peer, peerPath, options = {}) {
218
282
  return compareEntries(this, path, peer, peerPath, options);
219
283
  }
220
- async realpath(path, options = {}) {
284
+ realpath(path, options = {}) {
285
+ if (this.#tryResolveSync(path, options) !== undefined) {
286
+ return this.#filesystem.realpath(path, options);
287
+ }
288
+ return this.#realpathSlow(path, options);
289
+ }
290
+ async #realpathSlow(path, options) {
221
291
  const resolved = await this.#resolve(path, options);
222
292
  return reserved(resolved) ? resolved : this.#filesystem.realpath(path, options);
223
293
  }
224
- async readFile(path, options = {}) {
294
+ readFile(path, options = {}) {
295
+ if (this.#tryResolveSync(path, options) !== undefined) {
296
+ return this.#filesystem.readFile(path, options);
297
+ }
298
+ return this.#readFileSlow(path, options);
299
+ }
300
+ async #readFileSlow(path, options) {
225
301
  const resolved = await this.#resolve(path, options);
226
302
  if (resolved === nullPath) {
227
303
  nonnegative(options.maxBytes, path);
@@ -389,7 +465,14 @@ export class DeviceFileSystem {
389
465
  throw new FsError("ENOTSUP", { path });
390
466
  await this.#filesystem.writeStream(path, source, options);
391
467
  }
392
- async readdir(path, options = {}) {
468
+ readdir(path, options = {}) {
469
+ const fast = this.#tryResolveSync(path, options);
470
+ if (fast !== undefined && fast !== "/") {
471
+ return this.#filesystem.readdir(path, options);
472
+ }
473
+ return this.#readdirSlow(path, options);
474
+ }
475
+ async #readdirSlow(path, options) {
393
476
  const resolved = await this.#resolve(path, options);
394
477
  if (resolved === nullPath)
395
478
  throw new FsError("ENOTDIR", { path });
@@ -421,7 +504,13 @@ export class DeviceFileSystem {
421
504
  admitDirectoryEntries(merged.size, limit, path);
422
505
  return [...merged.values()];
423
506
  }
424
- async mkdir(path, options = {}) {
507
+ mkdir(path, options = {}) {
508
+ if (this.#tryResolveSync(path, options) !== undefined) {
509
+ return this.#filesystem.mkdir(path, options);
510
+ }
511
+ return this.#mkdirSlow(path, options);
512
+ }
513
+ async #mkdirSlow(path, options) {
425
514
  const resolved = await this.#resolve(path, options);
426
515
  if (reserved(resolved)) {
427
516
  if (resolved !== nullPath && options.recursive)
@@ -430,7 +519,13 @@ export class DeviceFileSystem {
430
519
  }
431
520
  await this.#filesystem.mkdir(path, options);
432
521
  }
433
- async rm(path, options = {}) {
522
+ rm(path, options = {}) {
523
+ if (this.#tryResolveSync(path, options) !== undefined) {
524
+ return this.#filesystem.rm(path, options);
525
+ }
526
+ return this.#rmSlow(path, options);
527
+ }
528
+ async #rmSlow(path, options) {
434
529
  await this.#mutable(path, options, false);
435
530
  await this.#filesystem.rm(path, options);
436
531
  }
@@ -466,11 +561,16 @@ export class DeviceFileSystem {
466
561
  return this.#filesystem.writeFileConditional(path, data, options);
467
562
  }
468
563
  async removeEntryConditional(path, options) {
564
+ options = { ...options };
565
+ const requested = options.returnRemainingStat === true;
469
566
  await this.#mutable(path, options, false);
470
567
  await requireOwnedMutation(this.#filesystem, path, "atomicEntryRemoval", options);
568
+ if (requested)
569
+ await requireOwnedMutation(this.#filesystem, path, "atomicEntryRemovalReceipt", options);
471
570
  if (!this.#filesystem.removeEntryConditional)
472
571
  throw new FsError("ENOTSUP", { path });
473
- await this.#filesystem.removeEntryConditional(path, options);
572
+ const receipt = await this.#filesystem.removeEntryConditional(path, options);
573
+ return requested && receipt !== undefined ? Object.freeze({ ...receipt }) : undefined;
474
574
  }
475
575
  async removeFileConditional(path, options) {
476
576
  await this.#mutable(path, options, false);
@@ -487,20 +587,95 @@ export class DeviceFileSystem {
487
587
  await this.#filesystem.removeTreeConditional(path, options);
488
588
  }
489
589
  async createStagedFile(path, name, content, options) {
590
+ options = snapshotStagingCreation(options, path);
490
591
  await this.#mutable(path, options, false);
491
592
  await requireOwnedMutation(this.#filesystem, path, "atomicFileStaging", options, true);
593
+ if (options.retainCleanup === true)
594
+ await requireOwnedMutation(this.#filesystem, path, "retainedStagingCleanup", options, true);
492
595
  if (!this.#filesystem.createStagedFile)
493
596
  throw new FsError("ENOTSUP", { path });
494
597
  return this.#filesystem.createStagedFile(path, name, content, options);
495
598
  }
496
599
  async publishStagedFile(staging, destination, options) {
600
+ options.signal?.throwIfAborted();
601
+ if (options.ancestors !== undefined) {
602
+ const entries = snapshotDirectoryAncestry(options.ancestors);
603
+ if (entries.some(entry => entry.path === deviceDirectory || entry.path.startsWith(`${deviceDirectory}/`)))
604
+ throw new FsError("ENOTSUP", { path: destination });
605
+ await requireOwnedMutation(this.#filesystem, destination, "atomicStagingAncestry", options, options.destination === null);
606
+ }
497
607
  for (const path of [staging.directory.path, staging.file.path, destination])
498
608
  await this.#mutable(path, options, false);
499
609
  await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
610
+ await requireOwnedMutation(this.#filesystem, destination, "atomicFileStaging", options, options.destination === null);
500
611
  if (!this.#filesystem.publishStagedFile)
501
612
  throw new FsError("ENOTSUP", { path: destination });
613
+ if (options.commitGuard !== undefined)
614
+ await requireOwnedMutation(this.#filesystem, destination, "guardedStagingPublication", options, options.destination === null);
502
615
  await this.#filesystem.publishStagedFile(staging, destination, options);
503
616
  }
617
+ async prepareDirectoryAncestry(ancestors, options = {}) {
618
+ options.signal?.throwIfAborted();
619
+ try {
620
+ const entries = snapshotDirectoryAncestry(ancestors);
621
+ const controls = options.signal === undefined ? {} : { signal: options.signal };
622
+ controls.signal?.throwIfAborted();
623
+ if (entries.some(entry => entry.path === deviceDirectory || entry.path.startsWith(`${deviceDirectory}/`))
624
+ || !this.#filesystem.prepareDirectoryAncestry) {
625
+ throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry" });
626
+ }
627
+ for (const entry of entries) {
628
+ const declared = await this.#filesystem.capabilitiesFor?.(entry.path, { ...controls, stagingAncestry: true }) ?? this.#filesystem.capabilities;
629
+ controls.signal?.throwIfAborted();
630
+ if (declared.synchronousDirectoryValidation !== true)
631
+ throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry", path: entry.path });
632
+ }
633
+ const guard = await this.#filesystem.prepareDirectoryAncestry(entries, controls);
634
+ controls.signal?.throwIfAborted();
635
+ if (typeof guard !== "function")
636
+ throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry" });
637
+ return () => {
638
+ controls.signal?.throwIfAborted();
639
+ runStagingGuard(guard);
640
+ return true;
641
+ };
642
+ }
643
+ catch (error) {
644
+ options.signal?.throwIfAborted();
645
+ throw error;
646
+ }
647
+ }
648
+ async prepareStagingResolution(path, options = {}) {
649
+ const controls = options.signal === undefined ? {} : { signal: options.signal };
650
+ controls.signal?.throwIfAborted();
651
+ if (normalizePath(path) !== path || !path.startsWith("/"))
652
+ throw new FsError("EINVAL", { path });
653
+ try {
654
+ const traversal = { virtual: false };
655
+ const resolved = await this.#resolve(path, controls, false, undefined, traversal);
656
+ controls.signal?.throwIfAborted();
657
+ if (traversal.virtual)
658
+ throw new FsError("ENOTSUP", { path });
659
+ const declared = ownedMutationCapabilities(this.#filesystem, await this.#filesystem.capabilitiesFor?.(path, { ...controls, stagingResolution: true }) ?? this.#filesystem.capabilities);
660
+ controls.signal?.throwIfAborted();
661
+ if (declared.synchronousStagingResolution !== true || !this.#filesystem.prepareStagingResolution)
662
+ throw new FsError("ENOTSUP", { path });
663
+ const receipt = snapshotStagingResolution(await this.#filesystem.prepareStagingResolution(path, controls));
664
+ controls.signal?.throwIfAborted();
665
+ if (receipt.path !== resolved)
666
+ throw new FsError("EAGAIN", { path });
667
+ if ([receipt.path, ...receipt.traversed.map(entry => entry.path)].some(entry => entry === deviceDirectory || entry.startsWith(`${deviceDirectory}/`))) {
668
+ throw new FsError("ENOTSUP", { path });
669
+ }
670
+ const validate = () => { controls.signal?.throwIfAborted(); runStagingGuard(receipt.validate); return true; };
671
+ runStagingGuard(validate);
672
+ return snapshotStagingResolution({ ...receipt, validate });
673
+ }
674
+ catch (error) {
675
+ controls.signal?.throwIfAborted();
676
+ throw error;
677
+ }
678
+ }
504
679
  async removeStagedFile(staging, options = {}) {
505
680
  for (const path of [staging.directory.path, staging.file.path])
506
681
  await this.#mutable(path, options, false);
@@ -558,7 +733,13 @@ export class DeviceFileSystem {
558
733
  throw new FsError("EISDIR", { path: destination });
559
734
  await this.#filesystem.writeFile(destination, new Uint8Array(), { ...options, flag: options.exclusive ? "wx" : "w" });
560
735
  }
561
- async access(path, mode = 0, options = {}) {
736
+ access(path, mode = 0, options = {}) {
737
+ if (this.#tryResolveSync(path, options) !== undefined) {
738
+ return this.#filesystem.access(path, mode, options);
739
+ }
740
+ return this.#accessSlow(path, mode, options);
741
+ }
742
+ async #accessSlow(path, mode, options) {
562
743
  const resolved = await this.#resolve(path, options);
563
744
  if (resolved === nullPath || resolved === deviceDirectory) {
564
745
  if (!Number.isInteger(mode) || mode < 0 || mode > 7)
@@ -2,4 +2,6 @@ import type { FileSystem, FsOptions } from "../../contracts/filesystem.js";
2
2
  export declare const nullPath = "/dev/null";
3
3
  export declare const deviceDirectory = "/dev";
4
4
  export declare function lexicalDevicePath(path: string): string;
5
- export declare function resolveDevicePath(filesystem: FileSystem, path: string, options: FsOptions, followFinal?: boolean, resizeCreate?: boolean): Promise<string>;
5
+ export declare function resolveDevicePath(filesystem: FileSystem, path: string, options: FsOptions, followFinal?: boolean, resizeCreate?: boolean, traversal?: {
6
+ virtual: boolean;
7
+ }): Promise<string>;