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