@poe-platform/safe-fs 0.1.799 → 0.1.801

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 (34) hide show
  1. package/README.md +16 -7
  2. package/dist/safe-fs/contracts/filesystem.d.ts +2 -0
  3. package/dist/safe-fs/contracts/io.js +26 -36
  4. package/dist/safe-fs/contracts/path.d.ts +2 -7
  5. package/dist/safe-fs/contracts/path.js +1 -7
  6. package/dist/safe-fs/contracts/virtual-path.d.ts +8 -3
  7. package/dist/safe-fs/contracts/virtual-path.js +22 -5
  8. package/dist/safe-fs/core.d.ts +1 -0
  9. package/dist/safe-fs/core.js +1 -0
  10. package/dist/safe-fs/fs/devices/path.js +18 -17
  11. package/dist/safe-fs/fs/memory/index.js +57 -43
  12. package/dist/safe-fs/fs/memory/limits.d.ts +5 -1
  13. package/dist/safe-fs/fs/memory/limits.js +7 -11
  14. package/dist/safe-fs/fs/memory/missing-target.d.ts +2 -1
  15. package/dist/safe-fs/fs/memory/missing-target.js +8 -1
  16. package/dist/safe-fs/fs/overlay/index.d.ts +4 -1
  17. package/dist/safe-fs/fs/overlay/index.js +57 -11
  18. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +11 -4
  19. package/dist/safe-fs/fs/overlay/memory-publication.js +240 -14
  20. package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +4 -0
  21. package/dist/safe-fs/fs/s3/http/request-fetch.js +74 -0
  22. package/dist/safe-fs/fs/s3/http/request-node.d.ts +4 -0
  23. package/dist/safe-fs/fs/s3/http/request-node.js +111 -0
  24. package/dist/safe-fs/fs/s3/http/request.d.ts +5 -4
  25. package/dist/safe-fs/fs/s3/http/request.js +1 -109
  26. package/dist/safe-fs/fs/s3/http/signature.js +7 -5
  27. package/dist/safe-fs/fs/s3/http/transport.js +6 -3
  28. package/dist/safe-fs/fs/s3/namespace.js +1 -1
  29. package/dist/safe-fs/fs/scoped.js +2 -2
  30. package/dist/safe-fs/fs/webdav/webdav.d.ts +3 -0
  31. package/dist/safe-fs/fs/webdav/webdav.js +5 -4
  32. package/dist/xml-ast/index.d.ts +2 -0
  33. package/dist/xml-ast/index.js +68 -31
  34. package/package.json +9 -2
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # safe-fs
2
2
 
3
- An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges.
3
+ An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges. Portable filesystem types and POSIX helpers compile without Node types.
4
+
5
+ Under `workerd` and `browser` conditions, the root export includes portable filesystems, `S3FileSystem`, and `MockS3Client`. Supply an S3 transport to use object storage without Node globals.
4
6
 
5
7
  ## Quickstart
6
8
 
@@ -44,7 +46,7 @@ To create new outputs with Safe Bash's `dos2unix`, `unix2dos`, or compression co
44
46
  | `@poe-platform/safe-fs` | Full Node API; selects the portable core under the `browser` and `workerd` conditions. |
45
47
  | `@poe-platform/safe-fs/core` | Memory, mounts, overlays, read-only, WebDAV, portable bridges, types, and errors. Bundle with the `browser` condition for browsers and Workers. |
46
48
  | `@poe-platform/safe-fs/node` | Host filesystem, Node bridge, S3, and configuration helpers; unavailable in browsers. |
47
- | `@poe-platform/safe-fs/fs/*` | Individual adapters: `memory`, `readonly`, `mount`, `overlay`, `webdav`, `real`, `s3`, and `s3/http`. S3 and its mock accept an injected transport in browsers and Workers; `s3/http` is Node-only. |
49
+ | `@poe-platform/safe-fs/fs/*` | Individual adapters: `memory`, `readonly`, `mount`, `overlay`, `webdav`, `real`, `s3`, and `s3/http`. S3, its mock, and the signed `s3/http` transport work in browsers and Workers. |
48
50
 
