@gajae-code/coding-agent 0.18.12 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/dist/types/capability/rule.d.ts +7 -2
  3. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  4. package/dist/types/cli/args.d.ts +7 -2
  5. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  6. package/dist/types/cli/customize-doctor.d.ts +4 -3
  7. package/dist/types/cli/public-command-errors.d.ts +1 -0
  8. package/dist/types/cli/root-flags.d.ts +0 -12
  9. package/dist/types/cli/update-cli.d.ts +7 -2
  10. package/dist/types/cli-main.d.ts +0 -6
  11. package/dist/types/commands/launch.d.ts +0 -6
  12. package/dist/types/commands/migrate.d.ts +1 -1
  13. package/dist/types/commands/sdk.d.ts +8 -0
  14. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  15. package/dist/types/config/autorouting-tier-map.d.ts +23 -1
  16. package/dist/types/config/config-hot-reload.d.ts +5 -0
  17. package/dist/types/config/file-lock.d.ts +44 -2
  18. package/dist/types/config/model-profile-activation.d.ts +19 -5
  19. package/dist/types/config/model-registry.d.ts +16 -3
  20. package/dist/types/config/model-resolver.d.ts +5 -4
  21. package/dist/types/config/settings-schema.d.ts +11 -1
  22. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  23. package/dist/types/customization/import.d.ts +2 -0
  24. package/dist/types/customization/types.d.ts +1 -1
  25. package/dist/types/dap/session.d.ts +11 -2
  26. package/dist/types/discovery/helpers.d.ts +7 -2
  27. package/dist/types/edit/index.d.ts +5 -0
  28. package/dist/types/edit/modes/patch.d.ts +9 -0
  29. package/dist/types/eval/py/runtime.d.ts +9 -0
  30. package/dist/types/export/ttsr.d.ts +19 -0
  31. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  32. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  33. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  34. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  36. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  37. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  38. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  39. package/dist/types/hindsight/content.d.ts +5 -6
  40. package/dist/types/internal-urls/types.d.ts +2 -0
  41. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  42. package/dist/types/main.d.ts +23 -18
  43. package/dist/types/migrate/action-planner.d.ts +10 -1
  44. package/dist/types/migrate/adapters/codex.d.ts +1 -1
  45. package/dist/types/migrate/adapters/index.d.ts +35 -3
  46. package/dist/types/migrate/hook-module.d.ts +23 -0
  47. package/dist/types/migrate/report.d.ts +2 -1
  48. package/dist/types/migrate/types.d.ts +69 -2
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +43 -0
  51. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  52. package/dist/types/modes/utils/host-status.d.ts +15 -11
  53. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  54. package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
  55. package/dist/types/sdk/broker/broker.d.ts +22 -1
  56. package/dist/types/sdk/broker/ensure.d.ts +5 -2
  57. package/dist/types/sdk/broker/hop.d.ts +9 -0
  58. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  59. package/dist/types/sdk/broker/session-index.d.ts +2 -0
  60. package/dist/types/sdk/broker/transport.d.ts +2 -0
  61. package/dist/types/sdk/bus/index.d.ts +21 -0
  62. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  63. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  64. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  65. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  66. package/dist/types/sdk/host/events.d.ts +4 -0
  67. package/dist/types/sdk/host/host.d.ts +2 -1
  68. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  69. package/dist/types/sdk/host/session-runtime.d.ts +8 -8
  70. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  71. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  72. package/dist/types/sdk/prompt-status.d.ts +11 -4
  73. package/dist/types/session/agent-session.d.ts +50 -8
  74. package/dist/types/session/session-manager.d.ts +16 -0
  75. package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
  76. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  77. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  78. package/dist/types/system-prompt.d.ts +3 -2
  79. package/dist/types/task/role-body.d.ts +11 -0
  80. package/dist/types/tools/awaiting-input.d.ts +28 -0
  81. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
  82. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  83. package/dist/types/tools/bash.d.ts +7 -0
  84. package/dist/types/tools/descriptors.d.ts +13 -2
  85. package/dist/types/tools/fetch.d.ts +6 -0
  86. package/dist/types/tools/irc.d.ts +5 -3
  87. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  88. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +2 -0
  90. package/dist/types/utils/file-mentions.d.ts +5 -0
  91. package/dist/types/utils/image-limits.d.ts +7 -0
  92. package/dist/types/utils/image-loading.d.ts +0 -1
  93. package/dist/types/utils/open.d.ts +0 -1
  94. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  95. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  96. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  97. package/dist/types/utils/title-generator.d.ts +13 -3
  98. package/dist/types/web/search/providers/codex.d.ts +6 -7
  99. package/package.json +9 -8
  100. package/scripts/acp-conformance-agent.ts +12 -2
  101. package/scripts/generate-sdk-operation-inventory.ts +16 -0
  102. package/src/capability/fs.ts +31 -3
  103. package/src/capability/rule.ts +11 -2
  104. package/src/cli/append-system-prompt.ts +12 -0
  105. package/src/cli/args.ts +11 -7
  106. package/src/cli/customize-doctor-precedence.ts +31 -0
  107. package/src/cli/customize-doctor.ts +49 -39
  108. package/src/cli/fast-help.ts +0 -2
  109. package/src/cli/migrate-cli.ts +14 -3
  110. package/src/cli/public-command-errors.ts +2 -0
  111. package/src/cli/root-flags.ts +0 -2
  112. package/src/cli/update-cli.ts +247 -6
  113. package/src/commands/harness.ts +25 -15
  114. package/src/commands/migrate.ts +3 -3
  115. package/src/commands/sdk.ts +81 -11
  116. package/src/commit/agentic/agent.ts +6 -6
  117. package/src/commit/analysis/conventional.ts +14 -5
  118. package/src/commit/map-reduce/index.ts +7 -7
  119. package/src/commit/pipeline.ts +12 -12
  120. package/src/config/autorouting-tier-map.ts +20 -1
  121. package/src/config/config-file.ts +106 -3
  122. package/src/config/config-hot-reload.ts +8 -0
  123. package/src/config/file-lock.ts +1431 -304
  124. package/src/config/keybindings.ts +154 -7
  125. package/src/config/model-profile-activation.ts +177 -39
  126. package/src/config/model-registry.ts +380 -55
  127. package/src/config/model-resolver.ts +49 -21
  128. package/src/config/settings-schema.ts +13 -1
  129. package/src/config/settings.ts +2 -0
  130. package/src/coordinator-mcp/server.ts +37 -2
  131. package/src/crash/sanitize.ts +3 -1
  132. package/src/customization/import.ts +276 -7
  133. package/src/customization/inventory.ts +24 -8
  134. package/src/customization/types.ts +1 -0
  135. package/src/dap/client.ts +3 -2
  136. package/src/dap/session.ts +161 -36
  137. package/src/debug/crash-diagnostics.ts +87 -5
  138. package/src/debug/report-bundle.ts +71 -2
  139. package/src/discovery/builtin.ts +4 -3
  140. package/src/discovery/helpers.ts +24 -14
  141. package/src/edit/index.ts +191 -5
  142. package/src/edit/modes/patch.ts +27 -0
  143. package/src/eval/py/runtime.ts +44 -4
  144. package/src/exec/non-interactive-env.ts +0 -1
  145. package/src/export/html/index.ts +60 -20
  146. package/src/export/ttsr.ts +176 -1
  147. package/src/extensibility/extensions/loader.ts +2 -0
  148. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  149. package/src/extensibility/extensions/types.ts +5 -1
  150. package/src/extensibility/hooks/loader.ts +10 -2
  151. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  152. package/src/extensibility/slash-commands.ts +5 -0
  153. package/src/gjc-runtime/gc-runtime.ts +4 -1
  154. package/src/gjc-runtime/launch-tmux.ts +23 -13
  155. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  156. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  157. package/src/gjc-runtime/state-writer.ts +109 -5
  158. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  159. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  160. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  161. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  162. package/src/harness-control-plane/finalize.ts +11 -0
  163. package/src/harness-control-plane/receipt-spool.ts +105 -28
  164. package/src/harness-control-plane/storage.ts +124 -6
  165. package/src/hindsight/content.ts +116 -16
  166. package/src/hindsight/mental-models.ts +22 -10
  167. package/src/hindsight/state.ts +7 -4
  168. package/src/hooks/native-skill-hook.ts +3 -1
  169. package/src/internal-urls/artifact-protocol.ts +3 -1
  170. package/src/internal-urls/docs-index.generated.ts +14 -14
  171. package/src/internal-urls/local-protocol.ts +28 -3
  172. package/src/internal-urls/types.ts +2 -0
  173. package/src/lsp/client.ts +2 -1
  174. package/src/lsp/clients/biome-client.ts +2 -0
  175. package/src/lsp/clients/swiftlint-client.ts +2 -0
  176. package/src/main.ts +330 -92
  177. package/src/memories/index.ts +25 -2
  178. package/src/migrate/action-planner.ts +517 -2
  179. package/src/migrate/adapters/claude-code.ts +141 -3
  180. package/src/migrate/adapters/codex.ts +57 -5
  181. package/src/migrate/adapters/index.ts +240 -5
  182. package/src/migrate/adapters/opencode.ts +4 -0
  183. package/src/migrate/executor.ts +141 -3
  184. package/src/migrate/hook-module.ts +193 -0
  185. package/src/migrate/report.ts +45 -1
  186. package/src/migrate/types.ts +73 -3
  187. package/src/modes/acp/acp-agent.ts +10 -1
  188. package/src/modes/components/agent-dashboard.ts +20 -4
  189. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  190. package/src/modes/components/customization/import-wizard.ts +29 -3
  191. package/src/modes/components/model-selector.ts +157 -103
  192. package/src/modes/components/welcome.ts +3 -1
  193. package/src/modes/controllers/command-controller.ts +271 -16
  194. package/src/modes/controllers/event-controller.ts +1 -1
  195. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  196. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  197. package/src/modes/controllers/input-controller.ts +2 -0
  198. package/src/modes/controllers/plan-mode-controller.ts +49 -4
  199. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  200. package/src/modes/controllers/selector-controller.ts +14 -1
  201. package/src/modes/interactive-mode.ts +55 -4
  202. package/src/modes/prompt-suggestion-controller.ts +1 -1
  203. package/src/modes/runtime-init.ts +5 -1
  204. package/src/modes/utils/host-status.ts +21 -10
  205. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  206. package/src/prompts/system/irc-incoming.md +3 -1
  207. package/src/prompts/tools/irc.md +1 -1
  208. package/src/runtime-mcp/manager.ts +8 -3
  209. package/src/runtime-mcp/project-host-values.ts +12 -8
  210. package/src/runtime-mcp/tool-bridge.ts +68 -4
  211. package/src/runtime-mcp/transports/stdio.ts +2 -1
  212. package/src/sdk/broker/broker-exit.ts +2 -0
  213. package/src/sdk/broker/broker.ts +143 -4
  214. package/src/sdk/broker/ensure.ts +74 -6
  215. package/src/sdk/broker/hop.ts +13 -1
  216. package/src/sdk/broker/lifecycle-ledger.ts +3 -0
  217. package/src/sdk/broker/lifecycle.ts +102 -6
  218. package/src/sdk/broker/session-index.ts +29 -4
  219. package/src/sdk/broker/transport.ts +17 -1
  220. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  221. package/src/sdk/bus/index.ts +408 -14
  222. package/src/sdk/bus/session-control.ts +2 -0
  223. package/src/sdk/bus/slack-daemon.ts +105 -9
  224. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  225. package/src/sdk/bus/telegram-daemon.ts +10 -2
  226. package/src/sdk/bus/threaded-render.ts +20 -1
  227. package/src/sdk/cli/session-cli.ts +2 -0
  228. package/src/sdk/host/control/dispatch.ts +13 -1
  229. package/src/sdk/host/control/operations.ts +10 -1
  230. package/src/sdk/host/events.ts +67 -4
  231. package/src/sdk/host/host.ts +8 -2
  232. package/src/sdk/host/prompt-image-upload.ts +438 -0
  233. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  234. package/src/sdk/host/session-runtime.test.ts +1537 -24
  235. package/src/sdk/host/session-runtime.ts +332 -36
  236. package/src/sdk/host/surface-policy.ts +4 -0
  237. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  238. package/src/sdk/host/websocket-transport.ts +19 -7
  239. package/src/sdk/lifecycle/service.ts +14 -1
  240. package/src/sdk/prompt-status.ts +6 -4
  241. package/src/sdk/protocol/adapter-validation.ts +28 -1
  242. package/src/sdk/protocol/operation-inventory.generated.json +160 -0
  243. package/src/sdk/protocol/operation-registry.ts +33 -3
  244. package/src/sdk/session.ts +123 -36
  245. package/src/session/agent-session.ts +1614 -329
  246. package/src/session/history-storage.ts +182 -12
  247. package/src/session/internal/managed-session-scope.ts +47 -18
  248. package/src/session/internal/managed-session-storage.ts +71 -1
  249. package/src/session/session-manager.ts +90 -5
  250. package/src/session-import/redact.ts +53 -2
  251. package/src/skill-state/workflow-mutation-guard.ts +1498 -18
  252. package/src/slash-commands/builtin-registry.ts +6 -3
  253. package/src/slash-commands/helpers/mcp.ts +11 -3
  254. package/src/ssh/ssh-executor.ts +9 -2
  255. package/src/stt/recorder.ts +26 -9
  256. package/src/system-prompt.ts +61 -16
  257. package/src/task/executor.ts +12 -8
  258. package/src/task/role-body.ts +84 -0
  259. package/src/task/token-log-mode.test.ts +162 -0
  260. package/src/task/token-log.ts +91 -2
  261. package/src/tools/archive-reader.ts +650 -37
  262. package/src/tools/ask.ts +1 -0
  263. package/src/tools/ast-grep.ts +7 -2
  264. package/src/tools/auto-generated-guard.ts +12 -3
  265. package/src/tools/awaiting-input.ts +114 -0
  266. package/src/tools/bash-allowed-prefixes.ts +61 -1
  267. package/src/tools/bash-skill-urls.ts +58 -5
  268. package/src/tools/bash.ts +99 -26
  269. package/src/tools/browser/tab-worker.ts +39 -13
  270. package/src/tools/browser.ts +17 -7
  271. package/src/tools/cron.ts +10 -1
  272. package/src/tools/debug.ts +9 -2
  273. package/src/tools/descriptors.test.ts +4 -4
  274. package/src/tools/descriptors.ts +16 -4
  275. package/src/tools/eval.ts +15 -0
  276. package/src/tools/fetch.ts +90 -2
  277. package/src/tools/gh.ts +27 -12
  278. package/src/tools/image-gen.ts +20 -6
  279. package/src/tools/irc.ts +5 -3
  280. package/src/tools/monitor.ts +9 -1
  281. package/src/tools/output-meta.ts +51 -3
  282. package/src/tools/read.ts +9 -2
  283. package/src/tools/recipe/runner.ts +23 -12
  284. package/src/tools/render-utils.ts +2 -2
  285. package/src/tools/search.ts +3 -2
  286. package/src/tools/ssh.ts +274 -8
  287. package/src/tools/subagent.ts +10 -4
  288. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  289. package/src/tools/tool-catalog.generated.ts +1 -1
  290. package/src/tools/ultragoal-ask-guard.ts +13 -1
  291. package/src/tools/write.ts +7 -0
  292. package/src/tui/tree-list.ts +15 -1
  293. package/src/utils/commit-message-generator.ts +3 -2
  294. package/src/utils/external-editor.ts +21 -7
  295. package/src/utils/file-mentions.ts +46 -6
  296. package/src/utils/git.ts +38 -7
  297. package/src/utils/image-limits.ts +8 -0
  298. package/src/utils/image-loading.ts +1 -1
  299. package/src/utils/open.ts +73 -4
  300. package/src/utils/pasted-image-loading.ts +9 -8
  301. package/src/utils/prompt-suggestion.ts +5 -4
  302. package/src/utils/shell-snapshot.ts +9 -1
  303. package/src/utils/title-generator.ts +65 -20
  304. package/src/web/search/index.ts +2 -1
  305. package/src/web/search/providers/codex.ts +29 -32
