@poe-platform/safe-fs 0.1.629 → 0.1.631

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 (49) hide show
  1. package/dist/safe-fs/bridge/stats.js +1 -1
  2. package/dist/safe-fs/contracts/descriptor.d.ts +31 -0
  3. package/dist/safe-fs/contracts/descriptor.js +1 -0
  4. package/dist/safe-fs/contracts/errors.d.ts +1 -1
  5. package/dist/safe-fs/contracts/errors.js +1 -1
  6. package/dist/safe-fs/contracts/filesystem.d.ts +14 -1
  7. package/dist/safe-fs/core.d.ts +3 -0
  8. package/dist/safe-fs/core.js +2 -0
  9. package/dist/safe-fs/fs/capabilities.d.ts +1 -1
  10. package/dist/safe-fs/fs/capabilities.js +5 -3
  11. package/dist/safe-fs/fs/descriptor.d.ts +21 -0
  12. package/dist/safe-fs/fs/descriptor.js +244 -0
  13. package/dist/safe-fs/fs/devices/index.d.ts +5 -1
  14. package/dist/safe-fs/fs/devices/index.js +53 -4
  15. package/dist/safe-fs/fs/memory/index.d.ts +8 -1
  16. package/dist/safe-fs/fs/memory/index.js +125 -8
  17. package/dist/safe-fs/fs/memory/limits.d.ts +1 -0
  18. package/dist/safe-fs/fs/memory/limits.js +7 -1
  19. package/dist/safe-fs/fs/mount/index.d.ts +5 -1
  20. package/dist/safe-fs/fs/mount/index.js +69 -6
  21. package/dist/safe-fs/fs/overlay/index.d.ts +2 -0
  22. package/dist/safe-fs/fs/overlay/index.js +9 -1
  23. package/dist/safe-fs/fs/quota/index.js +5 -1
  24. package/dist/safe-fs/fs/readonly/index.d.ts +2 -0
  25. package/dist/safe-fs/fs/readonly/index.js +35 -2
  26. package/dist/safe-fs/fs/real/index.d.ts +3 -0
  27. package/dist/safe-fs/fs/real/index.js +73 -3
  28. package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
  29. package/dist/safe-fs/fs/s3/filesystem.js +5 -0
  30. package/dist/safe-fs/fs/scoped.js +68 -17
  31. package/dist/safe-fs/fs/webdav/webdav.d.ts +3 -0
  32. package/dist/safe-fs/fs/webdav/webdav.js +5 -0
  33. package/dist/safe-fs/index.d.ts +3 -0
  34. package/dist/safe-fs/index.js +2 -0
  35. package/dist/safe-fs/python/emscripten.d.ts +14 -0
  36. package/dist/safe-fs/python/emscripten.js +354 -0
  37. package/dist/safe-fs/python/filesystem.d.ts +51 -0
  38. package/dist/safe-fs/python/filesystem.js +216 -0
  39. package/dist/safe-fs/python/flags.d.ts +3 -0
  40. package/dist/safe-fs/python/flags.js +14 -0
  41. package/dist/safe-fs/python/index.d.ts +7 -0
  42. package/dist/safe-fs/python/index.js +4 -0
  43. package/dist/safe-fs/python/request.d.ts +2 -0
  44. package/dist/safe-fs/python/request.js +61 -0
  45. package/dist/safe-fs/python/stat.d.ts +8 -0
  46. package/dist/safe-fs/python/stat.js +31 -0
  47. package/dist/safe-fs/xml.d.ts +3 -0
  48. package/dist/safe-fs/xml.js +41 -19
  49. package/package.json +5 -1
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  import { type MemoryFileSystemOptions } from "./limits.js";
4
4
  export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
