@openclaw/openshell-sandbox 2026.10.5-beta.3 → 2026.10.10-beta.1
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 +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
- package/node_modules/@openclaw/fs-safe/README.md +20 -4
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
- package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
- package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
- package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +15 -5
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
- package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
- package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
- package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
- package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
- package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
- package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
- package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
- package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
- package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
- package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
- package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
- package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
- package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
- package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
- package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
- package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
- package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
- package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
- package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
- package/node_modules/@openclaw/fs-safe/package.json +21 -12
- 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-freebsd-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
- 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-arm64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
- 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/package.json +5 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +0 -336
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs, {} from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
6
|
+
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { statIdentityFromNative } from "./native-stat-identity.js";
|
|
8
|
+
import { getNativeBinding } from "./native.js";
|
|
9
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
10
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
11
|
+
import { timestampSeconds } from "./copy-metadata.js";
|
|
12
|
+
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
13
|
+
import { noReplaceUnavailable } from "./native-noreplace.js";
|
|
14
|
+
import { createSuppressedError } from "./suppressed-error.js";
|
|
15
|
+
/** Shared by tree byte copying and per-file copying; never resolves the target. */
|
|
16
|
+
export async function readCopyLinkTarget(source) {
|
|
17
|
+
return await fsp.readlink(source, { encoding: "buffer" });
|
|
18
|
+
}
|
|
19
|
+
export function assertCopyLinkAvailable() {
|
|
20
|
+
const native = getNativeBinding();
|
|
21
|
+
if (process.platform === "darwin" && !native?.createCopySymlink) {
|
|
22
|
+
throw new FsSafeError("helper-unavailable", "exclusive macOS link copying requires the native helper; link(2) follows its source");
|
|
23
|
+
}
|
|
24
|
+
if (process.platform === "win32" && (!native?.copyLinkExclusive || !native.publishCopyLink || !native.removeCopyLink)) {
|
|
25
|
+
throw new FsSafeError("helper-unavailable", "copying Windows links without following them requires the native helper");
|
|
26
|
+
}
|
|
27
|
+
if (native && process.platform !== "win32" && !native.createCopySymlink) {
|
|
28
|
+
throw new FsSafeError("helper-unavailable", "native link copying is unavailable");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function admitCopyLink(sourcePath, preserveMetadata, signal) {
|
|
32
|
+
const parent = await createAsyncDirectoryGuard(path.dirname(sourcePath), { bigint: true });
|
|
33
|
+
sourcePath = path.join(parent.realPath, path.basename(sourcePath));
|
|
34
|
+
const identity = fs.lstatSync(sourcePath, { bigint: true });
|
|
35
|
+
if (!identity.isSymbolicLink())
|
|
36
|
+
return undefined;
|
|
37
|
+
const target = process.platform === "win32" ? undefined : await readCopyLinkTarget(sourcePath);
|
|
38
|
+
const verifySource = async () => {
|
|
39
|
+
signal?.throwIfAborted();
|
|
40
|
+
await assertAsyncDirectoryGuard(parent);
|
|
41
|
+
const current = inspectFileIdentitySync(() => fs.lstatSync(sourcePath, { bigint: true }), identity);
|
|
42
|
+
if (!current.isSymbolicLink() || current.mtimeNs !== identity.mtimeNs || current.ctimeNs !== identity.ctimeNs) {
|
|
43
|
+
throw new FsSafeError("path-mismatch", "copy source link changed");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
await verifySource();
|
|
47
|
+
return { kind: "link", stageBeforePublish: true, sourcePath, identity, target, preserveMetadata, signal, verifySource };
|
|
48
|
+
}
|
|
49
|
+
/** The caller has admitted the destination parent under Root's mutation policy. */
|
|
50
|
+
export async function copyLinkAtParent(params, input, parent, native) {
|
|
51
|
+
const temporaryName = `.fs-safe-${randomUUID()}.tmp`;
|
|
52
|
+
const temporaryPath = path.join(parent.realPath, temporaryName);
|
|
53
|
+
const destination = path.join(parent.realPath, params.basename);
|
|
54
|
+
let identity;
|
|
55
|
+
let sourceParent;
|
|
56
|
+
let failure;
|
|
57
|
+
const assertStage = () => {
|
|
58
|
+
assertSyncDirectoryGuard(parent);
|
|
59
|
+
const stat = inspectFileIdentitySync(() => fs.lstatSync(temporaryPath, { bigint: true }), identity);
|
|
60
|
+
if (!stat.isSymbolicLink() || stat.nlink !== 1n)
|
|
61
|
+
throw new FsSafeError("path-mismatch", "copy link stage changed");
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
await input.verifySource();
|
|
65
|
+
await assertAsyncDirectoryGuard(parent);
|
|
66
|
+
if (process.platform === "win32") {
|
|
67
|
+
if (!native?.binding.copyLinkExclusive)
|
|
68
|
+
throw new FsSafeError("helper-unavailable", "native Windows link copying is unavailable");
|
|
69
|
+
const sourceGuard = await createAsyncDirectoryGuard(path.dirname(input.sourcePath), { bigint: true });
|
|
70
|
+
sourceParent = await fsp.open(sourceGuard.realPath, fs.constants.O_RDONLY);
|
|
71
|
+
inspectFileIdentitySync(() => fs.fstatSync(sourceParent.fd, { bigint: true }), sourceGuard.stat);
|
|
72
|
+
await input.verifySource();
|
|
73
|
+
params.assertBeforeMutation?.();
|
|
74
|
+
assertSyncDirectoryGuard(parent);
|
|
75
|
+
identity = native.binding.copyLinkExclusive(sourceParent.fd, path.basename(input.sourcePath), native.fd, temporaryName, input.identity.dev, input.identity.ino, input.preserveMetadata, params.mode);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
if (native && !native.binding.createCopySymlink)
|
|
79
|
+
throw new FsSafeError("helper-unavailable", "native link copying is unavailable");
|
|
80
|
+
params.assertBeforeMutation?.();
|
|
81
|
+
assertSyncDirectoryGuard(parent);
|
|
82
|
+
if (native?.binding.createCopySymlink) {
|
|
83
|
+
const created = native.binding.createCopySymlink(native.fd, temporaryName, input.target, params.mode, input.preserveMetadata ? input.identity.atimeNs : undefined, input.preserveMetadata ? input.identity.mtimeNs : undefined);
|
|
84
|
+
identity = { dev: statIdentityFromNative(created.dev), ino: statIdentityFromNative(created.ino) };
|
|
85
|
+
if (created.errorCode)
|
|
86
|
+
throw Object.assign(new Error(created.errorMessage), { code: created.errorCode });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
fs.symlinkSync(input.target, temporaryPath);
|
|
90
|
+
identity = fs.lstatSync(temporaryPath, { bigint: true });
|
|
91
|
+
}
|
|
92
|
+
if (!native && input.preserveMetadata) {
|
|
93
|
+
assertStage();
|
|
94
|
+
fs.lutimesSync(temporaryPath, timestampSeconds(input.identity.atimeNs), timestampSeconds(input.identity.mtimeNs));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
assertStage();
|
|
98
|
+
await input.verifySource();
|
|
99
|
+
params.assertBeforeMutation?.();
|
|
100
|
+
input.signal?.throwIfAborted();
|
|
101
|
+
assertStage();
|
|
102
|
+
if (process.platform === "win32") {
|
|
103
|
+
native.binding.publishCopyLink(native.fd, temporaryName, params.basename, identity.dev, identity.ino);
|
|
104
|
+
}
|
|
105
|
+
else if (native) {
|
|
106
|
+
const renameNoReplace = native.binding.renameNoReplace;
|
|
107
|
+
if (!renameNoReplace)
|
|
108
|
+
throw new FsSafeError("helper-unavailable", "native link publication is unavailable");
|
|
109
|
+
try {
|
|
110
|
+
renameNoReplace(native.fd, temporaryName, native.fd, params.basename);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const unavailable = noReplaceUnavailable(error, "link copy publication", true);
|
|
114
|
+
if (!unavailable || isFsSafeNativeRequired() || process.platform !== "linux")
|
|
115
|
+
throw error;
|
|
116
|
+
// Linux link(2) links the symlink inode itself, never its target.
|
|
117
|
+
assertStage();
|
|
118
|
+
fs.linkSync(temporaryPath, destination);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
fs.linkSync(temporaryPath, destination);
|
|
123
|
+
}
|
|
124
|
+
params.onPublished?.(identity);
|
|
125
|
+
assertSyncDirectoryGuard(parent);
|
|
126
|
+
const final = inspectFileIdentitySync(() => fs.lstatSync(destination, { bigint: true }), identity);
|
|
127
|
+
if (!final.isSymbolicLink())
|
|
128
|
+
throw new FsSafeError("path-mismatch", "published copy link changed");
|
|
129
|
+
if (params.sync !== false)
|
|
130
|
+
await syncDirectoryBestEffort(parent.realPath);
|
|
131
|
+
await input.verifySource();
|
|
132
|
+
return identity;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
failure = { error };
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
let cleanupFailure;
|
|
140
|
+
try {
|
|
141
|
+
if (identity) {
|
|
142
|
+
assertSyncDirectoryGuard(parent);
|
|
143
|
+
const remaining = fs.lstatSync(temporaryPath, { bigint: true, throwIfNoEntry: false });
|
|
144
|
+
if (remaining) {
|
|
145
|
+
inspectFileIdentitySync(() => remaining, identity);
|
|
146
|
+
if (!remaining.isSymbolicLink())
|
|
147
|
+
throw new FsSafeError("path-mismatch", "copy link cleanup identity changed");
|
|
148
|
+
if (process.platform === "win32")
|
|
149
|
+
native.binding.removeCopyLink(native.fd, temporaryName, identity.dev, identity.ino);
|
|
150
|
+
else
|
|
151
|
+
fs.unlinkSync(temporaryPath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
cleanupFailure = { error };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await sourceParent?.close();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
cleanupFailure = { error: cleanupFailure
|
|
163
|
+
? createSuppressedError(error, cleanupFailure.error, "copy link cleanup and close failed") : error };
|
|
164
|
+
}
|
|
165
|
+
if (cleanupFailure)
|
|
166
|
+
throw failure
|
|
167
|
+
? createSuppressedError(cleanupFailure.error, failure.error, "copy link operation and cleanup failed")
|
|
168
|
+
: cleanupFailure.error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
export type CopyMetadata = Readonly<{
|
|
3
|
+
atimeNs: bigint;
|
|
4
|
+
mtimeNs: bigint;
|
|
5
|
+
restoreNative?: (fd: number) => void;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function timestampSeconds(nanoseconds: bigint): string;
|
|
8
|
+
export declare function captureCopyMetadata(fd: number, stat: BigIntStats): CopyMetadata;
|
|
9
|
+
export declare function restoreCopyMetadata(fd: number, metadata: CopyMetadata | undefined): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { getNativeBinding } from "./native.js";
|
|
4
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
5
|
+
export function timestampSeconds(nanoseconds) {
|
|
6
|
+
// Numeric strings retain fractional and pre-epoch times in Node's utimes API.
|
|
7
|
+
return String(Number(nanoseconds / 1000000000n) + Number(nanoseconds % 1000000000n) / 1e9);
|
|
8
|
+
}
|
|
9
|
+
export function captureCopyMetadata(fd, stat) {
|
|
10
|
+
let restoreNative;
|
|
11
|
+
const native = getNativeBinding();
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
if (native?.readCopyMetadata && native.restoreCopyMetadata) {
|
|
14
|
+
const snapshot = native.readCopyMetadata(fd);
|
|
15
|
+
const restore = native.restoreCopyMetadata.bind(native);
|
|
16
|
+
restoreNative = target => restore(target, snapshot);
|
|
17
|
+
}
|
|
18
|
+
else if (isFsSafeNativeRequired()) {
|
|
19
|
+
throw new FsSafeError("helper-unavailable", "native Windows copy metadata preservation is unavailable");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (native?.restoreCopyFileTimes) {
|
|
23
|
+
const restore = native.restoreCopyFileTimes.bind(native);
|
|
24
|
+
const { atimeNs, mtimeNs } = stat;
|
|
25
|
+
restoreNative = target => restore(target, atimeNs, mtimeNs);
|
|
26
|
+
}
|
|
27
|
+
else if (isFsSafeNativeRequired()) {
|
|
28
|
+
throw new FsSafeError("helper-unavailable", "native copy timestamp preservation is unavailable");
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({ atimeNs: stat.atimeNs, mtimeNs: stat.mtimeNs, restoreNative });
|
|
31
|
+
}
|
|
32
|
+
export function restoreCopyMetadata(fd, metadata) {
|
|
33
|
+
if (!metadata)
|
|
34
|
+
return;
|
|
35
|
+
if (metadata.restoreNative)
|
|
36
|
+
metadata.restoreNative(fd);
|
|
37
|
+
else
|
|
38
|
+
fs.futimesSync(fd, timestampSeconds(metadata.atimeNs), timestampSeconds(metadata.mtimeNs));
|
|
39
|
+
}
|
|
@@ -5,6 +5,7 @@ import { FsSafeError } from "./errors.js";
|
|
|
5
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
6
6
|
import { realpathSync } from "./realpath.js";
|
|
7
7
|
import { resolveRootPathSync, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
|
|
8
|
+
import { hasWindowsPathAlias } from "./windows-path-alias.js";
|
|
8
9
|
function admitUncachedCopyPath(input, create = false, admittedRoot) {
|
|
9
10
|
if (!path.isAbsolute(input.rootPath) || !path.isAbsolute(input.absolutePath)) {
|
|
10
11
|
throw new FsSafeError("invalid-path", "copy roots and paths must be absolute");
|
|
@@ -50,15 +51,23 @@ export function createCopyPathAdmissionCache() {
|
|
|
50
51
|
const assertParent = ({ root, parent, chain }) => {
|
|
51
52
|
assertSyncDirectoryGuard(root);
|
|
52
53
|
for (const guard of chain) {
|
|
54
|
+
// The full parent guard immediately below includes this exact identity.
|
|
55
|
+
if (guard === parent)
|
|
56
|
+
continue;
|
|
53
57
|
inspectDirectoryIdentitySync(guard.dir, { dev: BigInt(guard.stat.dev), ino: BigInt(guard.stat.ino) });
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
if (parent !== root)
|
|
60
|
+
assertSyncDirectoryGuard(parent);
|
|
56
61
|
assertSyncDirectoryGuard(root);
|
|
57
62
|
};
|
|
58
63
|
const admit = (input, create = false) => {
|
|
59
64
|
// Keep the public resolver for Windows aliases and noncanonical spellings,
|
|
60
65
|
// including raw dot segments whose traversal must not be normalized away.
|
|
61
|
-
|
|
66
|
+
const ordinaryWindowsPath = process.platform !== "win32" || (/^[a-z]:\\/iu.test(input.rootPath) &&
|
|
67
|
+
!hasWindowsPathAlias(input.rootPath, "filesystem") &&
|
|
68
|
+
!hasWindowsPathAlias(input.absolutePath, "filesystem") &&
|
|
69
|
+
!input.absolutePath.slice(3).split("\\").some(segment => /[. ]$/u.test(segment) || segment.includes("~")));
|
|
70
|
+
if (!ordinaryWindowsPath || !path.isAbsolute(input.rootPath) ||
|
|
62
71
|
!path.isAbsolute(input.absolutePath) || input.rootPath.includes("\0") || input.absolutePath.includes("\0") ||
|
|
63
72
|
path.resolve(input.rootPath) !== input.rootPath || path.resolve(input.absolutePath) !== input.absolutePath ||
|
|
64
73
|
!input.absolutePath.startsWith(input.rootPath.endsWith(path.sep) ? input.rootPath : `${input.rootPath}${path.sep}`)) {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
import { type ExactIdentityReceipt, type ExactFileIdentity } from "./file-identity.js";
|
|
1
2
|
import { type CopyPath } from "./copy-path-admission.js";
|
|
2
3
|
import { type CopyCloneMode } from "./copy-policy.js";
|
|
3
4
|
import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
4
5
|
export type CopyRootFileSyncOptions = {
|
|
5
6
|
source: CopyPath;
|
|
6
7
|
destination: CopyPath;
|
|
7
|
-
expectedSourceIdentity?:
|
|
8
|
-
dev: bigint;
|
|
9
|
-
ino: bigint;
|
|
10
|
-
};
|
|
8
|
+
expectedSourceIdentity?: ExactFileIdentity | ExactIdentityReceipt;
|
|
11
9
|
clone?: CopyCloneMode;
|
|
12
10
|
maxBytes?: number;
|
|
13
11
|
mode?: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { identityFromReceipt } from "./file-identity.js";
|
|
1
2
|
import fs, {} from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
@@ -50,7 +51,8 @@ function copyRootFileWithAdmissionSync(options, cache) {
|
|
|
50
51
|
const targetInput = options.destination;
|
|
51
52
|
const targetPath = { rootPath: targetInput.rootPath, absolutePath: targetInput.absolutePath };
|
|
52
53
|
const expectedInput = options.expectedSourceIdentity;
|
|
53
|
-
const
|
|
54
|
+
const expected = expectedInput === undefined ? undefined : identityFromReceipt(expectedInput);
|
|
55
|
+
const expectedSourceIdentity = expected && { dev: expected.dev, ino: expected.ino };
|
|
54
56
|
const clone = resolveCopyCloneMode(options.clone, "never");
|
|
55
57
|
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
56
58
|
const mode = options.mode;
|
|
@@ -68,7 +70,7 @@ function copyRootFileWithAdmissionSync(options, cache) {
|
|
|
68
70
|
let parentOpen = false;
|
|
69
71
|
let target;
|
|
70
72
|
let identity;
|
|
71
|
-
const native = getNativeBinding();
|
|
73
|
+
const native = process.platform !== "win32" ? getNativeBinding("copyFileExclusiveSync", "removeStagedFile") : getNativeBinding();
|
|
72
74
|
const nativeCopy = process.platform !== "win32" && native?.copyFileExclusiveSync && native.removeStagedFile
|
|
73
75
|
? native.copyFileExclusiveSync.bind(native) : undefined;
|
|
74
76
|
try {
|
|
@@ -4,12 +4,9 @@ import fsp, {} from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
7
|
+
import { timestampSeconds } from "./copy-metadata.js";
|
|
8
|
+
import { readCopyLinkTarget } from "./copy-link.js";
|
|
7
9
|
import { assertStagedDirectoryCurrent, exactIdentityMatches, openStagedDirectory, } from "./staged-directory.js";
|
|
8
|
-
function timestampSeconds(nanoseconds) {
|
|
9
|
-
// Dates discard sub-millisecond precision; negative numbers mean "now" in
|
|
10
|
-
// Node's utimes API. Numeric strings retain fractional, pre-epoch timestamps.
|
|
11
|
-
return String(Number(nanoseconds / 1000000000n) + Number(nanoseconds % 1000000000n) / 1e9);
|
|
12
|
-
}
|
|
13
10
|
/** Byte-copy adapter for the shared immutable-source, caller-owned namespace contract. */
|
|
14
11
|
export async function copyOwnedTree(source, destination, options) {
|
|
15
12
|
const callerSignal = options.signal ? AbortSignal.any([options.signal]) : undefined;
|
|
@@ -145,7 +142,7 @@ export async function copyOwnedTree(source, destination, options) {
|
|
|
145
142
|
else if (child.isSymbolicLink()) {
|
|
146
143
|
const link = process.platform === "win32"
|
|
147
144
|
? await fsp.readlink(childSource)
|
|
148
|
-
: await
|
|
145
|
+
: await readCopyLinkTarget(childSource);
|
|
149
146
|
let type;
|
|
150
147
|
if (process.platform === "win32") {
|
|
151
148
|
// Older Node releases infer link type from the destination, where a
|
|
@@ -21,7 +21,7 @@ function closeDescriptor(fd, failure) {
|
|
|
21
21
|
}
|
|
22
22
|
/** Inspect an existing real directory without creating probe files. */
|
|
23
23
|
export function probeTreeClone(parentPath) {
|
|
24
|
-
const native = getNativeBinding();
|
|
24
|
+
const native = getNativeBinding("probeTreeClone");
|
|
25
25
|
if (!native)
|
|
26
26
|
return undefined;
|
|
27
27
|
const parent = openStagedDirectory(assertAbsolutePathInput(parentPath));
|
|
@@ -64,9 +64,9 @@ async function materializeTree(source, destination, options, policy) {
|
|
|
64
64
|
throw new FsSafeError("invalid-path", "clone concurrency must be an integer between 1 and 32");
|
|
65
65
|
}
|
|
66
66
|
const native = policy === "always"
|
|
67
|
-
? requireNativeBinding()
|
|
67
|
+
? requireNativeBinding("probeTreeClone", "cloneTree")
|
|
68
68
|
: policy === "auto"
|
|
69
|
-
? getNativeBinding()
|
|
69
|
+
? getNativeBinding("probeTreeClone", "cloneTree")
|
|
70
70
|
: undefined;
|
|
71
71
|
const target = assertAbsolutePathInput(destination);
|
|
72
72
|
const name = path.basename(target);
|
|
@@ -204,7 +204,7 @@ function privateDirectoryBackend() {
|
|
|
204
204
|
if (getFsSafeNativeConfig().mode === "require") {
|
|
205
205
|
throw new FsSafeError("helper-unavailable", "private directory creation requires an up-to-date native parent identity capability");
|
|
206
206
|
}
|
|
207
|
-
warnNativeFallback("windows-private-directory", "Private Windows directory creation
|
|
207
|
+
warnNativeFallback("windows-private-directory", "Private Windows directory creation launches the packaged .NET helper for each check or creation.", "createPrivateDirectoryWithParentIdentity");
|
|
208
208
|
return undefined;
|
|
209
209
|
}
|
|
210
210
|
export async function createDirectory(targetPath, options = {}) {
|
|
@@ -53,17 +53,17 @@ export function assertPrivateFileCreationAvailable() {
|
|
|
53
53
|
typeof native.verifyPrivateWindowsFile !== "function")
|
|
54
54
|
unavailable();
|
|
55
55
|
}
|
|
56
|
-
function commandFallback() {
|
|
56
|
+
function commandFallback(capability) {
|
|
57
57
|
if (getFsSafeNativeConfig().mode === "require")
|
|
58
58
|
unavailable();
|
|
59
|
-
warnNativeFallback("windows-private-creation", "Private Windows creation
|
|
59
|
+
warnNativeFallback("windows-private-creation", "Private Windows creation launches the packaged .NET helper for each check or creation.", capability);
|
|
60
60
|
}
|
|
61
61
|
function creationOperation(name) {
|
|
62
62
|
const native = getNativeBinding();
|
|
63
63
|
const operation = native?.[name];
|
|
64
64
|
if (typeof operation === "function")
|
|
65
65
|
return operation.bind(native);
|
|
66
|
-
commandFallback();
|
|
66
|
+
commandFallback(name);
|
|
67
67
|
return undefined;
|
|
68
68
|
}
|
|
69
69
|
/** Full Windows identity, with security validation when requested; no repairs. */
|
|
@@ -300,7 +300,10 @@ export function assertBeforeCreation(selected, permissions, assertion, kind) {
|
|
|
300
300
|
if (permissions.private)
|
|
301
301
|
assertDarwinCreationDirectoryAcl(selected.parent.dir, selected.parent.stat, kind);
|
|
302
302
|
assertSynchronousCallbackResult(assertion?.(), "assertBeforeMutation");
|
|
303
|
-
|
|
303
|
+
// Ordinary callers enter immediately after prepareCreationPath's identical fence.
|
|
304
|
+
// A callback or private ACL inspection needs a fresh observation afterward.
|
|
305
|
+
if (assertion !== undefined || permissions.private)
|
|
306
|
+
selected.assertParent();
|
|
304
307
|
if (permissions.private && kind === "directory")
|
|
305
308
|
assertDarwinPrivateDirectoryMode(permissions.mode);
|
|
306
309
|
if (permissions.private)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DarwinAclInspection = {
|
|
2
|
+
kind: "none";
|
|
3
|
+
} | {
|
|
4
|
+
kind: "present";
|
|
5
|
+
inheritsToFiles: boolean;
|
|
6
|
+
inheritsToDirectories: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "unknown";
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
/** Inspect one no-follow descriptor. Unknown is never evidence of no ACL. */
|
|
12
|
+
export declare function inspectDarwinAcl(path: string): DarwinAclInspection;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { getNativeBinding } from "./native.js";
|
|
3
|
+
/** Inspect one no-follow descriptor. Unknown is never evidence of no ACL. */
|
|
4
|
+
export function inspectDarwinAcl(path) {
|
|
5
|
+
if (process.platform !== "darwin")
|
|
6
|
+
return { kind: "unknown", reason: "unsupported-platform" };
|
|
7
|
+
let fd;
|
|
8
|
+
try {
|
|
9
|
+
const native = getNativeBinding();
|
|
10
|
+
if (!native?.inspectDarwinAcl)
|
|
11
|
+
return { kind: "unknown", reason: "helper-unavailable" };
|
|
12
|
+
fd = fs.openSync(path, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
13
|
+
const facts = native.inspectDarwinAcl(fd);
|
|
14
|
+
if (facts.state === "absent" || facts.state === "empty")
|
|
15
|
+
return { kind: "none" };
|
|
16
|
+
if (facts.state === "present" && typeof facts.inheritsToFiles === "boolean" &&
|
|
17
|
+
typeof facts.inheritsToDirectories === "boolean") {
|
|
18
|
+
return { kind: "present", inheritsToFiles: facts.inheritsToFiles, inheritsToDirectories: facts.inheritsToDirectories };
|
|
19
|
+
}
|
|
20
|
+
return { kind: "unknown", reason: "incomplete-acl-facts" };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { kind: "unknown", reason: error instanceof Error ? error.message : String(error) };
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (fd !== undefined)
|
|
27
|
+
fs.closeSync(fd);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ExactFileIdentity, ExactIdentityReceipt } from "./exact-identity.js";
|
|
1
2
|
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
|
-
import type {
|
|
3
|
+
import type { FileIdentityInput } from "./file-identity.js";
|
|
3
4
|
export type DirectorySyncOutcome = {
|
|
4
5
|
status: "synced";
|
|
5
6
|
} | {
|
|
@@ -10,14 +11,16 @@ export type DirectoryReceipt<T extends Stats | BigIntStats = Stats> = {
|
|
|
10
11
|
path: string;
|
|
11
12
|
realPath: string;
|
|
12
13
|
identity: T;
|
|
14
|
+
exactIdentity?: ExactFileIdentity;
|
|
13
15
|
};
|
|
16
|
+
export type ExactDirectoryReceipt = DirectoryReceipt & ExactIdentityReceipt;
|
|
14
17
|
export type DurableDirectoryReceipt = DirectoryReceipt & {
|
|
15
18
|
parentSync: DirectorySyncOutcome | {
|
|
16
19
|
status: "not-needed";
|
|
17
20
|
};
|
|
18
21
|
};
|
|
19
22
|
export type PinnedDirectory = {
|
|
20
|
-
readonly receipt:
|
|
23
|
+
readonly receipt: ExactDirectoryReceipt;
|
|
21
24
|
assertCurrent(): Promise<void>;
|
|
22
25
|
sync(): Promise<DirectorySyncOutcome>;
|
|
23
26
|
close(): Promise<void>;
|
|
@@ -26,7 +29,7 @@ export type EnsureDurableDirectoryOptions = {
|
|
|
26
29
|
directoryPath: string;
|
|
27
30
|
label?: string;
|
|
28
31
|
mode?: number;
|
|
29
|
-
expectedExistingIdentity?:
|
|
32
|
+
expectedExistingIdentity?: FileIdentityInput;
|
|
30
33
|
create?: (directoryPath: string) => Promise<void>;
|
|
31
34
|
};
|
|
32
35
|
export declare function pinDirectory(directory: string | DirectoryReceipt<Stats | BigIntStats>, options?: {
|
|
@@ -40,4 +43,4 @@ export declare function syncDirectorySync(directory: string | DirectoryReceipt<S
|
|
|
40
43
|
}): DirectorySyncOutcome;
|
|
41
44
|
export declare function syncDirectoryBestEffort(directoryPath: string): Promise<void>;
|
|
42
45
|
export declare function syncDirectoryBestEffortSync(directoryPath: string): void;
|
|
43
|
-
export declare function ensureDurableDirectory(options: EnsureDurableDirectoryOptions): Promise<DurableDirectoryReceipt>;
|
|
46
|
+
export declare function ensureDurableDirectory(options: EnsureDurableDirectoryOptions): Promise<DurableDirectoryReceipt & ExactIdentityReceipt>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
4
|
+
import { hasSafeFileIdentity, sameFileIdentity } from "./file-identity.js";
|
|
5
5
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
6
|
import { isNotFoundPathError } from "./path.js";
|
|
7
7
|
import { directoryComponentNotDirectoryError } from "./root-errors.js";
|
|
@@ -9,6 +9,9 @@ import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservi
|
|
|
9
9
|
import { realpathSync } from "./realpath.js";
|
|
10
10
|
import { directoryEntryPath } from "./directory-entry-path.js";
|
|
11
11
|
import { assertStatObservationSync, inspectStatObservationSync, } from "./stat-observation.js";
|
|
12
|
+
// Numeric metadata is retained for existing internal consumers; rounded IDs
|
|
13
|
+
// must never become authority for a later comparison.
|
|
14
|
+
const exactGuardIdentities = new WeakMap();
|
|
12
15
|
export async function readDirectoryIdentity(dir) {
|
|
13
16
|
const guard = await createAsyncDirectoryGuard(dir, { bigint: true });
|
|
14
17
|
return Object.freeze({ dev: guard.stat.dev, ino: guard.stat.ino, realPath: guard.realPath });
|
|
@@ -43,9 +46,19 @@ export function captureDirectoryGuard(dir, mode, options) {
|
|
|
43
46
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
44
47
|
throw directoryComponentNotDirectoryError();
|
|
45
48
|
}
|
|
49
|
+
let exactIdentity;
|
|
50
|
+
if (!hasSafeFileIdentity(stat)) {
|
|
51
|
+
const exact = fsSync.lstatSync(operationPath, { bigint: true });
|
|
52
|
+
if (exact.isSymbolicLink() || !exact.isDirectory())
|
|
53
|
+
throw directoryComponentNotDirectoryError();
|
|
54
|
+
exactIdentity = { dev: exact.dev, ino: exact.ino };
|
|
55
|
+
}
|
|
46
56
|
const realPath = mode === "native" ? realpathSync.native(operationPath) : realpathSync(operationPath);
|
|
47
57
|
assertNoWindowsPathAlias(realPath, "filesystem");
|
|
48
|
-
|
|
58
|
+
const guard = { dir, realPath, stat };
|
|
59
|
+
if (exactIdentity)
|
|
60
|
+
exactGuardIdentities.set(guard, exactIdentity);
|
|
61
|
+
return guard;
|
|
49
62
|
}
|
|
50
63
|
export async function assertAsyncDirectoryGuard(guard) {
|
|
51
64
|
assertDirectoryGuard(guard, "native");
|
|
@@ -59,9 +72,11 @@ export function assertDirectoryGuard(guard, mode) {
|
|
|
59
72
|
const expectedRealPath = guard.realPath;
|
|
60
73
|
assertNoWindowsPathAlias(expectedRealPath, "filesystem");
|
|
61
74
|
const expectedStat = guard.stat;
|
|
62
|
-
const
|
|
75
|
+
const suppliedIdentity = { dev: expectedStat.dev, ino: expectedStat.ino };
|
|
76
|
+
const retainedIdentity = exactGuardIdentities.get(guard);
|
|
77
|
+
const expectedIdentity = retainedIdentity ?? suppliedIdentity;
|
|
63
78
|
const stat = typeof expectedIdentity.dev === "bigint" && typeof expectedIdentity.ino === "bigint"
|
|
64
|
-
? inspectDirectoryIdentityAtPathSync(operationPath, {
|
|
79
|
+
? retainedIdentity ? fsSync.lstatSync(operationPath, { bigint: true }) : inspectDirectoryIdentityAtPathSync(operationPath, {
|
|
65
80
|
dev: expectedIdentity.dev,
|
|
66
81
|
ino: expectedIdentity.ino,
|
|
67
82
|
})
|
|
@@ -74,7 +89,7 @@ export function assertDirectoryGuard(guard, mode) {
|
|
|
74
89
|
if (mode === "native" && !sameFileIdentity(stat, expectedIdentity)) {
|
|
75
90
|
throw new FsSafeError("path-mismatch", "directory changed during operation");
|
|
76
91
|
}
|
|
77
|
-
const realPath = mode === "native" || typeof
|
|
92
|
+
const realPath = mode === "native" || typeof suppliedIdentity.ino === "bigint"
|
|
78
93
|
? realpathSync.native(operationPath) : realpathSync(operationPath);
|
|
79
94
|
assertNoWindowsPathAlias(realPath, "filesystem");
|
|
80
95
|
if ((mode === "normalized" && !sameFileIdentity(stat, expectedIdentity)) || realPath !== expectedRealPath) {
|
|
@@ -5,6 +5,7 @@ export type DirectoryModeChecks = {
|
|
|
5
5
|
};
|
|
6
6
|
export type DirectoryModeOwner = {
|
|
7
7
|
verify(check?: () => void): Promise<void>;
|
|
8
|
+
verifyCanonical?(): Promise<boolean>;
|
|
8
9
|
apply(mode: number, checks?: DirectoryModeChecks): Promise<void>;
|
|
9
10
|
close(): Promise<void>;
|
|
10
11
|
};
|
|
@@ -12,6 +13,7 @@ export declare function assertOwnedDirectory(expected: Stats | BigIntStats, actu
|
|
|
12
13
|
/** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
|
|
13
14
|
export declare function ownDirectoryMode(params: {
|
|
14
15
|
inspect: () => Promise<number>;
|
|
16
|
+
inspectCanonical?: () => number | undefined;
|
|
15
17
|
chmod: (mode: number) => Promise<void>;
|
|
16
18
|
prepareChmod?: () => Promise<void>;
|
|
17
19
|
verifyChmod?: () => Promise<void>;
|
|
@@ -28,6 +30,7 @@ export declare function nodeDirectorySearchOnlyFlags(): {
|
|
|
28
30
|
export declare function pinNodeDirectoryForMode(dirPath: string, options?: {
|
|
29
31
|
expectedIdentity?: BigIntStats;
|
|
30
32
|
ownerUid?: number;
|
|
33
|
+
canonicalPath?: string;
|
|
31
34
|
}): Promise<DirectoryModeOwner>;
|
|
32
35
|
/** Synchronous counterpart with the same no-follow, identity, and owner checks. */
|
|
33
36
|
export declare function pinNodeDirectoryForModeSync(dirPath: string, options?: {
|
|
@@ -4,6 +4,8 @@ import { FsSafeError } from "./errors.js";
|
|
|
4
4
|
import { inspectDirectoryIdentity, inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
5
5
|
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
6
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
|
+
import { getNativeBinding } from "./native.js";
|
|
8
|
+
import { nativePolicyDirectoryObserver } from "./native-policy-directory-observation.js";
|
|
7
9
|
export function assertOwnedDirectory(expected, actual) {
|
|
8
10
|
if (actual.isSymbolicLink() || !actual.isDirectory()) {
|
|
9
11
|
throw new FsSafeError("not-file", "directory mode target must be a real directory");
|
|
@@ -23,7 +25,7 @@ export function ownDirectoryMode(params) {
|
|
|
23
25
|
pending = operation.catch(() => undefined);
|
|
24
26
|
return operation;
|
|
25
27
|
};
|
|
26
|
-
|
|
28
|
+
const owner = {
|
|
27
29
|
verify: (check) => enqueue(async () => {
|
|
28
30
|
check?.();
|
|
29
31
|
await params.inspect();
|
|
@@ -79,6 +81,14 @@ export function ownDirectoryMode(params) {
|
|
|
79
81
|
return closing;
|
|
80
82
|
},
|
|
81
83
|
};
|
|
84
|
+
if (params.inspectCanonical) {
|
|
85
|
+
owner.verifyCanonical = async () => {
|
|
86
|
+
let verified = false;
|
|
87
|
+
await enqueue(async () => { verified = params.inspectCanonical() !== undefined; });
|
|
88
|
+
return verified;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return owner;
|
|
82
92
|
}
|
|
83
93
|
/** Darwin descriptor inspection avoids requesting directory-content reads. */
|
|
84
94
|
export function nodeDarwinDirectoryMetadataFlags() {
|
|
@@ -131,7 +141,11 @@ export async function pinNodeDirectoryForMode(dirPath, options = {}) {
|
|
|
131
141
|
proc = route.proc;
|
|
132
142
|
return await fs.open(dirPath, route.flags | flags);
|
|
133
143
|
});
|
|
144
|
+
let observer;
|
|
134
145
|
try {
|
|
146
|
+
observer = options.canonicalPath === dirPath && ownerUid === undefined
|
|
147
|
+
? nativePolicyDirectoryObserver(getNativeBinding(), handle.fd, dirPath)
|
|
148
|
+
: undefined;
|
|
135
149
|
const inspect = async () => {
|
|
136
150
|
const opened = await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), expected);
|
|
137
151
|
assertOwnedDirectory(expected, opened);
|
|
@@ -153,8 +167,18 @@ export async function pinNodeDirectoryForMode(dirPath, options = {}) {
|
|
|
153
167
|
assertOwner(opened);
|
|
154
168
|
assertOwner(followed);
|
|
155
169
|
};
|
|
170
|
+
const observeCanonical = observer;
|
|
156
171
|
const owner = ownDirectoryMode({
|
|
157
172
|
inspect,
|
|
173
|
+
inspectCanonical: observeCanonical ? () => {
|
|
174
|
+
const observed = observeCanonical();
|
|
175
|
+
if (!observed)
|
|
176
|
+
return undefined;
|
|
177
|
+
if (observed.identity.dev !== expected.dev || observed.identity.ino !== expected.ino) {
|
|
178
|
+
throw new FsSafeError("path-mismatch", "directory changed before its mode could be applied");
|
|
179
|
+
}
|
|
180
|
+
return Number(observed.identity.mode & 4095n);
|
|
181
|
+
} : undefined,
|
|
158
182
|
prepareChmod: proc ? assertProcAuthority : undefined,
|
|
159
183
|
verifyChmod: proc ? assertProcAuthority : undefined,
|
|
160
184
|
async chmod(mode) {
|
|
@@ -165,12 +189,13 @@ export async function pinNodeDirectoryForMode(dirPath, options = {}) {
|
|
|
165
189
|
await handle.chmod(mode);
|
|
166
190
|
}
|
|
167
191
|
},
|
|
168
|
-
close: () => handle.close(),
|
|
192
|
+
close: () => { observer?.dispose(); return handle.close(); },
|
|
169
193
|
});
|
|
170
194
|
await owner.verify();
|
|
171
195
|
return owner;
|
|
172
196
|
}
|
|
173
197
|
catch (error) {
|
|
198
|
+
observer?.dispose();
|
|
174
199
|
try {
|
|
175
200
|
await handle.close();
|
|
176
201
|
}
|