@@ -1,5 +1,26 @@
1
+ import { gunzipSync } from "node:zlib";
1
2
  import { ToolError } from "./tool-errors";
2
3
 
4
+ const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
5
+ const MAX_ARCHIVE_ENTRY_BYTES = 16 * 1024 * 1024;
6
+ const MAX_ARCHIVE_TOTAL_BYTES = 64 * 1024 * 1024;
7
+ const MAX_ARCHIVE_ENTRY_COUNT = 8192;
8
+ const MAX_ARCHIVE_PATH_DEPTH = 48;
9
+ // Round each 512-byte header plus up to 511 bytes of data padding to 1 KiB per entry.
10
+ const MAX_TAR_CONTAINER_BYTES = MAX_ARCHIVE_TOTAL_BYTES + MAX_ARCHIVE_ENTRY_COUNT * 1024 + 1024;
11
+ const MAX_GZIP_LAYERS = 24;
12
+ const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
13
+ const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
14
+ const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
15
+ const ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06064b50;
16
+ const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE = 0x07064b50;
17
+ const ZIP_DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
18
+ const ZIP_CENTRAL_DIRECTORY_DIGITAL_SIGNATURE = 0x05054b50;
19
+ const ZIP_UINT16_MAX = 0xffff;
20
+ const ZIP_UINT32_MAX = 0xffff_ffff;
21
+ // fflate inflates a pushed chunk before invoking ondata, so keep each input chunk small.
22
+ const ZIP_INFLATE_CHUNK_BYTES = 512;
23
+
3
24
  let fflateModulePromise: Promise<typeof import("fflate")> | undefined;
