@openclaw/fs-safe 0.10.0 → 0.12.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 +114 -0
- package/LICENSE +1 -0
- package/README.md +39 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -1
- 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 +8 -8
- 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 +16 -9
- 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 +54 -42
- 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 +26 -9
- 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 +18 -11
- 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/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- 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 +99 -32
- 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 +15 -5
- 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 +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +32 -14
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +52 -16
- 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 +421 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +16 -2
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.d.ts +2 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +47 -3
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +59 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +43 -84
- 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 +14 -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-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +27 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +22 -3
- 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 +3 -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-boundary.d.ts +25 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +177 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +40 -13
- 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 +31 -5
- 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 +93 -70
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-options.d.ts +12 -1
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +14 -5
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +2 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +54 -26
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +24 -32
- 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-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +21 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/root.d.ts +2 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -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-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +27 -7
- 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 +2 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +49 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +8 -5
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +16 -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 +17 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- 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 +15 -7
- 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 +14 -12
- 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 +7 -1
- package/docs/archive.md +31 -5
- package/docs/atomic.md +17 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +33 -2
- package/docs/copy.md +75 -6
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +40 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +3 -3
- package/docs/file-store.md +21 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +146 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +34 -0
- package/docs/native-helper.md +14 -5
- package/docs/native.md +18 -2
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/path.md +1 -1
- package/docs/permissions.md +37 -3
- package/docs/public-api.md +36 -2
- package/docs/root.md +33 -3
- package/docs/secure-file.md +17 -14
- package/docs/security-model.md +1 -1
- package/docs/sidecar-lock.md +12 -3
- package/docs/store.md +22 -1
- package/docs/temp.md +39 -6
- package/docs/types.md +1 -1
- package/docs/writing.md +153 -3
- package/package.json +14 -8
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);
|
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,7 +35,8 @@ 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) => {
|
|
@@ -95,8 +96,7 @@ export async function extractNativeArchive(params) {
|
|
|
95
96
|
entries: plan,
|
|
96
97
|
durable: params.durable,
|
|
97
98
|
sourceDir: stagingDir,
|
|
98
|
-
|
|
99
|
-
destinationRealDir,
|
|
99
|
+
destinationGuard,
|
|
100
100
|
deadline: params.deadline,
|
|
101
101
|
});
|
|
102
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":"
|
|
1
|
+
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsOpG,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
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { classifyArchiveParserError } from "./archive-parser-errors.js";
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import { Readable } from "node:stream";
|
|
4
|
+
import { Readable, Writable } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
5
6
|
import { readBoundedAsync } from "./bounded-read.js";
|
|
6
7
|
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
7
8
|
import { formatErrorDetail } from "./error-detail.js";
|
|
8
9
|
import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
9
10
|
import { assertPortableArchiveKind, resolveArchiveKind } from "./archive-kind.js";
|
|
10
11
|
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
|
|
12
|
+
import { isGzipBuffer } from "./archive-gzip-tail.js";
|
|
11
13
|
import { inspectTar, replayTar } from "./archive-tar-stream.js";
|
|
12
|
-
import {
|
|
14
|
+
import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
|
|
13
15
|
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
14
16
|
import { FsSafeError } from "./errors.js";
|
|
15
17
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
16
18
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
19
|
+
import { realpathSync } from "./realpath.js";
|
|
17
20
|
import { getNativeBinding } from "./native.js";
|
|
18
21
|
import { admitZipBuffer } from "./archive-zip-admission.js";
|
|
19
22
|
import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
|
|
@@ -30,40 +33,24 @@ function normalizedRequestedEntry(entryPath) {
|
|
|
30
33
|
}
|
|
31
34
|
return normalized;
|
|
32
35
|
}
|
|
33
|
-
async function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const chunks = [];
|
|
38
|
-
let total = 0;
|
|
39
|
-
readable.on("data", (chunk) => {
|
|
40
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
41
|
-
total += buffer.length;
|
|
42
|
-
if (total > maxBytes) {
|
|
43
|
-
readable.pause();
|
|
44
|
-
reject(new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
chunks.push(buffer);
|
|
48
|
-
});
|
|
49
|
-
readable.once("end", () => resolve(Buffer.concat(chunks, total)));
|
|
50
|
-
readable.once("error", reject);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const chunks = [];
|
|
36
|
+
async function readAdmittedTarPayload(stream, size) {
|
|
37
|
+
// Complete admission has bounded this exact range. Copy as it arrives so
|
|
38
|
+
// decoder chunks can be released before the caller receives the owned result.
|
|
39
|
+
const result = Buffer.allocUnsafe(size);
|
|
54
40
|
let total = 0;
|
|
55
41
|
for await (const chunk of stream) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (total > maxBytes) {
|
|
59
|
-
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
42
|
+
if (chunk.length > size - total) {
|
|
43
|
+
throw new ArchiveFormatError("invalid admitted TAR payload size");
|
|
60
44
|
}
|
|
61
|
-
|
|
45
|
+
chunk.copy(result, total);
|
|
46
|
+
total += chunk.length;
|
|
62
47
|
}
|
|
63
|
-
|
|
48
|
+
if (total !== size)
|
|
49
|
+
throw new ArchiveFormatError("truncated admitted TAR range");
|
|
50
|
+
return result;
|
|
64
51
|
}
|
|
65
52
|
async function readArchiveInput(archivePath) {
|
|
66
|
-
const resolved =
|
|
53
|
+
const resolved = realpathSync.native(archivePath);
|
|
67
54
|
const before = await inspectFileIdentity(async () => {
|
|
68
55
|
const stat = fsSync.lstatSync(archivePath, { bigint: true });
|
|
69
56
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
@@ -102,12 +89,8 @@ async function readArchiveInput(archivePath) {
|
|
|
102
89
|
await handle.close().catch(() => undefined);
|
|
103
90
|
}
|
|
104
91
|
}
|
|
105
|
-
async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
106
|
-
const archive = await
|
|
107
|
-
maxArchiveBytes: DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
|
|
108
|
-
maxEntryBytes: maxBytes,
|
|
109
|
-
maxExtractedBytes: maxBytes,
|
|
110
|
-
});
|
|
92
|
+
async function readZipEntry(buffer, entryPath, maxBytes, physicalCount) {
|
|
93
|
+
const archive = await loadAdmittedZipArchive(buffer, physicalCount);
|
|
111
94
|
let entry;
|
|
112
95
|
// JSZip keys retain some aliases and may use Unicode Path metadata. Scan the
|
|
113
96
|
// effective entries once, after raw ZIP admission has rejected collisions.
|
|
@@ -126,12 +109,31 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
|
126
109
|
(entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE) {
|
|
127
110
|
throw new Error(`archive entry is a link: ${formatErrorDetail(entryPath)}`);
|
|
128
111
|
}
|
|
112
|
+
const integrity = createZipIntegrityTransform(entry);
|
|
129
113
|
const stream = typeof entry.nodeStream === "function"
|
|
130
114
|
? entry.nodeStream()
|
|
131
115
|
: Readable.from(await entry.async("nodebuffer"));
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
116
|
+
const chunks = [];
|
|
117
|
+
let total = 0;
|
|
118
|
+
const destination = new Writable({
|
|
119
|
+
write(chunk, _encoding, callback) {
|
|
120
|
+
total += chunk.length;
|
|
121
|
+
if (total > maxBytes) {
|
|
122
|
+
callback(new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
chunks.push(chunk);
|
|
126
|
+
callback();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
// Iterator teardown can mask limit errors on Node 22; keep them in stream callbacks.
|
|
131
|
+
await pipeline(stream, integrity, destination);
|
|
132
|
+
return Buffer.concat(chunks, total);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw normalizeZipIntegrityError(error);
|
|
136
|
+
}
|
|
135
137
|
}
|
|
136
138
|
async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
|
|
137
139
|
const seenPaths = new Set();
|
|
@@ -153,9 +155,19 @@ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
|
|
|
153
155
|
}
|
|
154
156
|
if (selected.size > maxBytes)
|
|
155
157
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
158
|
+
if (!isGzipBuffer(archiveBuffer)) {
|
|
159
|
+
// Complete admission already validated this private snapshot through EOF.
|
|
160
|
+
// Copy the admitted payload so the result cannot expose or mutate its input.
|
|
161
|
+
const end = selected.offset + selected.size;
|
|
162
|
+
if (!Number.isSafeInteger(selected.offset) || selected.offset < 0 ||
|
|
163
|
+
!Number.isSafeInteger(end) || end < selected.offset || end > archiveBuffer.length) {
|
|
164
|
+
throw new ArchiveFormatError("invalid admitted TAR range");
|
|
165
|
+
}
|
|
166
|
+
return Buffer.from(archiveBuffer.subarray(selected.offset, end));
|
|
167
|
+
}
|
|
156
168
|
let result;
|
|
157
|
-
await replayTar({ archiveBuffer, limits, members: [selected], async consume(
|
|
158
|
-
result = await
|
|
169
|
+
await replayTar({ archiveBuffer, limits, members: [selected], async consume(member, payload) {
|
|
170
|
+
result = await readAdmittedTarPayload(payload, member.size);
|
|
159
171
|
} });
|
|
160
172
|
return result;
|
|
161
173
|
}
|
|
@@ -222,6 +234,6 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
222
234
|
if (native)
|
|
223
235
|
return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, physicalCount);
|
|
224
236
|
assertPortableArchiveKind(kind);
|
|
225
|
-
return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes)
|
|
237
|
+
return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes, physicalCount)
|
|
226
238
|
: await readTarEntry(buffer, requestedEntry, options.maxBytes);
|
|
227
239
|
}
|
|
@@ -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 +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"}
|
|
@@ -2,9 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import { Readable, Writable } from "node:stream";
|
|
3
3
|
import { pipeline } from "node:stream/promises";
|
|
4
4
|
import { createGunzip } from "node:zlib";
|
|
5
|
-
import { GzipInput, validateGzipBufferTail, validateGzipContainerTail } from "./archive-gzip-tail.js";
|
|
5
|
+
import { GzipInput, isGzipBuffer, validateGzipBufferTail, validateGzipContainerTail } from "./archive-gzip-tail.js";
|
|
6
6
|
import { ArchiveFormatError } from "./archive-errors.js";
|
|
7
7
|
import { TarParserStream } from "./archive-tar-wasm.js";
|
|
8
|
+
import { readFileWindowFully } from "./positional-read.js";
|
|
8
9
|
function* bufferChunks(buffer) {
|
|
9
10
|
for (let offset = 0; offset < buffer.length; offset += 65536)
|
|
10
11
|
yield buffer.subarray(offset, offset + 65536);
|
|
@@ -13,8 +14,8 @@ async function gzipFile(filePath) {
|
|
|
13
14
|
const handle = await fs.promises.open(filePath, "r");
|
|
14
15
|
try {
|
|
15
16
|
const magic = Buffer.alloc(2);
|
|
16
|
-
const
|
|
17
|
-
return bytesRead === 2 && magic
|
|
17
|
+
const bytesRead = await readFileWindowFully(handle, magic, 0);
|
|
18
|
+
return bytesRead === 2 && isGzipBuffer(magic);
|
|
18
19
|
}
|
|
19
20
|
finally {
|
|
20
21
|
await handle.close();
|
|
@@ -22,13 +23,13 @@ async function gzipFile(filePath) {
|
|
|
22
23
|
}
|
|
23
24
|
async function withTarStream(params, consume) {
|
|
24
25
|
const buffer = params.archiveBuffer;
|
|
25
|
-
const gzip = buffer !== undefined ? buffer
|
|
26
|
+
const gzip = buffer !== undefined ? isGzipBuffer(buffer) : await gzipFile(params.archivePath);
|
|
26
27
|
const parser = new TarParserStream(params.limits, params.onMember);
|
|
27
28
|
const input = buffer !== undefined
|
|
28
29
|
? Readable.from(bufferChunks(buffer), { objectMode: false, highWaterMark: 65536 })
|
|
29
30
|
: fs.createReadStream(params.archivePath, { highWaterMark: 65536 });
|
|
30
|
-
// Match the WASM input window for
|
|
31
|
-
const decoder = gzip ? createGunzip(
|
|
31
|
+
// Match the WASM input window for both staged files and buffered reads.
|
|
32
|
+
const decoder = gzip ? createGunzip({ chunkSize: 65536 }) : undefined;
|
|
32
33
|
const gzipInput = decoder ? new GzipInput(decoder) : undefined;
|
|
33
34
|
const destroy = (error) => {
|
|
34
35
|
input.destroy(error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar-wasm.d.ts","sourceRoot":"","sources":["../src/archive-tar-wasm.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyClE,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,SAAS;IAER,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAD9D,OAAO,CAAC,GAAG,CAAkB;IAC7B,YAAY,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,aAAA,EAQjG;IACD,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,IAAI;IAIH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"archive-tar-wasm.d.ts","sourceRoot":"","sources":["../src/archive-tar-wasm.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyClE,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,SAAS;IAER,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAD9D,OAAO,CAAC,GAAG,CAAkB;IAC7B,YAAY,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,aAAA,EAQjG;IACD,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,IAAI;IAIH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAqB/F;IACQ,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAKjD;IACQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAKpF;CACF"}
|
package/dist/archive-tar-wasm.js
CHANGED
|
@@ -53,21 +53,24 @@ export class TarParserStream extends Transform {
|
|
|
53
53
|
_transform(chunk, _encoding, callback) {
|
|
54
54
|
try {
|
|
55
55
|
const abi = this.abi;
|
|
56
|
-
for (let offset = 0; offset < chunk.length;) {
|
|
56
|
+
for (let offset = 0; offset < chunk.length; offset += 65536) {
|
|
57
57
|
const length = Math.min(65536, chunk.length - offset);
|
|
58
58
|
this.bytes(abi.input_ptr(), length).set(chunk.subarray(offset, offset + length));
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
// A member event can stop before the end of this already-copied inbox.
|
|
60
|
+
for (let consumed = 0; consumed < length;) {
|
|
61
|
+
const used = abi.push(consumed, length - consumed);
|
|
62
|
+
if (used < 0)
|
|
63
|
+
throw parserError(this.text());
|
|
64
|
+
if (used === 0 || used > length - consumed)
|
|
65
|
+
throw new ArchiveFormatError("TAR WASM made no progress");
|
|
66
|
+
consumed += used;
|
|
67
|
+
const type = abi.member_type();
|
|
68
|
+
if (type >= 0)
|
|
69
|
+
this.onMember?.({
|
|
70
|
+
path: this.text(), type: types.get(type) ?? "Unsupported", size: abi.member_size(),
|
|
71
|
+
mode: abi.member_mode(), offset: abi.member_offset(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
71
74
|
}
|
|
72
75
|
callback(null, chunk);
|
|
73
76
|
}
|
|
@@ -3,6 +3,6 @@ import { type ResolvedArchiveExtractLimits } from "./archive-limits.js";
|
|
|
3
3
|
import { type ZipDirectoryEntry } from "./archive-zip-directory.js";
|
|
4
4
|
/** Admit physical records before a decoder can normalize or collapse names. */
|
|
5
5
|
export declare function admitZipBuffer(input: Uint8Array, limits: ResolvedArchiveExtractLimits): number;
|
|
6
|
-
/** The extraction input is
|
|
6
|
+
/** The extraction input is private and immutable; read bounded metadata windows. */
|
|
7
7
|
export declare function admitZipFile(archivePath: string, limits: ResolvedArchiveExtractLimits, deadline: ExtractionDeadline, onEntry?: (entry: ZipDirectoryEntry) => void): Promise<number>;
|
|
8
8
|
//# sourceMappingURL=archive-zip-admission.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-zip-admission.d.ts","sourceRoot":"","sources":["../src/archive-zip-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAA+C,KAAK,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAErH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAUtF,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAU9F;
|
|
1
|
+
{"version":3,"file":"archive-zip-admission.d.ts","sourceRoot":"","sources":["../src/archive-zip-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAA+C,KAAK,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAErH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAUtF,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAU9F;AAMD,oFAAoF;AACpF,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,QAAQ,EAAE,kBAAkB,EACvF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAC3C,OAAO,CAAC,MAAM,CAAC,CA8DjB"}
|
|
@@ -23,7 +23,9 @@ export function admitZipBuffer(input, limits) {
|
|
|
23
23
|
}
|
|
24
24
|
return step.value;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const METADATA_WINDOW_BYTES = 4096;
|
|
27
|
+
const CACHED_READS_PER_YIELD = 32;
|
|
28
|
+
/** The extraction input is private and immutable; read bounded metadata windows. */
|
|
27
29
|
export async function admitZipFile(archivePath, limits, deadline, onEntry) {
|
|
28
30
|
deadline.check();
|
|
29
31
|
const opened = await openRootFile({
|
|
@@ -35,20 +37,54 @@ export async function admitZipFile(archivePath, limits, deadline, onEntry) {
|
|
|
35
37
|
try {
|
|
36
38
|
checkSize(opened.stat.size, limits);
|
|
37
39
|
const scan = scanZipDirectory(opened.stat.size, limits, onEntry);
|
|
40
|
+
// Separate windows keep alternating central and local records hot. Never
|
|
41
|
+
// refill a buffer: the scanner can retain its views after cache eviction.
|
|
42
|
+
const windows = [];
|
|
43
|
+
let cachedReads = 0;
|
|
38
44
|
let step = scan.next();
|
|
39
45
|
while (!step.done) {
|
|
46
|
+
deadline.check();
|
|
40
47
|
const { offset, length } = step.value;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const cached = windows.findIndex(window => offset >= window.offset &&
|
|
49
|
+
length <= window.bytes.length - (offset - window.offset));
|
|
50
|
+
let bytes;
|
|
51
|
+
if (cached !== -1) {
|
|
52
|
+
if (++cachedReads >= CACHED_READS_PER_YIELD) {
|
|
53
|
+
cachedReads = 0;
|
|
54
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
55
|
+
deadline.check();
|
|
56
|
+
}
|
|
57
|
+
const window = windows[cached];
|
|
58
|
+
if (cached === 1)
|
|
59
|
+
windows.reverse();
|
|
60
|
+
bytes = window.bytes.subarray(offset - window.offset, offset - window.offset + length);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const cacheable = length > 0 && length <= METADATA_WINDOW_BYTES;
|
|
64
|
+
const capacity = cacheable ? Math.min(METADATA_WINDOW_BYTES, opened.stat.size - offset) : length;
|
|
65
|
+
const buffer = Buffer.alloc(capacity);
|
|
66
|
+
let received = 0;
|
|
67
|
+
while (received < capacity) {
|
|
68
|
+
deadline.check();
|
|
69
|
+
const count = await new Promise((resolve, reject) => {
|
|
70
|
+
fs.read(opened.fd, buffer, received, capacity - received, offset + received, (error, count) => error ? reject(error) : resolve(count));
|
|
71
|
+
});
|
|
72
|
+
cachedReads = 0;
|
|
73
|
+
deadline.check();
|
|
74
|
+
if (!count) {
|
|
75
|
+
if (received < length)
|
|
76
|
+
zipFormat("truncated staged record");
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
received += count;
|
|
80
|
+
}
|
|
81
|
+
const filled = buffer.subarray(0, received);
|
|
82
|
+
bytes = filled.subarray(0, length);
|
|
83
|
+
if (cacheable) {
|
|
84
|
+
windows.unshift({ offset, bytes: filled });
|
|
85
|
+
if (windows.length > 2)
|
|
86
|
+
windows.pop();
|
|
87
|
+
}
|
|
52
88
|
}
|
|
53
89
|
step = scan.next(bytes);
|
|
54
90
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ZipArchiveWithFiles = {
|
|
2
|
+
files: Record<string, unknown>;
|
|
3
|
+
};
|
|
4
|
+
/** Internal: the caller has admitted these unchanged bytes and their physical count. */
|
|
5
|
+
export declare function loadAdmittedZipArchive(buffer: Buffer | Uint8Array, entryCount: number): Promise<ZipArchiveWithFiles>;
|
|
6
|
+
//# sourceMappingURL=archive-zip-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-zip-loader.d.ts","sourceRoot":"","sources":["../src/archive-zip-loader.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAMF,wFAAwF;AACxF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,mBAAmB,CAAC,CAoB9B"}
|