@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,437 @@
1
+ // ======================================
2
+ // ZipCopyNode.ts - Efficient ZIP Entry Copying
3
+ // Copyright (c) 2025 NeoWare, Inc. All rights reserved.
4
+ // ======================================
5
+ //
6
+ // Efficient ZIP entry copying using ZipEntry instances directly.
7
+ // Leverages existing offsets and sizes to copy raw bytes without
8
+ // unnecessary parsing and reconstruction.
9
+
10
+ import * as fs from 'fs';
11
+ import ZipEntry from '../core/ZipEntry';
12
+ import ZipkitNode from './ZipkitNode';
13
+ import { LOCAL_HDR, CENTRAL_END, GP_FLAG } from '../core/constants/Headers';
14
+
15
+ /**
16
+ * Options for copying ZIP files
17
+ */
18
+ export interface CopyOptions {
19
+ /** Filter function to determine which entries to copy */
20
+ entryFilter?: (entry: ZipEntry) => boolean;
21
+ /** Sort function to reorder entries before copying */
22
+ entrySorter?: (a: ZipEntry, b: ZipEntry) => number;
23
+ /** Whether to preserve the ZIP file comment (default: true) */
24
+ preserveComments?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Result of copying a ZIP file
29
+ */
30
+ export interface CopyResult {
31
+ /** Information about copied entries */
32
+ entries: Array<{
33
+ filename: string;
34
+ localHeaderOffset: number;
35
+ compressedSize: number;
36
+ }>;
37
+ /** Offset to the start of the central directory */
38
+ centralDirOffset: number;
39
+ /** Total number of entries copied */
40
+ totalEntries: number;
41
+ }
42
+
43
+ /**
44
+ * Result of copying only ZIP entry data (no central directory or EOCD).
45
+ * Use with writeCentralDirectoryAndEOCD after optionally adding more entries
46
+ * to allow building a ZIP that includes both copied and new entries.
47
+ */
48
+ export interface CopyEntriesOnlyResult {
49
+ /** Path to the destination file (entry data only; not yet a valid ZIP) */
50
+ destPath: string;
51
+ /** Offset at which entry data ends; central directory should start here after any new entries are appended */
52
+ dataEndOffset: number;
53
+ /** Copied entries with localHdrOffset set for the destination file */
54
+ copiedEntries: ZipEntry[];
55
+ }
56
+
57
+ /**
58
+ * Options for finalizing a ZIP file (writing central directory and EOCD)
59
+ */
60
+ export interface FinalizeZipOptions {
61
+ /** ZIP file comment (default: empty) */
62
+ zipComment?: string;
63
+ }
64
+
65
+ /**
66
+ * Efficient ZIP file copying class
67
+ *
68
+ * Uses ZipEntry instances directly to copy entries without decompression/recompression.
69
+ * Supports filtering and reordering entries while maintaining ZIP file validity.
70
+ *
71
+ * Entry data and the central directory / EOCD are separated so you can add files
72
+ * to the copy before finalizing: use copyZipEntriesOnly, append new entries at
73
+ * dataEndOffset, then call writeCentralDirectoryAndEOCD with all entries.
74
+ *
75
+ * @example Full copy (one shot)
76
+ * ```typescript
77
+ * const zipCopy = new ZipCopyNode(new ZipkitNode());
78
+ * const result = await zipCopy.copyZipFile('source.zip', 'dest.zip', {
79
+ * entryFilter: (entry) => !entry.filename.startsWith('.'),
80
+ * entrySorter: (a, b) => a.filename.localeCompare(b.filename)
81
+ * });
82
+ * ```
83
+ *
84
+ * @example Copy then add files before finalizing
85
+ * ```typescript
86
+ * const zipCopy = new ZipCopyNode(new ZipkitNode());
87
+ * const { destPath, dataEndOffset, copiedEntries } = await zipCopy.copyZipEntriesOnly('source.zip', 'dest.zip');
88
+ * // ... append new entries to destPath at dataEndOffset, collect new ZipEntry[] with localHdrOffset set ...
89
+ * const allEntries = [...copiedEntries, ...newEntries];
90
+ * zipCopy.writeCentralDirectoryAndEOCD(destPath, allEntries, { zipComment: '' });
91
+ * ```
92
+ */
93
+ export class ZipCopyNode {
94
+ private zipkitNode: ZipkitNode;
95
+
96
+ /**
97
+ * Creates a new ZipCopyNode instance
98
+ * @param zipkitNode - Optional ZipkitNode instance. If not provided, creates a new one.
99
+ */
100
+ constructor(zipkitNode?: ZipkitNode) {
101
+ this.zipkitNode = zipkitNode || new ZipkitNode();
102
+ }
103
+
104
+ /**
105
+ * Calculate local header size by reading the actual local header
106
+ *
107
+ * Reads the first 30 bytes of the local header to get the exact
108
+ * filename and extra field lengths, ensuring accuracy even when
109
+ * local header differs from central directory.
110
+ *
111
+ * @param sourceFd - File descriptor for source ZIP file
112
+ * @param entry - ZipEntry with localHdrOffset
113
+ * @returns Size of the local header in bytes
114
+ */
115
+ private calculateLocalHeaderSize(sourceFd: number, entry: ZipEntry): number {
116
+ // Read the fixed 30-byte local header
117
+ const headerBuffer = Buffer.alloc(LOCAL_HDR.SIZE);
118
+ fs.readSync(sourceFd, headerBuffer, 0, LOCAL_HDR.SIZE, entry.localHdrOffset);
119
+
120
+ // Verify signature
121
+ if (headerBuffer.readUInt32LE(0) !== LOCAL_HDR.SIGNATURE) {
122
+ throw new Error(
123
+ `Invalid local file header signature for entry ${entry.filename} at offset ${entry.localHdrOffset}`
124
+ );
125
+ }
126
+
127
+ // Read filename and extra field lengths from the actual header
128
+ const filenameLength = headerBuffer.readUInt16LE(LOCAL_HDR.FNAME_LEN);
129
+ const extraFieldLength = headerBuffer.readUInt16LE(LOCAL_HDR.EXTRA_LEN);
130
+
131
+ // Calculate total local header size
132
+ return LOCAL_HDR.SIZE + filenameLength + extraFieldLength;
133
+ }
134
+
135
+ /**
136
+ * Copy entry bytes directly from source to destination
137
+ *
138
+ * Copies the local header and compressed data as a single operation.
139
+ *
140
+ * Handles various entry types:
141
+ * - Normal entries: [local header][filename][extra][compressed data]
142
+ * - Encrypted entries: compressedSize includes 12-byte encryption header
143
+ * - Data descriptor entries: [local header][filename][extra][data][data descriptor (16 bytes)]
144
+ *
145
+ * For data descriptor entries, the local header has compressed size = 0,
146
+ * but the actual size is in the central directory (entry.compressedSize).
147
+ *
148
+ * @param sourceFd - File descriptor for source ZIP file
149
+ * @param destFd - File descriptor for destination ZIP file
150
+ * @param entry - ZipEntry with source offset information
151
+ * @returns Number of bytes written
152
+ */
153
+ private copyEntryBytes(
154
+ sourceFd: number,
155
+ destFd: number,
156
+ entry: ZipEntry
157
+ ): number {
158
+ // Calculate local header size by reading the actual header
159
+ const localHeaderSize = this.calculateLocalHeaderSize(sourceFd, entry);
160
+
161
+ // Determine total entry size
162
+ // For data descriptor entries, add 16 bytes for the data descriptor
163
+ const hasDataDescriptor = (entry.bitFlags & GP_FLAG.DATA_DESC) !== 0;
164
+ const totalEntrySize = localHeaderSize + entry.compressedSize + (hasDataDescriptor ? 16 : 0);
165
+
166
+ // Read the entire entry (local header + compressed data + data descriptor if present) from source
167
+ const entryBuffer = Buffer.alloc(totalEntrySize);
168
+ const bytesRead = fs.readSync(sourceFd, entryBuffer, 0, totalEntrySize, entry.localHdrOffset);
169
+
170
+ if (bytesRead !== totalEntrySize) {
171
+ throw new Error(
172
+ `Failed to read complete entry for ${entry.filename}: ` +
173
+ `expected ${totalEntrySize} bytes, got ${bytesRead}`
174
+ );
175
+ }
176
+
177
+ // Verify data descriptor signature if present
178
+ if (hasDataDescriptor) {
179
+ const dataDescOffset = localHeaderSize + entry.compressedSize;
180
+ const dataDescSig = entryBuffer.readUInt32LE(dataDescOffset);
181
+ if (dataDescSig !== 0x08074b50) { // DATA_DESCRIPTOR signature
182
+ throw new Error(
183
+ `Invalid data descriptor signature for entry ${entry.filename} ` +
184
+ `(expected 0x08074b50, got 0x${dataDescSig.toString(16)})`
185
+ );
186
+ }
187
+ }
188
+
189
+ // Write to destination
190
+ const bytesWritten = fs.writeSync(destFd, entryBuffer, 0, totalEntrySize);
191
+
192
+ if (bytesWritten !== totalEntrySize) {
193
+ throw new Error(
194
+ `Failed to write complete entry for ${entry.filename}: ` +
195
+ `expected ${totalEntrySize} bytes, wrote ${bytesWritten}`
196
+ );
197
+ }
198
+
199
+ return bytesWritten;
200
+ }
201
+
202
+ /**
203
+ * Clone a ZipEntry with a new local header offset
204
+ *
205
+ * Creates a new ZipEntry instance with all properties copied from the source,
206
+ * but with an updated localHdrOffset for the destination file.
207
+ *
208
+ * @param entry - Source ZipEntry to clone
209
+ * @param newLocalHdrOffset - New local header offset for the destination
210
+ * @returns New ZipEntry instance with updated offset
211
+ */
212
+ private cloneEntryWithOffset(
213
+ entry: ZipEntry,
214
+ newLocalHdrOffset: number
215
+ ): ZipEntry {
216
+ // Create a new ZipEntry with the same filename and comment
217
+ const cloned = new ZipEntry(entry.filename, entry.comment || null, entry.debug);
218
+
219
+ // Copy all properties
220
+ cloned.verMadeBy = entry.verMadeBy;
221
+ cloned.verExtract = entry.verExtract;
222
+ cloned.bitFlags = entry.bitFlags;
223
+ cloned.cmpMethod = entry.cmpMethod;
224
+ cloned.timeDateDOS = entry.timeDateDOS;
225
+ cloned.crc = entry.crc;
226
+ cloned.compressedSize = entry.compressedSize;
227
+ cloned.uncompressedSize = entry.uncompressedSize;
228
+ cloned.volNumber = entry.volNumber;
229
+ cloned.intFileAttr = entry.intFileAttr;
230
+ cloned.extFileAttr = entry.extFileAttr;
231
+ cloned.localHdrOffset = newLocalHdrOffset; // Update offset
232
+
233
+ // Copy extra field if present
234
+ if (entry.extraField) {
235
+ cloned.extraField = Buffer.from(entry.extraField);
236
+ }
237
+
238
+ // Copy metadata
239
+ cloned.isEncrypted = entry.isEncrypted;
240
+ cloned.isStrongEncrypt = entry.isStrongEncrypt;
241
+ cloned.isDirectory = entry.isDirectory;
242
+ cloned.isMetaData = entry.isMetaData;
243
+ cloned.isUpdated = entry.isUpdated;
244
+
245
+ // Copy platform-specific data
246
+ cloned.platform = entry.platform;
247
+ cloned.universalTime = entry.universalTime;
248
+ cloned.uid = entry.uid;
249
+ cloned.gid = entry.gid;
250
+ cloned.sha256 = entry.sha256;
251
+
252
+ // Copy symlink data
253
+ cloned.isSymlink = entry.isSymlink;
254
+ cloned.linkTarget = entry.linkTarget;
255
+
256
+ // Copy hardlink data
257
+ cloned.isHardLink = entry.isHardLink;
258
+ cloned.originalEntry = entry.originalEntry;
259
+ cloned.inode = entry.inode;
260
+
261
+ return cloned;
262
+ }
263
+
264
+ /**
265
+ * Write central directory and End of Central Directory record to an open file descriptor.
266
+ * Used internally by copyZipFile and by writeCentralDirectoryAndEOCD.
267
+ */
268
+ private writeCentralDirectoryAndEOCDToFd(
269
+ destFd: number,
270
+ entries: ZipEntry[],
271
+ zipComment: string
272
+ ): { centralDirOffset: number; centralDirSize: number } {
273
+ const centralDirStartOffset = fs.fstatSync(destFd).size;
274
+
275
+ for (const entry of entries) {
276
+ const centralDirBuffer = entry.centralDirEntry();
277
+ fs.writeSync(destFd, centralDirBuffer, 0, centralDirBuffer.length);
278
+ }
279
+
280
+ const centralDirEndOffset = fs.fstatSync(destFd).size;
281
+ const centralDirSize = centralDirEndOffset - centralDirStartOffset;
282
+
283
+ const commentBytes = Buffer.from(zipComment, 'utf8');
284
+ const commentLength = Math.min(commentBytes.length, 0xFFFF);
285
+
286
+ const eocdBuffer = Buffer.alloc(22 + commentLength);
287
+ let pos = 0;
288
+
289
+ eocdBuffer.writeUInt32LE(CENTRAL_END.SIGNATURE, pos);
290
+ pos += 4;
291
+ eocdBuffer.writeUInt16LE(0, pos);
292
+ pos += 2;
293
+ eocdBuffer.writeUInt16LE(0, pos);
294
+ pos += 2;
295
+ eocdBuffer.writeUInt16LE(entries.length, pos);
296
+ pos += 2;
297
+ eocdBuffer.writeUInt16LE(entries.length, pos);
298
+ pos += 2;
299
+ eocdBuffer.writeUInt32LE(centralDirSize, pos);
300
+ pos += 4;
301
+ eocdBuffer.writeUInt32LE(centralDirStartOffset, pos);
302
+ pos += 4;
303
+ eocdBuffer.writeUInt16LE(commentLength, pos);
304
+ pos += 2;
305
+ if (commentLength > 0) {
306
+ commentBytes.copy(eocdBuffer, pos, 0, commentLength);
307
+ }
308
+
309
+ fs.writeSync(destFd, eocdBuffer, 0, eocdBuffer.length);
310
+
311
+ return { centralDirOffset: centralDirStartOffset, centralDirSize };
312
+ }
313
+
314
+ /**
315
+ * Copy only ZIP entry data to the destination file (no central directory or EOCD).
316
+ * Use this when you want to add more entries before finalizing. Then call
317
+ * writeCentralDirectoryAndEOCD with all entries (copied + new) to produce a valid ZIP.
318
+ *
319
+ * @param sourceZipPath - Path to source ZIP file
320
+ * @param destZipPath - Path to destination file (will contain only entry data until finalized)
321
+ * @param options - Optional copy options (filtering, sorting)
322
+ * @returns Result with dataEndOffset and copiedEntries for use when adding files and finalizing
323
+ */
324
+ async copyZipEntriesOnly(
325
+ sourceZipPath: string,
326
+ destZipPath: string,
327
+ options?: CopyOptions
328
+ ): Promise<CopyEntriesOnlyResult> {
329
+ const sourceEntries = await this.zipkitNode.loadZipFile(sourceZipPath);
330
+
331
+ let entriesToCopy = options?.entryFilter
332
+ ? sourceEntries.filter(options.entryFilter)
333
+ : sourceEntries;
334
+
335
+ if (options?.entrySorter) {
336
+ entriesToCopy = [...entriesToCopy].sort(options.entrySorter);
337
+ }
338
+
339
+ if (entriesToCopy.length === 0) {
340
+ throw new Error('No entries to copy after filtering');
341
+ }
342
+
343
+ const sourceFd = fs.openSync(sourceZipPath, 'r');
344
+ const destFd = fs.openSync(destZipPath, 'w');
345
+
346
+ try {
347
+ let destOffset = 0;
348
+ const copiedEntries: ZipEntry[] = [];
349
+
350
+ for (const sourceEntry of entriesToCopy) {
351
+ const newLocalHdrOffset = destOffset;
352
+ const bytesWritten = this.copyEntryBytes(sourceFd, destFd, sourceEntry);
353
+ destOffset += bytesWritten;
354
+ const clonedEntry = this.cloneEntryWithOffset(sourceEntry, newLocalHdrOffset);
355
+ copiedEntries.push(clonedEntry);
356
+ }
357
+
358
+ return {
359
+ destPath: destZipPath,
360
+ dataEndOffset: destOffset,
361
+ copiedEntries,
362
+ };
363
+ } finally {
364
+ fs.closeSync(sourceFd);
365
+ fs.closeSync(destFd);
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Append central directory and End of Central Directory to a file that already
371
+ * contains ZIP entry data (e.g. from copyZipEntriesOnly plus any newly added entries).
372
+ * Call this after adding files to produce a valid ZIP.
373
+ *
374
+ * @param destZipPath - Path to the partial ZIP file (entry data only)
375
+ * @param entries - All entries in order (copied + any new), each with localHdrOffset set
376
+ * @param options - Optional finalize options (e.g. zipComment)
377
+ */
378
+ writeCentralDirectoryAndEOCD(
379
+ destZipPath: string,
380
+ entries: ZipEntry[],
381
+ options?: FinalizeZipOptions
382
+ ): void {
383
+ if (entries.length === 0) {
384
+ throw new Error('At least one entry is required to finalize');
385
+ }
386
+
387
+ const zipComment = options?.zipComment ?? '';
388
+ const destFd = fs.openSync(destZipPath, 'a');
389
+
390
+ try {
391
+ this.writeCentralDirectoryAndEOCDToFd(destFd, entries, zipComment);
392
+ } finally {
393
+ fs.closeSync(destFd);
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Copy ZIP file entries efficiently
399
+ *
400
+ * Main method that copies entries from source ZIP to destination ZIP.
401
+ * Uses ZipEntry instances directly to avoid unnecessary parsing.
402
+ *
403
+ * @param sourceZipPath - Path to source ZIP file
404
+ * @param destZipPath - Path to destination ZIP file
405
+ * @param options - Optional copy options (filtering, sorting, etc.)
406
+ * @returns Copy result with entry information
407
+ */
408
+ async copyZipFile(
409
+ sourceZipPath: string,
410
+ destZipPath: string,
411
+ options?: CopyOptions
412
+ ): Promise<CopyResult> {
413
+ // Copy only entry data, then write central directory and EOCD (no added entries)
414
+ const { dataEndOffset, copiedEntries } = await this.copyZipEntriesOnly(
415
+ sourceZipPath,
416
+ destZipPath,
417
+ options
418
+ );
419
+
420
+ const zipComment =
421
+ options?.preserveComments !== false && this.zipkitNode.getZipComment()
422
+ ? this.zipkitNode.getZipComment()!
423
+ : '';
424
+
425
+ this.writeCentralDirectoryAndEOCD(destZipPath, copiedEntries, { zipComment });
426
+
427
+ return {
428
+ entries: copiedEntries.map(entry => ({
429
+ filename: entry.filename,
430
+ localHeaderOffset: entry.localHdrOffset,
431
+ compressedSize: entry.compressedSize,
432
+ })),
433
+ centralDirOffset: dataEndOffset,
434
+ totalEntries: copiedEntries.length,
435
+ };
436
+ }
437
+ }