@openclaw/openshell-sandbox 2026.10.2 → 2026.10.5-beta.2
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 +69 -111
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +35 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +8 -5
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +27 -50
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
- package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
- package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +34 -7
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +15 -15
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
- package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/archive.md +3 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +48 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
- package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
- package/node_modules/@openclaw/fs-safe/package.json +15 -15
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +0 -4
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +0 -35
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
|
@@ -1,9 +1,6 @@
|
|
|
1
|
+
import { type CopyPath } from "./copy-path-admission.js";
|
|
1
2
|
import { type CopyCloneMode } from "./copy-policy.js";
|
|
2
3
|
import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
3
|
-
type CopyPath = {
|
|
4
|
-
rootPath: string;
|
|
5
|
-
absolutePath: string;
|
|
6
|
-
};
|
|
7
4
|
export type CopyRootFileSyncOptions = {
|
|
8
5
|
source: CopyPath;
|
|
9
6
|
destination: CopyPath;
|
|
@@ -32,4 +29,10 @@ export type CopiedRootFileSync = OwnedFileDescriptorSync & Readonly<{
|
|
|
32
29
|
}>;
|
|
33
30
|
/** Exclusively copies an admitted source and transfers the owned destination fd. */
|
|
34
31
|
export declare function copyRootFileSync(options: CopyRootFileSyncOptions): CopiedRootFileSync;
|
|
35
|
-
export {
|
|
32
|
+
export type RootFileCopyBatchSync = {
|
|
33
|
+
copyFile(options: CopyRootFileSyncOptions): CopiedRootFileSync;
|
|
34
|
+
close(): void;
|
|
35
|
+
[Symbol.dispose](): void;
|
|
36
|
+
};
|
|
37
|
+
/** Reuses directory admissions while retaining fresh checks for every copy. */
|
|
38
|
+
export declare function createRootFileCopyBatchSync(): RootFileCopyBatchSync;
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import fs, {} from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
|
+
import { createCopyPathAdmissionCache } from "./copy-path-admission.js";
|
|
4
5
|
import { resolveCopyCloneMode } from "./copy-policy.js";
|
|
5
6
|
import { createFileWithAdmissionSync } from "./create.js";
|
|
6
7
|
import { ownFileDescriptorSync } from "./create-owned-file.js";
|
|
7
8
|
import { creationAdmissionFromParent, removeRecordedCreationFileSync } from "./creation-boundary.js";
|
|
8
|
-
import { assertSyncDirectoryGuard, captureDirectoryGuard } from "./directory-guard.js";
|
|
9
9
|
import { FsSafeError } from "./errors.js";
|
|
10
10
|
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
11
11
|
import { copyFileDescriptorSync } from "./file-handle-transfer.js";
|
|
12
12
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
13
13
|
import { getNativeBinding } from "./native.js";
|
|
14
|
-
import { realpathSync } from "./realpath.js";
|
|
15
14
|
import { openRootFileSync } from "./root-file.js";
|
|
16
|
-
import { resolveRootPathSync, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
|
|
17
15
|
import { inspectOpenedPathIdentitySync } from "./root-read-admission.js";
|
|
18
16
|
import { openStagedDirectory } from "./staged-directory.js";
|
|
19
17
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
@@ -26,42 +24,26 @@ function copyError(error) {
|
|
|
26
24
|
["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EXDEV", "EINVAL"].includes(code ?? "")
|
|
27
25
|
? "unsupported-platform" : "helper-failed", "guarded synchronous file copy failed", { cause: error });
|
|
28
26
|
}
|
|
29
|
-
function admitCopyPath(input, create = false) {
|
|
30
|
-
if (!path.isAbsolute(input.rootPath) || !path.isAbsolute(input.absolutePath)) {
|
|
31
|
-
throw new FsSafeError("invalid-path", "copy roots and paths must be absolute");
|
|
32
|
-
}
|
|
33
|
-
const root = captureDirectoryGuard(realpathSync(input.rootPath), "native", { bigint: true });
|
|
34
|
-
const resolve = () => {
|
|
35
|
-
const parent = create ? resolveRootPathSync({
|
|
36
|
-
...input, absolutePath: path.dirname(input.absolutePath), boundaryLabel: "copy parent",
|
|
37
|
-
rejectSymlinks: true, rejectFinalSymlink: true,
|
|
38
|
-
}) : undefined;
|
|
39
|
-
const selected = resolveRootPathSync({
|
|
40
|
-
...input, boundaryLabel: "copy", rejectSymlinks: !create, rejectFinalSymlink: !create,
|
|
41
|
-
policy: create ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
42
|
-
});
|
|
43
|
-
if (parent && path.dirname(selected.canonicalPath) !== parent.canonicalPath) {
|
|
44
|
-
throw new FsSafeError("path-mismatch", "copy destination parent changed");
|
|
45
|
-
}
|
|
46
|
-
return selected;
|
|
47
|
-
};
|
|
48
|
-
const selected = resolve();
|
|
49
|
-
const parent = captureDirectoryGuard(path.dirname(selected.canonicalPath), "native", { bigint: true });
|
|
50
|
-
const assertCurrent = () => {
|
|
51
|
-
assertSyncDirectoryGuard(root);
|
|
52
|
-
assertSyncDirectoryGuard(parent);
|
|
53
|
-
const current = resolve();
|
|
54
|
-
if (current.rootCanonicalPath !== root.realPath || current.canonicalPath !== selected.canonicalPath) {
|
|
55
|
-
throw new FsSafeError("path-mismatch", "copy path changed during operation");
|
|
56
|
-
}
|
|
57
|
-
assertSyncDirectoryGuard(root);
|
|
58
|
-
assertSyncDirectoryGuard(parent);
|
|
59
|
-
};
|
|
60
|
-
assertCurrent();
|
|
61
|
-
return { path: selected.canonicalPath, parent, assertCurrent };
|
|
62
|
-
}
|
|
63
27
|
/** Exclusively copies an admitted source and transfers the owned destination fd. */
|
|
64
28
|
export function copyRootFileSync(options) {
|
|
29
|
+
return copyRootFileWithAdmissionSync(options, createCopyPathAdmissionCache());
|
|
30
|
+
}
|
|
31
|
+
/** Reuses directory admissions while retaining fresh checks for every copy. */
|
|
32
|
+
export function createRootFileCopyBatchSync() {
|
|
33
|
+
const cache = createCopyPathAdmissionCache();
|
|
34
|
+
let closed = false;
|
|
35
|
+
const close = () => { closed = true; cache.clear(); };
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
copyFile(options) {
|
|
38
|
+
if (closed)
|
|
39
|
+
throw new FsSafeError("invalid-path", "copy batch is closed");
|
|
40
|
+
return copyRootFileWithAdmissionSync(options, cache);
|
|
41
|
+
},
|
|
42
|
+
close,
|
|
43
|
+
[Symbol.dispose]: close,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function copyRootFileWithAdmissionSync(options, cache) {
|
|
65
47
|
// Snapshot caller options before the first filesystem observation.
|
|
66
48
|
const sourceInput = options.source;
|
|
67
49
|
const sourcePath = { rootPath: sourceInput.rootPath, absolutePath: sourceInput.absolutePath };
|
|
@@ -89,17 +71,16 @@ export function copyRootFileSync(options) {
|
|
|
89
71
|
const native = getNativeBinding();
|
|
90
72
|
const nativeCopy = process.platform !== "win32" && native?.copyFileExclusiveSync && native.removeStagedFile
|
|
91
73
|
? native.copyFileExclusiveSync.bind(native) : undefined;
|
|
92
|
-
let nativeTarget = false;
|
|
93
74
|
try {
|
|
94
|
-
const source =
|
|
95
|
-
target =
|
|
75
|
+
const source = cache.admit(sourcePath);
|
|
76
|
+
target = cache.admit(targetPath, true);
|
|
96
77
|
// Preserve exclusive-create collision semantics for dangling Windows leaves.
|
|
97
78
|
assertExclusiveCreateLeaf(targetPath.absolutePath);
|
|
98
79
|
if (fs.lstatSync(target.path, { throwIfNoEntry: false })) {
|
|
99
80
|
throw new FsSafeError("already-exists", "copy destination already exists");
|
|
100
81
|
}
|
|
101
82
|
const opened = openRootFileSync({
|
|
102
|
-
...sourcePath, boundaryLabel: "copy source", maxBytes,
|
|
83
|
+
...sourcePath, rootRealPath: source.rootCanonicalPath, boundaryLabel: "copy source", maxBytes,
|
|
103
84
|
rejectHardlinks: sourceHardlinks !== "allow",
|
|
104
85
|
});
|
|
105
86
|
if (!opened.ok)
|
|
@@ -121,7 +102,6 @@ export function copyRootFileSync(options) {
|
|
|
121
102
|
}
|
|
122
103
|
};
|
|
123
104
|
const selectedMode = mode ?? (preserveSourceMode ? Number(sourceIdentity.mode & 511n) : 0o600 & ~process.umask());
|
|
124
|
-
let method = "copy";
|
|
125
105
|
let copied;
|
|
126
106
|
verifySource();
|
|
127
107
|
target.assertCurrent();
|
|
@@ -134,7 +114,6 @@ export function copyRootFileSync(options) {
|
|
|
134
114
|
target.assertCurrent();
|
|
135
115
|
copied = nativeCopy(opened.fd, parent.fd, path.basename(target.path), clone, maxBytes !== undefined && Number.isFinite(maxBytes) ? maxBytes : undefined);
|
|
136
116
|
targetOwner = ownFileDescriptorSync(copied.fd, closeNative);
|
|
137
|
-
nativeTarget = true;
|
|
138
117
|
}
|
|
139
118
|
else {
|
|
140
119
|
if (clone === "always")
|
|
@@ -155,13 +134,11 @@ export function copyRootFileSync(options) {
|
|
|
155
134
|
if (copied?.errorCode) {
|
|
156
135
|
throw copyError(Object.assign(new Error(copied.errorMessage), { code: copied.errorCode }));
|
|
157
136
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
method = copied.method;
|
|
137
|
+
const method = copied ? copied.method : "copy";
|
|
138
|
+
if (method !== "clone" && method !== "copy-file-range" && method !== "copy") {
|
|
139
|
+
throw new FsSafeError("helper-failed", "native copy returned an unknown method");
|
|
163
140
|
}
|
|
164
|
-
|
|
141
|
+
if (!copied) {
|
|
165
142
|
copyFileDescriptorSync(opened.fd, targetOwner.fd, { maxBytes });
|
|
166
143
|
}
|
|
167
144
|
verifySource();
|
|
@@ -186,7 +163,7 @@ export function copyRootFileSync(options) {
|
|
|
186
163
|
const cleanup = [];
|
|
187
164
|
if (targetOwner && target) {
|
|
188
165
|
try {
|
|
189
|
-
if (
|
|
166
|
+
if (parentOpen) {
|
|
190
167
|
const removed = native.removeStagedFile(parentOwner.fd, path.basename(target.path), targetOwner.fd);
|
|
191
168
|
if (removed === "preserved")
|
|
192
169
|
throw new FsSafeError("path-mismatch", "copy cleanup preserved a replacement");
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { FileLockSyncAcquireOptions, FileLockSyncHandle } from "./file-lock-sync.js";
|
|
2
2
|
import type { Root } from "./root-impl.js";
|
|
3
|
+
import { type FileLockSyncRootPath } from "./file-lock-sync-root.js";
|
|
4
|
+
import { type FileLockSyncRootFileReceipt } from "./file-lock-sync-root-io.js";
|
|
5
|
+
export declare function cleanupCreatedRootSyncLock(lockRootPath: FileLockSyncRootPath, fd: number, receipt: FileLockSyncRootFileReceipt, timer?: NodeJS.Timeout): void;
|
|
3
6
|
export declare function acquireFileLockSyncWithRoot<TPayload extends Record<string, unknown>>(targetPath: string, inputOptions: FileLockSyncAcquireOptions<TPayload>, lockRoot: Root): FileLockSyncHandle;
|
|
@@ -10,10 +10,40 @@ import { SyncLockAcquisition } from "./file-lock-sync-acquisition.js";
|
|
|
10
10
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
11
11
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
12
12
|
import { admitFileLockSyncRootPath, assertFileLockSyncRootPathsCurrent, captureFileLockSyncRootAuthority, normalizeFileLockSyncTargetWithRoot, } from "./file-lock-sync-root.js";
|
|
13
|
-
import { cleanupCreatedRootSyncLock, FileLockSyncRootArbitrationCollision, tryReuseCurrentRootSyncHeldLock, } from "./file-lock-sync-root-arbitration.js";
|
|
14
13
|
import { fileLockSyncRootSnapshotStillCurrent, } from "./file-lock-sync-root-io.js";
|
|
15
14
|
import { createFileLockSyncRootDirectory, createFileLockSyncRootFile, fileLockSyncRootGuardExists, fileLockSyncRootDirectoryReceiptStillCurrent, refreshFileLockSyncRootFileReceipt, removeFileLockSyncRootDirectory, removeFileLockSyncRootFile, } from "./file-lock-sync-root-mutation.js";
|
|
16
|
-
import { createRootSyncHeldLockHandle, ensureRootSyncExitCleanupRegistered, getRootSyncHeldLocks, readRootSidecarSnapshotSync, } from "./file-lock-sync-root-held.js";
|
|
15
|
+
import { createRootSyncHeldLockHandle, ensureRootSyncExitCleanupRegistered, getRootSyncHeldLocks, readRootSidecarSnapshotSync, verifyRootSyncHeldLock, } from "./file-lock-sync-root-held.js";
|
|
16
|
+
class FileLockSyncRootArbitrationCollision extends FsSafeError {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("path-mismatch", "file lock arbitration changed during local creation");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function cleanupCreatedRootSyncLock(lockRootPath, fd, receipt, timer) {
|
|
22
|
+
let timerCleanupFailed = false;
|
|
23
|
+
let timerCleanupError;
|
|
24
|
+
try {
|
|
25
|
+
if (timer)
|
|
26
|
+
clearInterval(timer);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
timerCleanupFailed = true;
|
|
30
|
+
timerCleanupError = error;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
if (!removeFileLockSyncRootFile(lockRootPath, receipt)) {
|
|
35
|
+
throw new FsSafeError("path-mismatch", "created sidecar lock changed before cleanup");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (fileCleanupError) {
|
|
39
|
+
if (timerCleanupFailed) {
|
|
40
|
+
throw createSuppressedError(timerCleanupError, fileCleanupError, "unpublished lock timer and file cleanup both failed");
|
|
41
|
+
}
|
|
42
|
+
throw fileCleanupError;
|
|
43
|
+
}
|
|
44
|
+
if (timerCleanupFailed)
|
|
45
|
+
throw timerCleanupError;
|
|
46
|
+
}
|
|
17
47
|
export function acquireFileLockSyncWithRoot(targetPath, inputOptions, lockRoot) {
|
|
18
48
|
// The public dispatcher already read lockRoot exactly once. Validate and
|
|
19
49
|
// snapshot that genuine Root before any remaining option or retry getter can
|
|
@@ -37,23 +67,39 @@ export function acquireFileLockSyncWithRoot(targetPath, inputOptions, lockRoot)
|
|
|
37
67
|
const heldLocks = getRootSyncHeldLocks();
|
|
38
68
|
const currentTargetHolder = () => heldLocks.get(normalizedTargetPath) ??
|
|
39
69
|
foreignSyncHeldLock("root", normalizedTargetPath);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
const tryReuseCurrentHeld = () => {
|
|
71
|
+
const held = heldLocks.get(normalizedTargetPath);
|
|
72
|
+
const reusable = held && !foreignSyncHeldLock("root", normalizedTargetPath) &&
|
|
73
|
+
options.reentrantOwner !== undefined && held.reentrantOwner !== undefined &&
|
|
74
|
+
options.reentrantOwner === held.reentrantOwner &&
|
|
75
|
+
authority.adapter === held.rootAuthority.adapter &&
|
|
76
|
+
lockRootPath.relativePath === held.rootPath.relativePath &&
|
|
77
|
+
path.relative(lockRootPath.path, held.rootPath.path) === "" &&
|
|
78
|
+
held.releaseState !== "released";
|
|
79
|
+
if (!reusable)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (!verifyRootSyncHeldLock(held)) {
|
|
82
|
+
throw new FsSafeError("path-mismatch", "held sidecar lock changed before reentrant reuse");
|
|
83
|
+
}
|
|
84
|
+
// The parser can reenter acquisition; recheck the exact held entry before reuse.
|
|
85
|
+
if (heldLocks.get(normalizedTargetPath) !== held || held.releaseState === "released" ||
|
|
86
|
+
foreignSyncHeldLock("root", normalizedTargetPath)) {
|
|
87
|
+
throw new FsSafeError("path-mismatch", "held sidecar lock changed during reentrant reuse");
|
|
88
|
+
}
|
|
89
|
+
held.refCount += 1;
|
|
90
|
+
held.revision += 1;
|
|
91
|
+
return createRootSyncHeldLockHandle(held);
|
|
92
|
+
};
|
|
47
93
|
if (heldLocks.has(normalizedTargetPath) && !foreignSyncHeldLock("root", normalizedTargetPath)) {
|
|
48
94
|
assertFileLockSyncRootPathsCurrent(guardedPaths);
|
|
49
|
-
const initiallyReusable =
|
|
95
|
+
const initiallyReusable = tryReuseCurrentHeld();
|
|
50
96
|
if (initiallyReusable)
|
|
51
97
|
return initiallyReusable;
|
|
52
98
|
}
|
|
53
99
|
const acquisition = new SyncLockAcquisition(lockPath, normalizedTargetPath, options.retry, options.timeoutMs);
|
|
54
100
|
let ownedReclaimGuard;
|
|
55
101
|
let reclaimCleanupAttempted = false;
|
|
56
|
-
const reuseCurrentHeld = () => ownedReclaimGuard ? undefined :
|
|
102
|
+
const reuseCurrentHeld = () => ownedReclaimGuard ? undefined : tryReuseCurrentHeld();
|
|
57
103
|
const releaseReclaimGuard = () => {
|
|
58
104
|
const receipt = ownedReclaimGuard;
|
|
59
105
|
if (!receipt)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import fsSync, {} from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { assertDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createSyncDirectoryGuard, } from "./directory-guard.js";
|
|
5
5
|
export async function withAsyncDirectoryGuards(guards, mutate, options = {}) {
|
|
6
6
|
for (const guard of guards) {
|
|
7
|
-
|
|
7
|
+
assertDirectoryGuard(guard, "native");
|
|
8
8
|
}
|
|
9
9
|
const result = await mutate();
|
|
10
10
|
if (options.verifyAfter !== false) {
|
|
11
11
|
try {
|
|
12
12
|
for (const guard of guards) {
|
|
13
|
-
|
|
13
|
+
assertDirectoryGuard(guard, "native");
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
catch (error) {
|
|
@@ -46,11 +46,11 @@ export async function guardedRename(params) {
|
|
|
46
46
|
const sourceGuard = await createAsyncDirectoryGuard(path.dirname(params.from));
|
|
47
47
|
const targetGuard = await createAsyncDirectoryGuard(path.dirname(params.to));
|
|
48
48
|
await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
|
|
49
|
+
// Authority must survive all awaited guards; do not yield before rename dispatch.
|
|
50
|
+
params.assertBeforeRename?.();
|
|
49
51
|
if (params.onSourceInspected) {
|
|
50
52
|
params.onSourceInspected(fsSync.lstatSync(params.from, { bigint: true }));
|
|
51
53
|
}
|
|
52
|
-
// Authority must survive all awaited guards; do not yield before rename dispatch.
|
|
53
|
-
params.assertBeforeRename?.();
|
|
54
54
|
await fs.rename(params.from, params.to);
|
|
55
55
|
params.onRenamed?.();
|
|
56
56
|
});
|
|
@@ -32,6 +32,16 @@ export const GUEST_FILESYSTEM_CREATE_STAGING_PYTHON = [
|
|
|
32
32
|
].join("\n");
|
|
33
33
|
export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
34
34
|
"def rename_no_replace(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
35
|
+
" import stat",
|
|
36
|
+
" source = os.lstat(src_basename, dir_fd=src_parent_fd)",
|
|
37
|
+
" kind = stat.S_IFMT(source.st_mode)",
|
|
38
|
+
" expected = (source.st_dev, source.st_ino, kind)",
|
|
39
|
+
" def verify(parent_fd, basename, links):",
|
|
40
|
+
" current = os.lstat(basename, dir_fd=parent_fd)",
|
|
41
|
+
" if (current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode)) != expected:",
|
|
42
|
+
" raise OSError(errno.ESTALE, 'no-replace publication identity changed', basename)",
|
|
43
|
+
" if kind == stat.S_IFREG and current.st_nlink != links:",
|
|
44
|
+
" raise OSError(errno.ESTALE, 'no-replace publication link count changed', basename)",
|
|
35
45
|
" libc = ctypes.CDLL(None, use_errno=True)",
|
|
36
46
|
" is_linux = sys.platform.startswith('linux')",
|
|
37
47
|
" if is_linux:",
|
|
@@ -62,6 +72,31 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
62
72
|
" raise OSError(error_code, os.strerror(error_code), dst_basename)",
|
|
63
73
|
" elif not is_linux:",
|
|
64
74
|
" raise OSError(errno.ENOSYS, 'atomic no-replace rename is unavailable')",
|
|
75
|
+
" # Only a definitive rejection of the fixed flags reaches this fallback.",
|
|
76
|
+
" if kind not in (stat.S_IFDIR, stat.S_IFREG):",
|
|
77
|
+
" raise OSError(errno.EINVAL, 'no-replace fallback requires a file or directory', src_basename)",
|
|
78
|
+
" verify(src_parent_fd, src_basename, 1)",
|
|
79
|
+
" try:",
|
|
80
|
+
" os.lstat(dst_basename, dir_fd=dst_parent_fd)",
|
|
81
|
+
" except FileNotFoundError:",
|
|
82
|
+
" pass",
|
|
83
|
+
" else:",
|
|
84
|
+
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), dst_basename)",
|
|
85
|
+
" if kind == stat.S_IFDIR:",
|
|
86
|
+
" verify(src_parent_fd, src_basename, 1)",
|
|
87
|
+
" # A directory rename can replace only an EMPTY directory raced into",
|
|
88
|
+
" # the absent target. Non-empty directories and files are refused.",
|
|
89
|
+
" # The slash enforces source directory type at the syscall, even if",
|
|
90
|
+
" # a peer substitutes a file/symlink after verification. Identity",
|
|
91
|
+
" # checks are not compare-and-swap; verify publication afterward.",
|
|
92
|
+
" try:",
|
|
93
|
+
" os.rename(src_basename + '/', dst_basename, src_dir_fd=src_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
94
|
+
" except OSError as error:",
|
|
95
|
+
" if error.errno in (errno.ENOTEMPTY, errno.ENOTDIR, errno.EEXIST):",
|
|
96
|
+
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), dst_basename) from error",
|
|
97
|
+
" raise",
|
|
98
|
+
" verify(dst_parent_fd, dst_basename, 1)",
|
|
99
|
+
" return",
|
|
65
100
|
" os.link(",
|
|
66
101
|
" src_basename,",
|
|
67
102
|
" dst_basename,",
|
|
@@ -69,12 +104,19 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
69
104
|
" dst_dir_fd=dst_parent_fd,",
|
|
70
105
|
" follow_symlinks=False,",
|
|
71
106
|
" )",
|
|
107
|
+
" # Only this exact two-name interval permits nlink=2. Never roll back the",
|
|
108
|
+
" # target on failure: it may be the only remaining link to the source.",
|
|
109
|
+
" verify(dst_parent_fd, dst_basename, 2)",
|
|
110
|
+
" verify(src_parent_fd, src_basename, 2)",
|
|
111
|
+
" os.unlink(src_basename, dir_fd=src_parent_fd)",
|
|
112
|
+
" verify(dst_parent_fd, dst_basename, 1)",
|
|
72
113
|
].join("\n");
|
|
73
114
|
export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
74
115
|
"def create_exclusive(parent_fd, basename, stdin_buffer):",
|
|
75
116
|
" staging_fd = None",
|
|
76
117
|
" staging_name = None",
|
|
77
118
|
" temp_fd = None",
|
|
119
|
+
" temp_inode = None",
|
|
78
120
|
" temp_name = 'payload'",
|
|
79
121
|
" try:",
|
|
80
122
|
" try:",
|
|
@@ -85,6 +127,7 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
|
85
127
|
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), basename)",
|
|
86
128
|
" staging_name, staging_fd = create_staging_dir(parent_fd)",
|
|
87
129
|
" temp_fd = os.open(temp_name, WRITE_FLAGS, 0o600, dir_fd=staging_fd)",
|
|
130
|
+
" temp_inode = inode_identity(os.fstat(temp_fd))",
|
|
88
131
|
" while True:",
|
|
89
132
|
" chunk = stdin_buffer.read(65536)",
|
|
90
133
|
" if not chunk:",
|
|
@@ -92,8 +135,7 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
|
92
135
|
" write_all(temp_fd, chunk)",
|
|
93
136
|
" # exclusive create payload is durable before publication",
|
|
94
137
|
" os.fsync(temp_fd)",
|
|
95
|
-
"
|
|
96
|
-
" # Publish with a native atomic no-replace rename.",
|
|
138
|
+
" # Publish with no-replace rename or the checked file fallback.",
|
|
97
139
|
" rename_no_replace(staging_fd, temp_name, parent_fd, basename)",
|
|
98
140
|
" target_stat = os.lstat(basename, dir_fd=parent_fd)",
|
|
99
141
|
" if temp_inode != inode_identity(target_stat):",
|
|
@@ -105,14 +147,19 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
|
105
147
|
" if staging_fd is not None:",
|
|
106
148
|
" # Cleanup stays relative to the private pinned directory, so a",
|
|
107
149
|
" # parent-path substitution cannot redirect payload deletion.",
|
|
150
|
+
" payload_removed = False",
|
|
108
151
|
" try:",
|
|
109
|
-
" os.
|
|
152
|
+
" current_payload = os.lstat(temp_name, dir_fd=staging_fd)",
|
|
153
|
+
" # Preserve ambiguous or partially published link pairs.",
|
|
154
|
+
" if temp_inode == inode_identity(current_payload) and current_payload.st_nlink == 1:",
|
|
155
|
+
" os.unlink(temp_name, dir_fd=staging_fd)",
|
|
156
|
+
" payload_removed = True",
|
|
110
157
|
" except FileNotFoundError:",
|
|
111
|
-
"
|
|
158
|
+
" payload_removed = True",
|
|
112
159
|
" staging_identity = entry_identity(os.fstat(staging_fd))",
|
|
113
160
|
" os.close(staging_fd)",
|
|
114
161
|
" staging_removed = False",
|
|
115
|
-
" if staging_name is not None:",
|
|
162
|
+
" if staging_name is not None and payload_removed:",
|
|
116
163
|
" try:",
|
|
117
164
|
" current_staging = os.lstat(staging_name, dir_fd=parent_fd)",
|
|
118
165
|
" if same_identity(staging_identity, current_staging):",
|
|
@@ -395,7 +395,7 @@ export const GUEST_FILESYSTEM_PYTHON = [
|
|
|
395
395
|
" break",
|
|
396
396
|
" write_all(temp_fd, chunk)",
|
|
397
397
|
" try:",
|
|
398
|
-
" os.fchmod(temp_fd, stat.S_IMODE(
|
|
398
|
+
" os.fchmod(temp_fd, stat.S_IMODE(src_file_stat.st_mode))",
|
|
399
399
|
" except AttributeError:",
|
|
400
400
|
" pass",
|
|
401
401
|
" os.fsync(temp_fd)",
|
|
@@ -139,6 +139,7 @@ export interface NativeBinding {
|
|
|
139
139
|
renameReplaceWithIdentity?(sourceParent: number, source: string, targetParent: number, target: string, dev: bigint, ino: bigint): void;
|
|
140
140
|
/** Windows-only, private handle custody; no borrowed/runtime descriptors. */
|
|
141
141
|
retainWindowsFile?(directory: string, basename: string, parentDev: bigint, parentIno: bigint, dev: bigint, ino: bigint, size: bigint, mtimeNs: bigint, ctimeNs: bigint, sha256: string, maxBytes: number): NativeRetainedFile;
|
|
142
|
+
retainWindowsSidecar?(parentFd: number, directory: string, basename: string, parentDev: bigint, parentIno: bigint, dev: bigint, ino: bigint, size: bigint, sha256: string, maxBytes: number): NativeRetainedFile;
|
|
142
143
|
watchRegister?(root: string, limit: number, callback: (batch: import("./watch-native.js").NativeWatchWireBatch) => void, persistent: boolean): number;
|
|
143
144
|
watchConfigure?(id: number, anchors: string[], exclusions: string[]): void;
|
|
144
145
|
watchEntries?(id: number, entries: import("./watch-native.js").NativeWatchEntry[]): {
|
|
@@ -5,7 +5,6 @@ export type NativeFileHandle = {
|
|
|
5
5
|
writeFile(data: string | Buffer, encoding?: BufferEncoding): Promise<void>;
|
|
6
6
|
};
|
|
7
7
|
export declare function nativeOpenFlags(flags: number): number;
|
|
8
|
-
export declare function writeNativeFd(fd: number, data: Buffer): void;
|
|
9
8
|
export declare function removeNativeCreatedFileIfStillPinned(params: {
|
|
10
9
|
parentPath: string;
|
|
11
10
|
parentFd: number;
|
|
@@ -10,31 +10,6 @@ export function nativeOpenFlags(flags) {
|
|
|
10
10
|
(closeOnExec ?? 0) |
|
|
11
11
|
(typeof fsSync.constants.O_NOFOLLOW === "number" ? fsSync.constants.O_NOFOLLOW : 0));
|
|
12
12
|
}
|
|
13
|
-
export function writeNativeFd(fd, data) {
|
|
14
|
-
let offset = 0;
|
|
15
|
-
while (offset < data.byteLength) {
|
|
16
|
-
const written = fsSync.writeSync(fd, data, offset, data.byteLength - offset);
|
|
17
|
-
if (written <= 0) {
|
|
18
|
-
throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
|
|
19
|
-
}
|
|
20
|
-
offset += written;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function wrapNativeFd(fd, closeFd) {
|
|
24
|
-
let open = true;
|
|
25
|
-
return {
|
|
26
|
-
fd,
|
|
27
|
-
async close() {
|
|
28
|
-
if (open) {
|
|
29
|
-
open = false;
|
|
30
|
-
closeFd(fd);
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
async writeFile(data, encoding) {
|
|
34
|
-
writeNativeFd(fd, Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8"));
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
13
|
export function removeNativeCreatedFileIfStillPinned(params) {
|
|
39
14
|
if (!params.created) {
|
|
40
15
|
return;
|
|
@@ -86,7 +61,28 @@ export async function createNativeExclusiveFile(targetPath, mode) {
|
|
|
86
61
|
fd = opened.fd;
|
|
87
62
|
fsSync.fchmodSync(fd, mode);
|
|
88
63
|
created = fsSync.fstatSync(fd, { bigint: true });
|
|
89
|
-
|
|
64
|
+
const ownedFd = fd;
|
|
65
|
+
let open = true;
|
|
66
|
+
return {
|
|
67
|
+
fd: ownedFd,
|
|
68
|
+
async close() {
|
|
69
|
+
if (open) {
|
|
70
|
+
open = false;
|
|
71
|
+
closeFd(ownedFd);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async writeFile(data, encoding) {
|
|
75
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8");
|
|
76
|
+
let offset = 0;
|
|
77
|
+
while (offset < buffer.byteLength) {
|
|
78
|
+
const written = fsSync.writeSync(ownedFd, buffer, offset, buffer.byteLength - offset);
|
|
79
|
+
if (written <= 0) {
|
|
80
|
+
throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
|
|
81
|
+
}
|
|
82
|
+
offset += written;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
90
86
|
}
|
|
91
87
|
catch (error) {
|
|
92
88
|
if (fd !== undefined) {
|
|
@@ -188,7 +188,11 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
188
188
|
if (params.sync !== false)
|
|
189
189
|
syncFileBestEffortSync(parentFd);
|
|
190
190
|
// Verification follows publication and final chmod, outside rollback handling.
|
|
191
|
-
|
|
191
|
+
if (params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false) {
|
|
192
|
+
await params.verifyPublished?.(targetFd, verificationIdentity, parentGuard, { fd: parentFd, binding });
|
|
193
|
+
}
|
|
194
|
+
else
|
|
195
|
+
await params.verifyPublished?.(targetFd, verificationIdentity, parentGuard);
|
|
192
196
|
completed = true;
|
|
193
197
|
return { dev: targetIdentity.dev, ino: targetIdentity.ino };
|
|
194
198
|
}
|
|
@@ -177,7 +177,10 @@ class NativeStagedFile {
|
|
|
177
177
|
}
|
|
178
178
|
const identity = published.staged.identity;
|
|
179
179
|
try {
|
|
180
|
-
|
|
180
|
+
if (exclusive)
|
|
181
|
+
await params.verifyPublished?.(staged.#file(), identity, parentGuard, { fd: parentFd, binding });
|
|
182
|
+
else
|
|
183
|
+
await params.verifyPublished?.(staged.#file(), identity, parentGuard);
|
|
181
184
|
}
|
|
182
185
|
catch (error) {
|
|
183
186
|
if (params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true) {
|
|
@@ -5,7 +5,8 @@ import { formatErrorDetail } from "./error-detail.js";
|
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
6
|
export const DEFAULT_PERMISSION_EXEC_TIMEOUT_MS = 30_000;
|
|
7
7
|
export function formatPermissionErrorDetail(value) {
|
|
8
|
-
|
|
8
|
+
// Escaping cannot shrink input; 401 code units decide the 400-character excerpt.
|
|
9
|
+
const formatted = formatErrorDetail(value.slice(0, 401));
|
|
9
10
|
return formatted.length > 400 ? `${formatted.slice(0, 399)}…` : formatted;
|
|
10
11
|
}
|
|
11
12
|
const MAX_CAUGHT_FAILURE_PROTOTYPES = 4;
|
|
@@ -2,6 +2,11 @@ import type { CopyFileInput } from "./copy-file-input.js";
|
|
|
2
2
|
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
4
|
import type { MutationDirectoryObservation } from "./pinned-mutation-observation.js";
|
|
5
|
+
import type { NativeBinding } from "./native-binding.js";
|
|
6
|
+
export type NativeWriteParent = Readonly<{
|
|
7
|
+
fd: number;
|
|
8
|
+
binding: NativeBinding;
|
|
9
|
+
}>;
|
|
5
10
|
export type PinnedWriteInput = {
|
|
6
11
|
kind: "buffer";
|
|
7
12
|
data: string | Buffer;
|
|
@@ -64,5 +69,5 @@ export type PinnedWriteParams = {
|
|
|
64
69
|
mutationAdmission?: PinnedWriteMutationAdmission;
|
|
65
70
|
onRenameIdentityMismatch?: "verify-content";
|
|
66
71
|
onPublished?: (identity: PublishedWriteIdentity) => void;
|
|
67
|
-
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
|
|
72
|
+
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard, nativeParent?: NativeWriteParent) => Promise<void>;
|
|
68
73
|
};
|
|
@@ -166,7 +166,7 @@ async function createTempWorkspace(options, scopedPrefix = false) {
|
|
|
166
166
|
else
|
|
167
167
|
admission.assertCurrent();
|
|
168
168
|
const stat = inspectDirectoryIdentitySync(dir);
|
|
169
|
-
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode);
|
|
169
|
+
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
|
|
170
170
|
// Retain while the child still has its private creation mode so an
|
|
171
171
|
// explicit dirMode such as 0 cannot make identity descriptor acquisition fail.
|
|
172
172
|
retainedChild = TempWorkspaceRetainedChild.retain(dir, stat);
|
|
@@ -261,7 +261,7 @@ function createTempWorkspaceSync(options, scopedPrefix = false) {
|
|
|
261
261
|
else {
|
|
262
262
|
stat = inspectDirectoryIdentitySync(dir);
|
|
263
263
|
}
|
|
264
|
-
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode);
|
|
264
|
+
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
|
|
265
265
|
retainedChild ??= TempWorkspaceRetainedChild.retain(dir, stat);
|
|
266
266
|
if (needsModeInitialization) {
|
|
267
267
|
retainedChild.initializeModeSync(dirMode, admission.ownerUid, admission.assertCurrent);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { nativeMoveNoReplace } from "./native-move-fallback.js";
|
|
2
|
+
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import fsSync, {} from "node:fs";
|
|
4
5
|
import fs, {} from "node:fs/promises";
|
|
@@ -270,12 +271,38 @@ export async function publishFileExclusive(params) {
|
|
|
270
271
|
}
|
|
271
272
|
if (strategy === "rename-noreplace") {
|
|
272
273
|
const binding = requireNativeBinding();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
try {
|
|
275
|
+
const fallback = nativeMoveNoReplace(binding, [
|
|
276
|
+
sourceNativeParent.handle.fd, sourceNativeParent.basename,
|
|
277
|
+
targetNativeParent.handle.fd, targetNativeParent.basename,
|
|
278
|
+
], sourceExactIdentity, getFsSafeNativeConfig().mode === "auto", "file publication");
|
|
279
|
+
if (fallback) {
|
|
280
|
+
failure.fallback = fallback;
|
|
281
|
+
failure.sourceRemoval = "removed";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error instanceof FsSafeError) {
|
|
286
|
+
if (error.details?.publication === "published") {
|
|
287
|
+
failure.targetCreated = true;
|
|
288
|
+
failure.preserveTarget = true;
|
|
289
|
+
failure.phase = "rename-verify";
|
|
290
|
+
failure.fallback = "link-unlink";
|
|
291
|
+
failure.sourceRemoval = error.details.sourceRemoval;
|
|
292
|
+
// Changed/unverified pairs cannot establish which inode was published.
|
|
293
|
+
if (failure.sourceRemoval === "still-linked")
|
|
294
|
+
rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
const code = error?.code;
|
|
299
|
+
if (code === "path-mismatch" || code === "hardlink") {
|
|
300
|
+
throw new FsSafeError(code, "publication source changed before fallback", { cause: error });
|
|
301
|
+
}
|
|
302
|
+
if (code === "ELOOP")
|
|
303
|
+
throw new FsSafeError("symlink", "publication source became a symlink", { cause: error });
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
279
306
|
rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
|
|
280
307
|
// A failed post-rename fence must not delete the only remaining name.
|
|
281
308
|
failure.preserveTarget = true;
|