@openclaw/openshell-sandbox 2026.9.6 → 2026.9.8
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/assets/icon.png +0 -0
- package/dist/index.js +75 -229
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +102 -0
- package/node_modules/@openclaw/fs-safe/README.md +24 -6
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-plan.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +9 -18
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-directory.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.d.ts +11 -11
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.js +3 -35
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.js +2 -12
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.d.ts +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +13 -10
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-manifest.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js +16 -8
- package/node_modules/@openclaw/fs-safe/dist/archive.d.ts +6 -3
- package/node_modules/@openclaw/fs-safe/dist/archive.js +22 -19
- package/node_modules/@openclaw/fs-safe/dist/atomic.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.js +2 -6
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.js +71 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutations.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +20 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +77 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +5 -7
- package/node_modules/@openclaw/fs-safe/dist/effective-uid.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-acquisition.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-options.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/file-store-prune.js +7 -6
- package/node_modules/@openclaw/fs-safe/dist/file-store.d.ts +4 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +19 -21
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +23 -31
- package/node_modules/@openclaw/fs-safe/dist/guest.js +21 -12
- package/node_modules/@openclaw/fs-safe/dist/json-document-store.d.ts +4 -9
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/local-file-access.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-roots.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/move-path-cleanup.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +51 -14
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +303 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +64 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +17 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions.d.ts +3 -10
- package/node_modules/@openclaw/fs-safe/dist/permissions.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +2 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +36 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +4 -10
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +66 -38
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +7 -11
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +21 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +123 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.js +47 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +16 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file.d.ts +3 -55
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +31 -11
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.d.ts +50 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.js +121 -0
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +28 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +9 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +88 -37
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.js +12 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +8 -5
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-observation.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/root-paths.d.ts +2 -6
- package/node_modules/@openclaw/fs-safe/dist/root-remove-identity.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +19 -12
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +56 -20
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.d.ts +6 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -16
- package/node_modules/@openclaw/fs-safe/dist/secret-read-policy.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +5 -14
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission-parser.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +11 -12
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-stale-admission.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +9 -7
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.d.ts +39 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/stat-observation.js +3 -10
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/symlink-parents.js +58 -7
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +28 -33
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +16 -17
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +52 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +5 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +27 -1
- package/node_modules/@openclaw/fs-safe/dist/walk.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +88 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +93 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +36 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +73 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +28 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +300 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +60 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +530 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +6 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +78 -3
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +66 -15
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +6 -2
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -2
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +93 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +102 -4
- package/node_modules/@openclaw/fs-safe/docs/copy.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +7 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +5 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/index.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +30 -0
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +20 -4
- package/node_modules/@openclaw/fs-safe/docs/native.md +63 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +66 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +30 -3
- package/node_modules/@openclaw/fs-safe/docs/retained-file.md +115 -0
- package/node_modules/@openclaw/fs-safe/docs/root.md +35 -9
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +19 -0
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/staged-symlink.md +124 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/temp.md +24 -4
- package/node_modules/@openclaw/fs-safe/docs/testing.md +210 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +6 -0
- package/node_modules/@openclaw/fs-safe/docs/walk.md +34 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +251 -0
- package/node_modules/@openclaw/fs-safe/docs/writing.md +21 -0
- package/node_modules/@openclaw/fs-safe/package.json +13 -9
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/node_modules/p-limit/index.d.ts +2 -0
- package/node_modules/p-limit/index.js +17 -1
- package/node_modules/p-limit/package.json +1 -1
- package/node_modules/p-limit/readme.md +2 -0
- package/package.json +6 -6
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.d.ts +0 -20
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.js +0 -78
|
@@ -7,7 +7,7 @@ function helperUnavailable(message, cause) {
|
|
|
7
7
|
...(cause instanceof Error ? { cause } : {}),
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
|
-
function copyOperationalCode(target, source) {
|
|
10
|
+
export function copyOperationalCode(target, source) {
|
|
11
11
|
const code = source?.code;
|
|
12
12
|
if (typeof code === "string") {
|
|
13
13
|
Object.defineProperty(target, "code", { configurable: true, value: code });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StagedFileReceipt } from "./staged-file-types.js";
|
|
2
|
+
/** Exact producer observations: never supply rounded Number identities. */
|
|
3
|
+
export type RetainedFileExpected = Readonly<Pick<StagedFileReceipt["identity"], "dev" | "ino" | "size" | "mtimeNs" | "ctimeNs"> & {
|
|
4
|
+
/** Lowercase SHA-256 of the producer's expected bytes. */
|
|
5
|
+
sha256: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type RetainedFileIssue = Readonly<{
|
|
8
|
+
phase: string;
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
}>;
|
|
13
|
+
/** Descriptive facts, not a deletion capability or a durable transaction receipt. */
|
|
14
|
+
export type RetainedFileResult = Readonly<{
|
|
15
|
+
status: "unsupported" | "not-attempted" | "preserved-mismatch" | "disposition-accepted" | "name-absent-after-settlement" | "failed" | "indeterminate";
|
|
16
|
+
phase: string;
|
|
17
|
+
/** Full native 64-bit volume serial and 128-bit file ID, when observed. */
|
|
18
|
+
identity?: string;
|
|
19
|
+
disposition: "not-attempted" | "accepted" | "rejected" | "indeterminate";
|
|
20
|
+
namespace: "not-observed" | "absent" | "original" | "foreign" | "unknown";
|
|
21
|
+
resources: "closed" | "close-failed";
|
|
22
|
+
persistence: "not-proven";
|
|
23
|
+
errors: readonly RetainedFileIssue[];
|
|
24
|
+
}>;
|
|
25
|
+
export type RetainedFileReceipt = Readonly<{
|
|
26
|
+
directory: string;
|
|
27
|
+
parent: Readonly<{
|
|
28
|
+
dev: bigint;
|
|
29
|
+
ino: bigint;
|
|
30
|
+
}>;
|
|
31
|
+
basename: string;
|
|
32
|
+
expected: RetainedFileExpected;
|
|
33
|
+
identity: string;
|
|
34
|
+
}>;
|
|
35
|
+
export interface RetainedFile extends Disposable {
|
|
36
|
+
readonly receipt: RetainedFileReceipt;
|
|
37
|
+
/** One-shot, synchronous authority admission; always settles owned resources. */
|
|
38
|
+
remove(): RetainedFileResult;
|
|
39
|
+
/** Closes only. Never requests deletion, including after an exception. */
|
|
40
|
+
dispose(): RetainedFileResult;
|
|
41
|
+
}
|
|
42
|
+
export type RetainedFileAdmission = Readonly<{
|
|
43
|
+
status: "retained";
|
|
44
|
+
file: RetainedFile;
|
|
45
|
+
}> | RetainedFileResult;
|
|
46
|
+
export type RetainFileInDirectoryOptions = Readonly<Pick<RetainedFileReceipt, "directory" | "parent" | "basename" | "expected"> & {
|
|
47
|
+
assertBeforeMutation: () => void;
|
|
48
|
+
/** Synchronous verification budget, default 16 MiB; maximum 64 MiB. */
|
|
49
|
+
maxBytes?: number;
|
|
50
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RetainedFileAdmission, RetainFileInDirectoryOptions } from "./retained-file-types.js";
|
|
2
|
+
/** Retain one existing regular file on supported local Windows NTFS. No fallback. */
|
|
3
|
+
export declare function retainFileInDirectory(options: RetainFileInDirectoryOptions): RetainedFileAdmission;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { getNativeBinding } from "./native.js";
|
|
3
|
+
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
4
|
+
function freeze(result) {
|
|
5
|
+
return Object.freeze({ ...result, errors: Object.freeze(result.errors.map((error) => Object.freeze({ ...error }))) });
|
|
6
|
+
}
|
|
7
|
+
function unsupported(cause) {
|
|
8
|
+
return freeze({ status: "unsupported", phase: "admission", disposition: "not-attempted",
|
|
9
|
+
namespace: "not-observed", resources: "closed", persistence: "not-proven",
|
|
10
|
+
errors: [{ phase: "admission", code: "helper-unavailable",
|
|
11
|
+
message: "existing-file retention requires the maintained Windows native capability", cause }] });
|
|
12
|
+
}
|
|
13
|
+
function exact(value, name, zero = false) {
|
|
14
|
+
if (typeof value !== "bigint" || value < (zero ? 0n : 1n) || value > 0xffffffffffffffffn) {
|
|
15
|
+
throw new TypeError(`${name} must be an exact ${zero ? "nonnegative" : "positive"} unsigned 64-bit bigint`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
class Owner {
|
|
20
|
+
receipt;
|
|
21
|
+
#native;
|
|
22
|
+
#assert;
|
|
23
|
+
#result;
|
|
24
|
+
#running = false;
|
|
25
|
+
#reentered = false;
|
|
26
|
+
constructor(native, receipt, assertion) {
|
|
27
|
+
this.#native = native;
|
|
28
|
+
this.receipt = receipt;
|
|
29
|
+
this.#assert = assertion;
|
|
30
|
+
Object.freeze(this);
|
|
31
|
+
}
|
|
32
|
+
#settle(remove) {
|
|
33
|
+
if (this.#result)
|
|
34
|
+
return this.#result;
|
|
35
|
+
if (this.#running) {
|
|
36
|
+
this.#reentered = true;
|
|
37
|
+
throw new TypeError("retained-file settlement cannot be reentered");
|
|
38
|
+
}
|
|
39
|
+
this.#running = true;
|
|
40
|
+
let rejection;
|
|
41
|
+
if (remove) {
|
|
42
|
+
try {
|
|
43
|
+
assertSynchronousCallbackResult(this.#assert(), "assertBeforeMutation");
|
|
44
|
+
if (this.#reentered)
|
|
45
|
+
throw new TypeError("retained-file authority reentered settlement");
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
rejection = { cause };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const result = this.#native.settle(remove && !rejection);
|
|
53
|
+
this.#result = freeze(rejection ? { ...result, phase: "authority", errors: [
|
|
54
|
+
{ phase: "authority", code: "denied-path", message: "current mutation authority rejected removal", cause: rejection.cause },
|
|
55
|
+
...result.errors,
|
|
56
|
+
] } : result);
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
// An unexpected binding exception cannot establish native effect or close.
|
|
60
|
+
// Do not retry or release a dependency on an unknown native outcome.
|
|
61
|
+
this.#result = freeze({ status: "indeterminate", phase: "binding", identity: this.receipt.identity,
|
|
62
|
+
disposition: remove && !rejection ? "indeterminate" : "not-attempted", namespace: "unknown",
|
|
63
|
+
resources: "close-failed", persistence: "not-proven", errors: [
|
|
64
|
+
...(rejection ? [{ phase: "authority", code: "denied-path", message: "current mutation authority rejected removal", cause: rejection.cause }] : []),
|
|
65
|
+
{ phase: "binding", code: "helper-failed", message: "native settlement did not return a result", cause },
|
|
66
|
+
] });
|
|
67
|
+
}
|
|
68
|
+
return this.#result;
|
|
69
|
+
}
|
|
70
|
+
remove() { return this.#settle(true); }
|
|
71
|
+
dispose() { return this.#settle(false); }
|
|
72
|
+
[Symbol.dispose]() {
|
|
73
|
+
const result = this.dispose();
|
|
74
|
+
if (result.resources !== "closed") {
|
|
75
|
+
throw new FsSafeError("helper-failed", "retained-file resource settlement is uncertain", { details: { result } });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Retain one existing regular file on supported local Windows NTFS. No fallback. */
|
|
80
|
+
export function retainFileInDirectory(options) {
|
|
81
|
+
// Snapshot once before any native resource admission or caller authority runs.
|
|
82
|
+
const directory = options.directory;
|
|
83
|
+
const basename = options.basename;
|
|
84
|
+
const sourceParent = options.parent;
|
|
85
|
+
const parent = Object.freeze({ dev: exact(sourceParent.dev, "parent.dev"), ino: exact(sourceParent.ino, "parent.ino") });
|
|
86
|
+
const source = options.expected;
|
|
87
|
+
const expected = Object.freeze({ dev: exact(source.dev, "expected.dev"), ino: exact(source.ino, "expected.ino"),
|
|
88
|
+
size: exact(source.size, "expected.size", true), mtimeNs: exact(source.mtimeNs, "expected.mtimeNs", true),
|
|
89
|
+
ctimeNs: exact(source.ctimeNs, "expected.ctimeNs", true), sha256: source.sha256 });
|
|
90
|
+
const assertion = options.assertBeforeMutation;
|
|
91
|
+
const maxBytes = options.maxBytes ?? 16 * 1024 * 1024;
|
|
92
|
+
if (typeof directory !== "string" || typeof basename !== "string" || typeof assertion !== "function"
|
|
93
|
+
|| typeof expected.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(expected.sha256)
|
|
94
|
+
|| !Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > 64 * 1024 * 1024 || expected.size > BigInt(maxBytes)) {
|
|
95
|
+
throw new TypeError("invalid retained-file directory, authority or bounded expected-byte contract");
|
|
96
|
+
}
|
|
97
|
+
if (process.platform !== "win32")
|
|
98
|
+
return unsupported();
|
|
99
|
+
let binding;
|
|
100
|
+
try {
|
|
101
|
+
binding = getNativeBinding();
|
|
102
|
+
}
|
|
103
|
+
catch (cause) {
|
|
104
|
+
return unsupported(cause);
|
|
105
|
+
}
|
|
106
|
+
if (!binding?.retainWindowsFile)
|
|
107
|
+
return unsupported();
|
|
108
|
+
let native;
|
|
109
|
+
try {
|
|
110
|
+
native = binding.retainWindowsFile(directory, basename, parent.dev, parent.ino, expected.dev, expected.ino, expected.size, expected.mtimeNs, expected.ctimeNs, expected.sha256, maxBytes);
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
// Factory errors have no returned owner; never infer absence or deletion.
|
|
114
|
+
return freeze({ ...unsupported(cause), status: "indeterminate", resources: "close-failed" });
|
|
115
|
+
}
|
|
116
|
+
const admission = native.admission;
|
|
117
|
+
if (admission.status !== "retained")
|
|
118
|
+
return freeze(admission);
|
|
119
|
+
const receipt = Object.freeze({ directory, parent, basename, expected, identity: admission.identity });
|
|
120
|
+
return Object.freeze({ status: "retained", file: new Owner(native, receipt, assertion) });
|
|
121
|
+
}
|
|
@@ -107,7 +107,7 @@ export function rootRelativeReadPath(root, filePath) {
|
|
|
107
107
|
resolveCandidateRoot: base === root.rootDir && root.rootDir !== root.rootReal,
|
|
108
108
|
});
|
|
109
109
|
if (admitted)
|
|
110
|
-
return admitted.relativePath
|
|
110
|
+
return `.${path.sep}${admitted.relativePath}`;
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
113
|
const prefix = ensureTrailingSep(base);
|
|
@@ -116,7 +116,8 @@ export function rootRelativeReadPath(root, filePath) {
|
|
|
116
116
|
let start = prefix.length;
|
|
117
117
|
while (raw[start] === path.sep)
|
|
118
118
|
start += 1;
|
|
119
|
-
|
|
119
|
+
// An admitted absolute tail stays literal, including a leading home marker.
|
|
120
|
+
return `.${path.sep}${raw.slice(start)}`;
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return raw;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RootContext } from "./root-context.js";
|
|
2
|
+
import { type RootDirectoryObservationGuard } from "./root-directory-list.js";
|
|
3
|
+
import { type ExactStatIdentity } from "./stat-observation.js";
|
|
4
|
+
import type { DirEntry } from "./types.js";
|
|
5
|
+
/** One literal entry lookup under an operation-local admitted parent; never follow the leaf. */
|
|
6
|
+
export declare function lookupRootDirectoryEntry(root: RootContext, guard: RootDirectoryObservationGuard, name: string): Promise<{
|
|
7
|
+
entry: DirEntry;
|
|
8
|
+
identity: ExactStatIdentity;
|
|
9
|
+
} | undefined>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { isNotFoundPathError } from "./path.js";
|
|
5
|
+
import { assertValidRootRelativePath } from "./root-context.js";
|
|
6
|
+
import { assertRootDirectoryObservationGuard, pathStatFromStats } from "./root-directory-list.js";
|
|
7
|
+
import { inspectStatObservationSync } from "./stat-observation.js";
|
|
8
|
+
/** One literal entry lookup under an operation-local admitted parent; never follow the leaf. */
|
|
9
|
+
export async function lookupRootDirectoryEntry(root, guard, name) {
|
|
10
|
+
if (!name || name === "." || name === ".." || path.basename(name) !== name || path.isAbsolute(name)) {
|
|
11
|
+
throw new FsSafeError("invalid-path", "directory lookup requires one literal name");
|
|
12
|
+
}
|
|
13
|
+
assertValidRootRelativePath(name);
|
|
14
|
+
await assertRootDirectoryObservationGuard(root, guard);
|
|
15
|
+
try {
|
|
16
|
+
const pathname = path.join(guard.realPath, name);
|
|
17
|
+
const observed = inspectStatObservationSync(bigint => bigint
|
|
18
|
+
? fs.lstatSync(pathname, { bigint: true }) : fs.lstatSync(pathname));
|
|
19
|
+
await assertRootDirectoryObservationGuard(root, guard);
|
|
20
|
+
return { entry: { name, ...pathStatFromStats(observed.stat) }, identity: observed.identity };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
await assertRootDirectoryObservationGuard(root, guard);
|
|
24
|
+
if (isNotFoundPathError(error))
|
|
25
|
+
return undefined;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { BigIntStats, Stats } from "node:fs";
|
|
|
2
2
|
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import { type RootContext } from "./root-context.js";
|
|
4
4
|
import type { RootPathDirectoryObservationGuard, RootPathObservationReceipt, RootPathParentObservationReceipt } from "./root-path.js";
|
|
5
|
+
import { type ExactStatIdentity } from "./stat-observation.js";
|
|
5
6
|
import type { DirEntry, PathStat } from "./types.js";
|
|
6
7
|
export declare function pathStatFromStats(stat: Stats | BigIntStats): PathStat;
|
|
7
8
|
export type RootDirectoryObservationGuard = AsyncDirectoryGuard<BigIntStats>;
|
|
@@ -21,6 +22,7 @@ export type RootDirectoryListing = {
|
|
|
21
22
|
next(): Promise<{
|
|
22
23
|
kind: "entry";
|
|
23
24
|
entry: DirEntry;
|
|
25
|
+
identity?: ExactStatIdentity;
|
|
24
26
|
} | {
|
|
25
27
|
kind: "limit";
|
|
26
28
|
name: string;
|
|
@@ -33,6 +35,12 @@ export type RootDirectoryListingOptions = {
|
|
|
33
35
|
snapshot: boolean;
|
|
34
36
|
maxNames?: number;
|
|
35
37
|
metadataBatchSize?: number;
|
|
38
|
+
/** Internal exact metadata lane, supported by streaming filesystem order. */
|
|
39
|
+
exactIdentity?: boolean;
|
|
40
|
+
/** Advisory scans may omit vanished leaves after revalidating their parent. */
|
|
41
|
+
skipVanished?: boolean;
|
|
42
|
+
/** Internal owner receives cleanup failures, including acquisition rollback. */
|
|
43
|
+
onCleanupFailure?: (error: unknown) => void;
|
|
36
44
|
admitEntry(): boolean;
|
|
37
45
|
};
|
|
38
|
-
export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions): Promise<RootDirectoryListing>;
|
|
46
|
+
export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions, receipt?: RootPathObservationReceipt): Promise<RootDirectoryListing>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUtf8 } from "node:buffer";
|
|
1
2
|
import fsSync from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -11,8 +12,25 @@ import { errorCauseOptions, rootPathChangedError } from "./root-errors.js";
|
|
|
11
12
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
12
13
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
13
14
|
import { realpathSync } from "./realpath.js";
|
|
15
|
+
import { inspectStatObservationSync } from "./stat-observation.js";
|
|
14
16
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
15
17
|
const METADATA_BATCH_SIZE = 32;
|
|
18
|
+
function directoryEntryName(bytes) {
|
|
19
|
+
if (!isUtf8(bytes)) {
|
|
20
|
+
throw new FsSafeError("invalid-path", "directory entry name is not valid UTF-8");
|
|
21
|
+
}
|
|
22
|
+
return bytes.toString("utf8");
|
|
23
|
+
}
|
|
24
|
+
async function readDirectoryEntryName(handle) {
|
|
25
|
+
const entry = await handle.read();
|
|
26
|
+
// Bun returns the raw Buffer directly; Node returns a Dirent whose name is a Buffer.
|
|
27
|
+
// Node's Dir types still declare only string names.
|
|
28
|
+
return entry === null ? undefined
|
|
29
|
+
: directoryEntryName(Buffer.isBuffer(entry) ? entry : entry.name);
|
|
30
|
+
}
|
|
31
|
+
function openDirectoryNames(directory) {
|
|
32
|
+
return fs.opendir(directory, { bufferSize: 1, encoding: "buffer" });
|
|
33
|
+
}
|
|
16
34
|
export function pathStatFromStats(stat) {
|
|
17
35
|
const mtimeMs = typeof stat.mtimeMs === "bigint"
|
|
18
36
|
? "mtimeNs" in stat && typeof stat.mtimeNs === "bigint"
|
|
@@ -173,14 +191,7 @@ function normalizeInitialDirectoryError(error) {
|
|
|
173
191
|
export async function listDirectoryPath(root, directory, withFileTypes, receipt) {
|
|
174
192
|
let guard;
|
|
175
193
|
if (receipt) {
|
|
176
|
-
|
|
177
|
-
receipt.directoryGuard.dir !== directory ||
|
|
178
|
-
receipt.target !== receipt.directoryGuard ||
|
|
179
|
-
(!isNativeDirectoryObservationGuard(receipt.directoryGuard) &&
|
|
180
|
-
!receipt.directoryGuard.stat.isDirectory())) {
|
|
181
|
-
throw new FsSafeError("path-mismatch", "directory observation receipt does not match target");
|
|
182
|
-
}
|
|
183
|
-
guard = receipt.directoryGuard;
|
|
194
|
+
guard = directoryGuardFromReceipt(directory, receipt);
|
|
184
195
|
}
|
|
185
196
|
else {
|
|
186
197
|
try {
|
|
@@ -192,13 +203,23 @@ export async function listDirectoryPath(root, directory, withFileTypes, receipt)
|
|
|
192
203
|
}
|
|
193
204
|
return await listGuardedDirectoryPath(root, guard, withFileTypes, receipt);
|
|
194
205
|
}
|
|
206
|
+
function directoryGuardFromReceipt(directory, receipt) {
|
|
207
|
+
if (receipt.kind !== "directory" || receipt.targetPath !== directory ||
|
|
208
|
+
receipt.directoryGuard.dir !== directory ||
|
|
209
|
+
receipt.target !== receipt.directoryGuard ||
|
|
210
|
+
(!isNativeDirectoryObservationGuard(receipt.directoryGuard) &&
|
|
211
|
+
!receipt.directoryGuard.stat.isDirectory())) {
|
|
212
|
+
throw new FsSafeError("path-mismatch", "directory observation receipt does not match target");
|
|
213
|
+
}
|
|
214
|
+
return receipt.directoryGuard;
|
|
215
|
+
}
|
|
195
216
|
async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
|
|
196
217
|
let entries;
|
|
197
218
|
try {
|
|
198
219
|
const beforeObservation = getFsSafeTestHooks()?.beforeRootListObservation;
|
|
199
220
|
if (beforeObservation)
|
|
200
221
|
await beforeObservation(guard.realPath, withFileTypes);
|
|
201
|
-
const names = (await fs.readdir(guard.realPath)).sort();
|
|
222
|
+
const names = (await fs.readdir(guard.realPath, { encoding: "buffer" })).map(directoryEntryName).sort();
|
|
202
223
|
entries = withFileTypes
|
|
203
224
|
? names.map(name => ({
|
|
204
225
|
name,
|
|
@@ -221,19 +242,30 @@ async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
|
|
|
221
242
|
await assertRootDirectoryObservationGuard(root, guard);
|
|
222
243
|
return entries;
|
|
223
244
|
}
|
|
224
|
-
export async function openRootDirectoryListing(root, directory, options) {
|
|
225
|
-
|
|
226
|
-
throw
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
245
|
+
export async function openRootDirectoryListing(root, directory, options, receipt) {
|
|
246
|
+
if (options.exactIdentity && options.order !== "filesystem")
|
|
247
|
+
throw new TypeError("exact entry identities require filesystem order");
|
|
248
|
+
let guard;
|
|
249
|
+
if (receipt) {
|
|
250
|
+
guard = directoryGuardFromReceipt(directory, receipt);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const admitted = await createRootDirectoryObservationGuard(root, directory).catch((error) => {
|
|
254
|
+
throw normalizeDirectoryError(error);
|
|
255
|
+
});
|
|
256
|
+
// Retain exact admission identities; metadata rechecks can use numeric Stats
|
|
257
|
+
// only when every identity component is losslessly representable.
|
|
258
|
+
guard = extendDirectoryObservationGuard({
|
|
259
|
+
stat: admitted.stat,
|
|
260
|
+
identity: { dev: admitted.stat.dev, ino: admitted.stat.ino },
|
|
261
|
+
}, admitted.dir, admitted.realPath);
|
|
262
|
+
}
|
|
234
263
|
const assertCurrent = async () => {
|
|
235
264
|
options.signal?.throwIfAborted();
|
|
236
|
-
|
|
265
|
+
if (receipt)
|
|
266
|
+
assertRootPathObservationReceiptCurrent(root, receipt);
|
|
267
|
+
else
|
|
268
|
+
await assertRootDirectoryObservationGuard(root, guard);
|
|
237
269
|
options.signal?.throwIfAborted();
|
|
238
270
|
};
|
|
239
271
|
let handle;
|
|
@@ -248,23 +280,29 @@ export async function openRootDirectoryListing(root, directory, options) {
|
|
|
248
280
|
const close = async () => {
|
|
249
281
|
const owned = handle;
|
|
250
282
|
handle = undefined;
|
|
251
|
-
|
|
283
|
+
try {
|
|
284
|
+
await owned?.close();
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
options.onCleanupFailure?.(error);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
252
290
|
};
|
|
253
291
|
try {
|
|
254
292
|
await assertCurrent();
|
|
255
293
|
if (options.order === "filesystem") {
|
|
256
294
|
// A one-entry buffer keeps the truncation lookahead independent of width.
|
|
257
|
-
handle = await
|
|
295
|
+
handle = await openDirectoryNames(guard.realPath);
|
|
258
296
|
}
|
|
259
297
|
else if (options.snapshot) {
|
|
260
|
-
snapshot = await listGuardedDirectoryPath(root, guard, true);
|
|
298
|
+
snapshot = await listGuardedDirectoryPath(root, guard, true, receipt);
|
|
261
299
|
}
|
|
262
300
|
else if (options.maxNames !== undefined) {
|
|
263
301
|
names = [];
|
|
264
|
-
handle = await
|
|
302
|
+
handle = await openDirectoryNames(guard.realPath);
|
|
265
303
|
while (true) {
|
|
266
304
|
await assertCurrent();
|
|
267
|
-
const name =
|
|
305
|
+
const name = await readDirectoryEntryName(handle);
|
|
268
306
|
await assertCurrent();
|
|
269
307
|
if (name === undefined)
|
|
270
308
|
break;
|
|
@@ -276,7 +314,7 @@ export async function openRootDirectoryListing(root, directory, options) {
|
|
|
276
314
|
await close();
|
|
277
315
|
}
|
|
278
316
|
else {
|
|
279
|
-
names = await fs.readdir(guard.realPath);
|
|
317
|
+
names = (await fs.readdir(guard.realPath, { encoding: "buffer" })).map(directoryEntryName);
|
|
280
318
|
}
|
|
281
319
|
await assertCurrent();
|
|
282
320
|
names?.sort();
|
|
@@ -361,17 +399,30 @@ export async function openRootDirectoryListing(root, directory, options) {
|
|
|
361
399
|
return { kind: "limit", name: pendingLimit };
|
|
362
400
|
return;
|
|
363
401
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
402
|
+
for (;;) {
|
|
403
|
+
await assertCurrent();
|
|
404
|
+
const name = await readDirectoryEntryName(handle);
|
|
405
|
+
await assertCurrent();
|
|
406
|
+
if (name === undefined)
|
|
407
|
+
return;
|
|
408
|
+
if (!options.admitEntry())
|
|
409
|
+
return { kind: "limit", name };
|
|
410
|
+
// The stream's post-read fence is also the pre-stat fence in this owned operation.
|
|
411
|
+
const pathname = path.join(guard.realPath, name);
|
|
412
|
+
let observed;
|
|
413
|
+
try {
|
|
414
|
+
observed = options.exactIdentity ? inspectStatObservationSync(bigint => bigint
|
|
415
|
+
? fsSync.lstatSync(pathname, { bigint: true }) : fsSync.lstatSync(pathname)) : { stat: fsSync.lstatSync(pathname) };
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
await assertCurrent();
|
|
419
|
+
if (options.skipVanished && isNotFoundPathError(error))
|
|
420
|
+
continue;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
await assertCurrent();
|
|
424
|
+
return { kind: "entry", entry: { name, ...pathStatFromStats(observed.stat) }, identity: observed.identity };
|
|
425
|
+
}
|
|
375
426
|
}
|
|
376
427
|
catch (error) {
|
|
377
428
|
throw normalizeDirectoryError(error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import type
|
|
2
|
+
import { type PinnedOpenSyncFinalAdmission } from "./pinned-open.js";
|
|
3
3
|
export type RootFileAdmissionFs = Pick<typeof fs, "lstatSync" | "realpathSync">;
|
|
4
4
|
type ExactIdentity = Readonly<{
|
|
5
5
|
dev: bigint;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { isExpectedPathError } from "./pinned-open.js";
|
|
3
4
|
import { realpathSync } from "./realpath.js";
|
|
4
5
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
5
6
|
import { hardlinkedPathNotAllowedError } from "./root-errors.js";
|
|
@@ -8,12 +9,6 @@ import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-pa
|
|
|
8
9
|
function rootIdentityMismatch(cause) {
|
|
9
10
|
return new FsSafeError("path-mismatch", "canonical root directory identity changed or could not be verified", cause === undefined ? {} : { cause });
|
|
10
11
|
}
|
|
11
|
-
function isExpectedPathError(error) {
|
|
12
|
-
const code = typeof error === "object" && error !== null && "code" in error
|
|
13
|
-
? String(error.code)
|
|
14
|
-
: "";
|
|
15
|
-
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
16
|
-
}
|
|
17
12
|
function inspectCanonicalRoot(ioFs, rootPath, expected) {
|
|
18
13
|
try {
|
|
19
14
|
assertNoWindowsPathAlias(rootPath);
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveRootPathSyncWithCanonicalRootObservation, resolveRootPathWithCanonicalRootObservation, } from "./root-path.js";
|
|
4
4
|
import { readSymlinkResolution } from "./root-symlink-policy.js";
|
|
5
|
-
import { openPinnedFileSync, } from "./pinned-open.js";
|
|
5
|
+
import { isExpectedPathError, openPinnedFileSync, } from "./pinned-open.js";
|
|
6
6
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
8
8
|
import { createRootFileFinalAdmission, observeCanonicalRoot, } from "./root-file-final-admission.js";
|
|
@@ -198,10 +198,7 @@ function snapshotAsyncResolution(params, absolutePath) {
|
|
|
198
198
|
function toRootObservationError(error) {
|
|
199
199
|
if (error instanceof FsSafeError)
|
|
200
200
|
return toBoundaryValidationError(error);
|
|
201
|
-
|
|
202
|
-
? String(error.code)
|
|
203
|
-
: "";
|
|
204
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") {
|
|
201
|
+
if (isExpectedPathError(error)) {
|
|
205
202
|
return { ok: false, reason: "path", error };
|
|
206
203
|
}
|
|
207
204
|
return { ok: false, reason: "io", error };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RootContext } from "./root-context.js";
|
|
2
|
+
/** Internal registration; never reconstruct authority from public pathname fields. */
|
|
3
|
+
export declare function registerRootHandleContext(handle: object, context: RootContext): void;
|
|
4
|
+
export declare function rootHandleContext(handle: object): RootContext;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
const contexts = new WeakMap();
|
|
3
|
+
/** Internal registration; never reconstruct authority from public pathname fields. */
|
|
4
|
+
export function registerRootHandleContext(handle, context) {
|
|
5
|
+
contexts.set(handle, context);
|
|
6
|
+
}
|
|
7
|
+
export function rootHandleContext(handle) {
|
|
8
|
+
const context = contexts.get(handle);
|
|
9
|
+
if (!context)
|
|
10
|
+
throw new FsSafeError("invalid-path", "watch requires a genuine fs-safe Root");
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
@@ -5,7 +5,7 @@ import { type ReadResult } from "./read-opened-file.js";
|
|
|
5
5
|
import { type RootEntriesOptions } from "./root-entries.js";
|
|
6
6
|
import { type RootContext } from "./root-context.js";
|
|
7
7
|
import type { DirEntry, PathStat } from "./types.js";
|
|
8
|
-
import { type RootWalkEntry, type RootWalkOptions } from "./root-walk.js";
|
|
8
|
+
import { type RootWalkEntry, type RootWalkOptions, type RootWalkSymlinkPolicy } from "./root-walk.js";
|
|
9
9
|
import { type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCreateJsonOptions, type RootCreateOptions, type RootCreateStreamOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions } from "./root-options.js";
|
|
10
10
|
export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
|
|
11
11
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
@@ -54,7 +54,7 @@ export interface Root {
|
|
|
54
54
|
}): Promise<DirEntry[]>;
|
|
55
55
|
entries(relativePath: string, options?: RootEntriesOptions): AsyncIterableIterator<DirEntry>;
|
|
56
56
|
move(fromRelative: string, toRelative: string, options?: RootMoveOptions): Promise<void>;
|
|
57
|
-
walk(relativePath: string, options: RootWalkOptions): AsyncIterableIterator<RootWalkEntry
|
|
57
|
+
walk<Policy extends RootWalkSymlinkPolicy>(relativePath: string, options: RootWalkOptions<Policy>): AsyncIterableIterator<RootWalkEntry<Policy>>;
|
|
58
58
|
}
|
|
59
59
|
export declare class RootHandle implements Root {
|
|
60
60
|
private readonly context;
|
|
@@ -97,7 +97,7 @@ export declare class RootHandle implements Root {
|
|
|
97
97
|
}): Promise<DirEntry[]>;
|
|
98
98
|
move(fromRelative: string, toRelative: string, options?: RootMoveOptions): Promise<void>;
|
|
99
99
|
entries(relativePath: string, options?: RootEntriesOptions): AsyncIterableIterator<DirEntry>;
|
|
100
|
-
walk(relativePath: string, options: RootWalkOptions): AsyncIterableIterator<RootWalkEntry
|
|
100
|
+
walk<Policy extends RootWalkSymlinkPolicy>(relativePath: string, options: RootWalkOptions<Policy>): AsyncIterableIterator<RootWalkEntry<Policy>>;
|
|
101
101
|
}
|
|
102
102
|
export declare function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
|
|
103
103
|
export declare function rootFromDirectoryGuard(guard: {
|
|
@@ -51,6 +51,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
import { randomUUID } from "node:crypto";
|
|
54
|
+
import { registerRootHandleContext } from "./root-handle-context.js";
|
|
54
55
|
import fsSync, { constants as fsConstants } from "node:fs";
|
|
55
56
|
import fs from "node:fs/promises";
|
|
56
57
|
import path from "node:path";
|
|
@@ -85,7 +86,7 @@ import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-li
|
|
|
85
86
|
import { statResolvedPathInRoot } from "./root-path-stat.js";
|
|
86
87
|
import { entriesInRoot } from "./root-entries.js";
|
|
87
88
|
import { assertMoveMutationAllowed } from "./root-move-preflight.js";
|
|
88
|
-
import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
89
|
+
import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, createRootObservationGuard, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
89
90
|
import { errorCauseOptions, fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
|
|
90
91
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
91
92
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
@@ -199,6 +200,7 @@ export class RootHandle {
|
|
|
199
200
|
this.rootWithSep = context.rootWithSep;
|
|
200
201
|
this.defaults = defaults;
|
|
201
202
|
registerFileLockSyncRootAdapter(this, context, defaults);
|
|
203
|
+
registerRootHandleContext(this, context);
|
|
202
204
|
}
|
|
203
205
|
mutationOptions(options) {
|
|
204
206
|
return {
|
|
@@ -409,9 +411,13 @@ export class RootHandle {
|
|
|
409
411
|
assertValidRootRelativePath(relativePath);
|
|
410
412
|
return walkRoot({
|
|
411
413
|
rootReal: this.context.rootReal,
|
|
414
|
+
observeRoot: () => createRootObservationGuard(this.context),
|
|
412
415
|
stat: relative => this.stat(relative),
|
|
413
|
-
list: async (relative, listingOptions) => {
|
|
416
|
+
list: async (relative, listingOptions, receipt) => {
|
|
414
417
|
validatePinnedRelativePath(relative);
|
|
418
|
+
if (receipt) {
|
|
419
|
+
return await openRootDirectoryListing(this.context, receipt.targetPath, listingOptions, receipt);
|
|
420
|
+
}
|
|
415
421
|
const resolved = await resolvePinnedPathInRoot(this.context, { relativePath: relative, allowRoot: true });
|
|
416
422
|
return await openRootDirectoryListing(this.context, resolved.resolved, listingOptions);
|
|
417
423
|
},
|
|
@@ -798,7 +804,6 @@ async function mkdirPathInRoot(root, params) {
|
|
|
798
804
|
const prepared = policy && resolved.relativePosix !== ""
|
|
799
805
|
? await preparePinnedWriteMutationAdmission({
|
|
800
806
|
rootReal: resolved.rootReal,
|
|
801
|
-
rootWithSep: ensureTrailingSep(resolved.rootReal),
|
|
802
807
|
rootIdentity: root.rootIdentity,
|
|
803
808
|
resolvedTargetPath: resolved.resolved,
|
|
804
809
|
originalPath: params.relativePath,
|
|
@@ -1045,10 +1050,8 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
1045
1050
|
throw err;
|
|
1046
1051
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
1047
1052
|
}
|
|
1048
|
-
const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
|
|
1049
1053
|
return {
|
|
1050
1054
|
rootReal: resolved.rootCanonicalPath,
|
|
1051
|
-
rootWithSep,
|
|
1052
1055
|
canonicalPath: resolved.canonicalPath,
|
|
1053
1056
|
};
|
|
1054
1057
|
}
|