@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-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/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
- package/node_modules/@openclaw/fs-safe/README.md +20 -4
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
- package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
- package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
- package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +15 -5
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
- package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
- package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
- package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
- package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
- package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
- package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
- package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
- package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
- package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
- package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
- package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
- package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
- package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
- package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
- package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
- package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
- package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
- package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
- package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
- package/node_modules/@openclaw/fs-safe/package.json +21 -12
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +0 -336
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
|
@@ -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);
|
|
@@ -261,7 +263,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
261
263
|
if (verifyPosixMode)
|
|
262
264
|
assertPinnedWriteMode(handle.fd, 0o600, params.private);
|
|
263
265
|
// Content writes may clear set-ID bits; finalize them through the owned fd.
|
|
264
|
-
|
|
266
|
+
fsSync.fchmodSync(handle.fd, params.mode);
|
|
265
267
|
if (verifyPosixMode)
|
|
266
268
|
assertPinnedWriteMode(handle.fd, params.mode, params.private);
|
|
267
269
|
if (params.sync !== false) {
|
|
@@ -270,7 +272,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
270
272
|
else
|
|
271
273
|
await syncFileBestEffort(handle);
|
|
272
274
|
}
|
|
273
|
-
const stat = fsSync.fstatSync(handle.fd);
|
|
275
|
+
const stat = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
274
276
|
if (params.sync !== false)
|
|
275
277
|
await syncDirectoryBestEffort(parentPath);
|
|
276
278
|
// Publication is complete. A failed outer check must not remove its target.
|
|
@@ -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
|
+
}
|
|
@@ -25,6 +25,6 @@ export async function createPrivateDirectory(targetPath, options) {
|
|
|
25
25
|
if (components.some(component => component.endsWith(".") || component.endsWith(" "))) {
|
|
26
26
|
throw Object.assign(new Error("private directory path has an ambiguous component"), { code: "EINVAL" });
|
|
27
27
|
}
|
|
28
|
-
warnNativeFallback("windows-private-directory", "Private Windows directory creation
|
|
28
|
+
warnNativeFallback("windows-private-directory", "Private Windows directory creation launches the packaged .NET helper for each check or creation.", "createPrivateDirectory");
|
|
29
29
|
await createPrivateWindowsDirectoryCommand(path.win32.resolve(targetPath));
|
|
30
30
|
}
|
|
@@ -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;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type ExactIdentityReceipt } from "./exact-identity.js";
|
|
1
2
|
import type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
|
|
2
3
|
export type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety, } from "./temp-workspace-owner.js";
|
|
3
4
|
export type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
|
|
4
|
-
export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace>;
|
|
5
|
+
export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace & ExactIdentityReceipt>;
|
|
5
6
|
export declare function withTempWorkspace<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspace) => Promise<T>): Promise<T>;
|
|
6
|
-
export declare function tempWorkspaceSync(options: TempWorkspaceOptions): TempWorkspaceSync;
|
|
7
|
+
export declare function tempWorkspaceSync(options: TempWorkspaceOptions): TempWorkspaceSync & ExactIdentityReceipt;
|
|
7
8
|
export declare function withTempWorkspaceSync<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspaceSync) => T): T;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exactFileIdentity } from "./exact-identity.js";
|
|
1
2
|
import { randomInt, randomUUID } from "node:crypto";
|
|
2
3
|
import fsSync, {} from "node:fs";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
@@ -61,6 +62,26 @@ function createTempWorkspaceWithRequestedModeSync(childPrefix, dirMode, prepareC
|
|
|
61
62
|
}
|
|
62
63
|
throw collision;
|
|
63
64
|
}
|
|
65
|
+
function createNativeTempWorkspace(childPrefix, capability) {
|
|
66
|
+
const create = capability.binding?.createTempDirectoryBeneath;
|
|
67
|
+
if (!create || capability.parent?.access !== "read")
|
|
68
|
+
return undefined;
|
|
69
|
+
for (let attempt = 0; attempt < TEMP_WORKSPACE_DIRECT_CREATE_ATTEMPTS; attempt += 1) {
|
|
70
|
+
const dir = randomTempWorkspaceChildPath(childPrefix);
|
|
71
|
+
capability.prepareChildCreation();
|
|
72
|
+
let created;
|
|
73
|
+
try {
|
|
74
|
+
created = create.call(capability.binding, capability.parent.fd, path.basename(dir));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.code === "EEXIST")
|
|
78
|
+
continue;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
return { dir, ...TempWorkspaceRetainedChild.adoptCreated(dir, created.fd, created.searchOnly) };
|
|
82
|
+
}
|
|
83
|
+
throw Object.assign(new Error("temp workspace name collisions exhausted"), { code: "EEXIST" });
|
|
84
|
+
}
|
|
64
85
|
function resolveWorkspaceLeaf(dir, fileName) {
|
|
65
86
|
const resolved = path.join(dir, assertWorkspaceFileName(fileName));
|
|
66
87
|
assertNoWindowsPathAlias(resolved, "filesystem", "temp workspace path uses a Windows filesystem namespace alias");
|
|
@@ -143,7 +164,7 @@ function registerTempWorkspace(dir, retainedChild, capability, admission, dirMod
|
|
|
143
164
|
catch (error) {
|
|
144
165
|
throwTempWorkspaceCreationFailure(error, retainedChild, capability, owner);
|
|
145
166
|
}
|
|
146
|
-
return { owner: owner, unregisterTempDir, identity: { dev: Number(stat.dev), ino: Number(stat.ino) } };
|
|
167
|
+
return { owner: owner, unregisterTempDir, exactIdentity: exactFileIdentity(stat), identity: { dev: Number(stat.dev), ino: Number(stat.ino) } };
|
|
147
168
|
}
|
|
148
169
|
async function createTempWorkspace(options, scopedPrefix = false) {
|
|
149
170
|
const { rootDir, dirMode, mode, cleanupSafety } = tempWorkspaceSettings(options);
|
|
@@ -154,12 +175,17 @@ async function createTempWorkspace(options, scopedPrefix = false) {
|
|
|
154
175
|
let dir;
|
|
155
176
|
let retainedChild;
|
|
156
177
|
try {
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
178
|
+
// Both creation routes first capture the complete ancestry and associate
|
|
179
|
+
// the provisional parent descriptor without another capability probe.
|
|
180
|
+
const created = createNativeTempWorkspace(childPrefix, capability);
|
|
181
|
+
if (created) {
|
|
182
|
+
dir = created.dir;
|
|
183
|
+
retainedChild = created.retained;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
capability.prepareChildCreation();
|
|
187
|
+
dir = await fs.mkdtemp(childPrefix);
|
|
188
|
+
}
|
|
163
189
|
assertNoWindowsPathAlias(dir, "filesystem", "temp workspace path uses a Windows filesystem namespace alias");
|
|
164
190
|
if (capability.parent)
|
|
165
191
|
capability.assertCurrent();
|
|
@@ -169,7 +195,7 @@ async function createTempWorkspace(options, scopedPrefix = false) {
|
|
|
169
195
|
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
|
|
170
196
|
// Retain while the child still has its private creation mode so an
|
|
171
197
|
// explicit dirMode such as 0 cannot make identity descriptor acquisition fail.
|
|
172
|
-
retainedChild
|
|
198
|
+
retainedChild ??= TempWorkspaceRetainedChild.retain(dir, stat);
|
|
173
199
|
if (needsModeInitialization) {
|
|
174
200
|
await retainedChild.initializeMode(dirMode, admission.ownerUid, admission.assertCurrent);
|
|
175
201
|
}
|
|
@@ -177,12 +203,13 @@ async function createTempWorkspace(options, scopedPrefix = false) {
|
|
|
177
203
|
catch (error) {
|
|
178
204
|
throwTempWorkspaceCreationFailure(error, retainedChild, capability);
|
|
179
205
|
}
|
|
180
|
-
const { owner, identity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
|
|
206
|
+
const { owner, identity, exactIdentity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
|
|
181
207
|
// Once registered, even a store-construction failure remains exit-cleanable.
|
|
182
208
|
const store = fileStore({ rootDir: dir, private: true, dirMode, mode });
|
|
183
209
|
return {
|
|
184
210
|
dir,
|
|
185
211
|
identity,
|
|
212
|
+
exactIdentity,
|
|
186
213
|
store,
|
|
187
214
|
path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
|
188
215
|
write: async (fileName, data) => await store.write(assertWorkspaceFileName(fileName), data, { mode }),
|
|
@@ -273,12 +300,13 @@ function createTempWorkspaceSync(options, scopedPrefix = false) {
|
|
|
273
300
|
catch (error) {
|
|
274
301
|
throwTempWorkspaceCreationFailure(error, retainedChild, capability);
|
|
275
302
|
}
|
|
276
|
-
const { owner, identity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
|
|
303
|
+
const { owner, identity, exactIdentity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
|
|
277
304
|
// Once registered, even a store-construction failure remains exit-cleanable.
|
|
278
305
|
const store = fileStoreSync({ rootDir: dir, private: true, dirMode, mode });
|
|
279
306
|
return {
|
|
280
307
|
dir,
|
|
281
308
|
identity,
|
|
309
|
+
exactIdentity,
|
|
282
310
|
store,
|
|
283
311
|
path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
|
|
284
312
|
write: (fileName, data) => store.write(assertWorkspaceFileName(fileName), data, { mode }),
|
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExactFileIdentity } from "./exact-identity.js";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
4
|
export type PublishFileExclusiveSyncFailurePolicy = "rollback" | "preserve";
|
|
@@ -11,6 +12,7 @@ export type PublishFileExclusiveFailureDetails = {
|
|
|
11
12
|
phase: PublishFileExclusiveFailurePhase;
|
|
12
13
|
targetCreated: boolean;
|
|
13
14
|
targetIdentity?: FileIdentityStat;
|
|
15
|
+
exactTargetIdentity?: ExactFileIdentity;
|
|
14
16
|
cleanup: PublishFileExclusiveCleanup;
|
|
15
17
|
directorySync?: PublishFileExclusiveDirectorySyncFailure;
|
|
16
18
|
fallback?: "link-unlink";
|
|
@@ -21,6 +23,7 @@ export type PublishFailureState = {
|
|
|
21
23
|
phase: PublishFileExclusiveFailurePhase;
|
|
22
24
|
targetCreated: boolean;
|
|
23
25
|
targetIdentity?: FileIdentityStat;
|
|
26
|
+
exactTargetIdentity?: ExactFileIdentity;
|
|
24
27
|
targetCleanupIdentity?: FileIdentityStat;
|
|
25
28
|
preserveTarget: boolean;
|
|
26
29
|
directorySync?: PublishFileExclusiveDirectorySyncFailure;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { exactFileIdentity } from "./exact-identity.js";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
export function rememberCreatedTarget(state, identity, phase) {
|
|
3
4
|
state.targetCreated = true;
|
|
5
|
+
state.exactTargetIdentity = exactFileIdentity(identity);
|
|
4
6
|
state.targetIdentity = { dev: Number(identity.dev), ino: Number(identity.ino) };
|
|
5
7
|
state.targetCleanupIdentity = { dev: identity.dev, ino: identity.ino };
|
|
6
8
|
state.phase = phase;
|
|
@@ -10,6 +12,7 @@ export function publicationFailure(error, state, cleanup) {
|
|
|
10
12
|
const details = {
|
|
11
13
|
phase: state.phase,
|
|
12
14
|
targetCreated: state.targetCreated,
|
|
15
|
+
...(state.exactTargetIdentity ? { exactTargetIdentity: state.exactTargetIdentity } : {}),
|
|
13
16
|
...(state.targetIdentity ? { targetIdentity: state.targetIdentity } : {}),
|
|
14
17
|
...(state.directorySync ? { directorySync: state.directorySync } : {}),
|
|
15
18
|
...(state.fallback ? { fallback: state.fallback, publication: "published", sourceRemoval: state.sourceRemoval } : {}),
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
import { type ExactFileIdentity } from "./exact-identity.js";
|
|
1
2
|
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
3
|
import { type DirectoryReceipt, type DirectorySyncOutcome } from "./directory-durability.js";
|
|
3
|
-
import { type
|
|
4
|
+
import { type FileIdentityInput } from "./file-identity.js";
|
|
4
5
|
import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failure.js";
|
|
6
|
+
export { isHardlinkFallbackError } from "./hardlink-fallback.js";
|
|
5
7
|
export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
|
|
6
8
|
export type PublishFileExclusiveResult = {
|
|
7
9
|
method: "hardlink" | "exclusive-copy" | "rename-noreplace";
|
|
8
10
|
/** Linux auto fallback; exclusive and identity-checked, but not crash-atomic. */
|
|
9
11
|
fallback?: "link-unlink";
|
|
10
12
|
identity: Stats;
|
|
13
|
+
exactIdentity: ExactFileIdentity;
|
|
11
14
|
directorySync: DirectorySyncOutcome;
|
|
12
15
|
};
|
|
13
|
-
export declare function isHardlinkFallbackError(error: unknown): boolean;
|
|
14
16
|
export declare function publishFileExclusive(params: {
|
|
15
17
|
sourcePath: string;
|
|
16
18
|
targetPath: string;
|
|
17
|
-
expectedSourceIdentity?:
|
|
19
|
+
expectedSourceIdentity?: FileIdentityInput;
|
|
18
20
|
strategy: PublishFileExclusiveStrategy;
|
|
19
21
|
onSyncFailure?: PublishFileExclusiveSyncFailurePolicy;
|
|
20
22
|
parentReceipt?: DirectoryReceipt<Stats | BigIntStats>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exactFileIdentity, numberStats } from "./exact-identity.js";
|
|
1
2
|
import { nativeMoveNoReplace } from "./native-move-fallback.js";
|
|
2
3
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
@@ -9,9 +10,9 @@ import { pinDirectory, } from "./directory-durability.js";
|
|
|
9
10
|
import { FsSafeError } from "./errors.js";
|
|
10
11
|
import { ownDirectoryReceipt } from "./directory-receipt.js";
|
|
11
12
|
import { hashFileHandle } from "./file-hash.js";
|
|
12
|
-
import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
|
|
13
|
+
import { sameFileIdentity, sameFileIdentityForCleanup, hasSafeFileIdentity, identityFromReceipt, } from "./file-identity.js";
|
|
13
14
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
14
|
-
import { getNativeBinding, requireNativeBinding } from "./native.js";
|
|
15
|
+
import { getNativeBinding, requireNativeBinding, selectNativeBinding } from "./native.js";
|
|
15
16
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
16
17
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
17
18
|
import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
|
|
@@ -19,11 +20,9 @@ import { admitStandalonePublicationPath, assertNoWindowsPathAlias } from "./wind
|
|
|
19
20
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
20
21
|
import { hasErrorCode } from "./file-cleanup.js";
|
|
21
22
|
import { writeAllToFile } from "./write-file-handle.js";
|
|
22
|
-
|
|
23
|
+
import { isHardlinkFallbackError } from "./hardlink-fallback.js";
|
|
24
|
+
export { isHardlinkFallbackError } from "./hardlink-fallback.js";
|
|
23
25
|
const NATIVE_COPY_FALLBACK_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV"]);
|
|
24
|
-
export function isHardlinkFallbackError(error) {
|
|
25
|
-
return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
|
|
26
|
-
}
|
|
27
26
|
function directoryOpenFlags() {
|
|
28
27
|
return (fsSync.constants.O_RDONLY |
|
|
29
28
|
(typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
|
|
@@ -78,13 +77,16 @@ async function copyPinnedSource(params) {
|
|
|
78
77
|
if (params.native && params.targetNativeParent) {
|
|
79
78
|
const closeFd = captureNativeFdClose(params.native);
|
|
80
79
|
for (const method of ["clone", "copy-file-range"]) {
|
|
80
|
+
const native = selectNativeBinding(params.native, method === "clone" ? "cloneFileExclusive" : "copyFileRangeExclusive");
|
|
81
|
+
if (!native)
|
|
82
|
+
continue;
|
|
81
83
|
let nativeFd;
|
|
82
84
|
try {
|
|
83
85
|
if (method === "clone") {
|
|
84
|
-
nativeFd =
|
|
86
|
+
nativeFd = native.cloneFileExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
|
|
85
87
|
}
|
|
86
88
|
else {
|
|
87
|
-
const copied = await
|
|
89
|
+
const copied = await native.copyFileRangeExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
|
|
88
90
|
if (copied.errorCode) {
|
|
89
91
|
throw Object.assign(new Error(copied.errorMessage ?? "native copy failed"), {
|
|
90
92
|
code: copied.errorCode,
|
|
@@ -184,7 +186,7 @@ async function removeCreatedTargetIfUnchanged(targetPath, identity) {
|
|
|
184
186
|
async function syncPublishedParent(params) {
|
|
185
187
|
params.failure.phase = "directory-sync";
|
|
186
188
|
try {
|
|
187
|
-
await getFsSafeTestHooks()?.beforePublishDirectorySync?.(params.method, params.targetPath, params.failure.
|
|
189
|
+
await getFsSafeTestHooks()?.beforePublishDirectorySync?.(params.method, params.targetPath, params.failure.exactTargetIdentity);
|
|
188
190
|
// PinnedDirectory.sync() revalidates descriptor/path identity immediately
|
|
189
191
|
// before and after fsync; keep that check inside the shared sync boundary.
|
|
190
192
|
return await params.parent.sync();
|
|
@@ -195,6 +197,12 @@ async function syncPublishedParent(params) {
|
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
export async function publishFileExclusive(params) {
|
|
200
|
+
const expectedInput = params.expectedSourceIdentity;
|
|
201
|
+
const expected = expectedInput && identityFromReceipt(expectedInput);
|
|
202
|
+
const expectedSourceIdentity = expected && { dev: expected.dev, ino: expected.ino };
|
|
203
|
+
if (expectedSourceIdentity && !hasSafeFileIdentity(expectedSourceIdentity)) {
|
|
204
|
+
throw new FsSafeError("path-mismatch", "publication source identity is not exact; pass fs.stat(path, { bigint: true })");
|
|
205
|
+
}
|
|
198
206
|
const sourcePathInput = admitStandalonePublicationPath(params.sourcePath, "publication source uses a Windows filesystem namespace alias");
|
|
199
207
|
const targetPathInput = admitStandalonePublicationPath(params.targetPath, "publication target uses a Windows filesystem namespace alias");
|
|
200
208
|
const parentReceiptInput = params.parentReceipt;
|
|
@@ -234,18 +242,13 @@ export async function publishFileExclusive(params) {
|
|
|
234
242
|
parent = await pinDirectory(parentReceipt ?? parentPath, {
|
|
235
243
|
label: "publication parent",
|
|
236
244
|
});
|
|
237
|
-
const sourceIdentity = fsSync.fstatSync(source.fd);
|
|
238
245
|
const sourceExactIdentity = fsSync.fstatSync(source.fd, { bigint: true });
|
|
239
246
|
const sourcePathExactIdentity = fsSync.lstatSync(sourcePath, { bigint: true });
|
|
240
|
-
const expectedSourceIdentity = params.expectedSourceIdentity;
|
|
241
247
|
if (sourcePathExactIdentity.isSymbolicLink() ||
|
|
242
248
|
!sourcePathExactIdentity.isFile() ||
|
|
243
249
|
!sameFileIdentity(sourcePathExactIdentity, sourceExactIdentity) ||
|
|
244
250
|
(expectedSourceIdentity &&
|
|
245
|
-
!sameFileIdentity(expectedSourceIdentity,
|
|
246
|
-
typeof expectedSourceIdentity.ino === "bigint"
|
|
247
|
-
? sourceExactIdentity
|
|
248
|
-
: sourceIdentity))) {
|
|
251
|
+
!sameFileIdentity(expectedSourceIdentity, sourceExactIdentity))) {
|
|
249
252
|
throw new FsSafeError("path-mismatch", "publication source identity did not match");
|
|
250
253
|
}
|
|
251
254
|
const assertSourceCurrent = () => assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
|
|
@@ -258,19 +261,22 @@ export async function publishFileExclusive(params) {
|
|
|
258
261
|
else
|
|
259
262
|
assertSourceCurrent();
|
|
260
263
|
assertPublishedTargetCurrent(targetPath, identity, "publication target changed during directory sync");
|
|
261
|
-
|
|
264
|
+
const observed = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
265
|
+
if (!sameFileIdentity(observed, identity))
|
|
266
|
+
throw new FsSafeError("path-mismatch", "publication descriptor changed");
|
|
267
|
+
return { method, ...(failure.fallback ? { fallback: failure.fallback } : {}), identity: numberStats(observed), exactIdentity: exactFileIdentity(observed), directorySync };
|
|
262
268
|
};
|
|
263
269
|
await parent.assertCurrent();
|
|
264
270
|
assertSourceCurrent();
|
|
265
271
|
const native = strategy === "rename-noreplace"
|
|
266
|
-
? requireNativeBinding()
|
|
267
|
-
: getNativeBinding();
|
|
272
|
+
? requireNativeBinding("renameNoReplace")
|
|
273
|
+
: getNativeBinding("linkBeneath");
|
|
268
274
|
if (native) {
|
|
269
275
|
sourceNativeParent = await openNativeParent(sourcePath);
|
|
270
276
|
targetNativeParent = await openNativeParent(targetPath);
|
|
271
277
|
}
|
|
272
278
|
if (strategy === "rename-noreplace") {
|
|
273
|
-
const binding = requireNativeBinding();
|
|
279
|
+
const binding = requireNativeBinding("renameNoReplace");
|
|
274
280
|
try {
|
|
275
281
|
const fallback = nativeMoveNoReplace(binding, [
|
|
276
282
|
sourceNativeParent.handle.fd, sourceNativeParent.basename,
|
|
@@ -313,7 +319,7 @@ export async function publishFileExclusive(params) {
|
|
|
313
319
|
return await finishPublication("rename-noreplace", source, sourceExactIdentity);
|
|
314
320
|
}
|
|
315
321
|
try {
|
|
316
|
-
if (native) {
|
|
322
|
+
if (native?.linkBeneath) {
|
|
317
323
|
native.linkBeneath(sourceNativeParent.handle.fd, sourceNativeParent.basename, targetNativeParent.handle.fd, targetNativeParent.basename);
|
|
318
324
|
}
|
|
319
325
|
else {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExactFileIdentity } from "./exact-identity.js";
|
|
1
2
|
import type { Stats } from "node:fs";
|
|
2
3
|
import type { FileHandle } from "node:fs/promises";
|
|
3
4
|
import type { ContainmentGuarantee } from "./containment.js";
|
|
@@ -6,6 +7,7 @@ export type ReadResult = {
|
|
|
6
7
|
containment: ContainmentGuarantee;
|
|
7
8
|
realPath: string;
|
|
8
9
|
stat: Stats;
|
|
10
|
+
exactIdentity: ExactFileIdentity;
|
|
9
11
|
};
|
|
10
12
|
type OpenedFile = Omit<ReadResult & {
|
|
11
13
|
handle: FileHandle;
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileHandleBoundedWithSize } from "./bounded-read.js";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
export async function readOpenedFileSafely(params) {
|
|
5
5
|
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
6
6
|
if (maxBytes !== undefined && params.opened.stat.size > maxBytes) {
|
|
7
7
|
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got ${params.opened.stat.size})`);
|
|
8
8
|
}
|
|
9
|
-
const buffer = maxBytes
|
|
10
|
-
? await params.opened.handle.readFile()
|
|
11
|
-
: await readFileHandleBounded(params.opened.handle, maxBytes);
|
|
9
|
+
const buffer = await readFileHandleBoundedWithSize(params.opened.handle, maxBytes ?? Infinity, params.opened.stat.size);
|
|
12
10
|
return {
|
|
13
11
|
buffer,
|
|
14
12
|
containment: params.opened.containment,
|
|
15
13
|
realPath: params.opened.realPath,
|
|
16
14
|
stat: params.opened.stat,
|
|
15
|
+
exactIdentity: params.opened.exactIdentity,
|
|
17
16
|
};
|
|
18
17
|
}
|
|
@@ -4,9 +4,41 @@ import { getSystemErrorName } from "node:util";
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { getNativeBinding } from "./native.js";
|
|
6
6
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
7
|
+
import { isWindowsAppContainer } from "./windows-appcontainer.js";
|
|
7
8
|
const runtimePlatform = process.versions.bun ? platform() : undefined;
|
|
8
9
|
const bunPosix = runtimePlatform === "darwin" || runtimePlatform === "linux" ? runtimePlatform : undefined;
|
|
9
10
|
function resolve(input, native) {
|
|
11
|
+
if (native && process.platform === "win32") {
|
|
12
|
+
try {
|
|
13
|
+
return fs.realpathSync.native(input);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code !== "EPERM" || error.syscall !== "realpath")
|
|
17
|
+
throw error;
|
|
18
|
+
let binding;
|
|
19
|
+
try {
|
|
20
|
+
binding = getNativeBinding();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
binding = undefined;
|
|
24
|
+
}
|
|
25
|
+
if (!binding || !isWindowsAppContainer(binding))
|
|
26
|
+
throw error;
|
|
27
|
+
if (!binding?.resolveWindowsCanonicalPath) {
|
|
28
|
+
throw new FsSafeError("helper-unavailable", "Windows canonicalization after a denied DOS-name query requires the native binding", { cause: error });
|
|
29
|
+
}
|
|
30
|
+
let resolved;
|
|
31
|
+
try {
|
|
32
|
+
resolved = binding.resolveWindowsCanonicalPath(input);
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
throw new FsSafeError("path-mismatch", "Windows canonical path cannot be verified safely", { cause });
|
|
36
|
+
}
|
|
37
|
+
if (resolved === null)
|
|
38
|
+
throw error;
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
10
42
|
if (bunPosix) {
|
|
11
43
|
// Bun 1.4.2 normalizes native path components and opens the leaf for reads.
|
|
12
44
|
// The Rust OS resolver preserves permissions, locks and physical spelling.
|