@openclaw/openshell-sandbox 2026.10.5 → 2026.10.7
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/dist/index.js +16 -18
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +48 -0
- package/node_modules/@openclaw/fs-safe/README.md +14 -4
- 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/clone-metadata.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-link.js +169 -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-root-file-sync.js +1 -1
- 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/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-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/entry-publication.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/file-hash.js +4 -3
- 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/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/json-durable-queue-ownership.js +16 -3
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +57 -18
- package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +1 -1
- 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 +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +19 -1
- 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 +18 -0
- 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.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native.d.ts +5 -3
- package/node_modules/@openclaw/fs-safe/dist/native.js +25 -6
- 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/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 +36 -47
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +5 -3
- 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-producer-handoff.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +12 -11
- 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.js +12 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +6 -92
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
- 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/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/watch-native.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +11 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +9 -1
- 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/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +35 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +22 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +4 -2
- 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 +13 -5
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +15 -5
- package/node_modules/@openclaw/fs-safe/docs/native.md +51 -2
- 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/quickstart.md +23 -3
- package/node_modules/@openclaw/fs-safe/docs/reading.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
- package/node_modules/@openclaw/fs-safe/docs/store.md +11 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +45 -80
- package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
- package/node_modules/@openclaw/fs-safe/package.json +18 -10
- 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
|
@@ -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
|
+
}
|
|
@@ -68,7 +68,7 @@ function copyRootFileWithAdmissionSync(options, cache) {
|
|
|
68
68
|
let parentOpen = false;
|
|
69
69
|
let target;
|
|
70
70
|
let identity;
|
|
71
|
-
const native = getNativeBinding();
|
|
71
|
+
const native = process.platform !== "win32" ? getNativeBinding("copyFileExclusiveSync", "removeStagedFile") : getNativeBinding();
|
|
72
72
|
const nativeCopy = process.platform !== "win32" && native?.copyFileExclusiveSync && native.removeStagedFile
|
|
73
73
|
? native.copyFileExclusiveSync.bind(native) : undefined;
|
|
74
74
|
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);
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -230,7 +230,7 @@ export function retainEntryForPublication(options) {
|
|
|
230
230
|
inspectEntry(sourcePath, source.expected);
|
|
231
231
|
const sourceFd = kind === "symlink"
|
|
232
232
|
? binding.openStagedSymlink(sourceParent.fd, source.basename)
|
|
233
|
-
:
|
|
233
|
+
: native.openBeneath(sourceParent.fd, source.basename, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK |
|
|
234
234
|
(kind === "directory" ? fs.constants.O_DIRECTORY : 0))?.fd;
|
|
235
235
|
if (!Number.isInteger(sourceFd) || sourceFd < 0) {
|
|
236
236
|
throw new FsSafeError("helper-unavailable", "publication source descriptor is unavailable");
|
|
@@ -113,7 +113,8 @@ export async function copyFileHandle(source, target, options = {}) {
|
|
|
113
113
|
const assertBeforeMutation = composeMutationAssertions(undefined, options.assertBeforeMutation);
|
|
114
114
|
const sourceStat = await inspectFileIdentity(() => {
|
|
115
115
|
signal?.throwIfAborted();
|
|
116
|
-
|
|
116
|
+
const fd = source.fd;
|
|
117
|
+
return fd < 0 ? source.stat({ bigint: true }) : fs.fstatSync(fd, { bigint: true });
|
|
117
118
|
});
|
|
118
119
|
signal?.throwIfAborted();
|
|
119
120
|
if (!sourceStat.isFile())
|
|
@@ -123,7 +124,8 @@ export async function copyFileHandle(source, target, options = {}) {
|
|
|
123
124
|
}
|
|
124
125
|
const targetStat = await inspectFileIdentity(() => {
|
|
125
126
|
signal?.throwIfAborted();
|
|
126
|
-
|
|
127
|
+
const fd = target.fd;
|
|
128
|
+
return fd < 0 ? target.stat({ bigint: true }) : fs.fstatSync(fd, { bigint: true });
|
|
127
129
|
});
|
|
128
130
|
signal?.throwIfAborted();
|
|
129
131
|
if (!targetStat.isFile())
|
|
@@ -55,7 +55,7 @@ import fsSync from "node:fs";
|
|
|
55
55
|
import fs from "node:fs/promises";
|
|
56
56
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
57
57
|
import { FsSafeError } from "./errors.js";
|
|
58
|
-
import { getNativeBinding } from "./native.js";
|
|
58
|
+
import { getNativeBinding, selectNativeBinding } from "./native.js";
|
|
59
59
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
60
60
|
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
61
61
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
@@ -100,11 +100,12 @@ export async function hashFileHandle(handle, native = getNativeBinding(), { maxB
|
|
|
100
100
|
if (stat.size > maxBytes) {
|
|
101
101
|
throw new FsSafeError("too-large", `SHA-256 input exceeds ${maxBytes} bytes`);
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
const hasher = selectNativeBinding(native, "sha256File");
|
|
104
|
+
if (hasher) {
|
|
104
105
|
// A completed N-API task can mask later aborts on the same signal.
|
|
105
106
|
const nativeSignal = signal ? AbortSignal.any([signal]) : undefined;
|
|
106
107
|
try {
|
|
107
|
-
const result = await
|
|
108
|
+
const result = await hasher.sha256File(handle.fd, Number.isFinite(maxBytes) ? maxBytes : undefined, nativeSignal);
|
|
108
109
|
signal?.throwIfAborted();
|
|
109
110
|
return result;
|
|
110
111
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SidecarLockAcquireOptions, type SidecarLockHandle, type SidecarLockHeldEntry, type SidecarLockRetryOptions, type SidecarLockStaleRecovery } from "./sidecar-lock.js";
|
|
2
|
+
export { tryAcquireWriteLease, type FileWriteLease } from "./file-write-lease.js";
|
|
2
3
|
export { acquireFileLockSync, withFileLockSync, type FileLockSyncAcquireOptions, type FileLockSyncHandle, } from "./file-lock-sync.js";
|
|
3
4
|
export type FileLockRetryOptions = SidecarLockRetryOptions;
|
|
4
5
|
export type FileLockStaleRecovery = SidecarLockStaleRecovery;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSidecarLockManager, } from "./sidecar-lock.js";
|
|
2
2
|
import { getFsSafeLockConfig } from "./lock-config.js";
|
|
3
|
+
export { tryAcquireWriteLease } from "./file-write-lease.js";
|
|
3
4
|
export { acquireFileLockSync, withFileLockSync, } from "./file-lock-sync.js";
|
|
4
5
|
function resolveFileLockManagerKey(targetPath, managerKey) {
|
|
5
6
|
return managerKey ?? `fs-safe.file-lock:${targetPath}`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type FileWriteLease = {
|
|
2
|
+
readonly fd: number;
|
|
3
|
+
/** False after release or when the kernel breaks or downgrades the lease. */
|
|
4
|
+
isHeld(): boolean;
|
|
5
|
+
release(): void;
|
|
6
|
+
[Symbol.dispose](): void;
|
|
7
|
+
};
|
|
8
|
+
/** Linux kernel write lease. The caller retains the open fd and SIGIO handling. */
|
|
9
|
+
export declare function tryAcquireWriteLease(fd: number): FileWriteLease | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { requireNativeBinding } from "./native.js";
|
|
3
|
+
/** Linux kernel write lease. The caller retains the open fd and SIGIO handling. */
|
|
4
|
+
export function tryAcquireWriteLease(fd) {
|
|
5
|
+
if (process.platform !== "linux") {
|
|
6
|
+
throw new FsSafeError("unsupported-platform", "File write leases require Linux");
|
|
7
|
+
}
|
|
8
|
+
if (!Number.isInteger(fd) || fd < 0 || fd > 0x7fffffff) {
|
|
9
|
+
throw new FsSafeError("invalid-path", "Expected an open file descriptor");
|
|
10
|
+
}
|
|
11
|
+
const native = requireNativeBinding();
|
|
12
|
+
const acquire = native.tryAcquireWriteLease?.bind(native);
|
|
13
|
+
const held = native.isFileWriteLeaseHeld?.bind(native);
|
|
14
|
+
const unlock = native.releaseFileWriteLease?.bind(native);
|
|
15
|
+
if (!acquire || !held || !unlock) {
|
|
16
|
+
throw new FsSafeError("helper-unavailable", "Native file write leases are unavailable");
|
|
17
|
+
}
|
|
18
|
+
const call = (operation) => {
|
|
19
|
+
try {
|
|
20
|
+
return operation();
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
throw new FsSafeError("helper-failed", "Native file write lease operation failed", { cause });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
if (!call(() => acquire(fd)))
|
|
27
|
+
return null;
|
|
28
|
+
let released = false;
|
|
29
|
+
const release = () => {
|
|
30
|
+
if (released)
|
|
31
|
+
return;
|
|
32
|
+
// Never retry an ambiguous settlement against a potentially recycled fd.
|
|
33
|
+
released = true;
|
|
34
|
+
call(() => unlock(fd));
|
|
35
|
+
};
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
fd,
|
|
38
|
+
isHeld: () => !released && call(() => held(fd)),
|
|
39
|
+
release,
|
|
40
|
+
[Symbol.dispose]: release,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -92,6 +92,9 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
92
92
|
" try:",
|
|
93
93
|
" os.rename(src_basename + '/', dst_basename, src_dir_fd=src_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
94
94
|
" except OSError as error:",
|
|
95
|
+
" # ENOTDIR can describe the slash-checked source, not just the target.",
|
|
96
|
+
" if error.errno == errno.ENOTDIR:",
|
|
97
|
+
" verify(src_parent_fd, src_basename, 1)",
|
|
95
98
|
" if error.errno in (errno.ENOTEMPTY, errno.ENOTDIR, errno.EEXIST):",
|
|
96
99
|
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), dst_basename) from error",
|
|
97
100
|
" raise",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isHardlinkFallbackError(error: unknown): boolean;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// These refusals leave the destination uncreated. EEXIST is deliberately excluded.
|
|
2
|
+
const HARDLINK_FALLBACK_CODES = new Set(["EACCES", "EPERM", "EXDEV", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
|
|
3
|
+
export function isHardlinkFallbackError(error) {
|
|
4
|
+
return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
|
|
5
|
+
}
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { syncDirectory } from "./directory-durability.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
8
8
|
import { sameFileIdentityForCleanup, sha256Hex } from "./file-identity.js";
|
|
9
|
+
import { isHardlinkFallbackError } from "./hardlink-fallback.js";
|
|
9
10
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
10
11
|
import { serializePathWrite } from "./write-queue.js";
|
|
11
12
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
@@ -61,6 +62,18 @@ async function withQueueEntryLock(paths, run) {
|
|
|
61
62
|
assertNoWindowsPathAlias(resolvedJsonPath);
|
|
62
63
|
return await serializePathWrite(paths.jsonPath, async () => await withQueueTransferLock(resolvedJsonPath, run));
|
|
63
64
|
}
|
|
65
|
+
async function linkQueueGeneration(sourcePath, targetPath) {
|
|
66
|
+
try {
|
|
67
|
+
await fs.link(sourcePath, targetPath);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// Shared inode identity is recovery evidence; a copy cannot replace it.
|
|
71
|
+
if (isHardlinkFallbackError(error)) {
|
|
72
|
+
throw new FsSafeError("helper-unavailable", "durable queue ownership requires hardlink support", { cause: error });
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
64
77
|
async function claimDurableQueueEntryUnlocked(paths, options = {}) {
|
|
65
78
|
const processingPath = paths.processingPath;
|
|
66
79
|
await recoverDurableQueueRetirement({ jsonPath: paths.jsonPath, processingPath });
|
|
@@ -92,7 +105,7 @@ async function claimDurableQueueEntryUnlocked(paths, options = {}) {
|
|
|
92
105
|
throw new FsSafeError("path-mismatch", "queue entry is not exclusively owned");
|
|
93
106
|
}
|
|
94
107
|
try {
|
|
95
|
-
await
|
|
108
|
+
await linkQueueGeneration(paths.jsonPath, processingPath);
|
|
96
109
|
}
|
|
97
110
|
catch (error) {
|
|
98
111
|
if (getErrorCode(error) === "EEXIST") {
|
|
@@ -227,7 +240,7 @@ export async function moveDurableQueueEntryToFailed(params) {
|
|
|
227
240
|
throw new FsSafeError("already-exists", "failed queue destination already exists");
|
|
228
241
|
}
|
|
229
242
|
if (!failed)
|
|
230
|
-
await
|
|
243
|
+
await linkQueueGeneration(sourcePath, failedPath);
|
|
231
244
|
await syncDirectory(path.dirname(failedPath));
|
|
232
245
|
await fs.unlink(sourcePath);
|
|
233
246
|
await syncDirectory(path.dirname(sourcePath));
|
|
@@ -305,7 +318,7 @@ async function recoverDurableQueueRetirement(params) {
|
|
|
305
318
|
if (!sameFileIdentityForCleanup(entry, processing)) {
|
|
306
319
|
const pending = regularQueueFileIdentity(params.jsonPath, "retirement");
|
|
307
320
|
if (!pending) {
|
|
308
|
-
await
|
|
321
|
+
await linkQueueGeneration(entryPath, params.jsonPath);
|
|
309
322
|
await syncDirectory(path.dirname(params.jsonPath));
|
|
310
323
|
}
|
|
311
324
|
else if (sameFileIdentityForCleanup(pending, entry)) {
|
|
@@ -124,6 +124,8 @@ export interface NativeWindowsDirectoryReceipt {
|
|
|
124
124
|
}
|
|
125
125
|
export interface NativeDarwinAclFacts {
|
|
126
126
|
state: "absent" | "empty" | "present";
|
|
127
|
+
inheritsToFiles?: boolean;
|
|
128
|
+
inheritsToDirectories?: boolean;
|
|
127
129
|
}
|
|
128
130
|
type NativeTwoPathArgs = [
|
|
129
131
|
sourceRootFd: number,
|
|
@@ -132,6 +134,37 @@ type NativeTwoPathArgs = [
|
|
|
132
134
|
targetRelPath: string
|
|
133
135
|
];
|
|
134
136
|
export interface NativeBinding {
|
|
137
|
+
readCopyMetadata?(fd: number): Buffer;
|
|
138
|
+
restoreCopyMetadata?(fd: number, snapshot: Buffer): void;
|
|
139
|
+
restoreCopyFileTimes?(fd: number, atimeNs: bigint, mtimeNs: bigint): void;
|
|
140
|
+
copyLinkExclusive?(sourceParent: number, sourceName: string, parent: number, name: string, dev: bigint, ino: bigint, preserveMetadata: boolean, mode: number): {
|
|
141
|
+
dev: bigint;
|
|
142
|
+
ino: bigint;
|
|
143
|
+
};
|
|
144
|
+
publishCopyLink?(parent: number, name: string, destination: string, dev: bigint, ino: bigint): void;
|
|
145
|
+
removeCopyLink?(parent: number, name: string, dev: bigint, ino: bigint): void;
|
|
146
|
+
createCopySymlink?(parent: number, name: string, target: Buffer, mode: number, atimeNs?: bigint, mtimeNs?: bigint): {
|
|
147
|
+
dev: bigint;
|
|
148
|
+
ino: bigint;
|
|
149
|
+
errorCode?: string;
|
|
150
|
+
errorMessage?: string;
|
|
151
|
+
};
|
|
152
|
+
tryAcquireWriteLease?(fd: number): boolean;
|
|
153
|
+
isFileWriteLeaseHeld?(fd: number): boolean;
|
|
154
|
+
releaseFileWriteLease?(fd: number): void;
|
|
155
|
+
holdWindowsSharingLock?(path: string): {
|
|
156
|
+
close(): void;
|
|
157
|
+
};
|
|
158
|
+
setWindowsFileAttributes?(path: string, attrs: {
|
|
159
|
+
readOnly?: boolean;
|
|
160
|
+
hidden?: boolean;
|
|
161
|
+
system?: boolean;
|
|
162
|
+
}): void;
|
|
163
|
+
readWindowsFileExtents?(path: string): {
|
|
164
|
+
vcn: bigint;
|
|
165
|
+
lcn: bigint;
|
|
166
|
+
clusters: bigint;
|
|
167
|
+
}[];
|
|
135
168
|
rootRemovalStat?(parent: number, name: string): NativeRootRemovalEntry;
|
|
136
169
|
rootRemovalUnlink?(parent: number, name: string, dev: bigint, ino: bigint, directory: boolean): void;
|
|
137
170
|
openRootRemovalDirectory?(parent: number, name: string, dev: bigint, ino: bigint): NativeRootRemovalDirectory;
|
|
@@ -169,6 +202,11 @@ export interface NativeBinding {
|
|
|
169
202
|
};
|
|
170
203
|
/** Internal: consumes only a descriptor returned by this binding. */
|
|
171
204
|
closeOwnedFd(fd: number): void;
|
|
205
|
+
createPipe?(): {
|
|
206
|
+
reader: number;
|
|
207
|
+
writer: number;
|
|
208
|
+
atomicCloseOnExec: boolean;
|
|
209
|
+
};
|
|
172
210
|
/** Internal Darwin-only synchronous inspection; the caller retains its fd. */
|
|
173
211
|
inspectDarwinAcl?(fd: number, inheritanceTarget?: "file" | "directory"): NativeDarwinAclFacts;
|
|
174
212
|
/** POSIX system canonicalization; confinement and identity policy stay with callers. */
|
|
@@ -183,18 +221,18 @@ export interface NativeBinding {
|
|
|
183
221
|
/** Linux/Windows byte transfer; callers retain both admitted descriptors until settlement. */
|
|
184
222
|
copyFileContents?(sourceFd: number, targetFd: number, signal?: AbortSignal): Promise<void>;
|
|
185
223
|
/** Internal: same private, immutable input ownership as the ZIP buffer reader. */
|
|
186
|
-
openTarBufferNative(buffer: Buffer, kind: string, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
224
|
+
openTarBufferNative?(buffer: Buffer, kind: string, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
187
225
|
readonly entries: NativeArchiveEntry[];
|
|
188
226
|
readEntry(index: number, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
|
|
189
227
|
}>;
|
|
190
228
|
/** Internal: input remains private, unpooled and immutable until the reader is released. */
|
|
191
|
-
openZipBufferNative(buffer: Buffer, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
229
|
+
openZipBufferNative?(buffer: Buffer, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
192
230
|
readonly entries: NativeArchiveEntry[];
|
|
193
231
|
readEntry(index: number, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
|
|
194
232
|
}>;
|
|
195
|
-
readCloneFileMetadata(paths: string[]): Promise<(Buffer | null)[]>;
|
|
196
|
-
probeTreeClone(parentFd: number): "apfs" | "btrfs" | "refs" | "xfs" | "zfs" | null;
|
|
197
|
-
cloneTree(sourceFd: number | null, parentFd: number, basename: string, concurrency: number, signal?: AbortSignal): Promise<void>;
|
|
233
|
+
readCloneFileMetadata?(paths: string[]): Promise<(Buffer | null)[]>;
|
|
234
|
+
probeTreeClone?(parentFd: number): "apfs" | "btrfs" | "refs" | "xfs" | "zfs" | null;
|
|
235
|
+
cloneTree?(sourceFd: number | null, parentFd: number, basename: string, concurrency: number, signal?: AbortSignal): Promise<void>;
|
|
198
236
|
openStagedSymlink?(parentFd: number, basename: string): number;
|
|
199
237
|
stagedSymlinkTarget?(parentFd: number, basename: string, linkFd: number): string;
|
|
200
238
|
stagedSymlinkMatches?(parentFd: number, basename: string, linkFd: number): boolean;
|
|
@@ -205,28 +243,28 @@ export interface NativeBinding {
|
|
|
205
243
|
removeStagedFile?(parentFd: number, basename: string, fileFd: number): "removed" | "name-absent" | "preserved";
|
|
206
244
|
copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined): Promise<NativeFileCopyResult>;
|
|
207
245
|
copyFileExclusiveSync?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined): NativeFileCopyResult;
|
|
208
|
-
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
209
|
-
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
210
|
-
createPrivateDirectory(path: string): void;
|
|
246
|
+
cloneFileExclusive?(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
247
|
+
copyFileRangeExclusive?(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
248
|
+
createPrivateDirectory?(path: string): void;
|
|
211
249
|
/** Internal Windows creation receipts use full FILE_ID_INFO, never Node's projection. */
|
|
212
250
|
inspectWindowsDirectory?(path: string, requirePrivate: boolean): NativeWindowsDirectoryReceipt;
|
|
213
251
|
createPrivateDirectoryWithParentIdentity?(path: string, expectedParentIdentity: string): NativeWindowsDirectoryReceipt;
|
|
214
252
|
/** Protects an already-private borrowed Node file; descriptor ownership stays with Node. */
|
|
215
253
|
protectPrivateWindowsFile?(fd: number, path: string, expectedParentIdentity: string): NativeWindowsDirectoryReceipt;
|
|
216
254
|
verifyPrivateWindowsFile?(fd: number, path: string, expectedFileIdentity: string, expectedParentIdentity: string, expectedLinks: number): void;
|
|
217
|
-
extractArchiveNative(path: string, kind: string, rootFd: number, plan: NativeArchivePlanEntry[], limits: TarMeterLimits, signal: AbortSignal): Promise<void>;
|
|
218
|
-
fstatIdentity(fd: number): NativeFileIdentity;
|
|
219
|
-
inspectArchiveNative(path: string, kind: string, limits: TarMeterLimits, signal: AbortSignal): Promise<NativeArchiveEntry[]>;
|
|
220
|
-
linkBeneath(...args: NativeTwoPathArgs): void;
|
|
255
|
+
extractArchiveNative?(path: string, kind: string, rootFd: number, plan: NativeArchivePlanEntry[], limits: TarMeterLimits, signal: AbortSignal): Promise<void>;
|
|
256
|
+
fstatIdentity?(fd: number): NativeFileIdentity;
|
|
257
|
+
inspectArchiveNative?(path: string, kind: string, limits: TarMeterLimits, signal: AbortSignal): Promise<NativeArchiveEntry[]>;
|
|
258
|
+
linkBeneath?(...args: NativeTwoPathArgs): void;
|
|
221
259
|
/** Direct-child mkdir; true is receipt provenance only, never cleanup ownership. */
|
|
222
260
|
mkdirChildBeneath?(parentFd: number, basename: string, mode: number): boolean;
|
|
223
261
|
mkdirOpenChildBeneath?(parentFd: number, basename: string, mode: number, flags: number): {
|
|
224
262
|
fd: number;
|
|
225
263
|
created: boolean;
|
|
226
264
|
};
|
|
227
|
-
mkdirBeneath(rootFd: number, relPath: string, mode: number): void;
|
|
228
|
-
openBeneath(rootFd: number, relPath: string, flags: number): NativeOpenBeneathResult;
|
|
229
|
-
readOwnerAndDacl(path: string): NativeWindowsSecurityFacts;
|
|
265
|
+
mkdirBeneath?(rootFd: number, relPath: string, mode: number): void;
|
|
266
|
+
openBeneath?(rootFd: number, relPath: string, flags: number): NativeOpenBeneathResult;
|
|
267
|
+
readOwnerAndDacl?(path: string): NativeWindowsSecurityFacts;
|
|
230
268
|
/** Internal Windows-only inspection of the exact borrowed Node descriptor. */
|
|
231
269
|
inspectWindowsSecureFileHandle?(fd: number): NativeWindowsDescriptorSecurityFacts;
|
|
232
270
|
ownedTreeRemovalAvailable?(parentFd: number): boolean;
|
|
@@ -236,13 +274,14 @@ export interface NativeBinding {
|
|
|
236
274
|
retainWindowsEntryPublication?(sourcePath: string, name: string, sourceDev: bigint, sourceIno: bigint, targetPath: string, targetName: string, targetDev: bigint, targetIno: bigint, dev: bigint, ino: bigint, kind: "file" | "directory" | "symlink"): NativeWindowsEntryPublication;
|
|
237
275
|
entryPublicationFilesystem?(parentFd: number): string;
|
|
238
276
|
publishRetainedEntryNoReplace?(sourceParentFd: number, sourceBasename: string, sourceFd: number, targetParentFd: number, targetBasename: string): NativePublicationTransition;
|
|
239
|
-
renameNoReplace(...args: NativeTwoPathArgs): void;
|
|
277
|
+
renameNoReplace?(...args: NativeTwoPathArgs): void;
|
|
240
278
|
/** Linux auto-mode fallback through already-admitted direct-child parents. */
|
|
241
279
|
moveNoReplaceFallback?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
|
|
242
280
|
/** Identity-fenced retained-directory rename capability. */
|
|
243
281
|
renameNoReplaceWithIdentity?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
|
|
244
|
-
renameReplace(...args: NativeTwoPathArgs): void;
|
|
245
|
-
sha256File(fd: number, maxBytes?: number, signal?: AbortSignal): Promise<NativeFileHash>;
|
|
282
|
+
renameReplace?(...args: NativeTwoPathArgs): void;
|
|
283
|
+
sha256File?(fd: number, maxBytes?: number, signal?: AbortSignal): Promise<NativeFileHash>;
|
|
246
284
|
}
|
|
285
|
+
export type CapableNativeBinding<K extends keyof NativeBinding> = NativeBinding & Required<Pick<NativeBinding, K>>;
|
|
247
286
|
export declare function captureNativeFdClose(binding: NativeBinding): (fd: number) => void;
|
|
248
287
|
export {};
|
|
@@ -5,7 +5,7 @@ import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
|
5
5
|
export function getNativeDirectoryObservationBackend(enabled = true) {
|
|
6
6
|
if (!enabled)
|
|
7
7
|
return undefined;
|
|
8
|
-
const binding = getNativeBinding();
|
|
8
|
+
const binding = getNativeBinding("observeDirectory");
|
|
9
9
|
return binding && typeof binding.observeDirectory === "function"
|
|
10
10
|
? binding
|
|
11
11
|
: undefined;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BigIntStats } from "node:fs";
|
|
2
2
|
import type { NativeBinding } from "./native-binding.js";
|
|
3
|
-
export declare function nativeMoveNoReplace(binding: NativeBinding, paths: Parameters<NativeBinding["renameNoReplace"]
|
|
3
|
+
export declare function nativeMoveNoReplace(binding: NativeBinding, paths: Parameters<NonNullable<NativeBinding["renameNoReplace"]>>, expected: BigIntStats | undefined, allowFallback: boolean, operation?: string): "link-unlink" | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { assertNativeCapabilities } from "./native.js";
|
|
2
3
|
import { cachedNoReplaceUnavailable, noReplaceUnavailable, rememberNoReplaceUnavailable, } from "./native-noreplace.js";
|
|
3
4
|
function fallbackError(error, operation) {
|
|
4
5
|
const code = error?.code;
|
|
@@ -24,6 +25,7 @@ function fallbackError(error, operation) {
|
|
|
24
25
|
return error;
|
|
25
26
|
}
|
|
26
27
|
export function nativeMoveNoReplace(binding, paths, expected, allowFallback, operation = "move") {
|
|
28
|
+
assertNativeCapabilities(binding, "renameNoReplace");
|
|
27
29
|
const [sourceFd, source, targetFd, target] = paths;
|
|
28
30
|
let unavailable = cachedNoReplaceUnavailable(binding, sourceFd, operation);
|
|
29
31
|
if (!unavailable) {
|
|
@@ -32,7 +32,7 @@ export function removeNativeCreatedFileIfStillPinned(params) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
export async function createNativeExclusiveFile(targetPath, mode) {
|
|
35
|
-
const binding = getNativeBinding();
|
|
35
|
+
const binding = getNativeBinding("openBeneath");
|
|
36
36
|
if (!binding) {
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
2
|
import type { FileHandle } from "node:fs/promises";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type NativeBinding } from "./native.js";
|
|
5
5
|
import { describeStagedDirectory } from "./staged-directory.js";
|
|
6
6
|
type RootIdentity = Pick<FileIdentityStat, "dev" | "ino">;
|
|
7
7
|
export type NativeRootAdmission = {
|