@openclaw/fs-safe 0.5.6 → 0.7.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 +89 -0
- package/README.md +23 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts +2 -0
- package/dist/archive-crc32.d.ts.map +1 -0
- package/dist/archive-crc32.js +14 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-kind.js +2 -2
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +31 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +88 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +135 -19
- package/dist/archive-tar-pax.d.ts +8 -0
- package/dist/archive-tar-pax.d.ts.map +1 -0
- package/dist/archive-tar-pax.js +100 -0
- package/dist/archive-tar-runtime.d.ts +4 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-admission.d.ts +7 -0
- package/dist/archive-zip-admission.d.ts.map +1 -0
- package/dist/archive-zip-admission.js +60 -0
- package/dist/archive-zip-count.d.ts +2 -0
- package/dist/archive-zip-count.d.ts.map +1 -0
- package/dist/archive-zip-count.js +140 -0
- package/dist/archive-zip-directory.d.ts +8 -0
- package/dist/archive-zip-directory.d.ts.map +1 -0
- package/dist/archive-zip-directory.js +224 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +1 -14
- package/dist/archive-zip-names.d.ts +12 -0
- package/dist/archive-zip-names.d.ts.map +1 -0
- package/dist/archive-zip-names.js +99 -0
- package/dist/archive-zip-preflight.d.ts +1 -1
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -145
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +63 -42
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +26 -9
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +15 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +4 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +22 -6
- package/dist/native-pinned-write-windows.d.ts +8 -0
- package/dist/native-pinned-write-windows.d.ts.map +1 -0
- package/dist/native-pinned-write-windows.js +92 -0
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +136 -127
- package/dist/native-staged-file.d.ts +24 -0
- package/dist/native-staged-file.d.ts.map +1 -0
- package/dist/native-staged-file.js +337 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -4
- package/dist/opened-realpath.d.ts +2 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +12 -7
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -1
- package/dist/permission-exec.d.ts +19 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +57 -11
- package/dist/permissions-public.d.ts +1 -1
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-windows.d.ts +3 -0
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +16 -5
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +26 -37
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +118 -98
- package/dist/root-paths.d.ts +11 -14
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +36 -27
- package/dist/root-write-verification.d.ts +11 -0
- package/dist/root-write-verification.d.ts.map +1 -0
- package/dist/root-write-verification.js +91 -0
- package/dist/secret-file.d.ts +1 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +49 -120
- package/dist/secret-read-async.d.ts +1 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +51 -72
- package/dist/secret-read-policy.d.ts +13 -0
- package/dist/secret-read-policy.d.ts.map +1 -0
- package/dist/secret-read-policy.js +28 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +42 -29
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/staged-directory.d.ts +16 -0
- package/dist/staged-directory.d.ts.map +1 -0
- package/dist/staged-directory.js +60 -0
- package/dist/staged-file-types.d.ts +56 -0
- package/dist/staged-file-types.d.ts.map +1 -0
- package/dist/staged-file-types.js +1 -0
- package/dist/staged-file.d.ts +10 -0
- package/dist/staged-file.d.ts.map +1 -0
- package/dist/staged-file.js +15 -0
- package/dist/strict-file-identity.d.ts +6 -0
- package/dist/strict-file-identity.d.ts.map +1 -0
- package/dist/strict-file-identity.js +48 -0
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-owner.d.ts +3 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +10 -2
- package/docs/advanced.md +19 -2
- package/docs/archive.md +250 -35
- package/docs/atomic.md +11 -2
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +18 -5
- package/docs/errors.md +16 -1
- package/docs/file-store.md +2 -0
- package/docs/index.md +3 -1
- package/docs/install.md +20 -8
- package/docs/json.md +8 -4
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +30 -8
- package/docs/native.md +73 -17
- package/docs/output.md +10 -0
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +3 -1
- package/docs/secret-file.md +12 -0
- package/docs/secure-file.md +21 -3
- package/docs/security-model.md +33 -1
- package/docs/sidecar-lock.md +15 -1
- package/docs/staged-file.md +178 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +16 -9
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/pinned-open.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { isUnsafeDeviceReadPath } from "./device-path.js";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
6
|
function isExpectedPathError(error) {
|
|
7
7
|
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
8
8
|
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
9
9
|
}
|
|
10
|
-
function sameFileIdentity(left, right) {
|
|
11
|
-
return hasSameFileIdentity(left, right);
|
|
12
|
-
}
|
|
13
10
|
export function openPinnedFileSync(params) {
|
|
14
11
|
const ioFs = params.ioFs ?? fs;
|
|
15
12
|
const allowedType = params.allowedType ?? "file";
|
|
@@ -29,53 +26,30 @@ export function openPinnedFileSync(params) {
|
|
|
29
26
|
if (isUnsafeDeviceReadPath(realPath)) {
|
|
30
27
|
return { ok: false, reason: "validation" };
|
|
31
28
|
}
|
|
32
|
-
const preOpenStat =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
error: new FsSafeError("not-file", "path does not have the required file type"),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
reason: "validation",
|
|
44
|
-
error: new FsSafeError("hardlink", "path must not be hardlinked"),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
if (params.maxBytes !== undefined &&
|
|
48
|
-
preOpenStat.isFile() &&
|
|
49
|
-
preOpenStat.size > params.maxBytes) {
|
|
50
|
-
return { ok: false, reason: "validation" };
|
|
51
|
-
}
|
|
29
|
+
const preOpenStat = inspectFileIdentitySync(() => {
|
|
30
|
+
const stat = ioFs.lstatSync(realPath, { bigint: true });
|
|
31
|
+
assertAllowedStat(stat, allowedType, params);
|
|
32
|
+
return stat;
|
|
33
|
+
});
|
|
52
34
|
fd = ioFs.openSync(realPath, openReadFlags);
|
|
53
35
|
const openedStat = ioFs.fstatSync(fd);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
reason: "validation",
|
|
65
|
-
error: new FsSafeError("hardlink", "path must not be hardlinked"),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
|
|
69
|
-
return { ok: false, reason: "validation" };
|
|
70
|
-
}
|
|
71
|
-
if (!sameFileIdentity(preOpenStat, openedStat)) {
|
|
72
|
-
return { ok: false, reason: "validation" };
|
|
73
|
-
}
|
|
36
|
+
const identity = inspectFileIdentitySync(() => {
|
|
37
|
+
const stat = ioFs.fstatSync(fd, { bigint: true });
|
|
38
|
+
assertAllowedStat(stat, allowedType, params);
|
|
39
|
+
return stat;
|
|
40
|
+
}, preOpenStat);
|
|
41
|
+
inspectFileIdentitySync(() => {
|
|
42
|
+
const stat = ioFs.lstatSync(realPath, { bigint: true });
|
|
43
|
+
assertAllowedStat(stat, allowedType, params);
|
|
44
|
+
return stat;
|
|
45
|
+
}, identity);
|
|
74
46
|
const opened = { ok: true, path: realPath, fd, stat: openedStat };
|
|
75
47
|
fd = null;
|
|
76
48
|
return opened;
|
|
77
49
|
}
|
|
78
50
|
catch (error) {
|
|
51
|
+
if (error instanceof FsSafeError)
|
|
52
|
+
return { ok: false, reason: "validation", error };
|
|
79
53
|
if (isExpectedPathError(error)) {
|
|
80
54
|
return { ok: false, reason: "path", error };
|
|
81
55
|
}
|
|
@@ -87,9 +61,14 @@ export function openPinnedFileSync(params) {
|
|
|
87
61
|
}
|
|
88
62
|
}
|
|
89
63
|
}
|
|
90
|
-
function
|
|
91
|
-
if (allowedType === "directory") {
|
|
92
|
-
|
|
64
|
+
function assertAllowedStat(stat, allowedType, params) {
|
|
65
|
+
if (stat.isSymbolicLink() || !(allowedType === "directory" ? stat.isDirectory() : stat.isFile())) {
|
|
66
|
+
throw new FsSafeError("not-file", "path does not have the required file type");
|
|
67
|
+
}
|
|
68
|
+
if (params.rejectHardlinks && stat.isFile() && stat.nlink > 1n) {
|
|
69
|
+
throw new FsSafeError("hardlink", "path must not be hardlinked");
|
|
70
|
+
}
|
|
71
|
+
if (params.maxBytes !== undefined && stat.isFile() && stat.size > params.maxBytes) {
|
|
72
|
+
throw new FsSafeError("too-large", "file exceeds byte limit");
|
|
93
73
|
}
|
|
94
|
-
return stat.isFile();
|
|
95
74
|
}
|
package/dist/pinned-write.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
|
+
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
4
|
export type PinnedWriteInput = {
|
|
4
5
|
kind: "buffer";
|
|
@@ -10,6 +11,10 @@ export type PinnedWriteInput = {
|
|
|
10
11
|
};
|
|
11
12
|
export type RenameIdentityMismatchPolicy = "throw" | "verify-content";
|
|
12
13
|
export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
14
|
+
export type PublishedWriteIdentity = Readonly<{
|
|
15
|
+
dev: bigint;
|
|
16
|
+
ino: bigint;
|
|
17
|
+
}>;
|
|
13
18
|
export type PinnedWriteParams = {
|
|
14
19
|
rootPath: string;
|
|
15
20
|
relativeParentPath: string;
|
|
@@ -21,6 +26,7 @@ export type PinnedWriteParams = {
|
|
|
21
26
|
input: PinnedWriteInput;
|
|
22
27
|
rootIdentity?: FileIdentityStat;
|
|
23
28
|
onRenameIdentityMismatch?: RenameIdentityMismatchPolicy;
|
|
29
|
+
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AsyncDirectoryGuard) => Promise<void>;
|
|
24
30
|
};
|
|
25
31
|
export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
|
|
26
32
|
export declare function runPinnedWriteWithRenamePolicy(params: PinnedWriteParams & {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAkE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhI,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAW3D,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAgEzC,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;IAExD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,mBAAmB,KAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/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,CA2B3B"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
5
6
|
import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
7
8
|
import { syncDirectoryBestEffort } from "./fsync.js";
|
|
@@ -11,6 +12,7 @@ import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
|
11
12
|
import { runPinnedWriteNative } from "./native-pinned-write.js";
|
|
12
13
|
import { getNativeBinding } from "./native.js";
|
|
13
14
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
15
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
14
16
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
15
17
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
16
18
|
function byteLength(input, encoding) {
|
|
@@ -59,20 +61,21 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
export async function runPinnedWriteHelper(params) {
|
|
64
|
+
const normalizedParams = { ...params, maxBytes: normalizeMaxBytes(params.maxBytes) };
|
|
62
65
|
assertSafeBasename(params.basename);
|
|
63
66
|
validatePinnedOperationPayload({
|
|
64
67
|
relativeParentPath: params.relativeParentPath,
|
|
65
68
|
});
|
|
66
69
|
// The explicit compatibility policy uses the guarded Node fallback, where
|
|
67
70
|
// content verification can replace the strict post-rename inode check.
|
|
68
|
-
if (
|
|
69
|
-
return await runPinnedWriteFallback(
|
|
71
|
+
if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
|
|
72
|
+
return await runPinnedWriteFallback(normalizedParams);
|
|
70
73
|
}
|
|
71
74
|
const native = getNativeBinding();
|
|
72
75
|
if (native) {
|
|
73
|
-
return await runPinnedWriteNative(native,
|
|
76
|
+
return await runPinnedWriteNative(native, normalizedParams);
|
|
74
77
|
}
|
|
75
|
-
return await runPinnedWriteFallback(
|
|
78
|
+
return await runPinnedWriteFallback(normalizedParams);
|
|
76
79
|
}
|
|
77
80
|
export async function runPinnedWriteWithRenamePolicy(params) {
|
|
78
81
|
const { targetPath, renameIdentity, ...writeParams } = params;
|
|
@@ -115,7 +118,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
115
118
|
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
116
119
|
const targetPath = path.join(parentPath, params.basename);
|
|
117
120
|
if (params.overwrite === false) {
|
|
118
|
-
|
|
121
|
+
const handle = await withAsyncDirectoryGuards([parentGuard], async () => await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode), {
|
|
119
122
|
onPostGuardFailure: async (openedHandle) => {
|
|
120
123
|
// The parent failed verification, so targetPath may now resolve
|
|
121
124
|
// somewhere else. Close the fd, but do not clean up by path.
|
|
@@ -124,6 +127,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
124
127
|
});
|
|
125
128
|
let created = true;
|
|
126
129
|
try {
|
|
130
|
+
const verificationIdentity = await handle.stat({ bigint: true });
|
|
127
131
|
await handle.chmod(params.mode);
|
|
128
132
|
if (params.input.kind === "buffer") {
|
|
129
133
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
@@ -139,9 +143,10 @@ async function runPinnedWriteFallback(params) {
|
|
|
139
143
|
}
|
|
140
144
|
await syncFileBestEffort(handle);
|
|
141
145
|
const stat = await handle.stat();
|
|
142
|
-
await handle.close().catch(() => undefined);
|
|
143
146
|
await syncDirectoryBestEffort(parentPath);
|
|
147
|
+
// Publication is complete. A failed outer check must not remove its target.
|
|
144
148
|
created = false;
|
|
149
|
+
await params.verifyPublished?.(handle.fd, verificationIdentity, parentGuard);
|
|
145
150
|
return { dev: stat.dev, ino: stat.ino };
|
|
146
151
|
}
|
|
147
152
|
finally {
|
|
@@ -160,10 +165,11 @@ async function runPinnedWriteFallback(params) {
|
|
|
160
165
|
: 0);
|
|
161
166
|
let handle;
|
|
162
167
|
let tempStat;
|
|
163
|
-
let
|
|
168
|
+
let readHandle;
|
|
164
169
|
let renamed = false;
|
|
165
170
|
try {
|
|
166
171
|
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
172
|
+
let verificationIdentity = await handle.stat({ bigint: true });
|
|
167
173
|
await handle.chmod(params.mode);
|
|
168
174
|
if (params.input.kind === "buffer") {
|
|
169
175
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
@@ -184,14 +190,13 @@ async function runPinnedWriteFallback(params) {
|
|
|
184
190
|
}
|
|
185
191
|
const expectedTempStat = tempStat;
|
|
186
192
|
await syncFileBestEffort(handle);
|
|
187
|
-
|
|
188
|
-
handle = undefined;
|
|
193
|
+
let verifiedIdentity = expectedTempStat;
|
|
189
194
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
190
195
|
await fs.rename(tempPath, targetPath);
|
|
191
196
|
renamed = true;
|
|
192
197
|
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
193
198
|
await syncDirectoryBestEffort(parentPath);
|
|
194
|
-
targetStat = await fs.lstat(targetPath);
|
|
199
|
+
const targetStat = await fs.lstat(targetPath);
|
|
195
200
|
if (targetStat.isSymbolicLink()) {
|
|
196
201
|
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
197
202
|
}
|
|
@@ -207,42 +212,26 @@ async function runPinnedWriteFallback(params) {
|
|
|
207
212
|
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
208
213
|
}
|
|
209
214
|
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
: 0);
|
|
214
|
-
const readHandle = await fs.open(targetPath, readFlags);
|
|
215
|
-
let actualHash;
|
|
216
|
-
let readHandleStat;
|
|
217
|
-
try {
|
|
218
|
-
// Capture fd-based identity before reading — this is stable across all
|
|
219
|
-
// subsequent lookups (on FUSE and locally), unlike the lstat-based
|
|
220
|
-
// targetStat that triggered this fallback.
|
|
221
|
-
readHandleStat = await readHandle.stat();
|
|
222
|
-
actualHash = sha256Hex(await readHandle.readFile());
|
|
223
|
-
}
|
|
224
|
-
finally {
|
|
225
|
-
await readHandle.close().catch(() => undefined);
|
|
226
|
-
}
|
|
215
|
+
readHandle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
216
|
+
const readHandleStat = await readHandle.stat({ bigint: true });
|
|
217
|
+
const actualHash = sha256Hex(await readHandle.readFile());
|
|
227
218
|
if (actualHash !== expectedHash) {
|
|
228
219
|
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
229
220
|
}
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
|
|
221
|
+
// The content-verified destination, not the old temp inode, is now pinned.
|
|
222
|
+
verificationIdentity = readHandleStat;
|
|
223
|
+
// Preserve the helper's legacy numeric return facts, not the private proof.
|
|
224
|
+
verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
|
|
234
225
|
}
|
|
235
226
|
});
|
|
227
|
+
await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
|
|
228
|
+
return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
|
|
236
229
|
}
|
|
237
|
-
|
|
230
|
+
finally {
|
|
231
|
+
await readHandle?.close().catch(() => undefined);
|
|
238
232
|
await handle?.close().catch(() => undefined);
|
|
239
233
|
if (!renamed) {
|
|
240
234
|
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
241
235
|
}
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
if (!targetStat) {
|
|
245
|
-
throw new FsSafeError("path-mismatch", "fallback target was not verified");
|
|
246
236
|
}
|
|
247
|
-
return { dev: targetStat.dev, ino: targetStat.ino };
|
|
248
237
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAGA,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,
|
|
1
|
+
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAGA,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,CAkBf"}
|
|
@@ -3,11 +3,12 @@ import { getNativeBinding } from "./native.js";
|
|
|
3
3
|
export async function createPrivateDirectory(targetPath, options) {
|
|
4
4
|
const platform = options?.platform ?? process.platform;
|
|
5
5
|
if (platform !== "win32") {
|
|
6
|
-
throw new FsSafeError("helper-unavailable", "private-directory creation is
|
|
6
|
+
throw new FsSafeError("helper-unavailable", "private-directory creation is supported only on Windows");
|
|
7
7
|
}
|
|
8
8
|
const native = getNativeBinding();
|
|
9
9
|
if (!native) {
|
|
10
|
-
throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the
|
|
10
|
+
throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the matching optional native platform package; " +
|
|
11
|
+
"install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
|
|
11
12
|
}
|
|
12
13
|
native.createPrivateDirectory(targetPath);
|
|
13
14
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type FileStore, type FileStoreSync } from "./file-store.js";
|
|
2
2
|
import { type TempPathIdentityReceipt } from "./temp-cleanup.js";
|
|
3
|
-
|
|
3
|
+
import { type TempWorkspaceCleanupResult, type TempWorkspaceCleanupSafety } from "./temp-workspace-owner.js";
|
|
4
|
+
export type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety, } from "./temp-workspace-owner.js";
|
|
4
5
|
export type TempWorkspaceOptions = {
|
|
5
6
|
rootDir: string;
|
|
6
7
|
prefix: string;
|
|
7
8
|
dirMode?: number;
|
|
8
9
|
mode?: number;
|
|
10
|
+
cleanupSafety?: TempWorkspaceCleanupSafety;
|
|
9
11
|
};
|
|
10
12
|
export type TempWorkspace = {
|
|
11
13
|
dir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,11 +4,18 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileStore, fileStoreSync, } from "./file-store.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
-
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
8
7
|
import { isNotFoundPathError } from "./path.js";
|
|
9
8
|
import { throwFsSafeReadError } from "./read-error.js";
|
|
10
9
|
import { matchRootFileOpenFailure, openRootFileSync, } from "./root-file.js";
|
|
11
10
|
import { registerTempPathForExit, } from "./temp-cleanup.js";
|
|
11
|
+
import { TempWorkspaceCleanupCapability, TempWorkspaceCleanupOwner, } from "./temp-workspace-owner.js";
|
|
12
|
+
function resolveTempWorkspaceCleanupSafety(value) {
|
|
13
|
+
if (value === undefined || value === "compatible")
|
|
14
|
+
return "compatible";
|
|
15
|
+
if (value === "require-bounded")
|
|
16
|
+
return value;
|
|
17
|
+
throw new TypeError("cleanupSafety must be compatible or require-bounded");
|
|
18
|
+
}
|
|
12
19
|
function sanitizeTempPrefix(prefix) {
|
|
13
20
|
const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
14
21
|
if (!sanitized || sanitized === "." || sanitized === "..") {
|
|
@@ -79,52 +86,47 @@ function ensurePrivateDirectorySync(dir, mode) {
|
|
|
79
86
|
// Best-effort on platforms that do not enforce POSIX modes.
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
|
-
async function cleanupWorkspace(dir, identity) {
|
|
83
|
-
let current;
|
|
84
|
-
try {
|
|
85
|
-
current = await fs.lstat(dir, { bigint: true });
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
return error.code === "ENOENT" ? "missing" : "identity-mismatch";
|
|
89
|
-
}
|
|
90
|
-
if (!sameFileIdentityForCleanup(current, identity)) {
|
|
91
|
-
return "identity-mismatch";
|
|
92
|
-
}
|
|
93
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
94
|
-
return "removed";
|
|
95
|
-
}
|
|
96
|
-
function cleanupWorkspaceSync(dir, identity) {
|
|
97
|
-
let current;
|
|
98
|
-
try {
|
|
99
|
-
current = fsSync.lstatSync(dir, { bigint: true });
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
return error.code === "ENOENT" ? "missing" : "identity-mismatch";
|
|
103
|
-
}
|
|
104
|
-
if (!sameFileIdentityForCleanup(current, identity)) {
|
|
105
|
-
return "identity-mismatch";
|
|
106
|
-
}
|
|
107
|
-
fsSync.rmSync(dir, { recursive: true, force: true });
|
|
108
|
-
return "removed";
|
|
109
|
-
}
|
|
110
89
|
async function createTempWorkspace(options) {
|
|
111
90
|
const dirMode = options.dirMode ?? 0o700;
|
|
112
91
|
const mode = options.mode ?? 0o600;
|
|
92
|
+
const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
|
|
113
93
|
const requestedRoot = path.resolve(options.rootDir);
|
|
114
94
|
const root = await fs.realpath(requestedRoot).catch(() => requestedRoot);
|
|
115
95
|
await ensurePrivateDirectory(root, dirMode);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
96
|
+
const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
|
|
97
|
+
let dir;
|
|
98
|
+
let stat;
|
|
99
|
+
let cleanupOwner;
|
|
100
|
+
let unregisterTempDir;
|
|
101
|
+
try {
|
|
102
|
+
dir = await fs.mkdtemp(path.join(root, sanitizeTempPrefix(options.prefix)));
|
|
103
|
+
await fs.chmod(dir, dirMode).catch(() => undefined);
|
|
104
|
+
stat = await fs.lstat(dir, { bigint: true });
|
|
105
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
106
|
+
throw new Error(`Temp workspace must be a directory: ${dir}`);
|
|
107
|
+
}
|
|
108
|
+
if (capability.parent)
|
|
109
|
+
capability.assertCurrent();
|
|
110
|
+
cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
|
|
111
|
+
unregisterTempDir = registerTempPathForExit(dir, {
|
|
112
|
+
cleanupSync: () => cleanupOwner.cleanupSync(),
|
|
113
|
+
});
|
|
121
114
|
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
try {
|
|
117
|
+
if (cleanupOwner)
|
|
118
|
+
cleanupOwner.cleanupSync();
|
|
119
|
+
else
|
|
120
|
+
capability.close();
|
|
121
|
+
}
|
|
122
|
+
catch (cleanupError) {
|
|
123
|
+
throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
const owner = cleanupOwner;
|
|
122
128
|
const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
|
|
123
|
-
|
|
124
|
-
const unregisterTempDir = registerTempPathForExit(dir, {
|
|
125
|
-
recursive: true,
|
|
126
|
-
identity: cleanupIdentity,
|
|
127
|
-
});
|
|
129
|
+
// Once registered, even a store-construction failure remains exit-cleanable.
|
|
128
130
|
const store = fileStore({ rootDir: dir, private: true, dirMode, mode });
|
|
129
131
|
return {
|
|
130
132
|
dir,
|
|
@@ -148,7 +150,7 @@ async function createTempWorkspace(options) {
|
|
|
148
150
|
},
|
|
149
151
|
cleanup: async () => {
|
|
150
152
|
try {
|
|
151
|
-
return await
|
|
153
|
+
return await owner.cleanup();
|
|
152
154
|
}
|
|
153
155
|
finally {
|
|
154
156
|
unregisterTempDir();
|
|
@@ -156,7 +158,7 @@ async function createTempWorkspace(options) {
|
|
|
156
158
|
},
|
|
157
159
|
[Symbol.asyncDispose]: async () => {
|
|
158
160
|
try {
|
|
159
|
-
await
|
|
161
|
+
await owner.cleanup();
|
|
160
162
|
}
|
|
161
163
|
finally {
|
|
162
164
|
unregisterTempDir();
|
|
@@ -182,6 +184,7 @@ export async function withTempWorkspace(options, run) {
|
|
|
182
184
|
export function tempWorkspaceSync(options) {
|
|
183
185
|
const dirMode = options.dirMode ?? 0o700;
|
|
184
186
|
const mode = options.mode ?? 0o600;
|
|
187
|
+
const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
|
|
185
188
|
const requestedRoot = path.resolve(options.rootDir);
|
|
186
189
|
let root = requestedRoot;
|
|
187
190
|
try {
|
|
@@ -191,23 +194,45 @@ export function tempWorkspaceSync(options) {
|
|
|
191
194
|
root = requestedRoot;
|
|
192
195
|
}
|
|
193
196
|
ensurePrivateDirectorySync(root, dirMode);
|
|
194
|
-
const
|
|
197
|
+
const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
|
|
198
|
+
let dir;
|
|
199
|
+
let stat;
|
|
200
|
+
let cleanupOwner;
|
|
201
|
+
let unregisterTempDir;
|
|
195
202
|
try {
|
|
196
|
-
fsSync.
|
|
203
|
+
dir = fsSync.mkdtempSync(path.join(root, sanitizeTempPrefix(options.prefix)));
|
|
204
|
+
try {
|
|
205
|
+
fsSync.chmodSync(dir, dirMode);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
209
|
+
}
|
|
210
|
+
stat = fsSync.lstatSync(dir, { bigint: true });
|
|
211
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
212
|
+
throw new Error(`Temp workspace must be a directory: ${dir}`);
|
|
213
|
+
}
|
|
214
|
+
if (capability.parent)
|
|
215
|
+
capability.assertCurrent();
|
|
216
|
+
cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
|
|
217
|
+
unregisterTempDir = registerTempPathForExit(dir, {
|
|
218
|
+
cleanupSync: () => cleanupOwner.cleanupSync(),
|
|
219
|
+
});
|
|
197
220
|
}
|
|
198
|
-
catch {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
catch (error) {
|
|
222
|
+
try {
|
|
223
|
+
if (cleanupOwner)
|
|
224
|
+
cleanupOwner.cleanupSync();
|
|
225
|
+
else
|
|
226
|
+
capability.close();
|
|
227
|
+
}
|
|
228
|
+
catch (cleanupError) {
|
|
229
|
+
throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
204
232
|
}
|
|
233
|
+
const owner = cleanupOwner;
|
|
205
234
|
const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
|
|
206
|
-
|
|
207
|
-
const unregisterTempDir = registerTempPathForExit(dir, {
|
|
208
|
-
recursive: true,
|
|
209
|
-
identity: cleanupIdentity,
|
|
210
|
-
});
|
|
235
|
+
// Once registered, even a store-construction failure remains exit-cleanable.
|
|
211
236
|
const store = fileStoreSync({ rootDir: dir, private: true, dirMode, mode });
|
|
212
237
|
return {
|
|
213
238
|
dir,
|
|
@@ -244,7 +269,7 @@ export function tempWorkspaceSync(options) {
|
|
|
244
269
|
},
|
|
245
270
|
cleanup: () => {
|
|
246
271
|
try {
|
|
247
|
-
return
|
|
272
|
+
return owner.cleanupSync();
|
|
248
273
|
}
|
|
249
274
|
finally {
|
|
250
275
|
unregisterTempDir();
|
|
@@ -252,7 +277,7 @@ export function tempWorkspaceSync(options) {
|
|
|
252
277
|
},
|
|
253
278
|
[Symbol.dispose]: () => {
|
|
254
279
|
try {
|
|
255
|
-
|
|
280
|
+
owner.cleanupSync();
|
|
256
281
|
}
|
|
257
282
|
finally {
|
|
258
283
|
unregisterTempDir();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AAsND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
|
package/dist/publish-file.js
CHANGED
|
@@ -8,6 +8,7 @@ import { hashFileHandle } from "./file-hash.js";
|
|
|
8
8
|
import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
|
|
9
9
|
import { syncNativeFileBestEffort } from "./native-operations.js";
|
|
10
10
|
import { getNativeBinding, requireNativeBinding } from "./native.js";
|
|
11
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
11
12
|
import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
|
|
12
13
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
13
14
|
const HARDLINK_FALLBACK_CODES = new Set([
|
|
@@ -29,10 +30,7 @@ export function isHardlinkFallbackError(error) {
|
|
|
29
30
|
return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
|
|
30
31
|
}
|
|
31
32
|
function sourceOpenFlags() {
|
|
32
|
-
return (
|
|
33
|
-
(process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
34
|
-
? fsSync.constants.O_NOFOLLOW
|
|
35
|
-
: 0));
|
|
33
|
+
return resolveReadOpenFlags();
|
|
36
34
|
}
|
|
37
35
|
function directoryOpenFlags() {
|
|
38
36
|
return (fsSync.constants.O_RDONLY |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBtB"}
|
package/dist/read-opened-file.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
1
2
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
2
3
|
import { FsSafeError } from "./errors.js";
|
|
3
4
|
export async function readOpenedFileSafely(params) {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
6
|
+
if (maxBytes !== undefined && params.opened.stat.size > maxBytes) {
|
|
7
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got ${params.opened.stat.size})`);
|
|
6
8
|
}
|
|
7
|
-
const buffer =
|
|
9
|
+
const buffer = maxBytes === undefined
|
|
8
10
|
? await params.opened.handle.readFile()
|
|
9
|
-
: await readFileHandleBounded(params.opened.handle,
|
|
11
|
+
: await readFileHandleBounded(params.opened.handle, maxBytes);
|
|
10
12
|
return {
|
|
11
13
|
buffer,
|
|
12
14
|
containment: params.opened.containment,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,MAAM,MAAM,SAAS,CAAC;AAc7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAEvD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CAQR;AAaD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CActF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAc3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CA6D3C;AA2DD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CAoCA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwExF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAyE7E"}
|