@@ -9,6 +9,7 @@ export declare class MemoryFileSystem implements FileSystem {
9
9
  readdir: true;
10
10
  realpath: true;
11
11
  access: true;
12
+ open: true;
12
13
  write: true;
13
14
  append: true;
14
15
  exclusiveCreate: true;
@@ -35,6 +36,7 @@ export declare class MemoryFileSystem implements FileSystem {
35
36
  atomicRename: true;
36
37
  atomicFileStaging: true;
37
38
  atomicFileMutation: true;
39
+ atomicEntryRemoval: true;
38
40
  atomicDirectoryMetadata: true;
39
41
  streamingRead: true;
40
42
  retainedRead: true;
@@ -46,6 +48,7 @@ export declare class MemoryFileSystem implements FileSystem {
46
48
  private nextInode;
47
49
  private readonly ledger;
48
50
  private readonly root;
51
+ private totalBytes;
49
52
  constructor(options?: MemoryFileSystemOptions);
50
53
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
51
54
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
@@ -55,6 +58,7 @@ export declare class MemoryFileSystem implements FileSystem {
55
58
  private releaseNode;
56
59
  private releaseReference;
57
60
  private replaceData;
61
+ private admitSize;
58
62
  private fail;
59
63
  private validatePath;
60
64
  private permission;
@@ -68,6 +72,7 @@ export declare class MemoryFileSystem implements FileSystem {
68
72
  private snapshot;
69
73
  private bytes;
70
74
  private allocate;
75
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
71
76
  private prepareWrite;
72
77
  private openWrite;
73
78
  private writeData;
@@ -80,6 +85,7 @@ export declare class MemoryFileSystem implements FileSystem {
80
85
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
81
86
  prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
82
87
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
88
+ removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
83
89
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
84
90
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
85
91
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
@@ -89,6 +95,7 @@ export declare class MemoryFileSystem implements FileSystem {
89
95
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
90
96
  rmdir(path: string, options?: FsOptions): Promise<void>;
91
97
  rm(path: string, options?: RemoveOptions): Promise<void>;
98
+ unlink(path: string, options?: FsOptions): Promise<void>;
92
99
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
93
100
  copyFile(source: string, destination: string, options?: CopyFileOptions): Promise<void>;
94
101
  realpath(path: string, options?: FsOptions): Promise<string>;
@@ -3,11 +3,13 @@ import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority
3
3
  import { getOwnedS3Entry } from "../s3/registry.js";
4
4
  import { getOwnedWebDavEntry } from "../webdav/resource-id.js";
5
5
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
+ import { openFileDescriptor } from "../descriptor.js";
6
7
  import { resolveMissingTarget } from "./missing-target.js";
7
8
  import { MemoryAllocation, MemoryLedger } from "./ledger.js";
8
9
  import { normalizeMemoryFileSystemLimits } from "./limits.js";
9
10
  export { defaultMemoryFileSystemLimits } from "./limits.js";
10
11
  const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
12
+ const preferredIoBlockSize = 64 * 1024;
11
13
  const ext4HtreeEof64 = (1n << 63n) - 1n;
12
14
  const ownedStats = new WeakMap();
13
15
  const ownedStores = new WeakMap();
@@ -55,7 +57,7 @@ const compareOwnedMemory = async (own, peer, options) => {
55
57
  export class MemoryFileSystem {
56
58
  capabilities = ((filesystem) => {
57
59
  return Object.freeze({
58
- read: true, stat: true, readdir: true, realpath: true, access: true,
60
+ read: true, stat: true, readdir: true, realpath: true, access: true, open: true,
59
61
  write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
60
62
  mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
61
63
  rename: true, atomicRenameNoReplace: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
@@ -66,7 +68,7 @@ export class MemoryFileSystem {
66
68
  permissions: true,
67
69
  timestamps: true,
68
70
  atomicRename: true,
69
- atomicFileStaging: true, atomicFileMutation: true,
71
+ atomicFileStaging: true, atomicFileMutation: true, atomicEntryRemoval: true,
70
72
  atomicDirectoryMetadata: true,
71
73
  streamingRead: true,
72
74
  retainedRead: true,
@@ -79,6 +81,7 @@ export class MemoryFileSystem {
79
81
  nextInode = 1;
80
82
  ledger;
81
83
  root;
84
+ totalBytes = 0;
82
85
  constructor(options = {}) {
83
86
  this.ledger = new MemoryLedger(normalizeMemoryFileSystemLimits(options));
84
87
  this.ledger.reserve(0, 1, "mkdir", "/");
@@ -129,7 +132,11 @@ export class MemoryFileSystem {
129
132
  this.ledger.reserve(bytes, 2, syscall, path);
130
133
  try {
131
134
  const node = create();
135
+ if (node.type === "file")
136
+ this.admitSize(undefined, node.data.byteLength, syscall, path);
132
137
  parent.entries.set(name, node);
138
+ if (node.type === "file")
139
+ this.totalBytes += node.data.byteLength;
133
140
  this.changed(parent);
134
141
  return node;
135
142
  }
@@ -142,8 +149,10 @@ export class MemoryFileSystem {
142
149
  if (node.nlink !== 0 || node.references !== 0)
143
150
  return;
144
151
  this.ledger.release(node.type === "symlink" ? node.target.length * 2 : 0, 1);
145
- if (node.type === "file")
152
+ if (node.type === "file") {
153
+ this.totalBytes -= node.data.byteLength;
146
154
  node.allocation.release();
155
+ }
147
156
  }
148
157
  releaseReference(node, path) {
149
158
  node.references--;
@@ -152,10 +161,17 @@ export class MemoryFileSystem {
152
161
  }
153
162
  replaceData(node, allocation, length = allocation.data.byteLength) {
154
163
  const previous = node.allocation;
164
+ this.totalBytes += length - node.data.byteLength;
155
165
  node.data = allocation.data.subarray(0, length);
156
166
  node.allocation = allocation;
157
167
  previous.release();
158
168
  }
169
+ admitSize(node, length, syscall, path) {
170
+ this.ledger.fileSize(length, syscall, path);
171
+ if (length - (node?.data.byteLength ?? 0) > (this.ledger.limits.maxBytes ?? Number.MAX_SAFE_INTEGER) - this.totalBytes) {
172
+ this.fail("ENOSPC", syscall, path);
173
+ }
174
+ }
159
175
  fail(code, syscall, path, dest) {
160
176
  throw new FsError(code, { syscall, path, ...(dest === undefined ? {} : { dest }) });
161
177
  }
@@ -262,6 +278,7 @@ export class MemoryFileSystem {
262
278
  snapshot(node) {
263
279
  return {
264
280
  type: node.type,
281
+ ioBlockSize: preferredIoBlockSize,
265
282
  ...(Number.isSafeInteger(node.revision) ? { revision: node.revision } : {}),
266
283
  preferredIoBlockSize: 4096,
267
284
  size: node.type === "file" ? node.data.byteLength
@@ -295,6 +312,72 @@ export class MemoryFileSystem {
295
312
  throw new FsError("EFBIG", { syscall, path, cause });
296
313
  }
297
314
  }
315
+ open(path, options) {
316
+ return openFileDescriptor(path, options, {
317
+ positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
318
+ }, async (admitted) => {
319
+ const location = this.resolve(path, "open", {
320
+ followFinal: admitted.creation !== "exclusive", allowMissing: admitted.creation !== "never",
321
+ });
322
+ let node = location.node;
323
+ if (node) {
324
+ if (admitted.creation === "exclusive")
325
+ this.fail("EEXIST", "open", path);
326
+ if (node.type !== "file")
327
+ this.fail("EISDIR", "open", path);
328
+ this.permission(node, admitted.access === "read" ? 4 : admitted.access === "write" ? 2 : 6, "open", path);
329
+ }
330
+ else {
331
+ this.permission(location.parent, 3, "open", path);
332
+ }
333
+ this.ledger.reserve(path.length * 2, 1, "open", path);
334
+ try {
335
+ node ??= this.openWrite(path, {}, "open", { location, mode: admitted.mode, append: false });
336
+ if (admitted.truncate)
337
+ this.resizeNode(node, 0, "open", path);
338
+ }
339
+ catch (error) {
340
+ this.ledger.release(path.length * 2, 1);
341
+ throw error;
342
+ }
343
+ node.references++;
344
+ const resource = { node, position: 0 };
345
+ return {
346
+ resource,
347
+ getPosition: async (retained) => retained.position,
348
+ stat: async (retained) => this.snapshot(retained.node),
349
+ read: async (retained, buffer, position) => {
350
+ const inode = retained.node;
351
+ const start = position ?? retained.position;
352
+ const count = Math.min(buffer.byteLength, Math.max(0, inode.data.byteLength - start));
353
+ buffer.set(inode.data.subarray(start, start + count));
354
+ if (position === null)
355
+ retained.position += count;
356
+ inode.atimeMs = Date.now();
357
+ return count;
358
+ },
359
+ write: async (retained, buffer, position) => {
360
+ const inode = retained.node;
361
+ const start = admitted.append ? inode.data.byteLength : position ?? retained.position;
362
+ const end = start + buffer.byteLength;
363
+ this.writeAt(inode, buffer, start, "write", path);
364
+ if (position === null)
365
+ retained.position = end;
366
+ return buffer.byteLength;
367
+ },
368
+ truncate: async (retained, length) => {
369
+ const inode = retained.node;
370
+ this.resizeNode(inode, length, "ftruncate", path);
371
+ },
372
+ sync: async () => { },
373
+ close: async (retained) => {
374
+ const inode = retained.node;
375
+ retained.node = undefined;
376
+ this.releaseReference(inode, path);
377
+ },
378
+ };
379
+ });
380
+ }
298
381
  prepareWrite(path, options, syscall) {
299
382
  const flag = options.flag ?? "w";
300
383
  if (!["w", "wx", "a", "ax"].includes(flag))
@@ -329,7 +412,7 @@ export class MemoryFileSystem {
329
412
  const target = this.prepareWrite(path, options, syscall);
330
413
  const current = target.location.node;
331
414
  const length = (target.append ? current?.data.byteLength ?? 0 : 0) + data.byteLength;
332
- this.ledger.fileSize(length, syscall, path);
415
+ this.admitSize(current, length, syscall, path);
333
416
  const growth = target.append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
334
417
  this.ledger.check(data.byteLength + growth + (current ? 0 : target.location.name.length * 2), current ? 0 : 2, syscall, path);
335
418
  const copied = this.bytes(data, syscall, path);
@@ -373,6 +456,7 @@ export class MemoryFileSystem {
373
456
  const end = position + data.byteLength;
374
457
  this.ledger.fileSize(end, syscall, path);
375
458
  const length = Math.max(node.data.byteLength, end);
459
+ this.admitSize(node, length, syscall, path);
376
460
  let allocation = node.allocation;
377
461
  if (length > allocation.data.byteLength) {
378
462
  this.ledger.check(length, 0, syscall, path);
@@ -394,8 +478,10 @@ export class MemoryFileSystem {
394
478
  }
395
479
  if (allocation !== node.allocation)
396
480
  this.replaceData(node, allocation, length);
397
- else
481
+ else {
482
+ this.totalBytes += length - node.data.byteLength;
398
483
  node.data = allocation.data.subarray(0, length);
484
+ }
399
485
  this.changed(node);
400
486
  }
401
487
  async readFile(path, options = {}) {
@@ -460,6 +546,8 @@ export class MemoryFileSystem {
460
546
  this.fail("EINVAL", "createStagedFile", directoryPath);
461
547
  if (content.type === "file" && !(content.data instanceof Uint8Array))
462
548
  throw new TypeError("Staged files require Uint8Array data");
549
+ if (content.type === "file")
550
+ this.admitSize(undefined, content.data.byteLength, "createStagedFile", directoryPath);
463
551
  if (content.type === "symlink")
464
552
  this.validatePath(content.target, "createStagedFile");
465
553
  const retained = (location.name.length + name.length) * 2 + (content.type === "symlink" ? content.target.length * 2 : 0);
@@ -480,6 +568,8 @@ export class MemoryFileSystem {
480
568
  file.mtimeMs = options.mtimeMs;
481
569
  directory.entries.set(name, file);
482
570
  location.parent.entries.set(location.name, directory);
571
+ if (file.type === "file")
572
+ this.totalBytes += file.data.byteLength;
483
573
  }
484
574
  catch (error) {
485
575
  allocation?.release();
@@ -566,6 +656,29 @@ export class MemoryFileSystem {
566
656
  this.writeData(path, data, { ...(options.mode === undefined ? {} : { mode: options.mode }), flag: options.append ? "a" : "w" }, "writeFileConditional");
567
657
  return Object.freeze(this.snapshot(location.parent.entries.get(location.name)));
568
658
  }
659
+ async removeEntryConditional(path, options) {
660
+ options.signal?.throwIfAborted();
661
+ const location = this.entry(path, "removeEntryConditional", true);
662
+ this.expectEntry(location.parent, options.parent, path, false);
663
+ this.expectEntry(location.node, options.expected, path);
664
+ const node = location.node;
665
+ if (this.terminalDot(path))
666
+ this.fail("EINVAL", "removeEntryConditional", path);
667
+ if (node === this.root)
668
+ this.fail("EBUSY", "removeEntryConditional", path);
669
+ if (node.type !== "file" && node.type !== "symlink" && node.type !== "directory")
670
+ this.fail("EINVAL", "removeEntryConditional", path);
671
+ this.permission(location.parent, 3, "removeEntryConditional", path);
672
+ if (node.type === "directory" && node.entries.size > 0)
673
+ this.fail("ENOTEMPTY", "removeEntryConditional", path);
674
+ location.parent.entries.delete(location.name);
675
+ this.ledger.release(location.name.length * 2, 1);
676
+ node.nlink--;
677
+ node.ctimeMs = Date.now();
678
+ node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
679
+ this.releaseNode(node);
680
+ this.changed(location.parent);
681
+ }
569
682
  async removeFileConditional(path, options) {
570
683
  options.signal?.throwIfAborted();
571
684
  const location = this.entry(path, "removeFileConditional", true);
@@ -692,6 +805,10 @@ export class MemoryFileSystem {
692
805
  }
693
806
  this.changed(location.parent);
694
807
  }
808
+ unlink(path, options = {}) {
809
+ // Do not allow an untyped recursive option to widen final-entry removal.
810
+ return this.rm(path, { ...(options.signal === undefined ? {} : { signal: options.signal }) });
811
+ }
695
812
  async rename(source, destination, options = {}) {
696
813
  options.signal?.throwIfAborted();
697
814
  try {
@@ -762,7 +879,6 @@ export class MemoryFileSystem {
762
879
  this.fail("EINVAL", "copyFile", source, destination);
763
880
  const node = this.writeData(destination, origin.data, { mode: origin.mode & 0o7777, flag: options.exclusive ? "wx" : "w" }, "copyFile");
764
881
  node.mode = origin.mode;
765
- this.changed(node);
766
882
  origin.atimeMs = Date.now();
767
883
  }
768
884
  catch (error) {
@@ -847,6 +963,7 @@ export class MemoryFileSystem {
847
963
  this.resizeNode(node, length, "truncate", path);
848
964
  }
849
965
  resizeNode(node, length, syscall, path) {
966
+ this.admitSize(node, length, syscall, path);
850
967
  const data = this.allocate(length, syscall, path);
851
968
  try {
852
969
  data.data.set(node.data.subarray(0, length));
@@ -987,7 +1104,7 @@ export class MemoryFileSystem {
987
1104
  async *readStream(path, options = {}) {
988
1105
  options.signal?.throwIfAborted();
989
1106
  const start = options.start ?? 0;
990
- const chunkSize = options.chunkSize ?? 64 * 1024;
1107
+ const chunkSize = options.chunkSize ?? preferredIoBlockSize;
991
1108
  this.integer(start, "readStream", path);
992
1109
  this.integer(chunkSize, "readStream", path);
993
1110
  if (chunkSize === 0)
@@ -1060,7 +1177,7 @@ function stockDescriptorWrite(filesystem) {
1060
1177
  return false;
1061
1178
  for (const name of ["writeStream", "writeFile", "appendFile", "access", "stat", "lstat", "realpath",
1062
1179
  "openWrite", "prepareWrite", "writeData", "addNode", "replaceData", "releaseReference", "releaseNode",
1063
- "resolve", "permission", "validatePath", "mode", "bytes", "allocate", "changed", "metadata", "integer", "writeAt", "fail"]) {
1180
+ "resolve", "permission", "validatePath", "mode", "bytes", "allocate", "admitSize", "changed", "metadata", "integer", "writeAt", "fail"]) {
1064
1181
  const descriptor = Object.getOwnPropertyDescriptor(filesystem, name)
1065
1182
  ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
1066
1183
  if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
@@ -1,4 +1,5 @@
1
1
  export interface MemoryFileSystemLimits {
2
+ readonly maxBytes?: number;
2
3
  readonly maxFileBytes: number;
3
4
  readonly maxRetainedBytes: number;
4
5
  readonly maxMetadataUnits: number;
@@ -6,8 +6,14 @@ export const defaultMemoryFileSystemLimits = Object.freeze({
6
6
  });
7
7
  export function normalizeMemoryFileSystemLimits(options) {
8
8
  const keys = ["maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
9
- const record = readConfigRecord(options, "memory option", keys);
9
+ const record = readConfigRecord(options, "memory option", [...keys, "maxBytes"]);
10
10
  const limits = { ...defaultMemoryFileSystemLimits };
11
+ if (Object.hasOwn(record, "maxBytes") && record.maxBytes !== undefined) {
12
+ if (typeof record.maxBytes !== "number" || !Number.isSafeInteger(record.maxBytes) || record.maxBytes < 0) {
13
+ throw new RangeError("maxBytes must be a nonnegative safe integer");
14
+ }
15
+ limits.maxBytes = record.maxBytes;
16
+ }
11
17
  for (const key of keys) {
12
18
  const value = Object.hasOwn(record, key) ? record[key] : limits[key];
13
19
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (key === "maxMetadataUnits" ? 1 : 0)) {
@@ -1,5 +1,6 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
+ import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
3
4
  export interface MountFileSystemOptions {
4
5
  readonly root: FileSystem;
5
6
  readonly mounts?: Readonly<Record<string, FileSystem>>;
@@ -23,6 +24,7 @@ export declare class MountFileSystem implements FileSystem {
23
24
  private entryPath;
24
25
  private optional;
25
26
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
27
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
26
28
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
27
29
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
28
30
  stat(path: string, options?: FsOptions): Promise<FileStat>;
@@ -30,9 +32,11 @@ export declare class MountFileSystem implements FileSystem {
30
32
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
31
33
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
32
34
  rmdir(path: string, options?: FsOptions): Promise<void>;
35
+ unlink(path: string, options?: FsOptions): Promise<void>;
33
36
  rm(path: string, options?: RemoveOptions): Promise<void>;
34
37
  private localStaging;
35
38
  writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
39
+ removeEntryConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
36
40
  removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
37
41
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
38
42
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
@@ -4,6 +4,7 @@ import { finishCleanup } from "../../contracts/cleanup.js";
4
4
  import { requireOwnedMutation, ownedMutationCapabilities, openRetainedReadFile, openRetainedResizeFile, readOnlyCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
5
5
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
6
  import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
7
+ import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
7
8
  import { pathNamespace } from "../path-namespace.js";
8
9
  import { compareIdentity } from "./identity.js";
9
10
  import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
@@ -21,16 +22,19 @@ function fail(code) {
21
22
  throw new FsError(code);
22
23
  }
23
24
  function snapshotStat(stat) {
24
- const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, nlink, uid, gid } = stat;
25
+ const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
25
26
  return {
26
27
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
27
28
  ...(revision === undefined ? {} : { revision }),
28
29
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
30
+ ...(ioBlockSize === undefined ? {} : { ioBlockSize }),
29
31
  ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
30
32
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
31
33
  ...(identityScope === undefined ? {} : { identityScope }),
32
34
  ...(ino === undefined ? {} : { ino }),
33
35
  ...(dev === undefined ? {} : { dev }),
36
+ ...(rdevMajor === undefined ? {} : { rdevMajor }),
37
+ ...(rdevMinor === undefined ? {} : { rdevMinor }),
34
38
  ...(nlink === undefined ? {} : { nlink }),
35
39
  ...(uid === undefined ? {} : { uid }),
36
40
  ...(gid === undefined ? {} : { gid }),
@@ -91,13 +95,14 @@ export class MountFileSystem {
91
95
  : mounts.every(({ backend }) => backend.capabilities.append === false) ? false : undefined;
92
96
  const common = (capability) => {
93
97
  const optional = {
94
- atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
98
+ open: ["open"],
99
+ atomicEntryRemoval: ["removeEntryConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
95
100
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
96
101
  descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
97
102
  };
98
103
  const values = mounts.map(({ backend }) => {
99
104
  if (backend.capabilities.readOnly === true
100
- && !["read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories"].includes(capability))
105
+ && !["open", "read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories"].includes(capability))
101
106
  return false;
102
107
  const declared = backend.capabilities[capability];
103
108
  if (capability === "descriptorWriteStream" && backend.capabilities.streamingWrite === false)
@@ -109,7 +114,7 @@ export class MountFileSystem {
109
114
  return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
110
115
  };
111
116
  const semantics = Object.fromEntries([
112
- "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access",
117
+ "atomicEntryRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open",
113
118
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
114
119
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
115
120
  "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "atomicResize", "symlinks", "hardlinks", "permissions", "timestamps",
@@ -141,8 +146,9 @@ export class MountFileSystem {
141
146
  ? { ...observed, descriptorWriteStream: false } : observed;
142
147
  const resize = declared.atomicResize === true && (typeof location.mount.backend.resizeFile !== "function" || declared.readOnly === true)
143
148
  ? { ...declared, atomicResize: false } : declared;
144
- const capabilities = location.synthetic ? { ...resize, retainedRead: false }
145
- : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, resize));
149
+ const withOpen = { ...resize, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
150
+ const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
151
+ : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
146
152
  if (location.synthetic)
147
153
  return readOnlyCapabilities(capabilities);
148
154
  if (this.mounts.length === 1 || capabilities.readOnly === true)
@@ -413,6 +419,38 @@ export class MountFileSystem {
413
419
  return location.mount.backend.readFile(location.local, options);
414
420
  });
415
421
  }
422
+ open(path, options) {
423
+ return openFileDescriptor(globalPath(path), options, {
424
+ positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
425
+ }, async (admitted) => {
426
+ try {
427
+ const location = await this.resolve(path, admitted, {
428
+ allowMissing: admitted.creation !== "never", followFinal: admitted.creation !== "exclusive",
429
+ });
430
+ if (location.synthetic)
431
+ fail("EISDIR");
432
+ if (admitted.access !== "read" || admitted.creation !== "never" || admitted.truncate || admitted.append)
433
+ this.mutable(location);
434
+ const backend = location.mount.backend;
435
+ const capabilities = await backend.capabilitiesFor?.(location.local, admitted) ?? backend.capabilities;
436
+ if (!backend.open || capabilities.open === false)
437
+ fail("ENOTSUP");
438
+ admitted.signal?.throwIfAborted();
439
+ const descriptor = await backend.open(location.local, admitted);
440
+ try {
441
+ return forwardFileDescriptor(descriptor, (syscall, forwarded, action) => this.operation(syscall, path, forwarded, action), descriptor.capabilities, snapshotStat);
442
+ }
443
+ catch (error) {
444
+ await finishCleanup(() => descriptor.close(), true);
445
+ throw error;
446
+ }
447
+ }
448
+ catch (error) {
449
+ admitted.signal?.throwIfAborted();
450
+ throw this.error(error, "open", path, admitted);
451
+ }
452
+ });
453
+ }
416
454
  writeFile(path, data, options = {}) {
417
455
  return this.operation("writeFile", path, options, async () => {
418
456
  const location = await this.resolve(path, options, { allowMissing: true, followFinal: !options.flag?.endsWith("x") });
@@ -499,6 +537,18 @@ export class MountFileSystem {
499
537
  await backend.rmdir(location.local, options);
500
538
  });
501
539
  }
540
+ unlink(path, options = {}) {
541
+ return this.operation("unlink", path, options, async () => {
542
+ const location = await this.resolve(path, options, { followFinal: false, entry: true });
543
+ this.mutable(location);
544
+ this.entryPath(path);
545
+ const backend = location.mount.backend;
546
+ if (!backend.unlink)
547
+ fail("ENOTSUP");
548
+ options.signal?.throwIfAborted();
549
+ await backend.unlink(location.local, options);
550
+ });
551
+ }
502
552
  rm(path, options = {}) {
503
553
  return this.operation("rm", path, options, async () => {
504
554
  let location;
@@ -544,6 +594,19 @@ export class MountFileSystem {
544
594
  return snapshotStat(await backend.writeFileConditional(location.local, data, options));
545
595
  }, undefined, true);
546
596
  }
597
+ removeEntryConditional(path, options) {
598
+ return this.operation("removeEntryConditional", path, options, async () => {
599
+ const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
600
+ if (this.protected(location.path))
601
+ fail("EBUSY");
602
+ this.mutable(location);
603
+ const backend = location.mount.backend;
604
+ await requireOwnedMutation(backend, location.local, "atomicEntryRemoval", options);
605
+ if (!backend.removeEntryConditional)
606
+ fail("ENOTSUP");
607
+ await backend.removeEntryConditional(location.local, options);
608
+ });
609
+ }
547
610
  removeFileConditional(path, options) {
548
611
  return this.operation("removeFileConditional", path, options, async () => {
549
612
  const location = await this.resolve(path, options, { followFinal: false, entry: true, allowMissing: true });
@@ -1,4 +1,5 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
+ import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
3
  import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
4
  export interface OverlayFileSystemOptions {
4
5
  readonly upper: FileSystem;
@@ -17,6 +18,7 @@ export declare class OverlayFileSystem implements FileSystem {
17
18
  private readonly linkOrigins;
18
19
  private queue;
19
20
  constructor(options: OverlayFileSystemOptions);
21
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
20
22
  private run;
21
23
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
22
24
  openReadFile(path: string, options?: OpenReadFileOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
@@ -8,16 +8,19 @@ import { compareIdentity } from "../mount/identity.js";
8
8
  import { compareEntries, registerEntryView } from "../mount/comparison.js";
9
9
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
10
10
  function snapshotStat(stat) {
11
- const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, nlink, uid, gid } = stat;
11
+ const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
12
12
  return {
13
13
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
14
14
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
15
+ ...(ioBlockSize === undefined ? {} : { ioBlockSize }),
15
16
  ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
16
17
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
17
18
  ...(revision === undefined ? {} : { revision }),
18
19
  ...(identityScope === undefined ? {} : { identityScope }),
19
20
  ...(ino === undefined ? {} : { ino }),
20
21
  ...(dev === undefined ? {} : { dev }),
22
+ ...(rdevMajor === undefined ? {} : { rdevMajor }),
23
+ ...(rdevMinor === undefined ? {} : { rdevMinor }),
21
24
  ...(nlink === undefined ? {} : { nlink }),
22
25
  ...(uid === undefined ? {} : { uid }),
23
26
  ...(gid === undefined ? {} : { gid }),
@@ -114,6 +117,7 @@ export class OverlayFileSystem {
114
117
  ].filter(([, value]) => value !== undefined));
115
118
  this.capabilities = Object.freeze({
116
119
  ...semantics,
120
+ open: false,
117
121
  atomicFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
118
122
  implicitDirectories: false,
119
123
  readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
@@ -135,6 +139,10 @@ export class OverlayFileSystem {
135
139
  });
136
140
  Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
137
141
  }
142
+ async open(path, options) {
143
+ options.signal?.throwIfAborted();
144
+ throw new FsError("ENOTSUP", { syscall: "open", path });
145
+ }
138
146
  async run(options, operation, cleanup = true) {
139
147
  options.signal?.throwIfAborted();
140
148
  const previous = this.queue;
@@ -183,6 +183,10 @@ export function withFileSystemQuota(fs, options) {
183
183
  throw new FileSystemQuotaError(options.maxBytes);
184
184
  };
185
185
  const mutations = {
186
+ async open(path, openOptions) {
187
+ openOptions.signal?.throwIfAborted();
188
+ throw new FsError("ENOTSUP", { syscall: "open", path });
189
+ },
186
190
  openResizeFile(path, resizeOptions = {}) {
187
191
  return mutate(async () => {
188
192
  let retained;
@@ -316,7 +320,7 @@ export function withFileSystemQuota(fs, options) {
316
320
  // capabilities and methods without violating invariants on own properties.
317
321
  return new Proxy(Object.create(fs), {
318
322
  get(_target, property) {
319
- if (property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
323
+ if (property === "removeEntryConditional" || property === "unlink" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
320
324
  return undefined;
321
325
  if (property === "capabilities")
322
326
  return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs)));
@@ -1,10 +1,12 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
2
  import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileResizeHandle, FileStat, OpenReadFileOptions, OpenResizeFileOptions, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
+ import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
3
4
  export declare class ReadOnlyFileSystem implements FileSystem {
4
5
  #private;
5
6
  constructor(filesystem: FileSystem);
6
7
  get capabilities(): FileSystemCapabilities;
7
8
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
9
+ open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
8
10
  openReadFile(path: string, options?: OpenReadFileOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
9
11
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
10
12
  stat(path: string, options?: FsOptions): Promise<FileStat>;