@openclaw/fs-safe 0.6.0 → 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 +50 -0
- package/README.md +7 -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-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 +24 -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 +69 -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 +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -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-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -26
- 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-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +10 -5
- 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 +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- 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/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +8 -9
- 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 +40 -26
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- 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/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/docs/advanced.md +8 -1
- package/docs/archive.md +194 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +8 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +10 -0
- 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 +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +4 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/package.json +10 -10
package/dist/json.js
CHANGED
|
@@ -3,9 +3,11 @@ import fsSync from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
7
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
7
8
|
import { readRegularFile, readRegularFileSync, statRegularFile } from "./regular-file.js";
|
|
8
9
|
import { openRootFileSync } from "./root-file.js";
|
|
10
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
11
|
import { writeTextAtomic } from "./text-atomic.js";
|
|
10
12
|
import { sleep } from "./timing.js";
|
|
11
13
|
const READ_RETRY_MAX_ATTEMPTS = 5;
|
|
@@ -48,19 +50,35 @@ async function readRegularFileIfExistsWithRetry(filePath, options = {}) {
|
|
|
48
50
|
}
|
|
49
51
|
const JSON_FILE_MODE = 0o600;
|
|
50
52
|
const JSON_DIR_MODE = 0o700;
|
|
51
|
-
const SUPPORTS_SYNC_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants;
|
|
52
53
|
function getErrorCode(err) {
|
|
53
54
|
return err instanceof Error ? err.code : undefined;
|
|
54
55
|
}
|
|
55
|
-
function trySetSecureMode(pathname) {
|
|
56
|
+
function trySetSecureMode(pathname, expectedIdentity) {
|
|
56
57
|
let fd;
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const before = fsSync.lstatSync(pathname, { bigint: true });
|
|
60
|
+
if (before.isSymbolicLink() ||
|
|
61
|
+
!before.isFile() ||
|
|
62
|
+
before.nlink !== 1n ||
|
|
63
|
+
!sameFileIdentityForCleanup(before, expectedIdentity)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
fd = fsSync.openSync(pathname, resolveReadOpenFlags());
|
|
67
|
+
const opened = fsSync.fstatSync(fd, { bigint: true });
|
|
68
|
+
const current = fsSync.lstatSync(pathname, { bigint: true });
|
|
69
|
+
if (!opened.isFile() ||
|
|
70
|
+
opened.nlink !== 1n ||
|
|
71
|
+
current.isSymbolicLink() ||
|
|
72
|
+
!current.isFile() ||
|
|
73
|
+
current.nlink !== 1n ||
|
|
74
|
+
!sameFileIdentityForCleanup(opened, expectedIdentity) ||
|
|
75
|
+
!sameFileIdentityForCleanup(current, opened)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
60
78
|
fsSync.fchmodSync(fd, JSON_FILE_MODE);
|
|
61
79
|
}
|
|
62
80
|
catch {
|
|
63
|
-
//
|
|
81
|
+
// Best-effort mode application never falls back to a mutable pathname.
|
|
64
82
|
}
|
|
65
83
|
finally {
|
|
66
84
|
if (fd !== undefined) {
|
|
@@ -129,6 +147,7 @@ function writeTempJsonFile(pathname, payload) {
|
|
|
129
147
|
try {
|
|
130
148
|
fsSync.writeFileSync(fd, payload, "utf8");
|
|
131
149
|
fsSync.fsyncSync(fd);
|
|
150
|
+
return fsSync.fstatSync(fd, { bigint: true });
|
|
132
151
|
}
|
|
133
152
|
finally {
|
|
134
153
|
fsSync.closeSync(fd);
|
|
@@ -152,10 +171,10 @@ export function writeJsonSync(pathname, data) {
|
|
|
152
171
|
const payload = `${stringifyJsonDocument(data, null, 2)}\n`;
|
|
153
172
|
fsSync.mkdirSync(path.dirname(targetPath), { recursive: true, mode: JSON_DIR_MODE });
|
|
154
173
|
try {
|
|
155
|
-
writeTempJsonFile(tmpPath, payload);
|
|
156
|
-
trySetSecureMode(tmpPath);
|
|
174
|
+
const tempIdentity = writeTempJsonFile(tmpPath, payload);
|
|
175
|
+
trySetSecureMode(tmpPath, tempIdentity);
|
|
157
176
|
renameJsonFileWithFallback(tmpPath, targetPath);
|
|
158
|
-
trySetSecureMode(targetPath);
|
|
177
|
+
trySetSecureMode(targetPath, tempIdentity);
|
|
159
178
|
trySyncDirectory(targetPath);
|
|
160
179
|
}
|
|
161
180
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"AAOA,OAAO,EAAQ,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oCAAoC,GAAG,sBAAsB,GAAG;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,sBAAsB,GAAG;IACnE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAqGF,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,oCAAoC,GAC5C,oBAAoB,GAAG,IAAI,CA6B7B;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA4CtC"}
|
package/dist/local-roots.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
3
4
|
import { FsSafeError } from "./errors.js";
|
|
4
5
|
import { expandHomePrefix, resolveUserPath } from "./home-dir.js";
|
|
5
6
|
import { isFileUrl, safeFileURLToPath } from "./local-file-access.js";
|
|
@@ -129,6 +130,7 @@ export function resolveLocalPathFromRootsSync(options) {
|
|
|
129
130
|
return null;
|
|
130
131
|
}
|
|
131
132
|
export async function readLocalFileFromRoots(options) {
|
|
133
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
132
134
|
const label = options.label ?? "local roots";
|
|
133
135
|
const requestedPath = path.resolve(resolveLocalPathInput(options.filePath, "file path"));
|
|
134
136
|
const rootDirs = options.roots.map((rootEntry) => resolveLocalRootInput(rootEntry, label));
|
|
@@ -147,8 +149,8 @@ export async function readLocalFileFromRoots(options) {
|
|
|
147
149
|
};
|
|
148
150
|
// Leave maxBytes absent when the caller omits it so Root's own default
|
|
149
151
|
// cap remains in force instead of being overwritten by undefined.
|
|
150
|
-
if (
|
|
151
|
-
readOptions.maxBytes =
|
|
152
|
+
if (maxBytes !== undefined) {
|
|
153
|
+
readOptions.maxBytes = maxBytes;
|
|
152
154
|
}
|
|
153
155
|
// A trusted root symlink has two valid spellings. Preserve the caller's
|
|
154
156
|
// lexical spelling when possible so Root.read() still enforces its
|
package/dist/native-binding.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TarMeterLimits } from "./archive-limits.js";
|
|
1
2
|
export interface NativeFileHash {
|
|
2
3
|
bytes: number;
|
|
3
4
|
digest: string;
|
|
@@ -31,6 +32,11 @@ export interface NativeOpenBeneathResult {
|
|
|
31
32
|
fd: number;
|
|
32
33
|
containment: "kernel-atomic" | "best-effort";
|
|
33
34
|
}
|
|
35
|
+
export interface NativeOwnedTreeRemovalResult {
|
|
36
|
+
outcome?: "removed" | "preserved";
|
|
37
|
+
errorCode?: string;
|
|
38
|
+
errorMessage?: string;
|
|
39
|
+
}
|
|
34
40
|
export interface NativeWindowsAccessControlEntry {
|
|
35
41
|
sid: string;
|
|
36
42
|
mask: number;
|
|
@@ -68,14 +74,17 @@ export interface NativeBinding {
|
|
|
68
74
|
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
69
75
|
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
70
76
|
createPrivateDirectory(path: string): void;
|
|
71
|
-
extractArchiveNative(path: string, kind: string, rootFd: number, plan: NativeArchivePlanEntry[],
|
|
77
|
+
extractArchiveNative(path: string, kind: string, rootFd: number, plan: NativeArchivePlanEntry[], limits: TarMeterLimits, signal: AbortSignal): Promise<void>;
|
|
72
78
|
fstatIdentity(fd: number): NativeFileIdentity;
|
|
73
|
-
inspectArchiveNative(path: string, kind: string,
|
|
79
|
+
inspectArchiveNative(path: string, kind: string, limits: TarMeterLimits, signal: AbortSignal): Promise<NativeArchiveEntry[]>;
|
|
74
80
|
linkBeneath(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
75
81
|
mkdirBeneath(rootFd: number, relPath: string, mode: number): void;
|
|
76
82
|
openBeneath(rootFd: number, relPath: string, flags: number): NativeOpenBeneathResult;
|
|
77
|
-
readArchiveEntryNative(path: string, kind: string, requested: string, maxBytes: number,
|
|
83
|
+
readArchiveEntryNative(path: string, kind: string, requested: string, maxBytes: number, limits: TarMeterLimits, signal: AbortSignal): Promise<Buffer>;
|
|
78
84
|
readOwnerAndDacl(path: string): NativeWindowsSecurityFacts;
|
|
85
|
+
ownedTreeRemovalAvailable?(parentFd: number): boolean;
|
|
86
|
+
removeOwnedTree?(parentFd: number, basename: string, directoryFd: number): Promise<NativeOwnedTreeRemovalResult>;
|
|
87
|
+
removeOwnedTreeSync?(parentFd: number, basename: string, directoryFd: number): NativeOwnedTreeRemovalResult;
|
|
79
88
|
renameNoReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
80
89
|
renameReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
81
90
|
sha256File(fd: number): Promise<NativeFileHash>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-sibling.d.ts","sourceRoot":"","sources":["../src/output-sibling.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"output-sibling.d.ts","sourceRoot":"","sources":["../src/output-sibling.ts"],"names":[],"mappings":"AAoBA,wBAAsB,2BAA2B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,CAAC,CAAC,CAYb"}
|
package/dist/output-sibling.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
|
-
import fs, {} from "node:fs/promises";
|
|
4
2
|
import path from "node:path";
|
|
5
|
-
import { createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
6
|
-
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
7
|
-
import { FsSafeError } from "./errors.js";
|
|
8
|
-
import { sameFileIdentity, sameFileIdentityForCleanup } from "./file-identity.js";
|
|
9
|
-
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
10
3
|
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
11
|
-
import {
|
|
12
|
-
import { serializePathWrite } from "./write-queue.js";
|
|
13
|
-
const OPEN_READ_WRITE_NOFOLLOW = fsSync.constants.O_RDWR |
|
|
14
|
-
(process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
15
|
-
? fsSync.constants.O_NOFOLLOW
|
|
16
|
-
: 0);
|
|
4
|
+
import { writeCallbackSibling } from "./sibling-staged-file.js";
|
|
17
5
|
function safeFallbackFileName(fallbackFileName) {
|
|
18
6
|
return sanitizeUntrustedFileName(fallbackFileName ?? "output.bin", "output.bin");
|
|
19
7
|
}
|
|
@@ -21,103 +9,16 @@ function buildSiblingTempPath(targetPath, fallbackFileName) {
|
|
|
21
9
|
const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), safeFallbackFileName(fallbackFileName));
|
|
22
10
|
return path.join(path.dirname(targetPath), `.fs-safe-output-${process.pid}-${randomUUID()}-${safeTail}.part`);
|
|
23
11
|
}
|
|
24
|
-
function assertStagedFile(pathname, opened, maxBytes) {
|
|
25
|
-
if (pathname.isSymbolicLink() || !pathname.isFile() || !opened.isFile()) {
|
|
26
|
-
throw new FsSafeError("not-file", "sibling-staged output must be a regular file");
|
|
27
|
-
}
|
|
28
|
-
if (!sameFileIdentity(pathname, opened)) {
|
|
29
|
-
throw new FsSafeError("path-mismatch", "sibling-staged output changed while opening");
|
|
30
|
-
}
|
|
31
|
-
if (opened.nlink > 1) {
|
|
32
|
-
throw new FsSafeError("hardlink", "hardlinked sibling-staged output not allowed");
|
|
33
|
-
}
|
|
34
|
-
if (maxBytes !== undefined && opened.size > maxBytes) {
|
|
35
|
-
throw new FsSafeError("too-large", `sibling-staged output exceeds maxBytes (${maxBytes})`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function openPinnedStagedFile(tempPath, maxBytes) {
|
|
39
|
-
const preview = await fs.lstat(tempPath);
|
|
40
|
-
if (preview.isSymbolicLink()) {
|
|
41
|
-
throw new FsSafeError("symlink", "symlink sibling-staged output not allowed");
|
|
42
|
-
}
|
|
43
|
-
const handle = await fs.open(tempPath, OPEN_READ_WRITE_NOFOLLOW);
|
|
44
|
-
try {
|
|
45
|
-
const identity = await handle.stat();
|
|
46
|
-
assertStagedFile(await fs.lstat(tempPath), identity, maxBytes);
|
|
47
|
-
return { handle, identity };
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
await handle.close().catch(() => undefined);
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function syncFile(handle) {
|
|
55
|
-
try {
|
|
56
|
-
await handle.sync();
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
if (error.code !== "EPERM")
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function cleanupTempPath(tempPath, identity) {
|
|
64
|
-
try {
|
|
65
|
-
const current = await fs.lstat(tempPath);
|
|
66
|
-
if (!identity || (!current.isSymbolicLink() && sameFileIdentityForCleanup(current, identity))) {
|
|
67
|
-
await fs.rm(tempPath, { force: true });
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
return error.code === "ENOENT";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
12
|
export async function writeExternalFileViaSibling(params) {
|
|
76
13
|
const finalPath = path.resolve(params.finalPath);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
unregisterTempPath.setIdentity(await pinned.handle.stat({ bigint: true }));
|
|
88
|
-
try {
|
|
89
|
-
if (params.mode !== undefined) {
|
|
90
|
-
await pinned.handle.chmod(params.mode);
|
|
91
|
-
}
|
|
92
|
-
await syncFile(pinned.handle);
|
|
93
|
-
await serializePathWrite(finalPath, async () => {
|
|
94
|
-
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
95
|
-
const currentIdentity = await pinned.handle.stat();
|
|
96
|
-
assertStagedFile(await fs.lstat(tempPath), currentIdentity, params.maxBytes);
|
|
97
|
-
if (!sameFileIdentity(currentIdentity, stagedIdentity)) {
|
|
98
|
-
throw new FsSafeError("path-mismatch", "sibling-staged output changed before rename");
|
|
99
|
-
}
|
|
100
|
-
await fs.rename(tempPath, finalPath);
|
|
101
|
-
renamed = true;
|
|
102
|
-
const published = await fs.lstat(finalPath);
|
|
103
|
-
if (published.isSymbolicLink() || !sameFileIdentity(published, currentIdentity)) {
|
|
104
|
-
throw new FsSafeError("path-mismatch", "sibling-staged output changed during rename");
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
await syncDirectoryBestEffort(parentPath);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
await pinned.handle.close().catch(() => undefined);
|
|
112
|
-
}
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
let cleanupComplete = renamed;
|
|
117
|
-
if (!renamed) {
|
|
118
|
-
cleanupComplete = await cleanupTempPath(tempPath, stagedIdentity);
|
|
119
|
-
}
|
|
120
|
-
if (cleanupComplete)
|
|
121
|
-
unregisterTempPath();
|
|
122
|
-
}
|
|
14
|
+
const { result } = await writeCallbackSibling({
|
|
15
|
+
tempPath: buildSiblingTempPath(finalPath, params.fallbackFileName),
|
|
16
|
+
write: params.write,
|
|
17
|
+
resolveFinalPath: () => finalPath,
|
|
18
|
+
mode: params.mode,
|
|
19
|
+
maxBytes: params.maxBytes,
|
|
20
|
+
syncTempFile: true,
|
|
21
|
+
syncParentDir: true,
|
|
22
|
+
});
|
|
23
|
+
return result;
|
|
123
24
|
}
|
package/dist/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAgDrC"}
|
package/dist/output.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
3
|
import { FsSafeError } from "./errors.js";
|
|
3
4
|
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
4
5
|
import { isPathInside } from "./path.js";
|
|
@@ -48,6 +49,7 @@ function assertFileTargetPath(targetPath) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
export async function writeExternalFileWithinRoot(options) {
|
|
52
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
51
53
|
const targetRoot = await root(options.rootDir);
|
|
52
54
|
const requestedTargetPath = options.path;
|
|
53
55
|
if (requestedTargetPath.length === 0) {
|
|
@@ -72,7 +74,7 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
72
74
|
finalPath: siblingFinalPath,
|
|
73
75
|
write: options.write,
|
|
74
76
|
fallbackFileName: options.fallbackFileName,
|
|
75
|
-
maxBytes
|
|
77
|
+
maxBytes,
|
|
76
78
|
mode: options.mode,
|
|
77
79
|
});
|
|
78
80
|
return { path: siblingFinalPath, result };
|
|
@@ -84,7 +86,7 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
84
86
|
try {
|
|
85
87
|
const result = await options.write(staged.path);
|
|
86
88
|
await targetRoot.copyIn(targetPath, staged.path, {
|
|
87
|
-
maxBytes
|
|
89
|
+
maxBytes,
|
|
88
90
|
mode: options.mode,
|
|
89
91
|
mkdir: true,
|
|
90
92
|
sourceHardlinks: "reject",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CAoDvB"}
|
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
|
}
|
|
@@ -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;
|
|
@@ -209,11 +212,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
209
212
|
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
210
213
|
}
|
|
211
214
|
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
212
|
-
|
|
213
|
-
(process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
214
|
-
? fsSync.constants.O_NOFOLLOW
|
|
215
|
-
: 0);
|
|
216
|
-
readHandle = await fs.open(targetPath, readFlags);
|
|
215
|
+
readHandle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
217
216
|
const readHandleStat = await readHandle.stat({ bigint: true });
|
|
218
217
|
const actualHash = sha256Hex(await readHandle.readFile());
|
|
219
218
|
if (actualHash !== expectedHash) {
|
|
@@ -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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"}
|