@poe-platform/safe-fs 0.1.720 → 0.1.722
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 +64 -18
- package/dist/safe-fs/bridge/filesystem.d.ts +2 -0
- package/dist/safe-fs/bridge/filesystem.js +17 -7
- package/dist/safe-fs/bridge/types.d.ts +2 -0
- package/dist/safe-fs/contracts/descriptor.d.ts +5 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +12 -0
- package/dist/safe-fs/contracts/io.d.ts +3 -1
- package/dist/safe-fs/contracts/io.js +48 -16
- package/dist/safe-fs/core.d.ts +2 -1
- package/dist/safe-fs/core.js +1 -1
- package/dist/safe-fs/fs/capabilities.js +6 -3
- package/dist/safe-fs/fs/descriptor.js +9 -2
- package/dist/safe-fs/fs/devices/index.d.ts +1 -0
- package/dist/safe-fs/fs/devices/index.js +24 -6
- package/dist/safe-fs/fs/memory/index.d.ts +1 -0
- package/dist/safe-fs/fs/memory/index.js +75 -3
- package/dist/safe-fs/fs/memory/limits.js +6 -4
- package/dist/safe-fs/fs/mount/index.d.ts +4 -0
- package/dist/safe-fs/fs/mount/index.js +74 -11
- package/dist/safe-fs/fs/object-publication/index.js +6 -6
- package/dist/safe-fs/fs/overlay/index.d.ts +4 -2
- package/dist/safe-fs/fs/overlay/index.js +92 -36
- package/dist/safe-fs/fs/quota/index.js +18 -16
- package/dist/safe-fs/fs/readonly/index.js +5 -3
- package/dist/safe-fs/fs/real/index.d.ts +21 -1
- package/dist/safe-fs/fs/real/index.js +266 -22
- package/dist/safe-fs/fs/s3/filesystem.d.ts +8 -0
- package/dist/safe-fs/fs/s3/filesystem.js +58 -12
- package/dist/safe-fs/fs/s3/http/request.d.ts +1 -1
- package/dist/safe-fs/fs/s3/http/request.js +1 -1
- package/dist/safe-fs/fs/s3/http/transport.js +8 -6
- package/dist/safe-fs/fs/s3/http/xml.d.ts +4 -1
- package/dist/safe-fs/fs/s3/http/xml.js +6 -2
- package/dist/safe-fs/fs/s3/namespace.js +12 -10
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +54 -35
- package/dist/safe-fs/fs/webdav/webdav.d.ts +4 -1
- package/dist/safe-fs/fs/webdav/webdav.js +30 -22
- package/dist/safe-fs/node/filesystem.d.ts +4 -1
- package/dist/safe-fs/node/filesystem.js +7 -1
- package/dist/safe-fs/node/host.d.ts +3 -0
- package/dist/safe-fs/node/host.js +78 -0
- package/dist/safe-fs/node/index.d.ts +2 -1
- package/dist/safe-fs/node/index.js +2 -1
- package/dist/safe-fs/platform/browser.d.ts +1 -0
- package/dist/safe-fs/platform/browser.js +1 -0
- package/dist/safe-fs/platform/node.d.ts +1 -0
- package/dist/safe-fs/platform/node.js +1 -0
- package/dist/safe-fs/python/emscripten.d.ts +1 -0
- package/dist/safe-fs/python/emscripten.js +5 -5
- package/dist/safe-fs/python/filesystem.js +5 -5
- package/dist/safe-fs/python/native.d.ts +1 -0
- package/dist/safe-fs/python/native.js +3 -2
- package/dist/safe-fs/xml.js +32 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,9 +23,11 @@ await fs.appendFile("note.txt", " world", "utf8");
|
|
|
23
23
|
console.log(await fs.readFile("note.txt", "utf8"));
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Output: `hello`, then `hello world`. Nothing touches the host filesystem. Raw adapters exchange `Uint8Array` values; the Node bridge adds strings, encodings, `Buffer` results, and stat predicates. Its `cwd` is
|
|
26
|
+
Output: `hello`, then `hello world`. Nothing touches the host filesystem. Raw adapters exchange `Uint8Array` values; the Node bridge adds strings, encodings, `Buffer` results, and stat predicates. Its `cwd` is the relative-path base and, by default, the confinement boundary. Set an explicit `root` to use a different boundary, for example `{ cwd: "/work", root: "/" }` to address a complete provider namespace. The bridge supports `unlink` when the provider offers atomic file removal; unsupported methods fail without host fallback. `createHostFileSystem()` provides trusted, unrestricted native host access when a rooted adapter is not desired.
|
|
27
27
|
|
|
28
|
-
For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. Read the safety boundary below before exposing it to untrusted code.
|
|
28
|
+
For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation, updates, and extraction can use this adapter's private owned staging in an isolated host tree. Its `trustedOwnedStaging` capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.
|
|
29
|
+
|
|
30
|
+
To create new outputs with Safe Bash's `dos2unix`, `unix2dos`, or compression commands, host adapters need atomic no-replace publication. Supply `createRealFileSystem({ root, renameNoReplace })` only when that callback binds a qualified native primitive such as Linux `renameat2` with `RENAME_NOREPLACE`. It receives resolved absolute host paths and an optional signal in its third argument. Existing destinations must reject `EEXIST` atomically; existence checks followed by rename and copy/delete are insufficient. Without this binding the adapter explicitly refuses those operations. See the [no-replace contract](src/contracts/filesystem.md#atomic-no-replace-rename).
|
|
29
31
|
|
|
30
32
|
## Entry points and shared identity
|
|
31
33
|
|
|
@@ -47,6 +49,10 @@ Existing `@poe-platform/safe-js/fs`, `/fs/core`, and `/fs/node` imports remain
|
|
|
47
49
|
re-exports, so `FsError` identity is shared; do not copy its implementation or the
|
|
48
50
|
`FileSystem` declaration. `FileSystem` is an interface, not a runtime class.
|
|
49
51
|
|
|
52
|
+
Custom filesystem views can use `registerEntryView(view, async path => ({ filesystem: backing, path }))`
|
|
53
|
+
from `/core` so `compareEntries` recognizes their backing entries in either direction.
|
|
54
|
+
The resolver can also return `readOnly: true`; each view can be registered once.
|
|
55
|
+
|
|
50
56
|
Legacy `poe-code/safe-fs` imports still use the CLI's bundled runtime. Keep an
|
|
51
57
|
application's factories, shell, and errors in one import family. At a host-owned
|
|
52
58
|
legacy adapter boundary, normalize foreign filesystem failures with `toFsError`
|
|
@@ -64,6 +70,7 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
|
|
|
64
70
|
| Remove an empty directory | Optional `rmdir`; never a recursive-delete fallback |
|
|
65
71
|
| Links and metadata | Optional `readlink`, `symlink`, `link`, `chmod`, `utimes`, `truncate` |
|
|
66
72
|
| Stream bytes | Optional `readStream`, `writeStream`, using async iterables of byte chunks |
|
|
73
|
+
| Retain an open file | Optional `open` with positioned I/O and synchronization; memory and real backends support `noFollow: true` to atomically refuse a final symlink |
|
|
67
74
|
| Publish immutable objects atomically | Optional `publishFileConditional` with opaque identity/version stats and authoritative compare-and-publish |
|
|
68
75
|
| Compare backing entries | Optional `compareEntry`, returning `same`, `distinct`, or `unknown` |
|
|
69
76
|
|
|
@@ -76,7 +83,7 @@ writes when the host supplies `store.createStaging`: private externally backed
|
|
|
76
83
|
pages keep working memory bounded without publishing the growing file after
|
|
77
84
|
every write. Conditional publication still occurs at sync/close, and retained
|
|
78
85
|
readers keep their old versions. Without that optional backend primitive, the
|
|
79
|
-
|
|
86
|
+
optional dirty-page budget limits unflushed output only when configured. See the
|
|
80
87
|
[object descriptor and spill contract](src/contracts/object-publication.md) for
|
|
81
88
|
backend methods, failure semantics and qualification; no provider storage is
|
|
82
89
|
configured automatically.
|
|
@@ -93,7 +100,7 @@ before implementing the host operation; ordinary writes do not provide it.
|
|
|
93
100
|
|
|
94
101
|
| Backend or wrapper | Use it for |
|
|
95
102
|
| --- | --- |
|
|
96
|
-
| `createMemoryFileSystem()` | Isolated, nonpersistent storage with links, permissions, timestamps, and streams |
|
|
103
|
+
| `createMemoryFileSystem()` | Isolated, nonpersistent storage with links, permissions, timestamps, and streams; each path resolution admits at most 65,536 cumulative UTF-16 code units across the input and followed symlink targets, rejecting excess with `ENAMETOOLONG` before component allocation |
|
|
97
104
|
| `createRealFileSystem({ root })` | An existing host directory, with virtual paths rooted inside it; Node only |
|
|
98
105
|
| `new S3FileSystem({ transport, bucket, … })` | Bucket/prefix storage through an explicitly supplied transport; Node only |
|
|
99
106
|
| `new WebDavFileSystem({ baseUrl, fetch, … })` | A WebDAV namespace through an explicitly supplied Fetch implementation |
|
|
@@ -219,12 +226,12 @@ There are no package environment variables, implicit credentials, or automatic `
|
|
|
219
226
|
|
|
220
227
|
| API | Options and defaults |
|
|
221
228
|
| --- | --- |
|
|
222
|
-
| `createFileSystem(config, { registry })` | Required `config.type`; `config.options` defaults to an empty record. `registry` is required. Built-in `memory` accepts
|
|
229
|
+
| `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`. |
|
|
223
230
|
| `createNodeFileSystemAdapterRegistry(extensions?)` | Optional map of additional adapter descriptors; defaults to only `memory` and `real`. |
|
|
224
|
-
| Memory / read-only | Memory
|
|
231
|
+
| 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. |
|
|
225
232
|
| Real | Required `root`: existing absolute host directory; the constructor/factory also accepts the root string directly. |
|
|
226
233
|
| Mount | Required `root`: fallback filesystem. `mounts` defaults to `{}` and maps absolute virtual paths to filesystems. |
|
|
227
|
-
| Overlay | Required `upper` and `lower`; `maxBufferBytes`
|
|
234
|
+
| Overlay | Required `upper` and `lower`; `maxBufferBytes` is unlimited unless configured. |
|
|
228
235
|
| Quota | `withFileSystemQuota` requires a nonnegative safe-integer `maxBytes`. It serializes mutations and counts files, symlinks, copies, hard links, truncation, and streaming writes. |
|
|
229
236
|
| Node bridge | `cwd` defaults to `/`, must be an absolute virtual path; optional lifetime `signal`. |
|
|
230
237
|
| Portable bridge | Same `cwd` and `signal`, plus required `codec` with `isEncoding`, `encode`, and `decode` functions. |
|
|
@@ -246,6 +253,8 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
246
253
|
|
|
247
254
|
`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). 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).
|
|
248
255
|
|
|
256
|
+
`collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Browser and Worker bundles additionally share a fixed 32 MiB budget across active collectors, even when byte limits are omitted. The returned view may retain geometric spare capacity. This budget covers 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.
|
|
257
|
+
|
|
249
258
|
<details>
|
|
250
259
|
<summary>S3 filesystem and HTTP transport options</summary>
|
|
251
260
|
|
|
@@ -261,11 +270,39 @@ For in-memory S3 simulations, `new MockS3Client({ buckets, pageSize?, now?, auth
|
|
|
261
270
|
| `readOnly` | `false` |
|
|
262
271
|
| `allowNonAtomicRename` | `true`; rename copies then deletes, without rollback guarantees |
|
|
263
272
|
| `pageSize` | 1,000; range 1–1,000 |
|
|
264
|
-
| `maxReadBytes` |
|
|
265
|
-
| `maxStreamBytes` |
|
|
266
|
-
| `maxListEntries` |
|
|
273
|
+
| `maxReadBytes` | Unlimited unless configured |
|
|
274
|
+
| `maxStreamBytes` | Unlimited unless configured |
|
|
275
|
+
| `maxListEntries` | Unlimited unless configured |
|
|
276
|
+
| `removalLimits.maxRequests` | 32 transport calls per `rm`, including lookup, listing, and deletes |
|
|
277
|
+
| `removalLimits.maxListEntries` | 32 returned listing entries in aggregate per `rm`, including lookup |
|
|
278
|
+
| `removalLimits.maxDeleteObjects` | 16 objects per `rm` |
|
|
267
279
|
| `compareEntry` | Optional trusted backing-identity callback |
|
|
268
280
|
|
|
281
|
+
Removal limits apply even when shell filesystem-call limits admit a recursive `rm` as one operation. Each limit accepts a positive safe integer. Traversal stops at the listing/request cap and rejects with `EFBIG`; all delete requests must fit the remaining request budget before the first mutation. Nonrecursive removal checks for children using pages of at most two entries. Configure larger `removalLimits` only where the deployment can afford the corresponding work. These limits count adapter transport calls; retries inside a supplied transport need their own limit. Remote failures or concurrent writers can still cause partial deletion after preflight.
|
|
282
|
+
|
|
283
|
+
For larger trees, a trusted integration can process one bounded batch per request/job using its explicitly supplied transport. This example uses at most 17 transport calls and retains at most 16 summaries; repeat in a later job until `done`. The prefix must come from trusted deployment configuration, include the filesystem's configured prefix, and end in `/`. This deliberately bypasses filesystem collision checks and deletes directory markers as well as files; serialize it with writers when complete removal is required.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
async function removeBatch(transport, bucket, trustedDirectoryPrefix, abortSignal) {
|
|
287
|
+
const options = { abortSignal };
|
|
288
|
+
const page = await transport.listObjectsV2({
|
|
289
|
+
Bucket: bucket, Prefix: trustedDirectoryPrefix, MaxKeys: 16,
|
|
290
|
+
}, options);
|
|
291
|
+
const objects = page.Contents ?? [];
|
|
292
|
+
if (objects.length > 16 || page.CommonPrefixes?.length
|
|
293
|
+
|| objects.some(object => !object.Key?.startsWith(trustedDirectoryPrefix))
|
|
294
|
+
|| typeof page.IsTruncated !== "boolean") {
|
|
295
|
+
throw new Error("Invalid batch listing");
|
|
296
|
+
}
|
|
297
|
+
for (const object of objects) {
|
|
298
|
+
await transport.deleteObject({ Bucket: bucket, Key: object.Key }, options);
|
|
299
|
+
}
|
|
300
|
+
return { deleted: objects.length, done: !page.IsTruncated };
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Each batch lists from the beginning because previous keys have been deleted; it does not reuse continuation tokens across mutations. A job runner should cap the number of batches and schedule remaining work separately.
|
|
305
|
+
|
|
269
306
|
`createS3HttpTransport` requires `endpoint` (an origin without path or credentials), `region`, and `credentials`. Credentials contain `accessKeyId`, `secretAccessKey`, and optional `sessionToken`, or come from an async provider receiving `{ signal }`.
|
|
270
307
|
|
|
271
308
|
| Optional field | Default / meaning |
|
|
@@ -273,9 +310,9 @@ For in-memory S3 simulations, `new MockS3Client({ buckets, pageSize?, now?, auth
|
|
|
273
310
|
| `addressingStyle` | `path`; alternative `virtual-hosted` requires a DNS endpoint |
|
|
274
311
|
| `listUrlEncoding` | `percent`; alternative `form` |
|
|
275
312
|
| `allowInsecureHttp` | `false`; HTTPS required unless explicitly enabled |
|
|
276
|
-
| `maxPutBytes`, `maxGetBytes` |
|
|
277
|
-
| `maxXmlBytes` |
|
|
278
|
-
| `requestTimeoutMs` |
|
|
313
|
+
| `maxPutBytes`, `maxGetBytes` | Unlimited unless configured |
|
|
314
|
+
| `maxXmlBytes` | Unlimited unless configured |
|
|
315
|
+
| `requestTimeoutMs` | Unlimited unless configured |
|
|
279
316
|
| `enableCopy` | `true`; disabling uses a buffered GET/PUT fallback |
|
|
280
317
|
| `verifiedConditionalOperations` | Optional `put`, `copy`, `delete` booleans, each defaulting to false; enable only after verifying the server's semantics |
|
|
281
318
|
| `clock` | Current date/time function, used for signing |
|
|
@@ -292,14 +329,16 @@ For in-memory S3 simulations, `new MockS3Client({ buckets, pageSize?, now?, auth
|
|
|
292
329
|
| --- | --- |
|
|
293
330
|
| `headers` | Empty; explicit authentication/custom headers. Protocol-reserved headers are rejected; authorization and cookies require HTTPS. |
|
|
294
331
|
| `requestStreamSupport` | `native` for global Fetch, otherwise false; accepts `native` or a boolean declaration for the injected transport |
|
|
295
|
-
| `maxResponseBytes` |
|
|
296
|
-
| `maxXmlBytes` |
|
|
297
|
-
| `maxEntries` |
|
|
298
|
-
| `timeoutMs` |
|
|
332
|
+
| `maxResponseBytes` | 16 MiB; applies to decoded response bytes |
|
|
333
|
+
| `maxXmlBytes` | 1 MiB before metadata decoding/parsing |
|
|
334
|
+
| `maxEntries` | Unlimited unless configured |
|
|
335
|
+
| `timeoutMs` | Unlimited unless configured |
|
|
299
336
|
| `overwritePolicy` | `lock`; alternative `etag` uses conditional overwrites |
|
|
300
337
|
| `atomicEmptyDirectory` | Optional trusted binding with the canonical `namespaceUrl` and `removeEmptyDirectory` callback; required for strict empty-only `rmdir` |
|
|
301
338
|
| `compareEntry` | Optional trusted backing-identity callback on Node; unavailable under browser policy |
|
|
302
339
|
|
|
340
|
+
Known identity Content-Length responses use one result buffer; other responses grow storage up to the configured ceiling and return a view without a final copy. Growth can temporarily retain the old and new buffers (up to three times the response size), plus transport chunks. These per-response defaults leave headroom in Workers; hosts must still budget for metadata parsing, text decoding, transport buffers, and concurrent reads, especially when raising the limits. Use streaming reads for large files.
|
|
341
|
+
|
|
303
342
|
See the [binding types](src/fs/webdav/webdav.ts) before implementing atomic directory removal. A recursive WebDAV DELETE does not satisfy that contract.
|
|
304
343
|
|
|
305
344
|
</details>
|
|
@@ -307,9 +346,16 @@ See the [binding types](src/fs/webdav/webdav.ts) before implementing atomic dire
|
|
|
307
346
|
## Safety boundary and limitations
|
|
308
347
|
|
|
309
348
|
- **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.
|
|
310
|
-
- **A read-only view is not an immutable store.** Other references can still change the backing filesystem. Overlays are not transactions; cancellation and cleanup do not guarantee rollback. Cross-mount rename can fail with `EXDEV`.
|
|
349
|
+
- **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 are not transactions; cancellation and cleanup do not guarantee rollback. Cross-mount rename can fail with `EXDEV`.
|
|
311
350
|
- **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.
|
|
312
351
|
- **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.
|
|
313
352
|
- **Browser support is filesystem-only.** The `browser` export condition selects the portable surface: memory, mounts, overlays, read-only, WebDAV, and the codec-based bridge. Real storage, S3, 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.
|
|
314
353
|
- **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`.
|
|
315
354
|
- **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.
|
|
355
|
+
|
|
356
|
+
`MemoryFileSystem.confineExtraction(roots)` returns a view for archive extraction.
|
|
357
|
+
It retains each root directory and its ancestors, refuses symlink ancestry at
|
|
358
|
+
mutation commit, and keeps streamed writes attached to the opened file node.
|
|
359
|
+
Other adapters omit this operation unless they can enforce the same boundary.
|
|
360
|
+
Trusted host decorators must explicitly preserve confinement when forwarding it;
|
|
361
|
+
custom mutation hooks must preserve the atomic commit contract.
|
|
@@ -14,6 +14,7 @@ export declare class FileSystemBridge<Binary extends Uint8Array> {
|
|
|
14
14
|
#private;
|
|
15
15
|
constructor(fs: FileSystem, options: {
|
|
16
16
|
readonly cwd?: string;
|
|
17
|
+
readonly root?: string;
|
|
17
18
|
readonly signal?: AbortSignal;
|
|
18
19
|
}, primitives: BridgePrimitives<Binary>);
|
|
19
20
|
readFile(path: unknown, options?: {
|
|
@@ -65,6 +66,7 @@ export declare class FileSystemBridge<Binary extends Uint8Array> {
|
|
|
65
66
|
}) | null): Promise<void>;
|
|
66
67
|
mkdir(path: unknown, options?: Mode | MakeDirectoryOptions | null): Promise<string | undefined>;
|
|
67
68
|
access(path: unknown, mode?: number): Promise<void>;
|
|
69
|
+
unlink(path: unknown): Promise<void>;
|
|
68
70
|
rm(path: unknown, value?: unknown): Promise<void>;
|
|
69
71
|
rmdir(path: unknown, value?: unknown): Promise<void>;
|
|
70
72
|
rename(source: unknown, destination: unknown): Promise<void>;
|
|
@@ -42,6 +42,7 @@ function timeValue(value) {
|
|
|
42
42
|
export class FileSystemBridge {
|
|
43
43
|
#fs;
|
|
44
44
|
#cwd;
|
|
45
|
+
#root;
|
|
45
46
|
#signal;
|
|
46
47
|
#primitives;
|
|
47
48
|
#codec;
|
|
@@ -63,6 +64,8 @@ export class FileSystemBridge {
|
|
|
63
64
|
});
|
|
64
65
|
this.#fs = fs;
|
|
65
66
|
this.#cwd = primitives.paths.resolve("/", cwd);
|
|
67
|
+
this.#root = primitives.paths.resolve("/", options.root ?? cwd);
|
|
68
|
+
assertBridgePath(this.#root, this.#cwd);
|
|
66
69
|
this.#signal = options.signal;
|
|
67
70
|
}
|
|
68
71
|
#encoding(value, fallback, names = false) {
|
|
@@ -83,7 +86,7 @@ export class FileSystemBridge {
|
|
|
83
86
|
throw fsError("ENOENT", "path", path);
|
|
84
87
|
checkSignal(this.#signal);
|
|
85
88
|
const absolute = this.#primitives.paths.isAbsolute(path) ? path : childPath(this.#cwd, path);
|
|
86
|
-
assertBridgePath(this.#
|
|
89
|
+
assertBridgePath(this.#root, absolute);
|
|
87
90
|
return absolute;
|
|
88
91
|
}
|
|
89
92
|
async #call(paths, operation, signal, noFollow = []) {
|
|
@@ -98,9 +101,9 @@ export class FileSystemBridge {
|
|
|
98
101
|
return await withSignal(combined, async () => {
|
|
99
102
|
const absolute = paths.map((path) => this.#path(path));
|
|
100
103
|
const resolved = [...absolute];
|
|
101
|
-
if (this.#
|
|
104
|
+
if (this.#root !== "/") {
|
|
102
105
|
for (let index = 0; index < absolute.length; index++) {
|
|
103
|
-
resolved[index] = await checkedBridgePath(this.#fs, this.#
|
|
106
|
+
resolved[index] = await checkedBridgePath(this.#fs, this.#root, absolute[index], options, !noFollow.includes(index));
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
checkSignal(combined);
|
|
@@ -208,7 +211,7 @@ export class FileSystemBridge {
|
|
|
208
211
|
if (!hasCode(error, "ENOENT"))
|
|
209
212
|
throw error;
|
|
210
213
|
firstCreated = candidate;
|
|
211
|
-
if (candidate === this.#
|
|
214
|
+
if (candidate === this.#root)
|
|
212
215
|
break;
|
|
213
216
|
const parent = this.#primitives.paths.dirname(candidate);
|
|
214
217
|
if (parent === candidate)
|
|
@@ -225,6 +228,13 @@ export class FileSystemBridge {
|
|
|
225
228
|
throw new TypeError("Invalid access mode");
|
|
226
229
|
await this.#call([path], (signal, resolved) => this.#fs.access(resolved[0], mode, signal));
|
|
227
230
|
}
|
|
231
|
+
async unlink(path) {
|
|
232
|
+
await this.#call([path], async (signal, resolved) => {
|
|
233
|
+
if (!this.#fs.unlink)
|
|
234
|
+
unsupported("unlink");
|
|
235
|
+
await this.#fs.unlink(resolved[0], signal);
|
|
236
|
+
}, undefined, [0]);
|
|
237
|
+
}
|
|
228
238
|
async rm(path, value) {
|
|
229
239
|
const options = optionsRecord(value, ["force", "recursive", "maxRetries", "retryDelay"]);
|
|
230
240
|
if (options.maxRetries !== undefined && options.maxRetries !== 0)
|
|
@@ -275,7 +285,7 @@ export class FileSystemBridge {
|
|
|
275
285
|
const from = this.#path(source);
|
|
276
286
|
const to = this.#path(destination);
|
|
277
287
|
const canonicalFrom = await this.realpath(from);
|
|
278
|
-
let ancestor = to === this.#
|
|
288
|
+
let ancestor = to === this.#root ? to : this.#primitives.paths.dirname(to);
|
|
279
289
|
let canonicalAncestor;
|
|
280
290
|
while (true) {
|
|
281
291
|
try {
|
|
@@ -342,7 +352,7 @@ export class FileSystemBridge {
|
|
|
342
352
|
async realpath(path, value) {
|
|
343
353
|
const codec = this.#encoding(optionsRecord(value, ["encoding"]).encoding, "utf8", true);
|
|
344
354
|
const target = await this.#call([path], (signal, resolved) => this.#fs.realpath(resolved[0], signal));
|
|
345
|
-
assertBridgePath(this.#
|
|
355
|
+
assertBridgePath(this.#root, target);
|
|
346
356
|
return codec === "buffer" ? this.#textBytes(target) : this.#codec.decode(this.#textBytes(target), codec);
|
|
347
357
|
}
|
|
348
358
|
async mkdtemp(prefix, value) {
|
|
@@ -371,7 +381,7 @@ export class FileSystemBridge {
|
|
|
371
381
|
const destination = this.#path(path);
|
|
372
382
|
const absoluteTarget = this.#path(this.#primitives.paths.isAbsolute(linkTarget)
|
|
373
383
|
? linkTarget : childPath(this.#primitives.paths.dirname(destination), linkTarget));
|
|
374
|
-
if (this.#
|
|
384
|
+
if (this.#root !== "/" && this.#primitives.paths.isAbsolute(linkTarget)) {
|
|
375
385
|
throw fsError("ENOTSUP", "symlink", destination);
|
|
376
386
|
}
|
|
377
387
|
const method = this.#fs.symlink;
|
|
@@ -7,6 +7,8 @@ export interface FsBridgeCodec {
|
|
|
7
7
|
export interface FsBridgeOptions {
|
|
8
8
|
readonly codec: FsBridgeCodec;
|
|
9
9
|
readonly cwd?: string;
|
|
10
|
+
/** Optional confinement boundary; defaults to cwd. */
|
|
11
|
+
readonly root?: string;
|
|
10
12
|
readonly signal?: AbortSignal;
|
|
11
13
|
}
|
|
12
14
|
export interface FsBridgeFileSystem extends FileSystem {
|
|
@@ -4,10 +4,15 @@ export interface OpenFileOptions extends FsOptions {
|
|
|
4
4
|
readonly creation?: "never" | "ifMissing" | "exclusive";
|
|
5
5
|
readonly truncate?: boolean;
|
|
6
6
|
readonly append?: boolean;
|
|
7
|
+
/** Atomically reject a final symlink; parent symlinks still follow provider policy. */
|
|
8
|
+
readonly noFollow?: boolean;
|
|
7
9
|
readonly mode?: number;
|
|
10
|
+
/** Apply mode exactly to new entries, without an additional host umask. */
|
|
11
|
+
readonly exactMode?: boolean;
|
|
8
12
|
readonly synchronization?: "data" | "all";
|
|
9
13
|
}
|
|
10
14
|
export interface FileDescriptorCapabilities {
|
|
15
|
+
readonly noFollow?: boolean;
|
|
11
16
|
readonly publication?: "conditional";
|
|
12
17
|
readonly position?: boolean;
|
|
13
18
|
readonly readObservation?: boolean;
|
|
@@ -6,6 +6,8 @@ export type FileType = "file" | "directory" | "symlink" | "character";
|
|
|
6
6
|
export type EntryComparison = "same" | "distinct" | "unknown";
|
|
7
7
|
export interface FileStat {
|
|
8
8
|
readonly type: FileType;
|
|
9
|
+
/** Backend-reported type of the containing filesystem; absence means unknown. */
|
|
10
|
+
readonly filesystemType?: string;
|
|
9
11
|
readonly size: number;
|
|
10
12
|
readonly allocatedBytes?: number;
|
|
11
13
|
readonly ioBlockSize?: number;
|
|
@@ -65,6 +67,8 @@ export interface FileSystemCapabilities {
|
|
|
65
67
|
readonly permissions?: boolean;
|
|
66
68
|
readonly timestamps?: boolean;
|
|
67
69
|
readonly atomicRename?: boolean;
|
|
70
|
+
/** Owned staging serialized within a trusted host; requires external tree isolation. */
|
|
71
|
+
readonly trustedOwnedStaging?: boolean;
|
|
68
72
|
readonly atomicFileStaging?: boolean;
|
|
69
73
|
readonly atomicFilePublication?: boolean;
|
|
70
74
|
readonly atomicFileMutation?: boolean;
|
|
@@ -89,6 +93,7 @@ export interface OpenReadFileOptions extends FsOptions {
|
|
|
89
93
|
}
|
|
90
94
|
export interface CapabilityQueryOptions extends OpenReadFileOptions {
|
|
91
95
|
readonly create?: boolean;
|
|
96
|
+
readonly creation?: OpenFileOptions["creation"];
|
|
92
97
|
}
|
|
93
98
|
export interface FileReadHandle {
|
|
94
99
|
/** Exact operations on the same retained object; close owns both facets. */
|
|
@@ -150,6 +155,8 @@ export interface AppendFileOptions extends FsOptions {
|
|
|
150
155
|
readonly mode?: number;
|
|
151
156
|
}
|
|
152
157
|
export interface MkdirOptions extends FsOptions {
|
|
158
|
+
/** Apply mode exactly to new entries, without an additional host umask. */
|
|
159
|
+
readonly exactMode?: boolean;
|
|
153
160
|
readonly recursive?: boolean;
|
|
154
161
|
readonly mode?: number;
|
|
155
162
|
}
|
|
@@ -217,6 +224,11 @@ export interface ConditionalFilePublicationOptions extends FsOptions {
|
|
|
217
224
|
readonly mtimeMs?: number;
|
|
218
225
|
}
|
|
219
226
|
export interface FileSystem {
|
|
227
|
+
/** Retain each extraction root and enforce no-symlink ancestry atomically with
|
|
228
|
+
* every mutation (including metadata and both hardlink paths). Returned views
|
|
229
|
+
* must refuse unsupported mutations; no check-then-write emulation is allowed.
|
|
230
|
+
* Roots and their ancestors must remain the retained directories. */
|
|
231
|
+
confineExtraction?(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
|
|
220
232
|
/** Explicit retained-object capability for qualified byte-path backends. */
|
|
221
233
|
readonly objects?: ObjectFileSystem;
|
|
222
234
|
/** Consume the complete source privately, then atomically compare/publish.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type ByteSource = AsyncIterable<Uint8Array>;
|
|
2
2
|
export interface CollectOptions {
|
|
3
|
-
readonly maxBytes
|
|
3
|
+
readonly maxBytes?: number;
|
|
4
|
+
/** Owned capacity, current input backing storage, and replacement allocation peak. */
|
|
5
|
+
readonly maxMemoryBytes?: number;
|
|
4
6
|
readonly signal?: AbortSignal;
|
|
5
7
|
}
|
|
6
8
|
export declare function toByteSource(input: string | Uint8Array): ByteSource;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FsError } from "./errors.js";
|
|
2
2
|
import { finishCleanup } from "./cleanup.js";
|
|
3
|
+
import { platform } from "#safe-fs-platform";
|
|
3
4
|
export function toByteSource(input) {
|
|
4
5
|
if (typeof input !== "string" && !(input instanceof Uint8Array)) {
|
|
5
6
|
throw new TypeError("Byte source input must be a string or Uint8Array");
|
|
@@ -10,29 +11,60 @@ export function toByteSource(input) {
|
|
|
10
11
|
yield bytes;
|
|
11
12
|
})();
|
|
12
13
|
}
|
|
14
|
+
let activeCollectionBytes = 0;
|
|
13
15
|
export async function collectBytes(source, options) {
|
|
14
|
-
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
16
|
+
if (options.maxBytes !== undefined && options.maxBytes !== Infinity && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)) {
|
|
15
17
|
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
16
18
|
}
|
|
17
|
-
|
|
19
|
+
if (options.maxMemoryBytes !== undefined && (!Number.isSafeInteger(options.maxMemoryBytes) || options.maxMemoryBytes < 0)) {
|
|
20
|
+
throw new RangeError("maxMemoryBytes must be a nonnegative safe integer");
|
|
21
|
+
}
|
|
22
|
+
const memoryLimit = options.maxMemoryBytes ?? Infinity;
|
|
23
|
+
let reserved = 0;
|
|
24
|
+
let inputBytes = 0;
|
|
25
|
+
let buffer = new Uint8Array(0);
|
|
18
26
|
let size = 0;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
throw new FsError("EFBIG", { syscall: "collectBytes", message: "output exceeds maxBytes" });
|
|
27
|
+
function reserve(bytes) {
|
|
28
|
+
if (bytes > memoryLimit - reserved || bytes > platform.maxCollectionBytes - activeCollectionBytes) {
|
|
29
|
+
throw new FsError("EFBIG", { syscall: "collectBytes", message: "collection exceeds memory budget" });
|
|
23
30
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
size += chunk.byteLength;
|
|
31
|
+
reserved += bytes;
|
|
32
|
+
activeCollectionBytes += bytes;
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
try {
|
|
35
|
+
options.signal?.throwIfAborted();
|
|
36
|
+
for await (const chunk of readBytes(source, options.signal)) {
|
|
37
|
+
activeCollectionBytes -= inputBytes;
|
|
38
|
+
reserved -= inputBytes;
|
|
39
|
+
inputBytes = 0;
|
|
40
|
+
if (chunk.byteLength > (options.maxBytes ?? Infinity) - size) {
|
|
41
|
+
throw new FsError("EFBIG", { syscall: "collectBytes", message: "output exceeds maxBytes" });
|
|
42
|
+
}
|
|
43
|
+
// A small view can retain a large backing buffer. Keep this reservation
|
|
44
|
+
// while awaiting the next chunk, including while its source is suspended.
|
|
45
|
+
reserve(chunk.buffer.byteLength);
|
|
46
|
+
inputBytes = chunk.buffer.byteLength;
|
|
47
|
+
const nextSize = size + chunk.byteLength;
|
|
48
|
+
if (nextSize > buffer.byteLength) {
|
|
49
|
+
const capacity = Math.min(options.maxBytes ?? Infinity, Math.max(nextSize, buffer.byteLength * 2));
|
|
50
|
+
// Admit both generations before allocation. Do not fall back to repeated
|
|
51
|
+
// exact-size growth when geometric growth exceeds the budget.
|
|
52
|
+
reserve(capacity);
|
|
53
|
+
const replacement = new Uint8Array(capacity);
|
|
54
|
+
replacement.set(buffer.subarray(0, size));
|
|
55
|
+
activeCollectionBytes -= buffer.byteLength;
|
|
56
|
+
reserved -= buffer.byteLength;
|
|
57
|
+
buffer = replacement;
|
|
58
|
+
}
|
|
59
|
+
buffer.set(chunk, size);
|
|
60
|
+
size = nextSize;
|
|
61
|
+
}
|
|
62
|
+
options.signal?.throwIfAborted();
|
|
63
|
+
return buffer.subarray(0, size);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
activeCollectionBytes -= reserved;
|
|
34
67
|
}
|
|
35
|
-
return result;
|
|
36
68
|
}
|
|
37
69
|
async function abortable(operation, signal) {
|
|
38
70
|
signal?.throwIfAborted();
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export type { RetainedFileSystemCleanupView, RetainedFileSystemCleanupOptions }
|
|
|
17
17
|
export * from "./fs/webdav/index.js";
|
|
18
18
|
export * from "./bridge/index.js";
|
|
19
19
|
export * from "./python/index.js";
|
|
20
|
-
export { compareEntries } from "./fs/mount/comparison.js";
|
|
20
|
+
export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
|
|
21
|
+
export type { EntryViewResolver } from "./fs/mount/comparison.js";
|
|
21
22
|
export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
|
|
22
23
|
export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
|
|
23
24
|
export * from "./contracts/object.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -15,7 +15,7 @@ export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
|
15
15
|
export * from "./fs/webdav/index.js";
|
|
16
16
|
export * from "./bridge/index.js";
|
|
17
17
|
export * from "./python/index.js";
|
|
18
|
-
export { compareEntries } from "./fs/mount/comparison.js";
|
|
18
|
+
export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
|
|
19
19
|
export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
|
|
20
20
|
export * from "./contracts/object.js";
|
|
21
21
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
@@ -108,7 +108,7 @@ export function readOnlyCapabilities(capabilities) {
|
|
|
108
108
|
mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
|
|
109
109
|
rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
|
|
110
110
|
randomAccessWrite: false, hardlinks: false, permissions: false, timestamps: false,
|
|
111
|
-
descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
111
|
+
descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
112
112
|
atomicFilePublication: false, atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
|
|
113
113
|
});
|
|
114
114
|
}
|
|
@@ -116,7 +116,7 @@ export function quotaCapabilities(capabilities) {
|
|
|
116
116
|
const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
|
|
117
117
|
const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
|
|
118
118
|
const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
|
|
119
|
-
return Object.freeze({ ...rest, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
119
|
+
return Object.freeze({ ...rest, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
120
120
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
121
121
|
...(streamingAppend === undefined ? {} : { streamingAppend }),
|
|
122
122
|
});
|
|
@@ -136,12 +136,15 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
|
|
|
136
136
|
unavailable.atomicFileStaging = false;
|
|
137
137
|
if (capabilities.atomicDirectoryMetadata === true && (capabilities.readOnly === true || typeof filesystem.prepareDirectory !== "function"))
|
|
138
138
|
unavailable.atomicDirectoryMetadata = false;
|
|
139
|
+
if (capabilities.trustedOwnedStaging === true && (capabilities.readOnly === true
|
|
140
|
+
|| ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"].some(method => typeof filesystem[method] !== "function")))
|
|
141
|
+
unavailable.trustedOwnedStaging = false;
|
|
139
142
|
return Object.keys(unavailable).length ? { ...capabilities, ...unavailable } : capabilities;
|
|
140
143
|
}
|
|
141
144
|
export async function requireOwnedMutation(filesystem, path, capability, options, create = false) {
|
|
142
145
|
options.signal?.throwIfAborted();
|
|
143
146
|
const capabilities = ownedMutationCapabilities(filesystem, await filesystem.capabilitiesFor?.(path, create ? { ...options, create: true } : options) ?? filesystem.capabilities);
|
|
144
147
|
options.signal?.throwIfAborted();
|
|
145
|
-
if (capabilities[capability] !== true)
|
|
148
|
+
if (capabilities[capability] !== true && !(capabilities.trustedOwnedStaging === true && ["atomicFileStaging", "atomicFileMutation", "atomicDirectoryMetadata"].includes(capability)))
|
|
146
149
|
throw new FsError("ENOTSUP", { path, syscall: capability });
|
|
147
150
|
}
|
|
@@ -2,6 +2,7 @@ import { FsError } from "../contracts/errors.js";
|
|
|
2
2
|
import { finishCleanup } from "../contracts/cleanup.js";
|
|
3
3
|
function admitCapabilities(path, options, capabilities) {
|
|
4
4
|
if (![capabilities.positionedRead, capabilities.positionedWrite, capabilities.truncate].every(value => typeof value === "boolean")
|
|
5
|
+
|| capabilities.noFollow !== undefined && typeof capabilities.noFollow !== "boolean"
|
|
5
6
|
|| capabilities.publication !== undefined && capabilities.publication !== "conditional"
|
|
6
7
|
|| capabilities.position !== undefined && typeof capabilities.position !== "boolean"
|
|
7
8
|
|| capabilities.readObservation !== undefined && typeof capabilities.readObservation !== "boolean"
|
|
@@ -10,7 +11,8 @@ function admitCapabilities(path, options, capabilities) {
|
|
|
10
11
|
|| capabilities.delegateZeroLengthWrite !== undefined && typeof capabilities.delegateZeroLengthWrite !== "boolean"
|
|
11
12
|
|| !["none", "volatile", "storage"].includes(capabilities.synchronization))
|
|
12
13
|
throw new FsError("EINVAL", { syscall: "open", path });
|
|
13
|
-
if (options.
|
|
14
|
+
if (options.noFollow && capabilities.noFollow !== true
|
|
15
|
+
|| options.truncate && !(capabilities.openTruncate ?? capabilities.truncate) || options.synchronization !== undefined && capabilities.synchronization === "none") {
|
|
14
16
|
throw new FsError("ENOTSUP", { syscall: "open", path });
|
|
15
17
|
}
|
|
16
18
|
}
|
|
@@ -68,6 +70,7 @@ class ManagedFileDescriptor {
|
|
|
68
70
|
this.#backend = backend;
|
|
69
71
|
const positionedAppendWrite = capabilities.positionedAppendWrite === true && capabilities.positionedWrite && options.access !== "read";
|
|
70
72
|
this.capabilities = Object.freeze({
|
|
73
|
+
...(capabilities.noFollow === undefined ? {} : { noFollow: capabilities.noFollow }),
|
|
71
74
|
...(capabilities.publication === undefined ? {} : { publication: capabilities.publication }),
|
|
72
75
|
...(capabilities.position === undefined ? {} : { position: capabilities.position }),
|
|
73
76
|
...(capabilities.readObservation === undefined ? {} : { readObservation: capabilities.readObservation }),
|
|
@@ -206,11 +209,13 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
|
|
|
206
209
|
throw new FsError("EINVAL", { syscall: "open", path });
|
|
207
210
|
const signal = options.signal;
|
|
208
211
|
signal?.throwIfAborted();
|
|
209
|
-
const keys = ["access", "creation", "truncate", "append", "mode", "synchronization", "signal"];
|
|
212
|
+
const keys = ["access", "creation", "truncate", "append", "mode", "exactMode", "noFollow", "synchronization", "signal"];
|
|
210
213
|
const { access, creation = "never", truncate = false, append = false, mode = 0o666, synchronization } = options;
|
|
211
214
|
if (Object.keys(options).some(key => !keys.includes(key))
|
|
212
215
|
|| !["read", "write", "readwrite"].includes(access)
|
|
213
216
|
|| !["never", "ifMissing", "exclusive"].includes(creation)
|
|
217
|
+
|| options.noFollow !== undefined && typeof options.noFollow !== "boolean"
|
|
218
|
+
|| options.exactMode !== undefined && typeof options.exactMode !== "boolean"
|
|
214
219
|
|| typeof truncate !== "boolean" || typeof append !== "boolean"
|
|
215
220
|
|| access === "read" && (truncate || append)
|
|
216
221
|
|| synchronization !== undefined && !["data", "all"].includes(synchronization)
|
|
@@ -218,6 +223,8 @@ export async function openFileDescriptor(path, options, capabilities, acquire) {
|
|
|
218
223
|
throw new FsError("EINVAL", { syscall: "open", path });
|
|
219
224
|
}
|
|
220
225
|
const admitted = Object.freeze({ access, creation, truncate, append, mode,
|
|
226
|
+
...(options.noFollow === undefined ? {} : { noFollow: options.noFollow }),
|
|
227
|
+
...(options.exactMode === undefined ? {} : { exactMode: options.exactMode }),
|
|
221
228
|
...(signal === undefined ? {} : { signal }), ...(synchronization === undefined ? {} : { synchronization }) });
|
|
222
229
|
const admittedCapabilities = Object.freeze({ ...capabilities });
|
|
223
230
|
admitCapabilities(path, admitted, admittedCapabilities);
|
|
@@ -39,6 +39,7 @@ export declare class DeviceFileSystem implements FileSystem {
|
|
|
39
39
|
access(path: string, mode?: number, options?: FsOptions): Promise<void>;
|
|
40
40
|
readlink(path: string, options?: FsOptions): Promise<string>;
|
|
41
41
|
symlink(target: string, path: string, options?: FsOptions): Promise<void>;
|
|
42
|
+
confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
|
|
42
43
|
link(existingPath: string, newPath: string, options?: FsOptions): Promise<void>;
|
|
43
44
|
chmod(path: string, mode: number, options?: FsOptions): Promise<void>;
|
|
44
45
|
utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
|