neozipkit 0.2.1 → 0.3.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 (301) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +410 -186
  3. package/contracts/ABI.txt +889 -0
  4. package/contracts/Bytecode.txt +1 -0
  5. package/contracts/COMPILATION_NOTES.md +59 -0
  6. package/contracts/QUICK_DEPLOY.md +286 -0
  7. package/contracts/README.md +235 -131
  8. package/contracts/VERIFICATION.md +308 -0
  9. package/contracts/deploy-interactive.js +448 -0
  10. package/contracts/deployment-arbitrum-sepolia.json +12 -0
  11. package/contracts/deployment-arbitrum.json +12 -0
  12. package/contracts/deployment-eth-sepolia.json +12 -0
  13. package/contracts/hardhat.config.js +114 -0
  14. package/contracts/package.json +20 -0
  15. package/contracts/scripts/compile-and-extract.js +119 -0
  16. package/contracts/scripts/verify-contract.js +223 -0
  17. package/contracts/src/{NZIP-NFT-Private.sol → NZIP-NFT-v2.0.sol} +66 -151
  18. package/contracts/src/NZIP-NFT.sol +9 -15
  19. package/dist/blockchain/browser/TokenVerifierBrowser.d.ts +67 -0
  20. package/dist/blockchain/browser/TokenVerifierBrowser.d.ts.map +1 -0
  21. package/dist/blockchain/browser/TokenVerifierBrowser.js +313 -0
  22. package/dist/blockchain/browser/TokenVerifierBrowser.js.map +1 -0
  23. package/dist/blockchain/browser/WalletManagerBrowser.d.ts +98 -0
  24. package/dist/blockchain/browser/WalletManagerBrowser.d.ts.map +1 -0
  25. package/dist/blockchain/browser/WalletManagerBrowser.js +434 -0
  26. package/dist/blockchain/browser/WalletManagerBrowser.js.map +1 -0
  27. package/dist/blockchain/browser/ZipkitMinterBrowser.d.ts +58 -0
  28. package/dist/blockchain/browser/ZipkitMinterBrowser.d.ts.map +1 -0
  29. package/dist/blockchain/browser/ZipkitMinterBrowser.js +317 -0
  30. package/dist/blockchain/browser/ZipkitMinterBrowser.js.map +1 -0
  31. package/dist/blockchain/browser/index.d.ts +11 -0
  32. package/dist/blockchain/browser/index.d.ts.map +1 -0
  33. package/dist/blockchain/browser/index.js +16 -0
  34. package/dist/blockchain/browser/index.js.map +1 -0
  35. package/dist/blockchain/core/WalletManager.d.ts +124 -0
  36. package/dist/blockchain/core/WalletManager.d.ts.map +1 -0
  37. package/dist/blockchain/{ZipKitWallet.js → core/WalletManager.js} +157 -177
  38. package/dist/blockchain/core/WalletManager.js.map +1 -0
  39. package/dist/blockchain/{ZipKitMinter.d.ts → core/ZipkitMinter.d.ts} +46 -11
  40. package/dist/blockchain/core/ZipkitMinter.d.ts.map +1 -0
  41. package/dist/blockchain/core/ZipkitMinter.js +548 -0
  42. package/dist/blockchain/core/ZipkitMinter.js.map +1 -0
  43. package/dist/blockchain/{ZipKitOTS.d.ts → core/ZipkitOTS.d.ts} +6 -15
  44. package/dist/blockchain/core/ZipkitOTS.d.ts.map +1 -0
  45. package/dist/blockchain/{ZipKitOTS.js → core/ZipkitOTS.js} +26 -84
  46. package/dist/blockchain/core/ZipkitOTS.js.map +1 -0
  47. package/dist/blockchain/core/ZipkitVerifier.d.ts +177 -0
  48. package/dist/blockchain/core/ZipkitVerifier.d.ts.map +1 -0
  49. package/dist/blockchain/core/ZipkitVerifier.js +632 -0
  50. package/dist/blockchain/core/ZipkitVerifier.js.map +1 -0
  51. package/dist/blockchain/core/contracts.d.ts +83 -0
  52. package/dist/blockchain/core/contracts.d.ts.map +1 -0
  53. package/dist/blockchain/core/contracts.js +341 -0
  54. package/dist/blockchain/core/contracts.js.map +1 -0
  55. package/dist/blockchain/core/index.d.ts +12 -0
  56. package/dist/blockchain/core/index.d.ts.map +1 -0
  57. package/dist/blockchain/core/index.js +59 -0
  58. package/dist/blockchain/core/index.js.map +1 -0
  59. package/dist/blockchain/index.d.ts +11 -7
  60. package/dist/blockchain/index.d.ts.map +1 -0
  61. package/dist/blockchain/index.js +18 -34
  62. package/dist/blockchain/index.js.map +1 -0
  63. package/dist/blockchain/node/WalletManagerNode.d.ts +47 -0
  64. package/dist/blockchain/node/WalletManagerNode.d.ts.map +1 -0
  65. package/dist/blockchain/node/WalletManagerNode.js +203 -0
  66. package/dist/blockchain/node/WalletManagerNode.js.map +1 -0
  67. package/dist/blockchain/node/index.d.ts +12 -0
  68. package/dist/blockchain/node/index.d.ts.map +1 -0
  69. package/dist/blockchain/node/index.js +24 -0
  70. package/dist/blockchain/node/index.js.map +1 -0
  71. package/dist/browser/ZipkitBrowser.d.ts +27 -0
  72. package/dist/browser/ZipkitBrowser.d.ts.map +1 -0
  73. package/dist/browser/{ZipKitBrowser.js → ZipkitBrowser.js} +96 -43
  74. package/dist/browser/ZipkitBrowser.js.map +1 -0
  75. package/dist/browser/index.d.ts +12 -0
  76. package/dist/browser/index.d.ts.map +1 -0
  77. package/dist/browser/index.js +42 -0
  78. package/dist/browser/index.js.map +1 -0
  79. package/dist/browser-esm/index.d.ts +12 -0
  80. package/dist/browser-esm/index.js +59559 -0
  81. package/dist/browser-esm/index.js.map +7 -0
  82. package/dist/browser-umd/index.d.ts +12 -0
  83. package/dist/browser-umd/index.js +59576 -0
  84. package/dist/browser-umd/index.js.map +7 -0
  85. package/dist/browser-umd/index.min.js +63 -0
  86. package/dist/browser.d.ts +8 -14
  87. package/dist/browser.js +13 -28
  88. package/dist/core/ZipCompress.d.ts +99 -0
  89. package/dist/core/ZipCompress.d.ts.map +1 -0
  90. package/dist/core/ZipCompress.js +309 -0
  91. package/dist/core/ZipCompress.js.map +1 -0
  92. package/dist/core/ZipDecompress.d.ts +58 -0
  93. package/dist/core/ZipDecompress.d.ts.map +1 -0
  94. package/dist/core/ZipDecompress.js +172 -0
  95. package/dist/core/ZipDecompress.js.map +1 -0
  96. package/dist/{shared → core}/ZipEntry.d.ts +8 -1
  97. package/dist/core/ZipEntry.d.ts.map +1 -0
  98. package/dist/{shared → core}/ZipEntry.js +225 -82
  99. package/dist/core/ZipEntry.js.map +1 -0
  100. package/dist/core/Zipkit.d.ts +315 -0
  101. package/dist/core/Zipkit.d.ts.map +1 -0
  102. package/dist/core/Zipkit.js +647 -0
  103. package/dist/core/Zipkit.js.map +1 -0
  104. package/dist/core/components/HashCalculator.d.ts +138 -0
  105. package/dist/core/components/HashCalculator.d.ts.map +1 -0
  106. package/dist/core/components/HashCalculator.js +360 -0
  107. package/dist/core/components/HashCalculator.js.map +1 -0
  108. package/dist/core/components/Logger.d.ts +73 -0
  109. package/dist/core/components/Logger.d.ts.map +1 -0
  110. package/dist/core/components/Logger.js +156 -0
  111. package/dist/core/components/Logger.js.map +1 -0
  112. package/dist/core/components/ProgressTracker.d.ts +43 -0
  113. package/dist/core/components/ProgressTracker.d.ts.map +1 -0
  114. package/dist/core/components/ProgressTracker.js +112 -0
  115. package/dist/core/components/ProgressTracker.js.map +1 -0
  116. package/dist/{shared → core}/components/Support.d.ts +1 -0
  117. package/dist/core/components/Support.d.ts.map +1 -0
  118. package/dist/{shared → core}/components/Support.js +1 -0
  119. package/dist/core/components/Support.js.map +1 -0
  120. package/dist/{shared → core}/components/Util.d.ts +7 -2
  121. package/dist/core/components/Util.d.ts.map +1 -0
  122. package/dist/{shared → core}/components/Util.js +14 -34
  123. package/dist/core/components/Util.js.map +1 -0
  124. package/dist/{shared → core}/constants/Errors.d.ts +1 -2
  125. package/dist/core/constants/Errors.d.ts.map +1 -0
  126. package/dist/{shared → core}/constants/Errors.js +1 -3
  127. package/dist/core/constants/Errors.js.map +1 -0
  128. package/dist/{shared → core}/constants/Headers.d.ts +3 -0
  129. package/dist/core/constants/Headers.d.ts.map +1 -0
  130. package/dist/{shared → core}/constants/Headers.js +20 -17
  131. package/dist/core/constants/Headers.js.map +1 -0
  132. package/dist/core/encryption/Manager.d.ts +58 -0
  133. package/dist/core/encryption/Manager.d.ts.map +1 -0
  134. package/dist/core/encryption/Manager.js +121 -0
  135. package/dist/core/encryption/Manager.js.map +1 -0
  136. package/dist/core/encryption/ZipCrypto.d.ts +172 -0
  137. package/dist/core/encryption/ZipCrypto.d.ts.map +1 -0
  138. package/dist/core/encryption/ZipCrypto.js +554 -0
  139. package/dist/core/encryption/ZipCrypto.js.map +1 -0
  140. package/dist/core/encryption/index.d.ts +9 -0
  141. package/dist/core/encryption/index.d.ts.map +1 -0
  142. package/dist/core/encryption/index.js +17 -0
  143. package/dist/core/encryption/index.js.map +1 -0
  144. package/dist/core/encryption/types.d.ts +29 -0
  145. package/dist/core/encryption/types.d.ts.map +1 -0
  146. package/dist/core/encryption/types.js +12 -0
  147. package/dist/core/encryption/types.js.map +1 -0
  148. package/dist/core/index.d.ts +24 -0
  149. package/dist/core/index.d.ts.map +1 -0
  150. package/dist/core/index.js +57 -0
  151. package/dist/core/index.js.map +1 -0
  152. package/dist/core/version.d.ts +5 -0
  153. package/dist/core/version.d.ts.map +1 -0
  154. package/dist/core/version.js +31 -0
  155. package/dist/core/version.js.map +1 -0
  156. package/dist/index.d.ts +8 -18
  157. package/dist/index.d.ts.map +1 -0
  158. package/dist/index.js +29 -35
  159. package/dist/index.js.map +1 -0
  160. package/dist/node/ZipCompressNode.d.ts +123 -0
  161. package/dist/node/ZipCompressNode.d.ts.map +1 -0
  162. package/dist/node/ZipCompressNode.js +576 -0
  163. package/dist/node/ZipCompressNode.js.map +1 -0
  164. package/dist/node/ZipDecompressNode.d.ts +120 -0
  165. package/dist/node/ZipDecompressNode.d.ts.map +1 -0
  166. package/dist/node/ZipDecompressNode.js +461 -0
  167. package/dist/node/ZipDecompressNode.js.map +1 -0
  168. package/dist/node/ZipkitNode.d.ts +406 -0
  169. package/dist/node/ZipkitNode.d.ts.map +1 -0
  170. package/dist/node/ZipkitNode.js +1305 -0
  171. package/dist/node/ZipkitNode.js.map +1 -0
  172. package/dist/node/index.d.ts +26 -0
  173. package/dist/node/index.d.ts.map +1 -0
  174. package/dist/node/index.js +65 -0
  175. package/dist/node/index.js.map +1 -0
  176. package/dist/types/index.d.ts +59 -0
  177. package/dist/types/index.d.ts.map +1 -0
  178. package/dist/types/index.js +36 -1
  179. package/dist/types/index.js.map +1 -0
  180. package/examples/README.md +269 -0
  181. package/examples/blockchain-tokenize.ts +354 -0
  182. package/examples/create-zip.ts +172 -0
  183. package/examples/extract-zip.ts +158 -0
  184. package/examples/list-zip.ts +161 -0
  185. package/examples/test-files/data.json +116 -0
  186. package/examples/test-files/document.md +80 -0
  187. package/examples/test-files/document.txt +2 -0
  188. package/examples/test-files/file1.txt +48 -0
  189. package/examples/test-files/file2.txt +80 -0
  190. package/examples/tsconfig.json +44 -0
  191. package/examples/unzip-token.ts +491 -0
  192. package/examples/verify-tokenized-zip.ts +305 -0
  193. package/examples/zip-token.ts +604 -0
  194. package/package.json +108 -24
  195. package/src/blockchain/README.md +59 -66
  196. package/src/{browser/TokenVerifierBrowser.ts-remove → blockchain/browser/TokenVerifierBrowser.ts} +125 -79
  197. package/src/blockchain/browser/WalletManagerBrowser.ts +486 -0
  198. package/src/blockchain/browser/ZipkitMinterBrowser.ts +400 -0
  199. package/src/blockchain/browser/index.ts +13 -0
  200. package/src/blockchain/{ZipKitWallet.ts → core/WalletManager.ts} +241 -143
  201. package/src/blockchain/core/ZipkitMinter.ts +716 -0
  202. package/src/blockchain/{ZipKitOTS.ts → core/ZipkitOTS.ts} +28 -62
  203. package/src/blockchain/core/ZipkitVerifier.ts +853 -0
  204. package/src/blockchain/core/contracts.ts +370 -0
  205. package/src/blockchain/core/index.ts +71 -0
  206. package/src/blockchain/index.ts +21 -55
  207. package/src/blockchain/node/WalletManagerNode.ts +171 -0
  208. package/src/blockchain/node/index.ts +40 -0
  209. package/src/browser/{ZipKitBrowser.ts → ZipkitBrowser.ts} +104 -41
  210. package/src/browser/index.esm.ts +37 -0
  211. package/src/browser/index.ts +25 -0
  212. package/src/core/ZipCompress.ts +398 -0
  213. package/src/core/ZipDecompress.ts +212 -0
  214. package/src/{shared → core}/ZipEntry.ts +240 -90
  215. package/src/core/Zipkit.ts +794 -0
  216. package/src/core/components/HashCalculator.ts +384 -0
  217. package/src/core/components/Logger.ts +180 -0
  218. package/src/core/components/ProgressTracker.ts +134 -0
  219. package/src/{shared → core}/components/Util.ts +12 -34
  220. package/src/{shared → core}/constants/Errors.ts +0 -4
  221. package/src/{shared → core}/constants/Headers.ts +19 -17
  222. package/src/core/encryption/Manager.ts +137 -0
  223. package/src/core/encryption/ZipCrypto.ts +650 -0
  224. package/src/core/encryption/index.ts +15 -0
  225. package/src/core/encryption/types.ts +33 -0
  226. package/src/core/index.ts +34 -0
  227. package/src/core/version.ts +33 -0
  228. package/src/index.ts +54 -25
  229. package/src/node/ZipCompressNode.ts +632 -0
  230. package/src/node/ZipDecompressNode.ts +508 -0
  231. package/src/node/ZipkitNode.ts +1559 -0
  232. package/src/node/index.ts +69 -0
  233. package/src/types/index.ts +131 -1
  234. package/src/types/modules.d.ts +1 -11
  235. package/src/types/opentimestamps.d.ts +1 -23
  236. package/contracts/INTEGRATION.md +0 -338
  237. package/contracts/NETWORKS.md +0 -156
  238. package/contracts/NZIP-NFT-bytecode.txt +0 -1
  239. package/contracts/constants.ts +0 -952
  240. package/contracts/deploy-bytecode.mjs +0 -94
  241. package/dist/blockchain/CertificateToken.d.ts +0 -48
  242. package/dist/blockchain/CertificateToken.js +0 -386
  243. package/dist/blockchain/ZipKitMinter.js +0 -347
  244. package/dist/blockchain/ZipKitNFT.d.ts +0 -3
  245. package/dist/blockchain/ZipKitNFT.js +0 -7
  246. package/dist/blockchain/ZipKitVerifier.d.ts +0 -69
  247. package/dist/blockchain/ZipKitVerifier.js +0 -289
  248. package/dist/blockchain/ZipKitWallet.d.ts +0 -69
  249. package/dist/blockchain/contracts.d.ts +0 -14
  250. package/dist/blockchain/contracts.js +0 -77
  251. package/dist/blockchain/types.d.ts +0 -47
  252. package/dist/blockchain/types.js +0 -30
  253. package/dist/browser/ZipKitBrowser.d.ts +0 -18
  254. package/dist/browser/ZipKitStampBrowser.d.ts +0 -2
  255. package/dist/browser/ZipKitStampBrowser.js +0 -12
  256. package/dist/browser-index.d.ts +0 -18
  257. package/dist/browser-index.js +0 -57
  258. package/dist/server/ZipKitServer.d.ts +0 -74
  259. package/dist/server/ZipKitServer.js +0 -310
  260. package/dist/shared/ZipKit.d.ts +0 -243
  261. package/dist/shared/ZipKit.js +0 -772
  262. package/dist/shared/components/CertificateOTS.d.ts +0 -15
  263. package/dist/shared/components/CertificateOTS.js +0 -116
  264. package/dist/shared/components/FileSys.d.ts +0 -2
  265. package/dist/shared/components/FileSys.js +0 -113
  266. package/dist/shared/components/HashAccumulator.d.ts +0 -55
  267. package/dist/shared/components/HashAccumulator.js +0 -100
  268. package/dist/shared/components/ZipCrypto.d.ts +0 -3
  269. package/dist/shared/components/ZipCrypto.js +0 -78
  270. package/dist/shared/components/merkleTree.d.ts +0 -66
  271. package/dist/shared/components/merkleTree.js +0 -136
  272. package/dist/shared/types/index.d.ts +0 -21
  273. package/dist/shared/types/index.js +0 -3
  274. package/dist/shared/types.d.ts +0 -33
  275. package/dist/shared/types.js +0 -8
  276. package/dist/shared/version.d.ts +0 -2
  277. package/dist/shared/version.js +0 -6
  278. package/src/blockchain/CertificateToken.ts +0 -444
  279. package/src/blockchain/ZipKitMinter.ts +0 -470
  280. package/src/blockchain/ZipKitNFT.ts +0 -12
  281. package/src/blockchain/ZipKitVerifier.ts +0 -382
  282. package/src/blockchain/contracts.ts +0 -88
  283. package/src/blockchain/types.ts +0 -91
  284. package/src/browser/CertificateGeneratorBrowser.ts-remove +0 -498
  285. package/src/browser/ZipKitStampBrowser.ts +0 -9
  286. package/src/browser-index.ts +0 -36
  287. package/src/server/README.md +0 -25
  288. package/src/server/ZipKitServer.ts +0 -305
  289. package/src/shared/ZipKit.ts +0 -881
  290. package/src/shared/components/CertificateOTS.ts +0 -132
  291. package/src/shared/components/FileSys.ts +0 -87
  292. package/src/shared/components/HashAccumulator.ts +0 -113
  293. package/src/shared/components/ZipCrypto.ts +0 -91
  294. package/src/shared/components/merkleTree.ts +0 -162
  295. package/src/shared/types/index.ts +0 -25
  296. package/src/shared/types/modules.d.ts +0 -22
  297. package/src/shared/types/opentimestamps.d.ts +0 -23
  298. package/src/shared/types.d.ts +0 -8
  299. package/src/shared/types.ts +0 -43
  300. package/src/shared/version.ts +0 -3
  301. /package/src/{shared → core}/components/Support.ts +0 -0
