@ismail-elkorchi/bytefold 0.6.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 (314) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/SPEC.md +285 -0
  4. package/dist/abort.d.ts +3 -0
  5. package/dist/abort.d.ts.map +1 -0
  6. package/dist/abort.js +33 -0
  7. package/dist/abort.js.map +1 -0
  8. package/dist/archive/errors.d.ts +34 -0
  9. package/dist/archive/errors.d.ts.map +1 -0
  10. package/dist/archive/errors.js +45 -0
  11. package/dist/archive/errors.js.map +1 -0
  12. package/dist/archive/httpArchiveErrors.d.ts +2 -0
  13. package/dist/archive/httpArchiveErrors.d.ts.map +1 -0
  14. package/dist/archive/httpArchiveErrors.js +25 -0
  15. package/dist/archive/httpArchiveErrors.js.map +1 -0
  16. package/dist/archive/index.d.ts +47 -0
  17. package/dist/archive/index.d.ts.map +1 -0
  18. package/dist/archive/index.js +1490 -0
  19. package/dist/archive/index.js.map +1 -0
  20. package/dist/archive/types.d.ts +91 -0
  21. package/dist/archive/types.d.ts.map +1 -0
  22. package/dist/archive/types.js +2 -0
  23. package/dist/archive/types.js.map +1 -0
  24. package/dist/archive/xzPreflight.d.ts +13 -0
  25. package/dist/archive/xzPreflight.d.ts.map +1 -0
  26. package/dist/archive/xzPreflight.js +44 -0
  27. package/dist/archive/xzPreflight.js.map +1 -0
  28. package/dist/archive/zipPreflight.d.ts +18 -0
  29. package/dist/archive/zipPreflight.d.ts.map +1 -0
  30. package/dist/archive/zipPreflight.js +50 -0
  31. package/dist/archive/zipPreflight.js.map +1 -0
  32. package/dist/binary.d.ts +12 -0
  33. package/dist/binary.d.ts.map +1 -0
  34. package/dist/binary.js +59 -0
  35. package/dist/binary.js.map +1 -0
  36. package/dist/bun/index.d.ts +19 -0
  37. package/dist/bun/index.d.ts.map +1 -0
  38. package/dist/bun/index.js +427 -0
  39. package/dist/bun/index.js.map +1 -0
  40. package/dist/compress/errors.d.ts +30 -0
  41. package/dist/compress/errors.d.ts.map +1 -0
  42. package/dist/compress/errors.js +40 -0
  43. package/dist/compress/errors.js.map +1 -0
  44. package/dist/compress/index.d.ts +12 -0
  45. package/dist/compress/index.d.ts.map +1 -0
  46. package/dist/compress/index.js +339 -0
  47. package/dist/compress/index.js.map +1 -0
  48. package/dist/compress/types.d.ts +41 -0
  49. package/dist/compress/types.d.ts.map +1 -0
  50. package/dist/compress/types.js +2 -0
  51. package/dist/compress/types.js.map +1 -0
  52. package/dist/compression/bzip2.d.ts +9 -0
  53. package/dist/compression/bzip2.d.ts.map +1 -0
  54. package/dist/compression/bzip2.js +546 -0
  55. package/dist/compression/bzip2.js.map +1 -0
  56. package/dist/compression/codecs.d.ts +6 -0
  57. package/dist/compression/codecs.d.ts.map +1 -0
  58. package/dist/compression/codecs.js +82 -0
  59. package/dist/compression/codecs.js.map +1 -0
  60. package/dist/compression/deflate64.d.ts +3 -0
  61. package/dist/compression/deflate64.d.ts.map +1 -0
  62. package/dist/compression/deflate64.js +549 -0
  63. package/dist/compression/deflate64.js.map +1 -0
  64. package/dist/compression/node-backend.d.ts +9 -0
  65. package/dist/compression/node-backend.d.ts.map +1 -0
  66. package/dist/compression/node-backend.js +103 -0
  67. package/dist/compression/node-backend.js.map +1 -0
  68. package/dist/compression/registry.d.ts +10 -0
  69. package/dist/compression/registry.d.ts.map +1 -0
  70. package/dist/compression/registry.js +30 -0
  71. package/dist/compression/registry.js.map +1 -0
  72. package/dist/compression/streams.d.ts +31 -0
  73. package/dist/compression/streams.d.ts.map +1 -0
  74. package/dist/compression/streams.js +147 -0
  75. package/dist/compression/streams.js.map +1 -0
  76. package/dist/compression/types.d.ts +19 -0
  77. package/dist/compression/types.d.ts.map +1 -0
  78. package/dist/compression/types.js +2 -0
  79. package/dist/compression/types.js.map +1 -0
  80. package/dist/compression/xz.d.ts +21 -0
  81. package/dist/compression/xz.d.ts.map +1 -0
  82. package/dist/compression/xz.js +1455 -0
  83. package/dist/compression/xz.js.map +1 -0
  84. package/dist/compression/xzFilters.d.ts +14 -0
  85. package/dist/compression/xzFilters.d.ts.map +1 -0
  86. package/dist/compression/xzFilters.js +736 -0
  87. package/dist/compression/xzFilters.js.map +1 -0
  88. package/dist/compression/xzIndexPreflight.d.ts +20 -0
  89. package/dist/compression/xzIndexPreflight.d.ts.map +1 -0
  90. package/dist/compression/xzIndexPreflight.js +371 -0
  91. package/dist/compression/xzIndexPreflight.js.map +1 -0
  92. package/dist/compression/xzScan.d.ts +15 -0
  93. package/dist/compression/xzScan.d.ts.map +1 -0
  94. package/dist/compression/xzScan.js +310 -0
  95. package/dist/compression/xzScan.js.map +1 -0
  96. package/dist/cp437.d.ts +2 -0
  97. package/dist/cp437.d.ts.map +1 -0
  98. package/dist/cp437.js +31 -0
  99. package/dist/cp437.js.map +1 -0
  100. package/dist/crc32.d.ts +7 -0
  101. package/dist/crc32.d.ts.map +1 -0
  102. package/dist/crc32.js +37 -0
  103. package/dist/crc32.js.map +1 -0
  104. package/dist/crc64.d.ts +6 -0
  105. package/dist/crc64.d.ts.map +1 -0
  106. package/dist/crc64.js +32 -0
  107. package/dist/crc64.js.map +1 -0
  108. package/dist/crypto/ctr.d.ts +11 -0
  109. package/dist/crypto/ctr.d.ts.map +1 -0
  110. package/dist/crypto/ctr.js +56 -0
  111. package/dist/crypto/ctr.js.map +1 -0
  112. package/dist/crypto/sha256.d.ts +16 -0
  113. package/dist/crypto/sha256.d.ts.map +1 -0
  114. package/dist/crypto/sha256.js +152 -0
  115. package/dist/crypto/sha256.js.map +1 -0
  116. package/dist/crypto/winzip-aes.d.ts +17 -0
  117. package/dist/crypto/winzip-aes.d.ts.map +1 -0
  118. package/dist/crypto/winzip-aes.js +98 -0
  119. package/dist/crypto/winzip-aes.js.map +1 -0
  120. package/dist/crypto/zipcrypto.d.ts +23 -0
  121. package/dist/crypto/zipcrypto.d.ts.map +1 -0
  122. package/dist/crypto/zipcrypto.js +99 -0
  123. package/dist/crypto/zipcrypto.js.map +1 -0
  124. package/dist/deno/index.d.ts +19 -0
  125. package/dist/deno/index.d.ts.map +1 -0
  126. package/dist/deno/index.js +422 -0
  127. package/dist/deno/index.js.map +1 -0
  128. package/dist/dosTime.d.ts +7 -0
  129. package/dist/dosTime.d.ts.map +1 -0
  130. package/dist/dosTime.js +21 -0
  131. package/dist/dosTime.js.map +1 -0
  132. package/dist/errorContext.d.ts +2 -0
  133. package/dist/errorContext.d.ts.map +1 -0
  134. package/dist/errorContext.js +24 -0
  135. package/dist/errorContext.js.map +1 -0
  136. package/dist/errors.d.ts +46 -0
  137. package/dist/errors.d.ts.map +1 -0
  138. package/dist/errors.js +51 -0
  139. package/dist/errors.js.map +1 -0
  140. package/dist/extraFields.d.ts +29 -0
  141. package/dist/extraFields.d.ts.map +1 -0
  142. package/dist/extraFields.js +201 -0
  143. package/dist/extraFields.js.map +1 -0
  144. package/dist/generated/unicodeCaseFolding.d.ts +4 -0
  145. package/dist/generated/unicodeCaseFolding.d.ts.map +1 -0
  146. package/dist/generated/unicodeCaseFolding.js +1594 -0
  147. package/dist/generated/unicodeCaseFolding.js.map +1 -0
  148. package/dist/http/errors.d.ts +26 -0
  149. package/dist/http/errors.d.ts.map +1 -0
  150. package/dist/http/errors.js +33 -0
  151. package/dist/http/errors.js.map +1 -0
  152. package/dist/index.d.ts +10 -0
  153. package/dist/index.d.ts.map +1 -0
  154. package/dist/index.js +7 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/limits.d.ts +22 -0
  157. package/dist/limits.d.ts.map +1 -0
  158. package/dist/limits.js +39 -0
  159. package/dist/limits.js.map +1 -0
  160. package/dist/node/index.d.ts +13 -0
  161. package/dist/node/index.d.ts.map +1 -0
  162. package/dist/node/index.js +448 -0
  163. package/dist/node/index.js.map +1 -0
  164. package/dist/node/zip/RandomAccess.d.ts +12 -0
  165. package/dist/node/zip/RandomAccess.d.ts.map +1 -0
  166. package/dist/node/zip/RandomAccess.js +38 -0
  167. package/dist/node/zip/RandomAccess.js.map +1 -0
  168. package/dist/node/zip/Sink.d.ts +17 -0
  169. package/dist/node/zip/Sink.d.ts.map +1 -0
  170. package/dist/node/zip/Sink.js +45 -0
  171. package/dist/node/zip/Sink.js.map +1 -0
  172. package/dist/node/zip/ZipReader.d.ts +51 -0
  173. package/dist/node/zip/ZipReader.d.ts.map +1 -0
  174. package/dist/node/zip/ZipReader.js +1540 -0
  175. package/dist/node/zip/ZipReader.js.map +1 -0
  176. package/dist/node/zip/ZipWriter.d.ts +21 -0
  177. package/dist/node/zip/ZipWriter.d.ts.map +1 -0
  178. package/dist/node/zip/ZipWriter.js +196 -0
  179. package/dist/node/zip/ZipWriter.js.map +1 -0
  180. package/dist/node/zip/entryStream.d.ts +22 -0
  181. package/dist/node/zip/entryStream.d.ts.map +1 -0
  182. package/dist/node/zip/entryStream.js +241 -0
  183. package/dist/node/zip/entryStream.js.map +1 -0
  184. package/dist/node/zip/entryWriter.d.ts +54 -0
  185. package/dist/node/zip/entryWriter.d.ts.map +1 -0
  186. package/dist/node/zip/entryWriter.js +512 -0
  187. package/dist/node/zip/entryWriter.js.map +1 -0
  188. package/dist/node/zip/index.d.ts +8 -0
  189. package/dist/node/zip/index.d.ts.map +1 -0
  190. package/dist/node/zip/index.js +5 -0
  191. package/dist/node/zip/index.js.map +1 -0
  192. package/dist/reader/RandomAccess.d.ts +55 -0
  193. package/dist/reader/RandomAccess.d.ts.map +1 -0
  194. package/dist/reader/RandomAccess.js +528 -0
  195. package/dist/reader/RandomAccess.js.map +1 -0
  196. package/dist/reader/ZipReader.d.ts +89 -0
  197. package/dist/reader/ZipReader.d.ts.map +1 -0
  198. package/dist/reader/ZipReader.js +1359 -0
  199. package/dist/reader/ZipReader.js.map +1 -0
  200. package/dist/reader/centralDirectory.d.ts +40 -0
  201. package/dist/reader/centralDirectory.d.ts.map +1 -0
  202. package/dist/reader/centralDirectory.js +311 -0
  203. package/dist/reader/centralDirectory.js.map +1 -0
  204. package/dist/reader/entryStream.d.ts +22 -0
  205. package/dist/reader/entryStream.d.ts.map +1 -0
  206. package/dist/reader/entryStream.js +122 -0
  207. package/dist/reader/entryStream.js.map +1 -0
  208. package/dist/reader/eocd.d.ts +22 -0
  209. package/dist/reader/eocd.d.ts.map +1 -0
  210. package/dist/reader/eocd.js +184 -0
  211. package/dist/reader/eocd.js.map +1 -0
  212. package/dist/reader/httpZipErrors.d.ts +4 -0
  213. package/dist/reader/httpZipErrors.d.ts.map +1 -0
  214. package/dist/reader/httpZipErrors.js +48 -0
  215. package/dist/reader/httpZipErrors.js.map +1 -0
  216. package/dist/reader/localHeader.d.ts +15 -0
  217. package/dist/reader/localHeader.d.ts.map +1 -0
  218. package/dist/reader/localHeader.js +37 -0
  219. package/dist/reader/localHeader.js.map +1 -0
  220. package/dist/reportSchema.d.ts +3 -0
  221. package/dist/reportSchema.d.ts.map +1 -0
  222. package/dist/reportSchema.js +3 -0
  223. package/dist/reportSchema.js.map +1 -0
  224. package/dist/streams/adapters.d.ts +10 -0
  225. package/dist/streams/adapters.d.ts.map +1 -0
  226. package/dist/streams/adapters.js +54 -0
  227. package/dist/streams/adapters.js.map +1 -0
  228. package/dist/streams/buffer.d.ts +5 -0
  229. package/dist/streams/buffer.d.ts.map +1 -0
  230. package/dist/streams/buffer.js +44 -0
  231. package/dist/streams/buffer.js.map +1 -0
  232. package/dist/streams/crcTransform.d.ts +15 -0
  233. package/dist/streams/crcTransform.d.ts.map +1 -0
  234. package/dist/streams/crcTransform.js +30 -0
  235. package/dist/streams/crcTransform.js.map +1 -0
  236. package/dist/streams/emit.d.ts +7 -0
  237. package/dist/streams/emit.d.ts.map +1 -0
  238. package/dist/streams/emit.js +13 -0
  239. package/dist/streams/emit.js.map +1 -0
  240. package/dist/streams/limits.d.ts +16 -0
  241. package/dist/streams/limits.d.ts.map +1 -0
  242. package/dist/streams/limits.js +39 -0
  243. package/dist/streams/limits.js.map +1 -0
  244. package/dist/streams/measure.d.ts +5 -0
  245. package/dist/streams/measure.d.ts.map +1 -0
  246. package/dist/streams/measure.js +9 -0
  247. package/dist/streams/measure.js.map +1 -0
  248. package/dist/streams/progress.d.ts +8 -0
  249. package/dist/streams/progress.d.ts.map +1 -0
  250. package/dist/streams/progress.js +69 -0
  251. package/dist/streams/progress.js.map +1 -0
  252. package/dist/streams/web.d.ts +5 -0
  253. package/dist/streams/web.d.ts.map +1 -0
  254. package/dist/streams/web.js +33 -0
  255. package/dist/streams/web.js.map +1 -0
  256. package/dist/tar/TarReader.d.ts +41 -0
  257. package/dist/tar/TarReader.d.ts.map +1 -0
  258. package/dist/tar/TarReader.js +930 -0
  259. package/dist/tar/TarReader.js.map +1 -0
  260. package/dist/tar/TarWriter.d.ts +25 -0
  261. package/dist/tar/TarWriter.d.ts.map +1 -0
  262. package/dist/tar/TarWriter.js +307 -0
  263. package/dist/tar/TarWriter.js.map +1 -0
  264. package/dist/tar/index.d.ts +4 -0
  265. package/dist/tar/index.d.ts.map +1 -0
  266. package/dist/tar/index.js +3 -0
  267. package/dist/tar/index.js.map +1 -0
  268. package/dist/tar/types.d.ts +67 -0
  269. package/dist/tar/types.d.ts.map +1 -0
  270. package/dist/tar/types.js +2 -0
  271. package/dist/tar/types.js.map +1 -0
  272. package/dist/text/caseFold.d.ts +7 -0
  273. package/dist/text/caseFold.d.ts.map +1 -0
  274. package/dist/text/caseFold.js +45 -0
  275. package/dist/text/caseFold.js.map +1 -0
  276. package/dist/types.d.ts +190 -0
  277. package/dist/types.d.ts.map +1 -0
  278. package/dist/types.js +2 -0
  279. package/dist/types.js.map +1 -0
  280. package/dist/web/index.d.ts +11 -0
  281. package/dist/web/index.d.ts.map +1 -0
  282. package/dist/web/index.js +95 -0
  283. package/dist/web/index.js.map +1 -0
  284. package/dist/writer/Sink.d.ts +21 -0
  285. package/dist/writer/Sink.d.ts.map +1 -0
  286. package/dist/writer/Sink.js +24 -0
  287. package/dist/writer/Sink.js.map +1 -0
  288. package/dist/writer/ZipWriter.d.ts +27 -0
  289. package/dist/writer/ZipWriter.d.ts.map +1 -0
  290. package/dist/writer/ZipWriter.js +153 -0
  291. package/dist/writer/ZipWriter.js.map +1 -0
  292. package/dist/writer/centralDirectoryWriter.d.ts +8 -0
  293. package/dist/writer/centralDirectoryWriter.d.ts.map +1 -0
  294. package/dist/writer/centralDirectoryWriter.js +77 -0
  295. package/dist/writer/centralDirectoryWriter.js.map +1 -0
  296. package/dist/writer/entryWriter.d.ts +54 -0
  297. package/dist/writer/entryWriter.d.ts.map +1 -0
  298. package/dist/writer/entryWriter.js +327 -0
  299. package/dist/writer/entryWriter.js.map +1 -0
  300. package/dist/writer/finalize.d.ts +10 -0
  301. package/dist/writer/finalize.d.ts.map +1 -0
  302. package/dist/writer/finalize.js +56 -0
  303. package/dist/writer/finalize.js.map +1 -0
  304. package/dist/zip/index.d.ts +8 -0
  305. package/dist/zip/index.d.ts.map +1 -0
  306. package/dist/zip/index.js +5 -0
  307. package/dist/zip/index.js.map +1 -0
  308. package/jsr.json +41 -0
  309. package/package.json +117 -0
  310. package/schemas/audit-report.schema.json +38 -0
  311. package/schemas/capabilities-report.schema.json +25 -0
  312. package/schemas/detection-report.schema.json +23 -0
  313. package/schemas/error.schema.json +22 -0
  314. package/schemas/normalize-report.schema.json +47 -0