49
51
  ```ts
50
52
  import { createMemoryFileSystem, FsError, type FileSystem } from "@poe-platform/safe-fs/core";
@@ -111,12 +113,17 @@ advertising `retainedStagingCleanup` accept `createStagedFile(..., { parent,
111
113
  retainCleanup: true })`. Use the returned `staging.cleanup.remove()` and always
112
114
  call `staging.cleanup.close()` in `finally`. Memory and its supported wrappers
113
115
  retain only the owned staging entries; replacement entries remain protected.
116
+ Overlays backed by stock Memory filesystems support conditional file mutations,
117
+ directory preparation, and confined staging. Publication may omit `ancestors`
118
+ when the destination shares the staging parent; the overlay checks the ancestry
119
+ retained before staging began.
114
120
  See the [staging contract](src/contracts/filesystem.md#atomic-owned-staging).
115
121
 
116
122
  Capture `retainFileSystemCleanup(scoped, callback, { maxOperations })` before
117
123
  cancellation to clean up through a bounded view after ordinary scoped operations
118
124
  close. Cleanup preserves its owning scope and limits across separately bundled
119
125
  command runtimes, including restrictions added by filesystem wrappers.
126
+ Omit `maxOperations` or pass `Infinity` for unlimited cleanup operations.
120
127
 
121
128
  | Backend or wrapper | Use it for |
122
129
  | --- | --- |
@@ -246,7 +253,7 @@ There are no package environment variables, implicit credentials, or automatic `
246
253
 
247
254
  | API | Options and defaults |
248
255
  | --- | --- |
249
- | `createFileSystem(config, { registry })` | Required `config.type`; `config.options` defaults to an empty record. `registry` is required. Built-in `memory` accepts optional file, retained-byte, metadata and total-byte quotas; built-in `real` requires `root`. |
256
+ | `createFileSystem(config, { registry })` | Required `config.type`; `config.options` defaults to an empty record. `registry` is required. Built-in `memory` accepts optional file, retained-byte, metadata, total-byte, `maxPathBytes` and `maxPathComponents` quotas (omitted, `undefined`, or `Infinity` disables each quota); built-in `real` requires `root`. |
250
257
  | `createNodeFileSystemAdapterRegistry(extensions?)` | Optional map of additional adapter descriptors; defaults to only `memory` and `real`. |
251
258
  | Memory / read-only | Memory accepts independent optional `maxFileBytes`, `maxRetainedBytes`, `maxMetadataUnits` and `maxBytes` quotas, all unlimited by default. Read-only takes the backing filesystem, without an options object. |
252
259
  | Real | Required `root`: existing absolute host directory; the constructor/factory also accepts the root string directly. |
@@ -271,9 +278,11 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
271
278
  | `copyFile` | `exclusive` (default false) |
272
279
  | `readStream` | `start` (default 0), `endExclusive` (default end of file), `chunkSize` (built-in default 64 KiB) |
273
280
 
281
+ `validatePath(path, { maxPathBytes, maxPathComponents })` accepts optional UTF-8 byte and component quotas; both default to `Infinity`. Memory accepts the same constructor options and counts cumulative symlink expansion. Device operations accept these quotas under `pathLimits`. WebDAV accepts `maxTimestampPropertyBytes` for timestamp metadata, also unlimited by default.
282
+
274
283
  `access` takes a separate mode bitmask from `ACCESS_MODES`. `chmod` takes a mode, `utimes` takes millisecond timestamps, and `truncate` takes a byte length (default 0). For conditional chmod, check `capabilitiesFor(path, { conditionalChmod: true })` (or `capabilities`) and require `conditionalChmod: true`; supply `parent`, `expected`, and complete root-to-parent `ancestors` together. An optional mutation-free `commitGuard` must return literal `true` synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the [bridge signatures](src/bridge/filesystem.ts).
275
284
 
276
- `collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxBytes` and `maxMemoryBytes` accept explicit `Infinity`. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Collection limits are disabled by default on Node, browser, and Worker targets. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.
285
+ `collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxBytes` and `maxMemoryBytes` accept explicit `Infinity`. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Collection limits are disabled by default on Node, browser, and Worker targets. Each call owns its budget; concurrent collections do not consume one another’s allowance. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.
277
286
 
