@xiaoso/dsh-tool-plus 0.1.0-beta.3 → 0.1.0-beta.4
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/lib/abort-D1_Z5Ut5.mjs +51 -0
- package/lib/abort-dIkZBV7Q.mjs +51 -0
- package/lib/amazon-bedrock-B0nxT06F.mjs +917 -0
- package/lib/amazon-bedrock-WQhvjHYu.mjs +917 -0
- package/lib/anthropic-CJCauvYz.mjs +3115 -0
- package/lib/anthropic-CZghe_9U.mjs +3115 -0
- package/lib/anthropic-sCMH7JID.mjs +262 -0
- package/lib/anthropic-tTntD8BD.mjs +262 -0
- package/lib/api-key-login-BzqNdM2y.mjs +157 -0
- package/lib/api-key-login-CWVwvHT1.mjs +157 -0
- package/lib/aws-credentials-CXryMobh.mjs +867 -0
- package/lib/aws-credentials-ChG79ey-.mjs +867 -0
- package/lib/azure-openai-responses-Cgmc-rbL.mjs +274 -0
- package/lib/azure-openai-responses-DuukbD0F.mjs +274 -0
- package/lib/bun-shim.mjs +81 -6
- package/lib/bundled-references-Cavaffah.mjs +55 -0
- package/lib/bundled-references-sLN0YdXU.mjs +55 -0
- package/lib/callback-server-CQ5Jo8F-.mjs +398 -0
- package/lib/callback-server-DDSrZ6l_.mjs +398 -0
- package/lib/client.js +1 -1
- package/lib/cursor-B3zg7EHj.mjs +273 -0
- package/lib/cursor-B6TUEmgP.mjs +135 -0
- package/lib/cursor-D8gvtTL3.mjs +135 -0
- package/lib/cursor-DkSX2h3H.mjs +5441 -0
- package/lib/cursor-aJrXeYCb.mjs +273 -0
- package/lib/cursor-mJQwUKXK.mjs +5441 -0
- package/lib/device-code-DBo3B5nh.mjs +51 -0
- package/lib/device-code-DIb0FU-H.mjs +51 -0
- package/lib/devin-BHZZyMhO.mjs +95 -0
- package/lib/devin-BK7DHeca.mjs +114 -0
- package/lib/devin-CfMp3cXS.mjs +95 -0
- package/lib/devin-D9zR5B_R.mjs +542 -0
- package/lib/devin-Q1yem3lp.mjs +114 -0
- package/lib/devin-vNZTNUON.mjs +542 -0
- package/lib/error-Dd8DIqs-.mjs +1449 -0
- package/lib/error-yCSG0mGC.mjs +1449 -0
- package/lib/event-stream-BbFZJy-z.mjs +151 -0
- package/lib/event-stream-loUa0s1T.mjs +151 -0
- package/lib/fs-promises-shim-Bx0fpxuk.mjs +57 -0
- package/lib/github-copilot-B1yiyVRh.mjs +104 -0
- package/lib/github-copilot-BWT3eAOF.mjs +208 -0
- package/lib/github-copilot-CYkjeTi3.mjs +208 -0
- package/lib/github-copilot-jscUZ8Hf.mjs +104 -0
- package/lib/gitlab-duo-CM7Ref1H.mjs +155 -0
- package/lib/gitlab-duo-gubO4FNw.mjs +155 -0
- package/lib/gitlab-duo-workflow-BGAF6JDw.mjs +91 -0
- package/lib/gitlab-duo-workflow-D78tdn8E.mjs +91 -0
- package/lib/google-BS4T25Pn.mjs +30 -0
- package/lib/google-TFe-7-of.mjs +30 -0
- package/lib/google-antigravity-DGmwC2KV.mjs +198 -0
- package/lib/google-antigravity-DNYhgTpA.mjs +198 -0
- package/lib/google-auth-B6gjsZvg.mjs +232 -0
- package/lib/google-auth-De7_s3eo.mjs +232 -0
- package/lib/google-gemini-cli-CWu8HvXW.mjs +211 -0
- package/lib/google-gemini-cli-Cb97bzQc.mjs +850 -0
- package/lib/google-gemini-cli-Cqds_Q5T.mjs +850 -0
- package/lib/google-gemini-cli-Dsztq0N5.mjs +211 -0
- package/lib/google-oauth-shared-CuEHaDDj.mjs +147 -0
- package/lib/google-oauth-shared-yqlyqPPC.mjs +147 -0
- package/lib/google-shared-CgKmmdO3.mjs +760 -0
- package/lib/google-shared-Dr-VG-ap.mjs +760 -0
- package/lib/google-vertex-BDN2tLaD.mjs +99 -0
- package/lib/google-vertex-DopVOQOG.mjs +99 -0
- package/lib/identity-Da1GPU9a.mjs +219 -0
- package/lib/identity-xRP7_eSR.mjs +219 -0
- package/lib/idle-iterator-Baoyn3Hp.mjs +366 -0
- package/lib/idle-iterator-DUjKQEGT.mjs +366 -0
- package/lib/index.mjs +1731 -1178
- package/lib/kimi--wUOWER5.mjs +236 -0
- package/lib/kimi-Dh0pv3WZ.mjs +236 -0
- package/lib/markit-BL3cH47U.mjs +1417 -0
- package/lib/markit-BNiNY6SE.mjs +1417 -0
- package/lib/minimax-code-DCbuA6Yk.mjs +53 -0
- package/lib/minimax-code-Uq8Qu9LW.mjs +53 -0
- package/lib/models-D_XnT6ay.mjs +104660 -0
- package/lib/models-SGASygJ3.mjs +104660 -0
- package/lib/ollama-DPzBmq89.mjs +589 -0
- package/lib/ollama-DoBz5SFm.mjs +589 -0
- package/lib/openai-codex-Bi-4S4PX.mjs +255 -0
- package/lib/openai-codex-Ch8CkT3p.mjs +255 -0
- package/lib/openai-codex-responses-CvxL_FMF.mjs +3144 -0
- package/lib/openai-codex-responses-pejZgdZo.mjs +3144 -0
- package/lib/openai-completions-CF8IyNtP.mjs +1360 -0
- package/lib/openai-completions-DIf361kF.mjs +1360 -0
- package/lib/openai-reasoning-fallback-Nj6yB2rY.mjs +286 -0
- package/lib/openai-reasoning-fallback-OdJREP1o.mjs +286 -0
- package/lib/openai-responses-BqXJInHX.mjs +867 -0
- package/lib/openai-responses-Db_a-U4-.mjs +867 -0
- package/lib/opencode-DhMolSoB.mjs +38 -0
- package/lib/opencode-Rq_Q2vAO.mjs +38 -0
- package/lib/perplexity-C4iLVMUj.mjs +194 -0
- package/lib/perplexity-Uu7Jzg5b.mjs +194 -0
- package/lib/proxy-DDqrmjjC.mjs +190 -0
- package/lib/proxy-pM_NIv-H.mjs +190 -0
- package/lib/request-debug-HJMoJncL.mjs +183 -0
- package/lib/sanitize-text-CfcXzw5X.mjs +33 -0
- package/lib/scrapers-BkFfcPHj.mjs +9198 -0
- package/lib/scrapers-CVptboMs.mjs +9198 -0
- package/lib/scrapers-nhQAvc8d.mjs +9198 -0
- package/lib/shell-snapshot-fn-env.sh +67 -43
- package/lib/src-BgpusnsB.mjs +5076 -0
- package/lib/src-lDmkSRbm.mjs +5006 -0
- package/lib/stream-BlldLkVq.mjs +10236 -0
- package/lib/stream-CRtexYLe.mjs +10236 -0
- package/lib/stream-markup-healing-DMsxBx1L.mjs +491 -0
- package/lib/stream-markup-healing-KMkiAA00.mjs +491 -0
- package/lib/turndown-DN7ByzEI.mjs +667 -0
- package/lib/types/config/settings.d.ts +9 -1
- package/lib/types/config/truncate.d.ts +9 -5
- package/lib/types/tools/bash/background.d.ts +1 -1
- package/lib/types/tools/bash/bash-executor.d.ts +4 -0
- package/lib/types/tools/bash/bash-interceptor.d.ts +10 -8
- package/lib/types/tools/bash/cd-workdir.d.ts +35 -0
- package/lib/types/tools/bash/non-interactive-env.d.ts +2 -0
- package/lib/types/tools/bash/render.d.ts +4 -3
- package/lib/types/tools/bash/sanitize-text.d.ts +5 -0
- package/lib/types/tools/bash/shell-snapshot.d.ts +4 -1
- package/lib/types/tools/bash/shell-tokenize.d.ts +60 -0
- package/lib/types/tools/bash/streaming-output.d.ts +12 -212
- package/lib/types/tools/bash/which.d.ts +20 -0
- package/lib/types/tools/bash/windows-shell.d.ts +31 -0
- package/lib/types/tools/edit/adapter/index.d.ts +25 -3
- package/lib/types/tools/edit/adapter/tools/auto-generated-guard.d.ts +7 -17
- package/lib/types/tools/glob/adapter/index.d.ts +27 -1
- package/lib/types/tools/grep/adapter/index.d.ts +29 -1
- package/lib/types/tools/omp/edit/dispatch.d.ts +43 -0
- package/lib/types/tools/omp/edit/hashline/noop-loop-guard.d.ts +3 -1
- package/lib/types/tools/omp/tools/auto-generated-guard.d.ts +20 -0
- package/lib/types/tools/omp/tools/write.d.ts +4 -4
- package/lib/types/tools/shared/auto-generated-guard.dsh.d.ts +16 -4
- package/lib/types/tools/shared/bun-shim.d.ts +4 -0
- package/lib/types/tools/shared/fs-promises-shim.d.ts +3 -1
- package/lib/types/tools/shared/omp-prompt.d.ts +13 -7
- package/lib/types/tools/shared/session-state.d.ts +9 -3
- package/lib/utils-Bw139LTz.mjs +8119 -0
- package/lib/utils-CZw4tVZQ.mjs +12315 -0
- package/lib/utils-CiGN7OHR.mjs +12315 -0
- package/lib/utils-Cp76gCyk.mjs +8119 -0
- package/lib/utils-D0GRDgAr.mjs +12315 -0
- package/lib/utils-D2YknRk4.mjs +51 -0
- package/lib/utils-DzXZDfYc.mjs +51 -0
- package/lib/vision-guard-01qSwFpf.mjs +92 -0
- package/lib/vision-guard-DzZGaOga.mjs +92 -0
- package/lib/wafer-CgtPHerf.mjs +17 -0
- package/lib/wafer-cBtrn6q9.mjs +17 -0
- package/lib/xai-oauth-Buk8XVvM.mjs +446 -0
- package/lib/xai-oauth-CMal8l1O.mjs +446 -0
- package/lib/xiaomi-CZgwvX2G.mjs +157 -0
- package/lib/xiaomi-D2qT5cAC.mjs +157 -0
- package/lib/zai-7DWMkEqM.mjs +203 -0
- package/lib/zai-C59YnomG.mjs +203 -0
- package/lib/zip-Cg9ANN_U.mjs +1316 -0
- package/lib/zip-D36ZKsP8.mjs +1316 -0
- package/package.json +1 -1
- package/presets/package.json +1 -1
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
import { installBunShim as __installBunShim } from "./bun-shim.mjs";
|
|
2
|
+
__installBunShim();
|
|
3
|
+
import { j as formatBytes } from "./src-lDmkSRbm.mjs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as zlib from "node:zlib";
|
|
6
|
+
//#region src/tools/omp/tools/tool-errors.ts
|
|
7
|
+
/**
|
|
8
|
+
* Standardized error types for tool execution.
|
|
9
|
+
*
|
|
10
|
+
* Tools should throw these instead of returning error text.
|
|
11
|
+
* The agent loop catches and renders them appropriately.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Base error for tool execution failures.
|
|
15
|
+
* Override render() for custom LLM-facing formatting.
|
|
16
|
+
*/
|
|
17
|
+
var ToolError = class extends Error {
|
|
18
|
+
context;
|
|
19
|
+
constructor(message, context) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.context = context;
|
|
22
|
+
this.name = "ToolError";
|
|
23
|
+
}
|
|
24
|
+
/** Render error for LLM consumption. Override for custom formatting. */
|
|
25
|
+
render() {
|
|
26
|
+
return this.message;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Error thrown when a tool operation is aborted (e.g., via AbortSignal).
|
|
31
|
+
*/
|
|
32
|
+
var ToolAbortError = class ToolAbortError extends Error {
|
|
33
|
+
static MESSAGE = "Operation aborted";
|
|
34
|
+
constructor(message = ToolAbortError.MESSAGE, options) {
|
|
35
|
+
super(message, options);
|
|
36
|
+
this.name = "ToolAbortError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Throw ToolAbortError if the signal is aborted.
|
|
41
|
+
* Use this instead of signal?.throwIfAborted() to get consistent error types.
|
|
42
|
+
*/
|
|
43
|
+
function throwIfAborted(signal) {
|
|
44
|
+
if (signal?.aborted) {
|
|
45
|
+
const reason = signal.reason instanceof Error ? signal.reason : void 0;
|
|
46
|
+
throw reason instanceof ToolAbortError ? reason : new ToolAbortError(void 0, { cause: signal.reason });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/tools/omp/utils/zip.ts
|
|
51
|
+
const ENCODER = new TextEncoder();
|
|
52
|
+
const UTF8_DECODER = new TextDecoder();
|
|
53
|
+
const LEGACY_NAME_DECODER = new TextDecoder("windows-1252");
|
|
54
|
+
/** Read a single ZIP entry as UTF-8 text, or `undefined` when the entry is absent. */
|
|
55
|
+
function unzipText(entries, entryPath) {
|
|
56
|
+
const data = entries[entryPath];
|
|
57
|
+
return data ? UTF8_DECODER.decode(data) : void 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Decode an in-memory ZIP archive into a `path → bytes` map of its file members
|
|
61
|
+
* (directory entries and `..`-escaping names are dropped). Shares the
|
|
62
|
+
* central-directory record parser with the lazy, file-backed reader.
|
|
63
|
+
*/
|
|
64
|
+
function unzip(bytes) {
|
|
65
|
+
const info = readCentralDirectoryInfoSync(bytes);
|
|
66
|
+
const centralDirectory = readMemoryRange(bytes, info.offset, info.offset + info.size);
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const entry of parseZipCentralDirectory(memoryByteSource(bytes), centralDirectory, info.entries)) {
|
|
69
|
+
if (entry.isDirectory || entry.storage?.type !== "zip") continue;
|
|
70
|
+
out[entry.path] = extractZipMember(bytes, entry.storage, entry.size);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Cap on tar/tar.gz archives loaded fully into memory for in-process indexing
|
|
76
|
+
* (gzip input is bounded to this decompressed size). ZIP is exempt: it is read
|
|
77
|
+
* via ranged central-directory access.
|
|
78
|
+
*/
|
|
79
|
+
const MAX_TAR_ARCHIVE_BYTES = 268435456;
|
|
80
|
+
/**
|
|
81
|
+
* Reject a tar input before materializing it. Tar parsing always retains the
|
|
82
|
+
* complete decoded stream, unlike ZIP's ranged central-directory reader.
|
|
83
|
+
*/
|
|
84
|
+
function assertTarArchiveSize(size) {
|
|
85
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new ToolError("Archive is too large to read safely");
|
|
86
|
+
if (size > MAX_TAR_ARCHIVE_BYTES) throw new ToolError(`Archive is too large to read in memory (${formatBytes(size)} > ${formatBytes(MAX_TAR_ARCHIVE_BYTES)} limit)`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cap on a single archive member's declared (uncompressed) size. The declared
|
|
90
|
+
* size is attacker-controlled metadata — a crafted ZIP entry can claim
|
|
91
|
+
* multi-GB sizes that would be allocated up front before any data inflates.
|
|
92
|
+
*/
|
|
93
|
+
const MAX_ARCHIVE_MEMBER_BYTES = 67108864;
|
|
94
|
+
/** Inflate one raw DEFLATE stream, bounded to its declared uncompressed size. */
|
|
95
|
+
function inflateRaw(bytes, declaredSize) {
|
|
96
|
+
return zlib.inflateRawSync(bytes, { maxOutputLength: Math.max(declaredSize, 1) });
|
|
97
|
+
}
|
|
98
|
+
function assertValidRange(start, end) {
|
|
99
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) throw new ToolError("Invalid ZIP archive range");
|
|
100
|
+
}
|
|
101
|
+
/** Read an exact in-memory range, throwing (not clamping) when it runs past the buffer. */
|
|
102
|
+
function readMemoryRange(buffer, start, end) {
|
|
103
|
+
assertValidRange(start, end);
|
|
104
|
+
if (end > buffer.byteLength) throw new ToolError("Invalid ZIP archive: truncated data");
|
|
105
|
+
return buffer.subarray(start, end);
|
|
106
|
+
}
|
|
107
|
+
function fileByteSource(filePath) {
|
|
108
|
+
const file = Bun.file(filePath);
|
|
109
|
+
const size = file.size;
|
|
110
|
+
if (!Number.isSafeInteger(size)) throw new ToolError("ZIP archive is too large to read safely");
|
|
111
|
+
return {
|
|
112
|
+
size,
|
|
113
|
+
async read(start, end) {
|
|
114
|
+
assertValidRange(start, end);
|
|
115
|
+
const bytes = await file.slice(start, end).bytes();
|
|
116
|
+
if (bytes.byteLength !== end - start) throw new ToolError("Invalid ZIP archive: truncated data");
|
|
117
|
+
return bytes;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function memoryByteSource(buffer) {
|
|
122
|
+
return {
|
|
123
|
+
size: buffer.byteLength,
|
|
124
|
+
async read(start, end) {
|
|
125
|
+
return readMemoryRange(buffer, start, end);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function normalizeArchiveLookupPath(rawPath) {
|
|
130
|
+
if (!rawPath) return "";
|
|
131
|
+
const parts = rawPath.replace(/\\/g, "/").split("/");
|
|
132
|
+
const normalizedParts = [];
|
|
133
|
+
for (const part of parts) {
|
|
134
|
+
if (!part || part === ".") continue;
|
|
135
|
+
if (part === "..") return void 0;
|
|
136
|
+
normalizedParts.push(part);
|
|
137
|
+
}
|
|
138
|
+
return normalizedParts.join("/");
|
|
139
|
+
}
|
|
140
|
+
function normalizeArchiveEntryPath(rawPath) {
|
|
141
|
+
const parts = rawPath.replace(/\\/g, "/").split("/");
|
|
142
|
+
const normalizedParts = [];
|
|
143
|
+
for (const part of parts) {
|
|
144
|
+
if (!part || part === ".") continue;
|
|
145
|
+
if (part === "..") return void 0;
|
|
146
|
+
normalizedParts.push(part);
|
|
147
|
+
}
|
|
148
|
+
if (normalizedParts.length === 0) return void 0;
|
|
149
|
+
return normalizedParts.join("/");
|
|
150
|
+
}
|
|
151
|
+
function isArchiveDirectoryName(rawPath) {
|
|
152
|
+
return rawPath.endsWith("/") || rawPath.endsWith("\\");
|
|
153
|
+
}
|
|
154
|
+
function upsertArchiveEntry(map, entry) {
|
|
155
|
+
const existing = map.get(entry.path);
|
|
156
|
+
if (!existing) {
|
|
157
|
+
map.set(entry.path, entry);
|
|
158
|
+
return entry;
|
|
159
|
+
}
|
|
160
|
+
if (existing.isDirectory && !entry.isDirectory) {
|
|
161
|
+
map.set(entry.path, entry);
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
if (!existing.isDirectory && entry.isDirectory) return;
|
|
165
|
+
const merged = {
|
|
166
|
+
...entry,
|
|
167
|
+
mtimeMs: entry.mtimeMs ?? existing.mtimeMs,
|
|
168
|
+
storage: entry.storage ?? existing.storage
|
|
169
|
+
};
|
|
170
|
+
map.set(entry.path, merged);
|
|
171
|
+
return merged;
|
|
172
|
+
}
|
|
173
|
+
function ensureParentDirectories(map) {
|
|
174
|
+
for (const entry of [...map.values()]) {
|
|
175
|
+
const parts = entry.path.split("/");
|
|
176
|
+
const stop = parts.length - 1;
|
|
177
|
+
for (let index = 1; index <= stop; index++) {
|
|
178
|
+
const dirPath = parts.slice(0, index).join("/");
|
|
179
|
+
if (!dirPath || map.has(dirPath)) continue;
|
|
180
|
+
map.set(dirPath, {
|
|
181
|
+
path: dirPath,
|
|
182
|
+
isDirectory: true,
|
|
183
|
+
size: 0
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Extensions that are ZIP containers under a different name — JVM (`.jar`,
|
|
190
|
+
* `.war`, `.ear`) and Android (`.apk`) packages are all ZIP archives. Treated
|
|
191
|
+
* as `zip` for member read/list and whole-archive rewrite.
|
|
192
|
+
*/
|
|
193
|
+
const ZIP_ALIAS_EXTENSIONS = [
|
|
194
|
+
"jar",
|
|
195
|
+
"war",
|
|
196
|
+
"ear",
|
|
197
|
+
"apk"
|
|
198
|
+
];
|
|
199
|
+
/**
|
|
200
|
+
* Regex alternation of every recognized archive extension, longest first so
|
|
201
|
+
* `.tar.gz` wins over `.tar`. Shared with `parseArchivePathCandidates` as its
|
|
202
|
+
* split pattern so extension recognition and path splitting never drift.
|
|
203
|
+
*/
|
|
204
|
+
const ARCHIVE_EXTENSION_ALTERNATION = [
|
|
205
|
+
"tar\\.gz",
|
|
206
|
+
"tgz",
|
|
207
|
+
"zip",
|
|
208
|
+
"tar",
|
|
209
|
+
...ZIP_ALIAS_EXTENSIONS
|
|
210
|
+
].join("|");
|
|
211
|
+
/** Infer an archive format from a filesystem path's extension. */
|
|
212
|
+
function archiveFormatFromPath(filePath) {
|
|
213
|
+
const normalized = filePath.toLowerCase();
|
|
214
|
+
if (normalized.endsWith(".tar.gz") || normalized.endsWith(".tgz")) return "tar.gz";
|
|
215
|
+
if (normalized.endsWith(".tar")) return "tar";
|
|
216
|
+
if (normalized.endsWith(".zip")) return "zip";
|
|
217
|
+
if (ZIP_ALIAS_EXTENSIONS.some((ext) => normalized.endsWith(`.${ext}`))) return "zip";
|
|
218
|
+
}
|
|
219
|
+
function formatArchiveEntryLines(entries) {
|
|
220
|
+
return entries.map((entry) => {
|
|
221
|
+
if (entry.isDirectory) return `${entry.name}/`;
|
|
222
|
+
const sizeSuffix = entry.size > 0 ? ` (${formatBytes(entry.size)})` : "";
|
|
223
|
+
return `${entry.name}${sizeSuffix}`;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 67324752;
|
|
227
|
+
const ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 33639248;
|
|
228
|
+
const ZIP64_EOCD_SIGNATURE = 101075792;
|
|
229
|
+
const ZIP64_EOCD_LOCATOR_SIGNATURE = 117853008;
|
|
230
|
+
const ZIP_EOCD_SIGNATURE = 101010256;
|
|
231
|
+
const ZIP_DATA_DESCRIPTOR_SIGNATURE = 134695760;
|
|
232
|
+
const ZIP_EOCD_MIN_LENGTH = 22;
|
|
233
|
+
const ZIP64_EOCD_LOCATOR_LENGTH = 20;
|
|
234
|
+
const ZIP_STORED_COMPRESSION = 0;
|
|
235
|
+
const ZIP_DEFLATE_COMPRESSION = 8;
|
|
236
|
+
const ZIP_UTF8_FLAG = 2048;
|
|
237
|
+
const ZIP_ENCRYPTED_FLAG = 1;
|
|
238
|
+
const ZIP_UINT16_MAX = 65535;
|
|
239
|
+
const ZIP_UINT32_MAX = 4294967295;
|
|
240
|
+
const ZIP_UINT32_RANGE = 4294967296;
|
|
241
|
+
function readUInt16LE(bytes, offset) {
|
|
242
|
+
return bytes[offset] | bytes[offset + 1] << 8;
|
|
243
|
+
}
|
|
244
|
+
function readUInt32LE(bytes, offset) {
|
|
245
|
+
return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;
|
|
246
|
+
}
|
|
247
|
+
function bytesMatchAscii(bytes, offset, value) {
|
|
248
|
+
if (bytes.byteLength < offset + value.length) return false;
|
|
249
|
+
for (let index = 0; index < value.length; index++) if (bytes[offset + index] !== value.charCodeAt(index)) return false;
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
function sniffArchiveFormat(bytes) {
|
|
253
|
+
if (bytes.byteLength >= 4) {
|
|
254
|
+
const signature = readUInt32LE(bytes, 0);
|
|
255
|
+
if (signature === ZIP_LOCAL_FILE_HEADER_SIGNATURE || signature === ZIP_EOCD_SIGNATURE || signature === ZIP_DATA_DESCRIPTOR_SIGNATURE) return "zip";
|
|
256
|
+
}
|
|
257
|
+
if (bytes.byteLength >= 2 && bytes[0] === 31 && bytes[1] === 139) return "tar.gz";
|
|
258
|
+
if (bytesMatchAscii(bytes, 257, "ustar")) return "tar";
|
|
259
|
+
}
|
|
260
|
+
function readUInt64LEAsNumber(bytes, offset) {
|
|
261
|
+
const value = readUInt32LE(bytes, offset) + readUInt32LE(bytes, offset + 4) * ZIP_UINT32_RANGE;
|
|
262
|
+
if (!Number.isSafeInteger(value)) throw new ToolError("ZIP archive uses offsets or sizes too large to read safely");
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
function findEndOfCentralDirectory(tail) {
|
|
266
|
+
for (let offset = tail.byteLength - ZIP_EOCD_MIN_LENGTH; offset >= 0; offset--) {
|
|
267
|
+
if (readUInt32LE(tail, offset) !== ZIP_EOCD_SIGNATURE) continue;
|
|
268
|
+
const commentLength = readUInt16LE(tail, offset + 20);
|
|
269
|
+
if (offset + ZIP_EOCD_MIN_LENGTH + commentLength === tail.byteLength) return offset;
|
|
270
|
+
}
|
|
271
|
+
throw new ToolError("Invalid ZIP archive: missing end of central directory");
|
|
272
|
+
}
|
|
273
|
+
async function readZip64CentralDirectoryInfo(source, tail, tailStart, eocdOffset) {
|
|
274
|
+
const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_LENGTH;
|
|
275
|
+
if (locatorOffset < 0) return void 0;
|
|
276
|
+
const locator = locatorOffset >= tailStart ? tail.subarray(locatorOffset - tailStart, locatorOffset - tailStart + ZIP64_EOCD_LOCATOR_LENGTH) : await source.read(locatorOffset, eocdOffset);
|
|
277
|
+
if (readUInt32LE(locator, 0) !== ZIP64_EOCD_LOCATOR_SIGNATURE) return void 0;
|
|
278
|
+
const zip64EocdDisk = readUInt32LE(locator, 4);
|
|
279
|
+
const zip64EocdOffset = readUInt64LEAsNumber(locator, 8);
|
|
280
|
+
const totalDisks = readUInt32LE(locator, 16);
|
|
281
|
+
if (zip64EocdDisk !== 0 || totalDisks > 1) throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
282
|
+
const record = await source.read(zip64EocdOffset, zip64EocdOffset + 56);
|
|
283
|
+
if (readUInt32LE(record, 0) !== ZIP64_EOCD_SIGNATURE) throw new ToolError("Invalid ZIP archive: missing ZIP64 end of central directory");
|
|
284
|
+
if (readUInt32LE(record, 16) !== 0 || readUInt32LE(record, 20) !== 0) throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
285
|
+
return {
|
|
286
|
+
entries: readUInt64LEAsNumber(record, 32),
|
|
287
|
+
size: readUInt64LEAsNumber(record, 40),
|
|
288
|
+
offset: readUInt64LEAsNumber(record, 48)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async function readZipCentralDirectoryInfo(source) {
|
|
292
|
+
const fileSize = source.size;
|
|
293
|
+
if (fileSize < ZIP_EOCD_MIN_LENGTH) throw new ToolError("Invalid ZIP archive: missing end of central directory");
|
|
294
|
+
const tailStart = fileSize - Math.min(fileSize, 65557);
|
|
295
|
+
const tail = await source.read(tailStart, fileSize);
|
|
296
|
+
const eocdIndex = findEndOfCentralDirectory(tail);
|
|
297
|
+
const eocdOffset = tailStart + eocdIndex;
|
|
298
|
+
if (readUInt16LE(tail, eocdIndex + 4) !== 0 || readUInt16LE(tail, eocdIndex + 6) !== 0) throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
299
|
+
let entries = readUInt16LE(tail, eocdIndex + 10);
|
|
300
|
+
let size = readUInt32LE(tail, eocdIndex + 12);
|
|
301
|
+
let offset = readUInt32LE(tail, eocdIndex + 16);
|
|
302
|
+
const needsZip64 = entries === ZIP_UINT16_MAX || size === ZIP_UINT32_MAX || offset === ZIP_UINT32_MAX;
|
|
303
|
+
const zip64Info = await readZip64CentralDirectoryInfo(source, tail, tailStart, eocdOffset);
|
|
304
|
+
if (zip64Info) ({entries, size, offset} = zip64Info);
|
|
305
|
+
else if (needsZip64) throw new ToolError("Invalid ZIP archive: missing ZIP64 central directory metadata");
|
|
306
|
+
if (offset + size > fileSize) throw new ToolError("Invalid ZIP archive: central directory exceeds file size");
|
|
307
|
+
return {
|
|
308
|
+
entries,
|
|
309
|
+
offset,
|
|
310
|
+
size
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function readZip64EntryValues(extra, placeholders, current) {
|
|
314
|
+
if (!placeholders.compressedSize && !placeholders.uncompressedSize && !placeholders.localHeaderOffset && !placeholders.diskStart) return current;
|
|
315
|
+
let offset = 0;
|
|
316
|
+
while (offset + 4 <= extra.byteLength) {
|
|
317
|
+
const headerId = readUInt16LE(extra, offset);
|
|
318
|
+
const dataSize = readUInt16LE(extra, offset + 2);
|
|
319
|
+
const dataStart = offset + 4;
|
|
320
|
+
const dataEnd = dataStart + dataSize;
|
|
321
|
+
if (dataEnd > extra.byteLength) throw new ToolError("Invalid ZIP archive: malformed extra field");
|
|
322
|
+
if (headerId === 1) {
|
|
323
|
+
let cursor = dataStart;
|
|
324
|
+
let uncompressedSize = current.uncompressedSize;
|
|
325
|
+
let compressedSize = current.compressedSize;
|
|
326
|
+
let localHeaderOffset = current.localHeaderOffset;
|
|
327
|
+
let diskStart = current.diskStart;
|
|
328
|
+
if (placeholders.uncompressedSize) {
|
|
329
|
+
if (cursor + 8 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
330
|
+
uncompressedSize = readUInt64LEAsNumber(extra, cursor);
|
|
331
|
+
cursor += 8;
|
|
332
|
+
}
|
|
333
|
+
if (placeholders.compressedSize) {
|
|
334
|
+
if (cursor + 8 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
335
|
+
compressedSize = readUInt64LEAsNumber(extra, cursor);
|
|
336
|
+
cursor += 8;
|
|
337
|
+
}
|
|
338
|
+
if (placeholders.localHeaderOffset) {
|
|
339
|
+
if (cursor + 8 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
340
|
+
localHeaderOffset = readUInt64LEAsNumber(extra, cursor);
|
|
341
|
+
cursor += 8;
|
|
342
|
+
}
|
|
343
|
+
if (placeholders.diskStart) {
|
|
344
|
+
if (cursor + 4 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
345
|
+
diskStart = readUInt32LE(extra, cursor);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
compressedSize,
|
|
349
|
+
uncompressedSize,
|
|
350
|
+
localHeaderOffset,
|
|
351
|
+
diskStart
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
offset = dataEnd;
|
|
355
|
+
}
|
|
356
|
+
throw new ToolError("Invalid ZIP archive: missing ZIP64 extra field");
|
|
357
|
+
}
|
|
358
|
+
function parseZipCentralDirectory(source, centralDirectory, expectedEntries) {
|
|
359
|
+
const entries = [];
|
|
360
|
+
let offset = 0;
|
|
361
|
+
for (let index = 0; index < expectedEntries; index++) {
|
|
362
|
+
if (offset + 46 > centralDirectory.byteLength) throw new ToolError("Invalid ZIP archive: truncated central directory");
|
|
363
|
+
if (readUInt32LE(centralDirectory, offset) !== ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE) throw new ToolError("Invalid ZIP archive: malformed central directory");
|
|
364
|
+
const flags = readUInt16LE(centralDirectory, offset + 8);
|
|
365
|
+
const compression = readUInt16LE(centralDirectory, offset + 10);
|
|
366
|
+
const compressedSizeRaw = readUInt32LE(centralDirectory, offset + 20);
|
|
367
|
+
const uncompressedSizeRaw = readUInt32LE(centralDirectory, offset + 24);
|
|
368
|
+
const fileNameLength = readUInt16LE(centralDirectory, offset + 28);
|
|
369
|
+
const extraLength = readUInt16LE(centralDirectory, offset + 30);
|
|
370
|
+
const commentLength = readUInt16LE(centralDirectory, offset + 32);
|
|
371
|
+
const diskStartRaw = readUInt16LE(centralDirectory, offset + 34);
|
|
372
|
+
const localHeaderOffsetRaw = readUInt32LE(centralDirectory, offset + 42);
|
|
373
|
+
const nameStart = offset + 46;
|
|
374
|
+
const extraStart = nameStart + fileNameLength;
|
|
375
|
+
const entryEnd = extraStart + extraLength + commentLength;
|
|
376
|
+
if (entryEnd > centralDirectory.byteLength) throw new ToolError("Invalid ZIP archive: truncated central directory entry");
|
|
377
|
+
const rawPath = ((flags & ZIP_UTF8_FLAG) === 0 ? LEGACY_NAME_DECODER : UTF8_DECODER).decode(centralDirectory.subarray(nameStart, extraStart));
|
|
378
|
+
const normalizedPath = normalizeArchiveEntryPath(rawPath);
|
|
379
|
+
if (normalizedPath) {
|
|
380
|
+
const values = readZip64EntryValues(centralDirectory.subarray(extraStart, extraStart + extraLength), {
|
|
381
|
+
compressedSize: compressedSizeRaw === ZIP_UINT32_MAX,
|
|
382
|
+
uncompressedSize: uncompressedSizeRaw === ZIP_UINT32_MAX,
|
|
383
|
+
localHeaderOffset: localHeaderOffsetRaw === ZIP_UINT32_MAX,
|
|
384
|
+
diskStart: diskStartRaw === ZIP_UINT16_MAX
|
|
385
|
+
}, {
|
|
386
|
+
compressedSize: compressedSizeRaw,
|
|
387
|
+
uncompressedSize: uncompressedSizeRaw,
|
|
388
|
+
localHeaderOffset: localHeaderOffsetRaw,
|
|
389
|
+
diskStart: diskStartRaw
|
|
390
|
+
});
|
|
391
|
+
if (values.diskStart !== 0) throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
392
|
+
const isDirectory = isArchiveDirectoryName(rawPath);
|
|
393
|
+
entries.push({
|
|
394
|
+
path: normalizedPath,
|
|
395
|
+
isDirectory,
|
|
396
|
+
size: isDirectory ? 0 : values.uncompressedSize,
|
|
397
|
+
storage: isDirectory ? void 0 : {
|
|
398
|
+
type: "zip",
|
|
399
|
+
source,
|
|
400
|
+
compressedSize: values.compressedSize,
|
|
401
|
+
compression,
|
|
402
|
+
flags,
|
|
403
|
+
localHeaderOffset: values.localHeaderOffset
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
offset = entryEnd;
|
|
408
|
+
}
|
|
409
|
+
return entries;
|
|
410
|
+
}
|
|
411
|
+
/** Decode a single ZIP member's already-read payload, bounded to its declared size. */
|
|
412
|
+
function decodeZipMember(compressed, compression, declaredSize) {
|
|
413
|
+
if (compression === ZIP_STORED_COMPRESSION) return compressed;
|
|
414
|
+
if (compression !== ZIP_DEFLATE_COMPRESSION) throw new ToolError(`Unsupported ZIP compression method: ${compression}`);
|
|
415
|
+
try {
|
|
416
|
+
return inflateRaw(compressed, declaredSize);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function readZipFileBytes(storage, uncompressedSize) {
|
|
422
|
+
if ((storage.flags & ZIP_ENCRYPTED_FLAG) !== 0) throw new ToolError("Encrypted ZIP entries are not supported");
|
|
423
|
+
const localHeader = await storage.source.read(storage.localHeaderOffset, storage.localHeaderOffset + 30);
|
|
424
|
+
if (readUInt32LE(localHeader, 0) !== ZIP_LOCAL_FILE_HEADER_SIGNATURE) throw new ToolError("Invalid ZIP archive: malformed local file header");
|
|
425
|
+
const fileNameLength = readUInt16LE(localHeader, 26);
|
|
426
|
+
const extraLength = readUInt16LE(localHeader, 28);
|
|
427
|
+
const dataStart = storage.localHeaderOffset + 30 + fileNameLength + extraLength;
|
|
428
|
+
return decodeZipMember(await storage.source.read(dataStart, dataStart + storage.compressedSize), storage.compression, uncompressedSize);
|
|
429
|
+
}
|
|
430
|
+
const TAR_BLOCK_SIZE = 512;
|
|
431
|
+
const TAR_NAME_OFFSET = 0;
|
|
432
|
+
const TAR_NAME_LENGTH = 100;
|
|
433
|
+
const TAR_SIZE_OFFSET = 124;
|
|
434
|
+
const TAR_SIZE_LENGTH = 12;
|
|
435
|
+
const TAR_MTIME_OFFSET = 136;
|
|
436
|
+
const TAR_MTIME_LENGTH = 12;
|
|
437
|
+
const TAR_CHECKSUM_OFFSET = 148;
|
|
438
|
+
const TAR_CHECKSUM_LENGTH = 8;
|
|
439
|
+
const TAR_TYPEFLAG_OFFSET = 156;
|
|
440
|
+
const TAR_LINKNAME_OFFSET = 157;
|
|
441
|
+
const TAR_LINKNAME_LENGTH = 100;
|
|
442
|
+
const TAR_MAGIC_OFFSET = 257;
|
|
443
|
+
const TAR_MAGIC = "ustar\0";
|
|
444
|
+
const TAR_VERSION_OFFSET = 263;
|
|
445
|
+
const TAR_VERSION = "00";
|
|
446
|
+
const TAR_PREFIX_OFFSET = 345;
|
|
447
|
+
const TAR_PREFIX_LENGTH = 155;
|
|
448
|
+
const TAR_GNU_SPARSE_ISEXTENDED_OFFSET = 482;
|
|
449
|
+
const TAR_GNU_SPARSE_CONT_ISEXTENDED_OFFSET = 504;
|
|
450
|
+
const TAR_MAX_PATH_BYTES = 4096;
|
|
451
|
+
const TAR_MAX_PAX_NUMERIC_BYTES = 32;
|
|
452
|
+
const TAR_ERROR_PATH_PREVIEW_BYTES = 256;
|
|
453
|
+
const GZIP_MAGIC_0 = 31;
|
|
454
|
+
const GZIP_MAGIC_1 = 139;
|
|
455
|
+
const TAR_TEXT_DECODER = new TextDecoder();
|
|
456
|
+
/**
|
|
457
|
+
* Decompress a gzip stream in-process, bounded to the tar archive cap so a
|
|
458
|
+
* gzip bomb cannot inflate without limit. Non-gzip input passes through.
|
|
459
|
+
*/
|
|
460
|
+
function gunzipIfNeeded(bytes) {
|
|
461
|
+
if (bytes.length >= 2 && bytes[0] === GZIP_MAGIC_0 && bytes[1] === GZIP_MAGIC_1) return new Uint8Array(zlib.gunzipSync(bytes, { maxOutputLength: MAX_TAR_ARCHIVE_BYTES }));
|
|
462
|
+
return bytes;
|
|
463
|
+
}
|
|
464
|
+
/** Read a NUL-terminated tar header string, clamped to the buffer bounds. */
|
|
465
|
+
function readTarString(buffer, offset, length) {
|
|
466
|
+
const limit = Math.min(offset + length, buffer.length);
|
|
467
|
+
let end = offset;
|
|
468
|
+
while (end < limit && buffer[end] !== 0) end++;
|
|
469
|
+
return TAR_TEXT_DECODER.decode(buffer.subarray(offset, end));
|
|
470
|
+
}
|
|
471
|
+
function tarBytesEqualAscii(bytes, value) {
|
|
472
|
+
return bytes.byteLength === value.length && bytesMatchAscii(bytes, 0, value);
|
|
473
|
+
}
|
|
474
|
+
function isUstarHeader(buffer, offset) {
|
|
475
|
+
const magicOffset = offset + TAR_MAGIC_OFFSET;
|
|
476
|
+
const versionOffset = offset + TAR_VERSION_OFFSET;
|
|
477
|
+
return bytesMatchAscii(buffer, magicOffset, TAR_MAGIC) && bytesMatchAscii(buffer, versionOffset, TAR_VERSION);
|
|
478
|
+
}
|
|
479
|
+
function assertTarPathBytes(size, field) {
|
|
480
|
+
if (size > TAR_MAX_PATH_BYTES) throw new ToolError(`Archive ${field} exceeds ${TAR_MAX_PATH_BYTES} bytes`);
|
|
481
|
+
}
|
|
482
|
+
function assertTarPathString(value, field) {
|
|
483
|
+
assertTarPathBytes(Buffer.byteLength(value, "utf-8"), field);
|
|
484
|
+
}
|
|
485
|
+
function formatTarPathForError(value) {
|
|
486
|
+
if (Buffer.byteLength(value, "utf-8") <= TAR_ERROR_PATH_PREVIEW_BYTES) return value;
|
|
487
|
+
let end = 0;
|
|
488
|
+
let size = 0;
|
|
489
|
+
for (const char of value) {
|
|
490
|
+
const charSize = Buffer.byteLength(char, "utf-8");
|
|
491
|
+
if (size + charSize > 253) break;
|
|
492
|
+
end += char.length;
|
|
493
|
+
size += charSize;
|
|
494
|
+
}
|
|
495
|
+
return `${value.slice(0, end)}...`;
|
|
496
|
+
}
|
|
497
|
+
function readTarMetadataPath(data, field) {
|
|
498
|
+
const nul = data.indexOf(0);
|
|
499
|
+
const value = data.subarray(0, nul === -1 ? data.byteLength : nul);
|
|
500
|
+
assertTarPathBytes(value.byteLength, field);
|
|
501
|
+
return TAR_TEXT_DECODER.decode(value);
|
|
502
|
+
}
|
|
503
|
+
function readPaxPath(data, field) {
|
|
504
|
+
assertTarPathBytes(data.byteLength, field);
|
|
505
|
+
return TAR_TEXT_DECODER.decode(data);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Read a tar numeric header field: GNU base-256 (high bit set) or the usual
|
|
509
|
+
* NUL/space-padded octal. GNU's binary form reserves its high bit as a marker
|
|
510
|
+
* and stores a signed two's-complement value in the remaining bits.
|
|
511
|
+
*/
|
|
512
|
+
function readTarNumeric(buffer, offset, length) {
|
|
513
|
+
if (offset < 0 || length <= 0 || offset + length > buffer.length) throw new ToolError("Invalid tar numeric field");
|
|
514
|
+
const first = buffer[offset];
|
|
515
|
+
let value = 0n;
|
|
516
|
+
if ((first & 128) !== 0) {
|
|
517
|
+
value = BigInt(first & 127);
|
|
518
|
+
for (let index = 1; index < length; index++) value = value << 8n | BigInt(buffer[offset + index]);
|
|
519
|
+
if ((first & 64) !== 0) value -= 1n << BigInt(length * 8 - 1);
|
|
520
|
+
} else for (let index = 0; index < length; index++) {
|
|
521
|
+
const byte = buffer[offset + index];
|
|
522
|
+
if (byte >= 48 && byte <= 55) value = value * 8n + BigInt(byte - 48);
|
|
523
|
+
}
|
|
524
|
+
return Number(value);
|
|
525
|
+
}
|
|
526
|
+
function readTarSize(buffer, offset) {
|
|
527
|
+
const size = readTarNumeric(buffer, offset, TAR_SIZE_LENGTH);
|
|
528
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new ToolError("Invalid tar member size");
|
|
529
|
+
return size;
|
|
530
|
+
}
|
|
531
|
+
function tarPaddedSize(size) {
|
|
532
|
+
const remainder = size % TAR_BLOCK_SIZE;
|
|
533
|
+
const padded = size + (remainder === 0 ? 0 : TAR_BLOCK_SIZE - remainder);
|
|
534
|
+
if (!Number.isSafeInteger(padded)) throw new ToolError("Invalid tar member size");
|
|
535
|
+
return padded;
|
|
536
|
+
}
|
|
537
|
+
function parsePaxSize(value, field) {
|
|
538
|
+
if (!/^\d+$/.test(value)) throw new ToolError(`Invalid tar ${field}`);
|
|
539
|
+
const size = Number(value);
|
|
540
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new ToolError(`Invalid tar ${field}`);
|
|
541
|
+
return size;
|
|
542
|
+
}
|
|
543
|
+
function isTarZeroBlock(buffer, offset) {
|
|
544
|
+
for (let i = 0; i < TAR_BLOCK_SIZE; i++) if (buffer[offset + i] !== 0) return false;
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
/** Verify a tar header block's checksum (both unsigned and signed conventions). */
|
|
548
|
+
function tarChecksumMatches(buffer, offset) {
|
|
549
|
+
const stored = readTarNumeric(buffer, offset + TAR_CHECKSUM_OFFSET, TAR_CHECKSUM_LENGTH);
|
|
550
|
+
let unsigned = 0;
|
|
551
|
+
let signed = 0;
|
|
552
|
+
for (let i = 0; i < TAR_BLOCK_SIZE; i++) {
|
|
553
|
+
const byte = i >= TAR_CHECKSUM_OFFSET && i < 156 ? 32 : buffer[offset + i] ?? 0;
|
|
554
|
+
unsigned += byte;
|
|
555
|
+
signed += byte << 24 >> 24;
|
|
556
|
+
}
|
|
557
|
+
return stored === unsigned || stored === signed;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Sentinel key marking that any `GNU.sparse.*` record appeared in a PAX
|
|
561
|
+
* header. A real record cannot shadow it: PAX sparse keys always carry a
|
|
562
|
+
* suffix after the trailing dot.
|
|
563
|
+
*/
|
|
564
|
+
const PAX_SPARSE_MARKER = "GNU.sparse.";
|
|
565
|
+
/**
|
|
566
|
+
* Parse a PAX extended-header payload into its `key → value` records. Only
|
|
567
|
+
* exactly consumed keys are retained (plus the sparse marker), so a crafted
|
|
568
|
+
* header packed with millions of unique records — including `GNU.sparse.*`
|
|
569
|
+
* junk — cannot amplify into heap.
|
|
570
|
+
*/
|
|
571
|
+
function parsePaxRecords(data) {
|
|
572
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
573
|
+
let pos = 0;
|
|
574
|
+
while (pos < data.length) {
|
|
575
|
+
let space = pos;
|
|
576
|
+
while (space < data.length && data[space] !== 32) space++;
|
|
577
|
+
if (space === pos || space >= data.length || space - pos > 16) throw new ToolError("Invalid tar PAX record");
|
|
578
|
+
let length = 0;
|
|
579
|
+
for (let index = pos; index < space; index++) {
|
|
580
|
+
const byte = data[index];
|
|
581
|
+
if (byte < 48 || byte > 57) throw new ToolError("Invalid tar PAX record");
|
|
582
|
+
length = length * 10 + (byte - 48);
|
|
583
|
+
if (length > data.length - pos) throw new ToolError("Invalid tar PAX record");
|
|
584
|
+
}
|
|
585
|
+
if (length <= 0 || pos + length > data.length || data[pos + length - 1] !== 10) throw new ToolError("Invalid tar PAX record");
|
|
586
|
+
const record = data.subarray(space + 1, pos + length - 1);
|
|
587
|
+
const eq = record.indexOf(61);
|
|
588
|
+
if (eq >= 0) {
|
|
589
|
+
const key = record.subarray(0, eq);
|
|
590
|
+
const value = record.subarray(eq + 1);
|
|
591
|
+
if (bytesMatchAscii(key, 0, PAX_SPARSE_MARKER)) {
|
|
592
|
+
attrs.set(PAX_SPARSE_MARKER, value.byteLength === 0 ? "" : "1");
|
|
593
|
+
if (tarBytesEqualAscii(key, "GNU.sparse.name")) attrs.set("GNU.sparse.name", readPaxPath(value, "PAX sparse path"));
|
|
594
|
+
else if (tarBytesEqualAscii(key, "GNU.sparse.realsize")) {
|
|
595
|
+
if (value.byteLength > TAR_MAX_PAX_NUMERIC_BYTES) throw new ToolError("Invalid tar sparse real size");
|
|
596
|
+
attrs.set("GNU.sparse.realsize", TAR_TEXT_DECODER.decode(value));
|
|
597
|
+
}
|
|
598
|
+
} else if (tarBytesEqualAscii(key, "path") || tarBytesEqualAscii(key, "linkpath")) {
|
|
599
|
+
const field = tarBytesEqualAscii(key, "path") ? "PAX path" : "PAX link target";
|
|
600
|
+
attrs.set(field === "PAX path" ? "path" : "linkpath", readPaxPath(value, field));
|
|
601
|
+
} else if (tarBytesEqualAscii(key, "size")) {
|
|
602
|
+
if (value.byteLength > TAR_MAX_PAX_NUMERIC_BYTES) throw new ToolError("Invalid tar member size");
|
|
603
|
+
attrs.set("size", TAR_TEXT_DECODER.decode(value));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
pos += length;
|
|
607
|
+
}
|
|
608
|
+
return attrs;
|
|
609
|
+
}
|
|
610
|
+
function applyGlobalPax(globalPax, update) {
|
|
611
|
+
for (const [key, value] of update) if (value === "") globalPax.delete(key);
|
|
612
|
+
else globalPax.set(key, value);
|
|
613
|
+
}
|
|
614
|
+
function paxAttribute(globalPax, localPax, key) {
|
|
615
|
+
if (localPax?.has(key)) return localPax.get(key);
|
|
616
|
+
return globalPax.get(key);
|
|
617
|
+
}
|
|
618
|
+
function paxDeclaresSparse(globalPax, localPax) {
|
|
619
|
+
return paxAttribute(globalPax, localPax, PAX_SPARSE_MARKER) === "1";
|
|
620
|
+
}
|
|
621
|
+
function indexOfAscii(bytes, value, start) {
|
|
622
|
+
for (let offset = start; offset <= bytes.byteLength - value.length; offset++) if (bytesMatchAscii(bytes, offset, value)) return offset;
|
|
623
|
+
return -1;
|
|
624
|
+
}
|
|
625
|
+
function normalizeOldGnuName(value, field) {
|
|
626
|
+
const portable = value.replace(/\\/g, "/");
|
|
627
|
+
if (path.posix.isAbsolute(portable)) throw new ToolError(`Invalid old-GNU ${field}`);
|
|
628
|
+
const normalized = normalizeArchiveEntryPath(portable);
|
|
629
|
+
if (!normalized) throw new ToolError(`Invalid old-GNU ${field}`);
|
|
630
|
+
assertTarPathString(normalized, field);
|
|
631
|
+
return normalized;
|
|
632
|
+
}
|
|
633
|
+
function renameOldGnuEntries(entries, pendingLinks, fromPath, toPath) {
|
|
634
|
+
const moved = [...entries.entries()].filter(([entryPath]) => entryPath === fromPath || entryPath.startsWith(`${fromPath}/`));
|
|
635
|
+
if (moved.length === 0) return;
|
|
636
|
+
for (const [entryPath] of moved) entries.delete(entryPath);
|
|
637
|
+
for (const [entryPath, entry] of moved) {
|
|
638
|
+
const nextPath = `${toPath}${entryPath.slice(fromPath.length)}`;
|
|
639
|
+
assertTarPathString(nextPath, "member path");
|
|
640
|
+
entry.path = nextPath;
|
|
641
|
+
const replaced = entries.get(nextPath);
|
|
642
|
+
if (replaced) pendingLinks.delete(replaced);
|
|
643
|
+
entries.set(nextPath, entry);
|
|
644
|
+
}
|
|
645
|
+
for (const pending of pendingLinks.values()) {
|
|
646
|
+
if (pending.kind !== "hard link") continue;
|
|
647
|
+
if (pending.targetPath === fromPath || pending.targetPath.startsWith(`${fromPath}/`)) pending.targetPath = `${toPath}${pending.targetPath.slice(fromPath.length)}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function applyOldGnuNameRecords(data, entries, pendingLinks) {
|
|
651
|
+
const terminator = data.indexOf(0);
|
|
652
|
+
const end = terminator === -1 ? data.byteLength : terminator;
|
|
653
|
+
let start = 0;
|
|
654
|
+
while (start < end) {
|
|
655
|
+
const newline = data.indexOf(10, start);
|
|
656
|
+
const lineEnd = newline === -1 || newline > end ? end : newline;
|
|
657
|
+
const line = data.subarray(start, lineEnd);
|
|
658
|
+
if (bytesMatchAscii(line, 0, "Rename ")) {
|
|
659
|
+
const separator = indexOfAscii(line, " to ", 7);
|
|
660
|
+
if (separator === -1) throw new ToolError("Invalid old-GNU name record");
|
|
661
|
+
const source = readTarMetadataPath(line.subarray(7, separator), "old-GNU source path");
|
|
662
|
+
const targetEnd = line[line.byteLength - 1] === 47 ? line.byteLength - 1 : line.byteLength;
|
|
663
|
+
const target = readTarMetadataPath(line.subarray(separator + 4, targetEnd), "old-GNU target path");
|
|
664
|
+
renameOldGnuEntries(entries, pendingLinks, normalizeOldGnuName(source, "source path"), normalizeOldGnuName(target, "target path"));
|
|
665
|
+
}
|
|
666
|
+
start = lineEnd + 1;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Index a tar (optionally gzip-compressed) archive entirely in TypeScript.
|
|
671
|
+
* Handles ustar/GNU/pax layouts, `./`-prefixed and `prefix`-split names, GNU
|
|
672
|
+
* `@LongLink` names/link targets, pax `path`/`linkpath`/`size` overrides, and
|
|
673
|
+
* hard links. This deliberately avoids `Bun.Archive`/libarchive, whose
|
|
674
|
+
* internal allocation-failure path calls `abort()` and takes down the whole
|
|
675
|
+
* process on a crafted or oversized member (#4774). Sparse members are
|
|
676
|
+
* indexed but flagged; reading their bytes throws a catchable `ToolError`
|
|
677
|
+
* rather than returning a misassembled payload.
|
|
678
|
+
*/
|
|
679
|
+
function readTarEntries(rawBytes) {
|
|
680
|
+
assertTarArchiveSize(rawBytes.byteLength);
|
|
681
|
+
let buffer;
|
|
682
|
+
try {
|
|
683
|
+
buffer = gunzipIfNeeded(rawBytes);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
686
|
+
}
|
|
687
|
+
assertTarArchiveSize(buffer.byteLength);
|
|
688
|
+
const entries = /* @__PURE__ */ new Map();
|
|
689
|
+
const pendingLinks = /* @__PURE__ */ new Map();
|
|
690
|
+
const addEntry = (entry, pendingLink) => {
|
|
691
|
+
const existing = entries.get(entry.path);
|
|
692
|
+
const indexed = upsertArchiveEntry(entries, entry);
|
|
693
|
+
if (!indexed) return;
|
|
694
|
+
if (existing) pendingLinks.delete(existing);
|
|
695
|
+
if (pendingLink) pendingLinks.set(indexed, pendingLink);
|
|
696
|
+
};
|
|
697
|
+
let offset = 0;
|
|
698
|
+
let longName;
|
|
699
|
+
let longLink;
|
|
700
|
+
let localPax;
|
|
701
|
+
const globalPax = /* @__PURE__ */ new Map();
|
|
702
|
+
let sawTerminator = false;
|
|
703
|
+
while (offset + TAR_BLOCK_SIZE <= buffer.length) {
|
|
704
|
+
if (isTarZeroBlock(buffer, offset)) {
|
|
705
|
+
sawTerminator = true;
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
if (!tarChecksumMatches(buffer, offset)) throw new ToolError("Invalid or corrupt tar archive header");
|
|
709
|
+
const headerOffset = offset;
|
|
710
|
+
const typeFlag = String.fromCharCode(buffer[headerOffset + TAR_TYPEFLAG_OFFSET] || 48);
|
|
711
|
+
let size = readTarSize(buffer, headerOffset + TAR_SIZE_OFFSET);
|
|
712
|
+
let name = readTarString(buffer, headerOffset + TAR_NAME_OFFSET, TAR_NAME_LENGTH);
|
|
713
|
+
if (isUstarHeader(buffer, headerOffset)) {
|
|
714
|
+
const prefix = readTarString(buffer, headerOffset + TAR_PREFIX_OFFSET, TAR_PREFIX_LENGTH);
|
|
715
|
+
if (prefix) name = `${prefix}/${name}`;
|
|
716
|
+
}
|
|
717
|
+
let linkName = readTarString(buffer, headerOffset + TAR_LINKNAME_OFFSET, TAR_LINKNAME_LENGTH);
|
|
718
|
+
const mtime = readTarNumeric(buffer, headerOffset + TAR_MTIME_OFFSET, TAR_MTIME_LENGTH);
|
|
719
|
+
offset += TAR_BLOCK_SIZE;
|
|
720
|
+
const dataBlocks = tarPaddedSize(size);
|
|
721
|
+
if (dataBlocks > buffer.length - offset) throw new ToolError("Archive member data is truncated");
|
|
722
|
+
const data = buffer.subarray(offset, offset + size);
|
|
723
|
+
if (typeFlag === "L") {
|
|
724
|
+
longName = readTarMetadataPath(data, "GNU long path");
|
|
725
|
+
offset += dataBlocks;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (typeFlag === "K") {
|
|
729
|
+
longLink = readTarMetadataPath(data, "GNU long link target");
|
|
730
|
+
offset += dataBlocks;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (typeFlag === "N") {
|
|
734
|
+
applyOldGnuNameRecords(data, entries, pendingLinks);
|
|
735
|
+
offset += dataBlocks;
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (typeFlag === "x" || typeFlag === "X") {
|
|
739
|
+
localPax = parsePaxRecords(data);
|
|
740
|
+
offset += dataBlocks;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (typeFlag === "g") {
|
|
744
|
+
applyGlobalPax(globalPax, parsePaxRecords(data));
|
|
745
|
+
offset += dataBlocks;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (longName !== void 0) name = longName;
|
|
749
|
+
if (longLink !== void 0) linkName = longLink;
|
|
750
|
+
const paxPath = paxAttribute(globalPax, localPax, "path");
|
|
751
|
+
if (paxPath !== void 0) name = paxPath;
|
|
752
|
+
const paxLinkPath = paxAttribute(globalPax, localPax, "linkpath");
|
|
753
|
+
if (paxLinkPath !== void 0) linkName = paxLinkPath;
|
|
754
|
+
const paxSize = paxAttribute(globalPax, localPax, "size");
|
|
755
|
+
if (paxSize !== void 0) size = parsePaxSize(paxSize, "member size");
|
|
756
|
+
const paxSparseName = paxAttribute(globalPax, localPax, "GNU.sparse.name");
|
|
757
|
+
if (paxSparseName !== void 0) name = paxSparseName;
|
|
758
|
+
let displaySize = size;
|
|
759
|
+
const paxSparseRealSize = paxAttribute(globalPax, localPax, "GNU.sparse.realsize");
|
|
760
|
+
if (paxSparseRealSize !== void 0) displaySize = parsePaxSize(paxSparseRealSize, "sparse real size");
|
|
761
|
+
const sparse = typeFlag === "S" || paxDeclaresSparse(globalPax, localPax);
|
|
762
|
+
if (typeFlag === "S" && buffer[headerOffset + TAR_GNU_SPARSE_ISEXTENDED_OFFSET] === 1) {
|
|
763
|
+
let extended = true;
|
|
764
|
+
while (extended) {
|
|
765
|
+
if (offset + TAR_BLOCK_SIZE > buffer.length) throw new ToolError("Archive sparse metadata is truncated");
|
|
766
|
+
extended = buffer[offset + TAR_GNU_SPARSE_CONT_ISEXTENDED_OFFSET] === 1;
|
|
767
|
+
offset += TAR_BLOCK_SIZE;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const dataOffset = offset;
|
|
771
|
+
const memberDataBlocks = tarPaddedSize(size);
|
|
772
|
+
if (memberDataBlocks > buffer.length - dataOffset) throw new ToolError(`Archive member '${formatTarPathForError(name)}' is truncated`);
|
|
773
|
+
offset += memberDataBlocks;
|
|
774
|
+
longName = void 0;
|
|
775
|
+
longLink = void 0;
|
|
776
|
+
localPax = void 0;
|
|
777
|
+
const isDirectory = typeFlag === "5" || name.endsWith("/");
|
|
778
|
+
const normalizedPath = normalizeArchiveEntryPath(name);
|
|
779
|
+
if (!normalizedPath) continue;
|
|
780
|
+
assertTarPathString(normalizedPath, "member path");
|
|
781
|
+
const scaledMtime = mtime * 1e3;
|
|
782
|
+
const mtimeMs = mtime !== 0 && Number.isSafeInteger(mtime) && Number.isSafeInteger(scaledMtime) ? scaledMtime : void 0;
|
|
783
|
+
if (isDirectory) {
|
|
784
|
+
addEntry({
|
|
785
|
+
path: normalizedPath,
|
|
786
|
+
isDirectory: true,
|
|
787
|
+
size: 0,
|
|
788
|
+
mtimeMs
|
|
789
|
+
});
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (typeFlag === "1" || typeFlag === "2") {
|
|
793
|
+
const kind = typeFlag === "1" ? "hard link" : "symlink";
|
|
794
|
+
const portableLinkName = linkName.replace(/\\/g, "/");
|
|
795
|
+
assertTarPathString(portableLinkName, "link target");
|
|
796
|
+
const targetPath = typeFlag === "1" ? normalizeArchiveEntryPath(portableLinkName) : path.posix.isAbsolute(portableLinkName) ? void 0 : normalizeArchiveLookupPath(path.posix.join(path.posix.dirname(normalizedPath), portableLinkName));
|
|
797
|
+
const entry = {
|
|
798
|
+
path: normalizedPath,
|
|
799
|
+
isDirectory: false,
|
|
800
|
+
size: 0,
|
|
801
|
+
mtimeMs
|
|
802
|
+
};
|
|
803
|
+
if (targetPath === void 0 || Buffer.byteLength(targetPath, "utf-8") > TAR_MAX_PATH_BYTES) {
|
|
804
|
+
if (kind === "hard link") throw new ToolError(`Archive hard link '${formatTarPathForError(normalizedPath)}' has an invalid target`);
|
|
805
|
+
entry.storage = {
|
|
806
|
+
type: "tar-link",
|
|
807
|
+
targetPath: portableLinkName
|
|
808
|
+
};
|
|
809
|
+
addEntry(entry);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
addEntry(entry, {
|
|
813
|
+
kind,
|
|
814
|
+
targetPath
|
|
815
|
+
});
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (typeFlag !== "0" && typeFlag !== "\0" && typeFlag !== "7" && typeFlag !== "S") continue;
|
|
819
|
+
addEntry({
|
|
820
|
+
path: normalizedPath,
|
|
821
|
+
isDirectory: false,
|
|
822
|
+
size: displaySize,
|
|
823
|
+
mtimeMs,
|
|
824
|
+
storage: {
|
|
825
|
+
type: "tar",
|
|
826
|
+
buffer,
|
|
827
|
+
dataOffset,
|
|
828
|
+
sparse
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
if (!sawTerminator) throw new ToolError("Not a valid tar archive: missing terminating zero block");
|
|
833
|
+
if (pendingLinks.size > 0) {
|
|
834
|
+
const entriesByPath = entries;
|
|
835
|
+
const directoryPrefixes = /* @__PURE__ */ new Set();
|
|
836
|
+
for (const entry of entriesByPath.values()) {
|
|
837
|
+
const memberPath = entry.path;
|
|
838
|
+
for (let cut = memberPath.lastIndexOf("/"); cut > 0; cut = memberPath.lastIndexOf("/", cut - 1)) {
|
|
839
|
+
const prefix = memberPath.slice(0, cut);
|
|
840
|
+
if (directoryPrefixes.has(prefix)) break;
|
|
841
|
+
directoryPrefixes.add(prefix);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const unresolved = new Set(pendingLinks.keys());
|
|
845
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
846
|
+
const findUnresolvedBlocker = (targetPath) => {
|
|
847
|
+
for (let end = targetPath.length; end > 0; end = targetPath.lastIndexOf("/", end - 1)) {
|
|
848
|
+
const prefixEntry = entriesByPath.get(targetPath.slice(0, end));
|
|
849
|
+
if (prefixEntry && unresolved.has(prefixEntry)) return prefixEntry;
|
|
850
|
+
}
|
|
851
|
+
return null;
|
|
852
|
+
};
|
|
853
|
+
const queue = [...unresolved];
|
|
854
|
+
while (queue.length > 0) {
|
|
855
|
+
const entry = queue.pop();
|
|
856
|
+
if (!unresolved.has(entry)) continue;
|
|
857
|
+
const pending = pendingLinks.get(entry);
|
|
858
|
+
let blocker = findUnresolvedBlocker(pending.targetPath);
|
|
859
|
+
let targetPath = pending.targetPath;
|
|
860
|
+
if (blocker === null) {
|
|
861
|
+
try {
|
|
862
|
+
targetPath = resolveDirectoryAliasPath(entriesByPath, targetPath);
|
|
863
|
+
} catch {}
|
|
864
|
+
if (targetPath !== pending.targetPath) blocker = findUnresolvedBlocker(targetPath);
|
|
865
|
+
}
|
|
866
|
+
if (blocker !== null && blocker !== entry) {
|
|
867
|
+
const waiting = dependents.get(blocker);
|
|
868
|
+
if (waiting) waiting.push(entry);
|
|
869
|
+
else dependents.set(blocker, [entry]);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
unresolved.delete(entry);
|
|
873
|
+
const settled = dependents.get(entry);
|
|
874
|
+
if (settled) {
|
|
875
|
+
dependents.delete(entry);
|
|
876
|
+
queue.push(...settled);
|
|
877
|
+
}
|
|
878
|
+
if (blocker === entry) {
|
|
879
|
+
if (pending.kind === "hard link") throw new ToolError(`Archive hard link '${formatTarPathForError(entry.path)}' has a cyclic target '${formatTarPathForError(pending.targetPath)}'`);
|
|
880
|
+
entry.storage = {
|
|
881
|
+
type: "tar-link",
|
|
882
|
+
targetPath: pending.targetPath
|
|
883
|
+
};
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const target = entriesByPath.get(targetPath);
|
|
887
|
+
if (target?.storage && !target.isDirectory && !unresolved.has(target)) {
|
|
888
|
+
entry.size = target.size;
|
|
889
|
+
entry.storage = target.storage;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (!(targetPath === "" || target?.isDirectory === true || directoryPrefixes.has(targetPath))) {
|
|
893
|
+
if (pending.kind === "symlink") {
|
|
894
|
+
entry.storage = {
|
|
895
|
+
type: "tar-link",
|
|
896
|
+
targetPath: pending.targetPath
|
|
897
|
+
};
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const reason = target ? "unreadable member" : "missing member";
|
|
901
|
+
throw new ToolError(`Archive hard link '${formatTarPathForError(entry.path)}' targets ${reason} '${formatTarPathForError(pending.targetPath)}'`);
|
|
902
|
+
}
|
|
903
|
+
if (pending.kind === "hard link") throw new ToolError(`Archive hard link '${formatTarPathForError(entry.path)}' targets directory '${formatTarPathForError(pending.targetPath)}'`);
|
|
904
|
+
entry.isDirectory = true;
|
|
905
|
+
entry.storage = {
|
|
906
|
+
type: "tar-link",
|
|
907
|
+
targetPath: pending.targetPath
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
if (unresolved.size > 0) throw new ToolError("Archive contains cyclic or unsupported links");
|
|
911
|
+
}
|
|
912
|
+
return [...entries.values()];
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Slice one indexed tar member's bytes out of the archive buffer. Sparse
|
|
916
|
+
* members cannot be reassembled from a contiguous slice, so reading them throws
|
|
917
|
+
* a catchable error instead of returning corrupt data.
|
|
918
|
+
*/
|
|
919
|
+
function assertArchiveMemberSize(size, memberPath) {
|
|
920
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new ToolError(`Archive member '${formatTarPathForError(memberPath)}' has an invalid size`);
|
|
921
|
+
if (size > MAX_ARCHIVE_MEMBER_BYTES) throw new ToolError(`Archive member '${formatTarPathForError(memberPath)}' is too large to extract in memory (${formatBytes(size)} > ${formatBytes(MAX_ARCHIVE_MEMBER_BYTES)} limit)`);
|
|
922
|
+
}
|
|
923
|
+
function extractTarMember(storage, size, memberPath) {
|
|
924
|
+
assertArchiveMemberSize(size, memberPath);
|
|
925
|
+
if (storage.sparse) throw new ToolError(`Archive member '${formatTarPathForError(memberPath)}' is a sparse file and cannot be read`);
|
|
926
|
+
if (size > storage.buffer.length - storage.dataOffset) throw new ToolError(`Archive member '${formatTarPathForError(memberPath)}' is truncated`);
|
|
927
|
+
return storage.buffer.subarray(storage.dataOffset, storage.dataOffset + size);
|
|
928
|
+
}
|
|
929
|
+
function throwUnreadableTarLink(storage, memberPath) {
|
|
930
|
+
throw new ToolError(`Archive symlink '${formatTarPathForError(memberPath)}' cannot be materialized from target '${formatTarPathForError(storage.targetPath)}'`);
|
|
931
|
+
}
|
|
932
|
+
/** ELOOP-style bound on directory-alias rewrites during a single path lookup. */
|
|
933
|
+
const MAX_LINK_RESOLUTION_DEPTH = 40;
|
|
934
|
+
/**
|
|
935
|
+
* Rewrite `archivePath` through directory symlink aliases until it no longer
|
|
936
|
+
* crosses one. Bounded: an exact revisit and an alias chain that keeps growing
|
|
937
|
+
* the path (e.g. a directory symlink targeting its own subtree, `a -> a/b`)
|
|
938
|
+
* both throw a catchable cyclic-symlink error instead of looping forever.
|
|
939
|
+
*/
|
|
940
|
+
function resolveDirectoryAliasPath(entries, archivePath) {
|
|
941
|
+
let resolvedPath = archivePath;
|
|
942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
943
|
+
for (let rewrites = 0; !seen.has(resolvedPath);) {
|
|
944
|
+
seen.add(resolvedPath);
|
|
945
|
+
let replacement;
|
|
946
|
+
for (let end = resolvedPath.length; end > 0; end = resolvedPath.lastIndexOf("/", end - 1)) {
|
|
947
|
+
const entry = entries.get(resolvedPath.slice(0, end));
|
|
948
|
+
if (!entry?.isDirectory || entry.storage?.type !== "tar-link") continue;
|
|
949
|
+
const suffix = resolvedPath.slice(end + 1);
|
|
950
|
+
replacement = suffix ? entry.storage.targetPath ? `${entry.storage.targetPath}/${suffix}` : suffix : entry.storage.targetPath;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
if (replacement === void 0) return resolvedPath;
|
|
954
|
+
if (++rewrites > MAX_LINK_RESOLUTION_DEPTH) break;
|
|
955
|
+
resolvedPath = replacement;
|
|
956
|
+
}
|
|
957
|
+
throw new ToolError(`Archive path '${archivePath}' crosses a cyclic symlink`);
|
|
958
|
+
}
|
|
959
|
+
async function readZipEntries(source) {
|
|
960
|
+
const directoryInfo = await readZipCentralDirectoryInfo(source);
|
|
961
|
+
return parseZipCentralDirectory(source, await source.read(directoryInfo.offset, directoryInfo.offset + directoryInfo.size), directoryInfo.entries);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Split an `archive.ext:inner/path` reference into every plausible
|
|
965
|
+
* `{ archivePath, subPath }` pair, longest archive prefix first. A path may
|
|
966
|
+
* contain more than one archive extension, so each candidate is a guess at
|
|
967
|
+
* where the archive ends and the member portion begins.
|
|
968
|
+
*/
|
|
969
|
+
function parseArchivePathCandidates(filePath) {
|
|
970
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
971
|
+
const pattern = new RegExp(`\\.(?:${ARCHIVE_EXTENSION_ALTERNATION})(?=(?::|$))`, "gi");
|
|
972
|
+
const seen = /* @__PURE__ */ new Set();
|
|
973
|
+
const candidates = [];
|
|
974
|
+
let match;
|
|
975
|
+
while (true) {
|
|
976
|
+
match = pattern.exec(normalized);
|
|
977
|
+
if (match === null) break;
|
|
978
|
+
const end = match.index + match[0].length;
|
|
979
|
+
const archivePath = filePath.slice(0, end);
|
|
980
|
+
const subPath = normalized.slice(end).replace(/^:+/, "");
|
|
981
|
+
const key = `${archivePath}\0${subPath}`;
|
|
982
|
+
if (seen.has(key)) continue;
|
|
983
|
+
seen.add(key);
|
|
984
|
+
candidates.push({
|
|
985
|
+
archivePath,
|
|
986
|
+
subPath
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
return candidates.sort((left, right) => right.archivePath.length - left.archivePath.length);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* An indexed, read-only view over a single archive. ZIP archives are indexed
|
|
993
|
+
* from the central directory and members are inflated on demand; tar archives
|
|
994
|
+
* are parsed from one in-memory buffer, members are sliced on demand, and
|
|
995
|
+
* directory symlink aliases are traversed lazily.
|
|
996
|
+
*/
|
|
997
|
+
var ArchiveReader = class {
|
|
998
|
+
format;
|
|
999
|
+
#entries = /* @__PURE__ */ new Map();
|
|
1000
|
+
constructor(format, entries) {
|
|
1001
|
+
this.format = format;
|
|
1002
|
+
for (const entry of entries) upsertArchiveEntry(this.#entries, entry);
|
|
1003
|
+
ensureParentDirectories(this.#entries);
|
|
1004
|
+
}
|
|
1005
|
+
getNode(subPath) {
|
|
1006
|
+
const normalizedPath = normalizeArchiveLookupPath(subPath);
|
|
1007
|
+
if (normalizedPath === void 0) return void 0;
|
|
1008
|
+
if (normalizedPath === "") return {
|
|
1009
|
+
path: "",
|
|
1010
|
+
isDirectory: true,
|
|
1011
|
+
size: 0
|
|
1012
|
+
};
|
|
1013
|
+
const resolvedPath = resolveDirectoryAliasPath(this.#entries, normalizedPath);
|
|
1014
|
+
if (resolvedPath === "") return {
|
|
1015
|
+
path: normalizedPath,
|
|
1016
|
+
isDirectory: true,
|
|
1017
|
+
size: 0
|
|
1018
|
+
};
|
|
1019
|
+
const entry = this.#entries.get(resolvedPath);
|
|
1020
|
+
if (!entry) return void 0;
|
|
1021
|
+
return {
|
|
1022
|
+
path: normalizedPath,
|
|
1023
|
+
isDirectory: entry.isDirectory,
|
|
1024
|
+
size: entry.size,
|
|
1025
|
+
mtimeMs: entry.mtimeMs
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
listDirectory(subPath) {
|
|
1029
|
+
const normalizedPath = normalizeArchiveLookupPath(subPath);
|
|
1030
|
+
if (normalizedPath === void 0) throw new ToolError("Archive path cannot contain '..'");
|
|
1031
|
+
const resolvedPath = normalizedPath ? resolveDirectoryAliasPath(this.#entries, normalizedPath) : "";
|
|
1032
|
+
if (normalizedPath && resolvedPath !== "") {
|
|
1033
|
+
const entry = this.#entries.get(resolvedPath);
|
|
1034
|
+
if (!entry) throw new ToolError(`Archive path '${normalizedPath}' not found`);
|
|
1035
|
+
if (!entry.isDirectory) throw new ToolError(`Archive path '${normalizedPath}' is not a directory`);
|
|
1036
|
+
}
|
|
1037
|
+
const sourcePrefix = resolvedPath ? `${resolvedPath}/` : "";
|
|
1038
|
+
const children = /* @__PURE__ */ new Map();
|
|
1039
|
+
for (const entry of this.#entries.values()) {
|
|
1040
|
+
if (resolvedPath) {
|
|
1041
|
+
if (!entry.path.startsWith(sourcePrefix) || entry.path === resolvedPath) continue;
|
|
1042
|
+
}
|
|
1043
|
+
const relativePath = resolvedPath ? entry.path.slice(sourcePrefix.length) : entry.path;
|
|
1044
|
+
const nextSegment = relativePath.split("/")[0];
|
|
1045
|
+
if (!nextSegment) continue;
|
|
1046
|
+
const childPath = normalizedPath ? `${normalizedPath}/${nextSegment}` : nextSegment;
|
|
1047
|
+
if (children.has(childPath)) continue;
|
|
1048
|
+
const sourceChildPath = resolvedPath ? `${resolvedPath}/${nextSegment}` : nextSegment;
|
|
1049
|
+
const childEntry = this.#entries.get(sourceChildPath);
|
|
1050
|
+
const isDirectory = childEntry?.isDirectory ?? relativePath.includes("/");
|
|
1051
|
+
children.set(childPath, {
|
|
1052
|
+
name: nextSegment,
|
|
1053
|
+
path: childPath,
|
|
1054
|
+
isDirectory,
|
|
1055
|
+
size: isDirectory ? 0 : childEntry?.size ?? entry.size,
|
|
1056
|
+
mtimeMs: childEntry?.mtimeMs ?? entry.mtimeMs
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
return [...children.values()].sort((left, right) => left.name.toLowerCase().localeCompare(right.name.toLowerCase()));
|
|
1060
|
+
}
|
|
1061
|
+
async readFile(subPath) {
|
|
1062
|
+
const normalizedPath = normalizeArchiveLookupPath(subPath);
|
|
1063
|
+
if (!normalizedPath) throw new ToolError("Archive file path is required");
|
|
1064
|
+
const resolvedPath = resolveDirectoryAliasPath(this.#entries, normalizedPath);
|
|
1065
|
+
if (resolvedPath === "") throw new ToolError(`Archive path '${normalizedPath}' is a directory`);
|
|
1066
|
+
const entry = this.#entries.get(resolvedPath);
|
|
1067
|
+
if (!entry) throw new ToolError(`Archive file '${normalizedPath}' not found`);
|
|
1068
|
+
if (entry.isDirectory) throw new ToolError(`Archive path '${normalizedPath}' is a directory`);
|
|
1069
|
+
if (!entry.storage) throw new ToolError(`Archive file '${normalizedPath}' has no readable storage`);
|
|
1070
|
+
assertArchiveMemberSize(entry.size, normalizedPath);
|
|
1071
|
+
let bytes;
|
|
1072
|
+
if (entry.storage.type === "tar") bytes = extractTarMember(entry.storage, entry.size, normalizedPath);
|
|
1073
|
+
else if (entry.storage.type === "tar-link") throwUnreadableTarLink(entry.storage, normalizedPath);
|
|
1074
|
+
else bytes = await readZipFileBytes(entry.storage, entry.size);
|
|
1075
|
+
return {
|
|
1076
|
+
path: normalizedPath,
|
|
1077
|
+
isDirectory: false,
|
|
1078
|
+
size: entry.size,
|
|
1079
|
+
mtimeMs: entry.mtimeMs,
|
|
1080
|
+
bytes
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
/**
|
|
1085
|
+
* Open an archive for reading. ZIP archives opened from a path are indexed
|
|
1086
|
+
* lazily via ranged central-directory reads (members inflate on demand); tar
|
|
1087
|
+
* archives and in-memory ZIPs are read from a single buffer.
|
|
1088
|
+
*/
|
|
1089
|
+
async function openArchive(source) {
|
|
1090
|
+
if (typeof source !== "string" && "bytes" in source) {
|
|
1091
|
+
if (source.format === "zip") return new ArchiveReader(source.format, await readZipEntries(memoryByteSource(source.bytes)));
|
|
1092
|
+
return new ArchiveReader(source.format, readTarEntries(source.bytes));
|
|
1093
|
+
}
|
|
1094
|
+
const filePath = typeof source === "string" ? source : source.path;
|
|
1095
|
+
const format = typeof source === "string" ? archiveFormatFromPath(filePath) : source.format;
|
|
1096
|
+
if (!format) throw new ToolError(`Unsupported archive format: ${filePath}`);
|
|
1097
|
+
if (format === "zip") return new ArchiveReader(format, await readZipEntries(fileByteSource(filePath)));
|
|
1098
|
+
const file = Bun.file(filePath);
|
|
1099
|
+
assertTarArchiveSize(file.size);
|
|
1100
|
+
return new ArchiveReader(format, readTarEntries(await file.bytes()));
|
|
1101
|
+
}
|
|
1102
|
+
/** Render the top-level entries of an in-memory archive as one line each. */
|
|
1103
|
+
async function listArchiveRoot(bytes, format, opts = {}) {
|
|
1104
|
+
const entries = (await openArchive({
|
|
1105
|
+
bytes,
|
|
1106
|
+
format
|
|
1107
|
+
})).listDirectory("");
|
|
1108
|
+
const lines = formatArchiveEntryLines(opts.limit !== void 0 && opts.limit > 0 ? entries.slice(0, opts.limit) : entries);
|
|
1109
|
+
return lines.length > 0 ? lines.join("\n") : "(empty archive directory)";
|
|
1110
|
+
}
|
|
1111
|
+
async function resolveArchiveBytes(source) {
|
|
1112
|
+
if (typeof source !== "string" && "bytes" in source) return source;
|
|
1113
|
+
const filePath = typeof source === "string" ? source : source.path;
|
|
1114
|
+
const format = typeof source === "string" ? archiveFormatFromPath(filePath) : source.format;
|
|
1115
|
+
if (!format) throw new ToolError(`Unsupported archive format: ${filePath}`);
|
|
1116
|
+
const file = Bun.file(filePath);
|
|
1117
|
+
if (format !== "zip") assertTarArchiveSize(file.size);
|
|
1118
|
+
return {
|
|
1119
|
+
bytes: await file.bytes(),
|
|
1120
|
+
format
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
async function memberToBytes(content) {
|
|
1124
|
+
if (typeof content === "string") return ENCODER.encode(content);
|
|
1125
|
+
if (content instanceof Uint8Array) return content;
|
|
1126
|
+
return new Uint8Array(await content.arrayBuffer());
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Fully materialize every file member into a `path → content` map: ZIP members
|
|
1130
|
+
* are inflated in memory, tar members are sliced from the decoded archive
|
|
1131
|
+
* buffer. Use this when you need every entry (rewrite, extract); for browsing
|
|
1132
|
+
* or single-member reads prefer `openArchive`, which is lazy for ZIP.
|
|
1133
|
+
*/
|
|
1134
|
+
async function readArchiveEntries(source) {
|
|
1135
|
+
const { bytes, format } = await resolveArchiveBytes(source);
|
|
1136
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1137
|
+
if (format === "zip") {
|
|
1138
|
+
const unzipped = unzip(bytes);
|
|
1139
|
+
for (const name in unzipped) entries.set(name, unzipped[name]);
|
|
1140
|
+
return entries;
|
|
1141
|
+
}
|
|
1142
|
+
for (const entry of readTarEntries(bytes)) {
|
|
1143
|
+
if (entry.isDirectory) {
|
|
1144
|
+
if (entry.storage?.type === "tar-link") throwUnreadableTarLink(entry.storage, entry.path);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (!entry.storage) throw new ToolError(`Archive file '${entry.path}' has no readable storage`);
|
|
1148
|
+
if (entry.storage.type === "tar-link") throwUnreadableTarLink(entry.storage, entry.path);
|
|
1149
|
+
if (entry.storage.type !== "tar") throw new ToolError(`Archive file '${entry.path}' has invalid tar storage`);
|
|
1150
|
+
entries.set(entry.path, extractTarMember(entry.storage, entry.size, entry.path));
|
|
1151
|
+
}
|
|
1152
|
+
return entries;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Serialize `entries` into an archive of `format` and write it to `destPath`.
|
|
1156
|
+
* ZIP is framed in memory, tar / tar.gz via `Bun.Archive` (gzip for tar.gz).
|
|
1157
|
+
* String members are encoded as UTF-8.
|
|
1158
|
+
*/
|
|
1159
|
+
async function writeArchive(destPath, format, entries) {
|
|
1160
|
+
if (format === "zip") {
|
|
1161
|
+
const record = {};
|
|
1162
|
+
for (const [name, content] of entries) record[name.replace(/\\/g, "/")] = await memberToBytes(content);
|
|
1163
|
+
await Bun.write(destPath, zip(record));
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const record = {};
|
|
1167
|
+
for (const [name, content] of entries) record[name.replace(/\\/g, "/")] = content;
|
|
1168
|
+
await Bun.Archive.write(destPath, record, format === "tar.gz" ? { compress: "gzip" } : void 0);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Extract every file member to `destDir`, creating parent directories as
|
|
1172
|
+
* needed. Entries that would escape `destDir` (via `..` or an absolute path)
|
|
1173
|
+
* are rejected. Returns the number of files written.
|
|
1174
|
+
*/
|
|
1175
|
+
async function extractArchive(source, destDir) {
|
|
1176
|
+
const extractRoot = path.resolve(destDir);
|
|
1177
|
+
const entries = await readArchiveEntries(source);
|
|
1178
|
+
let count = 0;
|
|
1179
|
+
for (const [name, content] of entries) {
|
|
1180
|
+
if (name.endsWith("/")) continue;
|
|
1181
|
+
const outputPath = path.resolve(extractRoot, name);
|
|
1182
|
+
if (!outputPath.startsWith(extractRoot + path.sep)) throw new ToolError(`Archive entry escapes extraction dir: ${name}`);
|
|
1183
|
+
await Bun.write(outputPath, content);
|
|
1184
|
+
count++;
|
|
1185
|
+
}
|
|
1186
|
+
return count;
|
|
1187
|
+
}
|
|
1188
|
+
function writeUInt16LE(buf, offset, value) {
|
|
1189
|
+
buf[offset] = value & 255;
|
|
1190
|
+
buf[offset + 1] = value >>> 8 & 255;
|
|
1191
|
+
}
|
|
1192
|
+
function writeUInt32LE(buf, offset, value) {
|
|
1193
|
+
buf[offset] = value & 255;
|
|
1194
|
+
buf[offset + 1] = value >>> 8 & 255;
|
|
1195
|
+
buf[offset + 2] = value >>> 16 & 255;
|
|
1196
|
+
buf[offset + 3] = value >>> 24 & 255;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Frame a `path → bytes` map into a ZIP archive in memory. Each member is raw
|
|
1200
|
+
* DEFLATE unless that would not shrink it, in which case it is stored. ZIP64 is
|
|
1201
|
+
* not emitted; archives beyond the 32-bit limits throw rather than corrupt.
|
|
1202
|
+
*/
|
|
1203
|
+
function zip(entries) {
|
|
1204
|
+
const localParts = [];
|
|
1205
|
+
const centralParts = [];
|
|
1206
|
+
let offset = 0;
|
|
1207
|
+
let count = 0;
|
|
1208
|
+
for (const name in entries) {
|
|
1209
|
+
const data = entries[name];
|
|
1210
|
+
const nameBytes = ENCODER.encode(name);
|
|
1211
|
+
const crc = zlib.crc32(data) >>> 0;
|
|
1212
|
+
const uncompressedSize = data.byteLength;
|
|
1213
|
+
const deflated = zlib.deflateRawSync(data);
|
|
1214
|
+
const stored = deflated.byteLength >= uncompressedSize;
|
|
1215
|
+
const method = stored ? ZIP_STORED_COMPRESSION : ZIP_DEFLATE_COMPRESSION;
|
|
1216
|
+
const payload = stored ? data : deflated;
|
|
1217
|
+
if (count + 1 >= ZIP_UINT16_MAX || nameBytes.byteLength > ZIP_UINT16_MAX || uncompressedSize >= ZIP_UINT32_MAX || offset + 30 + nameBytes.byteLength + payload.byteLength >= ZIP_UINT32_MAX) throw new ToolError("ZIP archive is too large to write (ZIP64 is not supported)");
|
|
1218
|
+
const header = new Uint8Array(30 + nameBytes.byteLength);
|
|
1219
|
+
writeUInt32LE(header, 0, ZIP_LOCAL_FILE_HEADER_SIGNATURE);
|
|
1220
|
+
writeUInt16LE(header, 4, 20);
|
|
1221
|
+
writeUInt16LE(header, 6, ZIP_UTF8_FLAG);
|
|
1222
|
+
writeUInt16LE(header, 8, method);
|
|
1223
|
+
writeUInt16LE(header, 12, 33);
|
|
1224
|
+
writeUInt32LE(header, 14, crc);
|
|
1225
|
+
writeUInt32LE(header, 18, payload.byteLength);
|
|
1226
|
+
writeUInt32LE(header, 22, uncompressedSize);
|
|
1227
|
+
writeUInt16LE(header, 26, nameBytes.byteLength);
|
|
1228
|
+
header.set(nameBytes, 30);
|
|
1229
|
+
localParts.push(header, payload);
|
|
1230
|
+
const record = new Uint8Array(46 + nameBytes.byteLength);
|
|
1231
|
+
writeUInt32LE(record, 0, ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE);
|
|
1232
|
+
writeUInt16LE(record, 4, 20);
|
|
1233
|
+
writeUInt16LE(record, 6, 20);
|
|
1234
|
+
writeUInt16LE(record, 8, ZIP_UTF8_FLAG);
|
|
1235
|
+
writeUInt16LE(record, 10, method);
|
|
1236
|
+
writeUInt16LE(record, 14, 33);
|
|
1237
|
+
writeUInt32LE(record, 16, crc);
|
|
1238
|
+
writeUInt32LE(record, 20, payload.byteLength);
|
|
1239
|
+
writeUInt32LE(record, 24, uncompressedSize);
|
|
1240
|
+
writeUInt16LE(record, 28, nameBytes.byteLength);
|
|
1241
|
+
writeUInt32LE(record, 42, offset);
|
|
1242
|
+
record.set(nameBytes, 46);
|
|
1243
|
+
centralParts.push(record);
|
|
1244
|
+
offset += header.byteLength + payload.byteLength;
|
|
1245
|
+
count++;
|
|
1246
|
+
}
|
|
1247
|
+
const centralSize = centralParts.reduce((sum, part) => sum + part.byteLength, 0);
|
|
1248
|
+
if (centralSize >= ZIP_UINT32_MAX || offset + centralSize + ZIP_EOCD_MIN_LENGTH >= ZIP_UINT32_MAX) throw new ToolError("ZIP archive is too large to write (ZIP64 is not supported)");
|
|
1249
|
+
const eocd = new Uint8Array(ZIP_EOCD_MIN_LENGTH);
|
|
1250
|
+
writeUInt32LE(eocd, 0, ZIP_EOCD_SIGNATURE);
|
|
1251
|
+
writeUInt16LE(eocd, 8, count);
|
|
1252
|
+
writeUInt16LE(eocd, 10, count);
|
|
1253
|
+
writeUInt32LE(eocd, 12, centralSize);
|
|
1254
|
+
writeUInt32LE(eocd, 16, offset);
|
|
1255
|
+
const out = new Uint8Array(offset + centralSize + ZIP_EOCD_MIN_LENGTH);
|
|
1256
|
+
let pos = 0;
|
|
1257
|
+
for (const part of localParts) {
|
|
1258
|
+
out.set(part, pos);
|
|
1259
|
+
pos += part.byteLength;
|
|
1260
|
+
}
|
|
1261
|
+
for (const part of centralParts) {
|
|
1262
|
+
out.set(part, pos);
|
|
1263
|
+
pos += part.byteLength;
|
|
1264
|
+
}
|
|
1265
|
+
out.set(eocd, pos);
|
|
1266
|
+
return out;
|
|
1267
|
+
}
|
|
1268
|
+
function readZip64CentralDirectoryInfoSync(bytes, eocdOffset) {
|
|
1269
|
+
const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_LENGTH;
|
|
1270
|
+
if (locatorOffset < 0) return void 0;
|
|
1271
|
+
const locator = readMemoryRange(bytes, locatorOffset, locatorOffset + ZIP64_EOCD_LOCATOR_LENGTH);
|
|
1272
|
+
if (readUInt32LE(locator, 0) !== ZIP64_EOCD_LOCATOR_SIGNATURE) return void 0;
|
|
1273
|
+
if (readUInt32LE(locator, 4) !== 0 || readUInt32LE(locator, 16) > 1) throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
1274
|
+
const zip64EocdOffset = readUInt64LEAsNumber(locator, 8);
|
|
1275
|
+
const record = readMemoryRange(bytes, zip64EocdOffset, zip64EocdOffset + 56);
|
|
1276
|
+
if (readUInt32LE(record, 0) !== ZIP64_EOCD_SIGNATURE) throw new ToolError("Invalid ZIP archive: missing ZIP64 end of central directory");
|
|
1277
|
+
if (readUInt32LE(record, 16) !== 0 || readUInt32LE(record, 20) !== 0) throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
1278
|
+
return {
|
|
1279
|
+
entries: readUInt64LEAsNumber(record, 32),
|
|
1280
|
+
size: readUInt64LEAsNumber(record, 40),
|
|
1281
|
+
offset: readUInt64LEAsNumber(record, 48)
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function readCentralDirectoryInfoSync(bytes) {
|
|
1285
|
+
const fileSize = bytes.byteLength;
|
|
1286
|
+
if (fileSize < ZIP_EOCD_MIN_LENGTH) throw new ToolError("Invalid ZIP archive: missing end of central directory");
|
|
1287
|
+
const tailStart = fileSize - Math.min(fileSize, 65557);
|
|
1288
|
+
const tail = readMemoryRange(bytes, tailStart, fileSize);
|
|
1289
|
+
const eocdIndex = findEndOfCentralDirectory(tail);
|
|
1290
|
+
if (readUInt16LE(tail, eocdIndex + 4) !== 0 || readUInt16LE(tail, eocdIndex + 6) !== 0) throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
1291
|
+
let entries = readUInt16LE(tail, eocdIndex + 10);
|
|
1292
|
+
let size = readUInt32LE(tail, eocdIndex + 12);
|
|
1293
|
+
let offset = readUInt32LE(tail, eocdIndex + 16);
|
|
1294
|
+
const needsZip64 = entries === ZIP_UINT16_MAX || size === ZIP_UINT32_MAX || offset === ZIP_UINT32_MAX;
|
|
1295
|
+
const zip64Info = readZip64CentralDirectoryInfoSync(bytes, tailStart + eocdIndex);
|
|
1296
|
+
if (zip64Info) ({entries, size, offset} = zip64Info);
|
|
1297
|
+
else if (needsZip64) throw new ToolError("Invalid ZIP archive: missing ZIP64 central directory metadata");
|
|
1298
|
+
if (offset + size > fileSize) throw new ToolError("Invalid ZIP archive: central directory exceeds file size");
|
|
1299
|
+
return {
|
|
1300
|
+
entries,
|
|
1301
|
+
offset,
|
|
1302
|
+
size
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function extractZipMember(bytes, storage, uncompressedSize) {
|
|
1306
|
+
if ((storage.flags & ZIP_ENCRYPTED_FLAG) !== 0) throw new ToolError("Encrypted ZIP entries are not supported");
|
|
1307
|
+
const headerStart = storage.localHeaderOffset;
|
|
1308
|
+
const localHeader = readMemoryRange(bytes, headerStart, headerStart + 30);
|
|
1309
|
+
if (readUInt32LE(localHeader, 0) !== ZIP_LOCAL_FILE_HEADER_SIGNATURE) throw new ToolError("Invalid ZIP archive: malformed local file header");
|
|
1310
|
+
const fileNameLength = readUInt16LE(localHeader, 26);
|
|
1311
|
+
const extraLength = readUInt16LE(localHeader, 28);
|
|
1312
|
+
const dataStart = headerStart + 30 + fileNameLength + extraLength;
|
|
1313
|
+
return decodeZipMember(readMemoryRange(bytes, dataStart, dataStart + storage.compressedSize), storage.compression, uncompressedSize);
|
|
1314
|
+
}
|
|
1315
|
+
//#endregion
|
|
1316
|
+
export { openArchive as a, sniffArchiveFormat as c, writeArchive as d, ToolAbortError as f, listArchiveRoot as i, unzip as l, throwIfAborted as m, extractArchive as n, parseArchivePathCandidates as o, ToolError as p, formatArchiveEntryLines as r, readArchiveEntries as s, archiveFormatFromPath as t, unzipText as u };
|