@@ -0,0 +1,1559 @@
1
+ // ======================================
2
+ // ZipkitNode.ts - Node.js File-Based ZIP Operations
3
+ // Copyright (c) 2025 NeoWare, Inc. All rights reserved.
4
+ // ======================================
5
+
6
+ import Zipkit, { CompressOptions, StreamingFileHandle } from '../core';
7
+ import ZipEntry from '../core/ZipEntry';
8
+ import Errors from '../core/constants/Errors';
9
+ import { ZipCompressNode } from './ZipCompressNode';
10
+ import { ZipDecompressNode } from './ZipDecompressNode';
11
+ import {
12
+ CENTRAL_END,
13
+ CENTRAL_DIR,
14
+ ZIP64_CENTRAL_END,
15
+ ZIP64_CENTRAL_DIR,
16
+ LOCAL_HDR,
17
+ GP_FLAG,
18
+ ENCRYPT_HDR_SIZE
19
+ } from '../core/constants/Headers';
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import { minimatch } from 'minimatch';
23
+
24
+ // Re-export everything from core Zipkit
25
+ export * from '../core';
26
+ export { ZipEntry, Errors };
27
+
28
+ // ============================================================================
29
+ // ZIP File Writer Interface
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Interface for ZIP file writing operations
34
+ * Tracks file descriptor, stream, current position, and entry positions
35
+ */
36
+ export interface ZipFileWriter {
37
+ outputFd: number;
38
+ outputStream: fs.WriteStream;
39
+ currentPosition: number;
40
+ entryPositions: Map<string, number>; // filename -> position
41
+ }
42
+
43
+ // ======================================
44
+ // ZipkitNode
45
+ // ======================================
46
+
47
+ /**
48
+ * ZipkitNode - Node.js file-based ZIP operations
49
+ *
50
+ * Extends Zipkit to provide file I/O operations for Node.js environments.
51
+ * Similar to ZipkitBrowser which provides Blob operations for browser environments.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const zip = new ZipkitNode();
56
+ * await zip.loadZipFile('archive.zip');
57
+ * await zip.extractToFile(entry, './output/file.txt');
58
+ * ```
59
+ */
60
+ export default class ZipkitNode extends Zipkit {
61
+ // Override _zipkitCmp to use ZipCompressNode instead of ZipCompress (lazy-loaded)
62
+ private _zipkitCmpNode: ZipCompressNode | null = null;
63
+ // Override _zipkitDeCmp to use ZipDecompressNode instead of ZipDecompress (lazy-loaded)
64
+ private _zipkitDeCmpNode: ZipDecompressNode | null = null;
65
+
66
+ // File-based ZIP loading properties (merged from ZipLoadEntriesServer)
67
+ private fileHandle: StreamingFileHandle | null = null;
68
+ private filePath: string | null = null;
69
+ private fileSize: number = 0;
70
+ // Note: centralDirSize and centralDirOffset are inherited from Zipkit base class
71
+
72
+ constructor(config?: { bufferSize?: number; debug?: boolean }) {
73
+ super(config);
74
+
75
+ // Note: ZipCompressNode and ZipDecompressNode are lazy-loaded when first accessed
76
+ // They will override the base class _zipkitCmp and _zipkitDeCmp on first access
77
+ }
78
+
79
+ /**
80
+ * Lazy-load ZipCompressNode instance and override base class _zipkitCmp
81
+ * @returns ZipCompressNode instance (created on first access)
82
+ */
83
+ private getZipCompressNode(): ZipCompressNode {
84
+ if (!this._zipkitCmpNode) {
85
+ this._zipkitCmpNode = new ZipCompressNode(this);
86
+ // Override the base class _zipkitCmp with ZipCompressNode
87
+ const zipkit = this as any;
88
+ zipkit._zipkitCmp = this._zipkitCmpNode;
89
+ }
90
+ return this._zipkitCmpNode;
91
+ }
92
+
93
+ /**
94
+ * Lazy-load ZipDecompressNode instance and override base class _zipkitDeCmp
95
+ * @returns ZipDecompressNode instance (created on first access)
96
+ */
97
+ private getZipDecompressNode(): ZipDecompressNode {
98
+ if (!this._zipkitDeCmpNode) {
99
+ this._zipkitDeCmpNode = new ZipDecompressNode(this);
100
+ // Override the base class _zipkitDeCmp with ZipDecompressNode
101
+ const zipkit = this as any;
102
+ zipkit._zipkitDeCmp = this._zipkitDeCmpNode;
103
+ }
104
+ return this._zipkitDeCmpNode;
105
+ }
106
+
107
+ // ============================================================================
108
+ // File Loading Methods
109
+ // ============================================================================
110
+
111
+ /**
112
+ * Load ZIP file from file path (streaming mode)
113
+ *
114
+ * **Required**: You must call this method before calling `getDirectory()` or any other ZIP operations.
115
+ * This method:
116
+ * 1. Resets all ZIP data
117
+ * 2. Opens the file handle
118
+ * 3. Loads EOCD and parses central directory
119
+ * 4. Populates this.zipEntries[] array
120
+ *
121
+ * @param filePath - Path to the ZIP file to load
122
+ * @returns Promise<ZipEntry[]> Array of all entries in the ZIP file
123
+ * @throws Error if Node.js environment not available
124
+ */
125
+ async loadZipFile(filePath: string): Promise<ZipEntry[]> {
126
+ // Access private members via type assertion (ZipkitServer extends Zipkit)
127
+ const zipkit = this as any;
128
+ zipkit.resetZipData();
129
+
130
+ // Reset file-based data
131
+ this.resetFileData();
132
+ this.filePath = filePath;
133
+
134
+ // Open file handle
135
+ this.fileHandle = await this.openFileHandle(filePath);
136
+ const stats = await this.fileHandle.stat();
137
+ this.fileSize = stats.size;
138
+
139
+ // Load EOCD to get central directory info (sets zipComment internally)
140
+ await this.loadEOCD();
141
+
142
+ // Load central directory in chunks
143
+ const entries: ZipEntry[] = [];
144
+ let offset = zipkit.centralDirOffset;
145
+ let remaining = zipkit.centralDirSize;
146
+ const bufferSize = this.getBufferSize();
147
+
148
+ while (remaining > 0) {
149
+ const currentBufferSize = Math.min(bufferSize, remaining);
150
+ const chunk = Buffer.alloc(currentBufferSize);
151
+ await this.fileHandle.read(chunk, 0, currentBufferSize, offset);
152
+
153
+ // Parse entries from chunk
154
+ let chunkOffset = 0;
155
+ while (chunkOffset < chunk.length) {
156
+ if (chunk.readUInt32LE(chunkOffset) !== CENTRAL_DIR.SIGNATURE) {
157
+ break; // End of central directory
158
+ }
159
+
160
+ // Parse central directory entry
161
+ const entry = new ZipEntry(null, null, false);
162
+ const entryData = chunk.subarray(chunkOffset);
163
+ const remainingData = entry.readZipEntry(entryData);
164
+
165
+ entries.push(entry);
166
+
167
+ // Move to next entry
168
+ chunkOffset += (entryData.length - remainingData.length);
169
+ }
170
+
171
+ offset += currentBufferSize;
172
+ remaining -= currentBufferSize;
173
+ }
174
+
175
+ // Store entries in zipEntries[] array (single source of truth)
176
+ this.zipEntries = entries;
177
+ return entries;
178
+ }
179
+
180
+
181
+ /**
182
+ * Alias for loadZipFile() for consistency
183
+ * @param filePath - Path to the ZIP file to load
184
+ * @returns Promise<ZipEntry[]> Array of all entries in the ZIP file
185
+ */
186
+ async loadZipFromFile(filePath: string): Promise<ZipEntry[]> {
187
+ return this.loadZipFile(filePath);
188
+ }
189
+
190
+ // ============================================================================
191
+ // File Extraction Methods
192
+ // ============================================================================
193
+
194
+ /**
195
+ * Extract file directly to disk with true streaming (no memory buffering)
196
+ * Wrapper for ZipDecompress.extractToFile()
197
+ *
198
+ * Note: ZSTD codec is lazily initialized on first use (module-level singleton).
199
+ * Initialization happens automatically when needed.
200
+ *
201
+ * @param entry - ZIP entry to extract
202
+ * @param outputPath - Path where the file should be written
203
+ * @param options - Optional extraction options:
204
+ * - skipHashCheck: Skip hash verification (default: false)
205
+ * - onProgress: Callback function receiving bytes extracted as parameter
206
+ * @returns Promise that resolves when extraction is complete
207
+ * @throws Error if not a File-based ZIP
208
+ */
209
+ async extractToFile(
210
+ entry: ZipEntry,
211
+ outputPath: string,
212
+ options?: {
213
+ skipHashCheck?: boolean;
214
+ onProgress?: (bytes: number) => void;
215
+ }
216
+ ): Promise<void> {
217
+ return this.getZipDecompressNode().extractToFile(entry, outputPath, options);
218
+ }
219
+
220
+ /**
221
+ * Alias for extractToFile() for consistency
222
+ * @param entry - ZIP entry to extract
223
+ * @param outputPath - Path where the file should be written
224
+ * @param options - Optional extraction options
225
+ * @returns Promise that resolves when extraction is complete
226
+ */
227
+ async extractEntryToFile(
228
+ entry: ZipEntry,
229
+ outputPath: string,
230
+ options?: {
231
+ skipHashCheck?: boolean;
232
+ onProgress?: (bytes: number) => void;
233
+ }
234
+ ): Promise<void> {
235
+ return this.extractToFile(entry, outputPath, options);
236
+ }
237
+
238
+ // ============================================================================
239
+ // File-Based Compression Methods (ZipCompressNode wrappers)
240
+ // ============================================================================
241
+
242
+ /**
243
+ * Compress data for a ZIP entry (Buffer-based)
244
+ * Override to use ZipCompressNode instead of ZipCompress
245
+ *
246
+ * @param entry - ZIP entry to compress
247
+ * @param data - Buffer containing data to compress
248
+ * @param options - Compression options
249
+ * @param onOutputBuffer - Optional callback for streaming output
250
+ * @returns Promise resolving to Buffer containing compressed data
251
+ */
252
+ async compressData(
253
+ entry: ZipEntry,
254
+ data: Buffer,
255
+ options?: CompressOptions,
256
+ onOutputBuffer?: (data: Buffer) => Promise<void>
257
+ ): Promise<Buffer> {
258
+ return this.getZipCompressNode().compressData(entry, data, options, onOutputBuffer);
259
+ }
260
+
261
+ /**
262
+ * Compress a file from disk
263
+ * Wrapper for ZipCompressNode.compressFile()
264
+ *
265
+ * @param filePath - Path to the file to compress
266
+ * @param entry - ZIP entry to compress (filename should already be set)
267
+ * @param options - Optional compression options
268
+ * @returns Promise resolving to Buffer containing compressed data
269
+ */
270
+ async compressFile(
271
+ filePath: string,
272
+ entry: ZipEntry,
273
+ options?: CompressOptions
274
+ ): Promise<Buffer> {
275
+ return this.getZipCompressNode().compressFile(filePath, entry, options);
276
+ }
277
+
278
+ /**
279
+ * Compress a file from disk using streaming for large files
280
+ * Wrapper for ZipCompressNode.compressFileStream()
281
+ *
282
+ * @param filePath - Path to the file to compress
283
+ * @param entry - ZIP entry to compress (filename should already be set)
284
+ * @param options - Optional compression options
285
+ * @param onOutputBuffer - Optional callback for streaming output
286
+ * @returns Promise resolving to Buffer containing compressed data
287
+ */
288
+ async compressFileStream(
289
+ filePath: string,
290
+ entry: ZipEntry,
291
+ options?: CompressOptions,
292
+ onOutputBuffer?: (data: Buffer) => Promise<void>
293
+ ): Promise<Buffer> {
294
+ return this.getZipCompressNode().compressFileStream(filePath, entry, options, onOutputBuffer);
295
+ }
296
+
297
+ /**
298
+ * Extract all entries from ZIP to a directory
299
+ *
300
+ * @param outputDir - Directory where files should be extracted
301
+ * @param options - Optional extraction options:
302
+ * - skipHashCheck: Skip hash verification (default: false)
303
+ * - onProgress: Callback function receiving (entry, bytes) as parameters
304
+ * - preservePaths: Preserve directory structure (default: true)
305
+ * @returns Promise that resolves when all extractions are complete
306
+ * @throws Error if not a File-based ZIP
307
+ */
308
+ async extractAll(
309
+ outputDir: string,
310
+ options?: {
311
+ skipHashCheck?: boolean;
312
+ onProgress?: (entry: ZipEntry, bytes: number) => void;
313
+ preservePaths?: boolean;
314
+ }
315
+ ): Promise<void> {
316
+ const entries = this.zipEntries;
317
+ const preservePaths = options?.preservePaths !== false;
318
+
319
+ // Ensure output directory exists
320
+ if (!fs.existsSync(outputDir)) {
321
+ fs.mkdirSync(outputDir, { recursive: true });
322
+ }
323
+
324
+ for (const entry of entries) {
325
+ if (!entry.filename) continue;
326
+
327
+ // Determine output path
328
+ let outputPath: string;
329
+ if (preservePaths) {
330
+ // Preserve directory structure
331
+ outputPath = path.join(outputDir, entry.filename);
332
+ // Create parent directories if needed
333
+ const parentDir = path.dirname(outputPath);
334
+ if (!fs.existsSync(parentDir)) {
335
+ fs.mkdirSync(parentDir, { recursive: true });
336
+ }
337
+ } else {
338
+ // Extract to flat structure (filename only)
339
+ const filename = path.basename(entry.filename);
340
+ outputPath = path.join(outputDir, filename);
341
+ }
342
+
343
+ // Extract entry
344
+ await this.extractToFile(entry, outputPath, {
345
+ skipHashCheck: options?.skipHashCheck,
346
+ onProgress: options?.onProgress ? (bytes: number) => options.onProgress!(entry, bytes) : undefined
347
+ });
348
+ }
349
+ }
350
+
351
+ // ============================================================================
352
+ // ZIP File Creation Subfunctions
353
+ // ============================================================================
354
+
355
+ /**
356
+ * Initialize ZIP file for writing
357
+ * Creates output file with seek capability and returns writer object
358
+ *
359
+ * @param outputPath - Path where the ZIP file should be created
360
+ * @returns Promise resolving to ZipFileWriter object
361
+ */
362
+ async initializeZipFile(outputPath: string): Promise<ZipFileWriter> {
363
+ // Ensure parent directory exists
364
+ const parentDir = path.dirname(outputPath);
365
+ if (parentDir && parentDir !== '.' && !fs.existsSync(parentDir)) {
366
+ fs.mkdirSync(parentDir, { recursive: true });
367
+ }
368
+
369
+ // Open file for writing with seek capability
370
+ const outputFd = fs.openSync(outputPath, 'w+');
371
+ const outputStream = fs.createWriteStream(outputPath);
372
+
373
+ return {
374
+ outputFd,
375
+ outputStream,
376
+ currentPosition: 0,
377
+ entryPositions: new Map<string, number>()
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Prepare ZipEntry from file path
383
+ * Validates file exists and creates entry with metadata from file stats
384
+ *
385
+ * @param filePath - Path to the file
386
+ * @param entryName - Optional entry name (defaults to basename)
387
+ * @returns Promise resolving to ZipEntry ready for compression
388
+ */
389
+ async prepareEntryFromFile(filePath: string, entryName?: string): Promise<ZipEntry> {
390
+ if (!fs.existsSync(filePath)) {
391
+ throw new Error(`File not found: ${filePath}`);
392
+ }
393
+
394
+ const stats = fs.statSync(filePath);
395
+ if (!stats.isFile()) {
396
+ throw new Error(`Path is not a file: ${filePath}`);
397
+ }
398
+
399
+ // Use provided entry name or default to basename
400
+ const name = entryName || path.basename(filePath);
401
+ const entry = this.createZipEntry(name);
402
+
403
+ // Set entry metadata from file stats
404
+ entry.uncompressedSize = stats.size;
405
+ entry.timeDateDOS = entry.setDateTime(stats.mtime);
406
+ entry.lastModTimeDate = entry.timeDateDOS;
407
+
408
+ return entry;
409
+ }
410
+
411
+ /**
412
+ * Write a ZIP entry to the file
413
+ * Handles sequential write: header (placeholder) → compress → data → update header
414
+ *
415
+ * @param writer - ZipFileWriter object
416
+ * @param entry - ZipEntry to write
417
+ * @param filePath - Path to source file
418
+ * @param options - Optional compression options
419
+ * @param callbacks - Optional callbacks for progress and hash calculation
420
+ * @returns Promise that resolves when entry is written
421
+ */
422
+ async writeZipEntry(
423
+ writer: ZipFileWriter,
424
+ entry: ZipEntry,
425
+ filePath: string,
426
+ options?: CompressOptions,
427
+ callbacks?: {
428
+ onProgress?: (entry: ZipEntry, bytes: number) => void;
429
+ onHashCalculated?: (entry: ZipEntry, hash: Buffer) => void;
430
+ }
431
+ ): Promise<void> {
432
+ // Set compression method based on options
433
+ const level = options?.level ?? 6;
434
+ if (level === 0) {
435
+ entry.cmpMethod = 0; // STORED
436
+ } else if (options?.useZstd !== false) {
437
+ entry.cmpMethod = 93; // ZSTD
438
+ } else {
439
+ entry.cmpMethod = 8; // DEFLATED
440
+ }
441
+
442
+ // Step 1: Create local header with placeholder compressed size (0)
443
+ entry.compressedSize = 0; // Placeholder - will be updated after compression
444
+ entry.localHdrOffset = writer.currentPosition;
445
+ const localHeader = entry.createLocalHdr();
446
+
447
+ // Step 2: Write local header to file
448
+ await new Promise<void>((resolve, reject) => {
449
+ writer.outputStream.write(localHeader, (error) => {
450
+ if (error) {
451
+ reject(error);
452
+ } else {
453
+ writer.currentPosition += localHeader.length;
454
+ writer.entryPositions.set(entry.filename || '', entry.localHdrOffset);
455
+ resolve();
456
+ }
457
+ });
458
+ });
459
+
460
+ // Step 3: Compress file and write data
461
+ const bufferSize = options?.bufferSize || this.getBufferSize();
462
+ const useZstd = options?.useZstd !== false;
463
+ const shouldUseChunked = !useZstd && entry.uncompressedSize && entry.uncompressedSize > bufferSize;
464
+
465
+ if (shouldUseChunked) {
466
+ // Use streaming compression for large files
467
+ // Data is written directly via onOutputBuffer callback
468
+ const onOutputBuffer = async (data: Buffer) => {
469
+ await new Promise<void>((resolve, reject) => {
470
+ writer.outputStream.write(data, (error) => {
471
+ if (error) {
472
+ reject(error);
473
+ } else {
474
+ writer.currentPosition += data.length;
475
+ if (callbacks?.onProgress) {
476
+ callbacks.onProgress(entry, data.length);
477
+ }
478
+ resolve();
479
+ }
480
+ });
481
+ });
482
+ };
483
+
484
+ // compressFileStream will set entry.compressedSize and entry.crc
485
+ await this.compressFileStream(filePath, entry, options, onOutputBuffer);
486
+ } else {
487
+ // Use regular buffer compression for small files
488
+ // compressFile will set entry.compressedSize and entry.crc
489
+ const compressedData = await this.compressFile(filePath, entry, options);
490
+
491
+ // Write compressed data to file
492
+ await new Promise<void>((resolve, reject) => {
493
+ writer.outputStream.write(compressedData, (error) => {
494
+ if (error) {
495
+ reject(error);
496
+ } else {
497
+ writer.currentPosition += compressedData.length;
498
+ if (callbacks?.onProgress) {
499
+ callbacks.onProgress(entry, compressedData.length);
500
+ }
501
+ resolve();
502
+ }
503
+ });
504
+ });
505
+ }
506
+
507
+ // Step 4: Update compressed size and CRC in local header
508
+ // entry.compressedSize and entry.crc are set by compression methods
509
+ if (entry.compressedSize === undefined) {
510
+ throw new Error(`Compressed size not set for entry: ${entry.filename}`);
511
+ }
512
+
513
+ const compressedSizeOffset = entry.localHdrOffset + 18;
514
+ const sizeBuffer = Buffer.alloc(4);
515
+ sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
516
+ fs.writeSync(writer.outputFd, sizeBuffer, 0, 4, compressedSizeOffset);
517
+
518
+ if (entry.crc !== undefined) {
519
+ const crcOffset = entry.localHdrOffset + 14;
520
+ const crcBuffer = Buffer.alloc(4);
521
+ crcBuffer.writeUInt32LE(entry.crc, 0);
522
+ fs.writeSync(writer.outputFd, crcBuffer, 0, 4, crcOffset);
523
+ }
524
+
525
+ // Call hash callback if provided
526
+ if (callbacks?.onHashCalculated && entry.sha256) {
527
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
528
+ callbacks.onHashCalculated(entry, hashBuffer);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Write central directory entries to ZIP file
534
+ *
535
+ * @param writer - ZipFileWriter object
536
+ * @param entries - Array of ZipEntry objects
537
+ * @param options - Optional options for archive comment and progress
538
+ * @returns Promise resolving to central directory size in bytes
539
+ */
540
+ async writeCentralDirectory(
541
+ writer: ZipFileWriter,
542
+ entries: ZipEntry[],
543
+ options?: {
544
+ archiveComment?: string;
545
+ onProgress?: (entry: ZipEntry) => void;
546
+ }
547
+ ): Promise<number> {
548
+ const centralDirStart = writer.currentPosition;
549
+
550
+ // Update entry local header offsets from tracked positions
551
+ for (const entry of entries) {
552
+ const actualPosition = writer.entryPositions.get(entry.filename || '');
553
+ if (actualPosition !== undefined) {
554
+ entry.localHdrOffset = actualPosition;
555
+ }
556
+ }
557
+
558
+ // Write central directory entries
559
+ for (const entry of entries) {
560
+ const centralDirEntry = entry.centralDirEntry();
561
+
562
+ await new Promise<void>((resolve, reject) => {
563
+ writer.outputStream.write(centralDirEntry, (error) => {
564
+ if (error) {
565
+ reject(error);
566
+ } else {
567
+ writer.currentPosition += centralDirEntry.length;
568
+ if (options?.onProgress) {
569
+ options.onProgress(entry);
570
+ }
571
+ resolve();
572
+ }
573
+ });
574
+ });
575
+ }
576
+
577
+ return writer.currentPosition - centralDirStart;
578
+ }
579
+
580
+ /**
581
+ * Write End of Central Directory record
582
+ *
583
+ * @param writer - ZipFileWriter object
584
+ * @param totalEntries - Total number of entries in ZIP
585
+ * @param centralDirSize - Size of central directory in bytes
586
+ * @param centralDirOffset - Offset to start of central directory
587
+ * @param archiveComment - Optional archive comment (max 65535 bytes)
588
+ * @returns Promise that resolves when EOCD is written
589
+ */
590
+ async writeEndOfCentralDirectory(
591
+ writer: ZipFileWriter,
592
+ totalEntries: number,
593
+ centralDirSize: number,
594
+ centralDirOffset: number,
595
+ archiveComment?: string
596
+ ): Promise<void> {
597
+ const comment = archiveComment || '';
598
+ const commentBytes = Buffer.from(comment, 'utf8');
599
+ const commentLength = Math.min(commentBytes.length, 0xFFFF); // Max 65535 bytes
600
+
601
+ const buffer = Buffer.alloc(22 + commentLength);
602
+ let offset = 0;
603
+
604
+ // End of central directory signature (4 bytes)
605
+ buffer.writeUInt32LE(0x06054b50, offset);
606
+ offset += 4;
607
+
608
+ // Number of this disk (2 bytes)
609
+ buffer.writeUInt16LE(0, offset);
610
+ offset += 2;
611
+
612
+ // Number of the disk with the start of the central directory (2 bytes)
613
+ buffer.writeUInt16LE(0, offset);
614
+ offset += 2;
615
+
616
+ // Total number of entries in the central directory on this disk (2 bytes)
617
+ buffer.writeUInt16LE(totalEntries, offset);
618
+ offset += 2;
619
+
620
+ // Total number of entries in the central directory (2 bytes)
621
+ buffer.writeUInt16LE(totalEntries, offset);
622
+ offset += 2;
623
+
624
+ // Size of the central directory (4 bytes)
625
+ buffer.writeUInt32LE(centralDirSize, offset);
626
+ offset += 4;
627
+
628
+ // Offset of start of central directory with respect to the starting disk number (4 bytes)
629
+ buffer.writeUInt32LE(centralDirOffset, offset);
630
+ offset += 4;
631
+
632
+ // ZIP file comment length (2 bytes)
633
+ buffer.writeUInt16LE(commentLength, offset);
634
+ offset += 2;
635
+
636
+ // ZIP file comment (variable length)
637
+ if (commentLength > 0) {
638
+ commentBytes.copy(buffer, offset, 0, commentLength);
639
+ }
640
+
641
+ // Write EOCD to file
642
+ await new Promise<void>((resolve, reject) => {
643
+ writer.outputStream.write(buffer, (error) => {
644
+ if (error) {
645
+ reject(error);
646
+ } else {
647
+ writer.currentPosition += buffer.length;
648
+ resolve();
649
+ }
650
+ });
651
+ });
652
+ }
653
+
654
+ /**
655
+ * Finalize ZIP file by closing handles
656
+ *
657
+ * @param writer - ZipFileWriter object
658
+ * @returns Promise that resolves when file is closed
659
+ */
660
+ async finalizeZipFile(writer: ZipFileWriter): Promise<void> {
661
+ // Close file descriptor
662
+ fs.closeSync(writer.outputFd);
663
+
664
+ // Close write stream
665
+ return new Promise<void>((resolve, reject) => {
666
+ writer.outputStream.end((error: Error | null) => {
667
+ if (error) {
668
+ reject(error);
669
+ } else {
670
+ resolve();
671
+ }
672
+ });
673
+ });
674
+ }
675
+
676
+ // ============================================================================
677
+ // File Creation Methods
678
+ // ============================================================================
679
+
680
+ /**
681
+ * Create a ZIP file from multiple file paths
682
+ * Simple API that uses the modular subfunctions
683
+ *
684
+ * @param filePaths - Array of file paths to add to ZIP
685
+ * @param outputPath - Path where the ZIP file should be created
686
+ * @param options - Optional compression options
687
+ * @returns Promise that resolves when ZIP creation is complete
688
+ */
689
+ async createZipFromFiles(
690
+ filePaths: string[],
691
+ outputPath: string,
692
+ options?: CompressOptions
693
+ ): Promise<void> {
694
+ // Initialize ZIP file
695
+ const writer = await this.initializeZipFile(outputPath);
696
+
697
+ try {
698
+ // Process each file
699
+ for (const filePath of filePaths) {
700
+ // Validate and create entry
701
+ const entry = await this.prepareEntryFromFile(filePath);
702
+
703
+ // Write entry to ZIP
704
+ await this.writeZipEntry(writer, entry, filePath, options);
705
+ }
706
+
707
+ // Write central directory
708
+ const entries = this.getDirectory();
709
+ const centralDirOffset = writer.currentPosition;
710
+ const centralDirSize = await this.writeCentralDirectory(writer, entries);
711
+
712
+ // Write EOCD
713
+ await this.writeEndOfCentralDirectory(
714
+ writer,
715
+ entries.length,
716
+ centralDirSize,
717
+ centralDirOffset
718
+ );
719
+ } finally {
720
+ await this.finalizeZipFile(writer);
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Add a file to the current ZIP
726
+ *
727
+ * @param filePath - Path to the file to add
728
+ * @param entryName - Name to use in ZIP (defaults to filename)
729
+ * @param options - Optional compression options
730
+ * @returns Promise resolving to the created ZipEntry
731
+ */
732
+ async addFileToZip(
733
+ filePath: string,
734
+ entryName?: string,
735
+ options?: CompressOptions
736
+ ): Promise<ZipEntry> {
737
+ // Use provided entry name or derive from file path
738
+ const name = entryName || path.basename(filePath);
739
+ const entry = this.createZipEntry(name);
740
+
741
+ // Use ZipCompressNode.compressFile() which handles file I/O and compression
742
+ await this.getZipCompressNode().compressFile(filePath, entry, options);
743
+
744
+ // Add to entries
745
+ this.zipEntries.push(entry);
746
+
747
+ return entry;
748
+ }
749
+
750
+ // ============================================================================
751
+ // File Management Methods
752
+ // ============================================================================
753
+
754
+ /**
755
+ * Get underlying file handle for advanced operations
756
+ *
757
+ * @returns StreamingFileHandle if file is loaded
758
+ * @throws Error if file handle not available
759
+ */
760
+ getFileHandle(): StreamingFileHandle {
761
+ if (!this.fileHandle) {
762
+ throw new Error('File handle not available');
763
+ }
764
+ return this.fileHandle;
765
+ }
766
+
767
+ /**
768
+ * Close file handle explicitly
769
+ *
770
+ * @returns Promise that resolves when file is closed
771
+ */
772
+ async closeFile(): Promise<void> {
773
+ if (this.fileHandle) {
774
+ await this.fileHandle.close();
775
+ this.fileHandle = null;
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Copy entry from another ZIP (compatibility method)
781
+ * Reads the local header and compressed data from the file and returns it as a Buffer
782
+ * This is used when updating an existing ZIP file to copy unchanged entries
783
+ *
784
+ * @param entry - ZIP entry to copy
785
+ * @returns Promise resolving to Buffer containing local header + compressed data
786
+ * @throws Error if file handle not available
787
+ */
788
+ async copyEntry(entry: ZipEntry): Promise<Buffer> {
789
+ if (!this.fileHandle) {
790
+ throw new Error('File handle not available');
791
+ }
792
+
793
+ // Read local file header (30 bytes)
794
+ const localHeaderBuffer = Buffer.alloc(LOCAL_HDR.SIZE);
795
+ await this.fileHandle.read(localHeaderBuffer, 0, LOCAL_HDR.SIZE, entry.localHdrOffset);
796
+
797
+ // Verify signature
798
+ if (localHeaderBuffer.readUInt32LE(0) !== LOCAL_HDR.SIGNATURE) {
799
+ throw new Error(Errors.INVALID_CEN);
800
+ }
801
+
802
+ // Extract header information
803
+ const filenameLength = localHeaderBuffer.readUInt16LE(LOCAL_HDR.FNAME_LEN);
804
+ const extraFieldLength = localHeaderBuffer.readUInt16LE(LOCAL_HDR.EXTRA_LEN);
805
+ const bitFlags = localHeaderBuffer.readUInt16LE(LOCAL_HDR.FLAGS);
806
+
807
+ // Check for encryption header
808
+ let encryptionHeaderLength = 0;
809
+ if (bitFlags & GP_FLAG.ENCRYPTED) {
810
+ encryptionHeaderLength = ENCRYPT_HDR_SIZE;
811
+ }
812
+
813
+ // Calculate sizes
814
+ const localHeaderSize = LOCAL_HDR.SIZE + filenameLength + extraFieldLength;
815
+ const totalLocalEntrySize = localHeaderSize + encryptionHeaderLength + entry.compressedSize;
816
+
817
+ // Read the entire local entry (header + filename + extra field + encryption header + compressed data)
818
+ const entryBuffer = Buffer.alloc(totalLocalEntrySize);
819
+ await this.fileHandle.read(entryBuffer, 0, totalLocalEntrySize, entry.localHdrOffset);
820
+
821
+ return entryBuffer;
822
+ }
823
+
824
+ // ============================================================================
825
+ // File Update Methods
826
+ // ============================================================================
827
+
828
+ /**
829
+ * Update existing ZIP file
830
+ *
831
+ * This is a placeholder for future implementation.
832
+ * Full implementation would require:
833
+ * - Reading existing ZIP structure
834
+ * - Identifying entries to update/add/remove
835
+ * - Writing updated ZIP file
836
+ *
837
+ * @param zipPath - Path to the ZIP file to update
838
+ * @param updates - Update operations (add, update, remove entries)
839
+ * @returns Promise that resolves when update is complete
840
+ */
841
+ async updateZipFile(
842
+ zipPath: string,
843
+ updates: {
844
+ add?: Array<{ filePath: string; entryName?: string; options?: CompressOptions }>;
845
+ update?: Array<{ entryName: string; filePath: string; options?: CompressOptions }>;
846
+ remove?: string[]; // Entry names to remove
847
+ }
848
+ ): Promise<void> {
849
+ // Placeholder for future implementation
850
+ // This would require significant ZIP file manipulation logic
851
+ throw new Error('updateZipFile() - Full implementation pending. Use neozip CLI for now.');
852
+ }
853
+
854
+ // ============================================================================
855
+ // File-based ZIP Loading Methods (merged from ZipLoadEntriesServer)
856
+ // ============================================================================
857
+
858
+ /**
859
+ * Open file handle for streaming mode
860
+ */
861
+ private async openFileHandle(filePath: string): Promise<StreamingFileHandle> {
862
+ const handle = await fs.promises.open(filePath, 'r');
863
+
864
+ return {
865
+ async read(buffer: Buffer, offset: number, length: number, position: number): Promise<number> {
866
+ const result = await handle.read(buffer, offset, length, position);
867
+ return result.bytesRead;
868
+ },
869
+ async stat(): Promise<{ size: number }> {
870
+ const stats = await handle.stat();
871
+ return { size: stats.size };
872
+ },
873
+ async close(): Promise<void> {
874
+ await handle.close();
875
+ }
876
+ };
877
+ }
878
+
879
+ /**
880
+ * Load End of Central Directory (EOCD) in streaming mode
881
+ */
882
+ private async loadEOCD(): Promise<void> {
883
+ if (!this.fileHandle) {
884
+ throw new Error('File handle not available');
885
+ }
886
+
887
+ // Read potential EOCD area (last 65KB + 22 bytes)
888
+ const searchSize = Math.min(0xFFFF + 22, this.fileSize);
889
+ const searchStart = this.fileSize - searchSize;
890
+ const buffer = Buffer.alloc(searchSize);
891
+
892
+ try {
893
+ await this.fileHandle.read(buffer, 0, searchSize, searchStart);
894
+
895
+ // Find EOCD signature
896
+ let eocdOffset = -1;
897
+ for (let i = buffer.length - 22; i >= 0; i--) {
898
+ if (buffer[i] === 0x50) { // Quick 'P' check
899
+ if (buffer.readUInt32LE(i) === CENTRAL_END.SIGNATURE) {
900
+ eocdOffset = searchStart + i;
901
+ break;
902
+ }
903
+ }
904
+ }
905
+
906
+ if (eocdOffset === -1) {
907
+ throw new Error(Errors.INVALID_FORMAT);
908
+ }
909
+
910
+ // Parse EOCD
911
+ const eocdBuffer = Buffer.alloc(22);
912
+ await this.fileHandle.read(eocdBuffer, 0, 22, eocdOffset);
913
+
914
+ if (eocdBuffer.readUInt32LE(0) === CENTRAL_END.SIGNATURE) {
915
+ // Standard ZIP format
916
+ const zipkit = this as any;
917
+ zipkit.centralDirSize = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_SIZE);
918
+ zipkit.centralDirOffset = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_OFFSET);
919
+
920
+ // Handle ZIP64
921
+ if (zipkit.centralDirOffset === 0xFFFFFFFF) {
922
+ await this.loadZIP64EOCD(eocdOffset);
923
+ }
924
+ } else {
925
+ throw new Error(Errors.INVALID_FORMAT);
926
+ }
927
+
928
+ // Load ZIP comment
929
+ const commentLength = eocdBuffer.readUInt16LE(CENTRAL_END.ZIP_COMMENT_LEN);
930
+ if (commentLength > 0) {
931
+ const commentBuffer = Buffer.alloc(commentLength);
932
+ await this.fileHandle.read(commentBuffer, 0, commentLength, eocdOffset + 22);
933
+ const zipkitAny = this as any;
934
+ zipkitAny.zipComment = commentBuffer.toString();
935
+ }
936
+ } finally {
937
+ // Clean up search buffer to help GC (can be up to 65KB)
938
+ // Note: Buffer will be GC'd when it goes out of scope, but explicit cleanup helps
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Load ZIP64 End of Central Directory
944
+ */
945
+ private async loadZIP64EOCD(eocdOffset: number): Promise<void> {
946
+ if (!this.fileHandle) {
947
+ throw new Error('File handle not available');
948
+ }
949
+
950
+ // Look for ZIP64 locator
951
+ const locatorOffset = eocdOffset - 20;
952
+ const locatorBuffer = Buffer.alloc(20);
953
+ await this.fileHandle.read(locatorBuffer, 0, 20, locatorOffset);
954
+
955
+ if (locatorBuffer.readUInt32LE(0) === ZIP64_CENTRAL_END.SIGNATURE) {
956
+ // Read ZIP64 EOCD
957
+ const zip64Offset = locatorBuffer.readBigUInt64LE(8);
958
+ const zip64Buffer = Buffer.alloc(56);
959
+ await this.fileHandle.read(zip64Buffer, 0, 56, Number(zip64Offset));
960
+
961
+ const zipkit = this as any;
962
+ zipkit.centralDirSize = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE));
963
+ zipkit.centralDirOffset = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET));
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Reset file-based ZIP data to initial state
969
+ */
970
+ private resetFileData(): void {
971
+ this.fileHandle = null;
972
+ this.filePath = null;
973
+ this.fileSize = 0;
974
+ // Note: centralDirSize and centralDirOffset are reset in base class resetZipData()
975
+ const zipkit = this as any;
976
+ zipkit.centralDirSize = 0;
977
+ zipkit.centralDirOffset = 0;
978
+ }
979
+
980
+ // ============================================================================
981
+ // ZIP File Extraction Subfunctions
982
+ // ============================================================================
983
+
984
+ /**
985
+ * Filter ZIP entries based on include/exclude patterns
986
+ *
987
+ * @param entries - Array of ZipEntry objects to filter
988
+ * @param options - Optional filtering options
989
+ * @returns Filtered array of ZipEntry objects
990
+ */
991
+ filterEntries(
992
+ entries: ZipEntry[],
993
+ options?: {
994
+ include?: string[];
995
+ exclude?: string[];
996
+ skipMetadata?: boolean; // Skip META-INF/* files (default: true)
997
+ }
998
+ ): ZipEntry[] {
999
+ const skipMetadata = options?.skipMetadata !== false;
1000
+
1001
+ return entries.filter(entry => {
1002
+ const filename = entry.filename || '';
1003
+
1004
+ // Skip metadata files if requested
1005
+ if (skipMetadata && (filename.startsWith('META-INF/') || filename === 'META-INF')) {
1006
+ return false;
1007
+ }
1008
+
1009
+ // Skip directories
1010
+ if (entry.isDirectory) {
1011
+ return false;
1012
+ }
1013
+
1014
+ // If no filtering patterns, include all
1015
+ if (!options?.include && !options?.exclude) {
1016
+ return true;
1017
+ }
1018
+
1019
+ const fileName = path.basename(filename);
1020
+ const relativePath = path.relative(process.cwd(), filename);
1021
+
1022
+ // Check include patterns first (if any)
1023
+ if (options.include && options.include.length > 0) {
1024
+ const matchesInclude = options.include.some(pattern =>
1025
+ minimatch(fileName, pattern) || minimatch(relativePath, pattern) || minimatch(filename, pattern)
1026
+ );
1027
+ if (!matchesInclude) {
1028
+ return false;
1029
+ }
1030
+ }
1031
+
1032
+ // Check exclude patterns
1033
+ if (options.exclude && options.exclude.length > 0) {
1034
+ const matchesExclude = options.exclude.some(pattern =>
1035
+ minimatch(fileName, pattern) || minimatch(relativePath, pattern) || minimatch(filename, pattern)
1036
+ );
1037
+ if (matchesExclude) {
1038
+ return false;
1039
+ }
1040
+ }
1041
+
1042
+ return true;
1043
+ });
1044
+ }
1045
+
1046
+ /**
1047
+ * Prepare extraction path for a ZIP entry
1048
+ *
1049
+ * @param entry - ZipEntry to extract
1050
+ * @param destination - Destination directory
1051
+ * @param options - Optional path options
1052
+ * @returns Absolute output path for the entry
1053
+ */
1054
+ prepareExtractionPath(
1055
+ entry: ZipEntry,
1056
+ destination: string,
1057
+ options?: {
1058
+ junkPaths?: boolean; // Extract to flat structure (default: false)
1059
+ }
1060
+ ): string {
1061
+ const filename = entry.filename || '';
1062
+
1063
+ // Determine output path
1064
+ let outputPath: string;
1065
+ if (options?.junkPaths) {
1066
+ // Extract to flat structure (filename only)
1067
+ outputPath = path.join(destination, path.basename(filename));
1068
+ } else {
1069
+ // Preserve directory structure
1070
+ outputPath = path.join(destination, filename);
1071
+ }
1072
+
1073
+ // Ensure parent directory exists
1074
+ const parentDir = path.dirname(outputPath);
1075
+ if (parentDir && parentDir !== '.' && !fs.existsSync(parentDir)) {
1076
+ fs.mkdirSync(parentDir, { recursive: true });
1077
+ }
1078
+
1079
+ return path.resolve(outputPath);
1080
+ }
1081
+
1082
+ /**
1083
+ * Extract timestamps from ZIP entry
1084
+ *
1085
+ * @param entry - ZipEntry to extract timestamps from
1086
+ * @returns Object with mtime, atime, ctime (Date objects or null)
1087
+ */
1088
+ extractEntryTimestamps(entry: ZipEntry): { mtime: Date | null; atime: Date | null; ctime: Date | null } {
1089
+ let mtime: Date | null = null;
1090
+ let atime: Date | null = null;
1091
+ let ctime: Date | null = null;
1092
+
1093
+ // Try extended timestamps first (most accurate)
1094
+ if ((entry as any).ntfsTime) {
1095
+ const ntfs = (entry as any).ntfsTime;
1096
+ if (ntfs.mtime) mtime = new Date(ntfs.mtime);
1097
+ if (ntfs.atime) atime = new Date(ntfs.atime);
1098
+ if (ntfs.ctime) ctime = new Date(ntfs.ctime);
1099
+ } else if ((entry as any).extendedTime) {
1100
+ const ext = (entry as any).extendedTime;
1101
+ if (ext.mtime) mtime = new Date(ext.mtime);
1102
+ if (ext.atime) atime = new Date(ext.atime);
1103
+ if (ext.ctime) ctime = new Date(ext.ctime);
1104
+ }
1105
+
1106
+ // Fall back to standard timestamps if extended not available
1107
+ if (!mtime) {
1108
+ if ((entry as any).parseDateTime && entry.lastModTimeDate) {
1109
+ // Use the parseDateTime method if available
1110
+ const parsedDate = (entry as any).parseDateTime(entry.lastModTimeDate);
1111
+ mtime = parsedDate ? new Date(parsedDate) : null;
1112
+ } else if (entry.lastModTimeDate) {
1113
+ mtime = new Date(entry.lastModTimeDate);
1114
+ } else if (entry.timeDateDOS) {
1115
+ // timeDateDOS is in seconds since 1970, convert to milliseconds
1116
+ mtime = new Date(entry.timeDateDOS * 1000);
1117
+ }
1118
+ }
1119
+
1120
+ return { mtime, atime, ctime };
1121
+ }
1122
+
1123
+ /**
1124
+ * Determine if an entry should be extracted based on overwrite logic
1125
+ *
1126
+ * @param entry - ZipEntry to check
1127
+ * @param outputPath - Path where file would be extracted
1128
+ * @param options - Optional overwrite options
1129
+ * @returns Promise resolving to decision object
1130
+ */
1131
+ async shouldExtractEntry(
1132
+ entry: ZipEntry,
1133
+ outputPath: string,
1134
+ options?: {
1135
+ overwrite?: boolean; // Always overwrite (default: false)
1136
+ never?: boolean; // Never overwrite (default: false)
1137
+ freshenOnly?: boolean; // Only extract if newer (default: false)
1138
+ updateOnly?: boolean; // Only extract if newer or doesn't exist (default: false)
1139
+ onOverwritePrompt?: (filename: string) => Promise<'y' | 'n' | 'a' | 'q'>; // Interactive prompt callback
1140
+ }
1141
+ ): Promise<{ shouldExtract: boolean; reason?: string }> {
1142
+ const fileExists = fs.existsSync(outputPath);
1143
+
1144
+ // If file doesn't exist, always extract (unless freshenOnly mode)
1145
+ if (!fileExists) {
1146
+ if (options?.freshenOnly) {
1147
+ return { shouldExtract: false, reason: 'not in destination' };
1148
+ }
1149
+ // For updateOnly or normal mode, extract new files
1150
+ return { shouldExtract: true };
1151
+ }
1152
+
1153
+ // File exists - apply overwrite logic
1154
+ if (options?.never) {
1155
+ return { shouldExtract: false, reason: 'never overwrite' };
1156
+ }
1157
+
1158
+ if (options?.overwrite) {
1159
+ return { shouldExtract: true };
1160
+ }
1161
+
1162
+ if (options?.freshenOnly || options?.updateOnly) {
1163
+ // Compare timestamps to determine if archive file is newer
1164
+ const existingStats = fs.statSync(outputPath);
1165
+ const timestamps = this.extractEntryTimestamps(entry);
1166
+ const archiveDate = timestamps.mtime || new Date(0);
1167
+
1168
+ if (archiveDate <= existingStats.mtime) {
1169
+ return { shouldExtract: false, reason: 'not newer' };
1170
+ }
1171
+
1172
+ // File in archive is newer, proceed with extraction
1173
+ return { shouldExtract: true };
1174
+ }
1175
+
1176
+ // Interactive mode - use callback if provided
1177
+ if (options?.onOverwritePrompt) {
1178
+ const response = await options.onOverwritePrompt(entry.filename || '');
1179
+ if (response === 'n') {
1180
+ return { shouldExtract: false, reason: 'user declined' };
1181
+ } else if (response === 'q') {
1182
+ return { shouldExtract: false, reason: 'user aborted' };
1183
+ } else if (response === 'y' || response === 'a') {
1184
+ return { shouldExtract: true };
1185
+ }
1186
+ }
1187
+
1188
+ // Default: skip if exists and no overwrite option
1189
+ return { shouldExtract: false, reason: 'file exists' };
1190
+ }
1191
+
1192
+ /**
1193
+ * Restore entry metadata (timestamps and permissions) to extracted file
1194
+ *
1195
+ * @param filePath - Path to the extracted file
1196
+ * @param entry - ZipEntry that was extracted
1197
+ * @param options - Optional metadata options
1198
+ */
1199
+ restoreEntryMetadata(
1200
+ filePath: string,
1201
+ entry: ZipEntry,
1202
+ options?: {
1203
+ preserveTimestamps?: boolean; // Restore file timestamps (default: true)
1204
+ preservePermissions?: boolean; // Restore file permissions (default: false)
1205
+ }
1206
+ ): void {
1207
+ const preserveTimestamps = options?.preserveTimestamps !== false;
1208
+ const preservePermissions = options?.preservePermissions === true;
1209
+
1210
+ // Restore timestamps
1211
+ if (preserveTimestamps) {
1212
+ try {
1213
+ const timestamps = this.extractEntryTimestamps(entry);
1214
+
1215
+ if (timestamps.mtime && timestamps.atime) {
1216
+ fs.utimesSync(filePath, timestamps.atime, timestamps.mtime);
1217
+ } else if (timestamps.mtime) {
1218
+ // If we only have modification time, use it for both
1219
+ fs.utimesSync(filePath, timestamps.mtime, timestamps.mtime);
1220
+ }
1221
+ } catch (error) {
1222
+ // Don't fail extraction if timestamp restoration fails
1223
+ // Some filesystems don't support timestamp modification
1224
+ }
1225
+ }
1226
+
1227
+ // Restore permissions (Unix only)
1228
+ if (preservePermissions && process.platform !== 'win32') {
1229
+ try {
1230
+ // Restore UID/GID if available
1231
+ if ((entry as any).uid !== null && (entry as any).uid !== undefined &&
1232
+ (entry as any).gid !== null && (entry as any).gid !== undefined) {
1233
+ // Only root can change ownership to different users
1234
+ if (process.getuid && process.getuid() === 0) {
1235
+ // Running as root - can change both UID and GID
1236
+ fs.chownSync(filePath, (entry as any).uid, (entry as any).gid);
1237
+ } else {
1238
+ // Not running as root - try to change group only if we're a member
1239
+ try {
1240
+ fs.chownSync(filePath, -1, (entry as any).gid); // -1 means don't change UID
1241
+ } catch (error) {
1242
+ // Ignore errors - insufficient privileges
1243
+ }
1244
+ }
1245
+ }
1246
+
1247
+ // Restore file mode if available
1248
+ if (entry.extFileAttr) {
1249
+ // Extract permission bits from external file attributes
1250
+ const permissions = (entry.extFileAttr >>> 16) & 0o777;
1251
+ if (permissions > 0) {
1252
+ fs.chmodSync(filePath, permissions);
1253
+ }
1254
+ }
1255
+ } catch (error) {
1256
+ // Don't fail extraction if permission restoration fails
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ /**
1262
+ * Extract a single entry to a file path
1263
+ * Handles symlinks, hardlinks, timestamps, and permissions
1264
+ *
1265
+ * @param entry - ZipEntry to extract
1266
+ * @param outputPath - Path where file should be extracted
1267
+ * @param options - Optional extraction options
1268
+ * @returns Promise resolving to extraction result
1269
+ */
1270
+ async extractEntryToPath(
1271
+ entry: ZipEntry,
1272
+ outputPath: string,
1273
+ options?: {
1274
+ skipHashCheck?: boolean; // Skip hash verification (default: false)
1275
+ preserveTimestamps?: boolean; // Restore file timestamps (default: true)
1276
+ preservePermissions?: boolean; // Restore file permissions (default: false)
1277
+ symlinks?: boolean; // Handle symbolic links (default: false)
1278
+ hardLinks?: boolean; // Handle hard links (default: false)
1279
+ onProgress?: (entry: ZipEntry, bytes: number) => void; // Progress callback
1280
+ }
1281
+ ): Promise<{ success: boolean; bytesExtracted: number; error?: string }> {
1282
+ const filename = entry.filename || '';
1283
+
1284
+ try {
1285
+ // Check if entry is a symbolic link
1286
+ const isSymlink = (entry as any).isSymlink && (entry as any).linkTarget;
1287
+ const S_IFLNK = 0o120000;
1288
+ const fileType = entry.extFileAttr ? ((entry.extFileAttr >>> 16) & 0o170000) : 0;
1289
+ const isSymlinkByAttr = fileType === S_IFLNK;
1290
+
1291
+ if ((isSymlink || isSymlinkByAttr) && options?.symlinks) {
1292
+ // Handle symbolic link
1293
+ let linkTarget = (entry as any).linkTarget;
1294
+
1295
+ if (!linkTarget) {
1296
+ // Extract target from file content
1297
+ const bufferBased = !this.fileHandle;
1298
+ if (bufferBased) {
1299
+ const data = await this.extract(entry, options?.skipHashCheck);
1300
+ if (data) {
1301
+ linkTarget = data.toString('utf8');
1302
+ }
1303
+ } else {
1304
+ // For file-based, extract to temp file and read
1305
+ const tempPath = path.join(require('os').tmpdir(), `neozip-symlink-${Date.now()}-${process.pid}`);
1306
+ try {
1307
+ await this.extractToFile(entry, tempPath, {
1308
+ skipHashCheck: options?.skipHashCheck
1309
+ });
1310
+ const data = fs.readFileSync(tempPath, 'utf8');
1311
+ linkTarget = data;
1312
+ // Clean up temp file
1313
+ fs.unlinkSync(tempPath);
1314
+ } catch (error) {
1315
+ // Clean up temp file if it exists
1316
+ if (fs.existsSync(tempPath)) {
1317
+ try {
1318
+ fs.unlinkSync(tempPath);
1319
+ } catch (cleanupError) {
1320
+ // Ignore cleanup errors
1321
+ }
1322
+ }
1323
+ return { success: false, bytesExtracted: 0, error: `Could not extract symbolic link target: ${error instanceof Error ? error.message : String(error)}` };
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ if (linkTarget && process.platform !== 'win32') {
1329
+ try {
1330
+ fs.symlinkSync(linkTarget, outputPath);
1331
+ return { success: true, bytesExtracted: Buffer.byteLength(linkTarget, 'utf8') };
1332
+ } catch (error) {
1333
+ return { success: false, bytesExtracted: 0, error: `Failed to create symbolic link: ${error instanceof Error ? error.message : String(error)}` };
1334
+ }
1335
+ } else {
1336
+ return { success: false, bytesExtracted: 0, error: 'Symbolic links not supported on this platform' };
1337
+ }
1338
+ }
1339
+
1340
+ // Check if entry is a hard link
1341
+ const isHardLink = (entry as any).isHardLink && (entry as any).originalEntry;
1342
+
1343
+ if (isHardLink && options?.hardLinks) {
1344
+ // Handle hard link
1345
+ const originalEntry = (entry as any).originalEntry;
1346
+ const outDir = path.dirname(outputPath);
1347
+ const originalPath = path.resolve(outDir, originalEntry);
1348
+
1349
+ if (fs.existsSync(originalPath) && process.platform !== 'win32') {
1350
+ try {
1351
+ fs.linkSync(originalPath, outputPath);
1352
+ return { success: true, bytesExtracted: 0 }; // No actual bytes extracted for hard links
1353
+ } catch (error) {
1354
+ return { success: false, bytesExtracted: 0, error: `Failed to create hard link: ${error instanceof Error ? error.message : String(error)}` };
1355
+ }
1356
+ } else {
1357
+ return { success: false, bytesExtracted: 0, error: 'Hard links not supported or original file not found' };
1358
+ }
1359
+ }
1360
+
1361
+ // Regular file extraction
1362
+ // Check if we're in buffer-based or file-based mode
1363
+ const bufferBased = !this.fileHandle;
1364
+ const fileBased = !!this.fileHandle;
1365
+
1366
+ // Use temp file for overwrite safety
1367
+ const fileExists = fs.existsSync(outputPath);
1368
+ const needsTempFile = fileExists;
1369
+ const tempPath = needsTempFile
1370
+ ? path.join(require('os').tmpdir(), `neozip-extract-${Date.now()}-${process.pid}-${path.basename(outputPath).replace(/[^a-zA-Z0-9]/g, '_')}`)
1371
+ : outputPath;
1372
+
1373
+ let bytesExtracted = 0;
1374
+ let extractionSucceeded = false;
1375
+
1376
+ try {
1377
+ if (bufferBased) {
1378
+ // Buffer-based (in-memory) mode: extract to buffer, then write to file
1379
+ const data = await this.extract(entry, options?.skipHashCheck);
1380
+
1381
+ if (!data) {
1382
+ return { success: false, bytesExtracted: 0, error: 'Extraction returned no data' };
1383
+ }
1384
+
1385
+ // Write buffer to temp file
1386
+ fs.writeFileSync(tempPath, data);
1387
+ bytesExtracted = data.length;
1388
+ extractionSucceeded = true;
1389
+
1390
+ if (options?.onProgress) {
1391
+ options.onProgress(entry, bytesExtracted);
1392
+ }
1393
+ } else if (fileBased) {
1394
+ // File-based mode: use direct streaming extraction to temp file
1395
+ await this.extractToFile(entry, tempPath, {
1396
+ skipHashCheck: options?.skipHashCheck,
1397
+ onProgress: (bytes: number) => {
1398
+ bytesExtracted = bytes;
1399
+ if (options?.onProgress) {
1400
+ options.onProgress(entry, bytes);
1401
+ }
1402
+ }
1403
+ });
1404
+
1405
+ // If we get here, extraction succeeded
1406
+ extractionSucceeded = true;
1407
+ } else {
1408
+ return { success: false, bytesExtracted: 0, error: 'ZIP file not loaded or unknown backend type' };
1409
+ }
1410
+
1411
+ // If extraction succeeded and we used a temp file, replace the original
1412
+ if (extractionSucceeded && needsTempFile) {
1413
+ // Delete the original file
1414
+ fs.unlinkSync(outputPath);
1415
+ // Move temp file to final location
1416
+ fs.renameSync(tempPath, outputPath);
1417
+ }
1418
+
1419
+ // Restore metadata (timestamps and permissions)
1420
+ this.restoreEntryMetadata(outputPath, entry, {
1421
+ preserveTimestamps: options?.preserveTimestamps,
1422
+ preservePermissions: options?.preservePermissions
1423
+ });
1424
+
1425
+ return { success: true, bytesExtracted };
1426
+ } catch (error) {
1427
+ // Clean up temp file if it exists
1428
+ if (needsTempFile && fs.existsSync(tempPath)) {
1429
+ try {
1430
+ fs.unlinkSync(tempPath);
1431
+ } catch (cleanupError) {
1432
+ // Ignore cleanup errors
1433
+ }
1434
+ }
1435
+
1436
+ return {
1437
+ success: false,
1438
+ bytesExtracted: 0,
1439
+ error: error instanceof Error ? error.message : String(error)
1440
+ };
1441
+ }
1442
+ } catch (error) {
1443
+ return {
1444
+ success: false,
1445
+ bytesExtracted: 0,
1446
+ error: error instanceof Error ? error.message : String(error)
1447
+ };
1448
+ }
1449
+ }
1450
+
1451
+ /**
1452
+ * Extract all files from a ZIP archive to a destination directory
1453
+ * Simple API that uses the modular subfunctions
1454
+ *
1455
+ * @param archivePath - Path to the ZIP file
1456
+ * @param destination - Directory where files should be extracted
1457
+ * @param options - Optional extraction options
1458
+ * @returns Promise resolving to extraction statistics
1459
+ */
1460
+ async extractZipFile(
1461
+ archivePath: string,
1462
+ destination: string,
1463
+ options?: {
1464
+ password?: string; // Password for encrypted archives
1465
+ overwrite?: boolean; // Always overwrite existing files
1466
+ junkPaths?: boolean; // Extract to flat structure
1467
+ include?: string[]; // Include patterns
1468
+ exclude?: string[]; // Exclude patterns
1469
+ preserveTimestamps?: boolean; // Restore file timestamps
1470
+ preservePermissions?: boolean; // Restore file permissions
1471
+ symlinks?: boolean; // Handle symbolic links
1472
+ hardLinks?: boolean; // Handle hard links
1473
+ skipHashCheck?: boolean; // Skip hash verification
1474
+ onProgress?: (entry: ZipEntry, bytes: number) => void; // Progress callback
1475
+ onOverwritePrompt?: (filename: string) => Promise<'y' | 'n' | 'a' | 'q'>; // Overwrite prompt callback
1476
+ }
1477
+ ): Promise<{ filesExtracted: number; bytesExtracted: number }> {
1478
+ // Ensure destination directory exists
1479
+ if (!fs.existsSync(destination)) {
1480
+ fs.mkdirSync(destination, { recursive: true });
1481
+ }
1482
+
1483
+ // Load ZIP file if not already loaded or if path changed
1484
+ if (!this.fileHandle || this.filePath !== archivePath) {
1485
+ await this.loadZipFile(archivePath);
1486
+ }
1487
+
1488
+ // Set password if provided (needed for decryption)
1489
+ if (options?.password) {
1490
+ (this as any).password = options.password;
1491
+ }
1492
+
1493
+ // Get all entries
1494
+ const entries = this.getDirectory();
1495
+
1496
+ // Filter entries
1497
+ const filteredEntries = this.filterEntries(entries, {
1498
+ include: options?.include,
1499
+ exclude: options?.exclude,
1500
+ skipMetadata: true
1501
+ });
1502
+
1503
+ // Extract each entry
1504
+ let filesExtracted = 0;
1505
+ let bytesExtracted = 0;
1506
+ let alwaysOverwrite = false; // Track "always" response from user
1507
+
1508
+ for (const entry of filteredEntries) {
1509
+ // Prepare output path
1510
+ const outputPath = this.prepareExtractionPath(entry, destination, {
1511
+ junkPaths: options?.junkPaths
1512
+ });
1513
+
1514
+ // Check if should extract
1515
+ const decision = await this.shouldExtractEntry(entry, outputPath, {
1516
+ overwrite: options?.overwrite || alwaysOverwrite,
1517
+ never: false,
1518
+ freshenOnly: false,
1519
+ updateOnly: false,
1520
+ onOverwritePrompt: async (filename: string) => {
1521
+ if (options?.onOverwritePrompt) {
1522
+ const response = await options.onOverwritePrompt(filename);
1523
+ if (response === 'a') {
1524
+ alwaysOverwrite = true;
1525
+ }
1526
+ return response;
1527
+ }
1528
+ return 'n'; // Default to no if no callback provided
1529
+ }
1530
+ });
1531
+
1532
+ if (!decision.shouldExtract) {
1533
+ // Check if user aborted
1534
+ if (decision.reason === 'user aborted') {
1535
+ break; // Stop extraction
1536
+ }
1537
+ continue;
1538
+ }
1539
+
1540
+ // Extract entry
1541
+ const result = await this.extractEntryToPath(entry, outputPath, {
1542
+ skipHashCheck: options?.skipHashCheck,
1543
+ preserveTimestamps: options?.preserveTimestamps !== false,
1544
+ preservePermissions: options?.preservePermissions,
1545
+ symlinks: options?.symlinks,
1546
+ hardLinks: options?.hardLinks,
1547
+ onProgress: options?.onProgress
1548
+ });
1549
+
1550
+ if (result.success) {
1551
+ filesExtracted++;
1552
+ bytesExtracted += result.bytesExtracted;
1553
+ }
1554
+ }
1555
+
1556
+ return { filesExtracted, bytesExtracted };
1557
+ }
1558
+ }
1559
+