@openclaw/fs-safe 0.5.1 → 0.5.3
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 +86 -0
- package/README.md +19 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +37 -6
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +4 -0
- package/dist/archive-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +12 -6
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +62 -37
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +15 -1
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +11 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -4
- package/dist/archive-zip-entry.d.ts +7 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +11 -1
- package/dist/archive-zip-integrity.d.ts +5 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +54 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +12 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +6 -6
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +62 -139
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -0
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +21 -4
- package/dist/pinned-write.js +2 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +45 -4
- package/dist/read-error.d.ts +2 -0
- package/dist/read-error.d.ts.map +1 -0
- package/dist/read-error.js +11 -0
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +216 -169
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +12 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +29 -6
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -1
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +9 -8
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -4
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +10 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +7 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +34 -8
- package/docs/file-store.md +14 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +21 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +42 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +15 -4
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
package/dist/archive-read.js
CHANGED
|
@@ -2,14 +2,16 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
4
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
|
-
import { ArchiveFormatError } from "./archive-errors.js";
|
|
5
|
+
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
6
|
+
import { formatErrorDetail } from "./error-detail.js";
|
|
6
7
|
import { normalizeArchiveEntryPath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
7
8
|
import { resolveArchiveKind } from "./archive-kind.js";
|
|
8
9
|
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
|
|
9
10
|
import { readTarEntryInfo } from "./archive-tar.js";
|
|
10
11
|
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
11
|
-
import { importOptionalTar } from "./archive-tar-runtime.js";
|
|
12
|
+
import { importOptionalTar, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
12
13
|
import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
14
|
+
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
13
15
|
import { FsSafeError } from "./errors.js";
|
|
14
16
|
import { sameFileIdentity } from "./file-identity.js";
|
|
15
17
|
import { getNativeBinding } from "./native.js";
|
|
@@ -20,7 +22,7 @@ function normalizedRequestedEntry(entryPath) {
|
|
|
20
22
|
validateArchiveEntryPath(entryPath, { escapeLabel: "archive root" });
|
|
21
23
|
const normalized = normalizeArchiveEntryPath(entryPath).replace(/^\.\//, "");
|
|
22
24
|
if (!normalized || normalized.endsWith("/")) {
|
|
23
|
-
throw new Error(`archive entry is not a file: ${entryPath}`);
|
|
25
|
+
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
24
26
|
}
|
|
25
27
|
return normalized;
|
|
26
28
|
}
|
|
@@ -96,16 +98,18 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
|
96
98
|
});
|
|
97
99
|
const entry = archive.files[entryPath];
|
|
98
100
|
if (!entry || entry.dir) {
|
|
99
|
-
throw new Error(`archive entry not found: ${entryPath}`);
|
|
101
|
+
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
100
102
|
}
|
|
101
103
|
if (typeof entry.unixPermissions === "number" &&
|
|
102
104
|
(entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE) {
|
|
103
|
-
throw new Error(`archive entry is a link: ${entryPath}`);
|
|
105
|
+
throw new Error(`archive entry is a link: ${formatErrorDetail(entryPath)}`);
|
|
104
106
|
}
|
|
105
107
|
const stream = typeof entry.nodeStream === "function"
|
|
106
108
|
? entry.nodeStream()
|
|
107
109
|
: Readable.from(await entry.async("nodebuffer"));
|
|
108
|
-
|
|
110
|
+
const integrity = createZipIntegrityTransform(entry);
|
|
111
|
+
stream.once("error", (error) => integrity.destroy(normalizeZipIntegrityError(error)));
|
|
112
|
+
return await readStreamBounded(stream.pipe(integrity), maxBytes);
|
|
109
113
|
}
|
|
110
114
|
async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
111
115
|
const tar = await importOptionalTar();
|
|
@@ -114,33 +118,49 @@ async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
|
114
118
|
maxMetaEntryBytes: DEFAULT_MAX_META_ENTRY_BYTES,
|
|
115
119
|
});
|
|
116
120
|
let matched;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
121
|
+
let entryError;
|
|
122
|
+
const seenPaths = new Set();
|
|
123
|
+
try {
|
|
124
|
+
await tar.t({
|
|
125
|
+
file: archivePath,
|
|
126
|
+
strict: true,
|
|
127
|
+
maxMetaEntrySize: DEFAULT_MAX_META_ENTRY_BYTES,
|
|
128
|
+
onReadEntry(entry) {
|
|
129
|
+
const info = readTarEntryInfo(entry);
|
|
130
|
+
validateArchiveEntryPath(info.path, { escapeLabel: "archive root" });
|
|
131
|
+
const normalized = normalizeArchiveEntryPath(info.path).replace(/^\.\//, "");
|
|
132
|
+
if (seenPaths.has(normalized)) {
|
|
133
|
+
entryError ??= new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
134
|
+
entry.resume();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
seenPaths.add(normalized);
|
|
138
|
+
if (normalized !== entryPath) {
|
|
139
|
+
entry.resume();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (info.type !== "File" && info.type !== "OldFile" && info.type !== "ContiguousFile") {
|
|
143
|
+
entryError ??= new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
144
|
+
entry.resume();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (info.size > maxBytes) {
|
|
148
|
+
entryError ??= new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
149
|
+
entry.resume();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
matched = readStreamBounded(entry, maxBytes);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw normalizeTarParserError(error);
|
|
158
|
+
}
|
|
159
|
+
if (entryError) {
|
|
160
|
+
throw entryError;
|
|
161
|
+
}
|
|
142
162
|
if (!matched) {
|
|
143
|
-
throw new Error(`archive entry not found: ${entryPath}`);
|
|
163
|
+
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
144
164
|
}
|
|
145
165
|
return await matched;
|
|
146
166
|
}
|
|
@@ -161,19 +181,24 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
161
181
|
const signal = new AbortController().signal;
|
|
162
182
|
const manifest = await native.inspectArchiveNative(staged.path, kind, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, signal);
|
|
163
183
|
let rawEntryPath;
|
|
184
|
+
const seenPaths = new Set();
|
|
164
185
|
for (const entry of manifest) {
|
|
165
186
|
validateArchiveEntryPath(entry.path, { escapeLabel: "archive root" });
|
|
166
187
|
const normalized = normalizeArchiveEntryPath(entry.path).replace(/^\.\//, "");
|
|
188
|
+
if (seenPaths.has(normalized)) {
|
|
189
|
+
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
190
|
+
}
|
|
191
|
+
seenPaths.add(normalized);
|
|
167
192
|
if (normalized === requestedEntry) {
|
|
168
193
|
if (entry.kind !== "file") {
|
|
169
|
-
throw new Error(`archive entry is not a file: ${entryPath}`);
|
|
194
|
+
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
170
195
|
}
|
|
171
196
|
rawEntryPath = entry.path;
|
|
172
|
-
break;
|
|
173
197
|
}
|
|
174
198
|
}
|
|
175
|
-
if (!rawEntryPath)
|
|
176
|
-
throw new Error(`archive entry not found: ${entryPath}`);
|
|
199
|
+
if (!rawEntryPath) {
|
|
200
|
+
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
201
|
+
}
|
|
177
202
|
return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, signal);
|
|
178
203
|
}
|
|
179
204
|
catch (error) {
|
|
@@ -181,7 +206,7 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
181
206
|
error.message.includes(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT)) {
|
|
182
207
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
183
208
|
}
|
|
184
|
-
if (error instanceof Error && error.message
|
|
209
|
+
if (error instanceof Error && isArchiveFormatErrorMessage(error.message)) {
|
|
185
210
|
throw new ArchiveFormatError(error.message, { cause: error });
|
|
186
211
|
}
|
|
187
212
|
throw error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAW7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAkC1F,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA4DD,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,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;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuChB;AAyHD,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,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAkFhB;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
|
package/dist/archive-staging.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
5
5
|
import { ArchiveSecurityError, } from "./archive-errors.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { formatErrorDetail } from "./error-detail.js";
|
|
7
8
|
import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
|
|
8
9
|
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
9
10
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
@@ -12,7 +13,7 @@ const ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK = "archive entry traverses symlink i
|
|
|
12
13
|
const ARCHIVE_STAGING_MODE = 0o700;
|
|
13
14
|
export { ArchiveSecurityError } from "./archive-errors.js";
|
|
14
15
|
function symlinkTraversalError(originalPath) {
|
|
15
|
-
return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${originalPath}`);
|
|
16
|
+
return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${formatErrorDetail(originalPath)}`);
|
|
16
17
|
}
|
|
17
18
|
async function createDirectoryIdentityGuard(dir) {
|
|
18
19
|
try {
|
|
@@ -306,7 +307,7 @@ export async function mergeExtractedTreeIntoDestination(params) {
|
|
|
306
307
|
continue;
|
|
307
308
|
}
|
|
308
309
|
if (!sourceStat.isFile()) {
|
|
309
|
-
throw new Error(`archive staging contains unsupported entry: ${originalPath}`);
|
|
310
|
+
throw new Error(`archive staging contains unsupported entry: ${formatErrorDetail(originalPath)}`);
|
|
310
311
|
}
|
|
311
312
|
await prepareArchiveOutputPath({
|
|
312
313
|
destinationDir: params.destinationDir,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AAqJA,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
|
package/dist/archive-tar-meta.js
CHANGED
|
@@ -19,7 +19,13 @@ class TarMetadataMeter extends Transform {
|
|
|
19
19
|
parseSize() {
|
|
20
20
|
const field = this.block.subarray(124, 136);
|
|
21
21
|
if ((field[0] ?? 0) & 0x80) {
|
|
22
|
-
|
|
22
|
+
if (field[0] !== 0x80) {
|
|
23
|
+
throw this.invalid("base-256 size is negative or malformed");
|
|
24
|
+
}
|
|
25
|
+
let value = 0n;
|
|
26
|
+
for (const byte of field.subarray(1)) {
|
|
27
|
+
value = (value << 8n) | BigInt(byte);
|
|
28
|
+
}
|
|
23
29
|
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
24
30
|
throw this.invalid("base-256 size exceeds the safe integer range");
|
|
25
31
|
}
|
|
@@ -40,6 +46,14 @@ class TarMetadataMeter extends Transform {
|
|
|
40
46
|
this.state = { kind: "header" };
|
|
41
47
|
return;
|
|
42
48
|
}
|
|
49
|
+
const nameEnd = this.block.subarray(0, 100).indexOf(0);
|
|
50
|
+
const name = this.block.subarray(0, nameEnd < 0 ? 100 : nameEnd);
|
|
51
|
+
if (name.length === 0) {
|
|
52
|
+
throw this.invalid("entry path is empty");
|
|
53
|
+
}
|
|
54
|
+
if (this.block[156] !== 0x35 && name.at(-1) === 0x2f) {
|
|
55
|
+
throw this.invalid("non-directory entry path ends with a separator");
|
|
56
|
+
}
|
|
43
57
|
const size = this.parseSize();
|
|
44
58
|
const type = this.block[156];
|
|
45
59
|
if ([0x78, 0x67, 0x4c, 0x4b, 0x58].includes(type ?? -1) && size > this.maxMetaEntryBytes) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,GAAG;IAC9C,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IAChF,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;IACnE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,SAAS,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,KAAK,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAC;IAC/E,CAAC,CAAC,OAAO,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,aAAa,EAAE,KAAK,CAAC;QACrB,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QACrE,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;KAClD,GAAG,SAAS,CAAC;IACd,CAAC,CAAC,OAAO,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG;YAAE,MAAM,IAAI,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KACvE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAS5D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS/D"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
1
2
|
export async function importOptionalTar() {
|
|
2
3
|
try {
|
|
3
4
|
return await import("tar");
|
|
@@ -6,3 +7,13 @@ export async function importOptionalTar() {
|
|
|
6
7
|
throw new Error('Optional archive dependency "tar" is not installed. Install it to use TAR archive helpers from @openclaw/fs-safe/archive.', { cause });
|
|
7
8
|
}
|
|
8
9
|
}
|
|
10
|
+
export function normalizeTarParserError(error) {
|
|
11
|
+
const code = error?.code;
|
|
12
|
+
if (typeof code !== "string" || !code.startsWith("TAR_")) {
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
+
return new ArchiveFormatError(`invalid TAR archive: ${message}`, {
|
|
17
|
+
cause: error instanceof Error ? error : undefined,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAWvF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAyB7D;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAgDnC"}
|
package/dist/archive-tar.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
1
|
+
import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
2
2
|
import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, resolveExtractLimits, } from "./archive-limits.js";
|
|
3
3
|
import { archiveEntryKindFromTarType, shouldExtractArchiveEntry, } from "./archive-policy.js";
|
|
4
4
|
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
5
|
+
import { formatErrorDetail } from "./error-detail.js";
|
|
5
6
|
const BLOCKED_TAR_ENTRY_TYPES = new Set([
|
|
6
7
|
"SymbolicLink",
|
|
7
8
|
"Link",
|
|
@@ -37,7 +38,10 @@ export function createTarEntryPreflightChecker(params) {
|
|
|
37
38
|
const limits = resolveExtractLimits(params.limits);
|
|
38
39
|
let entryCount = 0;
|
|
39
40
|
const budget = createByteBudgetTracker(limits);
|
|
41
|
+
const trackOutputPath = createArchiveOutputPathTracker();
|
|
40
42
|
return (entry) => {
|
|
43
|
+
entryCount += 1;
|
|
44
|
+
assertArchiveEntryCountWithinLimit(entryCount, limits);
|
|
41
45
|
validateArchiveEntryPath(entry.path, { escapeLabel: params.escapeLabel });
|
|
42
46
|
const relPath = stripArchivePath(entry.path, strip);
|
|
43
47
|
if (!relPath) {
|
|
@@ -45,14 +49,13 @@ export function createTarEntryPreflightChecker(params) {
|
|
|
45
49
|
}
|
|
46
50
|
validateArchiveEntryPath(relPath, { escapeLabel: params.escapeLabel });
|
|
47
51
|
assertArchiveEntryPathComponentsWithinLimit(relPath, limits);
|
|
52
|
+
trackOutputPath(relPath, entry.path);
|
|
48
53
|
resolveArchiveOutputPath({
|
|
49
54
|
rootDir: params.rootDir,
|
|
50
55
|
relPath,
|
|
51
56
|
originalPath: entry.path,
|
|
52
57
|
escapeLabel: params.escapeLabel,
|
|
53
58
|
});
|
|
54
|
-
entryCount += 1;
|
|
55
|
-
assertArchiveEntryCountWithinLimit(entryCount, limits);
|
|
56
59
|
const kind = archiveEntryKindFromTarType(entry.type);
|
|
57
60
|
if (!shouldExtractArchiveEntry({
|
|
58
61
|
filter: params.entryFilter,
|
|
@@ -62,8 +65,9 @@ export function createTarEntryPreflightChecker(params) {
|
|
|
62
65
|
return false;
|
|
63
66
|
}
|
|
64
67
|
if (BLOCKED_TAR_ENTRY_TYPES.has(entry.type)) {
|
|
65
|
-
throw new ArchiveSecurityError("entry-link", `tar entry is a link: ${entry.path}`);
|
|
68
|
+
throw new ArchiveSecurityError("entry-link", `tar entry is a link: ${formatErrorDetail(entry.path)}`);
|
|
66
69
|
}
|
|
70
|
+
budget.startEntry();
|
|
67
71
|
budget.addEntrySize(entry.size);
|
|
68
72
|
return true;
|
|
69
73
|
};
|
|
@@ -4,11 +4,17 @@ export type ZipEntry = {
|
|
|
4
4
|
dir: boolean;
|
|
5
5
|
unixPermissions?: number;
|
|
6
6
|
_data?: {
|
|
7
|
+
crc32?: number;
|
|
7
8
|
uncompressedSize?: number;
|
|
8
|
-
}
|
|
9
|
+
} | PromiseLike<unknown>;
|
|
9
10
|
nodeStream?: () => NodeJS.ReadableStream;
|
|
10
11
|
async: (type: "nodebuffer") => Promise<Buffer>;
|
|
11
12
|
};
|
|
13
|
+
export declare function zipEntryIntegrityMetadata(entry: ZipEntry): {
|
|
14
|
+
crc32?: number;
|
|
15
|
+
uncompressedSize?: number;
|
|
16
|
+
} | undefined;
|
|
17
|
+
export declare function hasDeferredEmptyZipData(entry: ZipEntry): boolean;
|
|
12
18
|
export declare function isZipSymlinkEntry(entry: ZipEntry): boolean;
|
|
13
19
|
export declare function zipEntryMode(entry: ZipEntry, policy: ArchiveEntryModePolicy | undefined): number;
|
|
14
20
|
export declare function zipEntryDeclaredSize(entry: ZipEntry): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-zip-entry.d.ts","sourceRoot":"","sources":["../src/archive-zip-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-zip-entry.d.ts","sourceRoot":"","sources":["../src/archive-zip-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7E,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,cAAc,CAAC;IACzC,KAAK,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAChD,CAAC;AAEF,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,GACd;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAI3D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAGhE;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAK1D;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,sBAAsB,GAAG,SAAS,GACzC,MAAM,CAMR;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAE5D"}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { resolveArchiveEntryMode, } from "./archive-policy.js";
|
|
2
|
+
export function zipEntryIntegrityMetadata(entry) {
|
|
3
|
+
const data = entry._data;
|
|
4
|
+
if (!data || "then" in data)
|
|
5
|
+
return undefined;
|
|
6
|
+
return data;
|
|
7
|
+
}
|
|
8
|
+
export function hasDeferredEmptyZipData(entry) {
|
|
9
|
+
const data = entry._data;
|
|
10
|
+
return Boolean(data && "then" in data);
|
|
11
|
+
}
|
|
2
12
|
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
|
3
13
|
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
|
4
14
|
export function isZipSymlinkEntry(entry) {
|
|
@@ -13,5 +23,5 @@ export function zipEntryMode(entry, policy) {
|
|
|
13
23
|
});
|
|
14
24
|
}
|
|
15
25
|
export function zipEntryDeclaredSize(entry) {
|
|
16
|
-
return Math.max(0, Math.floor(entry
|
|
26
|
+
return Math.max(0, Math.floor(zipEntryIntegrityMetadata(entry)?.uncompressedSize ?? 0));
|
|
17
27
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
import { type ZipEntry } from "./archive-zip-entry.js";
|
|
3
|
+
export declare function normalizeZipIntegrityError(error: unknown): Error;
|
|
4
|
+
export declare function createZipIntegrityTransform(entry: ZipEntry): Transform;
|
|
5
|
+
//# sourceMappingURL=archive-zip-integrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-zip-integrity.d.ts","sourceRoot":"","sources":["../src/archive-zip-integrity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,wBAAwB,CAAC;AAkBhC,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAQhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAgCtE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
3
|
+
import { hasDeferredEmptyZipData, zipEntryIntegrityMetadata, } from "./archive-zip-entry.js";
|
|
4
|
+
const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
|
|
5
|
+
let value = index;
|
|
6
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
7
|
+
value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
8
|
+
}
|
|
9
|
+
return value >>> 0;
|
|
10
|
+
});
|
|
11
|
+
function updateCrc32(previous, buffer) {
|
|
12
|
+
let crc = previous ^ -1;
|
|
13
|
+
for (const byte of buffer) {
|
|
14
|
+
crc = (crc >>> 8) ^ (CRC32_TABLE[(crc ^ byte) & 0xff] ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return (crc ^ -1) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
export function normalizeZipIntegrityError(error) {
|
|
19
|
+
if (error instanceof Error &&
|
|
20
|
+
error.message.includes("uncompressed data size mismatch")) {
|
|
21
|
+
return new ArchiveFormatError(`invalid ZIP entry data: ${error.message}`, { cause: error });
|
|
22
|
+
}
|
|
23
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
24
|
+
}
|
|
25
|
+
export function createZipIntegrityTransform(entry) {
|
|
26
|
+
const metadata = zipEntryIntegrityMetadata(entry);
|
|
27
|
+
const deferredEmpty = hasDeferredEmptyZipData(entry);
|
|
28
|
+
const expectedCrc32 = deferredEmpty ? 0 : metadata?.crc32;
|
|
29
|
+
const expectedSize = deferredEmpty ? 0 : metadata?.uncompressedSize;
|
|
30
|
+
if (typeof expectedCrc32 !== "number" ||
|
|
31
|
+
!Number.isInteger(expectedCrc32) ||
|
|
32
|
+
typeof expectedSize !== "number" ||
|
|
33
|
+
!Number.isSafeInteger(expectedSize) ||
|
|
34
|
+
expectedSize < 0) {
|
|
35
|
+
throw new ArchiveFormatError(`zip entry has invalid integrity metadata: ${entry.name}`);
|
|
36
|
+
}
|
|
37
|
+
let actualCrc32 = 0;
|
|
38
|
+
let actualSize = 0;
|
|
39
|
+
return new Transform({
|
|
40
|
+
transform(chunk, _encoding, callback) {
|
|
41
|
+
const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
|
|
42
|
+
actualCrc32 = updateCrc32(actualCrc32, buffer);
|
|
43
|
+
actualSize += buffer.byteLength;
|
|
44
|
+
callback(null, buffer);
|
|
45
|
+
},
|
|
46
|
+
flush(callback) {
|
|
47
|
+
if (actualSize !== expectedSize || actualCrc32 !== (expectedCrc32 >>> 0)) {
|
|
48
|
+
callback(new ArchiveFormatError(`zip entry integrity check failed: ${entry.name}`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
callback();
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-zip-preflight.d.ts","sourceRoot":"","sources":["../src/archive-zip-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-zip-preflight.d.ts","sourceRoot":"","sources":["../src/archive-zip-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAiLF,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAU5F;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,MAAM,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CA0B9B"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, resolveExtractLimits, } from "./archive-limits.js";
|
|
2
|
+
import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
|
|
2
3
|
const ZIP_EOCD_SIGNATURE = 0x06054b50;
|
|
3
4
|
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
|
|
4
5
|
const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
|
|
@@ -148,7 +149,17 @@ export async function loadZipArchiveWithPreflight(buffer, limits) {
|
|
|
148
149
|
assertArchiveEntryCountWithinLimit(entryCount, resolvedLimits);
|
|
149
150
|
}
|
|
150
151
|
const JSZip = await importOptionalJsZip();
|
|
151
|
-
|
|
152
|
+
let archive;
|
|
153
|
+
try {
|
|
154
|
+
archive = await JSZip.loadAsync(buffer);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw new ArchiveFormatError(`invalid ZIP archive: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
|
|
158
|
+
}
|
|
159
|
+
if (entryCount !== null && Object.keys(archive.files).length !== entryCount) {
|
|
160
|
+
throw new ArchiveSecurityError("entry-path", "zip archive contains duplicate or colliding entry names");
|
|
161
|
+
}
|
|
162
|
+
return archive;
|
|
152
163
|
}
|
|
153
164
|
async function importOptionalJsZip() {
|
|
154
165
|
try {
|
package/dist/archive.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA6DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAoNpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkKjF"}
|
package/dist/archive.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
|
-
import { resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
6
|
+
import { createArchiveOutputPathTracker, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
7
7
|
import { createPipelineTimeoutError, waitForDeadline, withExtractionDeadline, } from "./archive-deadline.js";
|
|
8
8
|
import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, } from "./archive-limits.js";
|
|
9
9
|
import { resolveArchiveKind } from "./archive-kind.js";
|
|
@@ -11,13 +11,14 @@ import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepar
|
|
|
11
11
|
import { createTarEntryPreflightChecker, readTarEntryInfo, } from "./archive-tar.js";
|
|
12
12
|
import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
13
13
|
import { isZipSymlinkEntry, zipEntryDeclaredSize, zipEntryMode, } from "./archive-zip-entry.js";
|
|
14
|
+
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
14
15
|
import { FsSafeError } from "./errors.js";
|
|
15
16
|
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
16
17
|
import { extractNativeArchive } from "./archive-native.js";
|
|
17
18
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
18
19
|
import { getNativeBinding } from "./native.js";
|
|
19
20
|
import { resolveArchiveEntryMode, shouldExtractArchiveEntry, } from "./archive-policy.js";
|
|
20
|
-
import { importOptionalTar } from "./archive-tar-runtime.js";
|
|
21
|
+
import { importOptionalTar, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
21
22
|
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
22
23
|
import { writeSiblingTempFile } from "./sibling-temp.js";
|
|
23
24
|
export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
@@ -81,10 +82,10 @@ async function writeZipFileEntry(params) {
|
|
|
81
82
|
handleClosedByStream = true;
|
|
82
83
|
});
|
|
83
84
|
try {
|
|
84
|
-
await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), writable, { signal: params.deadline.signal });
|
|
85
|
+
await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), createZipIntegrityTransform(params.entry), writable, { signal: params.deadline.signal });
|
|
85
86
|
}
|
|
86
87
|
catch (err) {
|
|
87
|
-
throw createPipelineTimeoutError(err, params.deadline);
|
|
88
|
+
throw normalizeZipIntegrityError(createPipelineTimeoutError(err, params.deadline));
|
|
88
89
|
}
|
|
89
90
|
params.deadline.check();
|
|
90
91
|
if (!handleClosedByStream) {
|
|
@@ -127,6 +128,7 @@ async function extractZip(params) {
|
|
|
127
128
|
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
|
|
128
129
|
assertArchiveEntryCountWithinLimit(entries.length, limits);
|
|
129
130
|
const budget = createByteBudgetTracker(limits);
|
|
131
|
+
const trackOutputPath = createArchiveOutputPathTracker();
|
|
130
132
|
await withStagedArchiveDestination({
|
|
131
133
|
destinationRealDir,
|
|
132
134
|
run: async (stagingDir) => {
|
|
@@ -142,6 +144,7 @@ async function extractZip(params) {
|
|
|
142
144
|
continue;
|
|
143
145
|
}
|
|
144
146
|
assertArchiveEntryPathComponentsWithinLimit(output.relPath, limits);
|
|
147
|
+
trackOutputPath(output.relPath, entry.name);
|
|
145
148
|
const isSymlink = isZipSymlinkEntry(entry);
|
|
146
149
|
const entryKind = isSymlink ? "symlink" : entry.dir ? "directory" : "file";
|
|
147
150
|
const entrySize = zipEntryDeclaredSize(entry);
|
|
@@ -263,6 +266,7 @@ export async function extractArchive(params) {
|
|
|
263
266
|
const info = readTarEntryInfo(entry);
|
|
264
267
|
const accepted = checkTarEntrySafety(info);
|
|
265
268
|
if (accepted) {
|
|
269
|
+
entry.path = normalizeArchiveEntryPath(info.path);
|
|
266
270
|
const relPath = stripArchivePath(info.path, Math.max(0, Math.floor(params.stripComponents ?? 0)));
|
|
267
271
|
if (relPath) {
|
|
268
272
|
acceptedEntries.push({
|
|
@@ -306,7 +310,7 @@ export async function extractArchive(params) {
|
|
|
306
310
|
});
|
|
307
311
|
}
|
|
308
312
|
catch (error) {
|
|
309
|
-
throw createPipelineTimeoutError(error, deadline);
|
|
313
|
+
throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
|
|
310
314
|
}
|
|
311
315
|
for (const accepted of acceptedEntries) {
|
|
312
316
|
const outputPath = resolveArchiveOutputPath({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAGlD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAwBnE;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,MAAM,EAAE;QAAE,gBAAgB,IAAI,MAAM,CAAC,cAAc,CAAA;KAAE,CAAA;CAAE,EACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAAC,cAAc,CAWvB"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { Transform } from "node:stream";
|
|
1
|
+
import { pipeline, Transform } from "node:stream";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
3
|
export function createMaxBytesTransform(maxBytes) {
|
|
4
|
+
if (maxBytes !== Number.POSITIVE_INFINITY &&
|
|
5
|
+
(!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
|
|
6
|
+
throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
|
|
7
|
+
}
|
|
4
8
|
let bytes = 0;
|
|
5
9
|
return new Transform({
|
|
6
10
|
transform(chunk, _encoding, callback) {
|
|
@@ -16,5 +20,12 @@ export function createMaxBytesTransform(maxBytes) {
|
|
|
16
20
|
}
|
|
17
21
|
export function createBoundedReadStream(opened, maxBytes) {
|
|
18
22
|
const stream = opened.handle.createReadStream();
|
|
19
|
-
|
|
23
|
+
if (maxBytes === undefined) {
|
|
24
|
+
return stream;
|
|
25
|
+
}
|
|
26
|
+
const bounded = createMaxBytesTransform(maxBytes);
|
|
27
|
+
// pipeline couples teardown in both directions: source failures reach the
|
|
28
|
+
// returned stream, and an early consumer close destroys the file stream.
|
|
29
|
+
pipeline(stream, bounded, () => undefined);
|
|
30
|
+
return bounded;
|
|
20
31
|
}
|
package/dist/deny-mutations.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deny-mutations.d.ts","sourceRoot":"","sources":["../src/deny-mutations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deny-mutations.d.ts","sourceRoot":"","sources":["../src/deny-mutations.ts"],"names":[],"mappings":"AAKA,wBAAsB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAI1F;AAMD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAqBF,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GAAG,SAAS,EACtC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,kBAAkB,GAAG,SAAS,EAC7C,UAAU,EAAE,kBAAkB,GAAG,SAAS,GACzC,kBAAkB,GAAG,SAAS,CAWhC"}
|