278
287
  <details>
279
288
  <summary>S3 filesystem and HTTP transport options</summary>
@@ -336,7 +345,7 @@ Each batch lists from the beginning because previous keys have been deleted; it
336
345
  | `enableCopy` | `true`; disabling uses a buffered GET/PUT fallback |
337
346
  | `verifiedConditionalOperations` | Optional `put`, `copy`, `delete` booleans, each defaulting to false; enable only after verifying the server's semantics |
338
347
  | `clock` | Current date/time function, used for signing |
339
- | `request` | Optional Node HTTP request factory; defaults to the built-in HTTP(S) client |
348
+ | `request` | Optional Node HTTP request factory; Node defaults to HTTP(S), browsers and Workers use global fetch |
340
349
 
341
350
  </details>
342
351
 
@@ -369,10 +378,10 @@ See the [binding types](src/fs/webdav/webdav.ts) before implementing atomic dire
369
378
  ## Safety boundary and limitations
370
379
 
371
380
  - **Not an OS sandbox.** Rooted storage and bridge confinement check paths and symlinks, but checks and host operations can race concurrent changes. Use a separate OS isolation boundary for hostile workloads. A custom adapter or transport is trusted code with its own host authority.
372
- - **A read-only view is not an immutable store.** Mount views serialize guest symlink, rename, and removal operations against active path operations. Finish or close read streams before awaiting these namespace changes. Other references can still change the backing filesystem; this coordination applies only to operations through the same mount view. Overlay reads require retained handles and stable object and ancestor identities; unsupported backends or changed read admission fail with `ENOTSUP`. Overlays with two stock MemoryFileSystem layers support race-safe patch publication and confined directory creation/removal. Publication checks both layers, retains logical directory identity across copy-up, and leaves lower bytes intact; wrapped or customized layers remain unsupported. Overlays are not transactions; cancellation and cleanup do not guarantee rollback. Cross-mount rename can fail with `EXDEV`.
381
+ - **A read-only view is not an immutable store.** Mount views serialize guest symlink, rename, and removal operations against active path operations. Finish or close read streams before awaiting these namespace changes. Other references can still change the backing filesystem; this coordination applies only to operations through the same mount view. Overlay reads require retained handles and stable object and ancestor identities; unsupported backends or changed read admission fail with `ENOTSUP`. Overlays with two stock MemoryFileSystem layers support race-safe patch publication, conditional file writes/removal, no-replace rename, and confined directory creation/removal and file writes. Compression file output, `mv -n`, `sed -i`, and ssconvert work with these layers. Upper-file writes retain file identity; lower-file writes copy up without changing lower bytes. Publication checks both layers, retains logical directory identity across copy-up, and leaves lower bytes intact; wrapped or customized layers remain unsupported. Overlays are not transactions; cancellation and cleanup do not guarantee rollback. Cross-mount rename can fail with `EXDEV`.
373
382
  - **Remote storage is not a POSIX disk.** S3 and WebDAV do not provide hardlinks or symlinks. S3 rename is non-atomic and may leave partial copies/deletions; `S3RenameError` reports the phase and affected keys. Strong empty-only removal and conditional writes depend on backend support, not a prior listing.
374
383
  - **The bridges are partial Node compatibility layers.** No synchronous/callback API, file handles, watchers, bigint stats, flush/retry support, or `cp` dereference/timestamp-preservation options. Missing optional operations fail rather than being approximated with destructive alternatives.
