@openclaw/fs-safe 0.10.0 → 0.11.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 +56 -0
- package/LICENSE +1 -0
- package/README.md +36 -5
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +4 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +4 -0
- package/dist/archive-durability.d.ts +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -5
- package/dist/archive-gzip-tail.d.ts +1 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -12
- package/dist/archive-native.js +4 -4
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +17 -9
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +7 -6
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +16 -13
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +13 -8
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +5 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +16 -9
- package/dist/copy-file-input.d.ts +0 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +4 -26
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +57 -26
- package/dist/copy.d.ts +1 -1
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +3 -1
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +91 -31
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +6 -4
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +3 -2
- package/dist/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +413 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +3 -2
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +6 -4
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -1
- package/dist/move-path-stage.d.ts.map +1 -1
- package/dist/move-path-stage.js +2 -1
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +4 -3
- package/dist/mutation-authority.d.ts +1 -0
- package/dist/mutation-authority.d.ts.map +1 -1
- package/dist/mutation-authority.js +4 -4
- package/dist/native-binding.d.ts +7 -2
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +3 -2
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +2 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -1
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +48 -6
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +2 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +8 -8
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +3 -1
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +21 -3
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +5 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +13 -12
- package/dist/root-impl.d.ts +13 -3
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +40 -36
- package/dist/root-options.d.ts +12 -1
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +4 -3
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +13 -9
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +2 -1
- package/dist/root-symlink-policy.d.ts.map +1 -1
- package/dist/root-symlink-policy.js +2 -2
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +2 -1
- package/dist/root.d.ts +2 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +21 -4
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +1 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +42 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +6 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +15 -4
- 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 +17 -0
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +12 -6
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +7 -5
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +3 -2
- package/dist/write-file-handle.d.ts +1 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +3 -2
- package/docs/advanced.md +4 -0
- package/docs/archive.md +25 -5
- package/docs/atomic.md +17 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +29 -1
- package/docs/copy.md +75 -6
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +36 -1
- package/docs/entries.md +109 -0
- package/docs/errors.md +3 -3
- package/docs/file-store.md +15 -0
- package/docs/guest.md +141 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install.md +31 -0
- package/docs/native-helper.md +10 -3
- package/docs/native.md +4 -0
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/permissions.md +11 -2
- package/docs/public-api.md +31 -2
- package/docs/root.md +30 -2
- package/docs/secure-file.md +2 -0
- package/docs/sidecar-lock.md +12 -3
- package/docs/temp.md +35 -6
- package/docs/types.md +1 -1
- package/docs/writing.md +153 -3
- package/package.json +14 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"AAuEA,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CA+G3C"}
|
|
@@ -2,12 +2,14 @@ import { syncFileBestEffort } from "./file-sync.js";
|
|
|
2
2
|
import fsSync, {} from "node:fs";
|
|
3
3
|
import fs, {} from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
5
|
+
import { assertAsyncDirectoryGuard, assertDirectoryIdentitySync, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
6
6
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
8
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
+
import { root } from "./root.js";
|
|
9
10
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
10
11
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
12
|
+
import { createOwnedTempFile } from "./temp-target.js";
|
|
11
13
|
import { serializePathWrite } from "./write-queue.js";
|
|
12
14
|
function assertRegularFile(stat) {
|
|
13
15
|
if (stat.isSymbolicLink()) {
|
|
@@ -27,17 +29,39 @@ async function inspectStage(inspect, expected) {
|
|
|
27
29
|
return stat;
|
|
28
30
|
}, expected);
|
|
29
31
|
}
|
|
32
|
+
// Own the workspace before the producer can leave partial output. The finished
|
|
33
|
+
// file still enters the ordinary sibling admission and publication lifecycle.
|
|
34
|
+
async function writeIsolatedProducer(params) {
|
|
35
|
+
const targetRoot = await root(path.dirname(params.tempPath));
|
|
36
|
+
const { target, identity } = await createOwnedTempFile({
|
|
37
|
+
rootDir: targetRoot.rootReal,
|
|
38
|
+
prefix: "fs-safe-output",
|
|
39
|
+
fileName: path.basename(params.tempPath),
|
|
40
|
+
});
|
|
41
|
+
const assertCurrent = () => {
|
|
42
|
+
params.assertParent();
|
|
43
|
+
assertDirectoryIdentitySync(target.dir, { ...identity, realPath: target.dir });
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
assertCurrent();
|
|
47
|
+
const result = await params.write(target.path);
|
|
48
|
+
assertCurrent();
|
|
49
|
+
await targetRoot.move(path.relative(targetRoot.rootReal, target.path), path.basename(params.tempPath), {
|
|
50
|
+
assertBeforeMutation: assertCurrent,
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await target.cleanup();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
30
58
|
// Callback paths are not owned until all three admission observations agree.
|
|
31
59
|
// Keep one descriptor and one exact identity through mode, sync, rename and cleanup.
|
|
32
60
|
// Read/write access is needed only when the caller requests file synchronization.
|
|
33
61
|
export async function writeCallbackSibling(params) {
|
|
34
62
|
const parent = path.dirname(params.tempPath);
|
|
35
|
-
const guard = await createAsyncDirectoryGuard(parent);
|
|
36
|
-
const
|
|
37
|
-
const assertParent = async () => {
|
|
38
|
-
await assertAsyncDirectoryGuard(guard);
|
|
39
|
-
await inspectFileIdentity(() => fsSync.lstatSync(parent, { bigint: true }), parentIdentity);
|
|
40
|
-
};
|
|
63
|
+
const guard = await createAsyncDirectoryGuard(parent, { bigint: true });
|
|
64
|
+
const assertParent = () => assertAsyncDirectoryGuard(guard);
|
|
41
65
|
let handle;
|
|
42
66
|
let identity;
|
|
43
67
|
let unregister;
|
|
@@ -54,7 +78,17 @@ export async function writeCallbackSibling(params) {
|
|
|
54
78
|
}
|
|
55
79
|
};
|
|
56
80
|
try {
|
|
57
|
-
const result =
|
|
81
|
+
const result = params.producerIsolation === "private-directory"
|
|
82
|
+
? await writeIsolatedProducer({
|
|
83
|
+
tempPath: params.tempPath,
|
|
84
|
+
write: params.write,
|
|
85
|
+
assertParent: () => assertDirectoryIdentitySync(parent, {
|
|
86
|
+
dev: guard.stat.dev,
|
|
87
|
+
ino: guard.stat.ino,
|
|
88
|
+
realPath: guard.realPath,
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
: await params.write(params.tempPath);
|
|
58
92
|
await assertParent();
|
|
59
93
|
const before = await inspectPath(params.tempPath);
|
|
60
94
|
try {
|
package/dist/sibling-temp.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type WriteSiblingTempFileOptions<T> = {
|
|
2
2
|
dir: string;
|
|
3
3
|
writeTemp: (tempPath: string) => Promise<T>;
|
|
4
|
+
/** Own a private sibling workspace before invoking the producer. */
|
|
5
|
+
producerIsolation?: "private-directory";
|
|
4
6
|
resolveFinalPath: (result: T) => string;
|
|
5
7
|
tempPrefix?: string;
|
|
6
8
|
dirMode?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AASF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAqBxC;AAwBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChB"}
|
package/dist/sibling-temp.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import crypto, { randomUUID } from "node:crypto";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
6
5
|
import { fitFileNameToPortableComponent, sanitizeUntrustedFileName } from "./filename.js";
|
|
7
6
|
import { applyDirectoryMode } from "./replace-file-descriptor.js";
|
|
7
|
+
import { realpathSync } from "./realpath.js";
|
|
8
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
8
9
|
import { root } from "./root.js";
|
|
9
10
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
10
11
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
@@ -19,7 +20,7 @@ function buildTempPath(dir, tempPrefix) {
|
|
|
19
20
|
}
|
|
20
21
|
export async function writeSiblingTempFile(options) {
|
|
21
22
|
const dir = path.resolve(options.dir);
|
|
22
|
-
await fs.mkdir(dir, { recursive: true, mode: options.dirMode ?? 0o700 });
|
|
23
|
+
await fs.mkdir(recursiveMkdirPath(dir), { recursive: true, mode: options.dirMode ?? 0o700 });
|
|
23
24
|
if (options.chmodDir !== false) {
|
|
24
25
|
await applyDirectoryMode({
|
|
25
26
|
fsModule: fs,
|
|
@@ -31,6 +32,7 @@ export async function writeSiblingTempFile(options) {
|
|
|
31
32
|
return await writeCallbackSibling({
|
|
32
33
|
tempPath: buildTempPath(dir, options.tempPrefix),
|
|
33
34
|
write: options.writeTemp,
|
|
35
|
+
producerIsolation: options.producerIsolation,
|
|
34
36
|
resolveFinalPath: options.resolveFinalPath,
|
|
35
37
|
mode: options.mode,
|
|
36
38
|
ignoreModeError: true,
|
|
@@ -55,7 +57,7 @@ function buildSiblingTempPath(params) {
|
|
|
55
57
|
export async function writeViaSiblingTempPath(params) {
|
|
56
58
|
let rootDir;
|
|
57
59
|
try {
|
|
58
|
-
rootDir =
|
|
60
|
+
rootDir = realpathSync.native(path.resolve(params.rootDir));
|
|
59
61
|
}
|
|
60
62
|
catch {
|
|
61
63
|
rootDir = path.resolve(params.rootDir);
|
|
@@ -63,7 +65,7 @@ export async function writeViaSiblingTempPath(params) {
|
|
|
63
65
|
const requestedTargetPath = path.resolve(params.targetPath);
|
|
64
66
|
let targetPath;
|
|
65
67
|
try {
|
|
66
|
-
const realDir =
|
|
68
|
+
const realDir = realpathSync.native(path.dirname(requestedTargetPath));
|
|
67
69
|
targetPath = path.join(realDir, path.basename(requestedTargetPath));
|
|
68
70
|
}
|
|
69
71
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAUA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAW3C,OAAO,EAWL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAI5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEvF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAqBF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CA6T5B"}
|
|
@@ -5,9 +5,11 @@ import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { fileObservation } from "./file-observation.js";
|
|
7
7
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
8
|
+
import { realpathSync } from "./realpath.js";
|
|
9
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
8
10
|
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
9
11
|
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
10
|
-
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
12
|
+
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockStaleMs, validateSidecarLockRetryOptions, validateSidecarLockCompromiseCheckIntervalMs, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
11
13
|
import { readSidecarLockRawSnapshot, parseSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
|
|
12
14
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
13
15
|
import { sleep } from "./timing.js";
|
|
@@ -22,9 +24,9 @@ async function resolveNormalizedTargetPath(targetPath, lockRoot) {
|
|
|
22
24
|
await lockRoot.resolve(".");
|
|
23
25
|
return path.join(parent, path.basename(resolved));
|
|
24
26
|
}
|
|
25
|
-
await fs.mkdir(dir, { recursive: true });
|
|
27
|
+
await fs.mkdir(recursiveMkdirPath(dir), { recursive: true });
|
|
26
28
|
try {
|
|
27
|
-
return path.join(
|
|
29
|
+
return path.join(realpathSync.native(dir), path.basename(resolved));
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
30
32
|
return resolved;
|
|
@@ -34,6 +36,8 @@ export async function acquireSidecarLock(options, context) {
|
|
|
34
36
|
const retry = options.retry ?? {};
|
|
35
37
|
validateSidecarLockRetryOptions(retry);
|
|
36
38
|
validateSidecarLockTimeoutMs(options.timeoutMs);
|
|
39
|
+
validateSidecarLockStaleMs(options.staleMs);
|
|
40
|
+
validateSidecarLockCompromiseCheckIntervalMs(options.compromiseCheckIntervalMs);
|
|
37
41
|
context.ensureExitCleanupRegistered();
|
|
38
42
|
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath, options.lockRoot);
|
|
39
43
|
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
@@ -153,7 +157,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
153
157
|
try {
|
|
154
158
|
handle =
|
|
155
159
|
(await createNativeExclusiveFile(lockPath, 0o600)) ??
|
|
156
|
-
(await fs.open(lockPath, "wx"));
|
|
160
|
+
(await fs.open(lockPath, "wx", 0o600));
|
|
157
161
|
}
|
|
158
162
|
catch (createError) {
|
|
159
163
|
lockFileCreateDenied = isTransientLockFileDenial(createError, lockPath);
|
|
@@ -194,7 +198,11 @@ export async function acquireSidecarLock(options, context) {
|
|
|
194
198
|
const returnedHandle = context.handleForHeldLock(normalizedTargetPath, createdHeld);
|
|
195
199
|
const interval = options.compromiseCheckIntervalMs;
|
|
196
200
|
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
201
|
+
let compromiseCheckInFlight = false;
|
|
197
202
|
createdHeld.compromiseTimer = setInterval(() => {
|
|
203
|
+
if (compromiseCheckInFlight)
|
|
204
|
+
return;
|
|
205
|
+
compromiseCheckInFlight = true;
|
|
198
206
|
void returnedHandle
|
|
199
207
|
.verifyStillHeld()
|
|
200
208
|
.catch(() => false)
|
|
@@ -204,6 +212,9 @@ export async function acquireSidecarLock(options, context) {
|
|
|
204
212
|
createdHeld.compromiseTimer = undefined;
|
|
205
213
|
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
206
214
|
}
|
|
215
|
+
})
|
|
216
|
+
.finally(() => {
|
|
217
|
+
compromiseCheckInFlight = false;
|
|
207
218
|
});
|
|
208
219
|
}, interval);
|
|
209
220
|
createdHeld.compromiseTimer.unref();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
2
|
export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
|
|
3
3
|
export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
|
|
4
|
+
export declare function validateSidecarLockStaleMs(staleMs: number | undefined): void;
|
|
5
|
+
export declare function validateSidecarLockCompromiseCheckIntervalMs(intervalMs: number | undefined): void;
|
|
4
6
|
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
5
7
|
export declare const maxTransientLockDenials = 8;
|
|
6
8
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAUvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAO5E;AAED,wBAAgB,4CAA4C,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAOjG;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAUjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
|
+
const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
2
3
|
function assertFiniteNonNegative(value, label) {
|
|
3
4
|
if (!Number.isFinite(value) || value < 0) {
|
|
4
5
|
throw new RangeError(`${label} must be a finite non-negative number`);
|
|
@@ -27,6 +28,22 @@ export function validateSidecarLockTimeoutMs(timeoutMs) {
|
|
|
27
28
|
return;
|
|
28
29
|
assertFiniteNonNegative(timeoutMs, "lock timeoutMs");
|
|
29
30
|
}
|
|
31
|
+
export function validateSidecarLockStaleMs(staleMs) {
|
|
32
|
+
// The lower-level sidecar manager historically treats an omitted threshold
|
|
33
|
+
// as disabling age-based reclamation. Its public type requires a number, but
|
|
34
|
+
// retain that fail-closed runtime behavior for JavaScript callers and older
|
|
35
|
+
// consumers.
|
|
36
|
+
if (staleMs === undefined || staleMs === Number.POSITIVE_INFINITY)
|
|
37
|
+
return;
|
|
38
|
+
assertFiniteNonNegative(staleMs, "lock staleMs");
|
|
39
|
+
}
|
|
40
|
+
export function validateSidecarLockCompromiseCheckIntervalMs(intervalMs) {
|
|
41
|
+
if (intervalMs === undefined || intervalMs === 0)
|
|
42
|
+
return;
|
|
43
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 1 || intervalMs > MAX_TIMER_DELAY_MS) {
|
|
44
|
+
throw new RangeError(`lock compromiseCheckIntervalMs must be 0 or a finite number between 1 and ${MAX_TIMER_DELAY_MS}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
export function computeSidecarLockDelayMs(retry, attempt) {
|
|
31
48
|
validateSidecarLockRetryOptions(retry);
|
|
32
49
|
const minTimeout = retry.minTimeout ?? 50;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,KAAK,iBAAiB,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,OAAO,CAKT;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAYvF;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAQ7E;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,iBAAiB,CAAC;CAC5B,CA+BA"}
|
package/dist/staged-directory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { realpathSync } from "./realpath.js";
|
|
4
5
|
export function exactIdentityMatches(expected, actual) {
|
|
5
6
|
return ["dev", "ino"].every((key) => {
|
|
6
7
|
const value = expected[key];
|
|
@@ -14,7 +15,7 @@ export function describeStagedDirectory(fd, pathname) {
|
|
|
14
15
|
}
|
|
15
16
|
const receipt = Object.freeze({
|
|
16
17
|
path: path.resolve(pathname),
|
|
17
|
-
realPath:
|
|
18
|
+
realPath: realpathSync(pathname),
|
|
18
19
|
identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
|
|
19
20
|
});
|
|
20
21
|
assertStagedDirectoryCurrent(receipt);
|
|
@@ -23,7 +24,7 @@ export function describeStagedDirectory(fd, pathname) {
|
|
|
23
24
|
export function assertStagedDirectoryCurrent(receipt) {
|
|
24
25
|
const current = fs.lstatSync(receipt.path, { bigint: true });
|
|
25
26
|
if (!current.isDirectory() || !exactIdentityMatches(receipt.identity, current) ||
|
|
26
|
-
|
|
27
|
+
realpathSync(receipt.path) !== receipt.realPath) {
|
|
27
28
|
throw new FsSafeError("path-mismatch", "staging directory pathname changed");
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -37,7 +38,7 @@ export function openStagedDirectory(directory) {
|
|
|
37
38
|
if (!before.isDirectory()) {
|
|
38
39
|
throw new FsSafeError("not-file", "staging parent must be a real directory");
|
|
39
40
|
}
|
|
40
|
-
if (expected && (!exactIdentityMatches(expected, before) ||
|
|
41
|
+
if (expected && (!exactIdentityMatches(expected, before) || realpathSync(pathname) !== expected.realPath)) {
|
|
41
42
|
throw new FsSafeError("path-mismatch", "stale staging directory receipt");
|
|
42
43
|
}
|
|
43
44
|
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
package/dist/temp-target.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
1
2
|
export type TempFile = {
|
|
2
3
|
dir: string;
|
|
3
4
|
path: string;
|
|
@@ -5,6 +6,12 @@ export type TempFile = {
|
|
|
5
6
|
cleanup: () => Promise<void>;
|
|
6
7
|
[Symbol.asyncDispose](): Promise<void>;
|
|
7
8
|
};
|
|
9
|
+
type TempFileOptions = {
|
|
10
|
+
rootDir?: string;
|
|
11
|
+
prefix: string;
|
|
12
|
+
fileName?: string;
|
|
13
|
+
onCleanupError?: (error: unknown) => void;
|
|
14
|
+
};
|
|
8
15
|
export declare function sanitizeTempFileName(fileName: string): string;
|
|
9
16
|
export declare function buildRandomTempFilePath(params: {
|
|
10
17
|
rootDir?: string;
|
|
@@ -13,16 +20,11 @@ export declare function buildRandomTempFilePath(params: {
|
|
|
13
20
|
now?: number;
|
|
14
21
|
uuid?: string;
|
|
15
22
|
}): string;
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
rootDir?: string;
|
|
24
|
-
prefix: string;
|
|
25
|
-
fileName?: string;
|
|
26
|
-
onCleanupError?: (error: unknown) => void;
|
|
27
|
-
}, fn: (tmpPath: string) => Promise<T>): Promise<T>;
|
|
23
|
+
export declare function createOwnedTempFile(params: TempFileOptions): Promise<{
|
|
24
|
+
target: TempFile;
|
|
25
|
+
identity: Readonly<Pick<fsSync.BigIntStats, "dev" | "ino">>;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function tempFile(params: TempFileOptions): Promise<TempFile>;
|
|
28
|
+
export declare function withTempFile<T>(params: TempFileOptions, fn: (tmpPath: string) => Promise<T>): Promise<T>;
|
|
29
|
+
export {};
|
|
28
30
|
//# sourceMappingURL=temp-target.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAQ7B,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,CAAC;AA8DF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAyCD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC;IAC1E,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;CAC7D,CAAC,CA2BD;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,eAAe,EACvB,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|
package/dist/temp-target.js
CHANGED
|
@@ -106,7 +106,7 @@ async function cleanupTempDir(dir, identity, onCleanupError) {
|
|
|
106
106
|
function resolveTempRoot(rootDir) {
|
|
107
107
|
return path.resolve(rootDir ?? resolveSecureTempRoot({ fallbackPrefix: "fs-safe" }));
|
|
108
108
|
}
|
|
109
|
-
export async function
|
|
109
|
+
export async function createOwnedTempFile(params) {
|
|
110
110
|
const rootDir = resolveTempRoot(params.rootDir);
|
|
111
111
|
const prefix = `${sanitizePrefix(params.prefix)}-`;
|
|
112
112
|
const dir = await fs.mkdtemp(path.join(rootDir, prefix));
|
|
@@ -124,13 +124,19 @@ export async function tempFile(params) {
|
|
|
124
124
|
}
|
|
125
125
|
};
|
|
126
126
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
target: {
|
|
128
|
+
dir,
|
|
129
|
+
path: file(),
|
|
130
|
+
file,
|
|
131
|
+
cleanup,
|
|
132
|
+
[Symbol.asyncDispose]: cleanup,
|
|
133
|
+
},
|
|
134
|
+
identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
|
|
132
135
|
};
|
|
133
136
|
}
|
|
137
|
+
export async function tempFile(params) {
|
|
138
|
+
return (await createOwnedTempFile(params)).target;
|
|
139
|
+
}
|
|
134
140
|
export async function withTempFile(params, fn) {
|
|
135
141
|
const target = await tempFile(params);
|
|
136
142
|
try {
|
package/dist/trash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAkLF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
|
package/dist/trash.js
CHANGED
|
@@ -3,6 +3,8 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
5
|
import { guardedRenameSync, guardedRmSync } from "./guarded-mutation.js";
|
|
6
|
+
import { realpathSync } from "./realpath.js";
|
|
7
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
6
8
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
7
9
|
const TRASH_DESTINATION_COLLISION_CODES = new Set(["EEXIST", "ENOTEMPTY", "ERR_FS_CP_EEXIST"]);
|
|
8
10
|
const TRASH_DESTINATION_RETRY_LIMIT = 4;
|
|
@@ -26,7 +28,7 @@ function resolveAllowedTrashRoots(allowedRoots) {
|
|
|
26
28
|
try {
|
|
27
29
|
// Keep both spellings: broken symlink targets cannot be realpathed and
|
|
28
30
|
// may only compare equal to the caller's lexical allowed root.
|
|
29
|
-
return [path.resolve(
|
|
31
|
+
return [path.resolve(realpathSync.native(root)), lexicalRoot];
|
|
30
32
|
}
|
|
31
33
|
catch {
|
|
32
34
|
return [lexicalRoot];
|
|
@@ -36,7 +38,7 @@ function resolveAllowedTrashRoots(allowedRoots) {
|
|
|
36
38
|
}
|
|
37
39
|
function resolveTrashTargetPath(targetPath) {
|
|
38
40
|
try {
|
|
39
|
-
return { path: path.resolve(
|
|
41
|
+
return { path: path.resolve(realpathSync.native(targetPath)), resolved: true };
|
|
40
42
|
}
|
|
41
43
|
catch {
|
|
42
44
|
// Broken symlinks are valid trash targets. Fall back to the lexical path,
|
|
@@ -75,13 +77,13 @@ function assertTrashTargetGuard(guard) {
|
|
|
75
77
|
function resolveTrashDir() {
|
|
76
78
|
const homeDir = os.homedir();
|
|
77
79
|
const trashDir = path.join(homeDir, ".Trash");
|
|
78
|
-
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
80
|
+
fs.mkdirSync(recursiveMkdirPath(trashDir), { recursive: true, mode: 0o700 });
|
|
79
81
|
const trashDirStat = fs.lstatSync(trashDir);
|
|
80
82
|
if (!trashDirStat.isDirectory() || trashDirStat.isSymbolicLink()) {
|
|
81
83
|
throw new Error(`Refusing to use non-directory/symlink trash directory: ${trashDir}`);
|
|
82
84
|
}
|
|
83
|
-
const realHome = path.resolve(
|
|
84
|
-
const resolvedTrashDir = path.resolve(
|
|
85
|
+
const realHome = path.resolve(realpathSync.native(homeDir));
|
|
86
|
+
const resolvedTrashDir = path.resolve(realpathSync.native(trashDir));
|
|
85
87
|
if (resolvedTrashDir === realHome || !isSameOrChildPath(resolvedTrashDir, realHome)) {
|
|
86
88
|
throw new Error(`Trash directory escaped home directory: ${trashDir}`);
|
|
87
89
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unicode-path.d.ts","sourceRoot":"","sources":["../src/unicode-path.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,UAAQ,GAAG,MAAM,CAUhF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const NON_ASCII = /[^\x00-\x7f]/;
|
|
2
|
+
export function lowerCaseNfc(value) {
|
|
3
|
+
return NON_ASCII.test(value) ? value.normalize("NFC").toLowerCase().normalize("NFC") : value.toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
export function maxNormalizedUtf8Bytes(value, includeRaw = false) {
|
|
6
|
+
const nfc = value.normalize("NFC");
|
|
7
|
+
const bytes = Buffer.byteLength(nfc, "utf8");
|
|
8
|
+
const maximum = includeRaw && nfc !== value ? Math.max(bytes, Buffer.byteLength(value, "utf8")) : bytes;
|
|
9
|
+
// ASCII NFC output also has identical NFD and one UTF-8 byte per code unit.
|
|
10
|
+
if (bytes === nfc.length)
|
|
11
|
+
return maximum;
|
|
12
|
+
return Math.max(maximum, Buffer.byteLength(value.normalize("NFD"), "utf8"));
|
|
13
|
+
}
|
package/dist/walk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAK7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AAgFF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CAyDjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CAyD1C"}
|
package/dist/walk.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { realpathSync } from "./realpath.js";
|
|
4
5
|
function validateWalkBudget(name, value) {
|
|
5
6
|
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
6
7
|
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
@@ -83,7 +84,7 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
83
84
|
return;
|
|
84
85
|
let realDir;
|
|
85
86
|
try {
|
|
86
|
-
realDir =
|
|
87
|
+
realDir = realpathSync(dir);
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
90
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -142,7 +143,7 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
142
143
|
return;
|
|
143
144
|
let realDir;
|
|
144
145
|
try {
|
|
145
|
-
realDir =
|
|
146
|
+
realDir = realpathSync.native(dir);
|
|
146
147
|
}
|
|
147
148
|
catch (error) {
|
|
148
149
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
export declare function writeAllToFile(target: FileHandle | number, data: string | Uint8Array, options?: {
|
|
3
3
|
encoding?: BufferEncoding;
|
|
4
|
+
position?: number;
|
|
4
5
|
assertBeforeMutation?: () => void;
|
|
5
6
|
}): Promise<void>;
|
|
6
7
|
//# sourceMappingURL=write-file-handle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-file-handle.d.ts","sourceRoot":"","sources":["../src/write-file-handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,
|
|
1
|
+
{"version":3,"file":"write-file-handle.d.ts","sourceRoot":"","sources":["../src/write-file-handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -6,17 +6,18 @@ export async function writeAllToFile(target, data, options = {}) {
|
|
|
6
6
|
let offset = 0;
|
|
7
7
|
while (offset < buffer.byteLength) {
|
|
8
8
|
const length = Math.min(WRITE_CHUNK_BYTES, buffer.byteLength - offset);
|
|
9
|
+
const position = options.position === undefined ? null : options.position + offset;
|
|
9
10
|
options.assertBeforeMutation?.();
|
|
10
11
|
const written = typeof target === "number"
|
|
11
12
|
? await new Promise((resolve, reject) => {
|
|
12
|
-
fs.write(target, buffer, offset, length,
|
|
13
|
+
fs.write(target, buffer, offset, length, position, (error, bytesWritten) => {
|
|
13
14
|
if (error)
|
|
14
15
|
reject(error);
|
|
15
16
|
else
|
|
16
17
|
resolve(bytesWritten);
|
|
17
18
|
});
|
|
18
19
|
})
|
|
19
|
-
: (await target.write(buffer, offset, length,
|
|
20
|
+
: (await target.write(buffer, offset, length, position)).bytesWritten;
|
|
20
21
|
if (written <= 0) {
|
|
21
22
|
throw new FsSafeError("helper-failed", "file write made no progress");
|
|
22
23
|
}
|
package/docs/advanced.md
CHANGED
|
@@ -32,6 +32,7 @@ The exports group into a handful of themes. Each documented helper has its own p
|
|
|
32
32
|
| `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
|
|
33
33
|
| `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
|
|
34
34
|
| `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
|
|
35
|
+
| `probePathCaseInsensitiveSync`, `ProbePathCaseOptions` | [path-case.md](path-case.md) | Observe local ASCII-case behavior with explicit read-only mode and owned temporary-probe cleanup. |
|
|
35
36
|
|
|
36
37
|
`ensureDirectoryWithinRoot({ rootDir, requestedPath, scopeLabel, defaultDirName?, mode? })`
|
|
37
38
|
returns `{ ok: true, path }` or `{ ok: false, error: string, diagnostic?: FsSafeError }`.
|
|
@@ -66,9 +67,12 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
66
67
|
|---|---|---|
|
|
67
68
|
| `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
|
|
68
69
|
| `readFileWindowFully`, `readFileWindowFullySync`, `ReadFileWindowOptions` | [positional-read.md](positional-read.md) | Fill a caller-owned buffer at an explicit file position, completing short reads and returning the EOF count without moving or closing the descriptor. |
|
|
70
|
+
| `copyFileHandle`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular-file handles from position zero with byte limits, settled cancellation, and a synchronous source observer; preserves cursors and leaves publication and cleanup to the caller. |
|
|
71
|
+
| `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
|
|
69
72
|
| `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
|
|
70
73
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|
|
71
74
|
| `sameFileIdentity`, `FileIdentityStat` | – | Compare two stats for same-inode equality. |
|
|
75
|
+
| `readDirectoryIdentity`, `assertDirectoryIdentitySync`, `DirectoryIdentity` | [directory-identity.md](directory-identity.md) | Observe exact bigint directory identity and synchronously check a caller-selected path, optionally retaining its canonical path. |
|
|
72
76
|
| `pathExists`, `pathExistsSync` | – | Boolean existence check that does not throw on `ENOENT`. |
|
|
73
77
|
| `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
|
|
74
78
|
| `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
|