@@ -0,0 +1,1540 @@
1
+ import { mkdir, symlink, mkdtemp, rm } from 'node:fs/promises';
2
+ import { createReadStream, createWriteStream } from 'node:fs';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { Readable } from 'node:stream';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { tmpdir } from 'node:os';
8
+ import { once } from 'node:events';
9
+ import { ZipError } from '../../errors.js';
10
+ import { mergeSignals, throwIfAborted } from '../../abort.js';
11
+ import { BufferRandomAccess, FileRandomAccess, HttpRandomAccess } from './RandomAccess.js';
12
+ import { wrapRandomAccessForZip } from '../../reader/httpZipErrors.js';
13
+ import { findEocd } from '../../reader/eocd.js';
14
+ import { iterCentralDirectory } from '../../reader/centralDirectory.js';
15
+ import { openEntryStream, openRawStream } from './entryStream.js';
16
+ import { buildAesExtra, parseAesExtra } from '../../extraFields.js';
17
+ import { readLocalHeader } from '../../reader/localHeader.js';
18
+ import { isWebWritable, readableFromBytes, toWebReadable } from '../../streams/adapters.js';
19
+ import { createCrcTransform } from '../../streams/crcTransform.js';
20
+ import { createMeasureTransform } from '../../streams/measure.js';
21
+ import { createProgressTracker, createProgressTransform } from '../../streams/progress.js';
22
+ import { normalizePathForCollision, toCollisionKey } from '../../text/caseFold.js';
23
+ import { FileSink, NodeWritableSink, WebWritableSink } from './Sink.js';
24
+ import { writeCentralDirectory } from '../../writer/centralDirectoryWriter.js';
25
+ import { finalizeArchive } from '../../writer/finalize.js';
26
+ import { writeRawEntry } from './entryWriter.js';
27
+ import { getCompressionCodec, hasCompressionCodec } from '../../compression/registry.js';
28
+ import { AGENT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS } from '../../limits.js';
29
+ const DEFAULT_LIMITS = DEFAULT_RESOURCE_LIMITS;
30
+ const AGENT_LIMITS = AGENT_RESOURCE_LIMITS;
31
+ export class ZipReader {
32
+ reader;
33
+ profile;
34
+ strict;
35
+ limits;
36
+ warningsList = [];
37
+ entriesList = null;
38
+ password;
39
+ storeEntries;
40
+ eocd = null;
41
+ signal;
42
+ constructor(reader, options) {
43
+ this.reader = reader;
44
+ const resolved = resolveReaderProfile(options);
45
+ this.profile = resolved.profile;
46
+ this.strict = resolved.strict;
47
+ this.limits = resolved.limits;
48
+ this.password = options?.password;
49
+ this.storeEntries = options?.shouldStoreEntries ?? true;
50
+ this.signal = mergeSignals(options?.signal, options?.http?.signal);
51
+ }
52
+ static async fromFile(pathLike, options) {
53
+ const reader = FileRandomAccess.fromPath(pathLike);
54
+ const instance = new ZipReader(wrapRandomAccessForZip(reader), options);
55
+ await instance.init();
56
+ return instance;
57
+ }
58
+ static async fromUint8Array(data, options) {
59
+ const reader = new BufferRandomAccess(data);
60
+ const instance = new ZipReader(wrapRandomAccessForZip(reader), options);
61
+ await instance.init();
62
+ return instance;
63
+ }
64
+ static async fromStream(stream, options) {
65
+ const signal = options?.signal ?? null;
66
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'bytefold-'));
67
+ const tempPath = path.join(tempDir, 'stream.zip');
68
+ const writable = createWriteStream(tempPath);
69
+ const webReadable = toWebReadable(stream);
70
+ const reader = webReadable.getReader();
71
+ try {
72
+ while (true) {
73
+ throwIfAborted(signal);
74
+ const { value, done } = await reader.read();
75
+ if (done)
76
+ break;
77
+ if (!value)
78
+ continue;
79
+ const canWrite = writable.write(value);
80
+ if (!canWrite) {
81
+ const drain = once(writable, 'drain');
82
+ if (signal) {
83
+ await Promise.race([
84
+ drain,
85
+ new Promise((_, reject) => {
86
+ if (signal.aborted) {
87
+ reject(signal.reason ?? new DOMException('The operation was aborted', 'AbortError'));
88
+ return;
89
+ }
90
+ signal.addEventListener('abort', () => reject(signal.reason ?? new DOMException('The operation was aborted', 'AbortError')), { once: true });
91
+ })
92
+ ]);
93
+ }
94
+ else {
95
+ await drain;
96
+ }
97
+ }
98
+ }
99
+ await new Promise((resolve, reject) => {
100
+ writable.end((err) => (err ? reject(err) : resolve()));
101
+ });
102
+ }
103
+ catch (err) {
104
+ writable.destroy();
105
+ await rm(tempDir, { recursive: true, force: true }).catch(() => { });
106
+ throw err;
107
+ }
108
+ finally {
109
+ reader.releaseLock();
110
+ }
111
+ const tempReader = new TempFileRandomAccess(tempPath, tempDir);
112
+ const instance = new ZipReader(wrapRandomAccessForZip(tempReader), options);
113
+ await instance.init();
114
+ return instance;
115
+ }
116
+ static async fromUrl(url, options) {
117
+ const httpSignal = mergeSignals(options?.signal, options?.http?.signal);
118
+ const httpOptions = options?.http ? { ...options.http } : {};
119
+ if (httpSignal) {
120
+ httpOptions.signal = httpSignal;
121
+ }
122
+ const reader = new HttpRandomAccess(url, Object.keys(httpOptions).length > 0 ? httpOptions : undefined);
123
+ const instance = new ZipReader(wrapRandomAccessForZip(reader), options);
124
+ await instance.init();
125
+ return instance;
126
+ }
127
+ entries() {
128
+ if (!this.storeEntries) {
129
+ throw new ZipError('ZIP_ENTRIES_NOT_STORED', 'Entries are not stored; use iterEntries() or enable shouldStoreEntries');
130
+ }
131
+ if (!this.entriesList)
132
+ return [];
133
+ return this.entriesList.map((entry) => ({ ...entry }));
134
+ }
135
+ warnings() {
136
+ return [...this.warningsList];
137
+ }
138
+ async *iterEntries(options) {
139
+ const signal = this.resolveSignal(options?.signal);
140
+ throwIfAborted(signal);
141
+ if (this.entriesList) {
142
+ for (const entry of this.entriesList) {
143
+ throwIfAborted(signal);
144
+ yield { ...entry };
145
+ }
146
+ return;
147
+ }
148
+ if (!this.eocd) {
149
+ throw new ZipError('ZIP_BAD_CENTRAL_DIRECTORY', 'Central directory has not been initialized');
150
+ }
151
+ const entries = [];
152
+ const totals = { totalUncompressed: 0n };
153
+ for await (const entry of iterCentralDirectory(this.reader, this.eocd.cdOffset, this.eocd.cdSize, this.eocd.totalEntries, {
154
+ strict: this.strict,
155
+ maxEntries: this.limits.maxEntries,
156
+ onWarning: (warning) => this.warningsList.push(warning),
157
+ ...(signal ? { signal } : {})
158
+ })) {
159
+ throwIfAborted(signal);
160
+ this.applyEntryLimits(entry, totals);
161
+ if (this.storeEntries)
162
+ entries.push(entry);
163
+ yield { ...entry };
164
+ }
165
+ if (this.storeEntries) {
166
+ this.entriesList = entries;
167
+ }
168
+ }
169
+ async forEachEntry(fn, options) {
170
+ for await (const entry of this.iterEntries(options)) {
171
+ await fn(entry);
172
+ }
173
+ }
174
+ async open(entry, options) {
175
+ const strict = options?.isStrict ?? this.strict;
176
+ const signal = this.resolveSignal(options?.signal);
177
+ const totals = { totalUncompressed: 0n };
178
+ const params = {
179
+ strict,
180
+ onWarning: (warning) => this.warningsList.push(warning)
181
+ };
182
+ const password = options?.password ?? this.password;
183
+ if (password !== undefined) {
184
+ params.password = password;
185
+ }
186
+ return openEntryStream(this.reader, entry, {
187
+ ...params,
188
+ ...(signal ? { signal } : {}),
189
+ ...progressParams(options),
190
+ limits: this.limits,
191
+ totals
192
+ });
193
+ }
194
+ async openRaw(entry, options) {
195
+ const signal = this.resolveSignal(options?.signal);
196
+ const { stream } = await openRawStream(this.reader, entry, {
197
+ ...(signal ? { signal } : {}),
198
+ ...progressParams(options)
199
+ });
200
+ return stream;
201
+ }
202
+ async normalizeToWritable(writable, options) {
203
+ const sink = isWebWritable(writable) ? new WebWritableSink(writable) : new NodeWritableSink(writable);
204
+ return this.normalizeToSink(sink, options);
205
+ }
206
+ async normalizeToFile(pathLike, options) {
207
+ const sink = new FileSink(pathLike);
208
+ return this.normalizeToSink(sink, options);
209
+ }
210
+ async extractAll(destDir, options) {
211
+ const baseDir = typeof destDir === 'string' ? destDir : fileURLToPath(destDir);
212
+ const strict = options?.isStrict ?? this.strict;
213
+ const password = options?.password ?? this.password;
214
+ const shouldAllowSymlinks = options?.shouldAllowSymlinks ?? false;
215
+ const limits = normalizeLimits(options?.limits ?? this.limits, this.limits);
216
+ const signal = this.resolveSignal(options?.signal);
217
+ let totalUncompressed = 0n;
218
+ const totals = { totalUncompressed: 0n };
219
+ const seenNames = new Map();
220
+ const seenNfc = new Map();
221
+ const seenCase = new Map();
222
+ await mkdir(baseDir, { recursive: true });
223
+ const iterOptions = signal ? { signal } : undefined;
224
+ for await (const entry of this.iterEntries(iterOptions)) {
225
+ throwIfAborted(signal);
226
+ totalUncompressed += entry.uncompressedSize;
227
+ if (totalUncompressed > limits.maxTotalUncompressedBytes) {
228
+ throw new ZipError('ZIP_LIMIT_EXCEEDED', 'Total uncompressed size exceeds limit');
229
+ }
230
+ const normalizedName = normalizePathForCollision(entry.name, entry.isDirectory);
231
+ if (normalizedName) {
232
+ const existing = seenNames.get(normalizedName);
233
+ if (existing) {
234
+ throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (duplicate). Rename entries to avoid collisions.', {
235
+ entryName: entry.name,
236
+ context: buildCollisionContext('duplicate', existing, entry.name, normalizedName, 'zip')
237
+ });
238
+ }
239
+ const nfcName = normalizedName.normalize('NFC');
240
+ const existingNfc = seenNfc.get(nfcName);
241
+ if (existingNfc && existingNfc.normalized !== normalizedName) {
242
+ throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (unicode_nfc). Rename entries to avoid collisions.', {
243
+ entryName: entry.name,
244
+ context: buildCollisionContext('unicode_nfc', existingNfc.original, entry.name, nfcName, 'zip')
245
+ });
246
+ }
247
+ const caseKey = toCollisionKey(normalizedName, entry.isDirectory);
248
+ const existingCase = seenCase.get(caseKey);
249
+ if (existingCase && existingCase.nfc !== nfcName) {
250
+ throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (case). Rename entries to avoid collisions.', {
251
+ entryName: entry.name,
252
+ context: buildCollisionContext('case', existingCase.original, entry.name, caseKey, 'zip')
253
+ });
254
+ }
255
+ seenNames.set(normalizedName, entry.name);
256
+ seenNfc.set(nfcName, { original: entry.name, normalized: normalizedName });
257
+ seenCase.set(caseKey, { original: entry.name, nfc: nfcName });
258
+ }
259
+ const targetPath = resolveEntryPath(baseDir, entry.name);
260
+ if (entry.isDirectory) {
261
+ await mkdir(targetPath, { recursive: true });
262
+ continue;
263
+ }
264
+ if (entry.isSymlink) {
265
+ if (!shouldAllowSymlinks) {
266
+ throw new ZipError('ZIP_SYMLINK_DISALLOWED', 'Symlink entries are disabled by default', {
267
+ entryName: entry.name
268
+ });
269
+ }
270
+ await mkdir(path.dirname(targetPath), { recursive: true });
271
+ const stream = await openEntryStream(this.reader, entry, {
272
+ strict,
273
+ onWarning: (warning) => this.warningsList.push(warning),
274
+ ...(signal ? { signal } : {}),
275
+ ...progressParams(options),
276
+ ...(password !== undefined ? { password } : {}),
277
+ limits,
278
+ totals
279
+ });
280
+ const buf = await new Response(stream).arrayBuffer();
281
+ const target = new TextDecoder('utf-8').decode(buf);
282
+ await symlink(target, targetPath);
283
+ continue;
284
+ }
285
+ await mkdir(path.dirname(targetPath), { recursive: true });
286
+ const stream = await openEntryStream(this.reader, entry, {
287
+ strict,
288
+ onWarning: (warning) => this.warningsList.push(warning),
289
+ ...(signal ? { signal } : {}),
290
+ ...progressParams(options),
291
+ ...(password !== undefined ? { password } : {}),
292
+ limits,
293
+ totals
294
+ });
295
+ const nodeReadable = Readable.fromWeb(stream);
296
+ await pipeline(nodeReadable, createWriteStream(targetPath));
297
+ }
298
+ }
299
+ async audit(options) {
300
+ const settings = this.resolveAuditSettings(options);
301
+ const signal = this.resolveSignal(options?.signal);
302
+ throwIfAborted(signal);
303
+ const issues = [];
304
+ const summary = {
305
+ entries: 0,
306
+ encryptedEntries: 0,
307
+ unsupportedEntries: 0,
308
+ warnings: 0,
309
+ errors: 0
310
+ };
311
+ const unsupportedEntries = new Set();
312
+ const ranges = [];
313
+ const seenNames = new Map();
314
+ const seenNfc = new Map();
315
+ const seenCase = new Map();
316
+ let totalUncompressed = 0n;
317
+ let totalExceeded = false;
318
+ const addIssue = (issue) => {
319
+ issues.push(issue);
320
+ if (issue.severity === 'warning')
321
+ summary.warnings += 1;
322
+ if (issue.severity === 'error')
323
+ summary.errors += 1;
324
+ };
325
+ const addParseWarning = (warning) => {
326
+ const severity = settings.strict ? 'error' : 'warning';
327
+ addIssue({
328
+ code: warning.code,
329
+ severity,
330
+ message: warning.message,
331
+ ...(warning.entryName ? { entryName: warning.entryName } : {})
332
+ });
333
+ };
334
+ let size;
335
+ try {
336
+ size = await this.reader.size(signal);
337
+ }
338
+ catch (err) {
339
+ addIssue(issueFromError(err));
340
+ return finalizeAuditReport(issues, summary);
341
+ }
342
+ let eocd;
343
+ try {
344
+ eocd = await findEocd(this.reader, false, signal, {
345
+ maxSearchBytes: settings.limits.maxZipEocdSearchBytes,
346
+ maxCommentBytes: settings.limits.maxZipCommentBytes,
347
+ maxCentralDirectoryBytes: settings.limits.maxZipCentralDirectoryBytes,
348
+ maxEntries: settings.limits.maxEntries,
349
+ rejectMultiDisk: true
350
+ });
351
+ }
352
+ catch (err) {
353
+ addIssue(issueFromError(err));
354
+ return finalizeAuditReport(issues, summary);
355
+ }
356
+ for (const warning of eocd.warnings) {
357
+ addParseWarning(warning);
358
+ }
359
+ const eocdEnd = eocd.eocdOffset + 22n + BigInt(eocd.comment.length);
360
+ const trailingBytes = size > eocdEnd ? size - eocdEnd : 0n;
361
+ if (trailingBytes > 0n) {
362
+ const severity = settings.rejectTrailingBytes ? 'error' : 'warning';
363
+ const trailingBytesNumber = toSafeNumber(trailingBytes);
364
+ if (trailingBytesNumber !== undefined) {
365
+ summary.trailingBytes = trailingBytesNumber;
366
+ }
367
+ addIssue({
368
+ code: 'ZIP_TRAILING_BYTES',
369
+ severity,
370
+ message: `Trailing bytes after EOCD: ${trailingBytes.toString()}`,
371
+ offset: eocdEnd.toString(),
372
+ details: { trailingBytes: trailingBytes.toString() }
373
+ });
374
+ }
375
+ const cdEnd = eocd.cdOffset + eocd.cdSize;
376
+ if (eocd.cdOffset < 0n || cdEnd > size) {
377
+ addIssue({
378
+ code: 'ZIP_OUT_OF_RANGE',
379
+ severity: 'error',
380
+ message: 'Central directory is outside file bounds',
381
+ offset: eocd.cdOffset.toString(),
382
+ details: {
383
+ cdOffset: eocd.cdOffset.toString(),
384
+ cdSize: eocd.cdSize.toString(),
385
+ fileSize: size.toString()
386
+ }
387
+ });
388
+ }
389
+ try {
390
+ for await (const entry of iterCentralDirectory(this.reader, eocd.cdOffset, eocd.cdSize, eocd.totalEntries, {
391
+ strict: false,
392
+ maxEntries: settings.limits.maxEntries,
393
+ onWarning: addParseWarning,
394
+ ...(signal ? { signal } : {})
395
+ })) {
396
+ throwIfAborted(signal);
397
+ summary.entries += 1;
398
+ if (entry.encrypted)
399
+ summary.encryptedEntries += 1;
400
+ const normalizedName = normalizePathForCollision(entry.name, entry.isDirectory);
401
+ if (normalizedName) {
402
+ const existing = seenNames.get(normalizedName);
403
+ if (existing) {
404
+ existing.count += 1;
405
+ addIssue({
406
+ code: 'ZIP_DUPLICATE_ENTRY',
407
+ severity: 'warning',
408
+ message: `Duplicate entry name: ${existing.original} vs ${entry.name}`,
409
+ entryName: entry.name,
410
+ details: {
411
+ occurrences: existing.count,
412
+ otherName: existing.original,
413
+ key: normalizedName,
414
+ collisionKind: 'duplicate'
415
+ }
416
+ });
417
+ }
418
+ else {
419
+ seenNames.set(normalizedName, { count: 1, original: entry.name });
420
+ const nfcName = normalizedName.normalize('NFC');
421
+ const existingNfc = seenNfc.get(nfcName);
422
+ if (existingNfc && existingNfc.normalized !== normalizedName) {
423
+ addIssue({
424
+ code: 'ZIP_UNICODE_COLLISION',
425
+ severity: 'error',
426
+ message: `Unicode normalization collision: ${existingNfc.original} vs ${entry.name}`,
427
+ entryName: entry.name,
428
+ details: { otherName: existingNfc.original, key: nfcName, collisionKind: 'unicode_nfc' }
429
+ });
430
+ }
431
+ else {
432
+ const caseKey = toCollisionKey(normalizedName, entry.isDirectory);
433
+ const existingCase = seenCase.get(caseKey);
434
+ if (existingCase && existingCase.nfc !== nfcName) {
435
+ addIssue({
436
+ code: 'ZIP_CASE_COLLISION',
437
+ severity: 'warning',
438
+ message: `Case-insensitive name collision: ${existingCase.original} vs ${entry.name}`,
439
+ entryName: entry.name,
440
+ details: { otherName: existingCase.original, key: caseKey, collisionKind: 'casefold' }
441
+ });
442
+ }
443
+ }
444
+ seenNfc.set(nfcName, { original: entry.name, normalized: normalizedName });
445
+ seenCase.set(toCollisionKey(normalizedName, entry.isDirectory), { original: entry.name, nfc: nfcName });
446
+ }
447
+ }
448
+ for (const issue of entryPathIssues(entry.name)) {
449
+ addIssue(issue);
450
+ }
451
+ if (entry.isSymlink) {
452
+ addIssue({
453
+ code: 'ZIP_SYMLINK_PRESENT',
454
+ severity: settings.symlinkSeverity,
455
+ message: 'Symlink entry present',
456
+ entryName: entry.name
457
+ });
458
+ }
459
+ const aesExtra = entry.method === 99 ? parseAesExtra(entry.extra.get(0x9901) ?? new Uint8Array(0)) : undefined;
460
+ if (entry.encrypted) {
461
+ if ((entry.flags & 0x40) !== 0) {
462
+ addIssue({
463
+ code: 'ZIP_UNSUPPORTED_ENCRYPTION',
464
+ severity: 'error',
465
+ message: 'Strong encryption is not supported',
466
+ entryName: entry.name
467
+ });
468
+ unsupportedEntries.add(entry.name);
469
+ }
470
+ else if (entry.method === 99 && !aesExtra) {
471
+ addIssue({
472
+ code: 'ZIP_UNSUPPORTED_ENCRYPTION',
473
+ severity: 'error',
474
+ message: 'AES encryption extra field missing or invalid',
475
+ entryName: entry.name
476
+ });
477
+ unsupportedEntries.add(entry.name);
478
+ }
479
+ }
480
+ const methodToCheck = entry.method === 99 ? aesExtra?.actualMethod : entry.method;
481
+ if (methodToCheck !== undefined && !hasCompressionCodec(methodToCheck)) {
482
+ addIssue({
483
+ code: 'ZIP_UNSUPPORTED_METHOD',
484
+ severity: 'error',
485
+ message: `Unsupported compression method ${methodToCheck}`,
486
+ entryName: entry.name,
487
+ details: { method: methodToCheck }
488
+ });
489
+ unsupportedEntries.add(entry.name);
490
+ }
491
+ if (entry.uncompressedSize > settings.limits.maxUncompressedEntryBytes) {
492
+ addIssue({
493
+ code: 'ZIP_LIMIT_EXCEEDED',
494
+ severity: 'error',
495
+ message: 'Entry exceeds max uncompressed size',
496
+ entryName: entry.name,
497
+ details: {
498
+ limit: settings.limits.maxUncompressedEntryBytes.toString(),
499
+ size: entry.uncompressedSize.toString()
500
+ }
501
+ });
502
+ }
503
+ totalUncompressed += entry.uncompressedSize;
504
+ if (!totalExceeded && totalUncompressed > settings.limits.maxTotalUncompressedBytes) {
505
+ totalExceeded = true;
506
+ addIssue({
507
+ code: 'ZIP_LIMIT_EXCEEDED',
508
+ severity: 'error',
509
+ message: 'Total uncompressed size exceeds limit',
510
+ details: {
511
+ limit: settings.limits.maxTotalUncompressedBytes.toString(),
512
+ size: totalUncompressed.toString()
513
+ }
514
+ });
515
+ }
516
+ if (entry.compressedSize > 0n) {
517
+ const ratio = Number(entry.uncompressedSize) / Number(entry.compressedSize);
518
+ if (ratio > settings.limits.maxCompressionRatio) {
519
+ addIssue({
520
+ code: 'ZIP_LIMIT_EXCEEDED',
521
+ severity: settings.strict ? 'error' : 'warning',
522
+ message: 'Compression ratio exceeds safety limit',
523
+ entryName: entry.name,
524
+ details: { ratio, limit: settings.limits.maxCompressionRatio }
525
+ });
526
+ }
527
+ }
528
+ if (entry.offset < 0n || entry.offset >= size) {
529
+ addIssue({
530
+ code: 'ZIP_OUT_OF_RANGE',
531
+ severity: 'error',
532
+ message: 'Local header offset is outside file bounds',
533
+ entryName: entry.name,
534
+ offset: entry.offset.toString(),
535
+ details: { offset: entry.offset.toString(), fileSize: size.toString() }
536
+ });
537
+ continue;
538
+ }
539
+ try {
540
+ const local = await readLocalHeader(this.reader, entry, signal);
541
+ const mismatchDetails = collectHeaderMismatches(entry, local);
542
+ if (mismatchDetails) {
543
+ addIssue({
544
+ code: 'ZIP_HEADER_MISMATCH',
545
+ severity: 'error',
546
+ message: 'Local header does not match central directory',
547
+ entryName: entry.name,
548
+ offset: entry.offset.toString(),
549
+ details: mismatchDetails
550
+ });
551
+ }
552
+ const dataEnd = local.dataOffset + entry.compressedSize;
553
+ if (dataEnd > size) {
554
+ addIssue({
555
+ code: 'ZIP_OUT_OF_RANGE',
556
+ severity: 'error',
557
+ message: 'Entry data extends beyond file bounds',
558
+ entryName: entry.name,
559
+ offset: local.dataOffset.toString(),
560
+ details: {
561
+ dataOffset: local.dataOffset.toString(),
562
+ dataEnd: dataEnd.toString(),
563
+ fileSize: size.toString()
564
+ }
565
+ });
566
+ }
567
+ else {
568
+ ranges.push({ start: entry.offset, end: dataEnd, entryName: entry.name });
569
+ if (dataEnd > eocd.cdOffset) {
570
+ addIssue({
571
+ code: 'ZIP_OUT_OF_RANGE',
572
+ severity: 'error',
573
+ message: 'Entry data overlaps central directory',
574
+ entryName: entry.name,
575
+ details: {
576
+ dataEnd: dataEnd.toString(),
577
+ cdOffset: eocd.cdOffset.toString()
578
+ }
579
+ });
580
+ }
581
+ }
582
+ }
583
+ catch (err) {
584
+ const details = errorDetails(err);
585
+ addIssue({
586
+ code: 'ZIP_HEADER_MISMATCH',
587
+ severity: 'error',
588
+ message: 'Failed to read local header',
589
+ entryName: entry.name,
590
+ offset: entry.offset.toString(),
591
+ ...(details ? { details } : {})
592
+ });
593
+ }
594
+ }
595
+ }
596
+ catch (err) {
597
+ addIssue(issueFromError(err));
598
+ }
599
+ ranges.sort((a, b) => (a.start < b.start ? -1 : a.start > b.start ? 1 : 0));
600
+ for (let i = 1; i < ranges.length; i += 1) {
601
+ const prev = ranges[i - 1];
602
+ const curr = ranges[i];
603
+ if (curr.start < prev.end) {
604
+ addIssue({
605
+ code: 'ZIP_OVERLAPPING_ENTRIES',
606
+ severity: 'error',
607
+ message: 'Entry data ranges overlap',
608
+ entryName: curr.entryName,
609
+ details: {
610
+ previousEntry: prev.entryName,
611
+ previousEnd: prev.end.toString(),
612
+ currentStart: curr.start.toString()
613
+ }
614
+ });
615
+ }
616
+ }
617
+ summary.unsupportedEntries = unsupportedEntries.size;
618
+ return finalizeAuditReport(issues, summary);
619
+ }
620
+ async assertSafe(options) {
621
+ const report = await this.audit(options);
622
+ const profile = options?.profile ?? this.profile;
623
+ const treatWarningsAsErrors = profile === 'agent';
624
+ const ok = report.ok && (!treatWarningsAsErrors || report.summary.warnings === 0);
625
+ if (ok)
626
+ return;
627
+ const message = treatWarningsAsErrors
628
+ ? 'ZIP audit reported warnings or errors'
629
+ : 'ZIP audit reported errors';
630
+ throw new ZipError('ZIP_AUDIT_FAILED', message, { cause: report });
631
+ }
632
+ async close() {
633
+ await this.reader.close();
634
+ }
635
+ async [Symbol.asyncDispose]() {
636
+ await this.close();
637
+ }
638
+ async init() {
639
+ const eocd = await findEocd(this.reader, this.strict, this.signal, {
640
+ maxSearchBytes: this.limits.maxZipEocdSearchBytes,
641
+ maxCommentBytes: this.limits.maxZipCommentBytes,
642
+ maxCentralDirectoryBytes: this.limits.maxZipCentralDirectoryBytes,
643
+ maxEntries: this.limits.maxEntries,
644
+ rejectMultiDisk: true
645
+ });
646
+ this.warningsList.push(...eocd.warnings);
647
+ this.eocd = eocd;
648
+ if (this.storeEntries) {
649
+ await this.loadEntries();
650
+ }
651
+ }
652
+ async loadEntries() {
653
+ if (this.entriesList)
654
+ return;
655
+ for await (const _ of this.iterEntries()) {
656
+ // iterEntries populates entriesList when shouldStoreEntries is enabled.
657
+ }
658
+ }
659
+ applyEntryLimits(entry, totals) {
660
+ if (entry.uncompressedSize > this.limits.maxUncompressedEntryBytes) {
661
+ throw new ZipError('ZIP_LIMIT_EXCEEDED', 'Entry exceeds max uncompressed size', {
662
+ entryName: entry.name
663
+ });
664
+ }
665
+ totals.totalUncompressed += entry.uncompressedSize;
666
+ if (totals.totalUncompressed > this.limits.maxTotalUncompressedBytes) {
667
+ throw new ZipError('ZIP_LIMIT_EXCEEDED', 'Total uncompressed size exceeds limit');
668
+ }
669
+ if (entry.compressedSize > 0n) {
670
+ const ratio = Number(entry.uncompressedSize) / Number(entry.compressedSize);
671
+ if (ratio > this.limits.maxCompressionRatio) {
672
+ const message = 'Compression ratio exceeds safety limit';
673
+ if (this.strict) {
674
+ throw new ZipError('ZIP_LIMIT_EXCEEDED', message, { entryName: entry.name });
675
+ }
676
+ this.warningsList.push({ code: 'ZIP_LIMIT_EXCEEDED', message, entryName: entry.name });
677
+ }
678
+ }
679
+ }
680
+ resolveAuditSettings(options) {
681
+ const profile = options?.profile ?? this.profile;
682
+ const defaults = profile === this.profile
683
+ ? { strict: this.strict, limits: this.limits }
684
+ : resolveProfileDefaults(profile);
685
+ const strict = options?.isStrict ?? defaults.strict;
686
+ const limits = normalizeLimits(options?.limits, defaults.limits);
687
+ return {
688
+ profile,
689
+ strict,
690
+ limits,
691
+ rejectTrailingBytes: profile === 'agent',
692
+ symlinkSeverity: profile === 'agent' ? 'error' : 'warning'
693
+ };
694
+ }
695
+ resolveSignal(signal) {
696
+ return mergeSignals(this.signal, signal);
697
+ }
698
+ async normalizeToSink(sink, options) {
699
+ const signal = this.resolveSignal(options?.signal);
700
+ throwIfAborted(signal);
701
+ const mode = options?.mode ?? 'safe';
702
+ const deterministic = options?.isDeterministic ?? true;
703
+ const onDuplicate = options?.onDuplicate ?? 'error';
704
+ const onCaseCollision = options?.onCaseCollision ?? 'error';
705
+ const onUnsupported = options?.onUnsupported ?? 'error';
706
+ const onSymlink = options?.onSymlink ?? 'error';
707
+ const preserveComments = options?.shouldPreserveComments ?? false;
708
+ const preserveTrailingBytes = options?.shouldPreserveTrailingBytes ?? false;
709
+ const limits = normalizeLimits(options?.limits, this.limits);
710
+ const outputMethod = options?.method ?? 8;
711
+ const password = options?.password ?? this.password;
712
+ const fixedMtime = new Date(1980, 0, 1, 0, 0, 0);
713
+ const issues = [];
714
+ const summary = {
715
+ entries: 0,
716
+ encryptedEntries: 0,
717
+ unsupportedEntries: 0,
718
+ warnings: 0,
719
+ errors: 0,
720
+ outputEntries: 0,
721
+ droppedEntries: 0,
722
+ renamedEntries: 0,
723
+ recompressedEntries: 0,
724
+ preservedEntries: 0
725
+ };
726
+ const addIssue = (issue) => {
727
+ issues.push(issue);
728
+ if (issue.severity === 'warning')
729
+ summary.warnings += 1;
730
+ if (issue.severity === 'error')
731
+ summary.errors += 1;
732
+ };
733
+ const normalizedEntries = await this.collectNormalizedEntries({
734
+ ...(signal ? { signal } : {}),
735
+ deterministic,
736
+ onDuplicate,
737
+ onCaseCollision,
738
+ onSymlink,
739
+ issues,
740
+ addIssue,
741
+ summary
742
+ });
743
+ const results = [];
744
+ const totals = { totalUncompressed: 0n };
745
+ let outputIndex = 0;
746
+ let tempDir = null;
747
+ if (mode === 'safe') {
748
+ tempDir = await mkdtemp(path.join(tmpdir(), 'bytefold-normalize-'));
749
+ }
750
+ try {
751
+ for (const item of normalizedEntries) {
752
+ throwIfAborted(signal);
753
+ if (item.dropped)
754
+ continue;
755
+ const entry = item.entry;
756
+ if (entry.encrypted)
757
+ summary.encryptedEntries += 1;
758
+ const name = item.normalizedName;
759
+ const mtime = deterministic ? fixedMtime : entry.mtime;
760
+ const externalAttributes = deterministic ? (entry.isDirectory ? 0x10 : 0) : entry.externalAttributes;
761
+ const comment = preserveComments && !deterministic ? entry.comment : undefined;
762
+ const aesExtra = entry.method === 99 ? parseAesExtra(entry.extra.get(0x9901) ?? new Uint8Array(0)) : undefined;
763
+ if (entry.method === 99 && !aesExtra) {
764
+ addIssue({
765
+ code: 'ZIP_UNSUPPORTED_ENCRYPTION',
766
+ severity: 'error',
767
+ message: 'AES extra field missing; cannot normalize entry',
768
+ entryName: entry.name
769
+ });
770
+ summary.unsupportedEntries += 1;
771
+ if (onUnsupported === 'drop') {
772
+ summary.droppedEntries += 1;
773
+ continue;
774
+ }
775
+ throw new ZipError('ZIP_UNSUPPORTED_ENCRYPTION', 'Missing AES extra field', {
776
+ entryName: entry.name
777
+ });
778
+ }
779
+ if (entry.isSymlink) {
780
+ if (onSymlink === 'drop') {
781
+ summary.droppedEntries += 1;
782
+ addIssue({
783
+ code: 'ZIP_SYMLINK_PRESENT',
784
+ severity: 'warning',
785
+ message: 'Symlink entry dropped during normalization',
786
+ entryName: entry.name
787
+ });
788
+ continue;
789
+ }
790
+ if (onSymlink === 'error') {
791
+ addIssue({
792
+ code: 'ZIP_SYMLINK_PRESENT',
793
+ severity: 'error',
794
+ message: 'Symlink entries are not allowed during normalization',
795
+ entryName: entry.name
796
+ });
797
+ throw new ZipError('ZIP_SYMLINK_DISALLOWED', 'Symlink entries are not allowed during normalization', {
798
+ entryName: entry.name
799
+ });
800
+ }
801
+ }
802
+ if (mode === 'safe') {
803
+ const methodToCheck = entry.method === 99 ? aesExtra?.actualMethod : entry.method;
804
+ if (methodToCheck !== undefined && !hasCompressionCodec(methodToCheck)) {
805
+ addIssue({
806
+ code: 'ZIP_UNSUPPORTED_METHOD',
807
+ severity: 'error',
808
+ message: `Unsupported compression method ${methodToCheck}`,
809
+ entryName: entry.name,
810
+ details: { method: methodToCheck }
811
+ });
812
+ summary.unsupportedEntries += 1;
813
+ if (onUnsupported === 'drop') {
814
+ summary.droppedEntries += 1;
815
+ continue;
816
+ }
817
+ throw new ZipError('ZIP_UNSUPPORTED_METHOD', `Unsupported compression method ${methodToCheck}`, {
818
+ entryName: entry.name,
819
+ method: methodToCheck
820
+ });
821
+ }
822
+ const method = entry.isDirectory ? 0 : outputMethod;
823
+ const codec = getCompressionCodec(method);
824
+ if (!codec || !codec.createCompressStream) {
825
+ addIssue({
826
+ code: 'ZIP_UNSUPPORTED_METHOD',
827
+ severity: 'error',
828
+ message: `Unsupported compression method ${method}`,
829
+ entryName: entry.name,
830
+ details: { method }
831
+ });
832
+ summary.unsupportedEntries += 1;
833
+ if (onUnsupported === 'drop') {
834
+ summary.droppedEntries += 1;
835
+ continue;
836
+ }
837
+ throw new ZipError('ZIP_UNSUPPORTED_METHOD', `Unsupported compression method ${method}`, {
838
+ entryName: entry.name,
839
+ method
840
+ });
841
+ }
842
+ if (entry.encrypted && !password) {
843
+ addIssue({
844
+ code: 'ZIP_PASSWORD_REQUIRED',
845
+ severity: 'error',
846
+ message: 'Password required for encrypted entry during normalization',
847
+ entryName: entry.name
848
+ });
849
+ if (onUnsupported === 'drop') {
850
+ summary.droppedEntries += 1;
851
+ continue;
852
+ }
853
+ throw new ZipError('ZIP_PASSWORD_REQUIRED', 'Password required for encrypted entry', {
854
+ entryName: entry.name
855
+ });
856
+ }
857
+ let source;
858
+ if (entry.isDirectory) {
859
+ source = readableFromBytes(new Uint8Array(0));
860
+ }
861
+ else {
862
+ source = await openEntryStream(this.reader, entry, {
863
+ strict: true,
864
+ onWarning: (warning) => addIssue({
865
+ code: warning.code,
866
+ severity: 'warning',
867
+ message: warning.message,
868
+ ...(warning.entryName ? { entryName: warning.entryName } : {})
869
+ }),
870
+ ...(password !== undefined ? { password } : {}),
871
+ ...(signal ? { signal } : {}),
872
+ ...progressParams(options),
873
+ limits,
874
+ totals
875
+ });
876
+ }
877
+ if (!tempDir) {
878
+ throw new ZipError('ZIP_UNSUPPORTED_FEATURE', 'Normalization temp directory missing');
879
+ }
880
+ const tempPath = path.join(tempDir, `entry-${outputIndex + 1}.bin`);
881
+ const spool = await spoolCompressedEntry({
882
+ source,
883
+ method,
884
+ entryName: name,
885
+ ...(signal ? { signal } : {}),
886
+ progress: progressParams(options),
887
+ tempPath
888
+ });
889
+ const dataStream = toWebReadable(createReadStream(tempPath));
890
+ const flags = 0x800;
891
+ const result = await writeRawEntry(sink, {
892
+ name,
893
+ source: dataStream,
894
+ method,
895
+ flags,
896
+ crc32: spool.crc32,
897
+ compressedSize: spool.compressedSize,
898
+ uncompressedSize: spool.uncompressedSize,
899
+ mtime,
900
+ comment,
901
+ externalAttributes,
902
+ zip64Mode: 'auto',
903
+ forceZip64: false,
904
+ ...(signal ? { signal } : {}),
905
+ ...(options ? { progress: progressParams(options) } : {})
906
+ });
907
+ results.push(result);
908
+ summary.outputEntries += 1;
909
+ outputIndex += 1;
910
+ summary.recompressedEntries += entry.isDirectory ? 0 : 1;
911
+ await rm(tempPath, { force: true }).catch(() => { });
912
+ continue;
913
+ }
914
+ // lossless mode
915
+ const methodToCheck = entry.method === 99 ? aesExtra?.actualMethod : entry.method;
916
+ if (methodToCheck !== undefined && !hasCompressionCodec(methodToCheck)) {
917
+ summary.unsupportedEntries += 1;
918
+ addIssue({
919
+ code: 'ZIP_UNSUPPORTED_METHOD',
920
+ severity: 'warning',
921
+ message: `Unsupported compression method ${methodToCheck} preserved in lossless mode`,
922
+ entryName: entry.name,
923
+ details: { method: methodToCheck }
924
+ });
925
+ }
926
+ const { stream: rawStream } = await openRawStream(this.reader, entry, {
927
+ ...(signal ? { signal } : {}),
928
+ ...progressParams(options)
929
+ });
930
+ const flags = 0x800 | (entry.encrypted ? 0x01 : 0);
931
+ const aesExtraBytes = aesExtra
932
+ ? buildAesExtra({
933
+ vendorVersion: aesExtra.vendorVersion,
934
+ strength: aesExtra.strength,
935
+ actualMethod: aesExtra.actualMethod
936
+ })
937
+ : undefined;
938
+ const result = await writeRawEntry(sink, {
939
+ name,
940
+ source: rawStream,
941
+ method: entry.method,
942
+ flags,
943
+ crc32: entry.crc32,
944
+ compressedSize: entry.compressedSize,
945
+ uncompressedSize: entry.uncompressedSize,
946
+ mtime,
947
+ comment,
948
+ externalAttributes,
949
+ zip64Mode: 'auto',
950
+ forceZip64: false,
951
+ ...(aesExtraBytes ? { aesExtra: aesExtraBytes } : {}),
952
+ ...(signal ? { signal } : {}),
953
+ ...(options ? { progress: progressParams(options) } : {})
954
+ });
955
+ results.push(result);
956
+ summary.outputEntries += 1;
957
+ outputIndex += 1;
958
+ summary.preservedEntries += 1;
959
+ }
960
+ const cdInfo = await writeCentralDirectory(sink, results, signal);
961
+ const finalizeOptions = {
962
+ entryCount: BigInt(results.length),
963
+ cdOffset: cdInfo.offset,
964
+ cdSize: cdInfo.size,
965
+ forceZip64: false,
966
+ hasZip64Entries: results.some((entry) => entry.zip64)
967
+ };
968
+ await finalizeArchive(sink, finalizeOptions, signal);
969
+ if (preserveTrailingBytes) {
970
+ const trailing = await readTrailingBytes(this.reader, this.eocd, signal);
971
+ if (trailing.length > 0) {
972
+ await sink.write(trailing);
973
+ }
974
+ }
975
+ await sink.close();
976
+ }
977
+ catch (err) {
978
+ await sink.close().catch(() => { });
979
+ if (tempDir) {
980
+ await rm(tempDir, { recursive: true, force: true }).catch(() => { });
981
+ }
982
+ throw err;
983
+ }
984
+ finally {
985
+ if (tempDir) {
986
+ await rm(tempDir, { recursive: true, force: true }).catch(() => { });
987
+ }
988
+ }
989
+ summary.entries = normalizedEntries.length;
990
+ const report = finalizeNormalizeReport(issues, summary);
991
+ return report;
992
+ }
993
+ async collectNormalizedEntries(params) {
994
+ const entries = [];
995
+ const nameIndex = new Map();
996
+ const caseIndex = new Map();
997
+ const nfcIndex = new Map();
998
+ const originalNames = new Map();
999
+ const iterOptions = params.signal ? { signal: params.signal } : undefined;
1000
+ for await (const entry of this.iterEntries(iterOptions)) {
1001
+ params.summary.entries += 1;
1002
+ const normalizedName = normalizeEntryName(entry.name, entry.isDirectory, params.addIssue);
1003
+ let targetName = normalizedName;
1004
+ let renamed = false;
1005
+ const existingIndex = nameIndex.get(targetName);
1006
+ if (existingIndex !== undefined) {
1007
+ if (params.onDuplicate === 'error') {
1008
+ const existingName = originalNames.get(targetName) ?? targetName;
1009
+ params.addIssue({
1010
+ code: 'ZIP_DUPLICATE_ENTRY',
1011
+ severity: 'error',
1012
+ message: `Duplicate entry name: ${existingName} vs ${entry.name}`,
1013
+ entryName: entry.name,
1014
+ details: { collisionKind: 'duplicate', otherName: existingName, key: targetName }
1015
+ });
1016
+ throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (duplicate). Rename entries to avoid collisions.', {
1017
+ entryName: entry.name,
1018
+ context: buildCollisionContext('duplicate', existingName, entry.name, targetName, 'zip')
1019
+ });
1020
+ }
1021
+ if (params.onDuplicate === 'last-wins') {
1022
+ entries[existingIndex].dropped = true;
1023
+ params.summary.droppedEntries += 1;
1024
+ params.addIssue({
1025
+ code: 'ZIP_DUPLICATE_ENTRY',
1026
+ severity: 'warning',
1027
+ message: `Duplicate entry name replaced by last occurrence: ${targetName}`,
1028
+ entryName: entry.name
1029
+ });
1030
+ }
1031
+ else if (params.onDuplicate === 'rename') {
1032
+ targetName = resolveConflictName(targetName, nameIndex, caseIndex);
1033
+ renamed = true;
1034
+ }
1035
+ }
1036
+ const nfcName = targetName.normalize('NFC');
1037
+ const existingNfc = nfcIndex.get(nfcName);
1038
+ if (existingNfc && existingNfc.target !== targetName) {
1039
+ params.addIssue({
1040
+ code: 'ZIP_UNICODE_COLLISION',
1041
+ severity: 'error',
1042
+ message: `Unicode normalization collision: ${existingNfc.original} vs ${entry.name}`,
1043
+ entryName: entry.name,
1044
+ details: { collisionKind: 'unicode_nfc', otherName: existingNfc.original, key: nfcName }
1045
+ });
1046
+ throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (unicode_nfc). Rename entries to avoid collisions.', {
1047
+ entryName: entry.name,
1048
+ context: buildCollisionContext('unicode_nfc', existingNfc.original, entry.name, nfcName, 'zip')
1049
+ });
1050
+ }
1051
+ const caseKey = toCollisionKey(targetName, entry.isDirectory);
1052
+ const existingCase = caseIndex.get(caseKey);
1053
+ if (existingCase && existingCase.target !== targetName) {
1054
+ if (params.onCaseCollision === 'error') {
1055
+ params.addIssue({
1056
+ code: 'ZIP_CASE_COLLISION',
1057
+ severity: 'error',
1058
+ message: `Case-insensitive name collision: ${existingCase.original} vs ${entry.name}`,
1059
+ entryName: entry.name,
1060
+ details: { collisionKind: 'casefold', otherName: existingCase.original, key: caseKey }
1061
+ });
1062
+ throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (case). Rename entries to avoid collisions.', {
1063
+ entryName: entry.name,
1064
+ context: buildCollisionContext('case', existingCase.original, entry.name, caseKey, 'zip')
1065
+ });
1066
+ }
1067
+ if (params.onCaseCollision === 'last-wins') {
1068
+ const previous = nameIndex.get(existingCase.target);
1069
+ if (previous !== undefined) {
1070
+ entries[previous].dropped = true;
1071
+ params.summary.droppedEntries += 1;
1072
+ }
1073
+ params.addIssue({
1074
+ code: 'ZIP_CASE_COLLISION',
1075
+ severity: 'warning',
1076
+ message: `Case-insensitive collision replaced by last occurrence: ${targetName}`,
1077
+ entryName: entry.name
1078
+ });
1079
+ }
1080
+ else if (params.onCaseCollision === 'rename') {
1081
+ targetName = resolveConflictName(targetName, nameIndex, caseIndex);
1082
+ renamed = true;
1083
+ }
1084
+ }
1085
+ nameIndex.set(targetName, entries.length);
1086
+ originalNames.set(targetName, entry.name);
1087
+ const finalNfc = targetName.normalize('NFC');
1088
+ nfcIndex.set(finalNfc, { original: entry.name, target: targetName });
1089
+ caseIndex.set(toCollisionKey(targetName, entry.isDirectory), {
1090
+ original: entry.name,
1091
+ target: targetName,
1092
+ nfc: finalNfc
1093
+ });
1094
+ if (renamed) {
1095
+ params.summary.renamedEntries += 1;
1096
+ params.addIssue({
1097
+ code: 'ZIP_NORMALIZED_NAME',
1098
+ severity: 'info',
1099
+ message: `Entry renamed to ${targetName}`,
1100
+ entryName: entry.name,
1101
+ details: { normalizedName: targetName }
1102
+ });
1103
+ }
1104
+ else if (targetName !== entry.name) {
1105
+ params.addIssue({
1106
+ code: 'ZIP_NORMALIZED_NAME',
1107
+ severity: 'info',
1108
+ message: `Entry name normalized to ${targetName}`,
1109
+ entryName: entry.name,
1110
+ details: { normalizedName: targetName }
1111
+ });
1112
+ }
1113
+ entries.push({
1114
+ entry: entry,
1115
+ normalizedName: targetName,
1116
+ dropped: false
1117
+ });
1118
+ }
1119
+ if (params.deterministic) {
1120
+ entries.sort((a, b) => (a.normalizedName < b.normalizedName ? -1 : a.normalizedName > b.normalizedName ? 1 : 0));
1121
+ }
1122
+ return entries;
1123
+ }
1124
+ }
1125
+ function normalizeEntryName(entryName, isDirectory, addIssue) {
1126
+ if (entryName.includes('\u0000')) {
1127
+ addIssue({
1128
+ code: 'ZIP_PATH_TRAVERSAL',
1129
+ severity: 'error',
1130
+ message: 'Entry name contains NUL byte',
1131
+ entryName
1132
+ });
1133
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Entry name contains NUL byte', { entryName });
1134
+ }
1135
+ const normalized = entryName.replace(/\\/g, '/');
1136
+ if (normalized.startsWith('/') || /^[a-zA-Z]:/.test(normalized)) {
1137
+ addIssue({
1138
+ code: 'ZIP_PATH_TRAVERSAL',
1139
+ severity: 'error',
1140
+ message: 'Absolute paths are not allowed in ZIP entries',
1141
+ entryName
1142
+ });
1143
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Absolute paths are not allowed in ZIP entries', { entryName });
1144
+ }
1145
+ const parts = normalized.split('/').filter((part) => part.length > 0 && part !== '.');
1146
+ if (parts.some((part) => part === '..')) {
1147
+ addIssue({
1148
+ code: 'ZIP_PATH_TRAVERSAL',
1149
+ severity: 'error',
1150
+ message: 'Path traversal detected in ZIP entry',
1151
+ entryName
1152
+ });
1153
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Path traversal detected in ZIP entry', { entryName });
1154
+ }
1155
+ let name = parts.join('/');
1156
+ if (isDirectory && !name.endsWith('/')) {
1157
+ name = name.length > 0 ? `${name}/` : '';
1158
+ }
1159
+ if (name.length === 0) {
1160
+ addIssue({
1161
+ code: 'ZIP_PATH_TRAVERSAL',
1162
+ severity: 'error',
1163
+ message: 'Entry name resolves to empty path',
1164
+ entryName
1165
+ });
1166
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Entry name resolves to empty path', { entryName });
1167
+ }
1168
+ return name;
1169
+ }
1170
+ function resolveConflictName(name, nameIndex, lowerIndex) {
1171
+ const trailingSlash = name.endsWith('/');
1172
+ const trimmed = trailingSlash ? name.slice(0, -1) : name;
1173
+ const slashIndex = trimmed.lastIndexOf('/');
1174
+ const dir = slashIndex >= 0 ? trimmed.slice(0, slashIndex + 1) : '';
1175
+ const file = slashIndex >= 0 ? trimmed.slice(slashIndex + 1) : trimmed;
1176
+ const dotIndex = file.lastIndexOf('.');
1177
+ const base = dotIndex > 0 ? file.slice(0, dotIndex) : file;
1178
+ const ext = dotIndex > 0 ? file.slice(dotIndex) : '';
1179
+ let counter = 1;
1180
+ while (true) {
1181
+ const candidate = `${dir}${base}~${counter}${ext}${trailingSlash ? '/' : ''}`;
1182
+ const caseKey = toCollisionKey(candidate, trailingSlash);
1183
+ if (!nameIndex.has(candidate) && !lowerIndex.has(caseKey)) {
1184
+ return candidate;
1185
+ }
1186
+ counter += 1;
1187
+ }
1188
+ }
1189
+ function buildCollisionContext(collisionType, nameA, nameB, key, format, collisionKind = collisionType === 'case' ? 'casefold' : collisionType) {
1190
+ return {
1191
+ collisionType,
1192
+ collisionKind,
1193
+ nameA,
1194
+ nameB,
1195
+ key,
1196
+ format
1197
+ };
1198
+ }
1199
+ async function spoolCompressedEntry(options) {
1200
+ const codec = getCompressionCodec(options.method);
1201
+ if (!codec || !codec.createCompressStream) {
1202
+ throw new ZipError('ZIP_UNSUPPORTED_METHOD', `Unsupported compression method ${options.method}`, {
1203
+ entryName: options.entryName,
1204
+ method: options.method
1205
+ });
1206
+ }
1207
+ const crcResult = { crc32: 0, bytes: 0n };
1208
+ const measure = { bytes: 0n };
1209
+ const compressTracker = createProgressTracker(options.progress, {
1210
+ kind: 'compress',
1211
+ entryName: options.entryName
1212
+ });
1213
+ const writeTracker = createProgressTracker(options.progress, {
1214
+ kind: 'write',
1215
+ entryName: options.entryName
1216
+ });
1217
+ let stream = options.source;
1218
+ stream = stream.pipeThrough(createCrcTransform(crcResult, { strict: true }));
1219
+ stream = stream.pipeThrough(createProgressTransform(compressTracker));
1220
+ const transform = await codec.createCompressStream();
1221
+ stream = stream.pipeThrough(transform);
1222
+ stream = stream.pipeThrough(createMeasureTransform(measure));
1223
+ const sink = new NodeWritableSink(createWriteStream(options.tempPath));
1224
+ try {
1225
+ await pipeToSink(stream, sink, options.signal, writeTracker);
1226
+ await sink.close();
1227
+ }
1228
+ catch (err) {
1229
+ await sink.close().catch(() => { });
1230
+ throw err;
1231
+ }
1232
+ return {
1233
+ compressedSize: measure.bytes,
1234
+ uncompressedSize: crcResult.bytes,
1235
+ crc32: crcResult.crc32
1236
+ };
1237
+ }
1238
+ async function pipeToSink(stream, sink, signal, tracker) {
1239
+ const reader = stream.getReader();
1240
+ try {
1241
+ while (true) {
1242
+ throwIfAborted(signal);
1243
+ const { value, done } = await reader.read();
1244
+ if (done)
1245
+ break;
1246
+ if (value) {
1247
+ await sink.write(value);
1248
+ tracker?.update(value.length, value.length);
1249
+ }
1250
+ }
1251
+ }
1252
+ catch (err) {
1253
+ await reader.cancel().catch(() => { });
1254
+ throw err;
1255
+ }
1256
+ finally {
1257
+ reader.releaseLock();
1258
+ }
1259
+ tracker?.flush();
1260
+ }
1261
+ async function readTrailingBytes(reader, eocd, signal) {
1262
+ if (!eocd)
1263
+ return new Uint8Array(0);
1264
+ const size = await reader.size(signal);
1265
+ const eocdEnd = eocd.eocdOffset + 22n + BigInt(eocd.comment.length);
1266
+ if (size <= eocdEnd)
1267
+ return new Uint8Array(0);
1268
+ const trailing = size - eocdEnd;
1269
+ const trailingNumber = toSafeNumber(trailing);
1270
+ if (trailingNumber === undefined) {
1271
+ throw new ZipError('ZIP_LIMIT_EXCEEDED', 'Trailing bytes too large to preserve');
1272
+ }
1273
+ return reader.read(eocdEnd, trailingNumber, signal);
1274
+ }
1275
+ function finalizeNormalizeReport(issues, summary) {
1276
+ const report = {
1277
+ ok: summary.errors === 0,
1278
+ summary,
1279
+ issues
1280
+ };
1281
+ report.toJSON = () => ({
1282
+ ok: report.ok,
1283
+ summary: report.summary,
1284
+ issues: issues.map(issueToJson)
1285
+ });
1286
+ return report;
1287
+ }
1288
+ function resolveReaderProfile(options) {
1289
+ const profile = options?.profile ?? 'strict';
1290
+ const defaults = profile === 'agent' ? AGENT_LIMITS : DEFAULT_LIMITS;
1291
+ const strictDefault = profile === 'compat' ? false : true;
1292
+ const strict = options?.isStrict ?? strictDefault;
1293
+ const limits = normalizeLimits(options?.limits, defaults);
1294
+ return { profile, strict, limits };
1295
+ }
1296
+ function resolveProfileDefaults(profile) {
1297
+ if (profile === 'compat') {
1298
+ return { strict: false, limits: DEFAULT_LIMITS };
1299
+ }
1300
+ if (profile === 'agent') {
1301
+ return { strict: true, limits: AGENT_LIMITS };
1302
+ }
1303
+ return { strict: true, limits: DEFAULT_LIMITS };
1304
+ }
1305
+ /** @internal */
1306
+ export function __getNodeZipDefaultsForProfile(profile) {
1307
+ return resolveProfileDefaults(profile).limits;
1308
+ }
1309
+ function normalizeLimits(limits, defaults = DEFAULT_LIMITS) {
1310
+ const maxTotal = toBigInt(limits?.maxTotalDecompressedBytes ?? limits?.maxTotalUncompressedBytes) ??
1311
+ defaults.maxTotalUncompressedBytes;
1312
+ return {
1313
+ maxEntries: limits?.maxEntries ?? defaults.maxEntries,
1314
+ maxUncompressedEntryBytes: toBigInt(limits?.maxUncompressedEntryBytes) ?? defaults.maxUncompressedEntryBytes,
1315
+ maxTotalUncompressedBytes: maxTotal,
1316
+ maxTotalDecompressedBytes: maxTotal,
1317
+ maxCompressionRatio: limits?.maxCompressionRatio ?? defaults.maxCompressionRatio,
1318
+ maxDictionaryBytes: toBigInt(limits?.maxDictionaryBytes) ?? defaults.maxDictionaryBytes,
1319
+ maxXzDictionaryBytes: toBigInt(limits?.maxXzDictionaryBytes ?? limits?.maxDictionaryBytes) ?? defaults.maxXzDictionaryBytes,
1320
+ maxXzBufferedBytes: typeof limits?.maxXzBufferedBytes === 'number' && Number.isFinite(limits.maxXzBufferedBytes)
1321
+ ? Math.max(1, Math.floor(limits.maxXzBufferedBytes))
1322
+ : defaults.maxXzBufferedBytes,
1323
+ maxXzIndexRecords: typeof limits?.maxXzIndexRecords === 'number' && Number.isFinite(limits.maxXzIndexRecords)
1324
+ ? Math.max(1, Math.floor(limits.maxXzIndexRecords))
1325
+ : defaults.maxXzIndexRecords,
1326
+ maxXzIndexBytes: typeof limits?.maxXzIndexBytes === 'number' && Number.isFinite(limits.maxXzIndexBytes)
1327
+ ? Math.max(8, Math.floor(limits.maxXzIndexBytes))
1328
+ : defaults.maxXzIndexBytes,
1329
+ maxXzPreflightBlockHeaders: typeof limits?.maxXzPreflightBlockHeaders === 'number' && Number.isFinite(limits.maxXzPreflightBlockHeaders)
1330
+ ? Math.max(0, Math.floor(limits.maxXzPreflightBlockHeaders))
1331
+ : defaults.maxXzPreflightBlockHeaders,
1332
+ maxZipCentralDirectoryBytes: typeof limits?.maxZipCentralDirectoryBytes === 'number' && Number.isFinite(limits.maxZipCentralDirectoryBytes)
1333
+ ? Math.max(0, Math.floor(limits.maxZipCentralDirectoryBytes))
1334
+ : defaults.maxZipCentralDirectoryBytes,
1335
+ maxZipCommentBytes: typeof limits?.maxZipCommentBytes === 'number' && Number.isFinite(limits.maxZipCommentBytes)
1336
+ ? Math.max(0, Math.floor(limits.maxZipCommentBytes))
1337
+ : defaults.maxZipCommentBytes,
1338
+ maxZipEocdSearchBytes: typeof limits?.maxZipEocdSearchBytes === 'number' && Number.isFinite(limits.maxZipEocdSearchBytes)
1339
+ ? Math.max(22, Math.floor(limits.maxZipEocdSearchBytes))
1340
+ : defaults.maxZipEocdSearchBytes,
1341
+ maxBzip2BlockSize: typeof limits?.maxBzip2BlockSize === 'number' && Number.isFinite(limits.maxBzip2BlockSize)
1342
+ ? Math.max(1, Math.min(9, Math.floor(limits.maxBzip2BlockSize)))
1343
+ : defaults.maxBzip2BlockSize,
1344
+ maxInputBytes: toBigInt(limits?.maxInputBytes) ?? defaults.maxInputBytes
1345
+ };
1346
+ }
1347
+ function progressParams(options) {
1348
+ if (!options)
1349
+ return {};
1350
+ const out = {};
1351
+ if (options.onProgress)
1352
+ out.onProgress = options.onProgress;
1353
+ if (options.progressIntervalMs !== undefined)
1354
+ out.progressIntervalMs = options.progressIntervalMs;
1355
+ if (options.progressChunkInterval !== undefined)
1356
+ out.progressChunkInterval = options.progressChunkInterval;
1357
+ return out;
1358
+ }
1359
+ function toBigInt(value) {
1360
+ if (value === undefined)
1361
+ return undefined;
1362
+ return typeof value === 'bigint' ? value : BigInt(value);
1363
+ }
1364
+ function resolveEntryPath(baseDir, entryName) {
1365
+ if (entryName.includes('\u0000')) {
1366
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Entry name contains NUL byte', { entryName });
1367
+ }
1368
+ const normalized = entryName.replace(/\\/g, '/');
1369
+ if (normalized.startsWith('/') || /^[a-zA-Z]:/.test(normalized)) {
1370
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Absolute paths are not allowed in ZIP entries', {
1371
+ entryName
1372
+ });
1373
+ }
1374
+ const parts = normalized.split('/').filter((part) => part.length > 0);
1375
+ if (parts.some((part) => part === '..')) {
1376
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Path traversal detected in ZIP entry', { entryName });
1377
+ }
1378
+ const resolved = path.resolve(baseDir, ...parts);
1379
+ const baseResolved = path.resolve(baseDir);
1380
+ if (resolved !== baseResolved && !resolved.startsWith(baseResolved + path.sep)) {
1381
+ throw new ZipError('ZIP_PATH_TRAVERSAL', 'Entry path escapes destination directory', { entryName });
1382
+ }
1383
+ return resolved;
1384
+ }
1385
+ function entryPathIssues(entryName) {
1386
+ const issues = [];
1387
+ if (entryName.includes('\u0000')) {
1388
+ issues.push({
1389
+ code: 'ZIP_PATH_TRAVERSAL',
1390
+ severity: 'error',
1391
+ message: 'Entry name contains NUL byte',
1392
+ entryName
1393
+ });
1394
+ return issues;
1395
+ }
1396
+ const normalized = entryName.replace(/\\/g, '/');
1397
+ if (normalized.startsWith('/') || /^[a-zA-Z]:/.test(normalized)) {
1398
+ issues.push({
1399
+ code: 'ZIP_PATH_TRAVERSAL',
1400
+ severity: 'error',
1401
+ message: 'Absolute paths are not allowed in ZIP entries',
1402
+ entryName
1403
+ });
1404
+ }
1405
+ const parts = normalized.split('/').filter((part) => part.length > 0);
1406
+ if (parts.some((part) => part === '..')) {
1407
+ issues.push({
1408
+ code: 'ZIP_PATH_TRAVERSAL',
1409
+ severity: 'error',
1410
+ message: 'Path traversal detected in ZIP entry',
1411
+ entryName
1412
+ });
1413
+ }
1414
+ return issues;
1415
+ }
1416
+ function collectHeaderMismatches(entry, local) {
1417
+ const details = {};
1418
+ if (entry.flags !== local.flags) {
1419
+ details.flags = { local: local.flags, central: entry.flags };
1420
+ }
1421
+ if (entry.method !== local.method) {
1422
+ details.method = { local: local.method, central: entry.method };
1423
+ }
1424
+ if (local.nameLen !== entry.rawNameBytes.length) {
1425
+ details.nameLength = { local: local.nameLen, central: entry.rawNameBytes.length };
1426
+ }
1427
+ if (!bytesEqual(local.nameBytes, entry.rawNameBytes)) {
1428
+ details.nameBytes = { mismatch: true };
1429
+ }
1430
+ if (local.extraLen !== entry.extraLength) {
1431
+ details.extraLength = { local: local.extraLen, central: entry.extraLength };
1432
+ }
1433
+ return Object.keys(details).length > 0 ? details : undefined;
1434
+ }
1435
+ function bytesEqual(a, b) {
1436
+ if (a.length !== b.length)
1437
+ return false;
1438
+ for (let i = 0; i < a.length; i += 1) {
1439
+ if (a[i] !== b[i])
1440
+ return false;
1441
+ }
1442
+ return true;
1443
+ }
1444
+ function issueFromError(err) {
1445
+ if (err instanceof ZipError) {
1446
+ return {
1447
+ code: err.code,
1448
+ severity: 'error',
1449
+ message: err.message,
1450
+ ...(err.entryName ? { entryName: err.entryName } : {}),
1451
+ ...(err.offset !== undefined ? { offset: err.offset.toString() } : {}),
1452
+ ...(err.cause ? { details: { cause: String(err.cause) } } : {})
1453
+ };
1454
+ }
1455
+ if (err instanceof Error) {
1456
+ return {
1457
+ code: 'ZIP_AUDIT_ERROR',
1458
+ severity: 'error',
1459
+ message: err.message,
1460
+ details: { name: err.name }
1461
+ };
1462
+ }
1463
+ return {
1464
+ code: 'ZIP_AUDIT_ERROR',
1465
+ severity: 'error',
1466
+ message: 'Unknown audit error'
1467
+ };
1468
+ }
1469
+ function errorDetails(err) {
1470
+ if (err instanceof ZipError) {
1471
+ return { code: err.code, message: err.message };
1472
+ }
1473
+ if (err instanceof Error) {
1474
+ return { name: err.name, message: err.message };
1475
+ }
1476
+ return { value: String(err) };
1477
+ }
1478
+ function toSafeNumber(value) {
1479
+ if (value > BigInt(Number.MAX_SAFE_INTEGER))
1480
+ return undefined;
1481
+ return Number(value);
1482
+ }
1483
+ function finalizeAuditReport(issues, summary) {
1484
+ const report = {
1485
+ ok: summary.errors === 0,
1486
+ summary,
1487
+ issues
1488
+ };
1489
+ report.toJSON = () => ({
1490
+ ok: report.ok,
1491
+ summary: report.summary,
1492
+ issues: issues.map(issueToJson)
1493
+ });
1494
+ return report;
1495
+ }
1496
+ function issueToJson(issue) {
1497
+ return {
1498
+ code: issue.code,
1499
+ severity: issue.severity,
1500
+ message: issue.message,
1501
+ ...(issue.entryName ? { entryName: issue.entryName } : {}),
1502
+ ...(issue.offset !== undefined ? { offset: issue.offset.toString() } : {}),
1503
+ ...(issue.details ? { details: sanitizeDetails(issue.details) } : {})
1504
+ };
1505
+ }
1506
+ function sanitizeDetails(value) {
1507
+ if (typeof value === 'bigint')
1508
+ return value.toString();
1509
+ if (Array.isArray(value))
1510
+ return value.map(sanitizeDetails);
1511
+ if (value && typeof value === 'object') {
1512
+ const out = {};
1513
+ for (const [key, val] of Object.entries(value)) {
1514
+ out[key] = sanitizeDetails(val);
1515
+ }
1516
+ return out;
1517
+ }
1518
+ return value;
1519
+ }
1520
+ class TempFileRandomAccess {
1521
+ filePath;
1522
+ tempDir;
1523
+ inner;
1524
+ constructor(filePath, tempDir) {
1525
+ this.filePath = filePath;
1526
+ this.tempDir = tempDir;
1527
+ this.inner = FileRandomAccess.fromPath(filePath);
1528
+ }
1529
+ size(signal) {
1530
+ return this.inner.size(signal);
1531
+ }
1532
+ read(offset, length, signal) {
1533
+ return this.inner.read(offset, length, signal);
1534
+ }
1535
+ async close() {
1536
+ await this.inner.close();
1537
+ await rm(this.tempDir, { recursive: true, force: true });
1538
+ }
1539
+ }
1540
+ //# sourceMappingURL=ZipReader.js.map