@openclaw/openshell-sandbox 2026.10.6 → 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/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
|
@@ -56,12 +56,27 @@ import { runPinnedWriteWindows } from "./native-pinned-write-windows.js";
|
|
|
56
56
|
import { capturePolicyAwareNativeParent } from "./native-policy-parent.js";
|
|
57
57
|
import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
|
|
58
58
|
import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
|
|
59
|
+
import { assertNativeCapabilities, getNativeBinding } from "./native.js";
|
|
59
60
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
60
61
|
import { cachedNoReplaceUnavailable } from "./native-noreplace.js";
|
|
61
62
|
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
63
|
+
import { copyLinkAtParent } from "./copy-link.js";
|
|
64
|
+
const writeCapabilities = ["openBeneath", "mkdirBeneath", "renameNoReplace", "renameReplace"];
|
|
65
|
+
const posixWriteCapabilities = [...writeCapabilities, "createStagedFile", "stagedFileMatches", "removeStagedFile"];
|
|
66
|
+
function pinnedWriteCapabilities(params) {
|
|
67
|
+
const windows = process.platform === "win32";
|
|
68
|
+
const capabilities = windows ? [...writeCapabilities, "fstatIdentity"] : posixWriteCapabilities;
|
|
69
|
+
const nativeCopy = params.input.kind === "file" && !params.private &&
|
|
70
|
+
(windows || !params.verifyPosixMode);
|
|
71
|
+
return nativeCopy ? [...capabilities, "copyFileExclusive"] : capabilities;
|
|
72
|
+
}
|
|
73
|
+
export function getPinnedWriteNativeBinding(params) {
|
|
74
|
+
return getNativeBinding(...pinnedWriteCapabilities(params));
|
|
75
|
+
}
|
|
62
76
|
export async function runPinnedWriteNative(binding, params, fallback) {
|
|
63
77
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
64
78
|
try {
|
|
79
|
+
assertNativeCapabilities(binding, ...pinnedWriteCapabilities(params));
|
|
65
80
|
const closeFd = captureNativeFdClose(binding);
|
|
66
81
|
const windows = process.platform === "win32";
|
|
67
82
|
if (!windows) {
|
|
@@ -112,6 +127,9 @@ export async function runPinnedWriteNative(binding, params, fallback) {
|
|
|
112
127
|
parentPathStat = admitted.guard.stat;
|
|
113
128
|
}
|
|
114
129
|
const verificationGuard = { dir: parentPath, realPath: parentPath, stat: parentPathStat };
|
|
130
|
+
if (params.input.kind === "link") {
|
|
131
|
+
return await copyLinkAtParent(params, params.input, verificationGuard, { binding, fd: parentFd });
|
|
132
|
+
}
|
|
115
133
|
if (params.overwrite === false) {
|
|
116
134
|
try {
|
|
117
135
|
fsSync.lstatSync(path.join(parentPath, params.basename));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BigIntStats } from "node:fs";
|
|
2
2
|
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type NativeBinding } from "./native.js";
|
|
4
4
|
import type { NativeRootAdmission } from "./native-parent-admission.js";
|
|
5
5
|
import type { PinnedWriteParams } from "./pinned-write-types.js";
|
|
6
6
|
import { describeStagedDirectory, type PolicyStagedDirectory } from "./staged-directory.js";
|
|
@@ -54,6 +54,7 @@ import fsSync, {} from "node:fs";
|
|
|
54
54
|
import path from "node:path";
|
|
55
55
|
import { inspectDirectoryIdentity, inspectDirectoryIdentitySync, } from "./directory-guard.js";
|
|
56
56
|
import { FsSafeError } from "./errors.js";
|
|
57
|
+
import { assertNativeCapabilities } from "./native.js";
|
|
57
58
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
58
59
|
import { NativePolicyDirectoryMismatch } from "./native-policy-directory-observation.js";
|
|
59
60
|
import { assertWindowsPolicyParentCurrent, closeWindowsPolicyParentAfterFailure, openWindowsPolicyParent, windowsParentObservation, } from "./native-policy-parent-windows.js";
|
|
@@ -76,6 +77,7 @@ function mkdirPosixPolicyChild(binding, parentFd, basename) {
|
|
|
76
77
|
if (typeof created === "boolean")
|
|
77
78
|
return created;
|
|
78
79
|
}
|
|
80
|
+
assertNativeCapabilities(binding, "mkdirBeneath");
|
|
79
81
|
binding.mkdirBeneath(parentFd, basename, 0o777);
|
|
80
82
|
return false;
|
|
81
83
|
}
|
|
@@ -122,6 +124,7 @@ function tryDescribePolicyPosixParent(binding, fd, pathname) {
|
|
|
122
124
|
export async function capturePolicyAwareNativeParent(binding, params, rootAdmission, windows, directoryFlags, fuseChildCreation = false) {
|
|
123
125
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
124
126
|
try {
|
|
127
|
+
assertNativeCapabilities(binding, "openBeneath");
|
|
125
128
|
const rootFd = rootAdmission.root.fd;
|
|
126
129
|
const closeFd = captureNativeFdClose(binding);
|
|
127
130
|
const observationDisposers = windows ? undefined : new Map();
|
|
@@ -5,7 +5,7 @@ import type { FileIdentityStat } from "./file-identity.js";
|
|
|
5
5
|
import { type NativeBinding } from "./native-binding.js";
|
|
6
6
|
import type { PinnedWriteInput, PinnedWriteParams } from "./pinned-write-types.js";
|
|
7
7
|
import type { PublishedFileReceipt, StagedFile, StagedFileCleanupReceipt, StagedFileReceipt } from "./staged-file-types.js";
|
|
8
|
-
export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding, "createStagedFile" | "stagedFileMatches" | "removeStagedFile">>;
|
|
8
|
+
export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding, "createStagedFile" | "stagedFileMatches" | "removeStagedFile" | "renameReplace" | "renameNoReplace">>;
|
|
9
9
|
export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
|
|
10
10
|
type StagedPermissionPolicy = "private-creation" | "mode-only";
|
|
11
11
|
type NativeStageOwner = Readonly<{
|
|
@@ -67,6 +67,7 @@ import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
|
|
|
67
67
|
import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
|
|
68
68
|
import { noReplaceUnavailable, rememberNoReplaceUnavailable } from "./native-noreplace.js";
|
|
69
69
|
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
70
|
+
import { restoreCopyMetadata } from "./copy-metadata.js";
|
|
70
71
|
export function assertNativeStaging(binding) {
|
|
71
72
|
if ([
|
|
72
73
|
binding.closeOwnedFd,
|
|
@@ -299,6 +300,8 @@ class NativeStagedFile {
|
|
|
299
300
|
} : this.#assertBeforeMutation;
|
|
300
301
|
if (!copied)
|
|
301
302
|
await writePinnedInput(fd, input, maxBytes, assertBeforeMutation);
|
|
303
|
+
if (input.kind === "file")
|
|
304
|
+
restoreCopyMetadata(fd, input.metadata);
|
|
302
305
|
if (this.#verifyMode)
|
|
303
306
|
this.#assertStagePermissions(fd);
|
|
304
307
|
if (this.#owner.sync !== false) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NativeBinding } from "./native-binding.js";
|
|
1
|
+
import { type NativeBinding, type CapableNativeBinding } from "./native-binding.js";
|
|
2
2
|
export type { NativeBinding } from "./native-binding.js";
|
|
3
3
|
export declare function __loadBundledNativeForTest(): NativeBinding;
|
|
4
4
|
export declare function __nativeLoaderDetectorsForTest(): {
|
|
@@ -7,7 +7,9 @@ export declare function __nativeLoaderDetectorsForTest(): {
|
|
|
7
7
|
elfInterpreter: boolean | undefined;
|
|
8
8
|
};
|
|
9
9
|
export declare function __nativeTargetForTest(platform: NodeJS.Platform, arch: string, musl?: boolean): string | undefined;
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function
|
|
10
|
+
export declare function selectNativeBinding<K extends keyof NativeBinding>(native: NativeBinding | undefined, ...capabilities: K[]): CapableNativeBinding<K> | undefined;
|
|
11
|
+
export declare function assertNativeCapabilities<K extends keyof NativeBinding>(native: NativeBinding, ...capabilities: K[]): asserts native is CapableNativeBinding<K>;
|
|
12
|
+
export declare function getNativeBinding<K extends keyof NativeBinding = never>(...capabilities: K[]): CapableNativeBinding<K> | undefined;
|
|
13
|
+
export declare function requireNativeBinding<K extends keyof NativeBinding = never>(...capabilities: K[]): CapableNativeBinding<K>;
|
|
12
14
|
export declare function __setNativeLoaderForTest(loader: () => NativeBinding): void;
|
|
13
15
|
export declare function __resetNativeLoaderForTest(): void;
|
|
@@ -127,12 +127,14 @@ function isMusl() {
|
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
129
|
function targetFor(platform, arch, musl) {
|
|
130
|
-
if (platform === "win32" && arch === "x64")
|
|
131
|
-
return
|
|
130
|
+
if (platform === "win32" && (arch === "x64" || arch === "arm64"))
|
|
131
|
+
return `win32-${arch}-msvc`;
|
|
132
132
|
if (platform === "darwin" && arch === "x64")
|
|
133
133
|
return "darwin-x64";
|
|
134
134
|
if (platform === "darwin" && arch === "arm64")
|
|
135
135
|
return "darwin-arm64";
|
|
136
|
+
if (platform === "freebsd" && (arch === "x64" || arch === "arm64"))
|
|
137
|
+
return `freebsd-${arch}`;
|
|
136
138
|
if (platform === "linux" && (arch === "x64" || arch === "arm64")) {
|
|
137
139
|
return `linux-${arch}-${musl ? "musl" : "gnu"}`;
|
|
138
140
|
}
|
|
@@ -166,7 +168,24 @@ export function __nativeLoaderDetectorsForTest() {
|
|
|
166
168
|
export function __nativeTargetForTest(platform, arch, musl = false) {
|
|
167
169
|
return targetFor(platform, arch, musl);
|
|
168
170
|
}
|
|
169
|
-
export function
|
|
171
|
+
export function selectNativeBinding(native, ...capabilities) {
|
|
172
|
+
if (!native)
|
|
173
|
+
return undefined;
|
|
174
|
+
const missing = capabilities.filter((name) => typeof native[name] !== "function");
|
|
175
|
+
if (missing.length === 0)
|
|
176
|
+
return native;
|
|
177
|
+
if (getFsSafeNativeConfig().mode === "require") {
|
|
178
|
+
throw new FsSafeError("helper-unavailable", `native fs-safe capabilities unavailable: ${missing.join(", ")}`);
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
export function assertNativeCapabilities(native, ...capabilities) {
|
|
183
|
+
const missing = capabilities.filter((name) => typeof native[name] !== "function");
|
|
184
|
+
if (missing.length) {
|
|
185
|
+
throw new FsSafeError("helper-unavailable", `native fs-safe capabilities unavailable: ${missing.join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function getNativeBinding(...capabilities) {
|
|
170
189
|
const { mode } = getFsSafeNativeConfig();
|
|
171
190
|
if (mode === "off")
|
|
172
191
|
return undefined;
|
|
@@ -182,7 +201,7 @@ export function getNativeBinding() {
|
|
|
182
201
|
}
|
|
183
202
|
}
|
|
184
203
|
if (binding)
|
|
185
|
-
return binding;
|
|
204
|
+
return selectNativeBinding(binding, ...capabilities);
|
|
186
205
|
if (mode === "require") {
|
|
187
206
|
throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
|
|
188
207
|
cause: loadError,
|
|
@@ -190,8 +209,8 @@ export function getNativeBinding() {
|
|
|
190
209
|
}
|
|
191
210
|
return undefined;
|
|
192
211
|
}
|
|
193
|
-
export function requireNativeBinding() {
|
|
194
|
-
const native = getNativeBinding();
|
|
212
|
+
export function requireNativeBinding(...capabilities) {
|
|
213
|
+
const native = getNativeBinding(...capabilities);
|
|
195
214
|
if (!native) {
|
|
196
215
|
throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
|
|
197
216
|
cause: loadError,
|
|
@@ -26,7 +26,15 @@ export async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
26
26
|
: [];
|
|
27
27
|
for (const fdPath of fdCandidates) {
|
|
28
28
|
try {
|
|
29
|
-
|
|
29
|
+
// Linux procfs already reports the descriptor's physical pathname. Keep
|
|
30
|
+
// the fresh identity check without walking that canonical path again.
|
|
31
|
+
const fdLink = fdPath.startsWith("/proc/") ? fsSync.readlinkSync(fdPath) : undefined;
|
|
32
|
+
// The deletion suffix can also name a live entry or a symlink. Preserve
|
|
33
|
+
// canonicalization for that ambiguous spelling, including literal names.
|
|
34
|
+
const fdRealPath = fdLink !== undefined && !fdLink.endsWith(" (deleted)")
|
|
35
|
+
? fdLink : realpathSync.native(fdPath);
|
|
36
|
+
if (!path.isAbsolute(fdRealPath))
|
|
37
|
+
continue;
|
|
30
38
|
const fdRealStat = statOptions ? fsSync.statSync(fdRealPath, statOptions) : fsSync.statSync(fdRealPath);
|
|
31
39
|
if (sameFileIdentity(handleStat, fdRealStat)) {
|
|
32
40
|
return { realPath: fdRealPath, stat: fdRealStat };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
3
4
|
import { readFileWindowFully } from "./positional-read.js";
|
|
@@ -10,7 +11,9 @@ export async function overwriteFileHandle(handle, data, options = {}) {
|
|
|
10
11
|
const beforeWrite = options.beforeWrite;
|
|
11
12
|
const payload = data.subarray(0, data.byteLength);
|
|
12
13
|
const payloadBytes = payload.byteLength;
|
|
13
|
-
const
|
|
14
|
+
const fd = handle.fd;
|
|
15
|
+
// Keep FileHandle's closed-handle error while avoiding a metadata I/O hop.
|
|
16
|
+
const stat = fd < 0 ? await handle.stat() : fs.fstatSync(fd);
|
|
14
17
|
if (!stat.isFile()) {
|
|
15
18
|
throw new FsSafeError("not-file", "overwrite handle must be a regular file");
|
|
16
19
|
}
|
|
@@ -3,3 +3,4 @@ export type { PermissionCommandFailure } from "./permission-exec.js";
|
|
|
3
3
|
export { createPrivateDirectory, type CreatePrivateDirectoryOptions, } from "./private-directory.js";
|
|
4
4
|
export { readOwnerAndDacl, type OwnerAndDaclResult, type WindowsAccessControlEntry, type WindowsAceFlags, } from "./owner-dacl.js";
|
|
5
5
|
export { readOwnerAndDaclBatch } from "./owner-dacl-batch.js";
|
|
6
|
+
export { inspectDarwinAcl, type DarwinAclInspection } from "./darwin-acl.js";
|
|
@@ -2,3 +2,4 @@ export { formatOctal, formatPermissionDetail, formatPermissionRemediation, inspe
|
|
|
2
2
|
export { createPrivateDirectory, } from "./private-directory.js";
|
|
3
3
|
export { readOwnerAndDacl, } from "./owner-dacl.js";
|
|
4
4
|
export { readOwnerAndDaclBatch } from "./owner-dacl-batch.js";
|
|
5
|
+
export { inspectDarwinAcl } from "./darwin-acl.js";
|
|
@@ -40,30 +40,25 @@ const normalize = (value) => normalizeLowercaseStringOrEmpty(value);
|
|
|
40
40
|
const defaultWindowsUserInfo = () => os.userInfo();
|
|
41
41
|
const defaultPermissionExec = executePermissionCommand;
|
|
42
42
|
function inspectWindowsPermissionsNative(targetPath, unverified) {
|
|
43
|
-
const native = getNativeBinding();
|
|
43
|
+
const native = getNativeBinding("readOwnerAndDacl");
|
|
44
44
|
if (!native)
|
|
45
45
|
return undefined;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (facts.fallbackRequired)
|
|
49
|
-
return undefined;
|
|
50
|
-
return {
|
|
51
|
-
...unverified,
|
|
52
|
-
source: "windows-acl",
|
|
53
|
-
worldWritable: facts.worldWritable,
|
|
54
|
-
groupWritable: facts.groupWritable,
|
|
55
|
-
worldReadable: facts.worldReadable,
|
|
56
|
-
groupReadable: facts.groupReadable,
|
|
57
|
-
ownerSid: facts.ownerSid,
|
|
58
|
-
ownerTrusted: facts.ownerClass !== "foreign",
|
|
59
|
-
aclSummary: `native owner=${facts.ownerClass} world=` +
|
|
60
|
-
`${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
|
|
61
|
-
`group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
46
|
+
const facts = native.readOwnerAndDacl(targetPath);
|
|
47
|
+
if (facts.fallbackRequired)
|
|
65
48
|
return undefined;
|
|
66
|
-
|
|
49
|
+
return {
|
|
50
|
+
...unverified,
|
|
51
|
+
source: "windows-acl",
|
|
52
|
+
worldWritable: facts.worldWritable,
|
|
53
|
+
groupWritable: facts.groupWritable,
|
|
54
|
+
worldReadable: facts.worldReadable,
|
|
55
|
+
groupReadable: facts.groupReadable,
|
|
56
|
+
ownerSid: facts.ownerSid,
|
|
57
|
+
ownerTrusted: facts.ownerClass !== "foreign",
|
|
58
|
+
aclSummary: `native owner=${facts.ownerClass} world=` +
|
|
59
|
+
`${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
|
|
60
|
+
`group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
|
|
61
|
+
};
|
|
67
62
|
}
|
|
68
63
|
export async function inspectWindowsPermissions(targetPath, unverified, opts) {
|
|
69
64
|
if (hasWindowsPathAlias(targetPath, "filesystem", "win32")) {
|
|
@@ -263,32 +258,26 @@ export async function inspectWindowsAcl(targetPath, opts) {
|
|
|
263
258
|
}
|
|
264
259
|
let owner;
|
|
265
260
|
if (process.platform === "win32" && opts?.env === undefined && opts?.exec === undefined) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// Preserve this inspector's structured fallback and command diagnostics
|
|
291
|
-
// when the optional helper or native descriptor query is unavailable.
|
|
261
|
+
const native = getNativeBinding("readOwnerAndDacl");
|
|
262
|
+
const stat = native && await safeStat(targetPath);
|
|
263
|
+
// Keep the named-query behavior for leaf links; the native reader follows them.
|
|
264
|
+
const facts = stat?.ok && !stat.isSymlink ? native?.readOwnerAndDacl(targetPath) : undefined;
|
|
265
|
+
if (facts && !facts.fallbackRequired && facts.isLocal && facts.aceListComplete && facts.unsupportedAceTypes.length === 0 &&
|
|
266
|
+
facts.aces.every(ace => ace.flags.inherited && ace.mask !== 0)) {
|
|
267
|
+
// .NET normalizes explicit ACEs, but retains nonzero inherited ACEs in
|
|
268
|
+
// their original order without merging them. Other forms keep its query.
|
|
269
|
+
// Adapt descriptor facts only: the public summary owns its SID and rights
|
|
270
|
+
// classification, which differs from the native secure-read policy.
|
|
271
|
+
owner = {
|
|
272
|
+
daclPresent: facts.daclPresent,
|
|
273
|
+
currentUserSid: facts.currentUserSid,
|
|
274
|
+
aces: facts.aces.map(ace => ({
|
|
275
|
+
sid: ace.sid,
|
|
276
|
+
mask: ace.mask,
|
|
277
|
+
deny: ace.aceType === "deny",
|
|
278
|
+
inheritOnly: ace.flags.inheritOnly,
|
|
279
|
+
})),
|
|
280
|
+
};
|
|
292
281
|
}
|
|
293
282
|
}
|
|
294
283
|
owner ??= await inspectWindowsOwner({ targetPath, env: opts?.env, exec: opts?.exec ?? defaultPermissionExec });
|
|
@@ -29,6 +29,8 @@ function streamChunkBytes(chunk) {
|
|
|
29
29
|
return snapshotByteView(chunk);
|
|
30
30
|
}
|
|
31
31
|
export async function writePinnedInput(target, input, maxBytes, assertBeforeMutation) {
|
|
32
|
+
if (input.kind === "link")
|
|
33
|
+
throw new FsSafeError("helper-failed", "link copying requires the link publication path");
|
|
32
34
|
if (input.kind === "file") {
|
|
33
35
|
await writeCopyFileToFd(typeof target === "number" ? target : target.fd, input, maxBytes, assertBeforeMutation);
|
|
34
36
|
return;
|
|
@@ -18,6 +18,7 @@ import { cleanupPinnedFilePath } from "./file-cleanup.js";
|
|
|
18
18
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
19
19
|
import { createStagedFileReceipt, settleStagedFile } from "./staged-file-settlement.js";
|
|
20
20
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
21
|
+
import { restoreCopyMetadata } from "./copy-metadata.js";
|
|
21
22
|
export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
|
|
22
23
|
const verifyPosixMode = process.platform !== "win32" &&
|
|
23
24
|
(params.verifyPosixMode === true || params.private === true);
|
|
@@ -66,6 +67,8 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
|
|
|
66
67
|
}
|
|
67
68
|
const expectedTempStat = tempStat;
|
|
68
69
|
await handle.chmod(params.mode);
|
|
70
|
+
if (params.input.kind === "file")
|
|
71
|
+
restoreCopyMetadata(handle.fd, params.input.metadata);
|
|
69
72
|
if (verifyPosixMode) {
|
|
70
73
|
assertPinnedWriteMode(handle.fd, params.mode, params.private);
|
|
71
74
|
assertBeforeMutation = pinnedWriteModeAssertion(handle.fd, params.mode, params.assertBeforeMutation, params.private);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CopyFileInput } from "./copy-file-input.js";
|
|
2
|
+
import type { CopyLinkInput } from "./copy-link.js";
|
|
2
3
|
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
4
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
5
|
import type { MutationDirectoryObservation } from "./pinned-mutation-observation.js";
|
|
@@ -16,7 +17,7 @@ export type PinnedWriteInput = {
|
|
|
16
17
|
kind: "stream";
|
|
17
18
|
stream: AsyncIterable<Uint8Array | string>;
|
|
18
19
|
stageBeforePublish?: boolean;
|
|
19
|
-
} | CopyFileInput;
|
|
20
|
+
} | CopyFileInput | CopyLinkInput;
|
|
20
21
|
export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
21
22
|
export type PublishedWriteIdentity = Readonly<{
|
|
22
23
|
dev: bigint;
|
|
@@ -12,8 +12,7 @@ import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
|
12
12
|
import { sha256Hex } from "./file-identity.js";
|
|
13
13
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
14
14
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
15
|
-
import { runPinnedWriteNative } from "./native-pinned-write.js";
|
|
16
|
-
import { getNativeBinding } from "./native.js";
|
|
15
|
+
import { getPinnedWriteNativeBinding, runPinnedWriteNative } from "./native-pinned-write.js";
|
|
17
16
|
import { validatePinnedRelativePath } from "./pinned-operation.js";
|
|
18
17
|
import { cleanupPinnedFilePath } from "./file-cleanup.js";
|
|
19
18
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
@@ -23,6 +22,7 @@ import { assertPinnedWriteMode, preparePinnedWriteMode } from "./pinned-write-mo
|
|
|
23
22
|
import { runPinnedStagedWrite } from "./pinned-write-staged.js";
|
|
24
23
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
25
24
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
25
|
+
import { copyLinkAtParent } from "./copy-link.js";
|
|
26
26
|
function assertSafeBasename(basename) {
|
|
27
27
|
if (!basename ||
|
|
28
28
|
basename === "." ||
|
|
@@ -88,7 +88,7 @@ export async function runPinnedWriteHelper(params) {
|
|
|
88
88
|
if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
|
|
89
89
|
return await runPinnedWriteFallback(normalizedParams);
|
|
90
90
|
}
|
|
91
|
-
const native =
|
|
91
|
+
const native = getPinnedWriteNativeBinding(normalizedParams);
|
|
92
92
|
if (native) {
|
|
93
93
|
return await runPinnedWriteNative(native, normalizedParams, input => runPinnedWriteFallback({ ...normalizedParams, input }));
|
|
94
94
|
}
|
|
@@ -225,6 +225,8 @@ async function runPinnedWriteFallback(params) {
|
|
|
225
225
|
}));
|
|
226
226
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
227
227
|
}
|
|
228
|
+
if (params.input.kind === "link")
|
|
229
|
+
return await copyLinkAtParent(params, params.input, parentGuard);
|
|
228
230
|
if (params.overwrite === false && params.input.kind !== "file" && !params.input.stageBeforePublish) {
|
|
229
231
|
const assertBeforeMutation = () => {
|
|
230
232
|
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
2
|
+
export type OwnedPipeSync = {
|
|
3
|
+
readonly reader: OwnedFileDescriptorSync;
|
|
4
|
+
readonly writer: OwnedFileDescriptorSync;
|
|
5
|
+
/** True when close-on-exec was set atomically at creation. */
|
|
6
|
+
readonly atomicCloseOnExec: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Creates a POSIX anonymous pipe with two native-owned, close-on-exec ends. */
|
|
9
|
+
export declare function createPipe(): OwnedPipeSync;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ownFileDescriptorSync } from "./create-owned-file.js";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
4
|
+
import { requireNativeBinding } from "./native.js";
|
|
5
|
+
/** Creates a POSIX anonymous pipe with two native-owned, close-on-exec ends. */
|
|
6
|
+
export function createPipe() {
|
|
7
|
+
if (!["linux", "darwin", "freebsd"].includes(process.platform)) {
|
|
8
|
+
throw new FsSafeError("unsupported-platform", "anonymous pipes require Linux, Darwin, or FreeBSD");
|
|
9
|
+
}
|
|
10
|
+
const native = requireNativeBinding();
|
|
11
|
+
if (!native.createPipe) {
|
|
12
|
+
throw new FsSafeError("helper-unavailable", "native anonymous pipes are unavailable");
|
|
13
|
+
}
|
|
14
|
+
const close = captureNativeFdClose(native);
|
|
15
|
+
try {
|
|
16
|
+
const { reader, writer, atomicCloseOnExec } = native.createPipe();
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
reader: ownFileDescriptorSync(reader, close),
|
|
19
|
+
writer: ownFileDescriptorSync(writer, close),
|
|
20
|
+
atomicCloseOnExec,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
throw new FsSafeError("helper-failed", "anonymous pipe creation failed", { cause });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -2,6 +2,7 @@ import fsSync, {} from "node:fs";
|
|
|
2
2
|
import fs, {} from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
|
+
import { isHardlinkFallbackError } from "./hardlink-fallback.js";
|
|
5
6
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
6
7
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
7
8
|
import { errorCauseOptions } from "./root-errors.js";
|
|
@@ -39,11 +40,7 @@ export function normalizeLinkError(error) {
|
|
|
39
40
|
if (code === "EEXIST") {
|
|
40
41
|
return new FsSafeError("already-exists", "isolated producer sibling already exists", errorCauseOptions(error));
|
|
41
42
|
}
|
|
42
|
-
if (
|
|
43
|
-
code === "ENOSYS" ||
|
|
44
|
-
code === "ENOTSUP" ||
|
|
45
|
-
code === "EOPNOTSUPP" ||
|
|
46
|
-
code === "EPERM") {
|
|
43
|
+
if (isHardlinkFallbackError(error)) {
|
|
47
44
|
return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", errorCauseOptions(error));
|
|
48
45
|
}
|
|
49
46
|
return error;
|
|
@@ -2,7 +2,10 @@ import fs, {} from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertSyncDirectoryGuard } from "./directory-guard.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
5
6
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
|
+
import { hasErrorCode } from "./file-cleanup.js";
|
|
8
|
+
import { isHardlinkFallbackError } from "./hardlink-fallback.js";
|
|
6
9
|
// Node has no portable no-replace rename. Publish the completed stage by link,
|
|
7
10
|
// then remove its verified temporary name without yielding.
|
|
8
11
|
export function publishCopyStage(params) {
|
|
@@ -27,7 +30,16 @@ export function publishCopyStage(params) {
|
|
|
27
30
|
throw new FsSafeError("already-exists", "destination already exists");
|
|
28
31
|
}
|
|
29
32
|
params.onPublicationAttempt?.();
|
|
30
|
-
|
|
33
|
+
let renamed = false;
|
|
34
|
+
try {
|
|
35
|
+
fs.linkSync(temporaryPath, targetPath);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (!isHardlinkFallbackError(error))
|
|
39
|
+
throw error;
|
|
40
|
+
publishThroughPlaceholder(params, assertCurrent);
|
|
41
|
+
renamed = true;
|
|
42
|
+
}
|
|
31
43
|
let observerRejected = false;
|
|
32
44
|
let observerError;
|
|
33
45
|
try {
|
|
@@ -38,16 +50,29 @@ export function publishCopyStage(params) {
|
|
|
38
50
|
observerError = error;
|
|
39
51
|
}
|
|
40
52
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
if (renamed) {
|
|
54
|
+
assertSyncDirectoryGuard(params.parentGuard);
|
|
55
|
+
if (fs.lstatSync(temporaryPath, { throwIfNoEntry: false })) {
|
|
56
|
+
throw new FsSafeError("path-mismatch", "renamed copy stage still exists");
|
|
57
|
+
}
|
|
58
|
+
const target = fs.lstatSync(targetPath, { bigint: true });
|
|
59
|
+
if (!target.isFile() || target.isSymbolicLink() || target.nlink !== 1n ||
|
|
60
|
+
!sameFileIdentityForCleanup(target, identity)) {
|
|
61
|
+
throw new FsSafeError("path-mismatch", "renamed copy target changed after publication");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
|
|
66
|
+
const current = fs.lstatSync(temporaryPath, { bigint: true });
|
|
67
|
+
if (parent.isSymbolicLink() || !parent.isDirectory() ||
|
|
68
|
+
!sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
|
|
69
|
+
current.isSymbolicLink() || !current.isFile() ||
|
|
70
|
+
!sameFileIdentityForCleanup(current, identity)) {
|
|
71
|
+
throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
|
|
72
|
+
}
|
|
73
|
+
// This removes the stage's name, never the published destination or source.
|
|
74
|
+
fs.unlinkSync(temporaryPath);
|
|
48
75
|
}
|
|
49
|
-
// This removes the stage's name, never the published destination or source.
|
|
50
|
-
fs.unlinkSync(temporaryPath);
|
|
51
76
|
}
|
|
52
77
|
catch (error) {
|
|
53
78
|
throw new FsSafeError("helper-failed", "published copy temporary cleanup failed", {
|
|
@@ -58,3 +83,60 @@ export function publishCopyStage(params) {
|
|
|
58
83
|
if (observerRejected)
|
|
59
84
|
throw observerError;
|
|
60
85
|
}
|
|
86
|
+
function isEmptyPlaceholder(current, identity) {
|
|
87
|
+
return current.isFile() && !current.isSymbolicLink() && current.nlink === 1n &&
|
|
88
|
+
current.size === 0n && sameFileIdentityForCleanup(current, identity);
|
|
89
|
+
}
|
|
90
|
+
function publishThroughPlaceholder(params, assertCurrent) {
|
|
91
|
+
const { targetPath, temporaryPath } = params;
|
|
92
|
+
// An empty placeholder can be observed before the completed stage atomically replaces it.
|
|
93
|
+
// The stage proved directory write access; a real permission denial still fails O_EXCL.
|
|
94
|
+
assertCurrent();
|
|
95
|
+
let fd;
|
|
96
|
+
try {
|
|
97
|
+
assertExclusiveCreateLeaf(targetPath);
|
|
98
|
+
fd = fs.openSync(targetPath, fs.constants.O_WRONLY | fs.constants.O_CREAT |
|
|
99
|
+
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (hasErrorCode(error, "EEXIST")) {
|
|
103
|
+
throw new FsSafeError("already-exists", "destination already exists", { cause: error });
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
let placeholder;
|
|
108
|
+
try {
|
|
109
|
+
try {
|
|
110
|
+
placeholder = fs.fstatSync(fd, { bigint: true });
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
fs.closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
assertCurrent();
|
|
116
|
+
const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
|
|
117
|
+
if (!current || !isEmptyPlaceholder(current, placeholder)) {
|
|
118
|
+
throw new FsSafeError("path-mismatch", "exclusive copy placeholder changed before publication");
|
|
119
|
+
}
|
|
120
|
+
fs.renameSync(temporaryPath, targetPath);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
try {
|
|
124
|
+
assertSyncDirectoryGuard(params.parentGuard);
|
|
125
|
+
const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
|
|
126
|
+
if (current) {
|
|
127
|
+
if (!placeholder)
|
|
128
|
+
throw new FsSafeError("path-mismatch", "copy placeholder identity unavailable for cleanup");
|
|
129
|
+
if (isEmptyPlaceholder(current, placeholder))
|
|
130
|
+
fs.unlinkSync(targetPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (cleanupError) {
|
|
134
|
+
throw new FsSafeError("helper-failed", "unpublished copy placeholder cleanup failed", {
|
|
135
|
+
cause: new AggregateError([error, cleanupError], "publication and cleanup failed"),
|
|
136
|
+
details: { publication: "not-published", path: targetPath, cleanup: "failed",
|
|
137
|
+
...(placeholder ? { dev: placeholder.dev, ino: placeholder.ino } : {}) },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type BigIntStats, type Stats } from "node:fs";
|
|
|
2
2
|
import { type DirectoryReceipt, type DirectorySyncOutcome } from "./directory-durability.js";
|
|
3
3
|
import { type FileIdentityStat } from "./file-identity.js";
|
|
4
4
|
import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failure.js";
|
|
5
|
+
export { isHardlinkFallbackError } from "./hardlink-fallback.js";
|
|
5
6
|
export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
|
|
6
7
|
export type PublishFileExclusiveResult = {
|
|
7
8
|
method: "hardlink" | "exclusive-copy" | "rename-noreplace";
|
|
@@ -10,7 +11,6 @@ export type PublishFileExclusiveResult = {
|
|
|
10
11
|
identity: Stats;
|
|
11
12
|
directorySync: DirectorySyncOutcome;
|
|
12
13
|
};
|
|
13
|
-
export declare function isHardlinkFallbackError(error: unknown): boolean;
|
|
14
14
|
export declare function publishFileExclusive(params: {
|
|
15
15
|
sourcePath: string;
|
|
16
16
|
targetPath: string;
|