neozipkit 0.1.1

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