4
25
  async function loadFflate(): Promise<typeof import("fflate")> {
5
26
  if (!fflateModulePromise) fflateModulePromise = import("fflate");
@@ -75,6 +96,43 @@ function isArchiveDirectoryName(rawPath: string): boolean {
75
96
  return rawPath.endsWith("/") || rawPath.endsWith("\\");
76
97
  }
77
98
 
99
+ function archiveEntryDepth(rawPath: string): number {
100
+ const normalized = normalizeArchiveEntryPath(rawPath);
101
+ if (!normalized) return 0;
102
+ return normalized.split("/").length;
103
+ }
104
+ function concatBytes(chunks: Uint8Array[], total: number): Uint8Array {
105
+ const out = new Uint8Array(total);
106
+ let offset = 0;
107
+ for (const chunk of chunks) {
108
+ out.set(chunk, offset);
109
+ offset += chunk.byteLength;
110
+ }
111
+ return out;
112
+ }
113
+
114
+ function archiveLimitError(kind: "entry" | "total" | "count" | "depth", detail = ""): ToolError {
115
+ const preview = detail.length > 120 ? `${detail.slice(0, 119)}…` : detail;
116
+ switch (kind) {
117
+ case "count":
118
+ return new ToolError(
119
+ `Archive has too many entries (limit ${MAX_ARCHIVE_ENTRY_COUNT}). Extract a subset with a dedicated tool.`,
120
+ );
121
+ case "total":
122
+ return new ToolError(
123
+ `Archive expands past the total limit (${MAX_ARCHIVE_TOTAL_BYTES} bytes). Extract a subset with a dedicated tool.`,
124
+ );
125
+ case "depth":
126
+ return new ToolError(
127
+ `Archive entry '${preview}' is nested too deeply (limit ${MAX_ARCHIVE_PATH_DEPTH}). Extract a subset with a dedicated tool.`,
128
+ );
129
+ case "entry":
130
+ return new ToolError(
131
+ `Archive entry '${preview}' expands past the per-entry limit (${MAX_ARCHIVE_ENTRY_BYTES} bytes). Extract a subset with a dedicated tool.`,
132
+ );
133
+ }
134
+ }
135
+
78
136
  function upsertArchiveEntry(map: Map<string, ArchiveIndexEntry>, entry: ArchiveIndexEntry): void {
79
137
  const existing = map.get(entry.path);
80
138
  if (!existing) {
@@ -123,70 +181,626 @@ function getArchiveFormatFromPath(filePath: string): ArchiveFormat | undefined {
123
181
  return undefined;
124
182
  }
125
183
 
126
- async function readTarEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
127
- let archive: Bun.Archive;
184
+ function gunzipBounded(bytes: Uint8Array, maxOutputLength: number): Uint8Array {
128
185
  try {
129
- archive = new Bun.Archive(bytes);
186
+ return gunzipSync(bytes, { maxOutputLength });
130
187
  } catch (error) {
188
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
189
+ if (code === "ERR_BUFFER_TOO_LARGE") throw archiveLimitError("total");
131
190
  throw new ToolError(error instanceof Error ? error.message : String(error));
132
191
  }
192
+ }
133
193
 
134
- let files: Map<string, File>;
135
- try {
136
- files = await archive.files();
137
- } catch (error) {
138
- throw new ToolError(error instanceof Error ? error.message : String(error));
194
+ function isGzipBytes(bytes: Uint8Array): boolean {
195
+ return bytes.byteLength >= 3 && bytes[0] === 0x1f && bytes[1] === 0x8b;
196
+ }
197
+
198
+ function inflateToTar(bytes: Uint8Array, labeledGzip: boolean): Uint8Array {
199
+ let payload = bytes;
200
+ let layers = 0;
201
+ let totalInflatedBytes = 0;
202
+ let sawGzip = labeledGzip || isGzipBytes(payload);
203
+ while (sawGzip) {
204
+ layers += 1;
205
+ if (layers > MAX_GZIP_LAYERS) throw archiveLimitError("total");
206
+ const remaining = MAX_TAR_CONTAINER_BYTES - totalInflatedBytes;
207
+ if (remaining <= 0) throw archiveLimitError("total");
208
+ payload = gunzipBounded(payload, remaining);
209
+ totalInflatedBytes += payload.byteLength;
210
+ sawGzip = isGzipBytes(payload);
211
+ }
212
+ return payload;
213
+ }
214
+
215
+ function tarText(header: Uint8Array, start: number, length: number): string {
216
+ let end = start;
217
+ const stop = start + length;
218
+ while (end < stop && header[end] !== 0) end += 1;
219
+ return new TextDecoder().decode(header.subarray(start, end));
220
+ }
221
+
222
+ function tarOctal(header: Uint8Array, start: number, length: number): number {
223
+ const text = tarText(header, start, length).trim();
224
+ if (!text) return 0;
225
+ if (!/^[0-7]+$/.test(text)) return Number.NaN;
226
+ return Number.parseInt(text, 8);
227
+ }
228
+
229
+ function tarEntryName(header: Uint8Array): string {
230
+ const name = tarText(header, 0, 100);
231
+ const prefix = tarText(header, 257, 5) === "ustar" ? tarText(header, 345, 155) : "";
232
+ return prefix ? `${prefix}/${name}` : name;
233
+ }
234
+
235
+ type PaxOverrides = { path?: string | null; size?: number | null };
236
+
237
+ function paxOverrides(data: Uint8Array, global = false): PaxOverrides {
238
+ const out: PaxOverrides = {};
239
+ const raw = Buffer.from(data).toString("latin1");
240
+ for (let index = 0; index < raw.length; ) {
241
+ const space = raw.indexOf(" ", index);
242
+ const token = space < 0 ? "" : raw.slice(index, space);
243
+ const length = /^\d+$/.test(token) ? Number(token) : 0;
244
+ const end = index + length;
245
+ const eq = space < 0 ? -1 : raw.indexOf("=", space);
246
+ if (length < 1 || !Number.isSafeInteger(length) || end > raw.length)
247
+ throw new ToolError("Unsupported tar pax header");
248
+ if (space >= end || eq < 0 || eq >= end || raw[end - 1] !== "\n")
249
+ throw new ToolError("Unsupported tar pax header");
250
+ const key = raw.slice(space + 1, eq);
251
+ const value = new TextDecoder().decode(data.subarray(eq + 1, end - 1));
252
+ if (key === "path") out.path = global && value === "" ? null : value;
253
+ else if (key === "size" && global && value === "") out.size = null;
254
+ else if (key === "size" && /^\d+$/.test(value) && Number.isSafeInteger(Number(value))) out.size = Number(value);
255
+ else if (key === "size") throw new ToolError("Unsupported tar size encoding");
256
+ index = end;
139
257
  }
258
+ return out;
259
+ }
140
260
 
261
+ async function readTarEntries(bytes: Uint8Array, labeledGzip: boolean): Promise<ArchiveIndexEntry[]> {
262
+ const payload = inflateToTar(bytes, labeledGzip);
263
+ if (payload.byteLength > MAX_TAR_CONTAINER_BYTES) throw archiveLimitError("total");
141
264
  const entries: ArchiveIndexEntry[] = [];
142
- for (const [rawPath, file] of files) {
143
- const normalizedPath = normalizeArchiveEntryPath(rawPath);
144
- if (!normalizedPath) continue;
145
- const mtimeMs = file.lastModified > 0 ? file.lastModified : undefined;
265
+ let entryCount = 0;
266
+ let memberBytes = 0;
267
+ let offset = 0;
268
+ let longName: string | undefined;
269
+ let pendingPax: PaxOverrides | undefined;
270
+ const globalPax: { path?: string; size?: number } = {};
271
+ while (offset + 512 <= payload.byteLength) {
272
+ const header = payload.subarray(offset, offset + 512);
273
+ if (header.every(byte => byte === 0)) break;
274
+ const headerSize = tarOctal(header, 124, 12);
275
+ if (!Number.isSafeInteger(headerSize) || headerSize < 0) throw new ToolError("Unsupported tar size encoding");
276
+ const typeFlag = header[156] ?? 0;
277
+ const headerName = tarEntryName(header);
278
+ let size = headerSize;
279
+ let rawName = typeFlag === 76 ? headerName : (longName ?? headerName);
280
+ if (typeFlag !== 120 && typeFlag !== 103 && typeFlag !== 76) {
281
+ rawName = pendingPax?.path ?? globalPax.path ?? rawName;
282
+ size = pendingPax?.size ?? globalPax.size ?? size;
283
+ pendingPax = undefined;
284
+ }
285
+ entryCount += 1;
286
+ if (entryCount > MAX_ARCHIVE_ENTRY_COUNT) throw archiveLimitError("count");
287
+ if (typeFlag < 100 && archiveEntryDepth(rawName) > MAX_ARCHIVE_PATH_DEPTH)
288
+ throw archiveLimitError("depth", rawName);
289
+ if (size > MAX_ARCHIVE_TOTAL_BYTES) throw archiveLimitError("total");
290
+ if (size > MAX_ARCHIVE_ENTRY_BYTES) throw archiveLimitError("entry", rawName);
291
+ const isRegular = typeFlag === 0 || typeFlag === 48;
292
+ if (isRegular) {
293
+ memberBytes += size;
294
+ if (memberBytes > MAX_ARCHIVE_TOTAL_BYTES) throw archiveLimitError("total");
295
+ }
296
+ const dataEnd = offset + 512 + size;
297
+ if (dataEnd > payload.byteLength) throw new ToolError("Truncated tar entry");
298
+ const data = payload.subarray(offset + 512, dataEnd);
299
+ offset += 512 + Math.ceil(size / 512) * 512;
300
+ if (typeFlag === 120 || typeFlag === 103) {
301
+ if (typeFlag === 103) {
302
+ const overrides = paxOverrides(data, true);
303
+ if (overrides.path === null) delete globalPax.path;
304
+ else if (overrides.path !== undefined) globalPax.path = overrides.path;
305
+ if (overrides.size === null) delete globalPax.size;
306
+ else if (overrides.size !== undefined) globalPax.size = overrides.size;
307
+ } else pendingPax = paxOverrides(data);
308
+ continue;
309
+ }
310
+ if (typeFlag === 76) {
311
+ longName = new TextDecoder().decode(data).replace(/\0+$/, "");
312
+ continue;
313
+ }
314
+ longName = undefined;
315
+ const normalizedPath = normalizeArchiveEntryPath(rawName);
316
+ if (!normalizedPath || !isRegular) continue;
317
+ const mtime = tarOctal(header, 136, 12);
318
+ const mtimeMs = Number.isFinite(mtime) && mtime > 0 ? mtime * 1000 : undefined;
146
319
  entries.push({
147
320
  path: normalizedPath,
148
321
  isDirectory: false,
149
- size: file.size,
322
+ size,
150
323
  mtimeMs,
151
- storage: { type: "tar", file },
324
+ storage: {
325
+ type: "tar",
326
+ file: new File([data], normalizedPath, { lastModified: mtimeMs ?? 0 }),
327
+ },
152
328
  });
153
329
  }
154
-
155
330
  return entries;
156
331
  }
157
332
 
158
- async function readZipEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
159
- const { unzip } = await loadFflate();
160
- const result = Promise.withResolvers<Record<string, Uint8Array>>();
161
- unzip(bytes, (error, files) => {
162
- if (error) {
163
- result.reject(error);
164
- return;
333
+ interface ZipDirectoryEntry {
334
+ name: string;
335
+ nameBytes: Uint8Array;
336
+ flags: number;
337
+ compression: number;
338
+ crc32: number;
339
+ compressedSize: number;
340
+ uncompressedSize: number;
341
+ localOffset: number;
342
+ }
343
+
344
+ interface Zip64ExtraNeeds {
345
+ uncompressedSize: boolean;
346
+ compressedSize: boolean;
347
+ localOffset: boolean;
348
+ diskNumber: boolean;
349
+ }
350
+
351
+ interface Zip64ExtraValues {
352
+ uncompressedSize?: number;
353
+ compressedSize?: number;
354
+ localOffset?: number;
355
+ diskNumber?: number;
356
+ }
357
+
358
+ function invalidZipArchive(): ToolError {
359
+ return new ToolError("Invalid ZIP archive");
360
+ }
361
+
362
+ function requireZipRange(view: DataView, offset: number, length: number, end = view.byteLength): void {
363
+ if (
364
+ !Number.isSafeInteger(offset) ||
365
+ !Number.isSafeInteger(length) ||
366
+ offset < 0 ||
367
+ length < 0 ||
368
+ offset + length > end
369
+ ) {
370
+ throw invalidZipArchive();
371
+ }
372
+ }
373
+
374
+ function zipUint64(view: DataView, offset: number, end: number): number {
375
+ requireZipRange(view, offset, 8, end);
376
+ const value = view.getBigUint64(offset, true);
377
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw invalidZipArchive();
378
+ return Number(value);
379
+ }
380
+
381
+ function parseZip64Extra(view: DataView, start: number, length: number, needs: Zip64ExtraNeeds): Zip64ExtraValues {
382
+ const end = start + length;
383
+ requireZipRange(view, start, length);
384
+ let zip64Start: number | undefined;
385
+ let zip64End = 0;
386
+ for (let offset = start; offset < end; ) {
387
+ requireZipRange(view, offset, 4, end);
388
+ const id = view.getUint16(offset, true);
389
+ const fieldLength = view.getUint16(offset + 2, true);
390
+ const fieldStart = offset + 4;
391
+ requireZipRange(view, fieldStart, fieldLength, end);
392
+ if (id === 0x0001) {
393
+ if (zip64Start !== undefined) throw invalidZipArchive();
394
+ zip64Start = fieldStart;
395
+ zip64End = fieldStart + fieldLength;
165
396
  }
166
- result.resolve(files);
397
+ offset = fieldStart + fieldLength;
398
+ }
399
+
400
+ const needsZip64 = Object.values(needs).some(Boolean);
401
+ if (zip64Start === undefined) {
402
+ if (needsZip64) throw invalidZipArchive();
403
+ return {};
404
+ }
405
+
406
+ const values: Zip64ExtraValues = {};
407
+ let offset = zip64Start;
408
+ if (needs.uncompressedSize) {
409
+ values.uncompressedSize = zipUint64(view, offset, zip64End);
410
+ offset += 8;
411
+ }
412
+ if (needs.compressedSize) {
413
+ values.compressedSize = zipUint64(view, offset, zip64End);
414
+ offset += 8;
415
+ }
416
+ if (needs.localOffset) {
417
+ values.localOffset = zipUint64(view, offset, zip64End);
418
+ offset += 8;
419
+ }
420
+ if (needs.diskNumber) {
421
+ requireZipRange(view, offset, 4, zip64End);
422
+ values.diskNumber = view.getUint32(offset, true);
423
+ }
424
+ return values;
425
+ }
426
+
427
+ function decodeZipName(bytes: Uint8Array, flags: number): string {
428
+ if ((flags & 0x0800) !== 0) return new TextDecoder().decode(bytes);
429
+ let name = "";
430
+ for (let offset = 0; offset < bytes.byteLength; offset += 0x4000) {
431
+ name += String.fromCharCode(...bytes.subarray(offset, offset + 0x4000));
432
+ }
433
+ return name;
434
+ }
435
+
436
+ function zipDescriptorEnd(
437
+ view: DataView,
438
+ start: number,
439
+ end: number,
440
+ entry: ZipDirectoryEntry,
441
+ zip64Sizes: boolean,
442
+ ): number {
443
+ const matches = (hasSignature: boolean): number | undefined => {
444
+ let offset = start;
445
+ if (hasSignature) {
446
+ if (offset + 4 > end || view.getUint32(offset, true) !== ZIP_DATA_DESCRIPTOR_SIGNATURE) return undefined;
447
+ offset += 4;
448
+ }
449
+ const sizeLength = zip64Sizes ? 8 : 4;
450
+ if (offset + 4 + sizeLength * 2 > end) return undefined;
451
+ if (view.getUint32(offset, true) !== entry.crc32) return undefined;
452
+ const compressedSize = zip64Sizes
453
+ ? view.getBigUint64(offset + 4, true)
454
+ : BigInt(view.getUint32(offset + 4, true));
455
+ const uncompressedSize = zip64Sizes
456
+ ? view.getBigUint64(offset + 4 + sizeLength, true)
457
+ : BigInt(view.getUint32(offset + 4 + sizeLength, true));
458
+ if (compressedSize !== BigInt(entry.compressedSize) || uncompressedSize !== BigInt(entry.uncompressedSize)) {
459
+ return undefined;
460
+ }
461
+ return offset + 4 + sizeLength * 2;
462
+ };
463
+
464
+ const withSignature = matches(true);
465
+ const withoutSignature = matches(false);
466
+ const descriptorEnd = withSignature ?? withoutSignature;
467
+ if (descriptorEnd === undefined) throw invalidZipArchive();
468
+ return descriptorEnd;
469
+ }
470
+
471
+ function validateZipLocalEntry(
472
+ view: DataView,
473
+ bytes: Uint8Array,
474
+ entry: ZipDirectoryEntry,
475
+ offsetCorrection: number,
476
+ centralDirectoryStart: number,
477
+ ): { entry: ZipDirectoryEntry; endOffset: number } {
478
+ const localOffset = entry.localOffset + offsetCorrection;
479
+ requireZipRange(view, localOffset, 30, centralDirectoryStart);
480
+ if (view.getUint32(localOffset, true) !== ZIP_LOCAL_FILE_HEADER_SIGNATURE) throw invalidZipArchive();
481
+ const flags = view.getUint16(localOffset + 6, true);
482
+ const compression = view.getUint16(localOffset + 8, true);
483
+ const crc32 = view.getUint32(localOffset + 14, true);
484
+ const compressedSize32 = view.getUint32(localOffset + 18, true);
485
+ const uncompressedSize32 = view.getUint32(localOffset + 22, true);
486
+ const nameLength = view.getUint16(localOffset + 26, true);
487
+ const extraLength = view.getUint16(localOffset + 28, true);
488
+ const nameStart = localOffset + 30;
489
+ const extraStart = nameStart + nameLength;
490
+ requireZipRange(view, nameStart, nameLength + extraLength, centralDirectoryStart);
491
+ const localName = bytes.subarray(nameStart, extraStart);
492
+ if (
493
+ flags !== entry.flags ||
494
+ compression !== entry.compression ||
495
+ localName.byteLength !== entry.nameBytes.byteLength ||
496
+ localName.some((byte, index) => byte !== entry.nameBytes[index])
497
+ ) {
498
+ throw invalidZipArchive();
499
+ }
500
+
501
+ const zip64Sizes = compressedSize32 === ZIP_UINT32_MAX || uncompressedSize32 === ZIP_UINT32_MAX;
502
+ const zip64 = parseZip64Extra(view, extraStart, extraLength, {
503
+ uncompressedSize: uncompressedSize32 === ZIP_UINT32_MAX,
504
+ compressedSize: compressedSize32 === ZIP_UINT32_MAX,
505
+ localOffset: false,
506
+ diskNumber: false,
167
507
  });
508
+ const compressedSize = compressedSize32 === ZIP_UINT32_MAX ? zip64.compressedSize : compressedSize32;
509
+ const uncompressedSize = uncompressedSize32 === ZIP_UINT32_MAX ? zip64.uncompressedSize : uncompressedSize32;
510
+ if (compressedSize === undefined || uncompressedSize === undefined) throw invalidZipArchive();
511
+
512
+ const hasDescriptor = (flags & 0x0008) !== 0;
513
+ if (hasDescriptor) {
514
+ if (crc32 !== 0 && crc32 !== entry.crc32) throw invalidZipArchive();
515
+ if (compressedSize !== 0 && compressedSize !== entry.compressedSize) throw invalidZipArchive();
516
+ if (uncompressedSize !== 0 && uncompressedSize !== entry.uncompressedSize) throw invalidZipArchive();
517
+ } else if (
518
+ crc32 !== entry.crc32 ||
519
+ compressedSize !== entry.compressedSize ||
520
+ uncompressedSize !== entry.uncompressedSize
521
+ ) {
522
+ throw invalidZipArchive();
523
+ }
168
524
 
169
- let files: Record<string, Uint8Array>;
170
- try {
171
- files = await result.promise;
172
- } catch (error) {
173
- throw new ToolError(error instanceof Error ? error.message : String(error));
525
+ const dataStart = extraStart + extraLength;
526
+ const dataEnd = dataStart + entry.compressedSize;
527
+ if (dataEnd > centralDirectoryStart) throw invalidZipArchive();
528
+ const endOffset = hasDescriptor
529
+ ? zipDescriptorEnd(view, dataEnd, centralDirectoryStart, entry, zip64Sizes)
530
+ : dataEnd;
531
+ return { entry: { ...entry, localOffset }, endOffset };
532
+ }
533
+
534
+ function readZipDirectory(bytes: Uint8Array): ZipDirectoryEntry[] {
535
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
536
+ const searchStart = Math.max(0, bytes.byteLength - 22 - ZIP_UINT16_MAX);
537
+ let lastError: unknown;
538
+ for (let offset = searchStart; offset <= bytes.byteLength - 22; offset++) {
539
+ if (view.getUint32(offset, true) !== ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE) continue;
540
+ const commentLength = view.getUint16(offset + 20, true);
541
+ if (offset + 22 + commentLength !== bytes.byteLength) continue;
542
+ try {
543
+ return parseZipDirectoryAt(bytes, view, offset);
544
+ } catch (error) {
545
+ lastError = error;
546
+ }
547
+ }
548
+ if (lastError instanceof Error) throw lastError;
549
+ throw invalidZipArchive();
550
+ }
551
+
552
+ function parseZipDirectoryAt(bytes: Uint8Array, view: DataView, endOffset: number): ZipDirectoryEntry[] {
553
+ const diskNumber = view.getUint16(endOffset + 4, true);
554
+ const centralDiskNumber = view.getUint16(endOffset + 6, true);
555
+ const entriesOnDisk16 = view.getUint16(endOffset + 8, true);
556
+ const entryCount16 = view.getUint16(endOffset + 10, true);
557
+ const centralSize32 = view.getUint32(endOffset + 12, true);
558
+ const centralOffset32 = view.getUint32(endOffset + 16, true);
559
+ if (
560
+ diskNumber !== 0 ||
561
+ centralDiskNumber !== 0 ||
562
+ (entriesOnDisk16 !== ZIP_UINT16_MAX && entriesOnDisk16 !== entryCount16)
563
+ ) {
564
+ throw invalidZipArchive();
565
+ }
566
+
567
+ const needsZip64 =
568
+ entriesOnDisk16 === ZIP_UINT16_MAX ||
569
+ entryCount16 === ZIP_UINT16_MAX ||
570
+ centralSize32 === ZIP_UINT32_MAX ||
571
+ centralOffset32 === ZIP_UINT32_MAX;
572
+ const locatorOffset = endOffset - 20;
573
+ const hasZip64Locator =
574
+ locatorOffset >= 0 && view.getUint32(locatorOffset, true) === ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE;
575
+ if (needsZip64 && !hasZip64Locator) throw invalidZipArchive();
576
+
577
+ let entryCount: number;
578
+ let centralSize: number;
579
+ let centralOffset: number;
580
+ let centralDirectoryEnd: number;
581
+ if (hasZip64Locator) {
582
+ const locatorDisk = view.getUint32(locatorOffset + 4, true);
583
+ const zip64EndOffset = zipUint64(view, locatorOffset + 8, locatorOffset + 20);
584
+ const totalDisks = view.getUint32(locatorOffset + 16, true);
585
+ if (locatorDisk !== 0 || totalDisks !== 1) throw invalidZipArchive();
586
+ requireZipRange(view, zip64EndOffset, 56, locatorOffset);
587
+ if (view.getUint32(zip64EndOffset, true) !== ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE) throw invalidZipArchive();
588
+ const zip64RecordSize = zipUint64(view, zip64EndOffset + 4, locatorOffset);
589
+ if (zip64RecordSize < 44 || zip64EndOffset + 12 + zip64RecordSize !== locatorOffset) throw invalidZipArchive();
590
+ if (view.getUint32(zip64EndOffset + 16, true) !== 0 || view.getUint32(zip64EndOffset + 20, true) !== 0) {
591
+ throw invalidZipArchive();
592
+ }
593
+ const entriesOnDisk = zipUint64(view, zip64EndOffset + 24, locatorOffset);
594
+ entryCount = zipUint64(view, zip64EndOffset + 32, locatorOffset);
595
+ centralSize = zipUint64(view, zip64EndOffset + 40, locatorOffset);
596
+ centralOffset = zipUint64(view, zip64EndOffset + 48, locatorOffset);
597
+ if (entriesOnDisk !== entryCount) throw invalidZipArchive();
598
+ if (
599
+ (entriesOnDisk16 !== ZIP_UINT16_MAX && entriesOnDisk16 !== entriesOnDisk) ||
600
+ (entryCount16 !== ZIP_UINT16_MAX && entryCount16 !== entryCount) ||
601
+ (centralSize32 !== ZIP_UINT32_MAX && centralSize32 !== centralSize) ||
602
+ (centralOffset32 !== ZIP_UINT32_MAX && centralOffset32 !== centralOffset)
603
+ ) {
604
+ throw invalidZipArchive();
605
+ }
606
+ centralDirectoryEnd = zip64EndOffset;
607
+ } else {
608
+ entryCount = entryCount16;
609
+ centralSize = centralSize32;
610
+ centralOffset = centralOffset32;
611
+ centralDirectoryEnd = endOffset;
612
+ }
613
+ if (entryCount > MAX_ARCHIVE_ENTRY_COUNT) throw archiveLimitError("count");
614
+ if (centralSize > centralDirectoryEnd) throw invalidZipArchive();
615
+ const centralDirectoryStart = centralDirectoryEnd - centralSize;
616
+ const offsetCorrection = centralDirectoryStart - centralOffset;
617
+ if (offsetCorrection < 0) throw invalidZipArchive();
618
+
619
+ const directoryEntries: ZipDirectoryEntry[] = [];
620
+ let totalUncompressed = 0;
621
+ let offset = centralDirectoryStart;
622
+ for (let index = 0; index < entryCount; index++) {
623
+ requireZipRange(view, offset, 46, centralDirectoryEnd);
624
+ if (view.getUint32(offset, true) !== ZIP_CENTRAL_DIRECTORY_SIGNATURE) throw invalidZipArchive();
625
+ const flags = view.getUint16(offset + 8, true);
626
+ const compression = view.getUint16(offset + 10, true);
627
+ const crc32 = view.getUint32(offset + 16, true);
628
+ const compressedSize32 = view.getUint32(offset + 20, true);
629
+ const uncompressedSize32 = view.getUint32(offset + 24, true);
630
+ const nameLength = view.getUint16(offset + 28, true);
631
+ const extraLength = view.getUint16(offset + 30, true);
632
+ const commentLength = view.getUint16(offset + 32, true);
633
+ const diskStart16 = view.getUint16(offset + 34, true);
634
+ const localOffset32 = view.getUint32(offset + 42, true);
635
+ const nameStart = offset + 46;
636
+ const extraStart = nameStart + nameLength;
637
+ const recordLength = 46 + nameLength + extraLength + commentLength;
638
+ requireZipRange(view, offset, recordLength, centralDirectoryEnd);
639
+ const zip64 = parseZip64Extra(view, extraStart, extraLength, {
640
+ uncompressedSize: uncompressedSize32 === ZIP_UINT32_MAX,
641
+ compressedSize: compressedSize32 === ZIP_UINT32_MAX,
642
+ localOffset: localOffset32 === ZIP_UINT32_MAX,
643
+ diskNumber: diskStart16 === ZIP_UINT16_MAX,
644
+ });
645
+ const uncompressedSize = uncompressedSize32 === ZIP_UINT32_MAX ? zip64.uncompressedSize : uncompressedSize32;
646
+ const compressedSize = compressedSize32 === ZIP_UINT32_MAX ? zip64.compressedSize : compressedSize32;
647
+ const localOffset = localOffset32 === ZIP_UINT32_MAX ? zip64.localOffset : localOffset32;
648
+ const diskStart = diskStart16 === ZIP_UINT16_MAX ? zip64.diskNumber : diskStart16;
649
+ if (
650
+ uncompressedSize === undefined ||
651
+ compressedSize === undefined ||
652
+ localOffset === undefined ||
653
+ diskStart !== 0
654
+ ) {
655
+ throw invalidZipArchive();
656
+ }
657
+ const nameBytes = bytes.subarray(nameStart, extraStart);
658
+ const name = decodeZipName(nameBytes, flags);
659
+ if (uncompressedSize > MAX_ARCHIVE_ENTRY_BYTES) throw archiveLimitError("entry", name);
660
+ totalUncompressed += uncompressedSize;
661
+ if (totalUncompressed > MAX_ARCHIVE_TOTAL_BYTES) throw archiveLimitError("total");
662
+ directoryEntries.push({
663
+ name,
664
+ nameBytes,
665
+ flags,
666
+ compression,
667
+ crc32,
668
+ compressedSize,
669
+ uncompressedSize,
670
+ localOffset,
671
+ });
672
+ offset += recordLength;
673
+ }
674
+ if (offset < centralDirectoryEnd) {
675
+ requireZipRange(view, offset, 6, centralDirectoryEnd);
676
+ const signatureLength = view.getUint16(offset + 4, true);
677
+ if (
678
+ view.getUint32(offset, true) !== ZIP_CENTRAL_DIRECTORY_DIGITAL_SIGNATURE ||
679
+ offset + 6 + signatureLength !== centralDirectoryEnd
680
+ ) {
681
+ throw invalidZipArchive();
682
+ }
683
+ offset = centralDirectoryEnd;
684
+ }
685
+ if (offset !== centralDirectoryEnd) throw invalidZipArchive();
686
+
687
+ const localEntries = directoryEntries
688
+ .map(entry => validateZipLocalEntry(view, bytes, entry, offsetCorrection, centralDirectoryStart))
689
+ .sort((left, right) => left.entry.localOffset - right.entry.localOffset);
690
+ for (let index = 1; index < localEntries.length; index++) {
691
+ const previous = localEntries[index - 1];
692
+ const current = localEntries[index];
693
+ if (previous && current && current.entry.localOffset < previous.endOffset) throw invalidZipArchive();
174
694
  }
695
+ return localEntries.map(item => item.entry);
696
+ }
175
697
 
698
+ async function readZipEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
699
+ const directoryEntries = readZipDirectory(bytes);
700
+ const { Inflate } = await loadFflate();
701
+ // Feed validated member ranges directly so descriptor signatures cannot truncate stored or deflate data.
702
+ let activeInflateBudget:
703
+ | {
704
+ entryBytes: number;
705
+ fileName: string;
706
+ }
707
+ | undefined;
708
+ class CappedZipInflate {
709
+ ondata: (error: Error | null, data: Uint8Array, final: boolean) => void = () => {};
710
+ #failed = false;
711
+ #inflater: InstanceType<typeof Inflate>;
712
+ constructor() {
713
+ const budget = activeInflateBudget;
714
+ if (!budget) throw new ToolError("Missing ZIP inflate budget");
715
+ // Inflate consumes compressed chunks incrementally, avoiding a member-sized compressed copy.
716
+ this.#inflater = new Inflate((data, final) => {
717
+ const entryRemaining = MAX_ARCHIVE_ENTRY_BYTES - budget.entryBytes;
718
+ const totalRemaining = MAX_ARCHIVE_TOTAL_BYTES - totalUncompressed;
719
+ if (data.byteLength > Math.min(entryRemaining, totalRemaining)) {
720
+ this.#failed = true;
721
+ throw archiveLimitError(entryRemaining <= totalRemaining ? "entry" : "total", budget.fileName);
722
+ }
723
+ budget.entryBytes += data.byteLength;
724
+ this.ondata(null, data, final);
725
+ });
726
+ }
727
+ push(data: Uint8Array, final: boolean): void {
728
+ if (this.#failed) return;
729
+ try {
730
+ this.#inflater.push(data, final);
731
+ } catch (error) {
732
+ this.#failed = true;
733
+ this.ondata(error instanceof Error ? error : new Error(String(error)), new Uint8Array(0), true);
734
+ }
735
+ }
736
+ }
176
737
  const entries: ArchiveIndexEntry[] = [];
177
- for (const [rawPath, fileBytes] of Object.entries(files)) {
178
- const normalizedPath = normalizeArchiveEntryPath(rawPath);
738
+ let totalUncompressed = 0;
739
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
740
+ for (const expected of directoryEntries) {
741
+ const normalizedPath = normalizeArchiveEntryPath(expected.name);
179
742
  if (!normalizedPath) continue;
180
- const isDirectory = isArchiveDirectoryName(rawPath);
743
+ if (archiveEntryDepth(expected.name) > MAX_ARCHIVE_PATH_DEPTH) throw archiveLimitError("depth", expected.name);
744
+ const isDirectory = isArchiveDirectoryName(expected.name);
745
+ const nameLength = view.getUint16(expected.localOffset + 26, true);
746
+ const extraLength = view.getUint16(expected.localOffset + 28, true);
747
+ const dataStart = expected.localOffset + 30 + nameLength + extraLength;
748
+ const dataEnd = dataStart + expected.compressedSize;
749
+ if (dataEnd > bytes.byteLength) throw invalidZipArchive();
750
+ const compressed = bytes.subarray(dataStart, dataEnd);
751
+ const chunks: Uint8Array[] = [];
752
+ let entryBytes = 0;
753
+ let failed: ToolError | undefined;
754
+ const take = (data: Uint8Array, final: boolean) => {
755
+ if (failed) return;
756
+ const size = data.byteLength;
757
+ if (entryBytes + size > MAX_ARCHIVE_ENTRY_BYTES) failed = archiveLimitError("entry", normalizedPath);
758
+ else if (totalUncompressed + size > MAX_ARCHIVE_TOTAL_BYTES) failed = archiveLimitError("total");
759
+ else {
760
+ entryBytes += size;
761
+ totalUncompressed += size;
762
+ if (size > 0 && !isDirectory) chunks.push(data);
763
+ }
764
+ if (final && !failed && entryBytes !== expected.uncompressedSize) failed = invalidZipArchive();
765
+ };
766
+ if (expected.compression === 0) take(compressed, true);
767
+ else if (expected.compression !== 8) throw invalidZipArchive();
768
+ else {
769
+ activeInflateBudget = { entryBytes: 0, fileName: normalizedPath };
770
+ const inflator = new CappedZipInflate();
771
+ inflator.ondata = (error, data, final) => {
772
+ if (error) failed = new ToolError(error.message);
773
+ else take(data ?? new Uint8Array(0), final);
774
+ };
775
+ try {
776
+ if (compressed.byteLength === 0) {
777
+ inflator.push(compressed, true);
778
+ } else {
779
+ for (let offset = 0; offset < compressed.byteLength && !failed; offset += ZIP_INFLATE_CHUNK_BYTES) {
780
+ const end = Math.min(offset + ZIP_INFLATE_CHUNK_BYTES, compressed.byteLength);
781
+ inflator.push(compressed.subarray(offset, end), end === compressed.byteLength);
782
+ }
783
+ }
784
+ } catch (error) {
785
+ if (!failed) failed = new ToolError(error instanceof Error ? error.message : String(error));
786
+ } finally {
787
+ activeInflateBudget = undefined;
788
+ }
789
+ }
790
+ if (failed) throw failed;
181
791
  entries.push({
182
792
  path: normalizedPath,
183
793
  isDirectory,
184
- size: isDirectory ? 0 : fileBytes.byteLength,
185
- storage: isDirectory ? undefined : { type: "zip", bytes: fileBytes },
794
+ size: isDirectory ? 0 : entryBytes,
795
+ storage: isDirectory
796
+ ? undefined
797
+ : { type: "zip", bytes: expected.compression === 0 ? compressed : concatBytes(chunks, entryBytes) },
186
798
  });
187
799
  }
188
800
 
189
- return entries;
801
+ return [
802
+ ...entries.reduce((map, entry) => map.set(entry.path, entry), new Map<string, ArchiveIndexEntry>()).values(),
803
+ ];
190
804
  }
191
805
 
192
806
  export function parseArchivePathCandidates(filePath: string): ArchivePathCandidate[] {
@@ -325,8 +939,7 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
325
939
  }
326
940
 
327
941
  // F20: cap the compressed archive read so opening a multi-GB archive cannot buffer it
328
- // whole into memory. (Zip-bomb expanded-size bounding would need a streaming inflate.)
329
- const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
942
+ // whole into memory. Inflate stops at one entry, the total, the entry count, and path depth.
330
943
  const file = Bun.file(filePath);
331
944
  if (file.size > MAX_ARCHIVE_BYTES) {
332
945
  throw new ToolError(
@@ -334,6 +947,6 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
334
947
  );
335
948
  }
336
949
  const bytes = await file.bytes();
337
- const entries = format === "zip" ? await readZipEntries(bytes) : await readTarEntries(bytes);
950
+ const entries = format === "zip" ? await readZipEntries(bytes) : await readTarEntries(bytes, format === "tar.gz");
338
951
  return new ArchiveReader(format, entries);
339
952
  }