@poe-platform/safe-fs 0.1.512 → 0.1.513

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 (31) hide show
  1. package/dist/safe-fs/bridge/stats.js +1 -1
  2. package/dist/safe-fs/contracts/errors.d.ts +1 -0
  3. package/dist/safe-fs/contracts/errors.js +1 -0
  4. package/dist/safe-fs/contracts/filesystem.d.ts +22 -2
  5. package/dist/safe-fs/fs/capabilities.d.ts +4 -2
  6. package/dist/safe-fs/fs/capabilities.js +80 -9
  7. package/dist/safe-fs/fs/devices/index.d.ts +4 -3
  8. package/dist/safe-fs/fs/devices/index.js +75 -16
  9. package/dist/safe-fs/fs/devices/path.d.ts +1 -1
  10. package/dist/safe-fs/fs/devices/path.js +111 -7
  11. package/dist/safe-fs/fs/memory/index.d.ts +5 -2
  12. package/dist/safe-fs/fs/memory/index.js +107 -10
  13. package/dist/safe-fs/fs/mount/index.d.ts +4 -3
  14. package/dist/safe-fs/fs/mount/index.js +66 -14
  15. package/dist/safe-fs/fs/overlay/index.d.ts +3 -3
  16. package/dist/safe-fs/fs/overlay/index.js +9 -3
  17. package/dist/safe-fs/fs/path-namespace.d.ts +4 -0
  18. package/dist/safe-fs/fs/path-namespace.js +63 -0
  19. package/dist/safe-fs/fs/quota/index.js +142 -6
  20. package/dist/safe-fs/fs/readonly/index.d.ts +4 -3
  21. package/dist/safe-fs/fs/readonly/index.js +14 -1
  22. package/dist/safe-fs/fs/real/index.d.ts +3 -2
  23. package/dist/safe-fs/fs/real/index.js +177 -8
  24. package/dist/safe-fs/fs/scoped.js +111 -13
  25. package/dist/safe-fs/native/fs-seek/linux-x64-glibc.node +0 -0
  26. package/dist/safe-fs/native/fs-seek/loader.d.ts +5 -0
  27. package/dist/safe-fs/native/fs-seek/loader.mjs +120 -0
  28. package/dist/safe-fs/native/fs-seek/manifest.json +46 -0
  29. package/dist/safe-fs/node/native-seek.d.ts +3 -0
  30. package/dist/safe-fs/node/native-seek.js +54 -0
  31. package/package.json +7 -1
@@ -8,6 +8,7 @@ import { MemoryAllocation, MemoryLedger } from "./ledger.js";
8
8
  import { normalizeMemoryFileSystemLimits } from "./limits.js";
9
9
  export { defaultMemoryFileSystemLimits } from "./limits.js";
10
10
  const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
11
+ const ext4HtreeEof64 = (1n << 63n) - 1n;
11
12
  const ownedStats = new WeakMap();
12
13
  const ownedStores = new WeakMap();
13
14
  const registeredAuthorities = new WeakSet();
@@ -67,6 +68,7 @@ export class MemoryFileSystem {
67
68
  atomicRename: true,
68
69
  streamingRead: true,
69
70
  retainedRead: true,
71
+ get retainedResize() { return stockRetainedResize(filesystem); },
70
72
  streamingWrite: true,
71
73
  get descriptorWriteStream() { return stockDescriptorWrite(filesystem); },
72
74
  });
