@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,794 @@
1
+ // ======================================
2
+ // Zipkit.ts - Enhanced Zipkit with Dual Mode Support
3
+ // Copyright (c) 2025 NeoWare, Inc. All rights reserved.
4
+ // ======================================
5
+
6
+ import ZipEntry from './ZipEntry';
7
+ import Errors from './constants/Errors';
8
+ import { Logger } from './components/Logger';
9
+ import { NEOZIPKIT_INFO } from '../types';
10
+ import { ZipCompress, CompressOptions } from './ZipCompress';
11
+ import ZipDecompress from './ZipDecompress';
12
+ import { sha256, crc32 } from './encryption/ZipCrypto';
13
+ import HashCalculator from './components/HashCalculator';
14
+ import {
15
+ LOCAL_HDR,
16
+ ENCRYPT_HDR_SIZE,
17
+ CMP_METHOD,
18
+ CENTRAL_END,
19
+ CENTRAL_DIR,
20
+ ZIP64_CENTRAL_END,
21
+ ZIP64_CENTRAL_DIR ,
22
+ GP_FLAG,
23
+ TIMESTAMP_SUBMITTED,
24
+ TIMESTAMP_METADATA,
25
+ TOKENIZED_METADATA,
26
+ LOCAL_FILE_HEADER,
27
+ CENTRAL_FILE_HEADER,
28
+ CENTRAL_DIRECTORY_END,
29
+ HDR_ID
30
+ } from './constants/Headers';
31
+
32
+ /**
33
+ * Streaming file handle interface
34
+ * Used for file-based ZIP operations in server environments
35
+ */
36
+ export interface StreamingFileHandle {
37
+ read(buffer: Buffer, offset: number, length: number, position: number): Promise<number>;
38
+ stat(): Promise<{ size: number }>;
39
+ close(): Promise<void>;
40
+ }
41
+
42
+ // Re-export interfaces and constants (keep exports grouped at top)
43
+ export {
44
+ ZipEntry,
45
+ Errors,
46
+ CMP_METHOD,
47
+ TIMESTAMP_METADATA,
48
+ TIMESTAMP_SUBMITTED,
49
+ TOKENIZED_METADATA,
50
+ LOCAL_HDR,
51
+ ENCRYPT_HDR_SIZE,
52
+ CENTRAL_END,
53
+ CENTRAL_DIR,
54
+ ZIP64_CENTRAL_END,
55
+ ZIP64_CENTRAL_DIR,
56
+ GP_FLAG,
57
+ LOCAL_FILE_HEADER,
58
+ CENTRAL_FILE_HEADER,
59
+ CENTRAL_DIRECTORY_END,
60
+ HDR_ID
61
+ };
62
+ export type { CompressOptions, CreateZipOptions } from './ZipCompress';
63
+
64
+ /**
65
+ * Configuration options for Zipkit instances
66
+ *
67
+ * Controls various aspects of ZIP file processing including memory usage,
68
+ * chunk sizes for streaming operations, and debug logging.
69
+ *
70
+ * @interface ZipkitConfig
71
+ * @property {number} [bufferSize] - Buffer size in bytes for streaming operations.
72
+ * Files larger than this size will be processed
73
+ * in chunks for memory efficiency. Default: 512KB (524288 bytes).
74
+ * @property {boolean} [debug] - Enable debug logging for detailed diagnostic information.
75
+ * When enabled, sets Logger level to 'debug'. Default: false.
76
+ *
77
+ * @example
78
+ * const zip = new Zipkit({
79
+ * bufferSize: 512 * 1024, // 512KB buffer
80
+ * debug: true
81
+ * });
82
+ */
83
+ export interface ZipkitConfig {
84
+ bufferSize?: number; // For streaming mode (default: 512KB)
85
+ debug?: boolean; // Enable debug logging
86
+ }
87
+
88
+ /**
89
+ * Zipkit - Enhanced ZIP File Processing Library with Dual Mode Support
90
+ *
91
+ * Core ZIP file processing library supporting both buffer-based (in-memory) and
92
+ * file-based (streaming) modes. See README.md for detailed documentation.
93
+ *
94
+ * @class Zipkit
95
+ * @static {string} version - Library version string
96
+ * @static {string} releaseDate - Library release date
97
+ */
98
+ export default class Zipkit {
99
+ static readonly version = NEOZIPKIT_INFO.version;
100
+ static readonly releaseDate = NEOZIPKIT_INFO.releaseDate;
101
+
102
+ // Configuration
103
+ private config: ZipkitConfig;
104
+
105
+ // In-memory mode data
106
+ private inBuffer: Buffer | null = null;
107
+ protected zipEntries: ZipEntry[] = []; // Protected: single source of truth for ZIP entry order (accessible by subclasses)
108
+ private centralDirSize: number = 0;
109
+ private centralDirOffset: number = 0;
110
+
111
+ // Common data
112
+ private zipComment: string | null = null;
113
+
114
+ // Private components (lazy-loaded)
115
+ private _zipkitCmp: ZipCompress | null = null;
116
+ private _zipkitDeCmp: ZipDecompress | null = null;
117
+
118
+ /**
119
+ * Creates a new Zipkit instance with optional configuration
120
+ *
121
+ * @param config - Optional configuration object (ZipkitConfig)
122
+ * - bufferSize: Buffer size for streaming operations (default: 512KB)
123
+ * - debug: Enable debug logging (default: false)
124
+ *
125
+ * @example
126
+ * const zip = new Zipkit({ bufferSize: 512 * 1024, debug: true });
127
+ */
128
+ constructor(config: ZipkitConfig = {}) {
129
+ this.config = {
130
+ bufferSize: config.bufferSize || 512 * 1024, // 512KB default (optimal for modern systems)
131
+ debug: config.debug || false // Default false
132
+ };
133
+
134
+ // Configure Logger based on debug setting
135
+ if (this.config.debug) {
136
+ Logger.setLevel('debug');
137
+ }
138
+
139
+ // Note: ZipCompress and ZipDecompress are lazy-loaded when first accessed
140
+ // ZSTD codec is also lazily initialized on first use (module-level singleton)
141
+ }
142
+
143
+ /**
144
+ * Lazy-load ZipCompress instance
145
+ * @returns ZipCompress instance (created on first access)
146
+ */
147
+ private getZipCompress(): ZipCompress {
148
+ if (!this._zipkitCmp) {
149
+ this._zipkitCmp = new ZipCompress(this);
150
+ }
151
+ return this._zipkitCmp;
152
+ }
153
+
154
+ /**
155
+ * Lazy-load ZipDecompress instance
156
+ * @returns ZipDecompress instance (created on first access)
157
+ */
158
+ private getZipDecompress(): ZipDecompress {
159
+ if (!this._zipkitDeCmp) {
160
+ this._zipkitDeCmp = new ZipDecompress(this);
161
+ }
162
+ return this._zipkitDeCmp;
163
+ }
164
+
165
+ /**
166
+ * Get the configured buffer size for streaming operations
167
+ * @returns Buffer size in bytes (default: 512KB)
168
+ */
169
+ getBufferSize(): number {
170
+ return this.config.bufferSize || 512 * 1024; // 512KB default (optimal for modern systems)
171
+ }
172
+
173
+ /**
174
+ * Check if ZIP is loaded in buffer mode
175
+ * @returns true if buffer-based ZIP is loaded
176
+ */
177
+ hasInBuffer(): boolean {
178
+ return this.inBuffer !== null && this.inBuffer.length > 0;
179
+ }
180
+
181
+
182
+ // ============================================================================
183
+ // ZipCompress Wrapper Methods
184
+ // ============================================================================
185
+
186
+ /**
187
+ * Compress data for a ZIP entry (Buffer-based only)
188
+ * Wrapper for ZipCompress.compressData()
189
+ *
190
+ * @param entry - ZIP entry to compress
191
+ * @param data - Buffer containing data to compress
192
+ * @param options - Compression options
193
+ * @param onOutputBuffer - Optional callback for streaming output
194
+ * @returns Promise resolving to Buffer containing compressed data
195
+ */
196
+ async compressData(
197
+ entry: ZipEntry,
198
+ data: Buffer,
199
+ options?: CompressOptions,
200
+ onOutputBuffer?: (data: Buffer) => Promise<void>
201
+ ): Promise<Buffer> {
202
+ return this.getZipCompress().compressData(entry, data, options, onOutputBuffer);
203
+ }
204
+
205
+ /**
206
+ * Compress data using deflate algorithm with chunked processing
207
+ * Wrapper for ZipCompress.deflateCompress()
208
+ *
209
+ * @param data - Data to compress (Buffer or chunked reader)
210
+ * @param options - Compression options
211
+ * @param bufferSize - Size of buffer to read (default: 512KB)
212
+ * @param entry - Optional ZIP entry for hash calculation
213
+ * @param onOutputBuffer - Optional callback for streaming output
214
+ * @returns Promise resolving to Buffer containing compressed data
215
+ */
216
+ async deflateCompress(
217
+ data: Buffer | { totalSize: number, onReadChunk: (position: number, size: number) => Buffer, onOutChunk: (chunk: Buffer) => void },
218
+ options?: CompressOptions,
219
+ bufferSize?: number,
220
+ entry?: ZipEntry,
221
+ onOutputBuffer?: (data: Buffer) => Promise<void>
222
+ ): Promise<Buffer> {
223
+ return this.getZipCompress().deflateCompress(data, options, bufferSize, entry, onOutputBuffer);
224
+ }
225
+
226
+ /**
227
+ * Compress data using deflate algorithm (synchronous, small buffers only)
228
+ * Wrapper for ZipCompress.deflate()
229
+ *
230
+ * @param inbuf - Buffer containing data to compress
231
+ * @param options - Compression options
232
+ * @returns Buffer containing compressed data
233
+ */
234
+ deflate(inbuf: Buffer, options?: CompressOptions): Buffer {
235
+ return this.getZipCompress().deflate(inbuf, options);
236
+ }
237
+
238
+ /**
239
+ * Compress data using Zstandard (zstd) algorithm
240
+ * Wrapper for ZipCompress.zstdCompress()
241
+ *
242
+ * Note: ZSTD codec is lazily initialized on first use (module-level singleton).
243
+ * Initialization happens automatically when needed.
244
+ *
245
+ * @param input - Buffer to compress OR chunked reader object
246
+ * @param options - Compression options
247
+ * @param bufferSize - Size of buffer to read if using chunked reader (default: 512KB)
248
+ * @param entry - Optional ZIP entry for hash calculation
249
+ * @param onOutputBuffer - Optional callback for streaming output
250
+ * @returns Promise resolving to Buffer containing compressed data
251
+ */
252
+ async zstdCompress(
253
+ input: Buffer | { totalSize: number, readChunk: (position: number, size: number) => Buffer },
254
+ options?: CompressOptions,
255
+ bufferSize?: number,
256
+ entry?: ZipEntry,
257
+ onOutputBuffer?: (data: Buffer) => Promise<void>
258
+ ): Promise<Buffer> {
259
+ return this.getZipCompress().zstdCompress(input, options, bufferSize, entry, onOutputBuffer);
260
+ }
261
+
262
+ /**
263
+ * Compress file data in memory and return ZIP entry information as Buffer
264
+ * Wrapper for ZipCompress.compressFileBuffer()
265
+ *
266
+ * @param entry - ZIP entry to compress
267
+ * @param fileData - File data buffer to compress
268
+ * @param cmpOptions - Compression options
269
+ * @returns Promise resolving to Buffer containing local header + compressed data
270
+ */
271
+ async compressFileBuffer(
272
+ entry: ZipEntry,
273
+ fileData: Buffer,
274
+ cmpOptions?: CompressOptions
275
+ ): Promise<Buffer> {
276
+ return this.getZipCompress().compressFileBuffer(entry, fileData, cmpOptions);
277
+ }
278
+
279
+ // ============================================================================
280
+ // ZipDecompress Wrapper Methods
281
+ // ============================================================================
282
+
283
+ /**
284
+ * Extract file data (Buffer-based ZIP only)
285
+ * Wrapper for ZipDecompress.extract()
286
+ *
287
+ * @param entry - ZIP entry to extract
288
+ * @param skipHashCheck - Skip hash verification (CRC-32 or SHA-256)
289
+ * @returns Promise resolving to Buffer containing extracted data, or null if failed
290
+ * @throws Error if not a Buffer-based ZIP
291
+ */
292
+ async extract(entry: ZipEntry, skipHashCheck?: boolean): Promise<Buffer | null> {
293
+ return this.getZipDecompress().extract(entry, skipHashCheck);
294
+ }
295
+
296
+
297
+ // ============================================================================
298
+ // Static Logging Wrapper Methods
299
+ // ============================================================================
300
+
301
+ /**
302
+ * Note: Logging for ZipCompress and ZipDecompress is controlled by their
303
+ * static loggingEnabled property. Set it directly:
304
+ * ZipCompress.loggingEnabled = true/false;
305
+ * ZipDecompress.loggingEnabled = true/false;
306
+ */
307
+
308
+ /**
309
+ * Load ZIP file from buffer (in-memory mode)
310
+ *
311
+ * **Required**: You must call this method (or the appropriate load method for your platform)
312
+ * before calling `getDirectory()` or any other ZIP operations. This method:
313
+ * 1. Resets all ZIP data
314
+ * 2. Stores the buffer in this.inBuffer
315
+ * 3. Loads EOCD and parses central directory
316
+ * 4. Populates this.zipEntries[] array
317
+ *
318
+ * For browser applications, use `ZipkitBrowser.loadZipBlob()` instead.
319
+ * For server applications with file paths, use `ZipkitServer.loadZipFile()` instead.
320
+ *
321
+ * @param inBuf - Buffer containing the complete ZIP file data
322
+ * @returns ZipEntry[] Array of all entries in the ZIP file
323
+ */
324
+ loadZip(inBuf: Buffer): ZipEntry[] {
325
+ this.resetZipData();
326
+ // Store buffer for backward compatibility with detection logic
327
+ this.inBuffer = inBuf;
328
+ // Load EOCD and parse central directory
329
+ this.loadEOCDFromBuffer(inBuf);
330
+ const entries = this.getDirectoryFromBuffer();
331
+ this.zipEntries = entries;
332
+ return entries;
333
+ }
334
+
335
+
336
+ /**
337
+ * Reset all ZIP-related data to initial state
338
+ *
339
+ * Clears all internal state including zipEntries[], inBuffer, and zipComment.
340
+ *
341
+ * Note: ZipCompress and ZipDecompress instances are not recreated.
342
+ * ZSTD codec is a module-level singleton and is lazily initialized on first use,
343
+ * so no cleanup is needed. No memory buffers are allocated until compression/decompression
344
+ * operations are performed.
345
+ */
346
+ private resetZipData(): void {
347
+ this.inBuffer = null;
348
+ this.zipEntries = [];
349
+ this.reset();
350
+ this.zipComment = null;
351
+ // Reset lazy-loaded components so they can be recreated on next access
352
+ this._zipkitCmp = null;
353
+ this._zipkitDeCmp = null;
354
+ // Note: ZSTD codec is module-level singleton, so no cleanup needed
355
+ }
356
+
357
+ /**
358
+ * Get central directory entries (synchronous, buffer-based only)
359
+ *
360
+ * Returns entries from zipEntries[] array which serves as unified storage.
361
+ *
362
+ * **Important**: This method does NOT load the ZIP file. You must call `loadZip()` first
363
+ * to populate zipEntries[]. If the ZIP is not loaded, this method returns an empty array.
364
+ *
365
+ * @param debug - Optional debug flag for logging
366
+ * @returns Array of ZipEntry objects, or empty array if ZIP not loaded
367
+ */
368
+ getDirectory(debug?: boolean): ZipEntry[] {
369
+ return this.zipEntries;
370
+ }
371
+
372
+
373
+ /**
374
+ * Get specific ZIP entry by filename from zipEntries[] array
375
+ *
376
+ * @param filename - The name/path of the file/directory to find
377
+ * @returns ZipEntry object if found, null if not found
378
+ */
379
+ getZipEntry(filename: string): ZipEntry | null {
380
+ try {
381
+ const centralDir = this.getDirectory();
382
+ return centralDir.find((entry: ZipEntry) => entry.filename === filename) || null;
383
+ } catch (error) {
384
+ return null;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Ensure buffer is available (in-memory mode)
390
+ */
391
+ public ensureBuffer(): Buffer {
392
+ if (!this.inBuffer) {
393
+ this.inBuffer = Buffer.alloc(0);
394
+ }
395
+ return this.inBuffer;
396
+ }
397
+
398
+ /**
399
+ * Parse local header (in-memory mode)
400
+ */
401
+ public parseLocalHeader(entry: ZipEntry, buffer: Buffer): Buffer {
402
+ const localData = buffer.subarray(entry.localHdrOffset);
403
+
404
+ if (localData.readUInt32LE(0) !== LOCAL_HDR.SIGNATURE) {
405
+ throw new Error(Errors.INVALID_CEN);
406
+ }
407
+
408
+ let _encryptLen = 0;
409
+ const _bitFlags = localData.readUInt16LE(LOCAL_HDR.FLAGS);
410
+ if (_bitFlags & GP_FLAG.ENCRYPTED) {
411
+ _encryptLen = ENCRYPT_HDR_SIZE;
412
+ }
413
+
414
+ let _fnameLen = localData.readUInt16LE(LOCAL_HDR.FNAME_LEN);
415
+ let _extraLen = localData.readUInt16LE(LOCAL_HDR.EXTRA_LEN);
416
+ let _localSize = LOCAL_HDR.SIZE + _fnameLen + _extraLen;
417
+
418
+ // For encrypted files: entry.compressedSize from central directory includes the 12-byte encryption header
419
+ // parseLocalHeader should return encrypted data WITHOUT the header (decryptBuffer will extract and prepend it)
420
+ // Structure: [local header][filename][extra][12-byte encryption header][encrypted compressed data]
421
+ // We need to read: encrypted compressed data (without the 12-byte header)
422
+ // Start: _localSize + _encryptLen (after the header)
423
+ // End: _localSize + entry.compressedSize (total includes header, so this gives us data without header)
424
+ const dataStart = _localSize + _encryptLen;
425
+ const dataEnd = _localSize + entry.compressedSize;
426
+
427
+ // Return encrypted data WITHOUT the encryption header
428
+ // decryptBuffer will extract the header from localSize to localSize+12 and prepend it
429
+ return localData.subarray(dataStart, dataEnd);
430
+ }
431
+
432
+ /**
433
+ * Test CRC32 checksum
434
+ * Always checks CRC against entry.crc from central directory
435
+ * For DATA_DESC files, CRC is stored in central directory, not local header
436
+ */
437
+ public testCRC32(entry: ZipEntry, data: Buffer): boolean {
438
+ // Always check CRC against entry.crc from central directory
439
+ // For DATA_DESC files, CRC is stored in central directory, not local header
440
+ const _crc = crc32(data);
441
+ const isValid = _crc === entry.crc;
442
+ Logger.debug(`[DEBUG] testCRC32() comparison: calculated=${_crc}, stored=${entry.crc}, isValid=${isValid ? '✓' : '✗'}, DATA_DESC=${(entry.bitFlags & 0x8) !== 0 ? 'YES' : 'NO'}`);
443
+ return isValid;
444
+ }
445
+
446
+ /**
447
+ * Test SHA256 hash
448
+ */
449
+ public testSHA256(entry: ZipEntry, data: Buffer): boolean {
450
+ if (!entry.sha256) {
451
+ throw new Error(Errors.UNKNOWN_SHA256);
452
+ }
453
+ const _sha256 = sha256(data);
454
+ const isValid = _sha256 === entry.sha256;
455
+ Logger.debug(`[DEBUG] testSHA256() comparison: calculated=${_sha256}, stored=${entry.sha256}, isValid=${isValid ? '✓' : '✗'}`);
456
+ return isValid;
457
+ }
458
+
459
+ /**
460
+ * Get memory usage statistics
461
+ *
462
+ * @returns Object containing backend type, total memory usage in bytes, and entry count
463
+ */
464
+ getMemoryUsage(): { backend: 'buffer' | 'file' | 'none'; memoryUsage: number; entries: number } {
465
+ const baseMemory = this.inBuffer?.length || 0;
466
+ const entriesMemory = this.zipEntries.length * 1024; // Approximate memory per entry
467
+
468
+ let backend: 'buffer' | 'file' | 'none';
469
+ if (this.inBuffer) {
470
+ backend = 'buffer';
471
+ } else {
472
+ // Zipkit is buffer-based only, so if no buffer, backend is 'none'
473
+ // For file-based operations, use ZipkitServer
474
+ backend = 'none';
475
+ }
476
+
477
+ return {
478
+ backend,
479
+ memoryUsage: baseMemory + entriesMemory,
480
+ entries: this.zipEntries.length
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Create a new ZIP entry and automatically add it to zipEntries[]
486
+ *
487
+ * @param filename - Entry filename
488
+ * @param data - Optional entry data (for CRC calculation)
489
+ * @param options - Optional entry options
490
+ * @returns Created ZipEntry instance
491
+ */
492
+ createZipEntry(filename: string, data?: Buffer, options?: any): ZipEntry {
493
+ const entry = new ZipEntry(filename, null, false);
494
+ // Set basic properties
495
+ if (data) {
496
+ entry.crc = crc32(data);
497
+ entry.uncompressedSize = data.length;
498
+ entry.compressedSize = data.length;
499
+ } else {
500
+ entry.crc = 0;
501
+ entry.uncompressedSize = 0;
502
+ entry.compressedSize = 0;
503
+ }
504
+ entry.cmpMethod = CMP_METHOD.STORED;
505
+ entry.bitFlags = 0;
506
+ // Note: lastModTime and lastModDate properties may not exist in ZipEntry
507
+ // entry.lastModTime = Math.floor(Date.now() / 1000);
508
+ // entry.lastModDate = Math.floor(Date.now() / 1000);
509
+
510
+ // Automatically add entry to zipEntries[] (maintains order as single source of truth)
511
+ this.zipEntries.push(entry);
512
+
513
+ return entry;
514
+ }
515
+
516
+ /**
517
+ * Copy entry from another ZIP (buffer-based only)
518
+ * For file-based ZIP operations, use ZipkitServer.copyEntry()
519
+ */
520
+ async copyEntry(sourceEntry: ZipEntry): Promise<Buffer> {
521
+ // For Buffer-based ZIP, we need to reconstruct the entry data
522
+ // This is a simplified version - in practice, you'd need to read from the original ZIP
523
+ if (sourceEntry.cmpData) {
524
+ const localHdr = sourceEntry.createLocalHdr();
525
+ return Buffer.concat([localHdr, sourceEntry.cmpData]);
526
+ }
527
+
528
+ throw new Error('Cannot copy entry: no compressed data available');
529
+ }
530
+
531
+ /**
532
+ * Central end header getter (for compatibility)
533
+ *
534
+ * Returns a function that creates the central end header using totalEntries from zipEntries[].
535
+ *
536
+ * @returns Function that creates central end header buffer
537
+ */
538
+ get centralEndHdr(): (centralDirSize: number, centralDirOffset: number) => Buffer {
539
+ // Return a function that calls centralEndHdr with totalEntries from zipEntries[]
540
+ return (centralDirSize: number, centralDirOffset: number) => {
541
+ const totalEntries = this.zipEntries.length;
542
+ return this.centralEndHdrMethod(centralDirSize, centralDirOffset, totalEntries);
543
+ };
544
+ }
545
+
546
+ /**
547
+ * Get ZIP comment
548
+ *
549
+ * @returns ZIP file comment string or null
550
+ */
551
+ getZipComment(): string | null {
552
+ return this.zipComment;
553
+ }
554
+
555
+ // ============================================================================
556
+ // Buffer-based ZIP Loading Methods (merged from ZipLoadEntries)
557
+ // ============================================================================
558
+
559
+ /**
560
+ * Load EOCD from in-memory buffer (buffer mode)
561
+ */
562
+ private loadEOCDFromBuffer(buffer: Buffer): void {
563
+ const fileSize = buffer.length;
564
+ const searchSize = Math.min(0xFFFF + 22, fileSize);
565
+ const searchStart = fileSize - searchSize;
566
+ const scan = buffer.subarray(searchStart, searchStart + searchSize);
567
+
568
+ // Find EOCD signature
569
+ let eocdOffset = -1;
570
+ for (let i = scan.length - 22; i >= 0; i--) {
571
+ if (scan[i] === 0x50) {
572
+ if (scan.readUInt32LE(i) === CENTRAL_END.SIGNATURE) {
573
+ eocdOffset = searchStart + i;
574
+ break;
575
+ }
576
+ }
577
+ }
578
+ if (eocdOffset === -1) {
579
+ throw new Error(Errors.INVALID_FORMAT);
580
+ }
581
+
582
+ const eocdBuffer = buffer.subarray(eocdOffset, eocdOffset + 22);
583
+ if (eocdBuffer.readUInt32LE(0) === CENTRAL_END.SIGNATURE) {
584
+ this.centralDirSize = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_SIZE);
585
+ this.centralDirOffset = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_OFFSET);
586
+
587
+ if (this.centralDirOffset === 0xFFFFFFFF) {
588
+ // ZIP64: locate locator (20 bytes before EOCD) and then ZIP64 EOCD (56 bytes)
589
+ const locatorOffset = eocdOffset - 20;
590
+ const locatorBuffer = buffer.subarray(locatorOffset, locatorOffset + 20);
591
+ if (locatorBuffer.readUInt32LE(0) === ZIP64_CENTRAL_END.SIGNATURE) {
592
+ const zip64Offset = Number(locatorBuffer.readBigUInt64LE(8));
593
+ const zip64Buffer = buffer.subarray(zip64Offset, zip64Offset + 56);
594
+ this.centralDirSize = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE));
595
+ this.centralDirOffset = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET));
596
+ }
597
+ }
598
+ } else {
599
+ throw new Error(Errors.INVALID_FORMAT);
600
+ }
601
+
602
+ // ZIP comment
603
+ const commentLength = eocdBuffer.readUInt16LE(CENTRAL_END.ZIP_COMMENT_LEN);
604
+ if (commentLength > 0) {
605
+ const commentStart = eocdOffset + 22;
606
+ this.zipComment = buffer.subarray(commentStart, commentStart + commentLength).toString();
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Get directory from buffer (buffer mode only)
612
+ * Note: zipEntries[] in Zipkit is the single source of truth for caching.
613
+ * This method always parses fresh from the buffer.
614
+ */
615
+ private getDirectoryFromBuffer(): ZipEntry[] {
616
+ if (!this.inBuffer) {
617
+ return [];
618
+ }
619
+
620
+ const buffer = this.inBuffer;
621
+ const entries: ZipEntry[] = [];
622
+ let offset = this.centralDirOffset;
623
+ let remaining = this.centralDirSize;
624
+ const bufferSize = this.config.bufferSize || 512 * 1024;
625
+
626
+ while (remaining > 0) {
627
+ const currentBufferSize = Math.min(bufferSize, remaining);
628
+ const chunk = buffer.subarray(offset, offset + currentBufferSize);
629
+
630
+ // Parse entries from chunk
631
+ let chunkOffset = 0;
632
+ while (chunkOffset < chunk.length) {
633
+ if (chunk.readUInt32LE(chunkOffset) !== CENTRAL_DIR.SIGNATURE) {
634
+ break; // End of central directory
635
+ }
636
+
637
+ // Parse central directory entry
638
+ const entry = new ZipEntry(null, null, false);
639
+ const entryData = chunk.subarray(chunkOffset);
640
+ const remainingData = entry.readZipEntry(entryData);
641
+
642
+ entries.push(entry);
643
+
644
+ // Move to next entry
645
+ chunkOffset += (entryData.length - remainingData.length);
646
+ }
647
+
648
+ offset += currentBufferSize;
649
+ remaining -= currentBufferSize;
650
+ }
651
+
652
+ // Return entries in original order (do NOT sort by default)
653
+ // Central directory order should match data section order
654
+ // Note: zipEntries[] in Zipkit is the single source of truth for caching
655
+ return entries;
656
+ }
657
+
658
+ /**
659
+ * Reset internal state (private method for Zipkit to call)
660
+ * Clears all ZIP-related data while keeping the instance alive
661
+ */
662
+ private reset(): void {
663
+ this.centralDirSize = 0;
664
+ this.centralDirOffset = 0;
665
+ }
666
+
667
+ /**
668
+ * Create central end header
669
+ * Note: totalEntries must be passed as parameter since zipEntries[] in Zipkit is the cache
670
+ */
671
+ private centralEndHdrMethod(centralDirSize: number, centralDirOffset: number, totalEntries: number): Buffer {
672
+ const ceBuf = Buffer.alloc(CENTRAL_END.SIZE);
673
+ ceBuf.writeUInt32LE(CENTRAL_END.SIGNATURE, 0);
674
+ ceBuf.writeUInt16LE(0, 4); // Number of this disk
675
+ ceBuf.writeUInt16LE(0, 6); // Number of the disk with the start of the central directory
676
+ ceBuf.writeUInt16LE(totalEntries, 8); // Total number of entries
677
+ ceBuf.writeUInt16LE(totalEntries, 10); // Total number of entries on this disk
678
+ ceBuf.writeUInt32LE(centralDirSize, 12); // Size of central directory
679
+ ceBuf.writeUInt32LE(centralDirOffset, 16); // Offset of start of central directory
680
+ ceBuf.writeUInt16LE(0, 20); // ZIP file comment length
681
+
682
+ return ceBuf;
683
+ }
684
+
685
+ /**
686
+ * Calculate Merkle Root of the ZIP file
687
+ *
688
+ * Excludes metadata files (META-INF) to ensure consistent calculation.
689
+ * Simply calls getDirectory() and returns null if empty.
690
+ *
691
+ * @returns Merkle root string or null if calculation fails
692
+ */
693
+ getMerkleRoot(): string | null {
694
+ // Simply get directory - no loading logic
695
+ let zipEntries: ZipEntry[];
696
+ try {
697
+ zipEntries = this.getDirectory();
698
+ } catch (error) {
699
+ // Catch any errors from getDirectory() - this should never happen
700
+ // but if it does, log it and return null
701
+ Logger.error(`getMerkleRoot(): Error calling getDirectory(): ${error instanceof Error ? error.message : String(error)}`);
702
+ Logger.error(`getMerkleRoot(): Stack trace: ${error instanceof Error ? error.stack : 'No stack trace'}`);
703
+ return null;
704
+ }
705
+
706
+ if (!zipEntries || zipEntries.length === 0) {
707
+ return null;
708
+ }
709
+
710
+ const hashAccumulator = new HashCalculator({ enableAccumulation: true });
711
+
712
+ // Filter out metadata files (META-INF) to ensure consistent Merkle Root calculation
713
+ // Inline check to avoid conflict with ZipkitBrowser's isMetadataFile() method
714
+ const contentEntries = zipEntries.filter(entry => {
715
+ const filename = entry.filename || '';
716
+ return filename !== TIMESTAMP_SUBMITTED &&
717
+ filename !== TIMESTAMP_METADATA &&
718
+ filename !== TOKENIZED_METADATA;
719
+ });
720
+
721
+ for (const entry of contentEntries) {
722
+ if (entry.sha256) {
723
+ // Convert hex string to Buffer
724
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
725
+ hashAccumulator.addHash(hashBuffer);
726
+ }
727
+ }
728
+
729
+ const merkleRoot = hashAccumulator.merkleRoot();
730
+ if (!merkleRoot) {
731
+ return null;
732
+ }
733
+
734
+ return merkleRoot;
735
+ }
736
+
737
+ /**
738
+ * Calculate Merkle Root of the ZIP file asynchronously
739
+ *
740
+ * Excludes metadata files (META-INF) to ensure consistent calculation.
741
+ * Works with both buffer-based and file-based ZIPs.
742
+ *
743
+ * @returns Promise resolving to Merkle root string or null if calculation fails
744
+ */
745
+ async getMerkleRootAsync(): Promise<string | null> {
746
+ let zipEntries: ZipEntry[] = [];
747
+
748
+ try {
749
+ // Get entries based on ZIP type
750
+ if (this.inBuffer) {
751
+ zipEntries = this.getDirectory();
752
+ } else {
753
+ // File-based: cannot get entries synchronously - return null
754
+ // Caller should use ZipkitServer.getMerkleRootAsync() for file-based ZIPs
755
+ Logger.error('getMerkleRootAsync() called on file-based ZIP. Use ZipkitServer.getMerkleRootAsync() instead.');
756
+ return null;
757
+ }
758
+ } catch (error) {
759
+ Logger.error(`Failed to get directory for merkle root calculation: ${error instanceof Error ? error.message : String(error)}`);
760
+ return null;
761
+ }
762
+
763
+ if (!zipEntries || zipEntries.length === 0) {
764
+ return null;
765
+ }
766
+
767
+ const hashAccumulator = new HashCalculator({ enableAccumulation: true });
768
+
769
+ // Filter out metadata files (META-INF) to ensure consistent Merkle Root calculation
770
+ // Inline check to avoid conflict with ZipkitBrowser's isMetadataFile() method
771
+ const contentEntries = zipEntries.filter(entry => {
772
+ const filename = entry.filename || '';
773
+ return filename !== TIMESTAMP_SUBMITTED &&
774
+ filename !== TIMESTAMP_METADATA &&
775
+ filename !== TOKENIZED_METADATA;
776
+ });
777
+
778
+ for (const entry of contentEntries) {
779
+ if (entry.sha256) {
780
+ // Convert hex string to Buffer
781
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
782
+ hashAccumulator.addHash(hashBuffer);
783
+ }
784
+ }
785
+
786
+ const merkleRoot = hashAccumulator.merkleRoot();
787
+ if (!merkleRoot) {
788
+ return null;
789
+ }
790
+
791
+ return merkleRoot;
792
+ }
793
+
794
+ }