@openclaw/fs-safe 0.9.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 +134 -0
- package/LICENSE +1 -0
- package/README.md +52 -4
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- 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 +3 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +23 -3
- 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 +16 -13
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -6
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +83 -74
- 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 +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +23 -10
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +18 -14
- 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 +12 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +82 -45
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +22 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +69 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +10 -1
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +27 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +222 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +125 -0
- 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/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -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 +9 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +135 -39
- 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 +4 -13
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +4 -2
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +8 -4
- 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/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- 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 +5 -8
- 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 +19 -18
- 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 +24 -25
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +56 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +30 -34
- package/dist/mutation-authority.d.ts +9 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +29 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +19 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +3 -1
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- 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 +5 -2
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +85 -53
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +7 -3
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +51 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +6 -5
- 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-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- 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 +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +53 -28
- 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 +26 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +219 -0
- 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 +6 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +16 -12
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +17 -51
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +358 -234
- package/dist/root-options.d.ts +77 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +59 -3
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- 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 +14 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +3 -5
- package/dist/root.d.ts +4 -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 +25 -8
- 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 +17 -5
- 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 +20 -1
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- 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/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +9 -7
- 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 +9 -28
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +26 -0
- package/docs/advanced.md +22 -4
- package/docs/archive.md +44 -9
- package/docs/atomic.md +24 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +36 -1
- package/docs/copy.md +155 -0
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +53 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +4 -4
- 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/local-roots.md +8 -1
- package/docs/native-helper.md +10 -3
- package/docs/native.md +18 -1
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/permissions.md +29 -10
- package/docs/positional-read.md +63 -0
- package/docs/public-api.md +31 -2
- package/docs/root.md +196 -6
- package/docs/secure-file.md +2 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +12 -3
- package/docs/temp.md +35 -6
- package/docs/timing.md +2 -0
- package/docs/types.md +19 -12
- package/docs/walk.md +54 -5
- package/docs/writing.md +173 -5
- package/package.json +19 -8
package/dist/archive-merge.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExtractionDeadline } from "./archive-deadline.js";
|
|
2
|
+
import { type ArchiveDirectoryGuard } from "./archive-staging.js";
|
|
2
3
|
export type ArchivePublicationEntry = {
|
|
3
4
|
path: string;
|
|
4
5
|
kind: "file" | "directory";
|
|
@@ -10,7 +11,10 @@ type MergeParams = {
|
|
|
10
11
|
destinationRealDir: string;
|
|
11
12
|
deadline?: ExtractionDeadline;
|
|
12
13
|
};
|
|
13
|
-
|
|
14
|
+
type GuardedMergeParams = Pick<MergeParams, "sourceDir" | "deadline"> & {
|
|
15
|
+
destinationGuard: ArchiveDirectoryGuard;
|
|
16
|
+
};
|
|
17
|
+
export declare function mergePlannedArchiveIntoDestination(params: GuardedMergeParams & {
|
|
14
18
|
entries: readonly ArchivePublicationEntry[];
|
|
15
19
|
durable?: boolean;
|
|
16
20
|
}): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-merge.d.ts","sourceRoot":"","sources":["../src/archive-merge.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-merge.d.ts","sourceRoot":"","sources":["../src/archive-merge.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAa9B,MAAM,MAAM,uBAAuB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjG,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AACF,KAAK,kBAAkB,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG;IACtE,gBAAgB,EAAE,qBAAqB,CAAC;CACzC,CAAC;AAEF,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,kBAAkB,GAAG;IAAE,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1F"}
|
package/dist/archive-merge.js
CHANGED
|
@@ -3,13 +3,13 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ownExtractionDestinationMutation } from "./archive-deadline.js";
|
|
5
5
|
import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createDirectoryIdentityGuard, createArchiveSymlinkTraversalError, preparePrivateArchiveOutputPath, } from "./archive-staging.js";
|
|
6
|
-
import {} from "./directory-guard.js";
|
|
7
6
|
import {} from "./directory-mode-owner.js";
|
|
8
7
|
import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
|
|
9
8
|
import { FsSafeError } from "./errors.js";
|
|
10
9
|
import { formatErrorDetail } from "./error-detail.js";
|
|
11
10
|
import { isPathInside } from "./path.js";
|
|
12
|
-
import {
|
|
11
|
+
import { rootFromDirectoryGuard } from "./root-impl.js";
|
|
12
|
+
import { realpathSync } from "./realpath.js";
|
|
13
13
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
14
14
|
import { onCopyPublication } from "./copy-publication.js";
|
|
15
15
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
@@ -18,16 +18,18 @@ export async function mergePlannedArchiveIntoDestination(params) {
|
|
|
18
18
|
await mergeTree(params, params.entries, params.durable === true);
|
|
19
19
|
}
|
|
20
20
|
export async function mergeExtractedTreeIntoDestination(params) {
|
|
21
|
-
await
|
|
21
|
+
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
|
|
22
|
+
await mergeTree({ sourceDir: params.sourceDir, deadline: params.deadline, destinationGuard });
|
|
22
23
|
}
|
|
23
24
|
async function mergeTree(params, publication, durable = true) {
|
|
24
25
|
const publishedFiles = [];
|
|
25
26
|
const publishedDirectories = [];
|
|
26
27
|
const check = () => params.deadline?.check();
|
|
27
28
|
check();
|
|
28
|
-
const destinationGuard =
|
|
29
|
-
|
|
30
|
-
const
|
|
29
|
+
const { destinationGuard } = params;
|
|
30
|
+
const destinationDir = destinationGuard.dir;
|
|
31
|
+
const destinationRealDir = destinationGuard.realPath;
|
|
32
|
+
const targetRoot = rootFromDirectoryGuard(destinationGuard);
|
|
31
33
|
check();
|
|
32
34
|
const sourceGuard = await createDirectoryIdentityGuard(params.sourceDir);
|
|
33
35
|
check();
|
|
@@ -35,7 +37,7 @@ async function mergeTree(params, publication, durable = true) {
|
|
|
35
37
|
for (const entry of publication ?? []) {
|
|
36
38
|
// Resolve admitted spelling in private staging, preserving the volume's case
|
|
37
39
|
// and Unicode behavior without assigning explicit modes to distinct parents.
|
|
38
|
-
const stagedPath =
|
|
40
|
+
const stagedPath = realpathSync.native(path.join(params.sourceDir, entry.path));
|
|
39
41
|
check();
|
|
40
42
|
if (!isPathInside(sourceGuard.realPath, stagedPath) || plan.has(stagedPath)) {
|
|
41
43
|
throw new FsSafeError("path-mismatch", "archive publication paths changed in staging");
|
|
@@ -64,12 +66,12 @@ async function mergeTree(params, publication, durable = true) {
|
|
|
64
66
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
65
67
|
const relPath = path.relative(params.sourceDir, sourcePath);
|
|
66
68
|
const originalPath = relPath.split(path.sep).join("/");
|
|
67
|
-
const destinationPath = path.join(
|
|
69
|
+
const destinationPath = path.join(destinationDir, relPath);
|
|
68
70
|
const sourceStat = fsSync.lstatSync(sourcePath);
|
|
69
71
|
check();
|
|
70
72
|
if (sourceStat.isSymbolicLink())
|
|
71
73
|
throw createArchiveSymlinkTraversalError(originalPath);
|
|
72
|
-
const sourceReal =
|
|
74
|
+
const sourceReal = realpathSync.native(sourcePath);
|
|
73
75
|
check();
|
|
74
76
|
if (!isPathInside(sourceGuard.realPath, sourceReal))
|
|
75
77
|
throw createArchiveSymlinkTraversalError(originalPath);
|
|
@@ -83,7 +85,8 @@ async function mergeTree(params, publication, durable = true) {
|
|
|
83
85
|
}
|
|
84
86
|
const mode = plan ? planned?.mode ?? 0o755 : sourceStat.mode & 0o777;
|
|
85
87
|
await preparePrivateArchiveOutputPath({
|
|
86
|
-
|
|
88
|
+
destinationDir, destinationRealDir, deadline: params.deadline,
|
|
89
|
+
relPath, outPath: destinationPath, originalPath, isDirectory: kind === "directory",
|
|
87
90
|
}, assertGuards, destinationGuard);
|
|
88
91
|
check();
|
|
89
92
|
if (kind === "directory") {
|
|
@@ -119,7 +122,7 @@ async function mergeTree(params, publication, durable = true) {
|
|
|
119
122
|
await assertDirectoryIdentityGuard(guard);
|
|
120
123
|
check();
|
|
121
124
|
await assertResolvedInsideDestination({
|
|
122
|
-
destinationRealDir
|
|
125
|
+
destinationRealDir, targetPath: destinationPath, originalPath,
|
|
123
126
|
});
|
|
124
127
|
check();
|
|
125
128
|
} });
|
|
@@ -162,7 +165,7 @@ async function mergeTree(params, publication, durable = true) {
|
|
|
162
165
|
check();
|
|
163
166
|
await assertGuards();
|
|
164
167
|
await assertResolvedInsideDestination({
|
|
165
|
-
destinationRealDir
|
|
168
|
+
destinationRealDir, targetPath: destinationPath, originalPath,
|
|
166
169
|
});
|
|
167
170
|
check();
|
|
168
171
|
const stat = fsSync.lstatSync(destinationPath);
|
|
@@ -172,7 +175,7 @@ async function mergeTree(params, publication, durable = true) {
|
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
catch (error) {
|
|
175
|
-
// copyIn
|
|
178
|
+
// copyIn cleans unpublished stages; merge never gains rollback authority.
|
|
176
179
|
if (error instanceof FsSafeError && (error.code === "hardlink" || error.code === "path-alias")) {
|
|
177
180
|
throw createArchiveSymlinkTraversalError(originalPath);
|
|
178
181
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAYnE;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAYnE;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CA+FhB"}
|
package/dist/archive-native.js
CHANGED
|
@@ -6,7 +6,7 @@ import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
|
|
|
6
6
|
import { createArchiveEntryPlanner } from "./archive-plan.js";
|
|
7
7
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
8
8
|
import { assertArchiveEntryCountWithinLimit, } from "./archive-limits.js";
|
|
9
|
-
import {
|
|
9
|
+
import { prepareArchiveDestinationGuard, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
10
10
|
import { mergePlannedArchiveIntoDestination } from "./archive-merge.js";
|
|
11
11
|
import { admitZipFile } from "./archive-zip-admission.js";
|
|
12
12
|
export function throwMappedNativeArchiveError(error) {
|
|
@@ -35,13 +35,15 @@ export async function extractNativeArchive(params) {
|
|
|
35
35
|
const physicalCount = params.kind === "zip"
|
|
36
36
|
? await admitZipFile(stagedArchive.path, limits, params.deadline, (entry) => { zipEntries.push(entry); })
|
|
37
37
|
: undefined;
|
|
38
|
-
const
|
|
38
|
+
const destinationGuard = await prepareArchiveDestinationGuard(params.destDir);
|
|
39
|
+
const destinationRealDir = destinationGuard.realPath;
|
|
39
40
|
await withStagedArchiveDestination({
|
|
40
41
|
destinationRealDir,
|
|
41
42
|
run: async (stagingDir) => {
|
|
42
43
|
params.deadline.check();
|
|
44
|
+
// N-API retains completed task state on its signal; each pass needs its own.
|
|
43
45
|
const manifest = await params.binding
|
|
44
|
-
.inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, params.deadline.signal)
|
|
46
|
+
.inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, AbortSignal.any([params.deadline.signal]))
|
|
45
47
|
.catch(throwMappedNativeArchiveError);
|
|
46
48
|
params.deadline.check();
|
|
47
49
|
assertArchiveEntryCountWithinLimit(manifest.length, limits);
|
|
@@ -84,7 +86,7 @@ export async function extractNativeArchive(params) {
|
|
|
84
86
|
(typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0));
|
|
85
87
|
try {
|
|
86
88
|
params.deadline.check();
|
|
87
|
-
await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan.map((entry) => ({ ...entry, mode: entry.kind === "directory" ? 0o700 : 0o600 })), tarLimits, params.deadline.signal).catch(throwMappedNativeArchiveError);
|
|
89
|
+
await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan.map((entry) => ({ ...entry, mode: entry.kind === "directory" ? 0o700 : 0o600 })), tarLimits, AbortSignal.any([params.deadline.signal])).catch(throwMappedNativeArchiveError);
|
|
88
90
|
}
|
|
89
91
|
finally {
|
|
90
92
|
await directory.close().catch(() => undefined);
|
|
@@ -94,8 +96,7 @@ export async function extractNativeArchive(params) {
|
|
|
94
96
|
entries: plan,
|
|
95
97
|
durable: params.durable,
|
|
96
98
|
sourceDir: stagingDir,
|
|
97
|
-
|
|
98
|
-
destinationRealDir,
|
|
99
|
+
destinationGuard,
|
|
99
100
|
deadline: params.deadline,
|
|
100
101
|
});
|
|
101
102
|
params.deadline.check();
|
package/dist/archive-parser.wasm
CHANGED
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyOpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}
|
package/dist/archive-read.js
CHANGED
|
@@ -2,22 +2,23 @@ import { classifyArchiveParserError } from "./archive-parser-errors.js";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
|
-
import {
|
|
5
|
+
import { readBoundedAsync } from "./bounded-read.js";
|
|
6
6
|
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
7
7
|
import { formatErrorDetail } from "./error-detail.js";
|
|
8
8
|
import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
9
9
|
import { assertPortableArchiveKind, resolveArchiveKind } from "./archive-kind.js";
|
|
10
10
|
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
|
|
11
|
+
import { isGzipBuffer } from "./archive-gzip-tail.js";
|
|
11
12
|
import { inspectTar, replayTar } from "./archive-tar-stream.js";
|
|
12
|
-
import {
|
|
13
|
+
import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
|
|
13
14
|
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
14
15
|
import { FsSafeError } from "./errors.js";
|
|
15
16
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
16
17
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
18
|
+
import { realpathSync } from "./realpath.js";
|
|
17
19
|
import { getNativeBinding } from "./native.js";
|
|
18
20
|
import { admitZipBuffer } from "./archive-zip-admission.js";
|
|
19
21
|
import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
|
|
20
|
-
import { tempFile } from "./temp-target.js";
|
|
21
22
|
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
|
22
23
|
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
|
23
24
|
function canonicalEntryPath(entryPath) {
|
|
@@ -63,8 +64,8 @@ async function readStreamBounded(stream, maxBytes) {
|
|
|
63
64
|
}
|
|
64
65
|
return Buffer.concat(chunks, total);
|
|
65
66
|
}
|
|
66
|
-
async function
|
|
67
|
-
const resolved =
|
|
67
|
+
async function readArchiveInput(archivePath) {
|
|
68
|
+
const resolved = realpathSync.native(archivePath);
|
|
68
69
|
const before = await inspectFileIdentity(async () => {
|
|
69
70
|
const stat = fsSync.lstatSync(archivePath, { bigint: true });
|
|
70
71
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
@@ -73,9 +74,7 @@ async function stageArchiveInput(archivePath) {
|
|
|
73
74
|
return stat;
|
|
74
75
|
});
|
|
75
76
|
const handle = await fs.open(resolved, resolveReadOpenFlags());
|
|
76
|
-
let staged;
|
|
77
77
|
try {
|
|
78
|
-
staged = await tempFile({ prefix: "fs-safe-archive-read", fileName: "archive.bin" });
|
|
79
78
|
const opened = await inspectFileIdentity(async () => {
|
|
80
79
|
const stat = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
81
80
|
if (!stat.isFile())
|
|
@@ -88,12 +87,14 @@ async function stageArchiveInput(archivePath) {
|
|
|
88
87
|
throw new Error("archive changed during validation");
|
|
89
88
|
return stat;
|
|
90
89
|
}, opened);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
90
|
+
// Native archive workers borrow this private buffer. A pooled allocation could
|
|
91
|
+
// share its ArrayBuffer with unrelated JS buffers while the worker runs.
|
|
92
|
+
return await readBoundedAsync(DEFAULT_MAX_ARCHIVE_BYTES_ZIP, async (buffer, length) => (await handle.read(buffer, 0, length, null)).bytesRead, { unpooled: true, observeRegularFileSize: () => {
|
|
93
|
+
const size = fsSync.fstatSync(handle.fd).size;
|
|
94
|
+
return Number.isSafeInteger(size) && size >= 0 ? size : undefined;
|
|
95
|
+
} });
|
|
94
96
|
}
|
|
95
97
|
catch (error) {
|
|
96
|
-
await staged?.cleanup().catch(() => undefined);
|
|
97
98
|
if (error instanceof FsSafeError && error.code === "path-mismatch") {
|
|
98
99
|
throw new FsSafeError("path-mismatch", "archive changed during validation", { cause: error });
|
|
99
100
|
}
|
|
@@ -103,12 +104,8 @@ async function stageArchiveInput(archivePath) {
|
|
|
103
104
|
await handle.close().catch(() => undefined);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
107
|
-
const archive = await
|
|
108
|
-
maxArchiveBytes: DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
|
|
109
|
-
maxEntryBytes: maxBytes,
|
|
110
|
-
maxExtractedBytes: maxBytes,
|
|
111
|
-
});
|
|
107
|
+
async function readZipEntry(buffer, entryPath, maxBytes, physicalCount) {
|
|
108
|
+
const archive = await loadAdmittedZipArchive(buffer, physicalCount);
|
|
112
109
|
let entry;
|
|
113
110
|
// JSZip keys retain some aliases and may use Unicode Path metadata. Scan the
|
|
114
111
|
// effective entries once, after raw ZIP admission has rejected collisions.
|
|
@@ -134,11 +131,11 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
|
134
131
|
stream.once("error", (error) => integrity.destroy(normalizeZipIntegrityError(error)));
|
|
135
132
|
return await readStreamBounded(stream.pipe(integrity), maxBytes);
|
|
136
133
|
}
|
|
137
|
-
async function readTarEntry(
|
|
134
|
+
async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
|
|
138
135
|
const seenPaths = new Set();
|
|
139
136
|
let selected;
|
|
140
137
|
const limits = resolveTarMeterLimits();
|
|
141
|
-
await inspectTar({
|
|
138
|
+
await inspectTar({ archiveBuffer, limits, onMember(info) {
|
|
142
139
|
const normalized = canonicalEntryPath(info.path);
|
|
143
140
|
if (seenPaths.has(normalized)) {
|
|
144
141
|
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
@@ -154,12 +151,68 @@ async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
|
154
151
|
}
|
|
155
152
|
if (selected.size > maxBytes)
|
|
156
153
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
154
|
+
if (!isGzipBuffer(archiveBuffer)) {
|
|
155
|
+
// Complete admission already validated this private snapshot through EOF.
|
|
156
|
+
// Copy the admitted payload so the result cannot expose or mutate its input.
|
|
157
|
+
const end = selected.offset + selected.size;
|
|
158
|
+
if (!Number.isSafeInteger(selected.offset) || selected.offset < 0 ||
|
|
159
|
+
!Number.isSafeInteger(end) || end < selected.offset || end > archiveBuffer.length) {
|
|
160
|
+
throw new ArchiveFormatError("invalid admitted TAR range");
|
|
161
|
+
}
|
|
162
|
+
return Buffer.from(archiveBuffer.subarray(selected.offset, end));
|
|
163
|
+
}
|
|
157
164
|
let result;
|
|
158
|
-
await replayTar({
|
|
165
|
+
await replayTar({ archiveBuffer, limits, members: [selected], async consume(_member, payload) {
|
|
159
166
|
result = await readStreamBounded(payload, maxBytes);
|
|
160
167
|
} });
|
|
161
168
|
return result;
|
|
162
169
|
}
|
|
170
|
+
function selectNativeEntry(manifest, requested, displayPath, physicalCount) {
|
|
171
|
+
if (physicalCount !== undefined && manifest.length !== physicalCount) {
|
|
172
|
+
throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
|
|
173
|
+
}
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
let selected;
|
|
176
|
+
for (const entry of manifest) {
|
|
177
|
+
const normalized = canonicalEntryPath(entry.path);
|
|
178
|
+
if (seen.has(normalized)) {
|
|
179
|
+
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
180
|
+
}
|
|
181
|
+
seen.add(normalized);
|
|
182
|
+
if (normalized === requested) {
|
|
183
|
+
if (entry.kind !== "file")
|
|
184
|
+
throw new Error(`archive entry is not a file: ${formatErrorDetail(displayPath)}`);
|
|
185
|
+
selected = entry;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!selected)
|
|
189
|
+
throw new Error(`archive entry not found: ${formatErrorDetail(displayPath)}`);
|
|
190
|
+
return selected;
|
|
191
|
+
}
|
|
192
|
+
function throwNativeReadError(error) {
|
|
193
|
+
if (error instanceof Error) {
|
|
194
|
+
const mapped = classifyArchiveParserError(error.message, { cause: error });
|
|
195
|
+
if (mapped)
|
|
196
|
+
throw mapped;
|
|
197
|
+
if (isArchiveFormatErrorMessage(error.message))
|
|
198
|
+
throw new ArchiveFormatError(error.message, { cause: error });
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
async function readNativeBufferEntry(native, buffer, kind, requested, displayPath, maxBytes, physicalCount) {
|
|
203
|
+
try {
|
|
204
|
+
const signal = new AbortController().signal;
|
|
205
|
+
const limits = resolveTarMeterLimits();
|
|
206
|
+
const reader = kind === "zip"
|
|
207
|
+
? await native.openZipBufferNative(buffer, limits, AbortSignal.any([signal]))
|
|
208
|
+
: await native.openTarBufferNative(buffer, kind, limits, AbortSignal.any([signal]));
|
|
209
|
+
const selected = selectNativeEntry(reader.entries, requested, displayPath, physicalCount);
|
|
210
|
+
return await reader.readEntry(selected.index, maxBytes, AbortSignal.any([signal]));
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
throwNativeReadError(error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
163
216
|
export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
164
217
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
165
218
|
throw new RangeError("maxBytes must be a non-negative safe integer");
|
|
@@ -169,58 +222,14 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
169
222
|
throw new Error(`unsupported archive: ${archivePath}`);
|
|
170
223
|
}
|
|
171
224
|
const requestedEntry = normalizedRequestedEntry(entryPath);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const manifest = await native.inspectArchiveNative(staged.path, kind, limits, signal);
|
|
183
|
-
let rawEntryPath;
|
|
184
|
-
if (physicalCount !== undefined && manifest.length !== physicalCount) {
|
|
185
|
-
throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
|
|
186
|
-
}
|
|
187
|
-
const seenPaths = new Set();
|
|
188
|
-
for (const entry of manifest) {
|
|
189
|
-
const normalized = canonicalEntryPath(entry.path);
|
|
190
|
-
if (seenPaths.has(normalized)) {
|
|
191
|
-
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
192
|
-
}
|
|
193
|
-
seenPaths.add(normalized);
|
|
194
|
-
if (normalized === requestedEntry) {
|
|
195
|
-
if (entry.kind !== "file") {
|
|
196
|
-
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
197
|
-
}
|
|
198
|
-
rawEntryPath = entry.path;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (!rawEntryPath) {
|
|
202
|
-
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
203
|
-
}
|
|
204
|
-
return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, limits, signal);
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
if (error instanceof Error) {
|
|
208
|
-
const mapped = classifyArchiveParserError(error.message, { cause: error });
|
|
209
|
-
if (mapped)
|
|
210
|
-
throw mapped;
|
|
211
|
-
}
|
|
212
|
-
if (error instanceof Error && isArchiveFormatErrorMessage(error.message)) {
|
|
213
|
-
throw new ArchiveFormatError(error.message, { cause: error });
|
|
214
|
-
}
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
assertPortableArchiveKind(kind);
|
|
219
|
-
return kind === "zip"
|
|
220
|
-
? await readZipEntry(staged.buffer, requestedEntry, options.maxBytes)
|
|
221
|
-
: await readTarEntry(staged.path, requestedEntry, options.maxBytes);
|
|
222
|
-
}
|
|
223
|
-
finally {
|
|
224
|
-
await staged.cleanup();
|
|
225
|
-
}
|
|
225
|
+
const buffer = await readArchiveInput(archivePath);
|
|
226
|
+
const physicalCount = kind === "zip"
|
|
227
|
+
? admitZipBuffer(buffer, resolveExtractLimits())
|
|
228
|
+
: undefined;
|
|
229
|
+
const native = getNativeBinding();
|
|
230
|
+
if (native)
|
|
231
|
+
return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, physicalCount);
|
|
232
|
+
assertPortableArchiveKind(kind);
|
|
233
|
+
return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes, physicalCount)
|
|
234
|
+
: await readTarEntry(buffer, requestedEntry, options.maxBytes);
|
|
226
235
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
1
2
|
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
3
|
import { type ExtractionDeadline } from "./archive-deadline.js";
|
|
3
4
|
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
5
|
+
export type ArchiveDirectoryGuard = AsyncDirectoryGuard<BigIntStats>;
|
|
4
6
|
export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
|
|
5
|
-
export declare function createDirectoryIdentityGuard(dir: string): Promise<
|
|
6
|
-
export declare function assertDirectoryIdentityGuard(guard:
|
|
7
|
+
export declare function createDirectoryIdentityGuard(dir: string): Promise<ArchiveDirectoryGuard>;
|
|
8
|
+
export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard): Promise<void>;
|
|
9
|
+
export declare function prepareArchiveDestinationGuard(destDir: string): Promise<ArchiveDirectoryGuard>;
|
|
7
10
|
export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
|
|
8
11
|
export declare function assertResolvedInsideDestination(params: {
|
|
9
12
|
destinationRealDir: string;
|
|
@@ -20,7 +23,7 @@ type ArchiveOutputPathParams = {
|
|
|
20
23
|
deadline?: ExtractionDeadline;
|
|
21
24
|
};
|
|
22
25
|
export declare function prepareArchiveOutputPath(params: ArchiveOutputPathParams): Promise<void>;
|
|
23
|
-
export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>, destinationGuard?:
|
|
26
|
+
export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>, destinationGuard?: ArchiveDirectoryGuard): Promise<void>;
|
|
24
27
|
export declare function withStagedArchiveDestination<T>(params: {
|
|
25
28
|
destinationRealDir: string;
|
|
26
29
|
stagingDirPrefix?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAgB7B,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;AAMrE,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAS1F,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAS9F;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAY9F;AAED,wBAAsB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyCpG;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnF;AA0BD,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB;AAiBD,KAAK,uBAAuB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7F;AAED,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,uBAAuB,EAAE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EACnE,gBAAgB,CAAC,EAAE,qBAAqB,GACvC,OAAO,CAAC,IAAI,CAAC,CAEf;AAkHD,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6Bb;AAED,OAAO,EAAE,iCAAiC,EAAE,MAAM,oBAAoB,CAAC;AAEvE,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
|
package/dist/archive-staging.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fsSync from "node:fs";
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
@@ -13,6 +13,8 @@ import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
|
13
13
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
14
14
|
import { expandRelativePathWithHome } from "./root-context.js";
|
|
15
15
|
import { resolveRootPath } from "./root-path.js";
|
|
16
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
17
|
+
import { realpathSync } from "./realpath.js";
|
|
16
18
|
const ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK = "archive entry traverses symlink in destination";
|
|
17
19
|
const ARCHIVE_STAGING_MODE = 0o700;
|
|
18
20
|
function checkExtractionDeadline(deadline) {
|
|
@@ -24,7 +26,7 @@ function symlinkTraversalError(originalPath) {
|
|
|
24
26
|
}
|
|
25
27
|
export async function createDirectoryIdentityGuard(dir) {
|
|
26
28
|
try {
|
|
27
|
-
return await createAsyncDirectoryGuard(dir);
|
|
29
|
+
return await createAsyncDirectoryGuard(dir, { bigint: true });
|
|
28
30
|
}
|
|
29
31
|
catch (err) {
|
|
30
32
|
if (err instanceof FsSafeError && err.code === "not-file") {
|
|
@@ -37,33 +39,51 @@ export async function assertDirectoryIdentityGuard(guard) {
|
|
|
37
39
|
try {
|
|
38
40
|
await assertAsyncDirectoryGuard(guard);
|
|
39
41
|
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof FsSafeError || isNotFoundPathError(err)) {
|
|
44
|
+
throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function prepareArchiveDestinationGuard(destDir) {
|
|
50
|
+
let stat;
|
|
51
|
+
try {
|
|
52
|
+
stat = inspectFileIdentitySync(() => {
|
|
53
|
+
const observed = fsSync.lstatSync(destDir, { bigint: true });
|
|
54
|
+
if (observed.isSymbolicLink()) {
|
|
55
|
+
throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
|
|
56
|
+
}
|
|
57
|
+
if (!observed.isDirectory()) {
|
|
58
|
+
throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
|
|
59
|
+
}
|
|
60
|
+
return observed;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err instanceof ArchiveSecurityError)
|
|
65
|
+
throw err;
|
|
66
|
+
if (err instanceof FsSafeError) {
|
|
67
|
+
throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
const realPath = realpathSync.native(destDir);
|
|
72
|
+
const guard = { dir: destDir, realPath, stat };
|
|
73
|
+
try {
|
|
74
|
+
inspectFileIdentitySync(() => fsSync.statSync(realPath, { bigint: true }), stat);
|
|
75
|
+
inspectFileIdentitySync(() => fsSync.lstatSync(destDir, { bigint: true }), stat);
|
|
76
|
+
}
|
|
40
77
|
catch (err) {
|
|
41
78
|
if (err instanceof FsSafeError) {
|
|
42
79
|
throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
|
|
43
80
|
}
|
|
44
81
|
throw err;
|
|
45
82
|
}
|
|
83
|
+
return guard;
|
|
46
84
|
}
|
|
47
85
|
export async function prepareArchiveDestinationDir(destDir) {
|
|
48
|
-
|
|
49
|
-
if (stat.isSymbolicLink()) {
|
|
50
|
-
throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
|
|
51
|
-
}
|
|
52
|
-
if (!stat.isDirectory()) {
|
|
53
|
-
throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
|
|
54
|
-
}
|
|
55
|
-
const realPath = fsSync.realpathSync.native(destDir);
|
|
56
|
-
const realStat = fsSync.statSync(realPath);
|
|
57
|
-
const postStat = fsSync.lstatSync(destDir);
|
|
58
|
-
if (realStat.dev !== stat.dev ||
|
|
59
|
-
realStat.ino !== stat.ino ||
|
|
60
|
-
postStat.isSymbolicLink() ||
|
|
61
|
-
!postStat.isDirectory() ||
|
|
62
|
-
postStat.dev !== stat.dev ||
|
|
63
|
-
postStat.ino !== stat.ino) {
|
|
64
|
-
throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
|
|
65
|
-
}
|
|
66
|
-
return realPath;
|
|
86
|
+
return (await prepareArchiveDestinationGuard(destDir)).realPath;
|
|
67
87
|
}
|
|
68
88
|
async function assertNoSymlinkTraversal(params) {
|
|
69
89
|
const parts = params.relPath.split(/[\\/]+/).filter(Boolean);
|
|
@@ -88,7 +108,7 @@ async function assertNoSymlinkTraversal(params) {
|
|
|
88
108
|
export async function assertResolvedInsideDestination(params) {
|
|
89
109
|
let resolved;
|
|
90
110
|
try {
|
|
91
|
-
resolved =
|
|
111
|
+
resolved = realpathSync.native(params.targetPath);
|
|
92
112
|
}
|
|
93
113
|
catch (err) {
|
|
94
114
|
if (isNotFoundPathError(err)) {
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import type { TarMeterLimits } from "./archive-limits.js";
|
|
2
2
|
import { type AdmittedTarMember } from "./archive-tar-wasm.js";
|
|
3
|
-
|
|
3
|
+
/** Buffers are private immutable snapshots, just like the staged file route. */
|
|
4
|
+
type TarInput = {
|
|
4
5
|
archivePath: string;
|
|
6
|
+
archiveBuffer?: never;
|
|
7
|
+
} | {
|
|
8
|
+
archiveBuffer: Buffer;
|
|
9
|
+
archivePath?: never;
|
|
10
|
+
};
|
|
11
|
+
export declare function inspectTar(params: TarInput & {
|
|
5
12
|
limits: TarMeterLimits;
|
|
6
13
|
signal?: AbortSignal;
|
|
7
14
|
onMember?: (entry: AdmittedTarMember) => void;
|
|
8
15
|
}): Promise<void>;
|
|
9
16
|
/** Replay in physical order, retaining at most one decoded chunk. Every range
|
|
10
|
-
* comes from complete admission of the immutable
|
|
11
|
-
export declare function replayTar<T extends AdmittedTarMember>(params: {
|
|
12
|
-
archivePath: string;
|
|
17
|
+
* comes from complete admission of the immutable input. */
|
|
18
|
+
export declare function replayTar<T extends AdmittedTarMember>(params: TarInput & {
|
|
13
19
|
limits: TarMeterLimits;
|
|
14
20
|
signal?: AbortSignal;
|
|
15
21
|
members: readonly T[];
|
|
16
22
|
consume(member: T, payload: AsyncIterable<Buffer>): Promise<void>;
|
|
17
23
|
}): Promise<void>;
|
|
24
|
+
export {};
|
|
18
25
|
//# sourceMappingURL=archive-tar-stream.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar-stream.d.ts","sourceRoot":"","sources":["../src/archive-tar-stream.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-tar-stream.d.ts","sourceRoot":"","sources":["../src/archive-tar-stream.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGhF,gFAAgF;AAChF,KAAK,QAAQ,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAuDhH,wBAAsB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG;IAClD,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;2DAC2D;AAC3D,wBAAsB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,QAAQ,GAAG;IAC9E,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
|