375
- - **Browser support is filesystem-only.** The `browser` and `workerd` export conditions select the portable surface: memory, mounts, overlays, read-only, WebDAV, and the codec-based bridge. S3 is available through `fs/s3` with an explicit transport, including its portable mock. Real storage, the Node bridge, and the configuration registry are not browser exports. WebDAV still needs server CORS support; no OPFS or directory-handle adapter is included. This does not make the SafeJS runtime browser-compatible.
384
+ - **Browser support is filesystem-only.** The `browser` and `workerd` export conditions select the portable surface: memory, mounts, overlays, read-only, WebDAV, and the codec-based bridge. S3 is available through `fs/s3` with an explicit transport, including its portable mock and `fs/s3/http`. The HTTP transport uses fetch in browsers and Workers and requires server CORS support in browsers. Fetch rejects keys containing dot segments that the URL parser would normalize, and never follows redirects. Node request factories are only supported in Node. Real storage, the Node bridge, and the configuration registry are not browser exports. WebDAV still needs server CORS support; no OPFS or directory-handle adapter is included. This does not make the SafeJS runtime browser-compatible.
376
385
  - **Portable temporary names require secure randomness.** Browser overlay staging and portable bridge `mkdtemp` require `crypto.randomUUID` or `crypto.getRandomValues`; without either, they fail with `ENOTSUP` rather than use `Math.random`.
377
386
  - **Allocation and identity may be unknown.** Optional `FileStat.allocatedBytes` is provider-reported allocation, not logical length or reclaimable space. Do not infer identity from size, timestamps, or inode numbers across unrelated backends.
378
387
 
@@ -105,6 +105,8 @@ export interface FileSystemCapabilities {
105
105
  readonly [capability: string]: boolean | undefined;
106
106
  }
107
107
  export interface FsOptions {
108
+ /** Optional virtual device path traversal quotas; disabled by default. */
109
+ readonly pathLimits?: import("./virtual-path.js").PathLimits;
108
110
  readonly signal?: AbortSignal;
109
111
  }