@@ -82,6 +84,7 @@ export class MemoryFileSystem {
82
84
  const root = this.root;
83
85
  ownedStores.set(this, {
84
86
  root,
87
+ ledger: this.ledger,
85
88
  capabilities: this.capabilities,
86
89
  intact: () => this.root === root,
87
90
  });
@@ -181,7 +184,7 @@ export class MemoryFileSystem {
181
184
  this.validatePath(path, syscall);
182
185
  const pending = path.split("/").filter(Boolean);
183
186
  if (path.endsWith("/"))
184
- pending.push(".");
187
+ pending.push("");
185
188
  const stack = [{ node: this.root, name: "" }];
186
189
  let links = 0;
187
190
  while (pending.length > 0) {
@@ -189,8 +192,9 @@ export class MemoryFileSystem {
189
192
  const current = stack[stack.length - 1].node;
190
193
  if (current.type !== "directory")
191
194
  this.fail("ENOTDIR", syscall, path);
192
- this.permission(current, 1, syscall, path);
193
- if (component === ".")
195
+ if (!(component === "" && pending.length === 0 && options.resizeCreate !== undefined))
196
+ this.permission(current, 1, syscall, path);
197
+ if (component === "." || component === "")
194
198
  continue;
195
199
  if (component === "..") {
196
200
  if (stack.length > 1)
@@ -199,6 +203,8 @@ export class MemoryFileSystem {
199
203
  }
200
204
  if (new TextEncoder().encode(component).byteLength > 255)
201
205
  this.fail("ENAMETOOLONG", syscall, path);
206
+ if (options.resizeCreate === true && pending.length === 1 && pending[0] === "")
207
+ this.fail("EISDIR", syscall, path);
202
208
  let node = current.entries.get(component);
203
209
  if (!node && options.createDirectories !== undefined) {
204
210
  this.permission(current, 3, syscall, path);
@@ -216,7 +222,9 @@ export class MemoryFileSystem {
216
222
  this.fail("ELOOP", syscall, path);
217
223
  const target = node.target.split("/").filter(Boolean);
218
224
  if (node.target.endsWith("/"))
219
- target.push(".");
225
+ target.push("");
226
+ if (options.resizeCreate !== undefined && target.at(-1) === "" && pending[0] === "")
227
+ target.pop();
220
228
  pending.unshift(...target);
221
229
  if (node.target.startsWith("/"))
222
230
  stack.splice(1);
@@ -251,11 +259,12 @@ export class MemoryFileSystem {
251
259
  snapshot(node) {
252
260
  return {
253
261
  type: node.type,
262
+ preferredIoBlockSize: 4096,
254
263
  size: node.type === "file" ? node.data.byteLength
255
264
  : node.type === "symlink" ? new TextEncoder().encode(node.target).byteLength : 0,
256
265
  mode: node.mode, ...(ownedStores.get(this)?.intact() ? { identityScope: this.identityScope } : {}),
257
266
  ino: node.ino, dev: 0, uid: 0, gid: 0,
258
- nlink: node.type === "directory"
267
+ nlink: node.type === "directory" && node.nlink !== 0
259
268
  ? 2 + [...node.entries.values()].filter((entry) => entry.type === "directory").length : node.nlink,
260
269
  atimeMs: node.atimeMs, mtimeMs: node.mtimeMs, ctimeMs: node.ctimeMs, birthtimeMs: node.birthtimeMs,
261
270
  };
@@ -650,7 +659,10 @@ export class MemoryFileSystem {
650
659
  this.integer(length, "truncate", path);
651
660
  const node = this.file(path, "truncate");
652
661
  this.permission(node, 2, "truncate", path);
653
- const data = this.allocate(length, "truncate", path);
662
+ this.resizeNode(node, length, "truncate", path);
663
+ }
664
+ resizeNode(node, length, syscall, path) {
665
+ const data = this.allocate(length, syscall, path);
654
666
  try {
655
667
  data.data.set(node.data.subarray(0, length));
656
668
  }
@@ -661,12 +673,71 @@ export class MemoryFileSystem {
661
673
  this.replaceData(node, data);
662
674
  this.changed(node);
663
675
  }
664
- async openReadFile(path, options = {}) {
676
+ async openResizeFile(path, options = {}) {
665
677
  options.signal?.throwIfAborted();
678
+ if (!stockRetainedResize(this))
679
+ throw new FsError("ENOTSUP", { syscall: "openResizeFile", path });
680
+ const location = this.resolve(path, "openResizeFile", { allowMissing: options.create === true, resizeCreate: options.create ?? false });
681
+ let mode = 0o666;
682
+ if (location.node) {
683
+ if (location.node.type !== "file")
684
+ this.fail("EISDIR", "openResizeFile", path);
685
+ this.permission(location.node, 2, "openResizeFile", path);
686
+ }
687
+ else {
688
+ this.permission(location.parent, 3, "openResizeFile", path);
689
+ mode = this.mode(options.mode, mode, "openResizeFile", path);
690
+ }
691
+ this.ledger.reserve(path.length * 2, 1, "openResizeFile", path);
692
+ let node;
693
+ try {
694
+ node = this.openWrite(path, {}, "openResizeFile", { location, mode, append: false });
695
+ node.references++;
696
+ }
697
+ catch (error) {
698
+ this.ledger.release(path.length * 2, 1);
699
+ throw error;
700
+ }
701
+ const snapshot = this.snapshot.bind(this);
702
+ const integer = this.integer.bind(this);
703
+ const resizeNode = this.resizeNode.bind(this);
704
+ const releaseReference = this.releaseReference.bind(this);
705
+ let closing;
706
+ const current = (signal, syscall) => {
707
+ signal?.throwIfAborted();
708
+ if (!node)
709
+ throw new FsError("EBADF", { syscall, path });
710
+ return node;
711
+ };
712
+ return {
713
+ async stat(options = {}) { return snapshot(current(options.signal, "fstat")); },
714
+ async seekEnd(options = {}) { return BigInt(current(options.signal, "lseek").data.byteLength); },
715
+ async truncate(length, options = {}) {
716
+ const file = current(options.signal, "ftruncate");
717
+ integer(length, "ftruncate", path);
718
+ resizeNode(file, length, "ftruncate", path);
719
+ },
720
+ close() {
721
+ if (!closing) {
722
+ const retained = node;
723
+ node = undefined;
724
+ closing = Promise.resolve().then(() => { if (retained)
725
+ releaseReference(retained, path); });
726
+ }
727
+ return closing;
728
+ },
729
+ };
730
+ }
731
+ async openReadFile(path, options = {}) {
732
+ const signal = options.signal;
733
+ signal?.throwIfAborted();
734
+ const allowDirectory = options.allowDirectory === true;
735
+ signal?.throwIfAborted();
666
736
  const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openReadFile", path }); };
667
737
  const owner = ownedStores.get(this);
668
738
  if (!owner || Object.getPrototypeOf(this) !== MemoryFileSystem.prototype
669
- || Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root)
739
+ || Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root
740
+ || Object.getOwnPropertyDescriptor(this, "ledger")?.value !== owner.ledger)
670
741
  unsupported();
671
742
  for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
672
743
  "file", "resolve", "permission", "validatePath", "fail", "snapshot", "integer", "releaseReference", "releaseNode"]) {
@@ -675,10 +746,16 @@ export class MemoryFileSystem {
675
746
  if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
676
747
  unsupported();
677
748
  }
678
- if (Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead !== true)
749
+ const retainedRead = Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead;
750
+ signal?.throwIfAborted();
751
+ if (retainedRead !== true)
679
752
  unsupported();
680
- let node = this.file(path, "openReadFile");
753
+ const selected = allowDirectory ? this.resolve(path, "openReadFile").node : this.file(path, "openReadFile");
754
+ if (selected.type !== "file" && selected.type !== "directory")
755
+ this.fail("EISDIR", "openReadFile", path);
756
+ let node = selected;
681
757
  this.permission(node, 4, "openReadFile", path);
758
+ signal?.throwIfAborted();
682
759
  this.ledger.reserve(path.length * 2, 1, "openReadFile", path);
683
760
  node.references++;
684
761
  const snapshot = this.snapshot.bind(this);
@@ -695,6 +772,10 @@ export class MemoryFileSystem {
695
772
  async stat(options = {}) {
696
773
  return snapshot(current(options.signal, "fstat"));
697
774
  },
775
+ async seekEnd(options = {}) {
776
+ const file = current(options.signal, "lseek");
777
+ return file.type === "directory" ? ext4HtreeEof64 : BigInt(file.data.byteLength);
778
+ },
698
779
  async read(position, maxBytes, options = {}) {
699
780
  const file = current(options.signal, "read");
700
781
  integer(position, "read", path);
@@ -702,6 +783,8 @@ export class MemoryFileSystem {
702
783
  if (maxBytes === 0 || maxBytes > Number.MAX_SAFE_INTEGER - position) {
703
784
  throw new FsError("EINVAL", { syscall: "read", path });
704
785
  }
786
+ if (file.type === "directory")
787
+ throw new FsError("EISDIR", { syscall: "read", path });
705
788
  const bytes = file.data.slice(position, position + maxBytes);
706
789
  file.atimeMs = Date.now();
707
790
  return bytes;
@@ -800,6 +883,20 @@ function stockDescriptorWrite(filesystem) {
800
883
  }
801
884
  return true;
802
885
  }
886
+ function stockRetainedResize(filesystem) {
887
+ if (!stockDescriptorWrite(filesystem))
888
+ return false;
889
+ const owner = ownedStores.get(filesystem);
890
+ if (!owner || Object.getOwnPropertyDescriptor(filesystem, "ledger")?.value !== owner.ledger)
891
+ return false;
892
+ for (const name of ["openResizeFile", "truncate", "resizeNode", "snapshot"]) {
893
+ const descriptor = Object.getOwnPropertyDescriptor(filesystem, name)
894
+ ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
895
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
896
+ return false;
897
+ }
898
+ return true;
899
+ }
803
900
  export function createMemoryFileSystem(options = {}) {
804
901
  return new MemoryFileSystem(normalizeMemoryFileSystemLimits(options));
805
902
  }
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  export interface MountFileSystemOptions {
4
4
  readonly root: FileSystem;
@@ -9,9 +9,10 @@ export declare class MountFileSystem implements FileSystem {
9
9
  private readonly mounts;
10
10
  constructor(options: MountFileSystemOptions);
11
11
  private select;
12
- capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
12
+ capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
13
13
  private protected;
14
- openReadFile(path: string, options?: FsOptions): Promise<FileReadHandle>;
14
+ openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
15
+ openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
15
16
  private error;
16
17
  private operation;
17
18
  private components;
@@ -1,9 +1,10 @@
1
1
  import { FsError, isFsError, toFsError } from "../../contracts/errors.js";
2
2
  import { readBytes } from "../../contracts/io.js";
3
3
  import { finishCleanup } from "../../contracts/cleanup.js";
4
- import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities } from "../capabilities.js";
4
+ import { 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 { pathNamespace } from "../path-namespace.js";
7
8
  import { compareIdentity } from "./identity.js";
8
9
  import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
9
10
  const syntheticStat = Object.freeze({
@@ -20,10 +21,11 @@ function fail(code) {
20
21
  throw new FsError(code);
21
22
  }
22
23
  function snapshotStat(stat) {
23
- const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
24
+ const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
24
25
  return {
25
26
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
26
27
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
28
+ ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
27
29
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
28
30
  ...(identityScope === undefined ? {} : { identityScope }),
29
31
  ...(ino === undefined ? {} : { ino }),
@@ -67,6 +69,10 @@ export class MountFileSystem {
67
69
  add(path, backend);
68
70
  }
69
71
  this.mounts = mounts.sort((left, right) => right.path.length - left.path.length);
72
+ const selectNamespace = this.select.bind(this);
73
+ Object.defineProperty(this, pathNamespace, {
74
+ value: Object.freeze({ select: (path) => selectNamespace(path).path }),
75
+ });
70
76
  registerEntryView(this, (path, options) => this.operation("compareEntry", path, options, async () => {
71
77
  const location = await this.resolve(path, options);
72
78
  if (location.synthetic)
@@ -85,7 +91,7 @@ export class MountFileSystem {
85
91
  const common = (capability) => {
86
92
  const optional = {
87
93
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
88
- descriptorWriteStream: ["writeStream"],
94
+ descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"],
89
95
  };
90
96
  const values = mounts.map(({ backend }) => {
91
97
  if (backend.capabilities.readOnly === true
@@ -104,7 +110,7 @@ export class MountFileSystem {
104
110
  "read", "stat", "readdir", "realpath", "access",
105
111
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
106
112
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
107
- "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "symlinks", "hardlinks", "permissions", "timestamps",
113
+ "streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "symlinks", "hardlinks", "permissions", "timestamps",
108
114
  ].map(capability => [capability, common(capability)]).filter(([, value]) => value !== undefined));
109
115
  this.capabilities = Object.freeze({
110
116
  get snapshotRmdir() { return mounts.some(({ backend }) => backend.capabilities.snapshotRmdir === true); },
@@ -122,14 +128,17 @@ export class MountFileSystem {
122
128
  }
123
129
  async capabilitiesFor(path, options = {}) {
124
130
  return this.operation("capabilitiesFor", path, options, async () => {
125
- const location = await this.resolve(path, options, { allowMissing: true });
131
+ const location = await this.resolve(path, options, {
132
+ allowMissing: options.create ?? true,
133
+ ...(options.create === undefined ? {} : { resizeCreate: options.create }),
134
+ });
126
135
  const observed = await location.mount.backend.capabilitiesFor?.(location.local, options)
127
136
  ?? location.mount.backend.capabilities;
128
137
  const declared = observed.descriptorWriteStream === true
129
138
  && (typeof location.mount.backend.writeStream !== "function" || observed.readOnly === true || observed.streamingWrite === false)
130
139
  ? { ...observed, descriptorWriteStream: false } : observed;
131
140
  const capabilities = location.synthetic ? { ...declared, retainedRead: false }
132
- : retainedReadCapabilities(location.mount.backend, declared);
141
+ : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, declared));
133
142
  if (location.synthetic)
134
143
  return readOnlyCapabilities(capabilities);
135
144
  if (this.mounts.length === 1 || capabilities.readOnly === true)
@@ -158,6 +167,27 @@ export class MountFileSystem {
158
167
  throw error ? this.error(error, "openReadFile", path, options) : error;
159
168
  }
160
169
  }
170
+ async openResizeFile(path, options = {}) {
171
+ try {
172
+ options.signal?.throwIfAborted();
173
+ const resolution = this.resolve(path, options, { allowMissing: options.create === true, resizeCreate: options.create ?? false });
174
+ const signal = options.signal;
175
+ const location = await (signal ? new Promise((resolve, reject) => {
176
+ const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
177
+ signal.addEventListener("abort", abort, { once: true });
178
+ resolution.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
179
+ if (signal.aborted)
180
+ abort();
181
+ }) : resolution);
182
+ options.signal?.throwIfAborted();
183
+ if (location.synthetic)
184
+ fail("EROFS");
185
+ return await openRetainedResizeFile(location.mount.backend, location.local, options);
186
+ }
187
+ catch (error) {
188
+ throw error ? this.error(error, "openResizeFile", path, options) : error;
189
+ }
190
+ }
161
191
  error(error, syscall, path, options, dest) {
162
192
  if (options.signal?.aborted && error === options.signal.reason)
163
193
  return error;
@@ -210,24 +240,28 @@ export class MountFileSystem {
210
240
  const stack = [await this.lookup("/", options)];
211
241
  let boundary;
212
242
  let verification;
243
+ let creationVerification;
213
244
  const verified = async (location) => {
214
- if (verification) {
215
- const canonical = await verification.mount.backend.realpath(verification.path, options);
245
+ const inspection = !location.stat && settings.resizeCreate === true ? creationVerification : verification;
246
+ if (inspection) {
247
+ const canonical = await inspection.mount.backend.realpath(inspection.path, options);
216
248
  validatePath(canonical);
217
249
  if (!canonical.startsWith("/") || normalizePath(canonical) !== canonical)
218
250
  fail("EIO");
219
- if (verification.finalName !== undefined) {
251
+ if (inspection.finalName !== undefined) {
220
252
  try {
221
- await verification.mount.backend.lstat(`${verification.path}/${verification.finalName}`, options);
253
+ await inspection.mount.backend.lstat(`${inspection.path}/${inspection.finalName}`, options);
254
+ if (!location.stat && settings.resizeCreate === true)
255
+ fail("ENOTSUP");
222
256
  }
223
257
  catch (error) {
224
258
  if (location.stat || toFsError(error).code !== "ENOENT")
225
259
  throw error;
226
260
  }
227
261
  }
228
- const expected = verification.finalName === undefined ? canonical
229
- : `${canonical === "/" ? "" : canonical}/${verification.finalName}`;
230
- if (location.mount !== verification.mount || location.local !== expected)
262
+ const expected = inspection.finalName === undefined ? canonical
263
+ : `${canonical === "/" ? "" : canonical}/${inspection.finalName}`;
264
+ if (location.mount !== inspection.mount || location.local !== expected)
231
265
  fail("ENOTSUP");
232
266
  }
233
267
  return location;
@@ -239,8 +273,10 @@ export class MountFileSystem {
239
273
  const current = stack[stack.length - 1];
240
274
  if (current.stat?.type !== "directory")
241
275
  fail("ENOTDIR");
276
+ if (component.trailing && pending.length === 0 && settings.resizeCreate !== undefined)
277
+ fail("EISDIR");
242
278
  if (!current.synthetic && !settings.createDirectories?.has(current.path)) {
243
- const mode = current.mount.backend.capabilities.permissions === false ? 0 : 1;
279
+ const mode = settings.resizeCreate === undefined && current.mount.backend.capabilities.permissions === false ? 0 : 1;
244
280
  await current.mount.backend.access(current.local, mode, options);
245
281
  }
246
282
  if (component.name === ".")
@@ -257,6 +293,8 @@ export class MountFileSystem {
257
293
  const nextPath = `${current.path === "/" ? "" : current.path}/${component.name}`;
258
294
  if (boundary && this.select(nextPath) !== boundary)
259
295
  fail("EACCES");
296
+ if (settings.resizeCreate === true && pending.length === 1 && pending[0].trailing)
297
+ fail("EISDIR");
260
298
  let next = settings.createDirectories?.get(nextPath)
261
299
  ?? await this.lookup(nextPath, options, current, settings.createDirectories?.has(current.path));
262
300
  if (!next.stat) {
@@ -299,6 +337,20 @@ export class MountFileSystem {
299
337
  const readlink = this.optional(next, "readlink", "symlinks");
300
338
  const target = await readlink.call(next.mount.backend, next.local, options);
301
339
  const targetParts = this.components(target);
340
+ if (settings.resizeCreate !== undefined && targetParts.at(-1)?.trailing && pending[0]?.trailing)
341
+ targetParts.pop();
342
+ if (settings.resizeCreate === true) {
343
+ const remainder = [...targetParts, ...pending];
344
+ const finalName = remainder.at(-1)?.name;
345
+ if (finalName !== undefined && finalName !== "." && finalName !== "..") {
346
+ const parent = target.startsWith("/") ? "" : next.local.slice(0, next.local.lastIndexOf("/"));
347
+ creationVerification = {
348
+ mount: next.mount,
349
+ path: `${parent}/${remainder.slice(0, -1).map(part => part.name).join("/")}`,
350
+ finalName,
351
+ };
352
+ }
353
+ }
302
354
  if (target.startsWith("/")) {
303
355
  const rootIndex = stack.findIndex((entry) => entry.path === next.mount.path);
304
356
  if (rootIndex < 0)
@@ -1,5 +1,5 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
3
  export interface OverlayFileSystemOptions {
4
4
  readonly upper: FileSystem;
5
5
  readonly lower: FileSystem;
@@ -18,8 +18,8 @@ export declare class OverlayFileSystem implements FileSystem {
18
18
  private queue;
19
19
  constructor(options: OverlayFileSystemOptions);
20
20
  private run;
21
- capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
22
- openReadFile(path: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
21
+ capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
22
+ openReadFile(path: string, options?: OpenReadFileOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
23
23
  private writable;
24
24
  private permission;
25
25
  private maybeStat;
@@ -8,10 +8,11 @@ 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, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
11
+ const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
12
12
  return {
13
13
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
14
14
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
15
+ ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
15
16
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
16
17
  ...(identityScope === undefined ? {} : { identityScope }),
17
18
  ...(ino === undefined ? {} : { ino }),
@@ -119,6 +120,7 @@ export class OverlayFileSystem {
119
120
  ...(effectiveAppend === undefined ? {} : { append: effectiveAppend }),
120
121
  atomicRename: false, atomicRenameNoReplace: false,
121
122
  descriptorWriteStream: false,
123
+ retainedResize: false,
122
124
  hardlinks: false,
123
125
  symlinks: writable && this.#upper.capabilities.symlinks === true
124
126
  && typeof this.#upper.symlink === "function" && typeof this.#upper.readlink === "function"
@@ -167,8 +169,12 @@ export class OverlayFileSystem {
167
169
  openReadFile(path, options = {}) {
168
170
  return this.run(options, async () => {
169
171
  const entry = await this.required(path, options);
170
- if (entry.stat.type !== "file")
171
- fail("EISDIR", path);
172
+ if (entry.stat.type !== "file") {
173
+ const allowDirectory = options.allowDirectory === true;
174
+ options.signal?.throwIfAborted();
175
+ if (entry.stat.type !== "directory" || !allowDirectory)
176
+ fail("EISDIR", path);
177
+ }
172
178
  this.permission(entry, 4);
173
179
  return openRetainedReadFile(entry.backend, entry.path, options);
174
180
  }, false);
@@ -0,0 +1,4 @@
1
+ import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
2
+ export declare const pathNamespace: unique symbol;
3
+ export declare function capturePathNamespace(filesystem: FileSystem, options: FsOptions): ((path: string) => string) | undefined;
4
+ export declare function readOnlyPathNamespace(projection: unknown): object | undefined;
@@ -0,0 +1,63 @@
1
+ import { FsError } from "../contracts/errors.js";
2
+ import { normalizePath, validatePath } from "../contracts/virtual-path.js";
3
+ export const pathNamespace = Symbol.for("@poe-code/safe-fs/path-namespace/v1");
4
+ const captureNamespace = Symbol.for("@poe-code/safe-fs/capture-path-namespace/v1");
5
+ export function capturePathNamespace(filesystem, options) {
6
+ options.signal?.throwIfAborted();
7
+ const projection = Reflect.get(filesystem, pathNamespace);
8
+ options.signal?.throwIfAborted();
9
+ if (projection === undefined)
10
+ return undefined;
11
+ return captureProjection(projection, options);
12
+ }
13
+ export function readOnlyPathNamespace(projection) {
14
+ if (projection === undefined)
15
+ return undefined;
16
+ const select = (path) => captureProjection(projection, {})(path);
17
+ Object.defineProperty(select, captureNamespace, {
18
+ value: Object.freeze((options) => captureProjection(projection, options)),
19
+ });
20
+ return Object.freeze({ select: Object.freeze(select) });
21
+ }
22
+ function captureProjection(projection, options) {
23
+ options.signal?.throwIfAborted();
24
+ if (projection === null || typeof projection !== "object")
25
+ throw new FsError("EIO");
26
+ const frozen = Object.isFrozen(projection);
27
+ options.signal?.throwIfAborted();
28
+ if (!frozen)
29
+ throw new FsError("EIO");
30
+ let select = Reflect.get(projection, "select");
31
+ options.signal?.throwIfAborted();
32
+ if (typeof select !== "function")
33
+ throw new FsError("EIO");
34
+ const capture = Reflect.get(select, captureNamespace);
35
+ options.signal?.throwIfAborted();
36
+ let receiver = projection;
37
+ if (capture !== undefined) {
38
+ if (typeof capture !== "function")
39
+ throw new FsError("EIO");
40
+ select = Reflect.apply(capture, select, [options]);
41
+ options.signal?.throwIfAborted();
42
+ if (typeof select !== "function")
43
+ throw new FsError("EIO");
44
+ receiver = undefined;
45
+ }
46
+ const selected = select;
47
+ return path => {
48
+ options.signal?.throwIfAborted();
49
+ const root = Reflect.apply(selected, receiver, [path]);
50
+ options.signal?.throwIfAborted();
51
+ if (typeof root !== "string" || !root.startsWith("/"))
52
+ throw new FsError("EIO", { path });
53
+ try {
54
+ validatePath(root);
55
+ }
56
+ catch (cause) {
57
+ throw new FsError("EIO", { path, cause });
58
+ }
59
+ if (normalizePath(root) !== root || root !== "/" && path !== root && !path.startsWith(`${root}/`))
60
+ throw new FsError("EIO", { path });
61
+ return root;
62
+ };
63
+ }