@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.
- package/README.md +16 -7
- package/dist/safe-fs/contracts/filesystem.d.ts +2 -0
- package/dist/safe-fs/contracts/io.js +26 -36
- package/dist/safe-fs/contracts/path.d.ts +2 -7
- package/dist/safe-fs/contracts/path.js +1 -7
- package/dist/safe-fs/contracts/virtual-path.d.ts +8 -3
- package/dist/safe-fs/contracts/virtual-path.js +22 -5
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/devices/path.js +18 -17
- package/dist/safe-fs/fs/memory/index.js +57 -43
- package/dist/safe-fs/fs/memory/limits.d.ts +5 -1
- package/dist/safe-fs/fs/memory/limits.js +7 -11
- package/dist/safe-fs/fs/memory/missing-target.d.ts +2 -1
- package/dist/safe-fs/fs/memory/missing-target.js +8 -1
- package/dist/safe-fs/fs/overlay/index.d.ts +4 -1
- package/dist/safe-fs/fs/overlay/index.js +57 -11
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +11 -4
- package/dist/safe-fs/fs/overlay/memory-publication.js +240 -14
- package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-fetch.js +74 -0
- package/dist/safe-fs/fs/s3/http/request-node.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-node.js +111 -0
- package/dist/safe-fs/fs/s3/http/request.d.ts +5 -4
- package/dist/safe-fs/fs/s3/http/request.js +1 -109
- package/dist/safe-fs/fs/s3/http/signature.js +7 -5
- package/dist/safe-fs/fs/s3/http/transport.js +6 -3
- package/dist/safe-fs/fs/s3/namespace.js +1 -1
- package/dist/safe-fs/fs/scoped.js +2 -2
- package/dist/safe-fs/fs/webdav/webdav.d.ts +3 -0
- package/dist/safe-fs/fs/webdav/webdav.js +5 -4
- package/dist/xml-ast/index.d.ts +2 -0
- package/dist/xml-ast/index.js +68 -31
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
4
|
-
export
|
|
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
|
|
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
|
|
2
|
-
export declare const MAX_PATH_COMPONENTS
|
|
3
|
-
export
|
|
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 =
|
|
3
|
-
export const MAX_PATH_COMPONENTS =
|
|
4
|
-
export function
|
|
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
|
|
23
|
+
if (maxBytes !== Infinity && pathByteLength(path) > maxBytes) {
|
|
9
24
|
throw new FsError("ENAMETOOLONG", { syscall: "resolve", path });
|
|
10
25
|
}
|
|
11
|
-
|
|
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++) {
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -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";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
34
|
-
let remainingComponents =
|
|
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
|
|
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
|
|
104
|
-
if (expanded >
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
let remainingComponents =
|
|
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
|
|
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
|
|
223
|
-
if (expanded >
|
|
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.
|
|
236
|
+
pending = targetSplit.concat(pending);
|
|
236
237
|
continue;
|
|
237
238
|
}
|
|
238
239
|
if (stat && pending.length && stat.type !== "directory") {
|