@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
export type CopyFileHandleOptions = {
|
|
3
|
+
maxBytes?: number;
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
onChunk?: (chunk: Uint8Array) => void;
|
|
6
|
+
assertBeforeMutation?: () => void;
|
|
7
|
+
};
|
|
8
|
+
/** Copies caller-owned regular files from offset zero without moving either cursor. */
|
|
9
|
+
export declare function copyFileHandle(source: FileHandle, target: FileHandle, options?: CopyFileHandleOptions): Promise<number>;
|
|
10
|
+
export declare function transferFileHandle(source: FileHandle, target: FileHandle | number, options: CopyFileHandleOptions & {
|
|
11
|
+
sizeHint: number;
|
|
12
|
+
targetPosition?: number;
|
|
13
|
+
}): Promise<number>;
|
|
14
|
+
//# sourceMappingURL=file-handle-transfer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-handle-transfer.d.ts","sourceRoot":"","sources":["../src/file-handle-transfer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAWnD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;CACnC,CAAC;AAEF,uFAAuF;AACvF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,OAAO,EAAE,qBAAqB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,MAAM,CAAC,CAyBjB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { assertSynchronousCallbackResult, composeMutationAssertions, rethrowMutationAuthorityError, } from "./mutation-authority.js";
|
|
4
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
5
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
6
|
+
/** Copies caller-owned regular files from offset zero without moving either cursor. */
|
|
7
|
+
export async function copyFileHandle(source, target, options = {}) {
|
|
8
|
+
options.signal?.throwIfAborted();
|
|
9
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
10
|
+
const sourceStat = await inspectFileIdentity(() => {
|
|
11
|
+
options.signal?.throwIfAborted();
|
|
12
|
+
return source.stat({ bigint: true });
|
|
13
|
+
});
|
|
14
|
+
options.signal?.throwIfAborted();
|
|
15
|
+
if (!sourceStat.isFile())
|
|
16
|
+
throw new FsSafeError("not-file", "copy source handle must be a regular file");
|
|
17
|
+
if (maxBytes !== undefined && Number.isFinite(maxBytes) && sourceStat.size > BigInt(maxBytes)) {
|
|
18
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes`);
|
|
19
|
+
}
|
|
20
|
+
const targetStat = await inspectFileIdentity(() => {
|
|
21
|
+
options.signal?.throwIfAborted();
|
|
22
|
+
return target.stat({ bigint: true });
|
|
23
|
+
});
|
|
24
|
+
options.signal?.throwIfAborted();
|
|
25
|
+
if (!targetStat.isFile())
|
|
26
|
+
throw new FsSafeError("not-file", "copy target handle must be a regular file");
|
|
27
|
+
if (sourceStat.dev === targetStat.dev && sourceStat.ino === targetStat.ino) {
|
|
28
|
+
throw new FsSafeError("path-alias", "copy source and target handles refer to the same file");
|
|
29
|
+
}
|
|
30
|
+
return await transferFileHandle(source, target, {
|
|
31
|
+
...options,
|
|
32
|
+
maxBytes,
|
|
33
|
+
sizeHint: Number(sourceStat.size),
|
|
34
|
+
targetPosition: 0,
|
|
35
|
+
assertBeforeMutation: composeMutationAssertions(undefined, options.assertBeforeMutation),
|
|
36
|
+
}).catch(rethrowMutationAuthorityError);
|
|
37
|
+
}
|
|
38
|
+
// Root.copyIn supplies already-admitted identities and retains its owned target cursor.
|
|
39
|
+
export async function transferFileHandle(source, target, options) {
|
|
40
|
+
const maxBytes = options.maxBytes ?? Infinity;
|
|
41
|
+
const buffer = Buffer.allocUnsafe(Math.min(512 * 1024, Math.max(64 * 1024, options.sizeHint), maxBytes + 1));
|
|
42
|
+
let position = 0;
|
|
43
|
+
while (true) {
|
|
44
|
+
options.signal?.throwIfAborted();
|
|
45
|
+
const { bytesRead } = await source.read(buffer, 0, Math.min(buffer.length, maxBytes - position + 1), position);
|
|
46
|
+
options.signal?.throwIfAborted();
|
|
47
|
+
if (bytesRead > maxBytes - position) {
|
|
48
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes`);
|
|
49
|
+
}
|
|
50
|
+
if (bytesRead === 0)
|
|
51
|
+
return position;
|
|
52
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
53
|
+
if (options.onChunk)
|
|
54
|
+
assertSynchronousCallbackResult(options.onChunk(chunk), "onChunk");
|
|
55
|
+
await writeAllToFile(target, chunk, {
|
|
56
|
+
position: options.targetPosition === undefined ? undefined : options.targetPosition + position,
|
|
57
|
+
assertBeforeMutation: () => {
|
|
58
|
+
options.signal?.throwIfAborted();
|
|
59
|
+
options.assertBeforeMutation?.();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
position += bytesRead;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/file-hash.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
import { type NativeBinding } from "./native.js";
|
|
3
3
|
export type Sha256FileInput = string | FileHandle;
|
|
4
|
+
export type Sha256FileSyncInput = string | number;
|
|
4
5
|
export type Sha256FileOptions = {
|
|
5
6
|
maxBytes?: number;
|
|
6
7
|
signal?: AbortSignal;
|
|
@@ -11,4 +12,6 @@ export type Sha256FileResult = {
|
|
|
11
12
|
};
|
|
12
13
|
export declare function hashFileHandle(handle: FileHandle, native?: NativeBinding | undefined, { maxBytes, signal }?: Sha256FileOptions): Promise<Sha256FileResult>;
|
|
13
14
|
export declare function sha256File(input: Sha256FileInput, options?: Sha256FileOptions): Promise<Sha256FileResult>;
|
|
15
|
+
/** Hash synchronously from offset zero, retaining ownership and position of a borrowed fd. */
|
|
16
|
+
export declare function sha256FileSync(input: Sha256FileSyncInput, options?: Sha256FileOptions): Sha256FileResult;
|
|
14
17
|
//# sourceMappingURL=file-hash.d.ts.map
|
package/dist/file-hash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAInE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,EACtD,EAAE,QAAmB,EAAE,MAAM,EAAE,GAAE,iBAAsB,GACtD,OAAO,CAAC,gBAAgB,CAAC,CAoD3B;
|
|
1
|
+
{"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAInE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAClD,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,EACtD,EAAE,QAAmB,EAAE,MAAM,EAAE,GAAE,iBAAsB,GACtD,OAAO,CAAC,gBAAgB,CAAC,CAoD3B;AAyDD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AAoDD,8FAA8F;AAC9F,wBAAgB,cAAc,CAC5B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CAMlB"}
|
package/dist/file-hash.js
CHANGED
|
@@ -5,7 +5,7 @@ import { normalizeMaxBytes } from "./byte-budget.js";
|
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { getNativeBinding } from "./native.js";
|
|
7
7
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
8
|
-
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
8
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
9
|
export async function hashFileHandle(handle, native = getNativeBinding(), { maxBytes = Infinity, signal } = {}) {
|
|
10
10
|
signal?.throwIfAborted();
|
|
11
11
|
const stat = fsSync.fstatSync(handle.fd);
|
|
@@ -55,46 +55,48 @@ export async function hashFileHandle(handle, native = getNativeBinding(), { maxB
|
|
|
55
55
|
position += bytesRead;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
function hashPathIdentity(filePath, afterOpen = false) {
|
|
59
|
+
const stat = fsSync.lstatSync(filePath, { bigint: true });
|
|
60
|
+
if (afterOpen && (stat.isSymbolicLink() || !stat.isFile())) {
|
|
61
|
+
throw new FsSafeError("path-mismatch", "SHA-256 path changed while opening");
|
|
62
|
+
}
|
|
63
|
+
if (stat.isSymbolicLink()) {
|
|
64
|
+
throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link");
|
|
65
|
+
}
|
|
66
|
+
if (!stat.isFile()) {
|
|
67
|
+
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
68
|
+
}
|
|
69
|
+
return stat;
|
|
70
|
+
}
|
|
71
|
+
function hashDescriptorIdentity(fd) {
|
|
72
|
+
const stat = fsSync.fstatSync(fd, { bigint: true });
|
|
73
|
+
if (!stat.isFile()) {
|
|
74
|
+
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
75
|
+
}
|
|
76
|
+
return stat;
|
|
77
|
+
}
|
|
78
|
+
function rethrowHashOpenError(error) {
|
|
79
|
+
if (error?.code === "ELOOP") {
|
|
80
|
+
throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link", {
|
|
81
|
+
cause: error,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
58
86
|
async function hashPath(filePath, options) {
|
|
59
|
-
const before = await inspectFileIdentity(
|
|
60
|
-
const stat = fsSync.lstatSync(filePath, { bigint: true });
|
|
61
|
-
if (stat.isSymbolicLink()) {
|
|
62
|
-
throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link");
|
|
63
|
-
}
|
|
64
|
-
if (!stat.isFile()) {
|
|
65
|
-
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
66
|
-
}
|
|
67
|
-
return stat;
|
|
68
|
-
});
|
|
87
|
+
const before = await inspectFileIdentity(() => hashPathIdentity(filePath));
|
|
69
88
|
let handle;
|
|
70
89
|
try {
|
|
71
90
|
options.signal?.throwIfAborted();
|
|
72
91
|
handle = await fs.open(filePath, resolveReadOpenFlags());
|
|
73
92
|
}
|
|
74
93
|
catch (error) {
|
|
75
|
-
|
|
76
|
-
throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link", {
|
|
77
|
-
cause: error,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
throw error;
|
|
94
|
+
rethrowHashOpenError(error);
|
|
81
95
|
}
|
|
82
96
|
try {
|
|
83
97
|
options.signal?.throwIfAborted();
|
|
84
|
-
const opened = await inspectFileIdentity(
|
|
85
|
-
|
|
86
|
-
if (!stat.isFile()) {
|
|
87
|
-
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
88
|
-
}
|
|
89
|
-
return stat;
|
|
90
|
-
}, before);
|
|
91
|
-
await inspectFileIdentity(async () => {
|
|
92
|
-
const stat = fsSync.lstatSync(filePath, { bigint: true });
|
|
93
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
94
|
-
throw new FsSafeError("path-mismatch", "SHA-256 path changed while opening");
|
|
95
|
-
}
|
|
96
|
-
return stat;
|
|
97
|
-
}, opened);
|
|
98
|
+
const opened = await inspectFileIdentity(() => hashDescriptorIdentity(handle.fd), before);
|
|
99
|
+
await inspectFileIdentity(() => hashPathIdentity(filePath, true), opened);
|
|
98
100
|
return await hashFileHandle(handle, getNativeBinding(), options);
|
|
99
101
|
}
|
|
100
102
|
finally {
|
|
@@ -108,3 +110,61 @@ export async function sha256File(input, options = {}) {
|
|
|
108
110
|
? await hashPath(input, normalized)
|
|
109
111
|
: await hashFileHandle(input, getNativeBinding(), normalized);
|
|
110
112
|
}
|
|
113
|
+
function hashDescriptorSync(fd, { maxBytes = Infinity, signal }) {
|
|
114
|
+
signal?.throwIfAborted();
|
|
115
|
+
const stat = fsSync.fstatSync(fd);
|
|
116
|
+
if (!stat.isFile()) {
|
|
117
|
+
throw new FsSafeError("not-file", "SHA-256 input is not a regular file");
|
|
118
|
+
}
|
|
119
|
+
if (stat.size > maxBytes) {
|
|
120
|
+
throw new FsSafeError("too-large", `SHA-256 input exceeds ${maxBytes} bytes`);
|
|
121
|
+
}
|
|
122
|
+
const hash = createHash("sha256");
|
|
123
|
+
const buffer = Buffer.allocUnsafe(Math.min(256 * 1024, Math.max(64 * 1024, stat.size)));
|
|
124
|
+
let position = 0;
|
|
125
|
+
while (true) {
|
|
126
|
+
signal?.throwIfAborted();
|
|
127
|
+
const length = Math.min(buffer.length, maxBytes - position + 1);
|
|
128
|
+
const bytesRead = fsSync.readSync(fd, buffer, 0, length, position);
|
|
129
|
+
signal?.throwIfAborted();
|
|
130
|
+
if (bytesRead > maxBytes - position) {
|
|
131
|
+
throw new FsSafeError("too-large", `SHA-256 input exceeds ${maxBytes} bytes`);
|
|
132
|
+
}
|
|
133
|
+
if (bytesRead === 0) {
|
|
134
|
+
return { bytes: position, digest: hash.digest("hex") };
|
|
135
|
+
}
|
|
136
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
137
|
+
position += bytesRead;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function hashPathSync(filePath, options) {
|
|
141
|
+
const before = inspectFileIdentitySync(() => hashPathIdentity(filePath));
|
|
142
|
+
let fd;
|
|
143
|
+
try {
|
|
144
|
+
options.signal?.throwIfAborted();
|
|
145
|
+
fd = fsSync.openSync(filePath, resolveReadOpenFlags());
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
rethrowHashOpenError(error);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
options.signal?.throwIfAborted();
|
|
152
|
+
const opened = inspectFileIdentitySync(() => hashDescriptorIdentity(fd), before);
|
|
153
|
+
inspectFileIdentitySync(() => hashPathIdentity(filePath, true), opened);
|
|
154
|
+
return hashDescriptorSync(fd, options);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
try {
|
|
158
|
+
fsSync.closeSync(fd);
|
|
159
|
+
}
|
|
160
|
+
catch { /* Preserve the hashing outcome. */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Hash synchronously from offset zero, retaining ownership and position of a borrowed fd. */
|
|
164
|
+
export function sha256FileSync(input, options = {}) {
|
|
165
|
+
options.signal?.throwIfAborted();
|
|
166
|
+
const normalized = { maxBytes: normalizeMaxBytes(options.maxBytes), signal: options.signal };
|
|
167
|
+
return typeof input === "string"
|
|
168
|
+
? hashPathSync(input, normalized)
|
|
169
|
+
: hashDescriptorSync(input, normalized);
|
|
170
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EASL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EASL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AAWnC,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,yBAAyB,CAAC;AAMjC,MAAM,MAAM,0BAA0B,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAChC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,QAAQ,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,KAAK,CAAC;KAC1B,KAAK,OAAO,CAAC;IACd,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,CAAC;IACxE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IAC3D,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,IAAI,OAAO,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AAoJF,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,GAC5C,kBAAkB,CA0MpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,EAC7C,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAOH"}
|
package/dist/file-lock-sync.js
CHANGED
|
@@ -2,9 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { readSidecarLockSnapshotSync, readSidecarLockRawSnapshotSync, parseSidecarLockSnapshot, relativeSidecarLockPath, removeSidecarLockIfUnchangedSync, serializeSidecarLockPayload, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
5
|
-
import { computeSidecarLockDelayMs, isTransientLockFileDenial, maxTransientLockDenials, sidecarLockPayloadCreatedAtMs, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
5
|
+
import { computeSidecarLockDelayMs, isTransientLockFileDenial, maxTransientLockDenials, sidecarLockPayloadCreatedAtMs, validateSidecarLockStaleMs, validateSidecarLockCompromiseCheckIntervalMs, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
6
6
|
import { getFsSafeLockConfig } from "./lock-config.js";
|
|
7
7
|
import { sleepSync } from "./timing.js";
|
|
8
|
+
import { realpathSync } from "./realpath.js";
|
|
9
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
8
10
|
const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
9
11
|
const SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
|
|
10
12
|
const SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
|
|
@@ -91,11 +93,11 @@ function createSyncHeldLockHandle(held) {
|
|
|
91
93
|
}
|
|
92
94
|
function canonicalLockParentSync(parent) {
|
|
93
95
|
// Match Root's async realpath, including Windows short-name expansion.
|
|
94
|
-
return process.platform === "win32" ?
|
|
96
|
+
return process.platform === "win32" ? realpathSync.native(parent) : realpathSync(parent);
|
|
95
97
|
}
|
|
96
98
|
function normalizeTargetPath(targetPath) {
|
|
97
99
|
const resolved = path.resolve(targetPath);
|
|
98
|
-
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
100
|
+
fs.mkdirSync(recursiveMkdirPath(path.dirname(resolved)), { recursive: true });
|
|
99
101
|
try {
|
|
100
102
|
return path.join(canonicalLockParentSync(path.dirname(resolved)), path.basename(resolved));
|
|
101
103
|
}
|
|
@@ -140,6 +142,9 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
140
142
|
const timeoutMs = options.timeoutMs ?? defaults.timeoutMs;
|
|
141
143
|
validateSidecarLockRetryOptions(retry);
|
|
142
144
|
validateSidecarLockTimeoutMs(timeoutMs);
|
|
145
|
+
const staleMs = options.staleMs ?? defaults.staleMs ?? 30_000;
|
|
146
|
+
validateSidecarLockStaleMs(staleMs);
|
|
147
|
+
validateSidecarLockCompromiseCheckIntervalMs(options.compromiseCheckIntervalMs);
|
|
143
148
|
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
144
149
|
const lockPath = boundedLockPath(options.lockPath ?? `${normalizedTargetPath}.lock`, options.lockRoot);
|
|
145
150
|
const heldLocks = getSyncHeldLocks();
|
|
@@ -153,7 +158,6 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
153
158
|
}
|
|
154
159
|
// Process defaults fill the same fields here as withLockDefaults() fills for
|
|
155
160
|
// the asynchronous manager, so both acquirers honor configureFsSafeLocks().
|
|
156
|
-
const staleMs = options.staleMs ?? defaults.staleMs ?? 30_000;
|
|
157
161
|
const staleRecovery = options.staleRecovery ?? defaults.staleRecovery;
|
|
158
162
|
const startedAt = Date.now();
|
|
159
163
|
let attempt = 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAK1D,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC,CAAC,CAAC,GACnD,OAAO,CAAC,IAAI,CAAC,CASf;AAsCD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAwC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAOlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC;CAC1C,GAAG,eAAe,CAyDlB"}
|
|
@@ -13,6 +13,8 @@ import { ensureTrailingSep } from "./root-context.js";
|
|
|
13
13
|
import { RootHandle } from "./root-impl.js";
|
|
14
14
|
import { prepareSecretFileWrite } from "./secret-file.js";
|
|
15
15
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
16
|
+
import { realpathSync } from "./realpath.js";
|
|
17
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
16
18
|
function parentRelativePath(relativePath) {
|
|
17
19
|
const parent = path.posix.dirname(relativePath);
|
|
18
20
|
return parent === "." ? "" : parent;
|
|
@@ -27,7 +29,7 @@ export async function ensureParentInRoot(scopedRoot, relativePath, mode) {
|
|
|
27
29
|
}
|
|
28
30
|
export async function openWritableStoreRoot(params) {
|
|
29
31
|
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
30
|
-
await fs.mkdir(params.rootDir, { recursive: true, mode: params.dirMode });
|
|
32
|
+
await fs.mkdir(recursiveMkdirPath(params.rootDir), { recursive: true, mode: params.dirMode });
|
|
31
33
|
await fs.chmod(params.rootDir, params.dirMode).catch(() => undefined);
|
|
32
34
|
return await root(params.rootDir, { hardlinks: "reject", maxBytes });
|
|
33
35
|
}
|
|
@@ -149,12 +151,12 @@ export function ensureStoreDirectorySync(params) {
|
|
|
149
151
|
if (isPathRelativeEscape(relative)) {
|
|
150
152
|
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
151
153
|
}
|
|
152
|
-
syncFs.mkdirSync(rootDir, { recursive: true, mode: params.mode });
|
|
154
|
+
syncFs.mkdirSync(recursiveMkdirPath(rootDir), { recursive: true, mode: params.mode });
|
|
153
155
|
const rootStat = syncFs.lstatSync(rootDir);
|
|
154
156
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
155
157
|
throw new FsSafeError("not-file", `${params.messagePrefix} root must be a directory: ${rootDir}`);
|
|
156
158
|
}
|
|
157
|
-
const rootReal =
|
|
159
|
+
const rootReal = realpathSync(rootDir);
|
|
158
160
|
chmodDirectorySyncBestEffort(rootDir, params.mode);
|
|
159
161
|
let current = rootDir;
|
|
160
162
|
for (const segment of path.relative(rootDir, dir).split(path.sep).filter(Boolean)) {
|
|
@@ -172,8 +174,8 @@ export function ensureStoreDirectorySync(params) {
|
|
|
172
174
|
syncFs.mkdirSync(current, { mode: params.mode });
|
|
173
175
|
}
|
|
174
176
|
const currentRootStat = syncFs.lstatSync(rootDir);
|
|
175
|
-
const currentRootReal =
|
|
176
|
-
const currentReal =
|
|
177
|
+
const currentRootReal = realpathSync(rootDir);
|
|
178
|
+
const currentReal = realpathSync(current);
|
|
177
179
|
if (currentRootStat.isSymbolicLink() ||
|
|
178
180
|
!currentRootStat.isDirectory() ||
|
|
179
181
|
!sameFileIdentity(rootStat, currentRootStat) ||
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store-path.d.ts","sourceRoot":"","sources":["../src/file-store-path.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAe/D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAS9E"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { isPathInside, splitSafeRelativePath } from "./path.js";
|
|
4
|
+
export function assertRelativePath(relativePath) {
|
|
5
|
+
const raw = relativePath.trim();
|
|
6
|
+
if (!raw || raw !== relativePath) {
|
|
7
|
+
throw new FsSafeError("invalid-path", "store key must be non-empty and unpadded");
|
|
8
|
+
}
|
|
9
|
+
const segments = splitSafeRelativePath(raw);
|
|
10
|
+
if (segments.length === 0 ||
|
|
11
|
+
segments.join("/") !== raw ||
|
|
12
|
+
raw.normalize("NFC") !== raw ||
|
|
13
|
+
segments.some((segment) => /[ .]$/u.test(segment))) {
|
|
14
|
+
throw new FsSafeError("invalid-path", "store key must use one canonical relative spelling");
|
|
15
|
+
}
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
export function resolveStorePath(rootDir, relativePath) {
|
|
19
|
+
const key = assertRelativePath(relativePath);
|
|
20
|
+
const root = path.resolve(rootDir);
|
|
21
|
+
// The immutable key already passed segment validation; keep the containment check.
|
|
22
|
+
const target = path.resolve(root, key);
|
|
23
|
+
if (!isPathInside(root, target)) {
|
|
24
|
+
throw new FsSafeError("outside-workspace", "relative path escapes root");
|
|
25
|
+
}
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-prune.d.ts","sourceRoot":"","sources":["../src/file-store-prune.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-store-prune.d.ts","sourceRoot":"","sources":["../src/file-store-prune.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFhB"}
|
package/dist/file-store-prune.js
CHANGED
|
@@ -3,6 +3,8 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { isPathInside } from "./path.js";
|
|
6
|
+
import { realpathSync } from "./realpath.js";
|
|
7
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
6
8
|
import { root } from "./root.js";
|
|
7
9
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
8
10
|
export async function pruneExpiredStoreEntries(params) {
|
|
@@ -10,8 +12,8 @@ export async function pruneExpiredStoreEntries(params) {
|
|
|
10
12
|
const recursive = params.options.recursive ?? false;
|
|
11
13
|
const maxDepth = params.options.maxDepth;
|
|
12
14
|
const pruneEmptyDirs = (recursive || maxDepth !== undefined) && (params.options.pruneEmptyDirs ?? false);
|
|
13
|
-
await fs.mkdir(params.rootDir, { recursive: true, mode: params.dirMode });
|
|
14
|
-
const rootReal =
|
|
15
|
+
await fs.mkdir(recursiveMkdirPath(params.rootDir), { recursive: true, mode: params.dirMode });
|
|
16
|
+
const rootReal = realpathSync.native(params.rootDir);
|
|
15
17
|
const scopedRoot = await root(rootReal);
|
|
16
18
|
const rootGuard = {
|
|
17
19
|
dir: rootReal,
|
|
@@ -24,7 +26,7 @@ export async function pruneExpiredStoreEntries(params) {
|
|
|
24
26
|
!stat.isDirectory() ||
|
|
25
27
|
stat.dev !== rootGuard.stat.dev ||
|
|
26
28
|
stat.ino !== rootGuard.stat.ino ||
|
|
27
|
-
|
|
29
|
+
realpathSync.native(rootGuard.dir) !== rootGuard.realPath) {
|
|
28
30
|
throw new FsSafeError("path-mismatch", "store root changed during prune");
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -33,7 +35,7 @@ export async function pruneExpiredStoreEntries(params) {
|
|
|
33
35
|
if (!before || before.isSymbolicLink() || !before.isDirectory()) {
|
|
34
36
|
return null;
|
|
35
37
|
}
|
|
36
|
-
const real = observeOrNull(() =>
|
|
38
|
+
const real = observeOrNull(() => realpathSync.native(dir));
|
|
37
39
|
if (!real || !isPathInside(rootReal, real)) {
|
|
38
40
|
return null;
|
|
39
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAwDA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAwFT"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
2
|
+
import fs, {} from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { syncDirectorySync } from "./directory-durability.js";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
7
6
|
import { assertSyncDirectoryGuard, ensureParentSync, ensureStoreDirectorySync, } from "./file-store-boundary.js";
|
|
8
7
|
import { isPathInside } from "./path.js";
|
|
8
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
+
import { writeTempFileSync } from "./replace-file-descriptor.js";
|
|
10
|
+
import { SyncAtomicTempOwner } from "./replace-file-temp-owner.js";
|
|
11
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
12
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
10
13
|
function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
11
14
|
return ensureStoreDirectorySync({
|
|
@@ -15,6 +18,35 @@ function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
|
15
18
|
messagePrefix: "private store",
|
|
16
19
|
});
|
|
17
20
|
}
|
|
21
|
+
function verifyStoreFile(fd, expected, filePath) {
|
|
22
|
+
const assertFile = (stat, allowUnknown = false) => {
|
|
23
|
+
const unknown = allowUnknown && process.platform === "win32" && (stat.dev === 0n || stat.ino === 0n);
|
|
24
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n ||
|
|
25
|
+
(stat.dev !== expected.dev && !(unknown && stat.dev === 0n)) ||
|
|
26
|
+
(stat.ino !== expected.ino && !(unknown && stat.ino === 0n))) {
|
|
27
|
+
throw new FsSafeError("path-mismatch", "store file changed during write");
|
|
28
|
+
}
|
|
29
|
+
return unknown;
|
|
30
|
+
};
|
|
31
|
+
const assertDescriptor = (descriptor) => {
|
|
32
|
+
assertFile(inspectFileIdentitySync(() => fs.fstatSync(descriptor, { bigint: true }), expected));
|
|
33
|
+
};
|
|
34
|
+
assertDescriptor(fd);
|
|
35
|
+
if (assertFile(fs.lstatSync(filePath, { bigint: true }), true)) {
|
|
36
|
+
// A Windows pathname can have opaque metadata even while the retained writer is known.
|
|
37
|
+
// Reopen for identity only; equal content cannot establish that publication kept its file.
|
|
38
|
+
const reopened = fs.openSync(filePath, resolveReadOpenFlags());
|
|
39
|
+
try {
|
|
40
|
+
assertDescriptor(reopened);
|
|
41
|
+
assertFile(fs.lstatSync(filePath, { bigint: true }), true);
|
|
42
|
+
assertDescriptor(reopened);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
fs.closeSync(reopened);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
assertDescriptor(fd);
|
|
49
|
+
}
|
|
18
50
|
export function writeFileSyncAtomic(params) {
|
|
19
51
|
const filePath = path.resolve(params.filePath);
|
|
20
52
|
if (!isPathInside(params.rootDir, filePath)) {
|
|
@@ -43,58 +75,41 @@ export function writeFileSyncAtomic(params) {
|
|
|
43
75
|
});
|
|
44
76
|
}
|
|
45
77
|
const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
46
|
-
|
|
78
|
+
const owner = new SyncAtomicTempOwner(tempPath);
|
|
79
|
+
let originalError;
|
|
47
80
|
try {
|
|
48
81
|
getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
|
|
49
82
|
if (parentGuard) {
|
|
50
83
|
assertSyncDirectoryGuard(parentGuard);
|
|
51
84
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
owner.start();
|
|
86
|
+
const temp = writeTempFileSync({
|
|
87
|
+
fsModule: fs, tempPath, content: params.content, mode: params.mode, sync: false,
|
|
88
|
+
onIdentity: owner.onIdentity,
|
|
89
|
+
fchmodSync: (descriptor, mode) => {
|
|
57
90
|
try {
|
|
58
|
-
fs.fchmodSync(descriptor,
|
|
91
|
+
fs.fchmodSync(descriptor, mode);
|
|
59
92
|
}
|
|
60
93
|
catch {
|
|
61
94
|
// Best-effort on platforms that do not enforce POSIX modes.
|
|
62
95
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
finally {
|
|
72
|
-
fs.closeSync(descriptor);
|
|
73
|
-
}
|
|
74
|
-
})();
|
|
75
|
-
const tempPathStat = fs.lstatSync(tempPath);
|
|
76
|
-
if (tempPathStat.isSymbolicLink() ||
|
|
77
|
-
!tempPathStat.isFile() ||
|
|
78
|
-
tempPathStat.nlink > 1 ||
|
|
79
|
-
!sameFileIdentity(tempPathStat, tempStat)) {
|
|
80
|
-
throw new FsSafeError("path-mismatch", "store temp changed before publication");
|
|
81
|
-
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
owner.adopt(temp);
|
|
99
|
+
// Preserve the store's strict fsync errors; the generic temp helper tolerates EPERM.
|
|
100
|
+
if (params.durable)
|
|
101
|
+
fs.fsyncSync(temp.fd);
|
|
102
|
+
verifyStoreFile(temp.fd, owner.identity, tempPath);
|
|
82
103
|
if (parentGuard) {
|
|
83
104
|
assertSyncDirectoryGuard(parentGuard);
|
|
84
105
|
}
|
|
85
106
|
fs.renameSync(tempPath, filePath);
|
|
86
|
-
|
|
107
|
+
owner.markRenamed();
|
|
87
108
|
if (parentGuard) {
|
|
88
109
|
assertSyncDirectoryGuard(parentGuard);
|
|
89
110
|
}
|
|
90
111
|
try {
|
|
91
|
-
|
|
92
|
-
if (publishedStat.isSymbolicLink() ||
|
|
93
|
-
!publishedStat.isFile() ||
|
|
94
|
-
publishedStat.nlink > 1 ||
|
|
95
|
-
!sameFileIdentity(tempStat, publishedStat)) {
|
|
96
|
-
throw new FsSafeError("path-mismatch", "store target changed after write");
|
|
97
|
-
}
|
|
112
|
+
verifyStoreFile(temp.fd, owner.identity, filePath);
|
|
98
113
|
}
|
|
99
114
|
catch (error) {
|
|
100
115
|
if (error instanceof FsSafeError) {
|
|
@@ -117,14 +132,11 @@ export function writeFileSyncAtomic(params) {
|
|
|
117
132
|
}
|
|
118
133
|
return filePath;
|
|
119
134
|
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
originalError = error;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
120
139
|
finally {
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
fs.unlinkSync(tempPath);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// Best-effort cleanup after write failure.
|
|
127
|
-
}
|
|
128
|
-
}
|
|
140
|
+
owner.finish({ fsModule: fs, originalError, throwOnCleanupError: false });
|
|
129
141
|
}
|
|
130
142
|
}
|
package/dist/file-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
|