@openclaw/fs-safe 0.8.6 → 0.10.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 +93 -0
- package/README.md +17 -0
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -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 +24 -0
- package/dist/archive-durability.d.ts.map +1 -0
- package/dist/archive-durability.js +180 -0
- package/dist/archive-gzip-tail.d.ts +2 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +22 -4
- package/dist/archive-input.js +4 -4
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +3 -2
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +42 -9
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +4 -2
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +72 -70
- package/dist/archive-staging.d.ts +1 -1
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +24 -19
- 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 +20 -8
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +2 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/bounded-read.d.ts +7 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +73 -43
- 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 +23 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +91 -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 +15 -0
- package/dist/copy-publication.d.ts.map +1 -0
- package/dist/copy-publication.js +30 -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 +191 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +123 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +4 -4
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-hash.d.ts +6 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +49 -12
- package/dist/file-store-sync-write.d.ts +1 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +9 -6
- package/dist/file-store.d.ts +4 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +10 -1
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -12
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +1 -2
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +1 -0
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +9 -5
- 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 +9 -13
- package/dist/json-document-store.d.ts +3 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +1 -0
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.js +3 -2
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +31 -27
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +22 -24
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +4 -3
- 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 +55 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +55 -47
- package/dist/mutation-authority.d.ts +8 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +24 -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 +18 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +1 -0
- 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-file-failure.d.ts +1 -1
- package/dist/opened-file-failure.d.ts.map +1 -1
- package/dist/opened-file-failure.js +3 -2
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +38 -48
- package/dist/permissions.js +3 -3
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +5 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +50 -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 +9 -3
- 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 +26 -25
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +30 -17
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -5
- 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.map +1 -1
- package/dist/replace-file.js +8 -3
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +51 -26
- package/dist/root-directory-list.d.ts +24 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +201 -0
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +3 -0
- 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 +6 -50
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +342 -208
- package/dist/root-options.d.ts +66 -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 +56 -1
- 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-symlink-policy.d.ts +13 -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 +1 -4
- package/dist/root.d.ts +3 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +1 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secret-read-async.js +5 -5
- package/dist/secure-file.d.ts +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +23 -13
- package/dist/sibling-staged-file.js +7 -7
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +16 -7
- package/dist/sidecar-lock-acquire.d.ts +1 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +6 -4
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +5 -3
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +16 -4
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +9 -4
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.js +2 -2
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +7 -27
- 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 +6 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +25 -0
- package/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -2
- package/docs/advanced.md +18 -4
- package/docs/archive.md +57 -9
- package/docs/atomic.md +7 -0
- package/docs/contributing.md +7 -0
- package/docs/copy.md +86 -0
- package/docs/durability.md +17 -2
- package/docs/errors.md +1 -1
- package/docs/file-store.md +48 -3
- package/docs/json-store.md +10 -0
- package/docs/local-roots.md +8 -1
- package/docs/native.md +14 -1
- package/docs/permissions.md +20 -10
- package/docs/positional-read.md +63 -0
- package/docs/reading.md +6 -0
- package/docs/root.md +168 -6
- package/docs/secret-file.md +6 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +2 -0
- package/docs/timing.md +2 -0
- package/docs/types.md +18 -11
- package/docs/walk.md +54 -5
- package/docs/writing.md +25 -5
- package/package.json +16 -11
package/dist/archive-merge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { ownExtractionDestinationMutation } from "./archive-deadline.js";
|
|
@@ -10,13 +11,18 @@ import { formatErrorDetail } from "./error-detail.js";
|
|
|
10
11
|
import { isPathInside } from "./path.js";
|
|
11
12
|
import { root } from "./root.js";
|
|
12
13
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
14
|
+
import { onCopyPublication } from "./copy-publication.js";
|
|
15
|
+
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
16
|
+
import { finalizeArchivePublication } from "./archive-durability.js";
|
|
13
17
|
export async function mergePlannedArchiveIntoDestination(params) {
|
|
14
|
-
await mergeTree(params, params.entries);
|
|
18
|
+
await mergeTree(params, params.entries, params.durable === true);
|
|
15
19
|
}
|
|
16
20
|
export async function mergeExtractedTreeIntoDestination(params) {
|
|
17
21
|
await mergeTree(params);
|
|
18
22
|
}
|
|
19
|
-
async function mergeTree(params, publication) {
|
|
23
|
+
async function mergeTree(params, publication, durable = true) {
|
|
24
|
+
const publishedFiles = [];
|
|
25
|
+
const publishedDirectories = [];
|
|
20
26
|
const check = () => params.deadline?.check();
|
|
21
27
|
check();
|
|
22
28
|
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
|
|
@@ -29,7 +35,7 @@ async function mergeTree(params, publication) {
|
|
|
29
35
|
for (const entry of publication ?? []) {
|
|
30
36
|
// Resolve admitted spelling in private staging, preserving the volume's case
|
|
31
37
|
// and Unicode behavior without assigning explicit modes to distinct parents.
|
|
32
|
-
const stagedPath =
|
|
38
|
+
const stagedPath = fsSync.realpathSync.native(path.join(params.sourceDir, entry.path));
|
|
33
39
|
check();
|
|
34
40
|
if (!isPathInside(sourceGuard.realPath, stagedPath) || plan.has(stagedPath)) {
|
|
35
41
|
throw new FsSafeError("path-mismatch", "archive publication paths changed in staging");
|
|
@@ -59,11 +65,11 @@ async function mergeTree(params, publication) {
|
|
|
59
65
|
const relPath = path.relative(params.sourceDir, sourcePath);
|
|
60
66
|
const originalPath = relPath.split(path.sep).join("/");
|
|
61
67
|
const destinationPath = path.join(params.destinationDir, relPath);
|
|
62
|
-
const sourceStat =
|
|
68
|
+
const sourceStat = fsSync.lstatSync(sourcePath);
|
|
63
69
|
check();
|
|
64
70
|
if (sourceStat.isSymbolicLink())
|
|
65
71
|
throw createArchiveSymlinkTraversalError(originalPath);
|
|
66
|
-
const sourceReal =
|
|
72
|
+
const sourceReal = fsSync.realpathSync.native(sourcePath);
|
|
67
73
|
check();
|
|
68
74
|
if (!isPathInside(sourceGuard.realPath, sourceReal))
|
|
69
75
|
throw createArchiveSymlinkTraversalError(originalPath);
|
|
@@ -78,7 +84,7 @@ async function mergeTree(params, publication) {
|
|
|
78
84
|
const mode = plan ? planned?.mode ?? 0o755 : sourceStat.mode & 0o777;
|
|
79
85
|
await preparePrivateArchiveOutputPath({
|
|
80
86
|
...params, relPath, outPath: destinationPath, originalPath, isDirectory: kind === "directory",
|
|
81
|
-
}, assertGuards);
|
|
87
|
+
}, assertGuards, destinationGuard);
|
|
82
88
|
check();
|
|
83
89
|
if (kind === "directory") {
|
|
84
90
|
// Ownership spans open, descendants, finalization and close, including timeout.
|
|
@@ -98,6 +104,11 @@ async function mergeTree(params, publication) {
|
|
|
98
104
|
ancestors.push({ guard, owner });
|
|
99
105
|
try {
|
|
100
106
|
await walk(sourcePath);
|
|
107
|
+
if (publication) {
|
|
108
|
+
await assertGuards();
|
|
109
|
+
publishedDirectories.push({ guard, mode, parents: ancestors.slice(0, -1).map((ancestor) => ancestor.guard) });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
101
112
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("chmod", destinationPath);
|
|
102
113
|
check();
|
|
103
114
|
await assertGuards();
|
|
@@ -129,21 +140,39 @@ async function mergeTree(params, publication) {
|
|
|
129
140
|
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
130
141
|
await assertGuards();
|
|
131
142
|
try {
|
|
132
|
-
|
|
143
|
+
const options = {
|
|
144
|
+
mkdir: true, mode, durable: publication ? false : true,
|
|
145
|
+
};
|
|
146
|
+
if (publication && durable) {
|
|
147
|
+
options[onCopyPublication] = async (fd, identity) => {
|
|
148
|
+
check();
|
|
149
|
+
if ((mode & 0o400) === 0) {
|
|
150
|
+
// Final mode may prohibit reopening. Sync the writer's still-owned
|
|
151
|
+
// descriptor without widening permissions or syncing its parent.
|
|
152
|
+
syncFileBestEffortSync(fd);
|
|
153
|
+
check();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
publishedFiles.push({ relativePath: relPath, identity,
|
|
157
|
+
guards: ancestors.map((ancestor) => ancestor.guard) });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
await targetRoot.copyIn(relPath, sourcePath, options);
|
|
133
162
|
check();
|
|
134
163
|
await assertGuards();
|
|
135
164
|
await assertResolvedInsideDestination({
|
|
136
165
|
destinationRealDir: params.destinationRealDir, targetPath: destinationPath, originalPath,
|
|
137
166
|
});
|
|
138
167
|
check();
|
|
139
|
-
const stat =
|
|
168
|
+
const stat = fsSync.lstatSync(destinationPath);
|
|
140
169
|
check();
|
|
141
170
|
if (stat.isSymbolicLink() || (stat.isFile() && stat.nlink > 1)) {
|
|
142
171
|
throw createArchiveSymlinkTraversalError(originalPath);
|
|
143
172
|
}
|
|
144
173
|
}
|
|
145
174
|
catch (error) {
|
|
146
|
-
// copyIn
|
|
175
|
+
// copyIn cleans unpublished stages; merge never gains rollback authority.
|
|
147
176
|
if (error instanceof FsSafeError && (error.code === "hardlink" || error.code === "path-alias")) {
|
|
148
177
|
throw createArchiveSymlinkTraversalError(originalPath);
|
|
149
178
|
}
|
|
@@ -154,4 +183,8 @@ async function mergeTree(params, publication) {
|
|
|
154
183
|
}
|
|
155
184
|
};
|
|
156
185
|
await walk(params.sourceDir);
|
|
186
|
+
if (publication) {
|
|
187
|
+
await finalizeArchivePublication({ targetRoot, destinationGuard, sourceGuard,
|
|
188
|
+
files: publishedFiles, directories: publishedDirectories, durable, deadline: params.deadline });
|
|
189
|
+
}
|
|
157
190
|
}
|
package/dist/archive-native.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ExtractArchiveOptions } from "./archive-options.js";
|
|
|
5
5
|
import type { NativeBinding } from "./native.js";
|
|
6
6
|
export declare function throwMappedNativeArchiveError(error: unknown): never;
|
|
7
7
|
export declare function extractNativeArchive(params: {
|
|
8
|
+
durable?: boolean;
|
|
8
9
|
binding: NativeBinding;
|
|
9
10
|
archivePath: string;
|
|
10
11
|
destDir: string;
|
|
@@ -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,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
|
@@ -40,8 +40,9 @@ export async function extractNativeArchive(params) {
|
|
|
40
40
|
destinationRealDir,
|
|
41
41
|
run: async (stagingDir) => {
|
|
42
42
|
params.deadline.check();
|
|
43
|
+
// N-API retains completed task state on its signal; each pass needs its own.
|
|
43
44
|
const manifest = await params.binding
|
|
44
|
-
.inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, params.deadline.signal)
|
|
45
|
+
.inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, AbortSignal.any([params.deadline.signal]))
|
|
45
46
|
.catch(throwMappedNativeArchiveError);
|
|
46
47
|
params.deadline.check();
|
|
47
48
|
assertArchiveEntryCountWithinLimit(manifest.length, limits);
|
|
@@ -84,7 +85,7 @@ export async function extractNativeArchive(params) {
|
|
|
84
85
|
(typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0));
|
|
85
86
|
try {
|
|
86
87
|
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);
|
|
88
|
+
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
89
|
}
|
|
89
90
|
finally {
|
|
90
91
|
await directory.close().catch(() => undefined);
|
|
@@ -92,6 +93,7 @@ export async function extractNativeArchive(params) {
|
|
|
92
93
|
params.deadline.check();
|
|
93
94
|
await mergePlannedArchiveIntoDestination({
|
|
94
95
|
entries: plan,
|
|
96
|
+
durable: params.durable,
|
|
95
97
|
sourceDir: stagingDir,
|
|
96
98
|
destinationDir: params.destDir,
|
|
97
99
|
destinationRealDir,
|
|
@@ -9,6 +9,8 @@ export type ExtractArchiveOptions = {
|
|
|
9
9
|
archivePath: string;
|
|
10
10
|
destDir: string;
|
|
11
11
|
timeoutMs: number;
|
|
12
|
+
/** Sync published files and directories before returning. Defaults to false. */
|
|
13
|
+
durable?: boolean;
|
|
12
14
|
kind?: ArchiveKind;
|
|
13
15
|
stripComponents?: number;
|
|
14
16
|
tarGzip?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-options.d.ts","sourceRoot":"","sources":["../src/archive-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,CAAC"}
|
|
1
|
+
{"version":3,"file":"archive-options.d.ts","sourceRoot":"","sources":["../src/archive-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,CAAC"}
|
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":"AAeA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiOpG,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,7 +1,8 @@
|
|
|
1
1
|
import { classifyArchiveParserError } from "./archive-parser-errors.js";
|
|
2
|
+
import fsSync from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import { Readable } from "node:stream";
|
|
4
|
-
import {
|
|
5
|
+
import { readBoundedAsync } from "./bounded-read.js";
|
|
5
6
|
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
6
7
|
import { formatErrorDetail } from "./error-detail.js";
|
|
7
8
|
import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
@@ -16,7 +17,6 @@ import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
|
16
17
|
import { getNativeBinding } from "./native.js";
|
|
17
18
|
import { admitZipBuffer } from "./archive-zip-admission.js";
|
|
18
19
|
import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
|
|
19
|
-
import { tempFile } from "./temp-target.js";
|
|
20
20
|
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
|
21
21
|
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
|
22
22
|
function canonicalEntryPath(entryPath) {
|
|
@@ -62,37 +62,37 @@ async function readStreamBounded(stream, maxBytes) {
|
|
|
62
62
|
}
|
|
63
63
|
return Buffer.concat(chunks, total);
|
|
64
64
|
}
|
|
65
|
-
async function
|
|
66
|
-
const resolved =
|
|
65
|
+
async function readArchiveInput(archivePath) {
|
|
66
|
+
const resolved = fsSync.realpathSync.native(archivePath);
|
|
67
67
|
const before = await inspectFileIdentity(async () => {
|
|
68
|
-
const stat =
|
|
68
|
+
const stat = fsSync.lstatSync(archivePath, { bigint: true });
|
|
69
69
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
70
70
|
throw new Error(`archive is not a regular file: ${archivePath}`);
|
|
71
71
|
}
|
|
72
72
|
return stat;
|
|
73
73
|
});
|
|
74
74
|
const handle = await fs.open(resolved, resolveReadOpenFlags());
|
|
75
|
-
let staged;
|
|
76
75
|
try {
|
|
77
|
-
staged = await tempFile({ prefix: "fs-safe-archive-read", fileName: "archive.bin" });
|
|
78
76
|
const opened = await inspectFileIdentity(async () => {
|
|
79
|
-
const stat =
|
|
77
|
+
const stat = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
80
78
|
if (!stat.isFile())
|
|
81
79
|
throw new Error("archive changed during validation");
|
|
82
80
|
return stat;
|
|
83
81
|
}, before);
|
|
84
82
|
await inspectFileIdentity(async () => {
|
|
85
|
-
const stat =
|
|
83
|
+
const stat = fsSync.lstatSync(resolved, { bigint: true });
|
|
86
84
|
if (stat.isSymbolicLink() || !stat.isFile())
|
|
87
85
|
throw new Error("archive changed during validation");
|
|
88
86
|
return stat;
|
|
89
87
|
}, opened);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
88
|
+
// Native archive workers borrow this private buffer. A pooled allocation could
|
|
89
|
+
// share its ArrayBuffer with unrelated JS buffers while the worker runs.
|
|
90
|
+
return await readBoundedAsync(DEFAULT_MAX_ARCHIVE_BYTES_ZIP, async (buffer, length) => (await handle.read(buffer, 0, length, null)).bytesRead, { unpooled: true, observeRegularFileSize: () => {
|
|
91
|
+
const size = fsSync.fstatSync(handle.fd).size;
|
|
92
|
+
return Number.isSafeInteger(size) && size >= 0 ? size : undefined;
|
|
93
|
+
} });
|
|
93
94
|
}
|
|
94
95
|
catch (error) {
|
|
95
|
-
await staged?.cleanup().catch(() => undefined);
|
|
96
96
|
if (error instanceof FsSafeError && error.code === "path-mismatch") {
|
|
97
97
|
throw new FsSafeError("path-mismatch", "archive changed during validation", { cause: error });
|
|
98
98
|
}
|
|
@@ -133,11 +133,11 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
|
133
133
|
stream.once("error", (error) => integrity.destroy(normalizeZipIntegrityError(error)));
|
|
134
134
|
return await readStreamBounded(stream.pipe(integrity), maxBytes);
|
|
135
135
|
}
|
|
136
|
-
async function readTarEntry(
|
|
136
|
+
async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
|
|
137
137
|
const seenPaths = new Set();
|
|
138
138
|
let selected;
|
|
139
139
|
const limits = resolveTarMeterLimits();
|
|
140
|
-
await inspectTar({
|
|
140
|
+
await inspectTar({ archiveBuffer, limits, onMember(info) {
|
|
141
141
|
const normalized = canonicalEntryPath(info.path);
|
|
142
142
|
if (seenPaths.has(normalized)) {
|
|
143
143
|
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
@@ -154,11 +154,57 @@ async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
|
154
154
|
if (selected.size > maxBytes)
|
|
155
155
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
156
156
|
let result;
|
|
157
|
-
await replayTar({
|
|
157
|
+
await replayTar({ archiveBuffer, limits, members: [selected], async consume(_member, payload) {
|
|
158
158
|
result = await readStreamBounded(payload, maxBytes);
|
|
159
159
|
} });
|
|
160
160
|
return result;
|
|
161
161
|
}
|
|
162
|
+
function selectNativeEntry(manifest, requested, displayPath, physicalCount) {
|
|
163
|
+
if (physicalCount !== undefined && manifest.length !== physicalCount) {
|
|
164
|
+
throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
|
|
165
|
+
}
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
let selected;
|
|
168
|
+
for (const entry of manifest) {
|
|
169
|
+
const normalized = canonicalEntryPath(entry.path);
|
|
170
|
+
if (seen.has(normalized)) {
|
|
171
|
+
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
172
|
+
}
|
|
173
|
+
seen.add(normalized);
|
|
174
|
+
if (normalized === requested) {
|
|
175
|
+
if (entry.kind !== "file")
|
|
176
|
+
throw new Error(`archive entry is not a file: ${formatErrorDetail(displayPath)}`);
|
|
177
|
+
selected = entry;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!selected)
|
|
181
|
+
throw new Error(`archive entry not found: ${formatErrorDetail(displayPath)}`);
|
|
182
|
+
return selected;
|
|
183
|
+
}
|
|
184
|
+
function throwNativeReadError(error) {
|
|
185
|
+
if (error instanceof Error) {
|
|
186
|
+
const mapped = classifyArchiveParserError(error.message, { cause: error });
|
|
187
|
+
if (mapped)
|
|
188
|
+
throw mapped;
|
|
189
|
+
if (isArchiveFormatErrorMessage(error.message))
|
|
190
|
+
throw new ArchiveFormatError(error.message, { cause: error });
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
async function readNativeBufferEntry(native, buffer, kind, requested, displayPath, maxBytes, physicalCount) {
|
|
195
|
+
try {
|
|
196
|
+
const signal = new AbortController().signal;
|
|
197
|
+
const limits = resolveTarMeterLimits();
|
|
198
|
+
const reader = kind === "zip"
|
|
199
|
+
? await native.openZipBufferNative(buffer, limits, AbortSignal.any([signal]))
|
|
200
|
+
: await native.openTarBufferNative(buffer, kind, limits, AbortSignal.any([signal]));
|
|
201
|
+
const selected = selectNativeEntry(reader.entries, requested, displayPath, physicalCount);
|
|
202
|
+
return await reader.readEntry(selected.index, maxBytes, AbortSignal.any([signal]));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throwNativeReadError(error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
162
208
|
export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
163
209
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
164
210
|
throw new RangeError("maxBytes must be a non-negative safe integer");
|
|
@@ -168,58 +214,14 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
168
214
|
throw new Error(`unsupported archive: ${archivePath}`);
|
|
169
215
|
}
|
|
170
216
|
const requestedEntry = normalizedRequestedEntry(entryPath);
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const manifest = await native.inspectArchiveNative(staged.path, kind, limits, signal);
|
|
182
|
-
let rawEntryPath;
|
|
183
|
-
if (physicalCount !== undefined && manifest.length !== physicalCount) {
|
|
184
|
-
throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
|
|
185
|
-
}
|
|
186
|
-
const seenPaths = new Set();
|
|
187
|
-
for (const entry of manifest) {
|
|
188
|
-
const normalized = canonicalEntryPath(entry.path);
|
|
189
|
-
if (seenPaths.has(normalized)) {
|
|
190
|
-
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
191
|
-
}
|
|
192
|
-
seenPaths.add(normalized);
|
|
193
|
-
if (normalized === requestedEntry) {
|
|
194
|
-
if (entry.kind !== "file") {
|
|
195
|
-
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
196
|
-
}
|
|
197
|
-
rawEntryPath = entry.path;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (!rawEntryPath) {
|
|
201
|
-
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
202
|
-
}
|
|
203
|
-
return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, limits, signal);
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
if (error instanceof Error) {
|
|
207
|
-
const mapped = classifyArchiveParserError(error.message, { cause: error });
|
|
208
|
-
if (mapped)
|
|
209
|
-
throw mapped;
|
|
210
|
-
}
|
|
211
|
-
if (error instanceof Error && isArchiveFormatErrorMessage(error.message)) {
|
|
212
|
-
throw new ArchiveFormatError(error.message, { cause: error });
|
|
213
|
-
}
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
assertPortableArchiveKind(kind);
|
|
218
|
-
return kind === "zip"
|
|
219
|
-
? await readZipEntry(staged.buffer, requestedEntry, options.maxBytes)
|
|
220
|
-
: await readTarEntry(staged.path, requestedEntry, options.maxBytes);
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
await staged.cleanup();
|
|
224
|
-
}
|
|
217
|
+
const buffer = await readArchiveInput(archivePath);
|
|
218
|
+
const physicalCount = kind === "zip"
|
|
219
|
+
? admitZipBuffer(buffer, resolveExtractLimits())
|
|
220
|
+
: undefined;
|
|
221
|
+
const native = getNativeBinding();
|
|
222
|
+
if (native)
|
|
223
|
+
return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, physicalCount);
|
|
224
|
+
assertPortableArchiveKind(kind);
|
|
225
|
+
return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes)
|
|
226
|
+
: await readTarEntry(buffer, requestedEntry, options.maxBytes);
|
|
225
227
|
}
|
|
@@ -20,7 +20,7 @@ type ArchiveOutputPathParams = {
|
|
|
20
20
|
deadline?: ExtractionDeadline;
|
|
21
21
|
};
|
|
22
22
|
export declare function prepareArchiveOutputPath(params: ArchiveOutputPathParams): Promise<void>;
|
|
23
|
-
export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void
|
|
23
|
+
export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>, destinationGuard?: AsyncDirectoryGuard): Promise<void>;
|
|
24
24
|
export declare function withStagedArchiveDestination<T>(params: {
|
|
25
25
|
destinationRealDir: string;
|
|
26
26
|
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":"AAGA,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;AAkB7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAS1F,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAS5F;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAY5F;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;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,mBAAmB,GACrC,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,3 +1,4 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
@@ -44,16 +45,16 @@ export async function assertDirectoryIdentityGuard(guard) {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
export async function prepareArchiveDestinationDir(destDir) {
|
|
47
|
-
const stat =
|
|
48
|
+
const stat = fsSync.lstatSync(destDir);
|
|
48
49
|
if (stat.isSymbolicLink()) {
|
|
49
50
|
throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
|
|
50
51
|
}
|
|
51
52
|
if (!stat.isDirectory()) {
|
|
52
53
|
throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
|
|
53
54
|
}
|
|
54
|
-
const realPath =
|
|
55
|
-
const realStat =
|
|
56
|
-
const postStat =
|
|
55
|
+
const realPath = fsSync.realpathSync.native(destDir);
|
|
56
|
+
const realStat = fsSync.statSync(realPath);
|
|
57
|
+
const postStat = fsSync.lstatSync(destDir);
|
|
57
58
|
if (realStat.dev !== stat.dev ||
|
|
58
59
|
realStat.ino !== stat.ino ||
|
|
59
60
|
postStat.isSymbolicLink() ||
|
|
@@ -71,7 +72,7 @@ async function assertNoSymlinkTraversal(params) {
|
|
|
71
72
|
current = path.join(current, part);
|
|
72
73
|
let stat;
|
|
73
74
|
try {
|
|
74
|
-
stat =
|
|
75
|
+
stat = fsSync.lstatSync(current);
|
|
75
76
|
}
|
|
76
77
|
catch (err) {
|
|
77
78
|
if (isNotFoundPathError(err)) {
|
|
@@ -87,7 +88,7 @@ async function assertNoSymlinkTraversal(params) {
|
|
|
87
88
|
export async function assertResolvedInsideDestination(params) {
|
|
88
89
|
let resolved;
|
|
89
90
|
try {
|
|
90
|
-
resolved =
|
|
91
|
+
resolved = fsSync.realpathSync.native(params.targetPath);
|
|
91
92
|
}
|
|
92
93
|
catch (err) {
|
|
93
94
|
if (isNotFoundPathError(err)) {
|
|
@@ -113,10 +114,10 @@ async function mkdirArchiveOutput(params) {
|
|
|
113
114
|
export async function prepareArchiveOutputPath(params) {
|
|
114
115
|
await prepareOutputPath(params);
|
|
115
116
|
}
|
|
116
|
-
export async function preparePrivateArchiveOutputPath(params, assertGuards) {
|
|
117
|
-
await prepareOutputPath(params, assertGuards, true);
|
|
117
|
+
export async function preparePrivateArchiveOutputPath(params, assertGuards, destinationGuard) {
|
|
118
|
+
await prepareOutputPath(params, assertGuards, true, destinationGuard);
|
|
118
119
|
}
|
|
119
|
-
async function prepareOutputPath(params, assertGuards, privateWorkingMode = false) {
|
|
120
|
+
async function prepareOutputPath(params, assertGuards, privateWorkingMode = false, existingDestinationGuard) {
|
|
120
121
|
checkExtractionDeadline(params.deadline);
|
|
121
122
|
const targetRoot = privateWorkingMode ? {
|
|
122
123
|
async mkdir(relativePath) {
|
|
@@ -136,16 +137,24 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
|
|
|
136
137
|
mode: 0o700,
|
|
137
138
|
rejectSymlinks: true,
|
|
138
139
|
beforeComponent: async () => {
|
|
139
|
-
await
|
|
140
|
-
checkExtractionDeadline(params.deadline);
|
|
141
|
-
await assertGuards?.();
|
|
140
|
+
await assertOutputGuards();
|
|
142
141
|
checkExtractionDeadline(params.deadline);
|
|
143
142
|
},
|
|
144
143
|
});
|
|
145
144
|
},
|
|
146
145
|
} : await root(params.destinationRealDir);
|
|
147
146
|
checkExtractionDeadline(params.deadline);
|
|
148
|
-
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
|
|
147
|
+
const destinationGuard = existingDestinationGuard ?? await createDirectoryIdentityGuard(params.destinationRealDir);
|
|
148
|
+
const assertOutputGuards = async () => {
|
|
149
|
+
// The merge's callback verifies this same original destination guard.
|
|
150
|
+
if (existingDestinationGuard && assertGuards)
|
|
151
|
+
await assertGuards();
|
|
152
|
+
else {
|
|
153
|
+
await assertDirectoryIdentityGuard(destinationGuard);
|
|
154
|
+
checkExtractionDeadline(params.deadline);
|
|
155
|
+
await assertGuards?.();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
149
158
|
checkExtractionDeadline(params.deadline);
|
|
150
159
|
const relPath = params.relPath.split(path.sep).join(path.posix.sep);
|
|
151
160
|
await assertNoSymlinkTraversal({
|
|
@@ -158,9 +167,7 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
|
|
|
158
167
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", params.outPath);
|
|
159
168
|
checkExtractionDeadline(params.deadline);
|
|
160
169
|
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
161
|
-
await
|
|
162
|
-
checkExtractionDeadline(params.deadline);
|
|
163
|
-
await assertGuards?.();
|
|
170
|
+
await assertOutputGuards();
|
|
164
171
|
checkExtractionDeadline(params.deadline);
|
|
165
172
|
await mkdirArchiveOutput({
|
|
166
173
|
targetRoot,
|
|
@@ -184,9 +191,7 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
|
|
|
184
191
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", path.dirname(params.outPath));
|
|
185
192
|
checkExtractionDeadline(params.deadline);
|
|
186
193
|
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
187
|
-
await
|
|
188
|
-
checkExtractionDeadline(params.deadline);
|
|
189
|
-
await assertGuards?.();
|
|
194
|
+
await assertOutputGuards();
|
|
190
195
|
checkExtractionDeadline(params.deadline);
|
|
191
196
|
await mkdirArchiveOutput({
|
|
192
197
|
targetRoot,
|
|
@@ -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;AAEhF,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"}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { Writable } from "node:stream";
|
|
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, validateGzipContainerTail } from "./archive-gzip-tail.js";
|
|
5
|
+
import { GzipInput, 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
|
+
function* bufferChunks(buffer) {
|
|
9
|
+
for (let offset = 0; offset < buffer.length; offset += 65536)
|
|
10
|
+
yield buffer.subarray(offset, offset + 65536);
|
|
11
|
+
}
|
|
8
12
|
async function gzipFile(filePath) {
|
|
9
13
|
const handle = await fs.promises.open(filePath, "r");
|
|
10
14
|
try {
|
|
@@ -17,10 +21,14 @@ async function gzipFile(filePath) {
|
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
async function withTarStream(params, consume) {
|
|
20
|
-
const
|
|
24
|
+
const buffer = params.archiveBuffer;
|
|
25
|
+
const gzip = buffer !== undefined ? buffer[0] === 31 && buffer[1] === 139 : await gzipFile(params.archivePath);
|
|
21
26
|
const parser = new TarParserStream(params.limits, params.onMember);
|
|
22
|
-
const input =
|
|
23
|
-
|
|
27
|
+
const input = buffer !== undefined
|
|
28
|
+
? Readable.from(bufferChunks(buffer), { objectMode: false, highWaterMark: 65536 })
|
|
29
|
+
: fs.createReadStream(params.archivePath, { highWaterMark: 65536 });
|
|
30
|
+
// Match the WASM input window for buffered reads instead of emitting 16 KiB chunks.
|
|
31
|
+
const decoder = gzip ? createGunzip(buffer === undefined ? undefined : { chunkSize: 65536 }) : undefined;
|
|
24
32
|
const gzipInput = decoder ? new GzipInput(decoder) : undefined;
|
|
25
33
|
const destroy = (error) => {
|
|
26
34
|
input.destroy(error);
|
|
@@ -42,8 +50,12 @@ async function withTarStream(params, consume) {
|
|
|
42
50
|
const error = await settled;
|
|
43
51
|
if (error)
|
|
44
52
|
throw error;
|
|
45
|
-
if (gzipInput)
|
|
46
|
-
|
|
53
|
+
if (gzipInput) {
|
|
54
|
+
if (buffer !== undefined)
|
|
55
|
+
await validateGzipBufferTail(buffer, gzipInput.tailOffset, params.signal);
|
|
56
|
+
else
|
|
57
|
+
await validateGzipContainerTail(params.archivePath, gzipInput.tailOffset, params.signal);
|
|
58
|
+
}
|
|
47
59
|
return result;
|
|
48
60
|
}
|
|
49
61
|
finally {
|
|
@@ -57,7 +69,7 @@ export async function inspectTar(params) {
|
|
|
57
69
|
});
|
|
58
70
|
}
|
|
59
71
|
/** Replay in physical order, retaining at most one decoded chunk. Every range
|
|
60
|
-
* comes from complete admission of the immutable
|
|
72
|
+
* comes from complete admission of the immutable input. */
|
|
61
73
|
export async function replayTar(params) {
|
|
62
74
|
await withTarStream(params, async (parser) => {
|
|
63
75
|
const iterator = parser[Symbol.asyncIterator]();
|
|
@@ -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;
|
|
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,CAkB/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"}
|