110
112
  export interface ChmodOptions extends FsOptions {
@@ -1,6 +1,5 @@
1
1
  import { FsError } from "./errors.js";
2
2
  import { finishCleanup } from "./cleanup.js";
3
- import { platform } from "#safe-fs-platform";
4
3
  let sharedTextEncoder;
5
4
  const EMPTY_BYTES = new Uint8Array(0);
6
5
  const DONE_RESULT = Object.freeze({ done: true, value: undefined });
@@ -63,7 +62,6 @@ export function toByteSource(input) {
63
62
  const bytes = typeof input === "string" ? (sharedTextEncoder ??= new TextEncoder()).encode(input) : new Uint8Array(input);
64
63
  return new SingleChunkByteSource(bytes);
65
64
  }
66
- let activeCollectionBytes = 0;
67
65
  export async function collectBytes(source, options) {
68
66
  if (options.maxBytes !== undefined && options.maxBytes !== Infinity && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)) {
69
67
  throw new RangeError("maxBytes must be a nonnegative safe integer");
@@ -77,46 +75,38 @@ export async function collectBytes(source, options) {
77
75
  let buffer = new Uint8Array(0);
78
76
  let size = 0;
79
77
  function reserve(bytes) {
80
- if (bytes > memoryLimit - reserved || bytes > platform.maxCollectionBytes - activeCollectionBytes) {
78
+ if (bytes > memoryLimit - reserved) {
81
79
  throw new FsError("EFBIG", { syscall: "collectBytes", message: "collection exceeds memory budget" });
82
80
  }
83
81
  reserved += bytes;
84
- activeCollectionBytes += bytes;
85
82
  }
86
- try {
87
- options.signal?.throwIfAborted();
88
- for await (const chunk of readBytes(source, options.signal)) {
89
- activeCollectionBytes -= inputBytes;
90
- reserved -= inputBytes;
91
- inputBytes = 0;
92
- if (chunk.byteLength > (options.maxBytes ?? Infinity) - size) {
93
- throw new FsError("EFBIG", { syscall: "collectBytes", message: "output exceeds maxBytes" });
94
- }
95
- // A small view can retain a large backing buffer. Keep this reservation
96
- // while awaiting the next chunk, including while its source is suspended.
97
- reserve(chunk.buffer.byteLength);
98
- inputBytes = chunk.buffer.byteLength;
99
- const nextSize = size + chunk.byteLength;
100
- if (nextSize > buffer.byteLength) {
101
- const capacity = Math.min(options.maxBytes ?? Infinity, Math.max(nextSize, buffer.byteLength * 2));
102
- // Admit both generations before allocation. Do not fall back to repeated
103
- // exact-size growth when geometric growth exceeds the budget.
104
- reserve(capacity);
105
- const replacement = new Uint8Array(capacity);
106
- replacement.set(buffer.subarray(0, size));
107
- activeCollectionBytes -= buffer.byteLength;
108
- reserved -= buffer.byteLength;
109
- buffer = replacement;
110
- }
111
- buffer.set(chunk, size);
112
- size = nextSize;
83
+ options.signal?.throwIfAborted();
84
+ for await (const chunk of readBytes(source, options.signal)) {
85
+ reserved -= inputBytes;
86
+ inputBytes = 0;
87
+ if (chunk.byteLength > (options.maxBytes ?? Infinity) - size) {
88
+ throw new FsError("EFBIG", { syscall: "collectBytes", message: "output exceeds maxBytes" });
113
89
  }
114
- options.signal?.throwIfAborted();
115
- return buffer.subarray(0, size);
116
- }
117
- finally {
118
- activeCollectionBytes -= reserved;
90
+ // A small view can retain a large backing buffer. Keep this reservation
91
+ // while awaiting the next chunk, including while its source is suspended.
92
+ reserve(chunk.buffer.byteLength);
93
+ inputBytes = chunk.buffer.byteLength;
94
+ const nextSize = size + chunk.byteLength;
95
+ if (nextSize > buffer.byteLength) {
96
+ const capacity = Math.min(options.maxBytes ?? Infinity, Math.max(nextSize, buffer.byteLength * 2));
97
+ // Admit both generations before allocation. Do not fall back to repeated
98
+ // exact-size growth when geometric growth exceeds the budget.
99
+ reserve(capacity);
100
+ const replacement = new Uint8Array(capacity);
101
+ replacement.set(buffer.subarray(0, size));
102
+ reserved -= buffer.byteLength;
103
+ buffer = replacement;
104
+ }
105
+ buffer.set(chunk, size);
106
+ size = nextSize;
119
107
  }
108
+ options.signal?.throwIfAborted();
109
+ return buffer.subarray(0, size);
120
110
  }
121
111
  async function abortable(operation, signal) {
122
112
  signal?.throwIfAborted();
@@ -1,8 +1,3 @@
1
- import { posix } from "#safe-fs-platform-path";
2
1
  export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./virtual-path.js";
3
- export declare const posixPath: typeof posix;
4
- export declare const basename: typeof posix.basename;
5
- export declare const dirname: typeof posix.dirname;
6
- export declare const extname: typeof posix.extname;
7
- export declare const joinPath: typeof posix.join;
8
- export declare const isAbsolutePath: typeof posix.isAbsolute;
2
+ export { posixPath, basename, dirname, extname, joinPath, isAbsolutePath } from "./portable-path.js";
3
+ export type { PathLimits } from "./virtual-path.js";
@@ -1,8 +1,2 @@
1
- import { posix } from "#safe-fs-platform-path";
2
1
  export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./virtual-path.js";
3
- export const posixPath = posix;
4
- export const basename = posix.basename;
5
- export const dirname = posix.dirname;
6
- export const extname = posix.extname;
7
- export const joinPath = posix.join;
8
- export const isAbsolutePath = posix.isAbsolute;
2
+ export { posixPath, basename, dirname, extname, joinPath, isAbsolutePath } from "./portable-path.js";
@@ -1,6 +1,11 @@
1
- export declare const MAX_PATH_BYTES = 65536;
2
- export declare const MAX_PATH_COMPONENTS = 2048;
3
- export declare function validatePath(path: string, maxComponents?: number): void;
1
+ export declare const MAX_PATH_BYTES: number;
2
+ export declare const MAX_PATH_COMPONENTS: number;
3
+ export interface PathLimits {
4
+ readonly maxPathBytes?: number | undefined;
5
+ readonly maxPathComponents?: number | undefined;
6
+ }
7
+ export declare function pathByteLength(path: string): number;
8
+ export declare function validatePath(path: string, limits?: PathLimits | number): void;
4
9
  export declare function resolvePath2(cwd: string, path0: string): string;
5
10
  export declare function resolvePath(cwd: string, ...paths: string[]): string;
6
11
  export declare function normalizePath(path: string, cwd?: string): string;
@@ -1,14 +1,31 @@
1
1
  import { FsError } from "./errors.js";
2
- export const MAX_PATH_BYTES = 65_536;
3
- export const MAX_PATH_COMPONENTS = 2048;
4
- export function validatePath(path, maxComponents = MAX_PATH_COMPONENTS) {
2
+ export const MAX_PATH_BYTES = Infinity;
3
+ export const MAX_PATH_COMPONENTS = Infinity;
4
+ export function pathByteLength(path) {
5
+ let bytes = 0;
6
+ for (const point of path) {
7
+ const code = point.codePointAt(0);
8
+ bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
9
+ }
10
+ return bytes;
11
+ }
12
+ export function validatePath(path, limits = {}) {
13
+ const maxBytes = typeof limits === "number" ? Infinity : limits.maxPathBytes ?? Infinity;
14
+ const maxComponents = typeof limits === "number" ? limits : limits.maxPathComponents ?? Infinity;
15
+ for (const value of [maxBytes, maxComponents]) {
16
+ if (value !== Infinity && (!Number.isSafeInteger(value) || value < 0)) {
17
+ throw new RangeError("Path limits must be nonnegative safe integers or Infinity");
18
+ }
19
+ }
5
20
  if (typeof path !== "string" || path.includes("\0")) {
6
21
  throw new FsError("EINVAL", { syscall: "resolve", message: "paths must be strings without NUL bytes" });
7
22
  }
8
- if (path.length > MAX_PATH_BYTES) {
23
+ if (maxBytes !== Infinity && pathByteLength(path) > maxBytes) {
9
24
  throw new FsError("ENAMETOOLONG", { syscall: "resolve", path });
10
25
  }
11
- const limit = Math.min(maxComponents, MAX_PATH_COMPONENTS);
26
+ if (maxComponents === Infinity)
27
+ return;
28
+ const limit = maxComponents;
12
29
  let components = 0;
13
30
  let inComponent = false;
14
31
  for (let i = 0; i < path.length; i++) {
@@ -24,3 +24,4 @@ export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from ".
24
24
  export * from "./contracts/object.js";
25
25
  export { ObjectAuthority } from "./fs/object-authority.js";
26
26
  export { bindConditionalMutation, type ConditionalMutationBinding } from "./fs/memory/index.js";
27
+ export * from "./fs/s3/index.js";
@@ -20,3 +20,4 @@ export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
20
20
  export * from "./contracts/object.js";
21
21
  export { ObjectAuthority } from "./fs/object-authority.js";
22
22
  export { bindConditionalMutation } from "./fs/memory/index.js";
23
+ export * from "./fs/s3/index.js";
@@ -1,5 +1,5 @@
1
1
  import { FsError, isFsError } from "../../contracts/errors.js";
2
- import { MAX_PATH_COMPONENTS, validatePath } from "../../contracts/virtual-path.js";
2
+ import { pathByteLength, validatePath } from "../../contracts/virtual-path.js";
3
3
  import { capturePathNamespace, pathNamespace } from "../path-namespace.js";
4
4
  import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../memory/index.js";
5
5
  export const nullPath = "/dev/null";
@@ -23,20 +23,20 @@ export function lexicalDevicePath(path) {
23
23
  return `/${parts.join("/")}`;
24
24
  }
25
25
  async function resolveResizeDevicePath(filesystem, path, options, create, namespace, traversal) {
26
- validatePath(path);
26
+ validatePath(path, options.pathLimits);
27
27
  const components = (value) => {
28
28
  const result = value.split("/").filter(Boolean);
29
29
  if (value.endsWith("/"))
30
30
  result.push("");
31
31
  return result;
32
32
  };
33
- const pending = components(path);
34
- let remainingComponents = MAX_PATH_COMPONENTS - pending.length;
33
+ let pending = components(path);
34
+ let remainingComponents = (options.pathLimits?.maxPathComponents ?? Infinity) - pending.length;
35
35
  if (remainingComponents < 0)
36
36
  throw new FsError("ENAMETOOLONG", { path });
37
37
  const parts = [];
38
38
  let links = 0;
39
- let expanded = path.length;
39
+ let expanded = pathByteLength(path);
40
40
  let boundary;
41
41
  while (pending.length) {
42
42
  options.signal?.throwIfAborted();
@@ -99,9 +99,9 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
99
99
  throw new FsError("ELOOP", { path });
100
100
  const target = await Reflect.apply(readlink, filesystem, [candidate, options]);
101
101
  options.signal?.throwIfAborted();
102
- validatePath(target);
103
- expanded += target.length;
104
- if (expanded > 65536)
102
+ validatePath(target, options.pathLimits);
103
+ expanded += pathByteLength(target);
104
+ if (expanded > (options.pathLimits?.maxPathBytes ?? Infinity))
105
105
  throw new FsError("ENAMETOOLONG", { path });
106
106
  if (target.startsWith("/"))
107
107
  parts.splice(0, parts.length, ...(boundary ?? "/").split("/").filter(Boolean));
@@ -111,7 +111,7 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
111
111
  if (targetParts.length > remainingComponents)
112
112
  throw new FsError("ENAMETOOLONG", { path });
113
113
  remainingComponents -= targetParts.length;
114
- pending.unshift(...targetParts);
114
+ pending = targetParts.concat(pending);
115
115
  continue;
116
116
  }
117
117
  }
@@ -141,7 +141,8 @@ function exceedsComponentByteLimit(value) {
141
141
  }
142
142
  export async function resolveDevicePath(filesystem, path, options, followFinal = true, resizeCreate, traversal) {
143
143
  options.signal?.throwIfAborted();
144
- if (Reflect.get(filesystem, pathNamespace) === undefined) {
144
+ validatePath(path, options.pathLimits);
145
+ if (options.pathLimits === undefined && Reflect.get(filesystem, pathNamespace) === undefined) {
145
146
  const fast = tryResolveMemoryDevicePath(filesystem, path, resizeCreate);
146
147
  if (fast !== undefined)
147
148
  return fast;
@@ -155,13 +156,13 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
155
156
  const aliases = typeof lstat === "function";
156
157
  if (!aliases && lexical !== nullPath && lexical !== deviceDirectory && lexical !== "/")
157
158
  return lexical;
158
- const pending = path.split("/");
159
- let remainingComponents = MAX_PATH_COMPONENTS - pending.filter(Boolean).length;
159
+ let pending = path.split("/");
160
+ let remainingComponents = (options.pathLimits?.maxPathComponents ?? Infinity) - pending.filter(Boolean).length;
160
161
  if (remainingComponents < 0)
161
162
  throw new FsError("ENAMETOOLONG", { path });
162
163
  const parts = [];
163
164
  let links = 0;
164
- let expanded = path.length;
165
+ let expanded = pathByteLength(path);
165
166
  let absolute = path.startsWith("/");
166
167
  let traversalFailure;
167
168
  let failedDepth = Infinity;
@@ -218,9 +219,9 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
218
219
  throw new FsError("ELOOP", { path });
219
220
  const target = await Reflect.apply(readlink, filesystem, [lookup, options]);
220
221
  options.signal?.throwIfAborted();
221
- validatePath(target);
222
- expanded += target.length;
223
- if (expanded > 65536)
222
+ validatePath(target, options.pathLimits);
223
+ expanded += pathByteLength(target);
224
+ if (expanded > (options.pathLimits?.maxPathBytes ?? Infinity))
224
225
  throw new FsError("ENAMETOOLONG", { path });
225
226
  const targetSplit = target.split("/");
226
227
  const nonEmptyCount = targetSplit.filter(Boolean).length;
@@ -232,7 +233,7 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
232
233
  absolute = true;
233
234
  failedDepth = Infinity;
234
235
  }
235
- pending.unshift(...targetSplit);
236
+ pending = targetSplit.concat(pending);
236
237
  continue;
237
238
  }
238
239
  if (stat && pending.length && stat.type !== "directory") {