@poe-platform/safe-fs 0.1.805 → 0.1.807
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 +5 -5
- package/dist/safe-fs/bridge/filesystem.d.ts +1 -0
- package/dist/safe-fs/bridge/filesystem.js +3 -0
- package/dist/safe-fs/bridge/index.d.ts +1 -0
- package/dist/safe-fs/bridge/index.js +1 -0
- package/dist/safe-fs/config/registry.d.ts +2 -0
- package/dist/safe-fs/config/registry.js +15 -0
- package/dist/safe-fs/config.node.js +3 -14
- package/dist/safe-fs/config.portable.d.ts +3 -0
- package/dist/safe-fs/config.portable.js +9 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +12 -0
- package/dist/safe-fs/contracts/index.d.ts +1 -0
- package/dist/safe-fs/contracts/index.js +1 -0
- package/dist/safe-fs/contracts/io.js +1 -1
- package/dist/safe-fs/core.d.ts +3 -0
- package/dist/safe-fs/core.js +2 -0
- package/dist/safe-fs/fs/devices/index.d.ts +2 -1
- package/dist/safe-fs/fs/devices/index.js +41 -0
- package/dist/safe-fs/fs/memory/index.d.ts +2 -2
- package/dist/safe-fs/fs/memory/index.js +89 -84
- package/dist/safe-fs/fs/memory/ledger.d.ts +0 -4
- package/dist/safe-fs/fs/memory/ledger.js +0 -15
- package/dist/safe-fs/fs/mount/identity.d.ts +4 -0
- package/dist/safe-fs/fs/mount/identity.js +8 -0
- package/dist/safe-fs/fs/mount/index.d.ts +1 -0
- package/dist/safe-fs/fs/mount/index.js +31 -0
- package/dist/safe-fs/fs/object-publication/index.d.ts +3 -0
- package/dist/safe-fs/fs/object-publication/index.js +3 -2
- package/dist/safe-fs/fs/quota/index.js +1 -1
- package/dist/safe-fs/fs/s3/http/transport.js +1 -1
- package/dist/safe-fs/fs/s3/http/xml.js +2 -2
- package/dist/safe-fs/fs/scoped.js +32 -2
- package/dist/safe-fs/index.d.ts +2 -0
- package/dist/safe-fs/index.js +2 -0
- package/dist/safe-fs/node/filesystem.d.ts +2 -1
- package/dist/safe-fs/node/filesystem.js +1 -5
- package/dist/safe-fs/runtime-core.d.ts +1 -0
- package/dist/safe-fs/runtime-core.js +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ rename nor inode/version identity from placeholder native metadata. Retained
|
|
|
35
35
|
reads remain available, with native seek reporting `ENOTSUP`. Ordinary browser
|
|
36
36
|
bundles cannot import this backend. Node keeps its POSIX host profile.
|
|
37
37
|
|
|
38
|
-
For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree.
|
|
38
|
+
For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Archive extraction requires atomic ancestry verification. MemoryFileSystem supports it, including a writable root composed with read-only mounts such as `/skills/.system`; the real adapter refuses it. Mount extraction delegates confinement to the root backend and keeps other mounts read-only in the extraction view. Extraction roots inside a separate mount are rejected because directory and metadata mutations cannot atomically guard ancestors on another backend. Staged file publication supports nested writable mounts when every ancestor provides authoritative validation. 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.
|
|
39
39
|
|
|
40
40
|
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).
|
|
41
41
|
|
|
@@ -148,7 +148,7 @@ Omit `maxOperations` or pass `Infinity` for unlimited cleanup operations.
|
|
|
148
148
|
| `createOverlayFileSystem({ upper, lower })` | Reading through to a lower layer and writing changes to an upper layer |
|
|
149
149
|
| `withFileSystemQuota(filesystem, { maxBytes })` | Enforcing a cumulative logical-byte ceiling across every write path |
|
|
150
150
|
|
|
151
|
-
`createNodeFsBridge` offers a promises-shaped subset, including recursive `cp` and `mkdtemp`. A trusted `reserveReadFile` callback can reserve host resources and return a release function; cancellation retains the reservation until the backend read settles. The portable `createFsBridge` from `@poe-platform/safe-fs/
|
|
151
|
+
`createNodeFsBridge` offers a promises-shaped subset, including recursive `cp` and `mkdtemp`. A trusted `reserveReadFile` callback can reserve host resources and return a release function; cancellation retains the reservation until the backend read settles. The portable `createFsBridge` from `@poe-platform/safe-fs/bridge` instead requires a caller-supplied text codec and returns `Uint8Array` values.
|
|
152
152
|
|
|
153
153
|
## Write an adapter
|
|
154
154
|
|
|
@@ -253,7 +253,7 @@ console.log(new TextDecoder().decode(await catalog.readFile("/welcome.txt")));
|
|
|
253
253
|
|
|
254
254
|
Output: `Hello from the catalog`. `readFile` returns a copy so callers cannot mutate the stored snapshot. Reads check cancellation and `maxBytes`; required mutations always reject with `EROFS`. `stat` and `lstat` share an implementation because this adapter has no symlinks.
|
|
255
255
|
|
|
256
|
-
`createFileSystem` validates options before calling the factory. The Node registry includes `memory` and `real`, accepts new names, and rejects attempts to replace those defaults. For an explicit allowlist without either default, pass `new Map([["catalog", catalogAdapter]])` directly as `registry`. Names select registered factories; they do not load modules from config.
|
|
256
|
+
`createFileSystem` validates options before calling the factory. For Workers, import `createPortableFileSystemAdapterRegistry` and `createFileSystem` from `@poe-code/safe-fs/core`; the portable registry includes `memory` and caller-supplied adapters, without ambient filesystem access. The Node registry includes `memory` and `real`, accepts new names, and rejects attempts to replace those defaults. For an explicit allowlist without either default, pass `new Map([["catalog", catalogAdapter]])` directly as `registry`. Names select registered factories; they do not load modules from config.
|
|
257
257
|
|
|
258
258
|
For a mutable backend, preserve exclusive-create behavior, return independent byte buffers, handle cancellation and size limits, and declare only capabilities you actually provide. Use `FsError` for filesystem failures. Do not invent entry identity from matching paths or metadata; omit `compareEntry` when identity is unknown.
|
|
259
259
|
|
|
@@ -357,8 +357,8 @@ Each batch lists from the beginning because previous keys have been deleted; it
|
|
|
357
357
|
| `addressingStyle` | `path`; alternative `virtual-hosted` requires a DNS endpoint |
|
|
358
358
|
| `listUrlEncoding` | `percent`; alternative `form` |
|
|
359
359
|
| `allowInsecureHttp` | `false`; HTTPS required unless explicitly enabled |
|
|
360
|
-
| `maxPutBytes`, `maxGetBytes` | Unlimited
|
|
361
|
-
| `maxXmlBytes` | Unlimited
|
|
360
|
+
| `maxPutBytes`, `maxGetBytes` | Unlimited when omitted or `Infinity`; positive safe integers set byte caps |
|
|
361
|
+
| `maxXmlBytes` | Unlimited when omitted or `Infinity`; positive safe integers set a byte cap |
|
|
362
362
|
| `requestTimeoutMs` | Unlimited unless configured |
|
|
363
363
|
| `enableCopy` | `true`; disabling uses a buffered GET/PUT fallback |
|
|
364
364
|
| `verifiedConditionalOperations` | Optional `put`, `copy`, `delete` booleans, each defaulting to false; enable only after verifying the server's semantics |
|
|
@@ -12,6 +12,7 @@ type DirectoryOptions = {
|
|
|
12
12
|
};
|
|
13
13
|
export declare class FileSystemBridge<Binary extends Uint8Array> {
|
|
14
14
|
#private;
|
|
15
|
+
static getProvider(bridge: object): FileSystem | undefined;
|
|
15
16
|
constructor(fs: FileSystem, options: {
|
|
16
17
|
readonly cwd?: string;
|
|
17
18
|
readonly root?: string;
|
|
@@ -4,3 +4,4 @@ import type { FsBridgeOptions } from "./types.js";
|
|
|
4
4
|
export type { FsBridgeCodec, FsBridgeDirent, FsBridgeEncoding, FsBridgeFileSystem, FsBridgeOptions, FsBridgeStats } from "./types.js";
|
|
5
5
|
export type FsBridge = FileSystemBridge<Uint8Array<ArrayBuffer>>;
|
|
6
6
|
export declare function createFsBridge(fs: FileSystem, options: FsBridgeOptions): FsBridge;
|
|
7
|
+
export declare const getFsBridgeProvider: typeof FileSystemBridge.getProvider;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function extendAdapterRegistry(defaults, extensions) {
|
|
2
|
+
if (extensions !== undefined &&
|
|
3
|
+
(extensions === null ||
|
|
4
|
+
typeof extensions.get !== "function" ||
|
|
5
|
+
typeof extensions[Symbol.iterator] !== "function")) {
|
|
6
|
+
throw new TypeError("registry must be a filesystem adapter map.");
|
|
7
|
+
}
|
|
8
|
+
const registry = new Map(defaults);
|
|
9
|
+
for (const [name, descriptor] of extensions ?? []) {
|
|
10
|
+
if (registry.has(name))
|
|
11
|
+
throw new TypeError(`Filesystem adapter already registered: ${name}`);
|
|
12
|
+
registry.set(name, descriptor);
|
|
13
|
+
}
|
|
14
|
+
return registry;
|
|
15
|
+
}
|
|
@@ -1,22 +1,11 @@
|
|
|
1
|
+
import { extendAdapterRegistry } from "./config/registry.js";
|
|
1
2
|
import { createMemoryFileSystemAdapter } from "./config/memory.js";
|
|
2
3
|
import { createRealFileSystemAdapter } from "./config/real.js";
|
|
3
4
|
import { createMemoryFileSystem } from "./fs/memory/index.js";
|
|
4
5
|
import { createRealFileSystem } from "./fs/real/index.js";
|
|
5
6
|
export function createNodeFileSystemAdapterRegistry(extensions) {
|
|
6
|
-
|
|
7
|
-
(extensions === null ||
|
|
8
|
-
typeof extensions.get !== "function" ||
|
|
9
|
-
typeof extensions[Symbol.iterator] !== "function")) {
|
|
10
|
-
throw new TypeError("registry must be a filesystem adapter map.");
|
|
11
|
-
}
|
|
12
|
-
const registry = new Map([
|
|
7
|
+
return extendAdapterRegistry(new Map([
|
|
13
8
|
["memory", createMemoryFileSystemAdapter(createMemoryFileSystem)],
|
|
14
9
|
["real", createRealFileSystemAdapter(createRealFileSystem)]
|
|
15
|
-
]);
|
|
16
|
-
for (const [name, descriptor] of extensions ?? []) {
|
|
17
|
-
if (registry.has(name))
|
|
18
|
-
throw new TypeError(`Filesystem adapter already registered: ${name}`);
|
|
19
|
-
registry.set(name, descriptor);
|
|
20
|
-
}
|
|
21
|
-
return registry;
|
|
10
|
+
]), extensions);
|
|
22
11
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createMemoryFileSystemAdapter } from "./config/memory.js";
|
|
2
|
+
import { extendAdapterRegistry } from "./config/registry.js";
|
|
3
|
+
import { createMemoryFileSystem } from "./fs/memory/index.js";
|
|
4
|
+
/** Portable defaults never grant access to the host filesystem. */
|
|
5
|
+
export function createPortableFileSystemAdapterRegistry(extensions) {
|
|
6
|
+
return extendAdapterRegistry(new Map([
|
|
7
|
+
["memory", createMemoryFileSystemAdapter(createMemoryFileSystem)]
|
|
8
|
+
]), extensions);
|
|
9
|
+
}
|
|
@@ -284,6 +284,17 @@ export interface PublishStagedFileOptions extends FsOptions {
|
|
|
284
284
|
readonly parent: FileStat;
|
|
285
285
|
readonly destination: FileStat | null;
|
|
286
286
|
}
|
|
287
|
+
/** Publish one staged regular file and validate/retire sibling source entries in
|
|
288
|
+
* the same indivisible commit. Every expected version (including absence) must
|
|
289
|
+
* match before any mutation. Hosts must not emulate this with sequential calls.
|
|
290
|
+
* Unavailable hosts omit publishStagedFileSet. Sources remain caller-owned. */
|
|
291
|
+
export interface PublishStagedFileSetOptions extends PublishStagedFileOptions {
|
|
292
|
+
readonly companions: readonly {
|
|
293
|
+
readonly path: string;
|
|
294
|
+
readonly expected: FileStat | null;
|
|
295
|
+
readonly remove: boolean;
|
|
296
|
+
}[];
|
|
297
|
+
}
|
|
287
298
|
export interface PrepareDirectoryOptions extends FsOptions {
|
|
288
299
|
readonly expected: FileStat | null;
|
|
289
300
|
readonly parent: FileStat;
|
|
@@ -321,6 +332,7 @@ export interface FileSystem {
|
|
|
321
332
|
prepareDirectory?(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
322
333
|
createStagedFile?(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
323
334
|
publishStagedFile?(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
335
|
+
publishStagedFileSet?(staging: FileStaging, destination: string, options: PublishStagedFileSetOptions): Promise<FileStat>;
|
|
324
336
|
/** Admit a complete canonical root-to-directory prefix and prepare a guard.
|
|
325
337
|
* The returned guard checks current identities/search access synchronously;
|
|
326
338
|
* preparation is not validation, and the guard's result is not an async lease. */
|
|
@@ -211,7 +211,7 @@ class ReadBytesGenerator {
|
|
|
211
211
|
if (!this.finished && this.iterator?.return) {
|
|
212
212
|
this.finished = true;
|
|
213
213
|
const it = this.iterator;
|
|
214
|
-
const cleanup = Promise
|
|
214
|
+
const cleanup = new Promise(resolve => { resolve(it.return()); });
|
|
215
215
|
if (signal?.aborted)
|
|
216
216
|
void cleanup.catch(noop);
|
|
217
217
|
else
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -14,3 +14,6 @@ export * from "./contracts/object.js";
|
|
|
14
14
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
15
15
|
export * from "./fs/s3/index.js";
|
|
16
16
|
export * from "./fs/s3/http/index.js";
|
|
17
|
+
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
18
|
+
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
|
19
|
+
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -13,3 +13,5 @@ export * from "./contracts/object.js";
|
|
|
13
13
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
14
14
|
export * from "./fs/s3/index.js";
|
|
15
15
|
export * from "./fs/s3/http/index.js";
|
|
16
|
+
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
17
|
+
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
2
|
-
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
3
3
|
import type { ByteSource } from "../../contracts/io.js";
|
|
4
4
|
import { type EntryLocation } from "../mount/comparison.js";
|
|
5
5
|
import { pathNamespace } from "../path-namespace.js";
|
|
@@ -40,6 +40,7 @@ export declare class DeviceFileSystem implements FileSystem {
|
|
|
40
40
|
removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
|
|
41
41
|
createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
42
42
|
publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
43
|
+
publishStagedFileSet(receipt: FileStaging, destination: string, supplied: PublishStagedFileSetOptions): Promise<FileStat>;
|
|
43
44
|
prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
|
|
44
45
|
prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
|
|
45
46
|
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
@@ -639,6 +639,47 @@ export class DeviceFileSystem {
|
|
|
639
639
|
await requireOwnedMutation(this.#filesystem, destination, "guardedStagingPublication", options, options.destination === null);
|
|
640
640
|
await this.#filesystem.publishStagedFile(staging, destination, options);
|
|
641
641
|
}
|
|
642
|
+
async publishStagedFileSet(receipt, destination, supplied) {
|
|
643
|
+
const publish = this.#filesystem.publishStagedFileSet;
|
|
644
|
+
if (!publish)
|
|
645
|
+
throw new FsError("ENOTSUP", { path: destination });
|
|
646
|
+
const entry = (value) => ({ path: value.path, stat: { ...value.stat } });
|
|
647
|
+
const staging = { ...receipt, parent: entry(receipt.parent), directory: entry(receipt.directory), file: entry(receipt.file) };
|
|
648
|
+
const options = {
|
|
649
|
+
...supplied,
|
|
650
|
+
parent: { ...supplied.parent },
|
|
651
|
+
destination: supplied.destination === null ? null : { ...supplied.destination },
|
|
652
|
+
companions: supplied.companions.map(value => ({ ...value, expected: value.expected === null ? null : { ...value.expected } })),
|
|
653
|
+
...(supplied.ancestors === undefined ? {} : { ancestors: snapshotDirectoryAncestry(supplied.ancestors) }),
|
|
654
|
+
};
|
|
655
|
+
options.signal?.throwIfAborted();
|
|
656
|
+
const capabilities = await this.#filesystem.capabilitiesFor?.(destination, { ...options, create: options.destination === null }) ?? this.#filesystem.capabilities;
|
|
657
|
+
if (capabilities.readOnly === true)
|
|
658
|
+
throw new FsError("EROFS", { path: destination });
|
|
659
|
+
for (const path of [staging.parent.path, staging.directory.path])
|
|
660
|
+
await this.#mutable(path, options, false);
|
|
661
|
+
const guards = [];
|
|
662
|
+
if (options.ancestors)
|
|
663
|
+
guards.push(await this.prepareDirectoryAncestry(options.ancestors, options));
|
|
664
|
+
for (const path of [staging.file.path, destination, ...options.companions.map(value => value.path)]) {
|
|
665
|
+
await this.#mutable(path, options, false);
|
|
666
|
+
const resolution = await this.prepareStagingResolution(path, options);
|
|
667
|
+
guards.push(resolution.validate);
|
|
668
|
+
}
|
|
669
|
+
const callerGuard = options.commitGuard;
|
|
670
|
+
options.signal?.throwIfAborted();
|
|
671
|
+
return publish.call(this.#filesystem, staging, destination, { ...options, commitGuard: () => {
|
|
672
|
+
options.signal?.throwIfAborted();
|
|
673
|
+
if (callerGuard !== undefined)
|
|
674
|
+
runStagingGuard(callerGuard);
|
|
675
|
+
// Recheck every route in the backend's atomic commit, so a source cannot
|
|
676
|
+
// cross into the virtual device namespace after asynchronous admission.
|
|
677
|
+
for (const guard of guards)
|
|
678
|
+
runStagingGuard(guard);
|
|
679
|
+
options.signal?.throwIfAborted();
|
|
680
|
+
return true;
|
|
681
|
+
} });
|
|
682
|
+
}
|
|
642
683
|
async prepareDirectoryAncestry(ancestors, options = {}) {
|
|
643
684
|
options.signal?.throwIfAborted();
|
|
644
685
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
import { type MemoryAtomicView } from "./atomic-view.js";
|
|
4
4
|
import { MemoryAllocation, MemoryLedger } from "./ledger.js";
|
|
@@ -35,7 +35,6 @@ interface SymlinkNode extends Metadata {
|
|
|
35
35
|
}
|
|
36
36
|
type MemoryNode = FileNode | DirectoryNode | SymlinkNode;
|
|
37
37
|
declare class MemoryCache {
|
|
38
|
-
readonly allocations: MemoryAllocation[];
|
|
39
38
|
readonly files: FileNode[];
|
|
40
39
|
readonly directories: DirectoryNode[];
|
|
41
40
|
readonly removedScratch: MemoryNode[];
|
|
@@ -118,6 +117,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
118
117
|
prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
|
|
119
118
|
private verifyDirectoryAncestry;
|
|
120
119
|
publishStagedFile(receipt: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
|
|
120
|
+
publishStagedFileSet(receipt: FileStaging, destination: string, options: PublishStagedFileSetOptions): Promise<FileStat>;
|
|
121
121
|
removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
|
|
122
122
|
private removeStagingLocations;
|
|
123
123
|
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
@@ -324,13 +324,6 @@ class FastDirectoryEntriesMap {
|
|
|
324
324
|
}
|
|
325
325
|
const SHARED_POOL_CAPACITY = 256;
|
|
326
326
|
const SHARED_DIR_POOL_CAPACITY = 64;
|
|
327
|
-
const sharedAllocationPool = new Array(SHARED_POOL_CAPACITY).fill(DUMMY_POOL_ALLOCATION);
|
|
328
|
-
let sharedAllocationPoolLen = 0;
|
|
329
|
-
const SHARED_LARGE_POOL_CAPACITY = 8;
|
|
330
|
-
const sharedLargeAllocationPool = new Array(SHARED_LARGE_POOL_CAPACITY).fill(DUMMY_POOL_ALLOCATION);
|
|
331
|
-
let sharedLargeAllocationPoolLen = 0;
|
|
332
|
-
const sharedMediumAllocationPool = new Array(SHARED_LARGE_POOL_CAPACITY).fill(DUMMY_POOL_ALLOCATION);
|
|
333
|
-
let sharedMediumAllocationPoolLen = 0;
|
|
334
327
|
const sharedFileNodePool = new Array(SHARED_POOL_CAPACITY).fill(DUMMY_POOL_FILE_NODE);
|
|
335
328
|
let sharedFileNodePoolLen = 0;
|
|
336
329
|
const sharedDirectoryNodePool = new Array(SHARED_DIR_POOL_CAPACITY);
|
|
@@ -351,27 +344,9 @@ function collectRemovedChild(child, name) {
|
|
|
351
344
|
sharedRemovedKeyEntries++;
|
|
352
345
|
}
|
|
353
346
|
function replenishSharedMemoryPools() {
|
|
354
|
-
while (sharedAllocationPoolLen < 112) {
|
|
355
|
-
DUMMY_POOL_LEDGER.reserve(64, 0, "init", "/");
|
|
356
|
-
const alloc = new MemoryAllocation(new Uint8Array(64), DUMMY_POOL_LEDGER);
|
|
357
|
-
alloc.release();
|
|
358
|
-
sharedAllocationPool[sharedAllocationPoolLen++] = alloc;
|
|
359
|
-
}
|
|
360
347
|
while (sharedFileNodePoolLen < 112) {
|
|
361
348
|
sharedFileNodePool[sharedFileNodePoolLen++] = new MemoryFileNode(0, 0, 0, 0, DUMMY_POOL_ALLOCATION, undefined);
|
|
362
349
|
}
|
|
363
|
-
while (sharedLargeAllocationPoolLen < 4) {
|
|
364
|
-
DUMMY_POOL_LEDGER.reserve(65536, 0, "init", "/");
|
|
365
|
-
const alloc = new MemoryAllocation(new Uint8Array(65536), DUMMY_POOL_LEDGER);
|
|
366
|
-
alloc.release();
|
|
367
|
-
sharedLargeAllocationPool[sharedLargeAllocationPoolLen++] = alloc;
|
|
368
|
-
}
|
|
369
|
-
while (sharedMediumAllocationPoolLen < 4) {
|
|
370
|
-
DUMMY_POOL_LEDGER.reserve(16384, 0, "init", "/");
|
|
371
|
-
const alloc = new MemoryAllocation(new Uint8Array(16384), DUMMY_POOL_LEDGER);
|
|
372
|
-
alloc.release();
|
|
373
|
-
sharedMediumAllocationPool[sharedMediumAllocationPoolLen++] = alloc;
|
|
374
|
-
}
|
|
375
350
|
while (sharedDirectoryNodePoolLen < 16) {
|
|
376
351
|
sharedDirectoryNodePool[sharedDirectoryNodePoolLen++] = new MemoryDirectoryNode(0, 0, 0);
|
|
377
352
|
}
|
|
@@ -403,7 +378,6 @@ Object.assign(MemoryDirectoryNode.prototype, {
|
|
|
403
378
|
const DUMMY_POOL_DIR_NODE = new MemoryDirectoryNode(0, 0, null);
|
|
404
379
|
DUMMY_POOL_DIR_NODE.atimeMs = DUMMY_POOL_DIR_NODE.mtimeMs = DUMMY_POOL_DIR_NODE.ctimeMs = DUMMY_POOL_DIR_NODE.birthtimeMs = 1700000000000;
|
|
405
380
|
class MemoryCache {
|
|
406
|
-
allocations = [];
|
|
407
381
|
files = [];
|
|
408
382
|
directories = [];
|
|
409
383
|
removedScratch = [];
|
|
@@ -448,7 +422,7 @@ const compareOwnedMemory = async (own, peer, options) => {
|
|
|
448
422
|
let answer = "unknown";
|
|
449
423
|
const visited = new Set();
|
|
450
424
|
for (const [left, right] of [[own, peer], [peer, own]]) {
|
|
451
|
-
if (
|
|
425
|
+
if (!registeredAuthorities.has(left.filesystem) || visited.has(left.filesystem))
|
|
452
426
|
continue;
|
|
453
427
|
visited.add(left.filesystem);
|
|
454
428
|
const comparison = left.filesystem.compareEntry;
|
|
@@ -911,21 +885,6 @@ export class MemoryFileSystem {
|
|
|
911
885
|
node.sourceRef = undefined;
|
|
912
886
|
node.allocation = DUMMY_POOL_ALLOCATION;
|
|
913
887
|
alloc.release();
|
|
914
|
-
if (alloc.isReleased64()) {
|
|
915
|
-
alloc.detachLedger(DUMMY_POOL_LEDGER);
|
|
916
|
-
if (this.ledger.hasInfiniteRetained && this.ledger.hasInfiniteFileBytes && sharedAllocationPoolLen < SHARED_POOL_CAPACITY) {
|
|
917
|
-
sharedAllocationPool[sharedAllocationPoolLen++] = alloc;
|
|
918
|
-
}
|
|
919
|
-
else if (cache.allocations.length < 128) {
|
|
920
|
-
cache.allocations.push(alloc);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
else if (alloc.isReleased65536()) {
|
|
924
|
-
alloc.detachLedger(DUMMY_POOL_LEDGER);
|
|
925
|
-
if (this.ledger.hasInfiniteRetained && this.ledger.hasInfiniteFileBytes && sharedLargeAllocationPoolLen < SHARED_LARGE_POOL_CAPACITY) {
|
|
926
|
-
sharedLargeAllocationPool[sharedLargeAllocationPoolLen++] = alloc;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
888
|
if (sharedFileNodePoolLen < SHARED_POOL_CAPACITY) {
|
|
930
889
|
sharedFileNodePool[sharedFileNodePoolLen++] = node;
|
|
931
890
|
}
|
|
@@ -965,18 +924,6 @@ export class MemoryFileSystem {
|
|
|
965
924
|
node.view = length === allocation.data.byteLength ? allocation.data : undefined;
|
|
966
925
|
node.allocation = allocation;
|
|
967
926
|
previous.release();
|
|
968
|
-
if (previous.isReleased64()) {
|
|
969
|
-
previous.detachLedger(DUMMY_POOL_LEDGER);
|
|
970
|
-
if (this.ledger.hasInfiniteRetained && this.ledger.hasInfiniteFileBytes && sharedAllocationPoolLen < SHARED_POOL_CAPACITY) {
|
|
971
|
-
sharedAllocationPool[sharedAllocationPoolLen++] = previous;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
else if (previous.isReleased65536()) {
|
|
975
|
-
previous.detachLedger(DUMMY_POOL_LEDGER);
|
|
976
|
-
if (this.ledger.hasInfiniteRetained && this.ledger.hasInfiniteFileBytes && sharedLargeAllocationPoolLen < SHARED_LARGE_POOL_CAPACITY) {
|
|
977
|
-
sharedLargeAllocationPool[sharedLargeAllocationPoolLen++] = previous;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
927
|
}
|
|
981
928
|
admitSize(node, length, syscall, path) {
|
|
982
929
|
this.ledger.fileSize(length, syscall, path);
|
|
@@ -1354,36 +1301,7 @@ export class MemoryFileSystem {
|
|
|
1354
1301
|
if (length === 0) {
|
|
1355
1302
|
return DUMMY_POOL_ALLOCATION;
|
|
1356
1303
|
}
|
|
1357
|
-
|
|
1358
|
-
if (this.ledger.hasInfiniteRetained && this.ledger.hasInfiniteFileBytes && sharedMediumAllocationPoolLen > 0) {
|
|
1359
|
-
const pooled = sharedMediumAllocationPool[--sharedMediumAllocationPoolLen];
|
|
1360
|
-
sharedMediumAllocationPool[sharedMediumAllocationPoolLen] = DUMMY_POOL_ALLOCATION;
|
|
1361
|
-
pooled.reuse(this.ledger);
|
|
1362
|
-
return pooled;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
if (length === 65536) {
|
|
1366
|
-
if (this.ledger.hasInfiniteRetained && this.ledger.hasInfiniteFileBytes && sharedLargeAllocationPoolLen > 0) {
|
|
1367
|
-
const pooled = sharedLargeAllocationPool[--sharedLargeAllocationPoolLen];
|
|
1368
|
-
sharedLargeAllocationPool[sharedLargeAllocationPoolLen] = DUMMY_POOL_ALLOCATION;
|
|
1369
|
-
pooled.reuse(this.ledger);
|
|
1370
|
-
return pooled;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
if (length === 64) {
|
|
1374
|
-
let pooled;
|
|
1375
|
-
if (this.ledger.hasInfiniteRetained && this.ledger.hasInfiniteFileBytes && sharedAllocationPoolLen > 0) {
|
|
1376
|
-
pooled = sharedAllocationPool[--sharedAllocationPoolLen];
|
|
1377
|
-
sharedAllocationPool[sharedAllocationPoolLen] = DUMMY_POOL_ALLOCATION;
|
|
1378
|
-
}
|
|
1379
|
-
else {
|
|
1380
|
-
pooled = memoryCaches.get(this.ledger).allocations.pop();
|
|
1381
|
-
}
|
|
1382
|
-
if (pooled) {
|
|
1383
|
-
pooled.reuse(this.ledger);
|
|
1384
|
-
return pooled;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1304
|
+
// Views can outlive files: never recycle their backing buffers across owners.
|
|
1387
1305
|
try {
|
|
1388
1306
|
return new MemoryAllocation(new Uint8Array(length), this.ledger);
|
|
1389
1307
|
}
|
|
@@ -2329,6 +2247,93 @@ export class MemoryFileSystem {
|
|
|
2329
2247
|
this.fail("EAGAIN", "publishStagedFile", destination);
|
|
2330
2248
|
return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
|
|
2331
2249
|
}
|
|
2250
|
+
async publishStagedFileSet(receipt, destination, options) {
|
|
2251
|
+
const signal = options.signal;
|
|
2252
|
+
const guard = options.commitGuard;
|
|
2253
|
+
const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
|
|
2254
|
+
const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
|
|
2255
|
+
const staging = { parent: snapshot(receipt.parent), directory: snapshot(receipt.directory), file: snapshot(receipt.file) };
|
|
2256
|
+
const parent = { ...options.parent };
|
|
2257
|
+
const expected = options.destination === null ? null : { ...options.destination };
|
|
2258
|
+
const companions = options.companions.map(entry => ({ path: entry.path, expected: entry.expected === null ? null : { ...entry.expected }, remove: entry.remove }));
|
|
2259
|
+
const syscall = "publishStagedFileSet";
|
|
2260
|
+
const parentPath = destination.slice(0, destination.lastIndexOf("/")) || "/";
|
|
2261
|
+
const paths = new Set([destination]);
|
|
2262
|
+
const canonical = (path) => path.startsWith("/") && path.slice(1).split("/").every(part => part !== "" && part !== "." && part !== "..");
|
|
2263
|
+
if (!canonical(destination) || ancestors && ancestors.at(-1)?.path !== parentPath)
|
|
2264
|
+
this.fail("EINVAL", syscall, destination);
|
|
2265
|
+
for (const entry of companions) {
|
|
2266
|
+
if (!canonical(entry.path) || paths.has(entry.path) || (entry.path.slice(0, entry.path.lastIndexOf("/")) || "/") !== parentPath)
|
|
2267
|
+
this.fail("EINVAL", syscall, entry.path);
|
|
2268
|
+
paths.add(entry.path);
|
|
2269
|
+
}
|
|
2270
|
+
signal?.throwIfAborted();
|
|
2271
|
+
if (guard !== undefined)
|
|
2272
|
+
runStagingGuard(guard);
|
|
2273
|
+
signal?.throwIfAborted();
|
|
2274
|
+
if (ancestors)
|
|
2275
|
+
this.verifyDirectoryAncestry(ancestors, signal === undefined ? {} : { signal });
|
|
2276
|
+
const { directory, file } = this.stagingLocations(staging);
|
|
2277
|
+
this.expectEntry(file.node, staging.file.stat, staging.file.path);
|
|
2278
|
+
const target = this.entry(destination, syscall, true);
|
|
2279
|
+
this.expectEntry(target.parent, parent, destination, false);
|
|
2280
|
+
this.expectEntry(target.node, expected, destination);
|
|
2281
|
+
const node = file.node;
|
|
2282
|
+
if (node.type !== "file" || target.parent === directory.node || target.node === node)
|
|
2283
|
+
this.fail("EINVAL", syscall, destination);
|
|
2284
|
+
if (target.node && target.node.type !== "file")
|
|
2285
|
+
this.fail("EAGAIN", syscall, destination);
|
|
2286
|
+
this.permission(file.parent, 3, syscall, staging.file.path);
|
|
2287
|
+
this.permission(target.parent, 3, syscall, destination);
|
|
2288
|
+
const retired = [];
|
|
2289
|
+
for (const companion of companions) {
|
|
2290
|
+
const location = this.entry(companion.path, syscall, true);
|
|
2291
|
+
if (location.parent !== target.parent || location.node === node)
|
|
2292
|
+
this.fail("EINVAL", syscall, companion.path);
|
|
2293
|
+
this.expectEntry(location.node, companion.expected, companion.path);
|
|
2294
|
+
if (location.node && location.node.type !== "file")
|
|
2295
|
+
this.fail("EAGAIN", syscall, companion.path);
|
|
2296
|
+
if (companion.remove && location.node)
|
|
2297
|
+
retired.push(location);
|
|
2298
|
+
}
|
|
2299
|
+
const now = Date.now();
|
|
2300
|
+
const published = { ...this.snapshot(node), ctimeMs: now, revision: Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1) };
|
|
2301
|
+
// Admission and all source-set checks precede the single synchronous commit.
|
|
2302
|
+
// No calls to overridable async mutations can interleave a partial result.
|
|
2303
|
+
const nameGrowth = target.node ? 0 : (target.name.length - file.name.length) * 2;
|
|
2304
|
+
this.ledger.reserve(Math.max(0, nameGrowth), 0, syscall, destination);
|
|
2305
|
+
try {
|
|
2306
|
+
target.parent.entries.set(target.name, node);
|
|
2307
|
+
}
|
|
2308
|
+
catch (error) {
|
|
2309
|
+
this.ledger.release(Math.max(0, nameGrowth), 0);
|
|
2310
|
+
throw error;
|
|
2311
|
+
}
|
|
2312
|
+
memoryCaches.get(this.ledger).clearWrites();
|
|
2313
|
+
file.parent.entries.delete(file.name);
|
|
2314
|
+
const unlink = (entry) => {
|
|
2315
|
+
entry.node.nlink--;
|
|
2316
|
+
entry.node.ctimeMs = now;
|
|
2317
|
+
entry.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, entry.node.revision + 1);
|
|
2318
|
+
this.releaseNode(entry.node);
|
|
2319
|
+
};
|
|
2320
|
+
if (target.node) {
|
|
2321
|
+
this.ledger.release(file.name.length * 2, 1);
|
|
2322
|
+
unlink(target);
|
|
2323
|
+
}
|
|
2324
|
+
else if (nameGrowth < 0)
|
|
2325
|
+
this.ledger.release(-nameGrowth, 0);
|
|
2326
|
+
for (const entry of retired) {
|
|
2327
|
+
entry.parent.entries.delete(entry.name);
|
|
2328
|
+
this.ledger.release(entry.name.length * 2, 1);
|
|
2329
|
+
unlink(entry);
|
|
2330
|
+
}
|
|
2331
|
+
this.changed(file.parent, now);
|
|
2332
|
+
this.changed(target.parent, now);
|
|
2333
|
+
node.ctimeMs = now;
|
|
2334
|
+
node.revision = published.revision;
|
|
2335
|
+
return published;
|
|
2336
|
+
}
|
|
2332
2337
|
async removeStagedFile(staging, options = {}) {
|
|
2333
2338
|
options.signal?.throwIfAborted();
|
|
2334
2339
|
const { directory, file } = this.stagingLocations(staging);
|
|
@@ -23,8 +23,4 @@ export declare class MemoryAllocation {
|
|
|
23
23
|
constructor(data: Uint8Array, ledger: MemoryLedger);
|
|
24
24
|
retain(): void;
|
|
25
25
|
release(): void;
|
|
26
|
-
isReleased64(): boolean;
|
|
27
|
-
isReleased65536(): boolean;
|
|
28
|
-
detachLedger(dummyLedger: MemoryLedger): void;
|
|
29
|
-
reuse(ledger?: MemoryLedger): void;
|
|
30
26
|
}
|
|
@@ -89,21 +89,6 @@ export class MemoryAllocation {
|
|
|
89
89
|
this.ledger.release(this.data.byteLength, 0);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
isReleased64() {
|
|
93
|
-
return this.references === 0 && this.data.byteLength === 64;
|
|
94
|
-
}
|
|
95
|
-
isReleased65536() {
|
|
96
|
-
return this.references === 0 && this.data.byteLength === 65536;
|
|
97
|
-
}
|
|
98
|
-
detachLedger(dummyLedger) {
|
|
99
|
-
this.ledger = dummyLedger;
|
|
100
|
-
}
|
|
101
|
-
reuse(ledger) {
|
|
102
|
-
this.references = 1;
|
|
103
|
-
if (ledger !== undefined)
|
|
104
|
-
this.ledger = ledger;
|
|
105
|
-
this.data.fill(0);
|
|
106
|
-
}
|
|
107
92
|
}
|
|
108
93
|
Object.assign(MemoryAllocation.prototype, {
|
|
109
94
|
references: 1,
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { FileStat } from "../../contracts/filesystem.js";
|
|
2
2
|
export declare function compareIdentity(left: FileStat | undefined, right: FileStat | undefined): "same" | "distinct" | "unknown";
|
|
3
|
+
/** Compare content snapshots, including adapter generations when supplied.
|
|
4
|
+
* Identity is checked separately so identityless streaming adapters remain usable.
|
|
5
|
+
*/
|
|
6
|
+
export declare function compareFileVersion(left: FileStat, right: FileStat): boolean;
|
|
@@ -19,3 +19,11 @@ export function compareIdentity(left, right) {
|
|
|
19
19
|
return "distinct";
|
|
20
20
|
return "unknown";
|
|
21
21
|
}
|
|
22
|
+
/** Compare content snapshots, including adapter generations when supplied.
|
|
23
|
+
* Identity is checked separately so identityless streaming adapters remain usable.
|
|
24
|
+
*/
|
|
25
|
+
export function compareFileVersion(left, right) {
|
|
26
|
+
return left.type === right.type && left.size === right.size
|
|
27
|
+
&& left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs
|
|
28
|
+
&& left.revision === right.revision && left.opaqueVersion === right.opaqueVersion;
|
|
29
|
+
}
|
|
@@ -12,6 +12,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
12
12
|
private namespaceMutation;
|
|
13
13
|
private readonly namespaceWaiters;
|
|
14
14
|
constructor(options: MountFileSystemOptions);
|
|
15
|
+
confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
|
|
15
16
|
private select;
|
|
16
17
|
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
17
18
|
private supportsStagingAncestry;
|
|
@@ -9,6 +9,7 @@ import { pathNamespace } from "../path-namespace.js";
|
|
|
9
9
|
import { createStagingCleanup, snapshotStagingCreation } from "../staging-cleanup.js";
|
|
10
10
|
import { directoryAncestryPaths, inspectStagingBindings, runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "../staging-ancestry.js";
|
|
11
11
|
import { compareIdentity } from "./identity.js";
|
|
12
|
+
import { createReadOnlyFileSystem } from "../readonly/index.js";
|
|
12
13
|
import { snapshotConditionalChmod } from "../conditional-chmod.js";
|
|
13
14
|
import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
|
|
14
15
|
const syntheticStat = Object.freeze({
|
|
@@ -150,6 +151,36 @@ export class MountFileSystem {
|
|
|
150
151
|
...(retainedRead === undefined ? {} : { retainedRead }),
|
|
151
152
|
});
|
|
152
153
|
}
|
|
154
|
+
async confineExtraction(roots, options = {}) {
|
|
155
|
+
options.signal?.throwIfAborted();
|
|
156
|
+
const root = this.select("/").backend;
|
|
157
|
+
const captured = [...roots];
|
|
158
|
+
for (const path of captured) {
|
|
159
|
+
validatePath(path);
|
|
160
|
+
if (normalizePath(globalPath(path)) !== path)
|
|
161
|
+
fail("EINVAL");
|
|
162
|
+
// A backing confinement retains its own root-to-destination ancestry.
|
|
163
|
+
// It cannot retain directories belonging to another mounted backend.
|
|
164
|
+
if (this.select(path).path !== "/")
|
|
165
|
+
fail("ENOTSUP");
|
|
166
|
+
}
|
|
167
|
+
if (!root.confineExtraction)
|
|
168
|
+
fail("ENOTSUP");
|
|
169
|
+
const confined = await root.confineExtraction(captured, options);
|
|
170
|
+
options.signal?.throwIfAborted();
|
|
171
|
+
const observations = new Set(["prepareDirectoryAncestry", "prepareStagingResolution"]);
|
|
172
|
+
const backing = new Proxy(confined, {
|
|
173
|
+
get(target, key) {
|
|
174
|
+
// These only capture/validate bindings. Mutations must always use the
|
|
175
|
+
// backend's confined view, including creation, metadata and hardlinks.
|
|
176
|
+
const owner = observations.has(key) ? root : target;
|
|
177
|
+
const value = Reflect.get(owner, key);
|
|
178
|
+
return typeof value === "function" ? value.bind(owner) : value;
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
return new MountFileSystem({ root: backing, mounts: Object.fromEntries(this.mounts.filter(mount => mount.path !== "/").map(mount => [mount.path,
|
|
182
|
+
mount.backend.capabilities.readOnly === true ? mount.backend : createReadOnlyFileSystem(mount.backend)])) });
|
|
183
|
+
}
|
|
153
184
|
select(path) {
|
|
154
185
|
return this.mounts.find((mount) => within(mount.path, path));
|
|
155
186
|
}
|
|
@@ -15,6 +15,7 @@ export interface ObjectFilePublicationOptions extends FsOptions {
|
|
|
15
15
|
readonly mode: number;
|
|
16
16
|
}
|
|
17
17
|
export interface ObjectFileAcquireOptions extends FsOptions {
|
|
18
|
+
readonly noFollow?: boolean;
|
|
18
19
|
readonly access: OpenFileOptions["access"];
|
|
19
20
|
}
|
|
20
21
|
export interface ObjectFileStaging {
|
|
@@ -28,6 +29,8 @@ export interface ObjectFileStagingOptions extends FsOptions {
|
|
|
28
29
|
readonly maxFileBytes: number;
|
|
29
30
|
}
|
|
30
31
|
export interface ObjectFilePublicationStore {
|
|
32
|
+
/** Certifies acquire can reject symbolic links when noFollow is requested. */
|
|
33
|
+
readonly noFollow?: boolean;
|
|
31
34
|
acquire(path: string, options: ObjectFileAcquireOptions): Promise<ObjectFileVersion | undefined>;
|
|
32
35
|
publish?(path: string, expectedRevision: string | null, source: ByteSource, options: ObjectFilePublicationOptions): Promise<ObjectFileVersion>;
|
|
33
36
|
createStaging?(path: string, options: ObjectFileStagingOptions): Promise<ObjectFileStaging>;
|
|
@@ -11,6 +11,7 @@ export function withObjectFileDescriptors(filesystem, store, options = {}) {
|
|
|
11
11
|
const maxOpenFiles = options.maxOpenFiles ?? Infinity;
|
|
12
12
|
if (!Number.isSafeInteger(chunkBytes) || chunkBytes < 1
|
|
13
13
|
|| ![maxStagedBytes, maxStagedPages, maxFileBytes, maxOpenFiles].every(value => value === Infinity || Number.isSafeInteger(value) && value > 0)
|
|
14
|
+
|| store.noFollow !== undefined && typeof store.noFollow !== "boolean"
|
|
14
15
|
|| typeof store.acquire !== "function"
|
|
15
16
|
|| store.publish !== undefined && typeof store.publish !== "function"
|
|
16
17
|
|| store.createStaging !== undefined && typeof store.createStaging !== "function")
|
|
@@ -68,7 +69,7 @@ export function withObjectFileDescriptors(filesystem, store, options = {}) {
|
|
|
68
69
|
state.pages.clear();
|
|
69
70
|
};
|
|
70
71
|
const open = (path, openOptions) => openFileDescriptor(path, openOptions, {
|
|
71
|
-
publication: "conditional", position: true, positionedRead: true, positionedWrite: true,
|
|
72
|
+
publication: "conditional", position: true, positionedRead: true, positionedWrite: true, noFollow: store.noFollow === true,
|
|
72
73
|
truncate: true, synchronization: "storage",
|
|
73
74
|
}, async (admitted) => {
|
|
74
75
|
validatePath(path);
|
|
@@ -233,7 +234,7 @@ export function withObjectFileDescriptors(filesystem, store, options = {}) {
|
|
|
233
234
|
throw new FsError("ENOTSUP", { path, message: "Object writes require authoritative conditional publication" });
|
|
234
235
|
if (admitted.access !== "write" && capabilities.read === false)
|
|
235
236
|
throw new FsError("EACCES", { path });
|
|
236
|
-
const acquired = await store.acquire(path, { access: admitted.access, ...(admitted.signal ? { signal: admitted.signal } : {}) });
|
|
237
|
+
const acquired = await store.acquire(path, { access: admitted.access, ...(admitted.noFollow ? { noFollow: true } : {}), ...(admitted.signal ? { signal: admitted.signal } : {}) });
|
|
237
238
|
if (acquired) {
|
|
238
239
|
try {
|
|
239
240
|
state.head = version(acquired);
|
|
@@ -499,7 +499,7 @@ export function withFileSystemQuota(fs, options) {
|
|
|
499
499
|
// capabilities and methods without violating invariants on own properties.
|
|
500
500
|
return new Proxy(Object.create(fs), {
|
|
501
501
|
get(_target, property) {
|
|
502
|
-
if (property === "prepareStagingResolution" || property === "prepareDirectoryAncestry" || property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
|
|
502
|
+
if (property === "publishStagedFileSet" || property === "prepareStagingResolution" || property === "prepareDirectoryAncestry" || property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
|
|
503
503
|
return undefined;
|
|
504
504
|
if (property === "capabilities")
|
|
505
505
|
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }) });
|
|
@@ -22,7 +22,7 @@ function utf8ByteLength(value) {
|
|
|
22
22
|
return bytes;
|
|
23
23
|
}
|
|
24
24
|
function limit(value, fallback, name, maximum = Number.MAX_SAFE_INTEGER) {
|
|
25
|
-
if (value === undefined)
|
|
25
|
+
if (value === undefined || value === Infinity && fallback === Infinity)
|
|
26
26
|
return fallback;
|
|
27
27
|
const result = value;
|
|
28
28
|
if (!Number.isSafeInteger(result) || result < 1 || result > maximum)
|
|
@@ -37,8 +37,8 @@ function decodeEntities(text) {
|
|
|
37
37
|
}
|
|
38
38
|
export function parseXml(bytes, limits = {}) {
|
|
39
39
|
for (const limit of [limits.maxNodes, limits.maxDepth]) {
|
|
40
|
-
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1))
|
|
41
|
-
throw new RangeError("XML limits must be positive safe integers");
|
|
40
|
+
if (limit !== undefined && limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 1))
|
|
41
|
+
throw new RangeError("XML limits must be positive safe integers or Infinity");
|
|
42
42
|
}
|
|
43
43
|
let xml;
|
|
44
44
|
try {
|
|
@@ -18,7 +18,7 @@ export function retargetScopedFileSystem(scoped, charge, signal, cleanupCharge =
|
|
|
18
18
|
return true;
|
|
19
19
|
}
|
|
20
20
|
const operations = new Set([
|
|
21
|
-
"prepareDirectoryAncestry", "prepareStagingResolution",
|
|
21
|
+
"prepareDirectoryAncestry", "prepareStagingResolution", "publishStagedFileSet",
|
|
22
22
|
"publishFileConditional", "removeEntryConditional", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
|
|
23
23
|
"confineExtraction", "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
|
|
24
24
|
"copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir",
|
|
@@ -262,7 +262,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
262
262
|
if (property === "removeEntryConditional")
|
|
263
263
|
args[1] = resizeOptions({ ...args[1] });
|
|
264
264
|
let stagingSignal;
|
|
265
|
-
if (property === "createStagedFile" || property === "publishStagedFile") {
|
|
265
|
+
if (property === "createStagedFile" || property === "publishStagedFile" || property === "publishStagedFileSet") {
|
|
266
266
|
signal.throwIfAborted();
|
|
267
267
|
if (property === "createStagedFile")
|
|
268
268
|
args[3] = snapshotStagingCreation(args[3], args[0]);
|
|
@@ -281,6 +281,36 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
281
281
|
if (property === "symlink" && typeof args[1] === "string")
|
|
282
282
|
validatePath(args[1], maxPathComponents);
|
|
283
283
|
}
|
|
284
|
+
if (property === "publishStagedFileSet") {
|
|
285
|
+
if (original.capabilities.readOnly === true)
|
|
286
|
+
throw new FsError("EROFS", { path: args[1] });
|
|
287
|
+
const supplied = args[2];
|
|
288
|
+
const callerGuard = supplied.commitGuard;
|
|
289
|
+
const entry = (value) => ({ path: value.path, stat: { ...value.stat } });
|
|
290
|
+
const input = args[0];
|
|
291
|
+
const staging = { ...input, parent: entry(input.parent), directory: entry(input.directory), file: entry(input.file) };
|
|
292
|
+
const controls = {
|
|
293
|
+
...supplied,
|
|
294
|
+
signal: stagingSignal,
|
|
295
|
+
parent: { ...supplied.parent },
|
|
296
|
+
destination: supplied.destination === null ? null : { ...supplied.destination },
|
|
297
|
+
companions: supplied.companions.map(value => ({ ...value, expected: value.expected === null ? null : { ...value.expected } })),
|
|
298
|
+
...(supplied.ancestors === undefined ? {} : { ancestors: snapshotDirectoryAncestry(supplied.ancestors) }),
|
|
299
|
+
commitGuard: () => {
|
|
300
|
+
assertOpen({ signal: stagingSignal });
|
|
301
|
+
if (callerGuard !== undefined)
|
|
302
|
+
runStagingGuard(callerGuard);
|
|
303
|
+
assertOpen({ signal: stagingSignal });
|
|
304
|
+
return true;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
for (const path of [staging.parent.path, staging.directory.path, staging.file.path, args[1],
|
|
308
|
+
...controls.companions.map(value => value.path), ...(controls.ancestors ?? []).map(value => value.path)]) {
|
|
309
|
+
validatePath(path, maxPathComponents);
|
|
310
|
+
}
|
|
311
|
+
assertOpen(controls);
|
|
312
|
+
return Reflect.apply(method, original, [staging, args[1], controls]);
|
|
313
|
+
}
|
|
284
314
|
if (["writeFile", "appendFile", "writeStream", "mkdir"].includes(String(property))) {
|
|
285
315
|
const index = property === "mkdir" ? 1 : 2;
|
|
286
316
|
args[index] = creationOptions((args[index] ?? {}), property === "mkdir");
|
package/dist/safe-fs/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./contracts/index.js";
|
|
2
|
+
export * from "./bridge/index.js";
|
|
2
3
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
3
4
|
export * from "./python/index.js";
|
|
4
5
|
export { openFileDescriptor } from "./fs/descriptor.js";
|
|
@@ -21,3 +22,4 @@ export * from "./node/index.js";
|
|
|
21
22
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
22
23
|
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
|
23
24
|
export { createNodeFileSystemAdapterRegistry } from "./config.node.js";
|
|
25
|
+
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
package/dist/safe-fs/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./contracts/index.js";
|
|
2
|
+
export * from "./bridge/index.js";
|
|
2
3
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
3
4
|
export * from "./python/index.js";
|
|
4
5
|
export { openFileDescriptor } from "./fs/descriptor.js";
|
|
@@ -17,3 +18,4 @@ export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
|
17
18
|
export * from "./node/index.js";
|
|
18
19
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
19
20
|
export { createNodeFileSystemAdapterRegistry } from "./config.node.js";
|
|
21
|
+
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as FsPromises from "node:fs/promises";
|
|
2
2
|
import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
|
|
3
|
+
import { FileSystemBridge } from "../bridge/filesystem.js";
|
|
3
4
|
export type NodeFsImplementation = Pick<typeof FsPromises, "access" | "appendFile" | "chmod" | "copyFile" | "cp" | "link" | "lstat" | "mkdir" | "mkdtemp" | "readFile" | "readdir" | "readlink" | "realpath" | "unlink" | "rename" | "rm" | "rmdir" | "stat" | "symlink" | "truncate" | "utimes" | "writeFile">;
|
|
4
5
|
export interface NodeFsBridgeOptions {
|
|
5
6
|
readonly cwd?: string;
|
|
@@ -14,5 +15,5 @@ export interface NodeFsBridgeOptions {
|
|
|
14
15
|
export interface NodeFsBridgeFileSystem extends FileSystem {
|
|
15
16
|
rmdir?(path: string, options?: FsOptions): Promise<void>;
|
|
16
17
|
}
|
|
17
|
-
export declare
|
|
18
|
+
export declare const getNodeFsBridgeProvider: typeof FileSystemBridge.getProvider;
|
|
18
19
|
export declare function createNodeFsBridge(fs: FileSystem, options?: NodeFsBridgeOptions): NodeFsImplementation;
|
|
@@ -3,10 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { posix } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { FileSystemBridge } from "../bridge/filesystem.js";
|
|
6
|
-
const
|
|
7
|
-
export function getNodeFsBridgeProvider(bridge) {
|
|
8
|
-
return providers.get(bridge);
|
|
9
|
-
}
|
|
6
|
+
export const getNodeFsBridgeProvider = FileSystemBridge.getProvider;
|
|
10
7
|
export function createNodeFsBridge(fs, options = {}) {
|
|
11
8
|
const bridge = new FileSystemBridge(fs, options, {
|
|
12
9
|
codec: {
|
|
@@ -29,6 +26,5 @@ export function createNodeFsBridge(fs, options = {}) {
|
|
|
29
26
|
randomSuffix() { return randomBytes(6).toString("hex").slice(0, 6); },
|
|
30
27
|
paths: posix
|
|
31
28
|
});
|
|
32
|
-
providers.set(bridge, fs);
|
|
33
29
|
return bridge;
|
|
34
30
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./contracts/errors.js";
|
|
2
2
|
export * from "./contracts/filesystem.js";
|
|
3
3
|
export * from "./contracts/io.js";
|
|
4
|
+
export { compareIdentity, compareFileVersion } from "./fs/mount/identity.js";
|
|
4
5
|
export { openFileDescriptor } from "./fs/descriptor.js";
|
|
5
6
|
export type { DescriptorBackend, DescriptorOpenOptions } from "./fs/descriptor.js";
|
|
6
7
|
export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./contracts/errors.js";
|
|
2
2
|
export * from "./contracts/filesystem.js";
|
|
3
3
|
export * from "./contracts/io.js";
|
|
4
|
+
export { compareIdentity, compareFileVersion } from "./fs/mount/identity.js";
|
|
4
5
|
export { openFileDescriptor } from "./fs/descriptor.js";
|
|
5
6
|
export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
|
|
6
7
|
export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poe-platform/safe-fs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.807",
|
|
4
4
|
"description": "Composable filesystem with a portable core and explicit Node adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -130,6 +130,10 @@
|
|
|
130
130
|
"./runtime-core": {
|
|
131
131
|
"types": "./dist/safe-fs/runtime-core.d.ts",
|
|
132
132
|
"import": "./dist/safe-fs/runtime-core.js"
|
|
133
|
+
},
|
|
134
|
+
"./bridge": {
|
|
135
|
+
"types": "./dist/safe-fs/bridge/index.d.ts",
|
|
136
|
+
"import": "./dist/safe-fs/bridge/index.js"
|
|
133
137
|
}
|
|
134
138
|
},
|
|
135
139
|
"repository": {
|