@openclaw/fs-safe 0.7.2 → 0.8.1
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 +71 -0
- package/README.md +5 -1
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +9 -10
- package/dist/archive-merge.d.ts +18 -0
- package/dist/archive-merge.d.ts.map +1 -0
- package/dist/archive-merge.js +157 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +25 -5
- package/dist/archive-policy.d.ts +1 -1
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +3 -2
- package/dist/archive-staging.d.ts +13 -8
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +44 -192
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +9 -5
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-zip-admission.d.ts +2 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +8 -1
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +6 -2
- package/dist/archive-zip-entry.d.ts +1 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-names.d.ts +1 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +1 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +17 -25
- package/dist/directory-guard.d.ts +17 -5
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +8 -6
- package/dist/directory-mode-node.d.ts +8 -0
- package/dist/directory-mode-node.d.ts.map +1 -0
- package/dist/directory-mode-node.js +93 -0
- package/dist/directory-mode-owner.d.ts +21 -0
- package/dist/directory-mode-owner.d.ts.map +1 -0
- package/dist/directory-mode-owner.js +73 -0
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +13 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +8 -11
- package/dist/filename.d.ts +6 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +36 -0
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +2 -2
- package/dist/guarded-mutation.d.ts +2 -2
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/json-document-store.d.ts +3 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +2 -0
- package/dist/json-durable-queue-ownership.d.ts +3 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +7 -4
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +25 -28
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +18 -56
- package/dist/move-path-cleanup.d.ts +43 -0
- package/dist/move-path-cleanup.d.ts.map +1 -0
- package/dist/move-path-cleanup.js +159 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +30 -84
- package/dist/native-operations.d.ts +2 -4
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +7 -9
- package/dist/native-pinned-write-windows.d.ts +2 -2
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +5 -5
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +14 -4
- package/dist/native-staged-file.d.ts +2 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/opened-file-failure.d.ts +1 -0
- package/dist/opened-file-failure.d.ts.map +1 -1
- package/dist/opened-file-failure.js +4 -0
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +9 -3
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +32 -13
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +7 -3
- package/dist/regular-file.d.ts +1 -1
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +35 -6
- package/dist/replace-file-descriptor.d.ts +9 -1
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +58 -10
- package/dist/replace-file-temp-owner.d.ts +7 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +29 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +12 -38
- package/dist/root-context.d.ts +4 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +19 -0
- package/dist/root-impl.d.ts +43 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +13 -11
- package/dist/root-path-symlink.d.ts +6 -2
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +7 -6
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +6 -2
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +4 -0
- package/dist/root-write-verification.d.ts +3 -2
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -0
- package/dist/secret-file.d.ts +9 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +136 -74
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +9 -3
- package/dist/sidecar-lock-acquire.d.ts +1 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +24 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +6 -4
- package/dist/sidecar-lock-types.d.ts +7 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -1
- package/dist/sidecar-lock.d.ts +2 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +44 -3
- package/dist/write-open-flags.d.ts +5 -0
- package/dist/write-open-flags.d.ts.map +1 -0
- package/dist/write-open-flags.js +35 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +56 -7
- package/docs/atomic.md +8 -2
- package/docs/contributing.md +13 -0
- package/docs/durability.md +3 -2
- package/docs/errors.md +3 -1
- package/docs/file-store.md +38 -8
- package/docs/local-roots.md +4 -3
- package/docs/output.md +4 -1
- package/docs/private-file-store.md +11 -2
- package/docs/reading.md +3 -1
- package/docs/regular-file.md +16 -2
- package/docs/root.md +4 -2
- package/docs/secret-file.md +37 -2
- package/docs/sidecar-lock.md +33 -5
- package/docs/store.md +5 -1
- package/docs/temp.md +19 -1
- package/docs/writing.md +24 -3
- package/package.json +14 -10
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
4
|
import type { NativeBinding } from "./native.js";
|
|
5
5
|
import type { PinnedWriteParams } from "./pinned-write.js";
|
|
6
6
|
export declare function sameNativeIdentity(left: Pick<FileIdentityStat, "dev" | "ino">, right: Pick<FileIdentityStat, "dev" | "ino">): boolean;
|
|
7
|
-
export declare function runPinnedWriteWindows(binding: NativeBinding, params: PinnedWriteParams, root: FileHandle, parentFd: number, parentGuard:
|
|
7
|
+
export declare function runPinnedWriteWindows(binding: NativeBinding, params: PinnedWriteParams, root: FileHandle, parentFd: number, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
8
8
|
//# sourceMappingURL=native-pinned-write-windows.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"AAEA,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;AAO3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,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,CAiF3B"}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsSync, {} from "node:fs";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, syncNativeFileBestEffort, writeNativeInput, } from "./native-operations.js";
|
|
5
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
6
|
export function sameNativeIdentity(left, right) {
|
|
6
7
|
return left.dev === right.dev && left.ino === right.ino;
|
|
7
8
|
}
|
|
@@ -26,8 +27,8 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
26
27
|
try {
|
|
27
28
|
tempName = `.fs-safe-${randomUUID()}.tmp`;
|
|
28
29
|
tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const verificationIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(tempFd, { bigint: true }));
|
|
31
|
+
tempIdentity = verificationIdentity;
|
|
31
32
|
// Creation is requested at 0600 in the binding, but a restrictive umask
|
|
32
33
|
// can remove owner access. Keep the unpublished inode private and
|
|
33
34
|
// reopenable until the published name has been identity-fenced.
|
|
@@ -42,8 +43,9 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
42
43
|
}
|
|
43
44
|
renamed = true;
|
|
44
45
|
targetFd = binding.openBeneath(parentFd, params.basename, nativeOpenFlags(fsSync.constants.O_RDONLY)).fd;
|
|
46
|
+
const targetStat = inspectFileIdentitySync(() => fsSync.fstatSync(targetFd, { bigint: true }), verificationIdentity);
|
|
45
47
|
const targetIdentity = binding.fstatIdentity(targetFd);
|
|
46
|
-
if (!
|
|
48
|
+
if (!targetStat.isFile()) {
|
|
47
49
|
throw new FsSafeError("path-mismatch", "native write target changed after rename");
|
|
48
50
|
}
|
|
49
51
|
// Native exclusive creation starts at 0600. Apply the requested mode only
|
|
@@ -57,7 +59,6 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
57
59
|
closeWriteFd(targetFd);
|
|
58
60
|
targetFd = undefined;
|
|
59
61
|
removeNativeCreatedFileIfStillPinned({
|
|
60
|
-
binding,
|
|
61
62
|
parentPath,
|
|
62
63
|
parentFd,
|
|
63
64
|
basename: params.basename,
|
|
@@ -76,7 +77,6 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
76
77
|
const tempCloseError = closeWriteFd(tempFd);
|
|
77
78
|
if (!renamed) {
|
|
78
79
|
removeNativeCreatedFileIfStillPinned({
|
|
79
|
-
binding,
|
|
80
80
|
parentPath,
|
|
81
81
|
parentFd,
|
|
82
82
|
basename: tempName,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"
|
|
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,CA0FvH"}
|
|
@@ -53,10 +53,12 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
53
53
|
import fsSync from "node:fs";
|
|
54
54
|
import fs from "node:fs/promises";
|
|
55
55
|
import path from "node:path";
|
|
56
|
+
import { inspectDirectoryIdentity } from "./directory-guard.js";
|
|
56
57
|
import { FsSafeError } from "./errors.js";
|
|
57
58
|
import { runPinnedWriteWindows, sameNativeIdentity } from "./native-pinned-write-windows.js";
|
|
58
59
|
import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
|
|
59
60
|
import { describeStagedDirectory, exactIdentityMatches } from "./staged-directory.js";
|
|
61
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
60
62
|
export async function runPinnedWriteNative(binding, params) {
|
|
61
63
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
62
64
|
try {
|
|
@@ -79,8 +81,14 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
79
81
|
},
|
|
80
82
|
}, false);
|
|
81
83
|
try {
|
|
84
|
+
const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
|
|
85
|
+
? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
|
|
82
86
|
let rootMatches;
|
|
83
|
-
if (
|
|
87
|
+
if (exactRoot) {
|
|
88
|
+
inspectFileIdentitySync(() => fsSync.fstatSync(root.fd, { bigint: true }), exactRoot);
|
|
89
|
+
rootMatches = true;
|
|
90
|
+
}
|
|
91
|
+
else if (windows) {
|
|
84
92
|
const identity = binding.fstatIdentity(root.fd);
|
|
85
93
|
rootMatches = !params.rootIdentity || sameNativeIdentity(params.rootIdentity, identity);
|
|
86
94
|
}
|
|
@@ -99,14 +107,16 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
99
107
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
100
108
|
: params.rootPath);
|
|
101
109
|
const directory = windows ? undefined : describeStagedDirectory(parentFd, parentPath);
|
|
102
|
-
const parentPathStat =
|
|
103
|
-
|
|
110
|
+
const parentPathStat = exactRoot
|
|
111
|
+
? await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })))
|
|
112
|
+
: await fs.lstat(parentPath);
|
|
113
|
+
if (windows && !exactRoot) {
|
|
104
114
|
const parentIdentity = binding.fstatIdentity(parentFd);
|
|
105
115
|
if (parentPathStat.isSymbolicLink() || !sameNativeIdentity(parentPathStat, parentIdentity)) {
|
|
106
116
|
throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
|
-
else if (parentPathStat.isSymbolicLink() || !exactIdentityMatches(parentPathStat, directory.identity)) {
|
|
119
|
+
else if (!windows && (parentPathStat.isSymbolicLink() || !exactIdentityMatches(parentPathStat, directory.identity))) {
|
|
110
120
|
throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
|
|
111
121
|
}
|
|
112
122
|
const verificationGuard = { dir: parentPath, realPath: parentPath, stat: parentPathStat };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
2
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
3
|
import type { NativeBinding } from "./native-binding.js";
|
|
4
4
|
import type { PinnedWriteInput, PinnedWriteParams } from "./pinned-write.js";
|
|
@@ -9,7 +9,7 @@ declare class NativeStagedFile implements StagedFile {
|
|
|
9
9
|
#private;
|
|
10
10
|
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number);
|
|
11
11
|
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean): Promise<NativeStagedFile>;
|
|
12
|
-
static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard:
|
|
12
|
+
static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
13
13
|
get receipt(): StagedFileReceipt;
|
|
14
14
|
assertCurrent(): Promise<void>;
|
|
15
15
|
publish(basename: string, options: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE7E,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;;IAU1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EAOtB;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,GACnB,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,CAU3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IAoFK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAgD9F;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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BigIntStats } from "node:fs";
|
|
2
2
|
import type { FileHandle } from "node:fs/promises";
|
|
3
3
|
export declare function recordFileOpenFailure(error: unknown, filePath: string): never;
|
|
4
|
+
export declare function recordExclusiveCreateFailure(error: unknown, filePath: string): never;
|
|
4
5
|
export declare function openedPathResolutionError(error?: Error): Error;
|
|
5
6
|
export declare function recordPreOpenFileChange(error: unknown, handle: FileHandle, filePath: string, before: BigIntStats | undefined, opened: BigIntStats | undefined): Promise<void>;
|
|
6
7
|
export declare function recordOpenedFileFailure(error: unknown, handle: FileHandle, filePath: string, identity: BigIntStats): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opened-file-failure.d.ts","sourceRoot":"","sources":["../src/opened-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAG7E;AAED,wBAAgB,yBAAyB,CACvC,KAAK,GAAE,KAA8E,GACpF,KAAK,CAGP;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
1
|
+
{"version":3,"file":"opened-file-failure.d.ts","sourceRoot":"","sources":["../src/opened-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAG7E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAGpF;AAED,wBAAgB,yBAAyB,CACvC,KAAK,GAAE,KAA8E,GACpF,KAAK,CAGP;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -5,6 +5,10 @@ export function recordFileOpenFailure(error, filePath) {
|
|
|
5
5
|
recordFileObservationFailure(error, `open:${filePath}`);
|
|
6
6
|
throw error;
|
|
7
7
|
}
|
|
8
|
+
export function recordExclusiveCreateFailure(error, filePath) {
|
|
9
|
+
recordFileObservationFailure(error, `exclusive-create:${filePath}`);
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
8
12
|
export function openedPathResolutionError(error = new FsSafeError("path-mismatch", "unable to resolve opened file path")) {
|
|
9
13
|
recordFileObservationFailure(error, "resolution");
|
|
10
14
|
return error;
|
|
@@ -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":"AAuBA,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,13 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
3
|
+
import { fitFileNameToPortableComponent, sanitizeUntrustedFileName } from "./filename.js";
|
|
4
4
|
import { writeCallbackSibling } from "./sibling-staged-file.js";
|
|
5
5
|
function safeFallbackFileName(fallbackFileName) {
|
|
6
6
|
return sanitizeUntrustedFileName(fallbackFileName ?? "output.bin", "output.bin");
|
|
7
7
|
}
|
|
8
8
|
function buildSiblingTempPath(targetPath, fallbackFileName) {
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const prefix = `.fs-safe-output-${process.pid}-${randomUUID()}-`;
|
|
10
|
+
const suffix = ".part";
|
|
11
|
+
const safeTail = fitFileNameToPortableComponent({
|
|
12
|
+
prefix,
|
|
13
|
+
fileName: sanitizeUntrustedFileName(path.basename(targetPath), safeFallbackFileName(fallbackFileName)),
|
|
14
|
+
suffix,
|
|
15
|
+
});
|
|
16
|
+
return path.join(path.dirname(targetPath), `${prefix}${safeTail}${suffix}`);
|
|
11
17
|
}
|
|
12
18
|
export async function writeExternalFileViaSibling(params) {
|
|
13
19
|
const finalPath = path.resolve(params.finalPath);
|
package/dist/pinned-write.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
|
-
import { type
|
|
2
|
+
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
4
|
export type PinnedWriteInput = {
|
|
5
5
|
kind: "buffer";
|
|
@@ -26,7 +26,7 @@ export type PinnedWriteParams = {
|
|
|
26
26
|
input: PinnedWriteInput;
|
|
27
27
|
rootIdentity?: FileIdentityStat;
|
|
28
28
|
onRenameIdentityMismatch?: RenameIdentityMismatchPolicy;
|
|
29
|
-
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard:
|
|
29
|
+
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
|
|
30
30
|
};
|
|
31
31
|
export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
|
|
32
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;AAE5C,OAAO,
|
|
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,EAA4F,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG7J,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAY3D,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,sBAAsB,KAChC,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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import fsSync from "node:fs";
|
|
2
|
+
import fsSync, {} from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
6
|
-
import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
6
|
+
import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
8
8
|
import { syncDirectoryBestEffort } from "./fsync.js";
|
|
9
9
|
import { sameFileIdentity, sha256Hex } from "./file-identity.js";
|
|
@@ -13,6 +13,7 @@ import { runPinnedWriteNative } from "./native-pinned-write.js";
|
|
|
13
13
|
import { getNativeBinding } from "./native.js";
|
|
14
14
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
15
15
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
16
|
+
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
16
17
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
17
18
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
18
19
|
function byteLength(input, encoding) {
|
|
@@ -99,6 +100,11 @@ export async function runPinnedWriteWithRenamePolicy(params) {
|
|
|
99
100
|
}));
|
|
100
101
|
}
|
|
101
102
|
async function runPinnedWriteFallback(params) {
|
|
103
|
+
const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
|
|
104
|
+
? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
|
|
105
|
+
if (exactRoot)
|
|
106
|
+
await inspectDirectoryIdentity(params.rootPath, exactRoot);
|
|
107
|
+
const guardOptions = { bigint: exactRoot !== undefined };
|
|
102
108
|
let parentPath = params.relativeParentPath
|
|
103
109
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
104
110
|
: params.rootPath;
|
|
@@ -114,8 +120,8 @@ async function runPinnedWriteFallback(params) {
|
|
|
114
120
|
});
|
|
115
121
|
}
|
|
116
122
|
const parentGuard = params.mkdir
|
|
117
|
-
? await createAsyncDirectoryGuard(parentPath)
|
|
118
|
-
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
123
|
+
? await createAsyncDirectoryGuard(parentPath, guardOptions)
|
|
124
|
+
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath, guardOptions);
|
|
119
125
|
const targetPath = path.join(parentPath, params.basename);
|
|
120
126
|
if (params.overwrite === false) {
|
|
121
127
|
const handle = await withAsyncDirectoryGuards([parentGuard], async () => await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode), {
|
|
@@ -126,9 +132,10 @@ async function runPinnedWriteFallback(params) {
|
|
|
126
132
|
},
|
|
127
133
|
});
|
|
128
134
|
let created = true;
|
|
135
|
+
let createdIdentity;
|
|
129
136
|
try {
|
|
130
137
|
const verificationIdentity = await handle.stat({ bigint: true });
|
|
131
|
-
|
|
138
|
+
createdIdentity = verificationIdentity;
|
|
132
139
|
if (params.input.kind === "buffer") {
|
|
133
140
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
134
141
|
if (typeof params.input.data === "string") {
|
|
@@ -141,6 +148,8 @@ async function runPinnedWriteFallback(params) {
|
|
|
141
148
|
else {
|
|
142
149
|
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
143
150
|
}
|
|
151
|
+
// Content writes may clear set-ID bits; finalize them through the owned fd.
|
|
152
|
+
await handle.chmod(params.mode);
|
|
144
153
|
await syncFileBestEffort(handle);
|
|
145
154
|
const stat = await handle.stat();
|
|
146
155
|
await syncDirectoryBestEffort(parentPath);
|
|
@@ -150,9 +159,13 @@ async function runPinnedWriteFallback(params) {
|
|
|
150
159
|
return { dev: stat.dev, ino: stat.ino };
|
|
151
160
|
}
|
|
152
161
|
finally {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
try {
|
|
163
|
+
if (created) {
|
|
164
|
+
await cleanupPinnedFilePath({ pathname: targetPath, handle, identity: createdIdentity, parentGuard });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await handle.close().catch(() => undefined);
|
|
156
169
|
}
|
|
157
170
|
}
|
|
158
171
|
}
|
|
@@ -166,12 +179,13 @@ async function runPinnedWriteFallback(params) {
|
|
|
166
179
|
: 0);
|
|
167
180
|
let handle;
|
|
168
181
|
let tempStat;
|
|
182
|
+
let tempIdentity;
|
|
169
183
|
let readHandle;
|
|
170
184
|
let renamed = false;
|
|
171
185
|
try {
|
|
172
186
|
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
173
187
|
let verificationIdentity = await handle.stat({ bigint: true });
|
|
174
|
-
|
|
188
|
+
tempIdentity = verificationIdentity;
|
|
175
189
|
if (params.input.kind === "buffer") {
|
|
176
190
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
177
191
|
if (typeof params.input.data === "string") {
|
|
@@ -190,6 +204,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
190
204
|
throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
|
|
191
205
|
}
|
|
192
206
|
const expectedTempStat = tempStat;
|
|
207
|
+
await handle.chmod(params.mode);
|
|
193
208
|
await syncFileBestEffort(handle);
|
|
194
209
|
let verifiedIdentity = expectedTempStat;
|
|
195
210
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
@@ -229,10 +244,14 @@ async function runPinnedWriteFallback(params) {
|
|
|
229
244
|
return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
|
|
230
245
|
}
|
|
231
246
|
finally {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
try {
|
|
248
|
+
if (!renamed && handle) {
|
|
249
|
+
await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
await readHandle?.close().catch(() => undefined);
|
|
254
|
+
await handle?.close().catch(() => undefined);
|
|
236
255
|
}
|
|
237
256
|
}
|
|
238
257
|
}
|
|
@@ -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;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;
|
|
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;AA0ND,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
|
@@ -90,9 +90,11 @@ async function copyPinnedSource(params) {
|
|
|
90
90
|
throw error;
|
|
91
91
|
}
|
|
92
92
|
let target;
|
|
93
|
-
|
|
93
|
+
let createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
|
|
94
94
|
rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
|
|
95
95
|
try {
|
|
96
|
+
fsSync.fchmodSync(nativeFd, 0o600);
|
|
97
|
+
createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
|
|
96
98
|
await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
|
|
97
99
|
const identity = await fs.lstat(params.targetPath, { bigint: true });
|
|
98
100
|
target = await fs.open(params.targetPath, sourceOpenFlags());
|
|
@@ -124,9 +126,11 @@ async function copyPinnedSource(params) {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
const target = await fs.open(params.targetPath, "wx+", 0o600);
|
|
127
|
-
const
|
|
128
|
-
rememberCreatedTarget(params.failure,
|
|
129
|
+
const createdIdentity = await target.stat({ bigint: true });
|
|
130
|
+
rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
|
|
129
131
|
try {
|
|
132
|
+
await target.chmod(0o600);
|
|
133
|
+
const exactIdentity = await target.stat({ bigint: true });
|
|
130
134
|
await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, exactIdentity);
|
|
131
135
|
const hash = createHash("sha256");
|
|
132
136
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
package/dist/regular-file.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type RegularFileStatResult = {
|
|
|
6
6
|
missing: false;
|
|
7
7
|
stat: Stats;
|
|
8
8
|
};
|
|
9
|
-
type RegularFileAppendFlagConstants = Pick<typeof fsSync.constants, "O_APPEND" | "O_CREAT" | "O_WRONLY"> & Partial<Pick<typeof fsSync.constants, "O_NOFOLLOW">>;
|
|
9
|
+
type RegularFileAppendFlagConstants = Pick<typeof fsSync.constants, "O_APPEND" | "O_CREAT" | "O_WRONLY"> & Partial<Pick<typeof fsSync.constants, "O_NOFOLLOW" | "O_NONBLOCK">>;
|
|
10
10
|
export type AppendRegularFileOptions = {
|
|
11
11
|
filePath: string;
|
|
12
12
|
content: string | Uint8Array;
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAmB7B,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,GAAG,YAAY,CAAC,CAAC,CAAC;AAEtE,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,CASR;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,CAgFxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAgF7E"}
|
package/dist/regular-file.js
CHANGED
|
@@ -10,12 +10,14 @@ import { isNotFoundPathError } from "./path.js";
|
|
|
10
10
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
11
11
|
import { assertNoSymlinkParents, assertNoSymlinkParentsSync } from "./symlink-parents.js";
|
|
12
12
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
13
|
+
import { isNonRegularWriteOpenError, isNonRegularWriteOpenErrorSync, resolveNonblockingWriteFlag, } from "./write-open-flags.js";
|
|
13
14
|
export function resolveRegularFileAppendFlags(constants = fsSync.constants) {
|
|
14
15
|
const noFollow = constants.O_NOFOLLOW;
|
|
15
16
|
return (constants.O_CREAT |
|
|
16
17
|
constants.O_APPEND |
|
|
17
18
|
constants.O_WRONLY |
|
|
18
|
-
(typeof noFollow === "number" ? noFollow : 0)
|
|
19
|
+
(typeof noFollow === "number" ? noFollow : 0) |
|
|
20
|
+
resolveNonblockingWriteFlag(constants));
|
|
19
21
|
}
|
|
20
22
|
function regularFileTooLargeError(filePath, maxBytes, cause) {
|
|
21
23
|
return new FsSafeError("too-large", `File exceeds ${maxBytes} bytes: ${filePath}`, { cause });
|
|
@@ -265,7 +267,17 @@ export async function appendRegularFile(options) {
|
|
|
265
267
|
return;
|
|
266
268
|
}
|
|
267
269
|
await getFsSafeTestHooks()?.beforeRegularFileAppendOpen?.(options.filePath);
|
|
268
|
-
const
|
|
270
|
+
const flags = resolveRegularFileAppendFlags();
|
|
271
|
+
let handle;
|
|
272
|
+
try {
|
|
273
|
+
handle = await fs.open(options.filePath, flags, options.mode ?? 0o600);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (await isNonRegularWriteOpenError(error, options.filePath, flags)) {
|
|
277
|
+
throw new Error(`Refusing to append to non-file: ${options.filePath}`);
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
269
281
|
try {
|
|
270
282
|
let identity;
|
|
271
283
|
try {
|
|
@@ -287,8 +299,12 @@ export async function appendRegularFile(options) {
|
|
|
287
299
|
Number(identity.size) + contentBytes > options.maxFileBytes) {
|
|
288
300
|
return;
|
|
289
301
|
}
|
|
290
|
-
|
|
302
|
+
const mode = options.mode ?? 0o600;
|
|
303
|
+
// Tighten before writing; restore explicit special bits only after content is complete.
|
|
304
|
+
await handle.chmod(mode);
|
|
291
305
|
await handle.appendFile(options.content, options.encoding ?? "utf8");
|
|
306
|
+
if (mode & 0o7000)
|
|
307
|
+
await handle.chmod(mode);
|
|
292
308
|
}
|
|
293
309
|
finally {
|
|
294
310
|
await handle.close();
|
|
@@ -331,7 +347,17 @@ export function appendRegularFileSync(options) {
|
|
|
331
347
|
return;
|
|
332
348
|
}
|
|
333
349
|
getFsSafeTestHooks()?.beforeRegularFileAppendOpenSync?.(options.filePath);
|
|
334
|
-
const
|
|
350
|
+
const flags = resolveRegularFileAppendFlags();
|
|
351
|
+
let fd;
|
|
352
|
+
try {
|
|
353
|
+
fd = fsSync.openSync(options.filePath, flags, options.mode ?? 0o600);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (isNonRegularWriteOpenErrorSync(error, options.filePath, flags)) {
|
|
357
|
+
throw new Error(`Refusing to append to non-file: ${options.filePath}`);
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
335
361
|
try {
|
|
336
362
|
let identity;
|
|
337
363
|
try {
|
|
@@ -353,8 +379,11 @@ export function appendRegularFileSync(options) {
|
|
|
353
379
|
Number(identity.size) + contentBuffer.byteLength > options.maxFileBytes) {
|
|
354
380
|
return;
|
|
355
381
|
}
|
|
356
|
-
|
|
357
|
-
fsSync.
|
|
382
|
+
const mode = options.mode ?? 0o600;
|
|
383
|
+
fsSync.fchmodSync(fd, mode);
|
|
384
|
+
fsSync.appendFileSync(fd, contentBuffer);
|
|
385
|
+
if (mode & 0o7000)
|
|
386
|
+
fsSync.fchmodSync(fd, mode);
|
|
358
387
|
}
|
|
359
388
|
finally {
|
|
360
389
|
fsSync.closeSync(fd);
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
2
|
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
|
+
import { type DirectoryModeOwner } from "./directory-mode-owner.js";
|
|
3
4
|
type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
|
|
4
5
|
type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
|
|
5
6
|
export type SyncFchmod = (fd: number, mode: number) => void;
|
|
7
|
+
export declare function syncDirectoryBestEffort(fsModule: Pick<typeof fs, "open">, dirPath: string): Promise<void>;
|
|
8
|
+
export declare function syncDirectoryBestEffortSync(fsModule: Pick<typeof syncFs, "openSync" | "fsyncSync" | "closeSync">, dirPath: string): void;
|
|
9
|
+
export declare function pinDirectoryForMode(params: {
|
|
10
|
+
fsModule: AsyncTempFileSystem;
|
|
11
|
+
dirPath: string;
|
|
12
|
+
/** Compatibility for best-effort directory modes; admission and close still fail closed. */
|
|
13
|
+
ignoreChmodError?: boolean;
|
|
14
|
+
}): Promise<DirectoryModeOwner | undefined>;
|
|
6
15
|
export declare function applyDirectoryMode(params: {
|
|
7
16
|
fsModule: AsyncTempFileSystem;
|
|
8
17
|
dirPath: string;
|
|
9
18
|
mode: number;
|
|
10
|
-
/** Compatibility for best-effort directory modes; admission and close still fail closed. */
|
|
11
19
|
ignoreChmodError?: boolean;
|
|
12
20
|
}): Promise<void>;
|
|
13
21
|
export declare function applyDirectoryModeSync(params: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE5G,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,EACrE,OAAO,EAAE,MAAM,GACd,IAAI,CAgBN;AA2BD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CA4B1C;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAcP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CA0BzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CA0BxC"}
|
|
@@ -3,6 +3,40 @@ import fs, {} from "node:fs/promises";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
5
|
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
+
import { assertOwnedDirectory, ownDirectoryMode } from "./directory-mode-owner.js";
|
|
7
|
+
export async function syncDirectoryBestEffort(fsModule, dirPath) {
|
|
8
|
+
let handle;
|
|
9
|
+
try {
|
|
10
|
+
handle = await fsModule.open(dirPath, "r");
|
|
11
|
+
await handle.sync();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Best-effort on platforms/filesystems that do not support directory fsync.
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
await handle?.close().catch(() => undefined);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function syncDirectoryBestEffortSync(fsModule, dirPath) {
|
|
21
|
+
let fd;
|
|
22
|
+
try {
|
|
23
|
+
fd = fsModule.openSync(dirPath, "r");
|
|
24
|
+
fsModule.fsyncSync(fd);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Best-effort on platforms/filesystems that do not support directory fsync.
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (fd !== undefined) {
|
|
31
|
+
try {
|
|
32
|
+
fsModule.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Best-effort close after directory fsync.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
6
40
|
function directoryOpenFlags() {
|
|
7
41
|
return (syncFs.constants.O_RDONLY |
|
|
8
42
|
syncFs.constants.O_DIRECTORY |
|
|
@@ -20,7 +54,7 @@ function assertSameDirectory(expected, opened, dirPath) {
|
|
|
20
54
|
throw new FsSafeError("path-mismatch", `Atomic replace parent changed before its mode could be applied: ${dirPath}`);
|
|
21
55
|
}
|
|
22
56
|
}
|
|
23
|
-
export async function
|
|
57
|
+
export async function pinDirectoryForMode(params) {
|
|
24
58
|
// Node does not enforce POSIX directory modes on Windows, and its directory
|
|
25
59
|
// descriptors are not consistently openable. mkdir(mode) remains the only
|
|
26
60
|
// bounded behavior there; never fall back to a pathname chmod.
|
|
@@ -31,17 +65,31 @@ export async function applyDirectoryMode(params) {
|
|
|
31
65
|
assertDirectory(expected, params.dirPath);
|
|
32
66
|
const handle = await params.fsModule.open(params.dirPath, directoryOpenFlags());
|
|
33
67
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
const owner = ownDirectoryMode({
|
|
69
|
+
async inspect() {
|
|
70
|
+
const opened = await handle.stat();
|
|
71
|
+
assertOwnedDirectory(expected, opened);
|
|
72
|
+
return opened.mode & 0o7777;
|
|
73
|
+
},
|
|
74
|
+
chmod: (mode) => handle.chmod(mode),
|
|
75
|
+
close: () => handle.close(),
|
|
76
|
+
ignoreChmodError: params.ignoreChmodError,
|
|
77
|
+
});
|
|
78
|
+
await owner.verify();
|
|
79
|
+
return owner;
|
|
42
80
|
}
|
|
43
|
-
|
|
81
|
+
catch (error) {
|
|
44
82
|
await handle.close();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function applyDirectoryMode(params) {
|
|
87
|
+
const owner = await pinDirectoryForMode(params);
|
|
88
|
+
try {
|
|
89
|
+
await owner?.apply(params.mode);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await owner?.close();
|
|
45
93
|
}
|
|
46
94
|
}
|
|
47
95
|
export function applyDirectoryModeSync(params) {
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
2
|
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
|
+
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
4
|
type AsyncOwnerFileSystem = Pick<typeof fs, "lstat" | "open" | "unlink">;
|
|
4
5
|
type SyncOwnerFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "unlinkSync">;
|
|
6
|
+
export declare function cleanupPinnedFilePath(params: {
|
|
7
|
+
pathname: string;
|
|
8
|
+
handle: FileHandle;
|
|
9
|
+
identity?: BigIntStats;
|
|
10
|
+
parentGuard: AnyAsyncDirectoryGuard;
|
|
11
|
+
}): Promise<void>;
|
|
5
12
|
export declare class AsyncAtomicTempOwner {
|
|
6
13
|
#private;
|
|
7
14
|
readonly pathname: string;
|