@openclaw/fs-safe 0.9.0 → 0.10.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 +78 -0
- package/README.md +17 -0
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- package/dist/archive-gzip-tail.d.ts +2 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +20 -3
- package/dist/archive-merge.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +3 -2
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +67 -66
- package/dist/archive-tar-stream.d.ts +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +20 -8
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +2 -1
- package/dist/bounded-read.d.ts +7 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +73 -43
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +23 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +91 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +10 -1
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +27 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +191 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +123 -0
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-hash.d.ts +6 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +44 -8
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -12
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +1 -0
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +8 -4
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +2 -6
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +13 -14
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +22 -24
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +55 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +26 -31
- package/dist/mutation-authority.d.ts +8 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +24 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +18 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +1 -0
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +38 -48
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +5 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +50 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +6 -5
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +51 -26
- package/dist/root-directory-list.d.ts +24 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +201 -0
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +3 -0
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +6 -50
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +330 -210
- package/dist/root-options.d.ts +66 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +56 -1
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-symlink-policy.d.ts +13 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +1 -4
- package/dist/root.d.ts +3 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +4 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +2 -1
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +3 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.js +2 -2
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +6 -26
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +6 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +25 -0
- package/docs/advanced.md +18 -4
- package/docs/archive.md +22 -7
- package/docs/atomic.md +7 -0
- package/docs/contributing.md +7 -0
- package/docs/copy.md +86 -0
- package/docs/durability.md +17 -2
- package/docs/errors.md +1 -1
- package/docs/local-roots.md +8 -1
- package/docs/native.md +14 -1
- package/docs/permissions.md +20 -10
- package/docs/positional-read.md +63 -0
- package/docs/root.md +166 -4
- package/docs/security-model.md +14 -0
- package/docs/timing.md +2 -0
- package/docs/types.md +18 -11
- package/docs/walk.md +54 -5
- package/docs/writing.md +20 -2
- package/package.json +13 -8
package/dist/local-roots.js
CHANGED
|
@@ -2,10 +2,9 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { expandHomePrefix,
|
|
5
|
+
import { expandHomePrefix, resolveRequiredHomeDir } from "./home-dir.js";
|
|
6
6
|
import { isFileUrl, safeFileURLToPath } from "./local-file-access.js";
|
|
7
|
-
import {
|
|
8
|
-
import { resolveRootPathSync } from "./root-path.js";
|
|
7
|
+
import { ROOT_PATH_ALIAS_POLICIES, resolveRootPathSync } from "./root-path.js";
|
|
9
8
|
import { root } from "./root.js";
|
|
10
9
|
function resolveLocalPathInput(input, label) {
|
|
11
10
|
if (isFileUrl(input)) {
|
|
@@ -20,7 +19,14 @@ function resolveLocalPathInput(input, label) {
|
|
|
20
19
|
if (input.includes("\0")) {
|
|
21
20
|
throw new FsSafeError("invalid-path", `${label} must not contain NUL bytes`);
|
|
22
21
|
}
|
|
23
|
-
|
|
22
|
+
const homePrefix = input === "~" || input.startsWith("~/") ||
|
|
23
|
+
(path.sep === "\\" && input.startsWith("~\\"));
|
|
24
|
+
const expanded = homePrefix ? `${resolveRequiredHomeDir()}${path.sep}${input.slice(2)}` : input;
|
|
25
|
+
if (path.isAbsolute(expanded))
|
|
26
|
+
return expanded;
|
|
27
|
+
const drive = path.parse(expanded).root;
|
|
28
|
+
const base = drive ? path.resolve(drive) : process.cwd();
|
|
29
|
+
return `${base}${path.sep}${expanded.slice(drive.length)}`;
|
|
24
30
|
}
|
|
25
31
|
function resolveLocalRootInput(input, label) {
|
|
26
32
|
const trimmed = input.trim();
|
|
@@ -54,7 +60,7 @@ function resolveRootRealSync(rootDir) {
|
|
|
54
60
|
}
|
|
55
61
|
export function resolveLocalPathFromRootsSync(options) {
|
|
56
62
|
const label = options.label ?? "local roots";
|
|
57
|
-
const requestedPath =
|
|
63
|
+
const requestedPath = resolveLocalPathInput(options.filePath, "file path");
|
|
58
64
|
const rootDirs = options.roots.map((rootEntry) => resolveLocalRootInput(rootEntry, label));
|
|
59
65
|
for (const rootDir of rootDirs) {
|
|
60
66
|
const rootReal = resolveRootRealSync(rootDir);
|
|
@@ -69,6 +75,7 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
69
75
|
rootCanonicalPath: rootReal,
|
|
70
76
|
boundaryLabel: label,
|
|
71
77
|
rejectUnresolvedSymlinks: true,
|
|
78
|
+
policy: options.requireFile ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
72
79
|
});
|
|
73
80
|
}
|
|
74
81
|
catch {
|
|
@@ -77,11 +84,12 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
77
84
|
if (!candidate.exists && options.allowMissing !== true) {
|
|
78
85
|
continue;
|
|
79
86
|
}
|
|
87
|
+
if (candidate.exists && options.requireFile === true && candidate.kind !== "file")
|
|
88
|
+
continue;
|
|
80
89
|
if (candidate.exists && options.requireFile === true) {
|
|
81
90
|
try {
|
|
82
|
-
if (!fsSync.lstatSync(requestedPath).isFile())
|
|
91
|
+
if (!fsSync.lstatSync(requestedPath).isFile())
|
|
83
92
|
continue;
|
|
84
|
-
}
|
|
85
93
|
}
|
|
86
94
|
catch {
|
|
87
95
|
continue;
|
|
@@ -94,7 +102,7 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
94
102
|
export async function readLocalFileFromRoots(options) {
|
|
95
103
|
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
96
104
|
const label = options.label ?? "local roots";
|
|
97
|
-
const requestedPath =
|
|
105
|
+
const requestedPath = resolveLocalPathInput(options.filePath, "file path");
|
|
98
106
|
const rootDirs = options.roots.map((rootEntry) => resolveLocalRootInput(rootEntry, label));
|
|
99
107
|
for (const rootDir of rootDirs) {
|
|
100
108
|
let scopedRoot;
|
|
@@ -113,22 +121,12 @@ export async function readLocalFileFromRoots(options) {
|
|
|
113
121
|
if (maxBytes !== undefined) {
|
|
114
122
|
readOptions.maxBytes = maxBytes;
|
|
115
123
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.map((rootPath) => path.relative(rootPath, requestedPath))
|
|
123
|
-
.filter(Boolean);
|
|
124
|
-
for (const relativePath of relativePaths) {
|
|
125
|
-
try {
|
|
126
|
-
const result = await scopedRoot.read(relativePath, readOptions);
|
|
127
|
-
return { ...result, root: scopedRoot.rootReal };
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
// Try the canonical spelling before moving to the next configured root.
|
|
131
|
-
}
|
|
124
|
+
try {
|
|
125
|
+
const result = await scopedRoot.readAbsolute(requestedPath, readOptions);
|
|
126
|
+
return { ...result, root: scopedRoot.rootReal };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Root handles both trusted spellings; try the next configured root.
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
132
|
return null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
export declare function createMoveStageOwner(staged: string): Promise<{
|
|
3
|
+
record(created: BigIntStats): void;
|
|
4
|
+
assertCurrent: () => void;
|
|
5
|
+
published(): void;
|
|
6
|
+
cleanup(): Promise<void>;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=move-path-stage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"move-path-stage.d.ts","sourceRoot":"","sources":["../src/move-path-stage.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAQnD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM;oBAkBrC,WAAW,GAAG,IAAI;yBAPV,IAAI;iBAkBf,IAAI;eACA,OAAO,CAAC,IAAI,CAAC;GAajC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { guardedRm, guardedRmSync } from "./guarded-mutation.js";
|
|
6
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
|
+
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
8
|
+
export async function createMoveStageOwner(staged) {
|
|
9
|
+
const parent = await createAsyncDirectoryGuard(path.dirname(staged), { bigint: true });
|
|
10
|
+
let identity;
|
|
11
|
+
let unregister;
|
|
12
|
+
const changed = () => new FsSafeError("path-mismatch", "move staging path changed before publication");
|
|
13
|
+
const assertParent = () => {
|
|
14
|
+
const current = fsSync.lstatSync(parent.dir, { bigint: true });
|
|
15
|
+
if (!current.isDirectory() || current.isSymbolicLink() ||
|
|
16
|
+
!sameFileIdentityForCleanup(current, parent.stat) ||
|
|
17
|
+
fsSync.realpathSync.native(parent.dir) !== parent.realPath)
|
|
18
|
+
throw changed();
|
|
19
|
+
};
|
|
20
|
+
const assertCurrent = () => {
|
|
21
|
+
assertParent();
|
|
22
|
+
if (!identity || !sameFileIdentityForCleanup(fsSync.lstatSync(staged, { bigint: true }), identity)) {
|
|
23
|
+
throw changed();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
record(created) {
|
|
28
|
+
if (!sameFileIdentityForCleanup(created, created))
|
|
29
|
+
throw changed();
|
|
30
|
+
identity = created;
|
|
31
|
+
unregister = registerTempPathForExit(staged, {
|
|
32
|
+
identity,
|
|
33
|
+
cleanupSync: () => {
|
|
34
|
+
guardedRmSync({ target: staged, recursive: created.isDirectory(), assertBeforeMutation: assertCurrent });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
assertCurrent,
|
|
39
|
+
published() { unregister?.(); },
|
|
40
|
+
async cleanup() {
|
|
41
|
+
if (!identity)
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
await guardedRm({ target: staged, recursive: identity.isDirectory(), assertBeforeMutation: assertCurrent });
|
|
45
|
+
unregister?.();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code === "ENOENT" || error instanceof FsSafeError) {
|
|
49
|
+
unregister?.();
|
|
50
|
+
}
|
|
51
|
+
// Retry only the original receipt after transient cleanup I/O failures.
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
package/dist/move-path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,CAAC;AAEH,MAAM,MAAM,+BAA+B,GAAG;IAC5C,qFAAqF;IACrF,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,qFAAqF;IACrF,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE,0BAA0B,KAAK,IAAI,CAAC;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AAiRD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAwGf"}
|
package/dist/move-path.js
CHANGED
|
@@ -2,11 +2,13 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsSync, { constants as fsConstants } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
5
6
|
import { FsSafeError } from "./errors.js";
|
|
6
7
|
import { guardedRename } from "./guarded-mutation.js";
|
|
7
8
|
import { assertSourceStillMatches, cleanupCopiedEntry, createCleanupCopiedEntryState, entryIdentity, sameIdentity, sourceChangedError, } from "./move-path-cleanup.js";
|
|
8
9
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
-
import {
|
|
10
|
+
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
11
|
+
import { createMoveStageOwner } from "./move-path-stage.js";
|
|
10
12
|
export function moveCopyFallbackReasonForRenameError(error, platform = process.platform) {
|
|
11
13
|
const code = error?.code;
|
|
12
14
|
if (code === "EXDEV") {
|
|
@@ -96,11 +98,12 @@ async function writeAll(handle, buffer, bytesRead) {
|
|
|
96
98
|
let offset = 0;
|
|
97
99
|
while (offset < bytesRead) {
|
|
98
100
|
const { bytesWritten } = await handle.write(buffer, offset, bytesRead - offset);
|
|
101
|
+
if (bytesWritten <= 0)
|
|
102
|
+
throw new FsSafeError("helper-failed", "move copy made no progress");
|
|
99
103
|
offset += bytesWritten;
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
async function copyRegularFilePinned(params) {
|
|
103
|
-
let destinationCreated = false;
|
|
104
107
|
let openedIdentity;
|
|
105
108
|
let sourceHandle;
|
|
106
109
|
try {
|
|
@@ -132,9 +135,12 @@ async function copyRegularFilePinned(params) {
|
|
|
132
135
|
throw sourceChangedError(params.from);
|
|
133
136
|
}
|
|
134
137
|
await assertSourceStillMatches(params.from, openedIdentity);
|
|
138
|
+
const parentGuard = await createAsyncDirectoryGuard(path.dirname(params.to), { bigint: true });
|
|
135
139
|
const destinationHandle = await fs.open(params.to, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, modeBits(params.mode) || 0o666);
|
|
136
|
-
|
|
140
|
+
let destinationIdentity;
|
|
137
141
|
try {
|
|
142
|
+
destinationIdentity = fsSync.fstatSync(destinationHandle.fd, { bigint: true });
|
|
143
|
+
params.onCreated?.(destinationIdentity);
|
|
138
144
|
const scratch = Buffer.allocUnsafe(64 * 1024);
|
|
139
145
|
while (true) {
|
|
140
146
|
const { bytesRead } = await sourceHandle.read(scratch, 0, scratch.length, null);
|
|
@@ -154,16 +160,16 @@ async function copyRegularFilePinned(params) {
|
|
|
154
160
|
}
|
|
155
161
|
await destinationHandle.chmod(modeBits(params.mode));
|
|
156
162
|
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
await cleanupPinnedFilePath({
|
|
165
|
+
pathname: params.to, handle: destinationHandle, identity: destinationIdentity, parentGuard,
|
|
166
|
+
});
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
157
169
|
finally {
|
|
158
170
|
await destinationHandle.close();
|
|
159
171
|
}
|
|
160
172
|
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
if (destinationCreated) {
|
|
163
|
-
await fs.rm(params.to, { force: true }).catch(() => undefined);
|
|
164
|
-
}
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
173
|
finally {
|
|
168
174
|
await sourceHandle.close();
|
|
169
175
|
}
|
|
@@ -179,6 +185,7 @@ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
|
179
185
|
const target = await fs.readlink(from);
|
|
180
186
|
const targetType = process.platform === "win32" && fsSync.statSync(from).isDirectory() ? "junction" : undefined;
|
|
181
187
|
await fs.symlink(target, to, targetType);
|
|
188
|
+
options.onCreated?.(fsSync.lstatSync(to, { bigint: true }));
|
|
182
189
|
// readlink() is path-based; verify the symlink we copied is still the one
|
|
183
190
|
// we inspected before letting the staged destination become visible.
|
|
184
191
|
await assertSourceStillMatches(from, identity);
|
|
@@ -186,6 +193,7 @@ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
|
186
193
|
}
|
|
187
194
|
if (sourceStat.isDirectory()) {
|
|
188
195
|
await fs.mkdir(to, { mode: modeBits(sourceStat.mode) || 0o755 });
|
|
196
|
+
options.onCreated?.(fsSync.lstatSync(to, { bigint: true }));
|
|
189
197
|
const children = [];
|
|
190
198
|
const childNames = [];
|
|
191
199
|
const directory = await fs.opendir(from);
|
|
@@ -198,7 +206,9 @@ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
|
198
206
|
for (const child of childNames) {
|
|
199
207
|
children.push({
|
|
200
208
|
name: child,
|
|
201
|
-
manifest: await copyEntryWithManifest(path.join(from, child), path.join(to, child),
|
|
209
|
+
manifest: await copyEntryWithManifest(path.join(from, child), path.join(to, child), {
|
|
210
|
+
sourceHardlinks: options.sourceHardlinks, budget: options.budget,
|
|
211
|
+
}),
|
|
202
212
|
});
|
|
203
213
|
}
|
|
204
214
|
// Directory traversal is path-based in Node. Treat a changed parent as a
|
|
@@ -221,6 +231,7 @@ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
|
|
|
221
231
|
mode: sourceStat.mode,
|
|
222
232
|
rejectHardlinks: options.sourceHardlinks === "reject",
|
|
223
233
|
to,
|
|
234
|
+
onCreated: options.onCreated,
|
|
224
235
|
});
|
|
225
236
|
return { ...copiedIdentity, kind: "leaf" };
|
|
226
237
|
}
|
|
@@ -299,22 +310,22 @@ export async function movePathWithCopyFallback(options) {
|
|
|
299
310
|
const sourceIdentity = await assertCopyDestinationOutsideSource(sourcePath, targetPath);
|
|
300
311
|
const targetDir = path.dirname(targetPath);
|
|
301
312
|
const staged = path.join(targetDir, `.fs-safe-move-${process.pid}-${randomUUID()}.tmp`);
|
|
302
|
-
const
|
|
313
|
+
const stage = await createMoveStageOwner(staged);
|
|
303
314
|
try {
|
|
304
315
|
const manifest = await copyEntryWithManifest(sourcePath, staged, {
|
|
305
316
|
sourceHardlinks: rejectHardlinks ? "reject" : "allow",
|
|
317
|
+
onCreated: stage.record,
|
|
306
318
|
...(rejectHardlinks ? { budget: { discovered: 1 } } : {}),
|
|
307
319
|
}, sourceIdentity);
|
|
308
320
|
const cleanupState = createCleanupCopiedEntryState(sourcePath, manifest);
|
|
309
|
-
unregisterStaged.setIdentity(fsSync.lstatSync(staged, { bigint: true }));
|
|
310
321
|
await assertCopyDestinationOutsideSource(sourcePath, targetPath, manifest);
|
|
311
322
|
await guardedRename({
|
|
312
323
|
from: staged,
|
|
313
324
|
to: targetPath,
|
|
314
|
-
assertBeforeRename,
|
|
325
|
+
assertBeforeRename: () => { assertBeforeRename(); stage.assertCurrent(); },
|
|
315
326
|
onSourceInspected,
|
|
316
327
|
onRenamed: () => {
|
|
317
|
-
|
|
328
|
+
stage.published();
|
|
318
329
|
onRenamed();
|
|
319
330
|
},
|
|
320
331
|
});
|
|
@@ -325,23 +336,7 @@ export async function movePathWithCopyFallback(options) {
|
|
|
325
336
|
}
|
|
326
337
|
finally {
|
|
327
338
|
if (!destinationPublished) {
|
|
328
|
-
|
|
329
|
-
const stagedIdentity = fsSync.lstatSync(staged, { bigint: true });
|
|
330
|
-
if (!stagedIdentity.isSymbolicLink())
|
|
331
|
-
unregisterStaged.setIdentity(stagedIdentity);
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
if (error.code === "ENOENT") {
|
|
335
|
-
unregisterStaged();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
try {
|
|
339
|
-
await fs.rm(staged, { recursive: true, force: true });
|
|
340
|
-
unregisterStaged();
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// Keep the identity-bound exit cleanup registered for a later retry.
|
|
344
|
-
}
|
|
339
|
+
await stage.cleanup();
|
|
345
340
|
}
|
|
346
341
|
}
|
|
347
342
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export declare class MutationAuthorityError extends FsSafeError {
|
|
3
|
+
readonly rejection: unknown;
|
|
4
|
+
constructor(rejection: unknown);
|
|
5
|
+
}
|
|
6
|
+
export declare function composeMutationAssertions(defaultAssertion?: () => void, callAssertion?: () => void): (() => void) | undefined;
|
|
7
|
+
export declare function rethrowMutationAuthorityError(error: unknown): never;
|
|
8
|
+
//# sourceMappingURL=mutation-authority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-authority.d.ts","sourceRoot":"","sources":["../src/mutation-authority.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,qBAAa,sBAAuB,SAAQ,WAAW;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO;IAAvC,YAAqB,SAAS,EAAE,OAAO,EAEtC;CACF;AAcD,wBAAgB,yBAAyB,CACvC,gBAAgB,CAAC,EAAE,MAAM,IAAI,EAC7B,aAAa,CAAC,EAAE,MAAM,IAAI,GACzB,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAU1B;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAInE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export class MutationAuthorityError extends FsSafeError {
|
|
3
|
+
rejection;
|
|
4
|
+
constructor(rejection) {
|
|
5
|
+
super("denied-path", "mutation authority rejected the operation", { cause: rejection });
|
|
6
|
+
this.rejection = rejection;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function assertSynchronous(returned) {
|
|
10
|
+
if (returned !== null &&
|
|
11
|
+
(typeof returned === "object" || typeof returned === "function") &&
|
|
12
|
+
typeof returned.then === "function") {
|
|
13
|
+
// TypeScript permits async functions for () => void. Do not admit their work.
|
|
14
|
+
void Promise.resolve(returned).catch(() => undefined);
|
|
15
|
+
throw new TypeError("assertBeforeMutation must be synchronous");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function composeMutationAssertions(defaultAssertion, callAssertion) {
|
|
19
|
+
if (!defaultAssertion && !callAssertion)
|
|
20
|
+
return undefined;
|
|
21
|
+
return () => {
|
|
22
|
+
try {
|
|
23
|
+
assertSynchronous(defaultAssertion?.());
|
|
24
|
+
assertSynchronous(callAssertion?.());
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new MutationAuthorityError(error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function rethrowMutationAuthorityError(error) {
|
|
32
|
+
if (error instanceof MutationAuthorityError)
|
|
33
|
+
throw error.rejection;
|
|
34
|
+
// Cleanup failures and indeterminate publication receipts must retain their context.
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
package/dist/native-binding.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TarMeterLimits } from "./archive-limits.js";
|
|
2
2
|
import type { ArchiveMemberKind } from "./archive-plan.js";
|
|
3
|
+
import type { CopyCloneMode } from "./copy-policy.js";
|
|
3
4
|
export interface NativeFileHash {
|
|
4
5
|
bytes: number;
|
|
5
6
|
digest: string;
|
|
@@ -29,6 +30,12 @@ export interface NativeCopyResult {
|
|
|
29
30
|
errorCode?: string;
|
|
30
31
|
errorMessage?: string;
|
|
31
32
|
}
|
|
33
|
+
export interface NativeFileCopyResult {
|
|
34
|
+
fd: number;
|
|
35
|
+
method: "clone" | "copy-file-range" | "copy";
|
|
36
|
+
errorCode?: string;
|
|
37
|
+
errorMessage?: string;
|
|
38
|
+
}
|
|
32
39
|
export interface NativeOpenBeneathResult {
|
|
33
40
|
fd: number;
|
|
34
41
|
containment: "kernel-atomic" | "best-effort";
|
|
@@ -69,9 +76,25 @@ export interface NativeWindowsSecurityFacts {
|
|
|
69
76
|
aces: NativeWindowsAccessControlEntry[];
|
|
70
77
|
}
|
|
71
78
|
export interface NativeBinding {
|
|
79
|
+
/** Windows-only byte transfer; callers retain both admitted descriptors until settlement. */
|
|
80
|
+
copyFileContents?(sourceFd: number, targetFd: number, signal?: AbortSignal): Promise<void>;
|
|
81
|
+
/** Internal: same private, immutable input ownership as the ZIP buffer reader. */
|
|
82
|
+
openTarBufferNative(buffer: Buffer, kind: string, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
83
|
+
readonly entries: NativeArchiveEntry[];
|
|
84
|
+
readEntry(index: number, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
|
|
85
|
+
}>;
|
|
86
|
+
/** Internal: input remains private, unpooled and immutable until the reader is released. */
|
|
87
|
+
openZipBufferNative(buffer: Buffer, limits: TarMeterLimits, signal?: AbortSignal): Promise<{
|
|
88
|
+
readonly entries: NativeArchiveEntry[];
|
|
89
|
+
readEntry(index: number, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
|
|
90
|
+
}>;
|
|
91
|
+
readCloneFileMetadata(paths: string[]): Promise<(Buffer | null)[]>;
|
|
92
|
+
probeTreeClone(parentFd: number): "apfs" | "btrfs" | "refs" | "xfs" | null;
|
|
93
|
+
cloneTree(sourceFd: number | null, parentFd: number, basename: string, concurrency: number, signal?: AbortSignal): Promise<void>;
|
|
72
94
|
createStagedFile?(parentFd: number, basename: string): number;
|
|
73
95
|
stagedFileMatches?(parentFd: number, basename: string, fileFd: number): boolean;
|
|
74
96
|
removeStagedFile?(parentFd: number, basename: string, fileFd: number): "removed" | "name-absent" | "preserved";
|
|
97
|
+
copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined, sync: boolean): Promise<NativeFileCopyResult>;
|
|
75
98
|
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
76
99
|
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
77
100
|
createPrivateDirectory(path: string): void;
|
|
@@ -88,6 +111,6 @@ export interface NativeBinding {
|
|
|
88
111
|
removeOwnedTreeSync?(parentFd: number, basename: string, directoryFd: number): NativeOwnedTreeRemovalResult;
|
|
89
112
|
renameNoReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
90
113
|
renameReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
91
|
-
sha256File(fd: number): Promise<NativeFileHash>;
|
|
114
|
+
sha256File(fd: number, maxBytes?: number, signal?: AbortSignal): Promise<NativeFileHash>;
|
|
92
115
|
}
|
|
93
116
|
//# sourceMappingURL=native-binding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,7 @@ export type NativeFileHandle = {
|
|
|
10
10
|
};
|
|
11
11
|
export declare function nativeOpenFlags(flags: number): number;
|
|
12
12
|
export declare function writeNativeFd(fd: number, data: Buffer): void;
|
|
13
|
-
export declare function writeNativeInput(fd: number, input: PinnedWriteInput, maxBytes?: number): Promise<void>;
|
|
13
|
+
export declare function writeNativeInput(fd: number, input: PinnedWriteInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
|
|
14
14
|
export declare function removeNativeCreatedFileIfStillPinned(params: {
|
|
15
15
|
parentPath: string;
|
|
16
16
|
parentFd: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAI1D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS5D;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,gBAAgB,EACvB,QAAQ,CAAC,EAAE,MAAM,EACjB,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAsBD,wBAAgB,oCAAoC,CAAC,MAAM,EAAE;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB,GAAG,IAAI,CAqBP;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAwDvC"}
|
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
6
|
import { getNativeBinding } from "./native.js";
|
|
7
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
8
|
+
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
7
9
|
export function nativeOpenFlags(flags) {
|
|
8
10
|
const closeOnExec = fsSync.constants.O_CLOEXEC;
|
|
9
11
|
return (flags |
|
|
@@ -20,21 +22,25 @@ export function writeNativeFd(fd, data) {
|
|
|
20
22
|
offset += written;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
|
-
export async function writeNativeInput(fd, input, maxBytes) {
|
|
25
|
+
export async function writeNativeInput(fd, input, maxBytes, assertBeforeMutation) {
|
|
26
|
+
if (input.kind === "file") {
|
|
27
|
+
await writeCopyFileToFd(fd, input, maxBytes, assertBeforeMutation);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
24
30
|
let bytes = 0;
|
|
25
|
-
const write = (data) => {
|
|
31
|
+
const write = async (data) => {
|
|
26
32
|
bytes += data.byteLength;
|
|
27
33
|
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
28
34
|
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
await writeAllToFile(fd, data, { assertBeforeMutation });
|
|
31
37
|
};
|
|
32
38
|
if (input.kind === "buffer") {
|
|
33
|
-
write(typeof input.data === "string" ? Buffer.from(input.data, input.encoding ?? "utf8") : input.data);
|
|
39
|
+
await write(typeof input.data === "string" ? Buffer.from(input.data, input.encoding ?? "utf8") : input.data);
|
|
34
40
|
}
|
|
35
41
|
else {
|
|
36
42
|
for await (const chunk of input.stream) {
|
|
37
|
-
write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
43
|
+
await write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAM3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,EAC3C,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,GAC3C,OAAO,CAET;AAWD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAyF3B"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fsSync, {} from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, writeNativeInput, } from "./native-operations.js";
|
|
6
7
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
8
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
7
9
|
export function sameNativeIdentity(left, right) {
|
|
8
10
|
return left.dev === right.dev && left.ino === right.ino;
|
|
9
11
|
}
|
|
@@ -27,6 +29,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
27
29
|
let completed = false;
|
|
28
30
|
try {
|
|
29
31
|
tempName = `.fs-safe-${randomUUID()}.tmp`;
|
|
32
|
+
params.assertBeforeMutation?.();
|
|
30
33
|
tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
|
|
31
34
|
const verificationIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(tempFd, { bigint: true }));
|
|
32
35
|
tempIdentity = verificationIdentity;
|
|
@@ -34,9 +37,13 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
34
37
|
// can remove owner access. Keep the unpublished inode private and
|
|
35
38
|
// reopenable until the published name has been identity-fenced.
|
|
36
39
|
fsSync.fchmodSync(tempFd, 0o600);
|
|
37
|
-
await writeNativeInput(tempFd, params.input, params.maxBytes);
|
|
40
|
+
await writeNativeInput(tempFd, params.input, params.maxBytes, params.assertBeforeMutation);
|
|
38
41
|
if (params.sync !== false)
|
|
39
42
|
syncFileBestEffortSync(tempFd);
|
|
43
|
+
if (params.input.kind === "file")
|
|
44
|
+
await params.input.verifySource();
|
|
45
|
+
assertFinalSymlinkRejected(path.join(parentPath, params.basename), params.rejectFinalSymlink);
|
|
46
|
+
params.assertBeforeMutation?.();
|
|
40
47
|
if (params.overwrite === false) {
|
|
41
48
|
binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
|
|
42
49
|
}
|
|
@@ -44,6 +51,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
44
51
|
binding.renameReplace(parentFd, tempName, parentFd, params.basename);
|
|
45
52
|
}
|
|
46
53
|
renamed = true;
|
|
54
|
+
params.onPublished?.(verificationIdentity);
|
|
47
55
|
targetFd = binding.openBeneath(parentFd, params.basename, nativeOpenFlags(fsSync.constants.O_RDONLY)).fd;
|
|
48
56
|
const targetStat = inspectFileIdentitySync(() => fsSync.fstatSync(targetFd, { bigint: true }), verificationIdentity);
|
|
49
57
|
const targetIdentity = binding.fstatIdentity(targetFd);
|
|
@@ -61,12 +69,15 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
61
69
|
catch (error) {
|
|
62
70
|
closeWriteFd(targetFd);
|
|
63
71
|
targetFd = undefined;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
// Copies preserve the published name for caller-owned recovery.
|
|
73
|
+
if (params.input.kind !== "file") {
|
|
74
|
+
removeNativeCreatedFileIfStillPinned({
|
|
75
|
+
parentPath,
|
|
76
|
+
parentFd,
|
|
77
|
+
basename: params.basename,
|
|
78
|
+
created: tempIdentity,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
70
81
|
throw error;
|
|
71
82
|
}
|
|
72
83
|
if (params.sync !== false)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2FvH"}
|
|
@@ -100,6 +100,7 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
100
100
|
throw new FsSafeError("path-mismatch", "root path changed during native write");
|
|
101
101
|
}
|
|
102
102
|
if (params.mkdir) {
|
|
103
|
+
params.assertBeforeMutation?.();
|
|
103
104
|
binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
|
|
104
105
|
}
|
|
105
106
|
parentFd = binding.openBeneath(root.fd, params.relativeParentPath, directoryFlags).fd;
|
|
@@ -8,14 +8,14 @@ export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding,
|
|
|
8
8
|
export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
|
|
9
9
|
declare class NativeStagedFile implements StagedFile {
|
|
10
10
|
#private;
|
|
11
|
-
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean);
|
|
12
|
-
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean): Promise<NativeStagedFile>;
|
|
11
|
+
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
|
|
12
|
+
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
|
|
13
13
|
static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
14
14
|
get receipt(): StagedFileReceipt;
|
|
15
15
|
assertCurrent(): Promise<void>;
|
|
16
16
|
publish(basename: string, options: {
|
|
17
17
|
overwrite: boolean;
|
|
18
|
-
}): Promise<PublishedFileReceipt>;
|
|
18
|
+
}, onPublished?: PinnedWriteParams["onPublished"]): Promise<PublishedFileReceipt>;
|
|
19
19
|
cleanup(): Promise<StagedFileCleanupReceipt>;
|
|
20
20
|
[Symbol.asyncDispose](): Promise<void>;
|
|
21
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAUnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAa1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,OAAO,EACb,oBAAoB,CAAC,EAAE,MAAM,IAAI,EASlC;IAGD,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,EACpB,IAAI,UAAO,EACX,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IA8FK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,EAC/B,WAAW,CAAC,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAC7C,OAAO,CAAC,oBAAoB,CAAC,CAyD/B;IA6CK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC;AAEvD,YAAY,EACV,oBAAoB,EAAE,UAAU,EAAE,wBAAwB,EAAE,wBAAwB,EACpF,qBAAqB,EAAE,iBAAiB,GACzC,MAAM,wBAAwB,CAAC;AAEhC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACrC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtB"}
|