@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,917 @@
1
+ // ======================================
2
+ // ZipEntry.ts
3
+ // Copyright (c) 2025 NeoWare, Inc. All rights reserved.
4
+ // ======================================
5
+ // Zip Directory Item class
6
+
7
+ import Errors from './constants/Errors';
8
+ import {
9
+ LOCAL_HDR,
10
+ CENTRAL_DIR,
11
+ CMP_METHOD,
12
+ GP_FLAG,
13
+ FILE_SYSTEM,
14
+ HDR_ID,
15
+ DOS_FILE_ATTR,
16
+ } from './constants/Headers';
17
+ import { ZipFileEntry, FileData } from '../types';
18
+ import { Logger } from './components/Logger';
19
+ import { crc32 } from './encryption/ZipCrypto';
20
+
21
+ const VER_ENCODING = 30;
22
+ const VER_EXTRACT = 10; // Version needed to extract (1.0)
23
+
24
+ /**
25
+ * Class representing a single entry (file or directory) within a ZIP archive
26
+ */
27
+ export default class ZipEntry implements ZipFileEntry {
28
+ debug = true;
29
+
30
+ verMadeBy: number = 0; // Read Version Made By
31
+ verExtract: number = 0; // Read Version Needed to Extract
32
+ bitFlags: number = 0; // General purpose bit flag
33
+ cmpMethod: number = 0; // Compression method
34
+ timeDateDOS: number = 0; // DOS File Time(2 bytes) & Date(2 bytes)
35
+ crc: number = 0; // CRC-32
36
+ compressedSize: number = 0; // Compressed size
37
+ uncompressedSize: number = 0; // Uncompressed size
38
+ volNumber: number = 0; // Disk number start
39
+ intFileAttr: number = 0; // Internal file attributes
40
+ extFileAttr: number = 0; // External file attributes
41
+ localHdrOffset: number = 0; // Relative offset to local header from File/Disk start
42
+ filename: string = ''; // File name
43
+ extraField: Buffer | null = null; // Extra field
44
+ comment: string | null = null; // Entry comment
45
+
46
+ // File Data
47
+ fileBuffer: Buffer | null = null; // File Data Buffer
48
+
49
+ // Zip Compressed Data
50
+ cmpData: Buffer | null = null; // Compressed Data Buffer
51
+
52
+ isEncrypted: boolean = false; // Zip Entry is encrypted
53
+ isStrongEncrypt: boolean = false; // Zip Entry is strong encrypted
54
+ encryptHdr: Buffer | null = null; // Encrypted Header (12 bytes)
55
+ lastModTimeDate: number = 0; // Data Descriptor File Time & Date
56
+ decrypt: Function | null = null; // Decrypt Class Function
57
+
58
+ isUpdated: boolean = true; // Entry has been updated
59
+ isDirectory: boolean = false; // Entry is a directory
60
+ isMetaData: boolean = false; // Entry is Zip MetaData
61
+
62
+ // Platform specific data
63
+ platform: string | null = null; // Platform
64
+ universalTime: number | null = null; // Universal Time
65
+ uid: number | null = null; // User ID
66
+ gid: number | null = null; // Group ID
67
+ sha256: string | null = null; // SHA-256 hash of the file
68
+
69
+ // Symbolic link data
70
+ isSymlink: boolean = false; // Entry is a symbolic link
71
+ linkTarget: string | null = null; // Target path for symbolic links
72
+
73
+ // Hard link data
74
+ isHardLink: boolean = false; // Entry is a hard link
75
+ originalEntry: string | null = null; // Original entry name for hard links
76
+ inode: number | null = null; // Inode number for hard links
77
+
78
+ fileData?: FileData;
79
+
80
+ /**
81
+ * Creates a new ZIP entry
82
+ * @param fname - Name of the file within the ZIP
83
+ * @param comment - Optional comment for this entry
84
+ * @param debug - Enable debug logging
85
+ */
86
+ constructor(fname: string | null, comment?: string | null, debug?: boolean) {
87
+ this.filename = fname || '';
88
+ this.comment = comment || null;
89
+ this.debug = debug || false;
90
+
91
+ // Set the UTF-8 Language Encoding Flag (EFS) to indicate UTF-8 encoding for filenames
92
+ this.bitFlags |= GP_FLAG.EFS;
93
+ }
94
+
95
+ isMSDOS = this.platform === null;
96
+ isUnixLike = this.platform != null && this.platform !== 'win32';
97
+ isMacOS = this.platform != null && this.platform === 'darwin';
98
+ isLinux = this.platform != null && this.platform === 'linux';
99
+
100
+ VER_MADE_BY = (() => {
101
+ switch (this.platform) {
102
+ case 'darwin':
103
+ return (FILE_SYSTEM.DARWIN << 8) | VER_ENCODING; // macOS/Darwin
104
+ case 'win32':
105
+ return (FILE_SYSTEM.NTFS << 8) | VER_ENCODING; // Windows
106
+ default:
107
+ return (FILE_SYSTEM.UNIX << 8) | VER_ENCODING; // Unix/Linux
108
+ }
109
+ })();
110
+
111
+ /**
112
+ * Reads ZIP entry data from a central directory buffer
113
+ * @param data - Buffer containing central directory entry data
114
+ * @returns Buffer positioned at start of next entry
115
+ * @throws Error if central directory entry is invalid
116
+ */
117
+ readZipEntry(data: Buffer): Buffer {
118
+ // Check if buffer is too small before trying to read from it
119
+ if (data.length < CENTRAL_DIR.SIZE) {
120
+ throw new Error('Zip entry data is too small or corrupt');
121
+ }
122
+
123
+ // Verify this is a Central Directory Header
124
+ // data should be 46 bytes and start with "PK 01 02"
125
+ if (data.readUInt32LE(0) !== CENTRAL_DIR.SIGNATURE) {
126
+ throw new Error(Errors.INVALID_CEN);
127
+ }
128
+
129
+ // Read Zip version made by
130
+ this.verMadeBy = data.readUInt16LE(CENTRAL_DIR.VER_MADE);
131
+ // Read Zip version needed to extract
132
+ this.verExtract = data.readUInt16LE(CENTRAL_DIR.VER_EXT);
133
+ // encrypt, decrypt flags
134
+ this.bitFlags = data.readUInt16LE(CENTRAL_DIR.FLAGS);
135
+ // Test if Zip Entry is encrypted
136
+ if ((this.bitFlags & GP_FLAG.ENCRYPTED) != 0) {
137
+ this.isEncrypted = true;
138
+ if ((this.bitFlags & GP_FLAG.STRONG_ENCRYPT) != 0)
139
+ this.isStrongEncrypt = true;
140
+ }
141
+ // compression method
142
+ this.cmpMethod = data.readUInt16LE(CENTRAL_DIR.CMP_METHOD);
143
+ // modification time (2 bytes time, 2 bytes date)
144
+ this.timeDateDOS = data.readUInt32LE(CENTRAL_DIR.TIMEDATE_DOS);
145
+
146
+ // uncompressed file crc-32 value
147
+ this.crc = data.readUInt32LE(CENTRAL_DIR.CRC);
148
+ // compressed size
149
+ this.compressedSize = data.readUInt32LE(CENTRAL_DIR.CMP_SIZE);
150
+ // uncompressed size
151
+ this.uncompressedSize = data.readUInt32LE(CENTRAL_DIR.UNCMP_SIZE);
152
+ // volume number start
153
+ this.volNumber = data.readUInt16LE(CENTRAL_DIR.DISK_NUM);
154
+ // internal file attributes
155
+ this.intFileAttr = data.readUInt16LE(CENTRAL_DIR.INT_FILE_ATTR);
156
+ // external file attributes
157
+ this.extFileAttr = data.readUInt32LE(CENTRAL_DIR.EXT_FILE_ATTR);
158
+ if (this.extFileAttr & DOS_FILE_ATTR.DIRECTORY)
159
+ this.isDirectory = true;
160
+
161
+ // LOC header offset
162
+ this.localHdrOffset = data.readUInt32LE(CENTRAL_DIR.LOCAL_HDR_OFFSET);
163
+
164
+ // Filename Length - 2 bytes
165
+ let fnameLen = data.readUInt16LE(CENTRAL_DIR.FNAME_LEN);
166
+ const filename = data.toString('utf8', CENTRAL_DIR.SIZE, CENTRAL_DIR.SIZE + fnameLen);
167
+ this.filename = filename;
168
+ if (this.filename.endsWith('/'))
169
+ this.isDirectory = true;
170
+
171
+ // Extra Field Length - 2 bytes
172
+ let extraLen = data.readUInt16LE(CENTRAL_DIR.EXTRA_LEN);
173
+ if (extraLen > 0) {
174
+ this.extraField = data.subarray(CENTRAL_DIR.SIZE + fnameLen, CENTRAL_DIR.SIZE + fnameLen + extraLen);
175
+
176
+ // First pass: Check for Unicode Path to ensure correct filename before processing other fields
177
+ for (let i = 0; i < extraLen; ) {
178
+ let _id = this.extraField.readUInt16LE(i);
179
+ let _len = this.extraField.readUInt16LE(i + 2);
180
+ let _data = this.extraField.subarray(i + 4, i + 4 + _len);
181
+
182
+ if (_id === HDR_ID.UNICODE_PATH && _len >= 5) {
183
+ // Unicode Path Extra Field
184
+ const version = _data.readUInt8(0);
185
+ const nameCrc32 = _data.readUInt32LE(1);
186
+
187
+ // Calculate CRC32 of the current filename
188
+ const fnameBuf = Buffer.from(this.filename);
189
+ const calculatedCrc = crc32(fnameBuf);
190
+
191
+ // If CRCs match, use the UTF-8 filename from the extra field
192
+ if (calculatedCrc === nameCrc32) {
193
+ const unicodeName = _data.subarray(5).toString('utf8');
194
+ this.filename = unicodeName;
195
+ if (this.debug) {
196
+ Logger.log(`Using Unicode Path: ${this.filename}`);
197
+ }
198
+ }
199
+ }
200
+ i += 4 + _len;
201
+ }
202
+
203
+ // Second pass: Process all other extra fields
204
+ for (let i = 0; i < extraLen; ) {
205
+ let _id = this.extraField.readUInt16LE(i);
206
+ let _len = this.extraField.readUInt16LE(i + 2);
207
+ let _data = this.extraField.subarray(i + 4, i + 4 + _len);
208
+
209
+ if (_id === HDR_ID.SHA256) {
210
+ if (_len === 64)
211
+ // Early versions of NeoZip used a UTF-8 encoded string
212
+ this.sha256 = _data.toString('utf8');
213
+ else
214
+ this.sha256 = _data.toString('hex');
215
+ } else if (_id === HDR_ID.UNV_TIME) {
216
+ // Universal Time field has a flag byte followed by a 4-byte timestamp
217
+ if (_len >= 5) {
218
+ const flags = _data.readUInt8(0);
219
+ // Check if modification time is present (bit 0)
220
+ if (flags & 0x01) {
221
+ this.universalTime = _data.readUInt32LE(1);
222
+ }
223
+ }
224
+ } else if (_id === HDR_ID.UID_GID) {
225
+ // Extract UID/GID if present
226
+ if (_len >= 5) { // Version + UID size + UID + GID size + GID
227
+ const version = _data.readUInt8(0);
228
+ const uidSize = _data.readUInt8(1);
229
+ if (uidSize <= 4 && _len >= 2 + uidSize) {
230
+ // Read UID based on its size (1-4 bytes)
231
+ this.uid = _data.readUIntLE(2, uidSize);
232
+
233
+ // Check if GID is also present
234
+ if (_len >= 2 + uidSize + 1) {
235
+ const gidSize = _data.readUInt8(2 + uidSize);
236
+ if (gidSize <= 4 && _len >= 2 + uidSize + 1 + gidSize) {
237
+ this.gid = _data.readUIntLE(2 + uidSize + 1, gidSize);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ } else if (_id === HDR_ID.SYMLINK) {
243
+ // Extract symbolic link information
244
+ if (_len >= 3) { // Version + Target Length + Target
245
+ const version = _data.readUInt8(0);
246
+ const targetLength = _data.readUInt16LE(1);
247
+ if (targetLength > 0 && _len >= 3 + targetLength) {
248
+ this.isSymlink = true;
249
+ this.linkTarget = _data.subarray(3, 3 + targetLength).toString('utf8');
250
+ }
251
+ }
252
+ } else if (_id === HDR_ID.HARDLINK) {
253
+ // Extract hard link information
254
+ if (_len >= 11) { // Version + Inode + Original Length + Original
255
+ const version = _data.readUInt8(0);
256
+ this.inode = _data.readUInt32LE(1);
257
+ const originalLength = _data.readUInt16LE(5);
258
+ if (originalLength > 0 && _len >= 7 + originalLength) {
259
+ this.isHardLink = true;
260
+ this.originalEntry = _data.subarray(7, 7 + originalLength).toString('utf8');
261
+ }
262
+ }
263
+ }
264
+ // Skip Unicode Path here as we already processed it
265
+ i += 4 + _len;
266
+ }
267
+ }
268
+
269
+ // File Comment Length - 2 bytes
270
+ let comLen = data.readUInt16LE(CENTRAL_DIR.COMMENT_LEN);
271
+ if (comLen > 0)
272
+ this.comment = data.toString('utf8', CENTRAL_DIR.SIZE + fnameLen, CENTRAL_DIR.SIZE + fnameLen + comLen);
273
+
274
+ if (this.debug)
275
+ this.showVerboseInfo();
276
+
277
+ // Calculate the Buffer for the next entry
278
+ let rawSize = CENTRAL_DIR.SIZE + fnameLen + extraLen + comLen;
279
+ return data.subarray(rawSize);
280
+ }
281
+
282
+ /**
283
+ * Checks if the filename contains characters that require Unicode handling
284
+ * @returns true if the filename contains non-ASCII characters or special characters
285
+ */
286
+ needsUnicodeHandling(): boolean {
287
+ // Check if filename contains non-ASCII characters or special characters like apostrophes
288
+ return /[^\x00-\x7E]|['"]/.test(this.filename);
289
+ }
290
+
291
+ /**
292
+ * Adds UTF-8 Unicode Path field to a buffer
293
+ * @param buffer - The buffer to write to
294
+ * @param offset - The offset in the buffer to start writing
295
+ * @returns The new offset after writing
296
+ */
297
+ private addUnicodePathField(buffer: Buffer, offset: number): number {
298
+ // Create a UTF-8 buffer of the filename
299
+ const unicodePathBuf = Buffer.from(this.filename, 'utf8');
300
+
301
+ // Calculate CRC32 of the ASCII version of the filename
302
+ // Create an ASCII version by replacing non-ASCII chars with '?'
303
+ const asciiName = this.filename.replace(/[^\x00-\x7E]/g, '?');
304
+ const asciiNameBuf = Buffer.from(asciiName, 'ascii');
305
+ const nameCrc32 = crc32(asciiNameBuf);
306
+
307
+ // 1 byte version + 4 bytes CRC + filename
308
+ const unicodePathLen = 5 + unicodePathBuf.length;
309
+
310
+ // Write Unicode Path Extra Field (0x7075)
311
+ buffer.writeUInt16LE(HDR_ID.UNICODE_PATH, offset); // "up" header ID
312
+ buffer.writeUInt16LE(unicodePathLen, offset + 2); // data length
313
+ buffer.writeUInt8(1, offset + 4); // version (1)
314
+ buffer.writeUInt32LE(nameCrc32, offset + 5); // CRC-32 of standard filename
315
+ unicodePathBuf.copy(buffer, offset + 9); // UTF-8 version of filename
316
+
317
+ return offset + 4 + unicodePathLen; // 4 bytes for header + data length
318
+ }
319
+
320
+ /**
321
+ * Creates a local header for this ZIP entry
322
+ * @returns Buffer containing the local header data
323
+ */
324
+ createLocalHdr(): Buffer {
325
+ let extraFieldLen = 0;
326
+
327
+ // Only create Unicode Path field if needed
328
+ const needsUnicode = this.needsUnicodeHandling();
329
+
330
+ if (needsUnicode) {
331
+ // 1 byte version + 4 bytes CRC + filename + 4 bytes header
332
+ const unicodeNameLen = Buffer.from(this.filename, 'utf8').length;
333
+ extraFieldLen = 5 + unicodeNameLen + 4;
334
+ }
335
+
336
+ const data = Buffer.alloc(LOCAL_HDR.SIZE + this.filename.length + extraFieldLen);
337
+
338
+ // "PK\003\004"
339
+ data.writeUInt32LE(LOCAL_HDR.SIGNATURE, 0);
340
+ // version needed to extract
341
+ data.writeUInt16LE(VER_EXTRACT, LOCAL_HDR.VER_EXTRACT);
342
+ // general purpose bit flag
343
+ data.writeUInt16LE(this.bitFlags >>> 0, LOCAL_HDR.FLAGS);
344
+ // compression method
345
+ data.writeUInt16LE(this.cmpMethod, LOCAL_HDR.COMPRESSION);
346
+ // modification time (2 bytes time, 2 bytes date)
347
+ data.writeUInt32LE(this.timeDateDOS >>> 0, LOCAL_HDR.TIMEDATE_DOS);
348
+ // uncompressed file crc-32 value
349
+ data.writeUInt32LE(this.crc, LOCAL_HDR.CRC);
350
+ // compressed size
351
+ data.writeUInt32LE(this.compressedSize, LOCAL_HDR.CMP_SIZE);
352
+ // uncompressed size
353
+ data.writeUInt32LE(this.uncompressedSize, LOCAL_HDR.UNCMP_SIZE);
354
+ // filename length
355
+ data.writeUInt16LE(this.filename.length, LOCAL_HDR.FNAME_LEN);
356
+ // extra field length
357
+ data.writeUInt16LE(extraFieldLen, LOCAL_HDR.EXTRA_LEN);
358
+
359
+ // Write filename - use ASCII filename (replacing non-ASCII with ?)
360
+ // This ensures compatibility with older ZIP readers
361
+ const asciiName = this.filename.replace(/[^\x00-\x7E]/g, '?');
362
+ const fnameBuf = Buffer.from(asciiName);
363
+ fnameBuf.copy(data, LOCAL_HDR.SIZE);
364
+
365
+ let extraOffset = LOCAL_HDR.SIZE + fnameBuf.length;
366
+
367
+ // Add Unicode Path Extra Field only if needed
368
+ if (needsUnicode) {
369
+ extraOffset = this.addUnicodePathField(data, extraOffset);
370
+ }
371
+
372
+ // File comments are NOT stored in local headers (ZIP specification)
373
+ // They are only stored in the central directory
374
+
375
+ return data;
376
+ }
377
+
378
+ /**
379
+ * Creates a central directory entry for this ZIP entry
380
+ * @returns Buffer containing the central directory entry data
381
+ */
382
+ centralDirEntry(): Buffer {
383
+ // Calculate the length of the extra fields
384
+ const commentLen = this.comment ? Buffer.from(this.comment, 'utf8').length : 0;
385
+ const utfLen = this.universalTime ? 9 : 0; // 4 bytes header + 5 bytes data
386
+ const uidgidLen = this.uid && this.gid ? 11 + 4 : 0; // 1 byte version + 1 byte size + 4 bytes data + 1 byte size + 4 bytes data
387
+ const sha256Buf = this.sha256 ? Buffer.from(this.sha256, 'hex') : null;
388
+ const sha256Len = sha256Buf ? (sha256Buf.length + 4) : 0;
389
+
390
+ // Calculate symbolic link extra field length
391
+ const symlinkLen = this.isSymlink && this.linkTarget ?
392
+ (4 + 1 + 2 + Buffer.byteLength(this.linkTarget, 'utf8')) : 0; // 4 bytes header + 1 byte version + 2 bytes length + target
393
+
394
+ // Calculate hard link extra field length
395
+ const hardlinkLen = this.isHardLink && this.originalEntry && this.inode !== null ?
396
+ (4 + 1 + 4 + 2 + Buffer.byteLength(this.originalEntry, 'utf8')) : 0; // 4 bytes header + 1 byte version + 4 bytes inode + 2 bytes length + original
397
+
398
+ // Only add Unicode Path field if needed
399
+ const needsUnicode = this.needsUnicodeHandling();
400
+ let unicodePathLen = 0;
401
+
402
+ if (needsUnicode) {
403
+ // 1 byte version + 4 bytes CRC + filename + 4 bytes header
404
+ const unicodeNameLen = Buffer.from(this.filename, 'utf8').length;
405
+ unicodePathLen = 5 + unicodeNameLen + 4;
406
+ }
407
+
408
+ const extraLen = utfLen + sha256Len + uidgidLen + symlinkLen + hardlinkLen + (needsUnicode ? unicodePathLen : 0);
409
+
410
+ // Calculate actual filename length (ASCII conversion may change length)
411
+ const asciiName = this.filename.replace(/[^\x00-\x7E]/g, '?');
412
+ const fnameLen = asciiName.length;
413
+
414
+ // Central directory header size (46 Bytes + filename + comment + extra fields)
415
+ const data = Buffer.alloc(CENTRAL_DIR.SIZE + fnameLen + commentLen + extraLen);
416
+
417
+ // "PK\001\002"
418
+ data.writeUInt32LE(CENTRAL_DIR.SIGNATURE, 0);
419
+ // Version made by - Needs to be set for NeoZip
420
+ data.writeUInt16LE(this.isUpdated ? this.VER_MADE_BY : this.verMadeBy, CENTRAL_DIR.VER_MADE);
421
+ // Version needed to extract
422
+ data.writeInt16LE(this.isUpdated ? VER_EXTRACT : this.verMadeBy, CENTRAL_DIR.VER_EXT);
423
+ // Encrypt, Decrypt Flags
424
+ data.writeInt16LE(this.bitFlags >>> 0, CENTRAL_DIR.FLAGS);
425
+ // Compression method
426
+ data.writeInt16LE(this.cmpMethod, CENTRAL_DIR.CMP_METHOD);
427
+ // Modification time (2 bytes time, 2 bytes date)
428
+ data.writeUInt32LE(this.timeDateDOS >>> 0, CENTRAL_DIR.TIMEDATE_DOS);
429
+ // Uncompressed file CRC-32 value
430
+ data.writeUInt32LE(this.crc, CENTRAL_DIR.CRC);
431
+ // Compressed Size
432
+ data.writeUInt32LE(this.compressedSize, CENTRAL_DIR.CMP_SIZE);
433
+ // Uncompressed Size
434
+ data.writeUInt32LE(this.uncompressedSize, CENTRAL_DIR.UNCMP_SIZE);
435
+ // Filename Length
436
+ data.writeUInt16LE(this.filename.length, CENTRAL_DIR.FNAME_LEN);
437
+ // Extra Field Length
438
+ data.writeUInt16LE(extraLen, CENTRAL_DIR.EXTRA_LEN);
439
+ // File Comment Length
440
+ data.writeUInt16LE(commentLen, CENTRAL_DIR.COMMENT_LEN);
441
+ // Volume Number Start
442
+ data.writeUInt16LE(0, CENTRAL_DIR.DISK_NUM);
443
+ // Internal File Attributes
444
+ data.writeUInt16LE(this.intFileAttr >>> 0, CENTRAL_DIR.INT_FILE_ATTR);
445
+ // External File Attributes
446
+ data.writeUInt32LE(this.extFileAttr >>> 0, CENTRAL_DIR.EXT_FILE_ATTR);
447
+ // Local Header Offset
448
+ data.writeUInt32LE(this.localHdrOffset, CENTRAL_DIR.LOCAL_HDR_OFFSET);
449
+
450
+ // Write filename - use ASCII filename (replacing non-ASCII with ?)
451
+ // This ensures compatibility with older ZIP readers
452
+ const fnameBuf = Buffer.from(asciiName);
453
+ fnameBuf.copy(data, CENTRAL_DIR.SIZE);
454
+
455
+ // Add file comment immediately after filename (InfoZip format)
456
+ let currentOffset = CENTRAL_DIR.SIZE + fnameLen;
457
+ if (commentLen > 0 && this.comment) {
458
+ const commentBuf = Buffer.from(this.comment, 'utf8');
459
+ commentBuf.copy(data, currentOffset);
460
+ currentOffset += commentLen;
461
+ }
462
+
463
+ // Add Extra Field data after file comment
464
+ let extraOffset = currentOffset;
465
+
466
+ // Add Universal Time field
467
+ if (this.universalTime) {
468
+ data.writeUInt16LE(HDR_ID.UNV_TIME, extraOffset); // 0x5455
469
+ data.writeUInt16LE(5, extraOffset + 2); // Length of data (flags + time)
470
+ data.writeUInt8(1, extraOffset + 4); // Flags: modification time present
471
+ data.writeUInt32LE(Math.floor(Date.now() / 1000), extraOffset + 5); // Unix timestamp
472
+ extraOffset += 9;
473
+ }
474
+
475
+ // Add SHA-256 field
476
+ if (sha256Buf) {
477
+ data.writeUInt16LE(HDR_ID.SHA256, extraOffset); // 0x1f
478
+ data.writeUInt16LE(sha256Buf.length, extraOffset + 2); // Length of data
479
+ sha256Buf.copy(data, extraOffset + 4);
480
+ extraOffset += 4 + sha256Buf.length;
481
+ }
482
+
483
+ // Add UID/GID field
484
+ if (this.uid && this.gid) {
485
+ data.writeUInt16LE(HDR_ID.UID_GID, extraOffset); // 0x7875
486
+ data.writeUInt16LE(11, extraOffset + 2); // Length of data
487
+ data.writeUInt8(1, extraOffset + 4); // Version
488
+ data.writeUInt8(4, extraOffset + 5); // UID size
489
+ data.writeUInt32LE(this.uid, extraOffset + 6); // UID
490
+ data.writeUInt8(4, extraOffset + 10); // GID size
491
+ data.writeUInt32LE(this.gid, extraOffset + 11); // GID
492
+ extraOffset += 15;
493
+ }
494
+
495
+ // Add symbolic link field
496
+ if (this.isSymlink && this.linkTarget) {
497
+ const targetBuf = Buffer.from(this.linkTarget, 'utf8');
498
+ const dataLen = 1 + 2 + targetBuf.length; // version + length + target
499
+
500
+ data.writeUInt16LE(HDR_ID.SYMLINK, extraOffset); // 0x7855
501
+ data.writeUInt16LE(dataLen, extraOffset + 2); // Length of data
502
+ data.writeUInt8(1, extraOffset + 4); // Version
503
+ data.writeUInt16LE(targetBuf.length, extraOffset + 5); // Target length
504
+ targetBuf.copy(data, extraOffset + 7); // Target path
505
+ extraOffset += 4 + dataLen;
506
+ }
507
+
508
+ // Add hard link field
509
+ if (this.isHardLink && this.originalEntry && this.inode !== null) {
510
+ const originalBuf = Buffer.from(this.originalEntry, 'utf8');
511
+ const dataLen = 1 + 4 + 2 + originalBuf.length; // version + inode + length + original
512
+
513
+ data.writeUInt16LE(HDR_ID.HARDLINK, extraOffset); // 0x7865
514
+ data.writeUInt16LE(dataLen, extraOffset + 2); // Length of data
515
+ data.writeUInt8(1, extraOffset + 4); // Version
516
+ data.writeUInt32LE(this.inode, extraOffset + 5); // Inode number
517
+ data.writeUInt16LE(originalBuf.length, extraOffset + 9); // Original entry length
518
+ originalBuf.copy(data, extraOffset + 11); // Original entry path
519
+ extraOffset += 4 + dataLen;
520
+ }
521
+
522
+ // Add Unicode Path field if needed
523
+ if (needsUnicode) {
524
+ extraOffset = this.addUnicodePathField(data, extraOffset);
525
+ }
526
+
527
+ // File comment is already written immediately after filename
528
+
529
+ return data;
530
+ }
531
+
532
+ // ======================================
533
+ // Routines to handle the details of the Zip Entry
534
+ // ======================================
535
+
536
+ /**
537
+ * Sets the DOS date/time for this entry
538
+ * @param date - Date to convert to DOS format
539
+ * @returns number - DOS format date/time
540
+ */
541
+ setDateTime(date: Date): number {
542
+ if (!date) return 0;
543
+
544
+ // DOS date/time format:
545
+ // Date part (16 bits): Year (7 bits) + Month (4 bits) + Day (5 bits)
546
+ // Time part (16 bits): Hour (5 bits) + Minute (6 bits) + Second (5 bits, stored as seconds/2)
547
+
548
+ const year = date.getFullYear() - 1980; // Years since 1980
549
+ const month = date.getMonth() + 1; // Month (1-12)
550
+ const day = date.getDate(); // Day (1-31)
551
+ const hour = date.getHours(); // Hour (0-23)
552
+ const minute = date.getMinutes(); // Minute (0-59)
553
+ const second = date.getSeconds(); // Second (0-59)
554
+
555
+ // Pack date: year (7 bits) + month (4 bits) + day (5 bits)
556
+ const datePart = ((year & 0x7f) << 9) | ((month & 0x0f) << 5) | (day & 0x1f);
557
+
558
+ // Pack time: hour (5 bits) + minute (6 bits) + second/2 (5 bits)
559
+ const timePart = ((hour & 0x1f) << 11) | ((minute & 0x3f) << 5) | ((second >> 1) & 0x1f);
560
+
561
+ // Combine date and time parts
562
+ const time = (datePart << 16) | timePart;
563
+
564
+ return time;
565
+ }
566
+
567
+ /**
568
+ * Converts DOS date/time to JavaScript Date
569
+ * @param timeStamp - DOS format date/time
570
+ * @returns Date object or null if timestamp is 0
571
+ */
572
+ parseDateTime(timeStamp: number): Date | null {
573
+ if (timeStamp == 0)
574
+ return null;
575
+
576
+ // Extract date part (upper 16 bits)
577
+ const datePart = (timeStamp >> 16) & 0xffff;
578
+ const year = ((datePart >> 9) & 0x7f) + 1980; // Year (7 bits) + 1980
579
+ const month = ((datePart >> 5) & 0x0f) - 1; // Month (4 bits) - 1 for 0-based
580
+ const day = datePart & 0x1f; // Day (5 bits)
581
+
582
+ // Extract time part (lower 16 bits)
583
+ const timePart = timeStamp & 0xffff;
584
+ const hour = (timePart >> 11) & 0x1f; // Hour (5 bits)
585
+ const minute = (timePart >> 5) & 0x3f; // Minute (6 bits)
586
+ const second = (timePart & 0x1f) << 1; // Second (5 bits) * 2
587
+
588
+ return new Date(year, month, day, hour, minute, second);
589
+ }
590
+
591
+ /**
592
+ * Formats the entry's date in local format
593
+ * @returns String in MM/DD/YYYY format or "--/--/----" if no date
594
+ */
595
+ toLocalDateString(): string {
596
+ let _timeDate = this.parseDateTime(this.timeDateDOS);
597
+ if (_timeDate === null) return "--/--/----";
598
+
599
+ return _timeDate.toLocaleDateString('en-US', {
600
+ year: 'numeric',
601
+ month: '2-digit',
602
+ day: '2-digit'
603
+ }).slice(0,10);
604
+ }
605
+
606
+ /**
607
+ * Formats the entry's time
608
+ * @returns String in HH:MM format or "--:--" if no time
609
+ */
610
+ toTimeString(): string {
611
+ let _timeDate = this.parseDateTime(this.timeDateDOS);
612
+ if (_timeDate === null) return "--:--";
613
+
614
+ return _timeDate.toTimeString().slice(0, 5);
615
+ }
616
+
617
+ /**
618
+ * Formats the entry's date and time in local format
619
+ * @returns String like "Jan 01, 2024 13:45:30" or "--/--/-- --:--" if no date/time
620
+ */
621
+ toFormattedDateString(): string {
622
+ let _timeDate = this.parseDateTime(this.timeDateDOS);
623
+ if (_timeDate == null) return "--/--/-- --:--";
624
+
625
+ const datePart = _timeDate.toLocaleDateString('en-US', {
626
+ year: 'numeric',
627
+ month: 'short',
628
+ day: '2-digit'
629
+ });
630
+
631
+ const timePart = _timeDate.toLocaleTimeString('en-US', {
632
+ hour: '2-digit',
633
+ minute: '2-digit',
634
+ second: '2-digit',
635
+ hour12: false
636
+ });
637
+
638
+ return `${datePart} ${timePart}`;
639
+ }
640
+
641
+ /**
642
+ * Formats the entry's date and time in UTC
643
+ * @returns String like "Jan 01, 2024 13:45:30 UTC" or "--/--/-- --:--" if no date/time
644
+ */
645
+ toFormattedUTCDateString(): string {
646
+ let _timeDate = this.parseDateTime(this.timeDateDOS);
647
+ if (_timeDate == null) return "--/--/-- --:--";
648
+
649
+ const datePart = _timeDate.toLocaleDateString('en-US', {
650
+ year: 'numeric',
651
+ month: 'short',
652
+ day: '2-digit',
653
+ timeZone: 'UTC'
654
+ });
655
+
656
+ const timePart = _timeDate.toLocaleTimeString('en-US', {
657
+ hour: '2-digit',
658
+ minute: '2-digit',
659
+ second: '2-digit',
660
+ hour12: false,
661
+ timeZone: 'UTC'
662
+ });
663
+
664
+ return `${datePart} ${timePart} UTC`;
665
+ }
666
+
667
+ /**
668
+ * Converts compression method code to human-readable string
669
+ * @returns String describing the compression method
670
+ */
671
+ cmpMethodToString(): string {
672
+ switch (this.cmpMethod) {
673
+ case CMP_METHOD.STORED: return 'Stored';
674
+ case CMP_METHOD.SHRUNK: return 'Shrunk';
675
+ case CMP_METHOD.REDUCED1: return 'Reduced-1';
676
+ case CMP_METHOD.REDUCED2: return 'Reduced-2';
677
+ case CMP_METHOD.REDUCED3: return 'Reduced-3';
678
+ case CMP_METHOD.REDUCED4: return 'Reduced-4';
679
+ case CMP_METHOD.IMPLODED: return 'Imploded';
680
+ case CMP_METHOD.DEFLATED:
681
+ switch (this.bitFlags & 0x6) {
682
+ case 0: return 'Deflate-N'; // Deflate Normal
683
+ case 2: return 'Deflate-M'; // Deflate Maximum
684
+ case 4: return 'Deflate-F'; // Deflate Fast
685
+ case 6: return 'Deflate-S'; // Deflate Super Fast
686
+ }
687
+ case CMP_METHOD.ENHANCED_DEFLATE: return 'Deflate-Enh';
688
+ case CMP_METHOD.IBM_TERSE: return 'PKDCL-LZ77';
689
+ case CMP_METHOD.ZSTD: return 'Zstandard';
690
+
691
+ default: return 'Unknown';
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Converts file system code to human-readable string
697
+ * @returns String describing the file system
698
+ */
699
+ fileSystemToString(): string {
700
+ switch (this.verMadeBy >> 8) {
701
+ case FILE_SYSTEM.MSDOS: return 'MS-DOS';
702
+ case FILE_SYSTEM.AMIGA: return 'Amiga';
703
+ case FILE_SYSTEM.OPENVMS: return 'OpenVMS';
704
+ case FILE_SYSTEM.UNIX: return 'Unix';
705
+ case FILE_SYSTEM.VM_CMS: return 'VM/CMS';
706
+ case FILE_SYSTEM.ATARI: return 'Atari ST';
707
+ case FILE_SYSTEM.OS2: return 'OS/2 HPFS';
708
+ case FILE_SYSTEM.MAC: return 'Macintosh';
709
+ case FILE_SYSTEM.CP_M: return 'CP/M';
710
+ case FILE_SYSTEM.NTFS: return 'Windows NTFS';
711
+ case FILE_SYSTEM.MVS: return 'MVS (OS/390 - Z/OS)';
712
+ case FILE_SYSTEM.VSE: return 'VSE';
713
+ case FILE_SYSTEM.ACORN: return 'Acorn Risc';
714
+ case FILE_SYSTEM.ALTMVS: return 'Alternate MVS';
715
+ case FILE_SYSTEM.BEOS: return 'BeOS';
716
+ case FILE_SYSTEM.TANDEM: return 'Tandem';
717
+ case FILE_SYSTEM.OS400: return 'OS/400';
718
+ case FILE_SYSTEM.DARWIN: return 'Apple OS/X (Darwin)';
719
+ default: return 'Unknown';
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Converts MS-DOS file attributes to string representation
725
+ * @returns String like "----" where R=readonly, H=hidden, S=system, A=archive
726
+ */
727
+ private dosAttributesToString(): string {
728
+ let dosAttr = this.extFileAttr & 0xFFFF;
729
+ if (dosAttr === 0) return 'none';
730
+ let attrs = '';
731
+ attrs += (dosAttr & DOS_FILE_ATTR.READONLY) ? 'r' : '-';
732
+ attrs += (dosAttr & DOS_FILE_ATTR.HIDDEN) ? 'h' : '-';
733
+ attrs += (dosAttr & DOS_FILE_ATTR.SYSTEM) ? 's' : '-';
734
+ // attrs += (dosAttr & DOS_FILE_ATTR.VOLUME) ? 'v' : '-';
735
+ attrs += (dosAttr & DOS_FILE_ATTR.DIRECTORY) ? 'd' : '-';
736
+ attrs += (dosAttr & DOS_FILE_ATTR.ARCHIVE) ? 'a' : '-';
737
+ return attrs;
738
+ }
739
+
740
+ /**
741
+ * Outputs detailed information about this entry for debugging
742
+ * Includes compression, encryption, timestamps, and extra fields
743
+ */
744
+ showVerboseInfo() {
745
+ Logger.log('=== Central Directory Entry (%s) ===', this.filename);
746
+ Logger.log('unzip from start of archive: ', this.localHdrOffset);
747
+ Logger.log('File system or operating system of origin: ', this.fileSystemToString());
748
+ Logger.log('Version of encoding software: ', this.verMadeBy);
749
+ Logger.log('Compression Method: ', this.cmpMethodToString());
750
+ Logger.log('File Security Status: ',
751
+ this.bitFlags & GP_FLAG.ENCRYPTED ?
752
+ this.bitFlags & GP_FLAG.STRONG_ENCRYPT ? 'Strong Encrypt' : 'Encrypted' :
753
+ 'Not Encrypted');
754
+ Logger.log('File Modified (DOS date/time) ', this.toFormattedDateString());
755
+ Logger.log('File Modified (UTC) ', this.toFormattedUTCDateString());
756
+ Logger.log('Compressed Size: ', this.compressedSize);
757
+ Logger.log('UnCompressed Size: ', this.uncompressedSize);
758
+ Logger.log(`32-bit CRC value (hex): ${this.crc.toString(16).padStart(8, '0')}`);
759
+ Logger.log('Length of extra field: ', this.extraField?.length ?? 0);
760
+ Logger.log('Length of file comment: ', this.comment?.length ?? 0);
761
+ Logger.log('Unix File Attributes: ');
762
+ Logger.log('MS-DOS File Attributes: ', this.dosAttributesToString());
763
+ if (this.extraField) {
764
+ Logger.log('\nThe Central-Directory Extra Field contains:');
765
+ try {
766
+ for (let i = 0; i < this.extraField.length; ) {
767
+ // Ensure we have at least 4 bytes (header ID + length)
768
+ if (i + 4 > this.extraField.length) {
769
+ Logger.log(` Warning: Truncated extra field at offset ${i}`);
770
+ break;
771
+ }
772
+
773
+ let _id = this.extraField.readUInt16LE(i);
774
+ let _idStr = _id.toString(16).padStart(4, '0');
775
+ let _len = this.extraField.readUInt16LE(i + 2);
776
+
777
+ // Validate the length to ensure it doesn't exceed buffer bounds
778
+ if (_len < 0 || i + 4 + _len > this.extraField.length) {
779
+ Logger.log(` Warning: Invalid extra field length (${_len}) at offset ${i} for ID ${_idStr}`);
780
+ break;
781
+ }
782
+
783
+ let _data = this.extraField.subarray(i + 4, i + 4 + _len);
784
+
785
+ try {
786
+ if (_id === HDR_ID.SHA256) {
787
+ Logger.log(` ID[0x${_idStr}] NeoZip-SHA256: ${_data.toString('hex')}`);
788
+ } else if (_id === HDR_ID.UNV_TIME) {
789
+ if (_len >= 5) {
790
+ const flags = _data.readUInt8(0);
791
+ const timestamp = _data.readUInt32LE(1);
792
+ const date = new Date(timestamp * 1000);
793
+ Logger.log(` ID[0x${_idStr}] Universal Time: flags=${flags}, time=${date.toISOString()}`);
794
+ } else {
795
+ Logger.log(` ID[0x${_idStr}] Universal Time: (invalid length ${_len})`);
796
+ }
797
+ } else if (_id === HDR_ID.UID_GID) {
798
+ if (_len >= 5) {
799
+ const version = _data.readUInt8(0);
800
+ const uidSize = _data.readUInt8(1);
801
+ if (2 + uidSize > _len) {
802
+ Logger.log(` ID[0x${_idStr}] Unix UID/GID: (invalid UID size ${uidSize})`);
803
+ } else {
804
+ const uid = _data.readUIntLE(2, Math.min(uidSize, 4));
805
+ let gid = 0;
806
+ if (2 + uidSize + 1 < _len) {
807
+ const gidSize = _data.readUInt8(2 + uidSize);
808
+ if (2 + uidSize + 1 + gidSize <= _len) {
809
+ gid = _data.readUIntLE(2 + uidSize + 1, Math.min(gidSize, 4));
810
+ }
811
+ }
812
+ Logger.log(` ID[0x${_idStr}] Unix UID/GID: version=${version}, uid=${uid}, gid=${gid}`);
813
+ }
814
+ } else {
815
+ Logger.log(` ID[0x${_idStr}] Unix UID/GID: (invalid length ${_len})`);
816
+ }
817
+ } else if (_id === HDR_ID.SYMLINK) {
818
+ if (_len >= 3) {
819
+ const version = _data.readUInt8(0);
820
+ const targetLength = _data.readUInt16LE(1);
821
+ if (targetLength > 0 && _len >= 3 + targetLength) {
822
+ const target = _data.subarray(3, 3 + targetLength).toString('utf8');
823
+ Logger.log(` ID[0x${_idStr}] Symbolic Link: version=${version}, target="${target}"`);
824
+ } else {
825
+ Logger.log(` ID[0x${_idStr}] Symbolic Link: (invalid target length ${targetLength})`);
826
+ }
827
+ } else {
828
+ Logger.log(` ID[0x${_idStr}] Symbolic Link: (invalid length ${_len})`);
829
+ }
830
+ } else if (_id === HDR_ID.HARDLINK) {
831
+ if (_len >= 11) {
832
+ const version = _data.readUInt8(0);
833
+ const inode = _data.readUInt32LE(1);
834
+ const originalLength = _data.readUInt16LE(5);
835
+ if (originalLength > 0 && _len >= 7 + originalLength) {
836
+ const original = _data.subarray(7, 7 + originalLength).toString('utf8');
837
+ Logger.log(` ID[0x${_idStr}] Hard Link: version=${version}, inode=${inode}, original="${original}"`);
838
+ } else {
839
+ Logger.log(` ID[0x${_idStr}] Hard Link: (invalid original length ${originalLength})`);
840
+ }
841
+ } else {
842
+ Logger.log(` ID[0x${_idStr}] Hard Link: (invalid length ${_len})`);
843
+ }
844
+ } else if (_id === HDR_ID.UNICODE_PATH) {
845
+ if (_len >= 5) {
846
+ const version = _data.readUInt8(0);
847
+ const nameCrc32 = _data.readUInt32LE(1);
848
+ const unicodeName = _data.subarray(5).toString('utf8');
849
+
850
+ // Calculate CRC32 of the original filename for verification
851
+ const fnameBuf = Buffer.from(this.filename);
852
+ const calculatedCrc = crc32(fnameBuf);
853
+
854
+ const crcMatch = nameCrc32 === calculatedCrc ? 'MATCH' : 'MISMATCH';
855
+
856
+ Logger.log(` ID[0x${_idStr}] Unicode Path: version=${version}, CRC32=${nameCrc32.toString(16)} (${crcMatch})`);
857
+ Logger.log(` Path: "${unicodeName}"`);
858
+ } else {
859
+ Logger.log(` ID[0x${_idStr}] Unicode Path: (invalid length ${_len})`);
860
+ }
861
+ } else if (_id === HDR_ID.ZIP64) {
862
+ // ZIP64 Extended Information (0x0001)
863
+ Logger.log(` ID[0x${_idStr}] ZIP64 Extended Information:`);
864
+ if (_len >= 8) {
865
+ let offset = 0;
866
+
867
+ // Read uncompressed size (8 bytes) if present
868
+ if (offset + 8 <= _len) {
869
+ const uncompressedSize = _data.readBigUInt64LE(offset);
870
+ Logger.log(` Uncompressed Size (ZIP64): ${uncompressedSize.toString()} bytes`);
871
+ offset += 8;
872
+ }
873
+
874
+ // Read compressed size (8 bytes) if present
875
+ if (offset + 8 <= _len) {
876
+ const compressedSize = _data.readBigUInt64LE(offset);
877
+ Logger.log(` Compressed Size (ZIP64): ${compressedSize.toString()} bytes`);
878
+ offset += 8;
879
+ }
880
+
881
+ // Read local header offset (8 bytes) if present
882
+ if (offset + 8 <= _len) {
883
+ const localHeaderOffset = _data.readBigUInt64LE(offset);
884
+ Logger.log(` Local Header Offset (ZIP64): ${localHeaderOffset.toString()}`);
885
+ offset += 8;
886
+ }
887
+
888
+ // Read disk number (4 bytes) if present
889
+ if (offset + 4 <= _len) {
890
+ const diskNumber = _data.readUInt32LE(offset);
891
+ Logger.log(` Disk Number (ZIP64): ${diskNumber}`);
892
+ }
893
+ } else {
894
+ Logger.log(` ZIP64 Extended Information: (invalid length ${_len})`);
895
+ }
896
+ } else {
897
+ // For unknown fields, show a hex preview of the first few bytes
898
+ const preview = _len > 0
899
+ ? _data.slice(0, Math.min(16, _len)).toString('hex')
900
+ : '';
901
+ Logger.log(` ID[0x${_idStr}] Unknown field: length=${_len} bytes${preview ? ', data=' + preview + '...' : ''}`);
902
+ }
903
+ } catch (error: any) {
904
+ Logger.log(` Error parsing extra field ID[0x${_idStr}]: ${error.message}`);
905
+ }
906
+
907
+ i += 4 + _len;
908
+ }
909
+ } catch (error: any) {
910
+ Logger.log(` Error parsing extra fields: ${error.message}`);
911
+ }
912
+ }
913
+ Logger.log('\n');
914
+ }
915
+ }
916
+
917
+ export { ZipEntry };