@openclaw/fs-safe 0.15.0 → 0.17.0
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/CHANGELOG.md +86 -0
- package/README.md +36 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +2 -8
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +22 -23
- package/dist/archive-kind.d.ts +0 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +5 -17
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +4 -4
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +1 -0
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +6 -7
- package/dist/archive-tar-stream.d.ts +3 -0
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +56 -37
- package/dist/archive-tar-wasm.d.ts +16 -4
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +134 -34
- package/dist/archive-zip-count.d.ts.map +1 -1
- package/dist/archive-zip-count.js +21 -1
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +23 -1
- package/dist/archive-zip-loader.d.ts +2 -0
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +7 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +7 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +14 -9
- package/dist/byte-view.d.ts +3 -0
- package/dist/byte-view.d.ts.map +1 -0
- package/dist/byte-view.js +13 -0
- package/dist/clone-metadata.d.ts +1 -0
- package/dist/clone-metadata.d.ts.map +1 -1
- package/dist/clone-metadata.js +6 -2
- package/dist/create-directory.d.ts +20 -0
- package/dist/create-directory.d.ts.map +1 -0
- package/dist/create-directory.js +130 -0
- package/dist/create-file-async.d.ts +7 -0
- package/dist/create-file-async.d.ts.map +1 -0
- package/dist/create-file-async.js +121 -0
- package/dist/create-file.d.ts +8 -0
- package/dist/create-file.d.ts.map +1 -0
- package/dist/create-file.js +190 -0
- package/dist/create-owned-file.d.ts +8 -0
- package/dist/create-owned-file.d.ts.map +1 -0
- package/dist/create-owned-file.js +16 -0
- package/dist/create.d.ts +4 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +2 -0
- package/dist/creation-darwin.d.ts +6 -0
- package/dist/creation-darwin.d.ts.map +1 -0
- package/dist/creation-darwin.js +70 -0
- package/dist/creation-file-state.d.ts +19 -0
- package/dist/creation-file-state.d.ts.map +1 -0
- package/dist/creation-file-state.js +118 -0
- package/dist/creation-path.d.ts +21 -0
- package/dist/creation-path.d.ts.map +1 -0
- package/dist/creation-path.js +71 -0
- package/dist/creation-permissions.d.ts +19 -0
- package/dist/creation-permissions.d.ts.map +1 -0
- package/dist/creation-permissions.js +125 -0
- package/dist/directory-durability.d.ts +7 -7
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +22 -80
- package/dist/directory-guard.d.ts +3 -0
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-mode-node.d.ts +2 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +8 -0
- package/dist/directory-receipt.d.ts +24 -0
- package/dist/directory-receipt.d.ts.map +1 -0
- package/dist/directory-receipt.js +123 -0
- package/dist/file-cleanup.d.ts +20 -0
- package/dist/file-cleanup.d.ts.map +1 -0
- package/dist/file-cleanup.js +81 -0
- package/dist/file-contents.d.ts +6 -0
- package/dist/file-contents.d.ts.map +1 -0
- package/dist/file-contents.js +40 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +16 -4
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
- package/dist/file-lock-sync-root-acquire.js +3 -0
- package/dist/file-lock-sync-root-held.d.ts +1 -2
- package/dist/file-lock-sync-root-held.d.ts.map +1 -1
- package/dist/file-lock-sync-root-held.js +7 -5
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
- package/dist/file-lock-sync-stale-admission.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -11
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -6
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +3 -9
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +2 -5
- package/dist/file-store.js +3 -3
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +27 -19
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +2 -5
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +2 -6
- package/dist/json-durable-queue-paths.d.ts.map +1 -1
- package/dist/json-durable-queue-paths.js +2 -24
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +10 -9
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -75
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +19 -21
- package/dist/move-path-cleanup.d.ts +5 -19
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +57 -21
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +63 -40
- package/dist/native-binding.d.ts +11 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-fallback-warning.d.ts +4 -0
- package/dist/native-fallback-warning.d.ts.map +1 -0
- package/dist/native-fallback-warning.js +11 -0
- package/dist/native-operations.d.ts +0 -2
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -24
- package/dist/native-parent-admission.d.ts +2 -0
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +3 -2
- package/dist/native-pinned-write-windows.d.ts +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +173 -28
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +19 -3
- package/dist/native-policy-parent-windows.d.ts.map +1 -1
- package/dist/native-policy-parent-windows.js +15 -6
- package/dist/native-staged-file.d.ts +5 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +90 -40
- package/dist/native.js +2 -2
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +11 -2
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +10 -4
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -17
- package/dist/pinned-write-input.d.ts +4 -0
- package/dist/pinned-write-input.d.ts.map +1 -0
- package/dist/pinned-write-input.js +35 -0
- package/dist/pinned-write-mode.d.ts +5 -0
- package/dist/pinned-write-mode.d.ts.map +1 -0
- package/dist/pinned-write-mode.js +31 -0
- package/dist/pinned-write-staged.d.ts +6 -0
- package/dist/pinned-write-staged.d.ts.map +1 -0
- package/dist/pinned-write-staged.js +186 -0
- package/dist/pinned-write-types.d.ts +3 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +41 -147
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +18 -4
- package/dist/private-producer-handoff-sync.d.ts +14 -0
- package/dist/private-producer-handoff-sync.d.ts.map +1 -0
- package/dist/private-producer-handoff-sync.js +114 -0
- package/dist/private-producer-handoff.d.ts +22 -4
- package/dist/private-producer-handoff.d.ts.map +1 -1
- package/dist/private-producer-handoff.js +140 -77
- package/dist/publish-copy-stage.d.ts +2 -1
- package/dist/publish-copy-stage.d.ts.map +1 -1
- package/dist/publish-copy-stage.js +16 -7
- package/dist/publish-file.d.ts +2 -2
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +58 -98
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +35 -44
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +28 -26
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +13 -22
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +10 -16
- package/dist/replace-file-temp-owner.d.ts +0 -7
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +9 -60
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +9 -13
- package/dist/root-create-input.d.ts +2 -1
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +13 -4
- package/dist/root-directory-creation.d.ts +3 -3
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +15 -3
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +20 -3
- package/dist/root-file-final-admission.d.ts +1 -1
- package/dist/root-file-final-admission.d.ts.map +1 -1
- package/dist/root-file-final-admission.js +5 -2
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +3 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +78 -27
- package/dist/root-move-noreplace.d.ts +2 -0
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +22 -13
- package/dist/root-options.d.ts +12 -4
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-stat.d.ts.map +1 -1
- package/dist/root-path-stat.js +59 -7
- package/dist/root-read-admission.d.ts.map +1 -1
- package/dist/root-read-admission.js +7 -2
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +15 -1
- package/dist/root-write-publication.js +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secure-file-windows.d.ts +6 -0
- package/dist/secure-file-windows.d.ts.map +1 -1
- package/dist/secure-file-windows.js +34 -117
- package/dist/secure-file.js +2 -2
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +4 -6
- package/dist/sidecar-lock-handle.d.ts +3 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +6 -0
- package/dist/sidecar-lock-reclaim.d.ts +1 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +11 -8
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +2 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +3 -5
- package/dist/staged-directory.d.ts +2 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +6 -6
- package/dist/staged-file-settlement.d.ts +17 -0
- package/dist/staged-file-settlement.d.ts.map +1 -0
- package/dist/staged-file-settlement.js +57 -0
- package/dist/strict-file-identity.d.ts +1 -1
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +9 -9
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +2 -27
- package/dist/temp-workspace-owner.js +4 -4
- package/dist/unicode-path.d.ts.map +1 -1
- package/dist/unicode-path.js +3 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +4 -2
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -1
- package/dist/windows-security-bridge.cs +336 -0
- package/dist/windows-security-bridge.ps1 +15 -0
- package/dist/windows-security-command.d.ts +26 -0
- package/dist/windows-security-command.d.ts.map +1 -0
- package/dist/windows-security-command.js +363 -0
- package/dist/windows-security-facts.d.ts +6 -0
- package/dist/windows-security-facts.d.ts.map +1 -0
- package/dist/windows-security-facts.js +108 -0
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +23 -0
- package/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -8
- package/dist/write-queue.d.ts.map +1 -1
- package/dist/write-queue.js +1 -4
- package/docs/advanced.md +71 -2
- package/docs/archive.md +102 -39
- package/docs/atomic.md +29 -5
- package/docs/config.md +6 -2
- package/docs/contributing.md +48 -4
- package/docs/copy.md +2 -0
- package/docs/creation.md +132 -0
- package/docs/durability.md +59 -0
- package/docs/file-contents.md +68 -0
- package/docs/install.md +31 -7
- package/docs/json.md +5 -4
- package/docs/local-roots.md +2 -0
- package/docs/migrating-to-0.5.md +15 -6
- package/docs/migrating-to-0.6.md +9 -4
- package/docs/mutation-policy-proof.md +5 -3
- package/docs/native-helper.md +22 -9
- package/docs/native.md +47 -15
- package/docs/path.md +4 -4
- package/docs/permissions.md +37 -14
- package/docs/public-api.md +5 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +2 -2
- package/docs/regular-file.md +3 -0
- package/docs/root.md +43 -0
- package/docs/secret-file.md +11 -2
- package/docs/secure-file.md +9 -4
- package/docs/sidecar-lock.md +14 -5
- package/docs/staged-file.md +9 -3
- package/docs/store.md +3 -1
- package/docs/temp.md +4 -1
- package/docs/types.md +18 -2
- package/docs/walk.md +7 -0
- package/docs/writing.md +80 -7
- package/package.json +18 -15
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fsSync, {} from "node:fs";
|
|
3
|
+
import fs, {} from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createFileHandle } from "./create.js";
|
|
6
|
+
import { hasPreservedCreationArtifacts } from "./creation-file-state.js";
|
|
7
|
+
import { creationAdmissionFromParent } from "./creation-path.js";
|
|
8
|
+
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
9
|
+
import { FsSafeError } from "./errors.js";
|
|
10
|
+
import { sameFileIdentity, sha256Hex } from "./file-identity.js";
|
|
11
|
+
import { syncFileBestEffort } from "./file-sync.js";
|
|
12
|
+
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
13
|
+
import { writePinnedInput } from "./pinned-write-input.js";
|
|
14
|
+
import { assertPinnedWriteMode, pinnedWriteModeAssertion, preparePinnedWriteMode } from "./pinned-write-mode.js";
|
|
15
|
+
import { publishCopyStage } from "./publish-copy-stage.js";
|
|
16
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
17
|
+
import { cleanupPinnedFilePath } from "./file-cleanup.js";
|
|
18
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
19
|
+
import { createStagedFileReceipt, settleStagedFile } from "./staged-file-settlement.js";
|
|
20
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
21
|
+
export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
|
|
22
|
+
const verifyPosixMode = process.platform !== "win32" &&
|
|
23
|
+
(params.verifyPosixMode === true || params.private === true);
|
|
24
|
+
const targetPath = path.join(parentPath, params.basename);
|
|
25
|
+
// Private staging must not consume the destination basename's filename budget.
|
|
26
|
+
const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
|
|
27
|
+
const tempFlags = fsSync.constants.O_WRONLY |
|
|
28
|
+
fsSync.constants.O_CREAT |
|
|
29
|
+
fsSync.constants.O_EXCL |
|
|
30
|
+
(process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
31
|
+
? fsSync.constants.O_NOFOLLOW
|
|
32
|
+
: 0);
|
|
33
|
+
let handle;
|
|
34
|
+
let tempStat;
|
|
35
|
+
let tempIdentity;
|
|
36
|
+
let readHandle;
|
|
37
|
+
let renamed = false;
|
|
38
|
+
const completeCreate = params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true;
|
|
39
|
+
let receipt;
|
|
40
|
+
let publication = Object.freeze({ status: "not-published" });
|
|
41
|
+
let phase = "prepare";
|
|
42
|
+
let failure;
|
|
43
|
+
try {
|
|
44
|
+
params.assertBeforeMutation?.();
|
|
45
|
+
handle = params.private
|
|
46
|
+
? await createFileHandle(tempPath, {
|
|
47
|
+
private: true, mode: 0o600, assertBeforeMutation: params.assertBeforeMutation,
|
|
48
|
+
}, creationAdmissionFromParent(parentGuard))
|
|
49
|
+
: await fs.open(tempPath, tempFlags, verifyPosixMode ? 0o600 : params.mode);
|
|
50
|
+
let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
51
|
+
tempIdentity = verificationIdentity;
|
|
52
|
+
let assertBeforeMutation = params.assertBeforeMutation;
|
|
53
|
+
const assertBeforeWrite = verifyPosixMode
|
|
54
|
+
? await preparePinnedWriteMode(handle, Number(tempIdentity.mode & 4095n), assertBeforeMutation, params.private)
|
|
55
|
+
: assertBeforeMutation;
|
|
56
|
+
if (verifyPosixMode)
|
|
57
|
+
assertPinnedWriteMode(handle.fd, 0o600, params.private);
|
|
58
|
+
await writePinnedInput(handle, params.input, params.maxBytes, assertBeforeWrite);
|
|
59
|
+
if (verifyPosixMode)
|
|
60
|
+
assertPinnedWriteMode(handle.fd, 0o600, params.private);
|
|
61
|
+
tempStat = fsSync.fstatSync(handle.fd);
|
|
62
|
+
const tempPathStat = fsSync.lstatSync(tempPath);
|
|
63
|
+
if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
|
|
64
|
+
throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
|
|
65
|
+
}
|
|
66
|
+
const expectedTempStat = tempStat;
|
|
67
|
+
await handle.chmod(params.mode);
|
|
68
|
+
if (verifyPosixMode) {
|
|
69
|
+
assertPinnedWriteMode(handle.fd, params.mode, params.private);
|
|
70
|
+
assertBeforeMutation = pinnedWriteModeAssertion(handle.fd, params.mode, params.assertBeforeMutation, params.private);
|
|
71
|
+
}
|
|
72
|
+
if (params.sync !== false) {
|
|
73
|
+
if (params.strictFileSync)
|
|
74
|
+
await handle.sync();
|
|
75
|
+
else
|
|
76
|
+
await syncFileBestEffort(handle);
|
|
77
|
+
}
|
|
78
|
+
if (params.input.kind === "file")
|
|
79
|
+
await params.input.verifySource();
|
|
80
|
+
if (completeCreate) {
|
|
81
|
+
receipt = createStagedFileReceipt(Object.freeze({
|
|
82
|
+
path: parentGuard.dir,
|
|
83
|
+
realPath: parentGuard.realPath,
|
|
84
|
+
identity: Object.freeze({ dev: parentGuard.stat.dev, ino: parentGuard.stat.ino }),
|
|
85
|
+
}), path.basename(tempPath), fsSync.fstatSync(handle.fd, { bigint: true }));
|
|
86
|
+
}
|
|
87
|
+
let verifiedIdentity = expectedTempStat;
|
|
88
|
+
phase = "publish";
|
|
89
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
90
|
+
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
91
|
+
if (params.overwrite === false) {
|
|
92
|
+
publishCopyStage({
|
|
93
|
+
temporaryPath: tempPath, targetPath, fd: handle.fd,
|
|
94
|
+
identity: tempIdentity, parentGuard,
|
|
95
|
+
assertBeforeMutation,
|
|
96
|
+
onPublicationAttempt: () => {
|
|
97
|
+
if (receipt)
|
|
98
|
+
publication = Object.freeze({ status: "indeterminate", basename: params.basename, overwrite: false });
|
|
99
|
+
},
|
|
100
|
+
onPublished: (identity) => {
|
|
101
|
+
renamed = true;
|
|
102
|
+
if (receipt)
|
|
103
|
+
publication = Object.freeze({ status: "published", staged: receipt, basename: params.basename, overwrite: false });
|
|
104
|
+
params.onPublished?.(identity);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
assertBeforeMutation?.();
|
|
110
|
+
await fs.rename(tempPath, targetPath);
|
|
111
|
+
renamed = true;
|
|
112
|
+
params.onPublished?.(verificationIdentity);
|
|
113
|
+
}
|
|
114
|
+
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
115
|
+
if (params.sync !== false)
|
|
116
|
+
await syncDirectoryBestEffort(parentPath);
|
|
117
|
+
const targetStat = fsSync.lstatSync(targetPath);
|
|
118
|
+
if (targetStat.isSymbolicLink()) {
|
|
119
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
120
|
+
}
|
|
121
|
+
if (!sameFileIdentity(targetStat, expectedTempStat)) {
|
|
122
|
+
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
123
|
+
// different inode from the source temp fd even with zero concurrency. The
|
|
124
|
+
// caller must ensure mutual exclusion before passing "verify-content";
|
|
125
|
+
// fall back to a content hash for this rename-boundary check only.
|
|
126
|
+
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
127
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
128
|
+
}
|
|
129
|
+
if (params.input.kind !== "buffer") {
|
|
130
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
131
|
+
}
|
|
132
|
+
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
133
|
+
readHandle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
134
|
+
const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
|
|
135
|
+
const actualHash = sha256Hex(await readHandle.readFile());
|
|
136
|
+
if (actualHash !== expectedHash) {
|
|
137
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
138
|
+
}
|
|
139
|
+
// The content-verified destination, not the old temp inode, is now pinned.
|
|
140
|
+
verificationIdentity = readHandleStat;
|
|
141
|
+
// Preserve the helper's legacy numeric return facts, not the private proof.
|
|
142
|
+
verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
|
|
146
|
+
if (verifyPosixMode)
|
|
147
|
+
assertPinnedWriteMode((readHandle ?? handle).fd, params.mode, params.private);
|
|
148
|
+
return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
failure = { error };
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
const preservedPreparation = params.private && !handle && hasPreservedCreationArtifacts(failure?.error);
|
|
156
|
+
if (completeCreate || preservedPreparation) {
|
|
157
|
+
await settleStagedFile({
|
|
158
|
+
temporaryBasename: path.basename(tempPath), publication, phase, failure,
|
|
159
|
+
cleanup: async () => {
|
|
160
|
+
if (preservedPreparation)
|
|
161
|
+
return "preserved";
|
|
162
|
+
if (publication.status === "indeterminate")
|
|
163
|
+
return "preserved";
|
|
164
|
+
if (renamed) {
|
|
165
|
+
return failure?.error instanceof FsSafeError && failure.error.details?.cleanup === "failed"
|
|
166
|
+
? "failed" : "not-needed";
|
|
167
|
+
}
|
|
168
|
+
return handle ? await cleanupPinnedFilePath({
|
|
169
|
+
pathname: tempPath, handle, identity: tempIdentity, parentGuard, throwOnCleanupError: true,
|
|
170
|
+
}) : "not-needed";
|
|
171
|
+
},
|
|
172
|
+
close: [async () => { await readHandle?.close(); }, async () => { await handle?.close(); }],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
try {
|
|
177
|
+
if (!renamed && handle) {
|
|
178
|
+
await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await readHandle?.close().catch(() => undefined);
|
|
183
|
+
await handle?.close().catch(() => undefined);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -62,7 +62,10 @@ export type PinnedWriteParams = {
|
|
|
62
62
|
basename: string;
|
|
63
63
|
mkdir: boolean;
|
|
64
64
|
mode: number;
|
|
65
|
+
private?: boolean;
|
|
66
|
+
verifyPosixMode?: boolean;
|
|
65
67
|
sync?: boolean;
|
|
68
|
+
strictFileSync?: boolean;
|
|
66
69
|
overwrite?: boolean;
|
|
67
70
|
assertBeforeMutation?: () => void;
|
|
68
71
|
rejectFinalSymlink?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAgB3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACvF,YAAY,EACV,gBAAgB,EAAE,oBAAoB,EAAE,sBAAsB,EAC9D,8BAA8B,EAAE,gCAAgC,EAChE,6BAA6B,EAAE,+BAA+B,EAC9D,4BAA4B,EAAE,iBAAiB,GAChD,MAAM,yBAAyB,CAAC;AAkDjC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsD/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAED,wBAAsB,iCAAiC,CAAC,CAAC,EACvD,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,EAC5E,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
import { syncFileBestEffort } from "./file-sync.js";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import fsSync, {} from "node:fs";
|
|
4
3
|
import fs from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { createFileHandle } from "./create.js";
|
|
6
|
+
import { creationAdmissionFromParent } from "./creation-path.js";
|
|
6
7
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
7
8
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
9
|
import { FsSafeError } from "./errors.js";
|
|
9
10
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
10
|
-
import {
|
|
11
|
+
import { sha256Hex } from "./file-identity.js";
|
|
11
12
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
12
13
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
13
14
|
import { runPinnedWriteNative } from "./native-pinned-write.js";
|
|
14
15
|
import { getNativeBinding } from "./native.js";
|
|
15
16
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
16
|
-
import {
|
|
17
|
-
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
17
|
+
import { cleanupPinnedFilePath } from "./file-cleanup.js";
|
|
18
18
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
19
19
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
20
|
-
import {
|
|
20
|
+
import { writePinnedInput } from "./pinned-write-input.js";
|
|
21
|
+
import { assertPinnedWriteMode, preparePinnedWriteMode } from "./pinned-write-mode.js";
|
|
22
|
+
import { runPinnedStagedWrite } from "./pinned-write-staged.js";
|
|
21
23
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
22
|
-
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
23
|
-
import { publishCopyStage } from "./publish-copy-stage.js";
|
|
24
24
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
25
|
-
function byteLength(input, encoding) {
|
|
26
|
-
return typeof input === "string"
|
|
27
|
-
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
28
|
-
: input.byteLength;
|
|
29
|
-
}
|
|
30
25
|
function assertSafeBasename(basename) {
|
|
31
26
|
if (!basename ||
|
|
32
27
|
basename === "." ||
|
|
@@ -37,24 +32,10 @@ function assertSafeBasename(basename) {
|
|
|
37
32
|
throw new FsSafeError("invalid-path", "invalid target path");
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
|
-
function assertWithinMaxBytes(bytes, maxBytes) {
|
|
41
|
-
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
42
|
-
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
35
|
function fastParentGuardDeopt(error) {
|
|
46
36
|
return error instanceof FsSafeError &&
|
|
47
37
|
(error.code === "path-mismatch" || error.code === "not-file");
|
|
48
38
|
}
|
|
49
|
-
async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
|
|
50
|
-
let bytes = 0;
|
|
51
|
-
for await (const chunk of stream) {
|
|
52
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
53
|
-
bytes += buffer.byteLength;
|
|
54
|
-
assertWithinMaxBytes(bytes, maxBytes);
|
|
55
|
-
await writeAllToFile(handle, buffer, { assertBeforeMutation });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
39
|
const PINNED_WRITE_SNAPSHOT_KEYS = [
|
|
59
40
|
"rootPath", "relativeParentPath", "basename", "maxBytes", "rootIdentity",
|
|
60
41
|
];
|
|
@@ -142,6 +123,8 @@ export async function withPinnedWriteRenameIdentityLock(params, run) {
|
|
|
142
123
|
}, run);
|
|
143
124
|
}
|
|
144
125
|
async function runPinnedWriteFallback(params) {
|
|
126
|
+
const verifyPosixMode = process.platform !== "win32" &&
|
|
127
|
+
(params.verifyPosixMode === true || params.private === true);
|
|
145
128
|
const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
|
|
146
129
|
? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
|
|
147
130
|
const mutationAdmission = params.mutationAdmission;
|
|
@@ -246,15 +229,19 @@ async function runPinnedWriteFallback(params) {
|
|
|
246
229
|
}));
|
|
247
230
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
248
231
|
}
|
|
249
|
-
if (params.overwrite === false &&
|
|
250
|
-
(params.input.kind === "stream" && !params.input.stageBeforePublish))) {
|
|
232
|
+
if (params.overwrite === false && params.input.kind !== "file" && !params.input.stageBeforePublish) {
|
|
251
233
|
const assertBeforeMutation = () => {
|
|
252
234
|
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
253
235
|
params.assertBeforeMutation?.();
|
|
254
236
|
};
|
|
255
237
|
const handle = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
256
238
|
assertBeforeMutation();
|
|
257
|
-
|
|
239
|
+
if (params.private) {
|
|
240
|
+
return await createFileHandle(targetPath, {
|
|
241
|
+
private: true, mode: 0o600, assertBeforeMutation,
|
|
242
|
+
}, creationAdmissionFromParent(parentGuard));
|
|
243
|
+
}
|
|
244
|
+
return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, verifyPosixMode ? 0o600 : params.mode);
|
|
258
245
|
}, {
|
|
259
246
|
onPostGuardFailure: async (openedHandle) => {
|
|
260
247
|
// The parent failed verification, so targetPath may now resolve
|
|
@@ -263,29 +250,38 @@ async function runPinnedWriteFallback(params) {
|
|
|
263
250
|
},
|
|
264
251
|
});
|
|
265
252
|
let created = true;
|
|
253
|
+
let completed = false;
|
|
266
254
|
let createdIdentity;
|
|
267
255
|
try {
|
|
268
256
|
const verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
269
257
|
createdIdentity = verificationIdentity;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
258
|
+
const assertBeforeWrite = verifyPosixMode
|
|
259
|
+
? await preparePinnedWriteMode(handle, Number(createdIdentity.mode & 4095n), assertBeforeMutation, params.private)
|
|
260
|
+
: assertBeforeMutation;
|
|
261
|
+
if (verifyPosixMode)
|
|
262
|
+
assertPinnedWriteMode(handle.fd, 0o600, params.private);
|
|
263
|
+
await writePinnedInput(handle, params.input, params.maxBytes, assertBeforeWrite);
|
|
264
|
+
if (verifyPosixMode)
|
|
265
|
+
assertPinnedWriteMode(handle.fd, 0o600, params.private);
|
|
279
266
|
// Content writes may clear set-ID bits; finalize them through the owned fd.
|
|
280
267
|
await handle.chmod(params.mode);
|
|
281
|
-
if (
|
|
282
|
-
|
|
268
|
+
if (verifyPosixMode)
|
|
269
|
+
assertPinnedWriteMode(handle.fd, params.mode, params.private);
|
|
270
|
+
if (params.sync !== false) {
|
|
271
|
+
if (params.strictFileSync)
|
|
272
|
+
await handle.sync();
|
|
273
|
+
else
|
|
274
|
+
await syncFileBestEffort(handle);
|
|
275
|
+
}
|
|
283
276
|
const stat = fsSync.fstatSync(handle.fd);
|
|
284
277
|
if (params.sync !== false)
|
|
285
278
|
await syncDirectoryBestEffort(parentPath);
|
|
286
279
|
// Publication is complete. A failed outer check must not remove its target.
|
|
287
280
|
created = false;
|
|
288
281
|
await params.verifyPublished?.(handle.fd, verificationIdentity, parentGuard);
|
|
282
|
+
if (verifyPosixMode)
|
|
283
|
+
assertPinnedWriteMode(handle.fd, params.mode, params.private);
|
|
284
|
+
completed = true;
|
|
289
285
|
return { dev: stat.dev, ino: stat.ino };
|
|
290
286
|
}
|
|
291
287
|
finally {
|
|
@@ -295,114 +291,12 @@ async function runPinnedWriteFallback(params) {
|
|
|
295
291
|
}
|
|
296
292
|
}
|
|
297
293
|
finally {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
// Private staging must not consume the destination basename's filename budget.
|
|
303
|
-
const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
|
|
304
|
-
const tempFlags = fsSync.constants.O_WRONLY |
|
|
305
|
-
fsSync.constants.O_CREAT |
|
|
306
|
-
fsSync.constants.O_EXCL |
|
|
307
|
-
(process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
308
|
-
? fsSync.constants.O_NOFOLLOW
|
|
309
|
-
: 0);
|
|
310
|
-
let handle;
|
|
311
|
-
let tempStat;
|
|
312
|
-
let tempIdentity;
|
|
313
|
-
let readHandle;
|
|
314
|
-
let renamed = false;
|
|
315
|
-
try {
|
|
316
|
-
params.assertBeforeMutation?.();
|
|
317
|
-
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
318
|
-
let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
319
|
-
tempIdentity = verificationIdentity;
|
|
320
|
-
if (params.input.kind === "buffer") {
|
|
321
|
-
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
322
|
-
await writeAllToFile(handle, params.input.data, {
|
|
323
|
-
encoding: params.input.encoding, assertBeforeMutation: params.assertBeforeMutation,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
else if (params.input.kind === "file") {
|
|
327
|
-
await writeCopyFileToFd(handle.fd, params.input, params.maxBytes, params.assertBeforeMutation);
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
await writeStreamToHandle(params.input.stream, handle, params.maxBytes, params.assertBeforeMutation);
|
|
331
|
-
}
|
|
332
|
-
tempStat = fsSync.fstatSync(handle.fd);
|
|
333
|
-
const tempPathStat = fsSync.lstatSync(tempPath);
|
|
334
|
-
if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
|
|
335
|
-
throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
|
|
336
|
-
}
|
|
337
|
-
const expectedTempStat = tempStat;
|
|
338
|
-
await handle.chmod(params.mode);
|
|
339
|
-
if (params.sync !== false)
|
|
340
|
-
await syncFileBestEffort(handle);
|
|
341
|
-
if (params.input.kind === "file")
|
|
342
|
-
await params.input.verifySource();
|
|
343
|
-
let verifiedIdentity = expectedTempStat;
|
|
344
|
-
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
345
|
-
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
346
|
-
if (params.overwrite === false) {
|
|
347
|
-
publishCopyStage({
|
|
348
|
-
temporaryPath: tempPath, targetPath, fd: handle.fd,
|
|
349
|
-
identity: tempIdentity, parentGuard,
|
|
350
|
-
assertBeforeMutation: params.assertBeforeMutation,
|
|
351
|
-
onPublished: (identity) => {
|
|
352
|
-
renamed = true;
|
|
353
|
-
params.onPublished?.(identity);
|
|
354
|
-
},
|
|
355
|
-
});
|
|
294
|
+
if (completed)
|
|
295
|
+
await handle.close();
|
|
296
|
+
else
|
|
297
|
+
await handle.close().catch(() => undefined);
|
|
356
298
|
}
|
|
357
|
-
else {
|
|
358
|
-
params.assertBeforeMutation?.();
|
|
359
|
-
await fs.rename(tempPath, targetPath);
|
|
360
|
-
renamed = true;
|
|
361
|
-
params.onPublished?.(verificationIdentity);
|
|
362
|
-
}
|
|
363
|
-
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
364
|
-
if (params.sync !== false)
|
|
365
|
-
await syncDirectoryBestEffort(parentPath);
|
|
366
|
-
const targetStat = fsSync.lstatSync(targetPath);
|
|
367
|
-
if (targetStat.isSymbolicLink()) {
|
|
368
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
369
|
-
}
|
|
370
|
-
if (!sameFileIdentity(targetStat, expectedTempStat)) {
|
|
371
|
-
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
372
|
-
// different inode from the source temp fd even with zero concurrency. The
|
|
373
|
-
// caller must ensure mutual exclusion before passing "verify-content";
|
|
374
|
-
// fall back to a content hash for this rename-boundary check only.
|
|
375
|
-
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
376
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
377
|
-
}
|
|
378
|
-
if (params.input.kind !== "buffer") {
|
|
379
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
380
|
-
}
|
|
381
|
-
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
382
|
-
readHandle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
383
|
-
const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
|
|
384
|
-
const actualHash = sha256Hex(await readHandle.readFile());
|
|
385
|
-
if (actualHash !== expectedHash) {
|
|
386
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
387
|
-
}
|
|
388
|
-
// The content-verified destination, not the old temp inode, is now pinned.
|
|
389
|
-
verificationIdentity = readHandleStat;
|
|
390
|
-
// Preserve the helper's legacy numeric return facts, not the private proof.
|
|
391
|
-
verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
|
|
395
|
-
return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
|
|
396
|
-
}
|
|
397
|
-
finally {
|
|
398
|
-
try {
|
|
399
|
-
if (!renamed && handle) {
|
|
400
|
-
await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
finally {
|
|
404
|
-
await readHandle?.close().catch(() => undefined);
|
|
405
|
-
await handle?.close().catch(() => undefined);
|
|
406
299
|
}
|
|
407
300
|
}
|
|
301
|
+
return await runPinnedStagedWrite(params, parentPath, parentGuard);
|
|
408
302
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAgCf"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
import { getNativeBinding } from "./native.js";
|
|
4
|
+
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
5
|
+
import { warnNativeFallback } from "./native-fallback-warning.js";
|
|
3
6
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
7
|
+
import { createPrivateWindowsDirectoryCommand } from "./windows-security-command.js";
|
|
4
8
|
export async function createPrivateDirectory(targetPath, options) {
|
|
5
9
|
const platform = options?.platform ?? process.platform;
|
|
6
10
|
if (platform !== "win32") {
|
|
@@ -8,9 +12,19 @@ export async function createPrivateDirectory(targetPath, options) {
|
|
|
8
12
|
}
|
|
9
13
|
assertNoWindowsPathAlias(targetPath, "filesystem", "private directory path uses a Windows filesystem namespace alias", platform);
|
|
10
14
|
const native = getNativeBinding();
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
if (typeof native?.createPrivateDirectory === "function") {
|
|
16
|
+
native.createPrivateDirectory(targetPath);
|
|
17
|
+
return;
|
|
14
18
|
}
|
|
15
|
-
|
|
19
|
+
if (getFsSafeNativeConfig().mode === "require") {
|
|
20
|
+
throw new FsSafeError("helper-unavailable", "private Windows directory creation requires an up-to-date native helper");
|
|
21
|
+
}
|
|
22
|
+
// Preserve the native parser's raw component restrictions before resolve()
|
|
23
|
+
// could discard dot components or Win32 could trim trailing aliases.
|
|
24
|
+
const components = targetPath.replaceAll("/", "\\").split("\\");
|
|
25
|
+
if (components.some(component => component.endsWith(".") || component.endsWith(" "))) {
|
|
26
|
+
throw Object.assign(new Error("private directory path has an ambiguous component"), { code: "EINVAL" });
|
|
27
|
+
}
|
|
28
|
+
warnNativeFallback("windows-private-directory", "Private Windows directory creation uses a slower built-in system command.");
|
|
29
|
+
await createPrivateWindowsDirectoryCommand(path.win32.resolve(targetPath));
|
|
16
30
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
3
|
+
export declare function handoffCreatedFileSync(params: {
|
|
4
|
+
source: OwnedFileDescriptorSync;
|
|
5
|
+
sourcePath: string;
|
|
6
|
+
targetPath: string;
|
|
7
|
+
identity: BigIntStats;
|
|
8
|
+
assertSourceParent: () => void;
|
|
9
|
+
assertTargetParent: () => void;
|
|
10
|
+
assertBeforeMutation?: () => void;
|
|
11
|
+
verifyDescriptor?: (fd: number, path: string, links: number) => void;
|
|
12
|
+
onPublished?: () => void;
|
|
13
|
+
}): OwnedFileDescriptorSync;
|
|
14
|
+
//# sourceMappingURL=private-producer-handoff-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-producer-handoff-sync.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff-sync.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAyB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAW7F,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,uBAAuB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,GAAG,uBAAuB,CA0H1B"}
|