neozip-cli 0.75.0-beta → 0.75.1-beta

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.
@@ -0,0 +1,2157 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * NEOUNZIP - Standalone ZIP extraction tool with blockchain tokenization verification
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.main = main;
44
+ exports.parseArgs = parseArgs;
45
+ exports.showHelp = showHelp;
46
+ exports.showExtendedHelp = showExtendedHelp;
47
+ // No environment variables needed for neounzip
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const neozipkit_1 = require("neozipkit");
51
+ const blockchain_1 = require("neozipkit/blockchain");
52
+ const node_1 = __importDefault(require("neozipkit/node"));
53
+ const readline = __importStar(require("readline"));
54
+ const version_1 = require("./version");
55
+ const exit_codes_1 = require("./exit-codes");
56
+ /**
57
+ * Format bytes for display
58
+ */
59
+ function formatBytes(bytes) {
60
+ const units = ['B', 'KB', 'MB', 'GB'];
61
+ let i = 0;
62
+ let v = bytes;
63
+ while (v >= 1024 && i < units.length - 1) {
64
+ v /= 1024;
65
+ i++;
66
+ }
67
+ return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
68
+ }
69
+ /**
70
+ * Pad string to the right
71
+ */
72
+ function padRight(s, n) {
73
+ return (s + ' '.repeat(n)).slice(0, n);
74
+ }
75
+ /**
76
+ * Pad string to the left
77
+ */
78
+ function padLeft(s, n) {
79
+ return ((' '.repeat(n)) + s).slice(-n);
80
+ }
81
+ /**
82
+ * Convert date to DOS date parts
83
+ */
84
+ function toDateParts(date) {
85
+ const year = date.getFullYear();
86
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
87
+ const day = date.getDate().toString().padStart(2, '0');
88
+ const hours = date.getHours().toString().padStart(2, '0');
89
+ const minutes = date.getMinutes().toString().padStart(2, '0');
90
+ return {
91
+ date: `${month}-${day}-${year}`,
92
+ time: `${hours}:${minutes}`
93
+ };
94
+ }
95
+ /**
96
+ * Get method label
97
+ */
98
+ function methodLabel(method) {
99
+ switch (method.toLowerCase()) {
100
+ case 'stored': return 'Stored';
101
+ case 'deflate': return 'Deflate';
102
+ case 'deflate64': return 'Deflate64';
103
+ case 'bzip2': return 'BZip2';
104
+ case 'lzma': return 'LZMA';
105
+ case 'ppmd': return 'PPMd';
106
+ case 'zstd': return 'Zstd';
107
+ default: return method;
108
+ }
109
+ }
110
+ /**
111
+ * Get method name from method code
112
+ */
113
+ function getMethodName(method) {
114
+ switch (method) {
115
+ case 0: return 'stored';
116
+ case 8: return 'deflate';
117
+ case 9: return 'deflate64';
118
+ case 12: return 'bzip2';
119
+ case 14: return 'lzma';
120
+ case 95: return 'ppmd';
121
+ case 93: return 'zstd'; // Z Standard compression
122
+ default: return `unknown (${method})`;
123
+ }
124
+ }
125
+ /**
126
+ * Logging function that respects quiet mode
127
+ */
128
+ function log(message, options = { dest: '.', test: false, skipBlockchain: false, verbose: false, quiet: false }) {
129
+ if (!options.quiet) {
130
+ console.log(message);
131
+ }
132
+ }
133
+ /**
134
+ * Prompt user for overwrite confirmation
135
+ * Returns 'y' for yes, 'n' for no, 'a' for always, 'q' for quit/abort
136
+ */
137
+ function promptOverwrite(filename) {
138
+ return new Promise((resolve) => {
139
+ const rl = readline.createInterface({
140
+ input: process.stdin,
141
+ output: process.stdout
142
+ });
143
+ rl.question(`replace ${filename}? [y]es, [n]o, [a]ll, [q]uit: `, (answer) => {
144
+ rl.close();
145
+ const lower = answer.toLowerCase().trim();
146
+ if (lower === 'a' || lower === 'all') {
147
+ resolve('a');
148
+ }
149
+ else if (lower === 'q' || lower === 'quit' || lower === 'abort' || lower === 'c' || lower === 'cancel') {
150
+ resolve('q');
151
+ }
152
+ else if (lower === 'y' || lower === 'yes' || lower === '') {
153
+ resolve('y');
154
+ }
155
+ else {
156
+ resolve('n');
157
+ }
158
+ });
159
+ });
160
+ }
161
+ /**
162
+ * Extract entry - async for Buffer-based ZIPs
163
+ * For file-based ZIPs, use extractToFile() instead
164
+ */
165
+ async function extractEntry(zip, entry, skipBlockchain) {
166
+ // Use Zipkit.extract() method (Buffer-based ZIP only)
167
+ return await zip.extract(entry, skipBlockchain);
168
+ }
169
+ /**
170
+ * Helper function to check if ZIP is file-based
171
+ */
172
+ function isFileBased(zip) {
173
+ try {
174
+ zip.getFileHandle();
175
+ return true;
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ }
181
+ /**
182
+ * Helper function to check if ZIP is buffer-based
183
+ */
184
+ function isBufferBased(zip) {
185
+ return zip.inBuffer !== undefined && zip.inBuffer !== null;
186
+ }
187
+ /**
188
+ * Get directory - auto-detects backend (Buffer or File)
189
+ */
190
+ async function getDirectory(zip, debug) {
191
+ // Check backend type and use appropriate method
192
+ const bufferBased = isBufferBased(zip);
193
+ const fileBased = isFileBased(zip);
194
+ if (bufferBased) {
195
+ // Buffer-based: use synchronous getDirectory()
196
+ return zip.getDirectory(debug) || [];
197
+ }
198
+ else if (fileBased) {
199
+ // File-based: use getDirectory()
200
+ return await zip.getDirectory(debug) || [];
201
+ }
202
+ else {
203
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
204
+ }
205
+ }
206
+ /**
207
+ * Error logging function (always shows errors)
208
+ */
209
+ function logError(message) {
210
+ console.error(message);
211
+ }
212
+ /**
213
+ * Debug logging function (only shows in debug mode)
214
+ */
215
+ function logDebug(message, options = { dest: '', test: false, skipBlockchain: false, verbose: false, quiet: false }) {
216
+ if (options.debug && !options.quiet) {
217
+ console.log(`🐛 DEBUG: ${message}`);
218
+ }
219
+ }
220
+ /**
221
+ * Prompt user for OTS upgrade confirmation
222
+ */
223
+ async function promptForOtsUpgrade(otsResult, archivePath) {
224
+ if (!otsResult.upgraded || !otsResult.upgradedOts) {
225
+ return false;
226
+ }
227
+ console.log(' - Upgrade: Available (improved timestamp can be saved)');
228
+ const rl = readline.createInterface({
229
+ input: process.stdin,
230
+ output: process.stdout
231
+ });
232
+ return new Promise((resolve) => {
233
+ rl.question(' - Apply upgraded timestamp to archive now? (y/N): ', (answer) => {
234
+ rl.close();
235
+ const response = answer.toLowerCase().trim();
236
+ const shouldUpgrade = response === 'y' || response === 'yes';
237
+ resolve(shouldUpgrade);
238
+ });
239
+ });
240
+ }
241
+ /**
242
+ * Prompt user for action when verification times out
243
+ */
244
+ async function promptForTimeoutAction(networkConfig, currentRpcIndex, nonInteractive) {
245
+ if (nonInteractive) {
246
+ return 'exit';
247
+ }
248
+ const rpcUrls = networkConfig.rpcUrls || [];
249
+ const hasMoreRpcs = currentRpcIndex + 1 < rpcUrls.length;
250
+ console.log('\n⏱️ Verification timeout occurred');
251
+ console.log(` Current RPC: ${rpcUrls[currentRpcIndex] || 'unknown'}`);
252
+ if (hasMoreRpcs) {
253
+ console.log(` ${rpcUrls.length - currentRpcIndex - 1} more RPC URL(s) available`);
254
+ }
255
+ const rl = readline.createInterface({
256
+ input: process.stdin,
257
+ output: process.stdout
258
+ });
259
+ return new Promise((resolve) => {
260
+ console.log('\nChoose an option:');
261
+ console.log(' 1. Retry verification with same RPC');
262
+ if (hasMoreRpcs) {
263
+ console.log(' 2. Try next RPC URL');
264
+ }
265
+ console.log(' 3. Exit');
266
+ rl.question('Enter your choice (1-3): ', (answer) => {
267
+ rl.close();
268
+ const choice = answer.trim();
269
+ if (choice === '1') {
270
+ resolve('retry');
271
+ }
272
+ else if (choice === '2' && hasMoreRpcs) {
273
+ resolve('next-rpc');
274
+ }
275
+ else if (choice === '3') {
276
+ resolve('exit');
277
+ }
278
+ else {
279
+ // Invalid choice, default to exit
280
+ console.log('Invalid choice, exiting...');
281
+ resolve('exit');
282
+ }
283
+ });
284
+ });
285
+ }
286
+ /**
287
+ * Perform OTS upgrade on the archive
288
+ */
289
+ async function performOtsUpgrade(archivePath, upgradedOts) {
290
+ try {
291
+ // Create upgrade filename by adding "-upgrade" before the extension
292
+ const ext = path.extname(archivePath);
293
+ const baseName = path.basename(archivePath, ext);
294
+ const dirName = path.dirname(archivePath);
295
+ const upgradedPath = path.join(dirName, `${baseName}-upgrade${ext}`);
296
+ console.log(` - Upgraded OTS size: ${upgradedOts.length} bytes`);
297
+ console.log(` - Creating upgraded archive: ${upgradedPath}`);
298
+ // Copy the original file to the upgrade location first
299
+ if (fs.existsSync(upgradedPath)) {
300
+ fs.unlinkSync(upgradedPath);
301
+ console.log(` - Removed existing upgrade file`);
302
+ }
303
+ fs.copyFileSync(archivePath, upgradedPath);
304
+ console.log(` - Copied original file to upgrade location`);
305
+ // Use the upgradeOTS function
306
+ await (0, blockchain_1.upgradeOTS)(upgradedPath, upgradedOts);
307
+ console.log(' - Upgrade: Applied successfully');
308
+ console.log(` - Note: Upgraded timestamp saved to ${upgradedPath}`);
309
+ console.log(` - Original file ${archivePath} remains unchanged`);
310
+ }
311
+ catch (error) {
312
+ console.log(` - Upgrade failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
313
+ console.log(` - Error details: ${error instanceof Error ? error.stack : 'No stack trace'}`);
314
+ }
315
+ }
316
+ /**
317
+ * Prompt user for password (hidden input)
318
+ */
319
+ async function promptPassword(question) {
320
+ const readline = require('readline');
321
+ const rl = readline.createInterface({
322
+ input: process.stdin,
323
+ output: process.stdout
324
+ });
325
+ return new Promise((resolve) => {
326
+ // Hide the password input
327
+ process.stdout.write(question);
328
+ // Check if stdin has setRawMode method (Node.js environment)
329
+ if (process.stdin.setRawMode) {
330
+ process.stdin.setRawMode(true);
331
+ process.stdin.resume();
332
+ process.stdin.setEncoding('utf8');
333
+ let password = '';
334
+ process.stdin.on('data', (char) => {
335
+ const charStr = char.toString();
336
+ if (charStr === '\r' || charStr === '\n') {
337
+ // Enter pressed
338
+ process.stdin.setRawMode(false);
339
+ process.stdin.pause();
340
+ process.stdout.write('\n');
341
+ rl.close();
342
+ resolve(password);
343
+ }
344
+ else if (charStr === '\u0003') {
345
+ // Ctrl+C
346
+ process.stdin.setRawMode(false);
347
+ process.stdin.pause();
348
+ process.exit(0);
349
+ }
350
+ else if (charStr === '\u007f') {
351
+ // Backspace
352
+ if (password.length > 0) {
353
+ password = password.slice(0, -1);
354
+ process.stdout.write('\b \b');
355
+ }
356
+ }
357
+ else {
358
+ // Regular character
359
+ password += charStr;
360
+ process.stdout.write('*');
361
+ }
362
+ });
363
+ }
364
+ else {
365
+ // Fallback to regular readline if setRawMode is not available
366
+ rl.question(question, (answer) => {
367
+ rl.close();
368
+ resolve(answer.trim());
369
+ });
370
+ }
371
+ });
372
+ }
373
+ /**
374
+ * Ensure directory exists
375
+ */
376
+ function ensureDir(dirPath) {
377
+ if (!fs.existsSync(dirPath)) {
378
+ fs.mkdirSync(dirPath, { recursive: true });
379
+ }
380
+ }
381
+ /**
382
+ * Extract files to stdout (pipe mode)
383
+ */
384
+ async function pipeArchive(zip, options, filePatterns) {
385
+ const entries = await getDirectory(zip, false);
386
+ // Filter entries based on file patterns if provided
387
+ let filteredEntries = entries;
388
+ if (filePatterns && filePatterns.length > 0) {
389
+ filteredEntries = entries.filter(entry => {
390
+ const filename = entry.filename || '';
391
+ return filePatterns.some(pattern => {
392
+ // Simple pattern matching - could be enhanced with glob patterns
393
+ return filename.includes(pattern) || filename === pattern;
394
+ });
395
+ });
396
+ }
397
+ // Apply include/exclude filtering using ZipkitNode.filterEntries()
398
+ const finalEntries = zip.filterEntries(filteredEntries, {
399
+ include: options.include,
400
+ exclude: options.exclude,
401
+ skipMetadata: true // Skip META-INF/NZIP.TOKEN automatically
402
+ });
403
+ for (const entry of finalEntries) {
404
+ const filename = entry.filename || '';
405
+ // Skip token metadata
406
+ if (filename === 'META-INF/NZIP.TOKEN') {
407
+ continue;
408
+ }
409
+ // Skip directories
410
+ if (entry.isDirectory) {
411
+ continue;
412
+ }
413
+ try {
414
+ // Check if file is encrypted
415
+ const isEncrypted = entry.isEncrypted;
416
+ let data = null;
417
+ // Handle encrypted files - extractEntry() handles decryption and decompression
418
+ if (isEncrypted && !options.password) {
419
+ process.stderr.write(`neounzip: ${filename}: encrypted file, password required\n`);
420
+ continue;
421
+ }
422
+ // Set password on zip instance for decryption (if provided)
423
+ if (isEncrypted && options.password) {
424
+ zip.password = options.password;
425
+ }
426
+ // Use extractEntry which handles both encrypted and non-encrypted files
427
+ // It will decrypt (if password set) and decompress automatically
428
+ try {
429
+ data = await extractEntry(zip, entry, options.skipBlockchain);
430
+ if (!data) {
431
+ if (isEncrypted) {
432
+ process.stderr.write(`neounzip: ${filename}: incorrect password\n`);
433
+ }
434
+ else {
435
+ process.stderr.write(`neounzip: ${filename}: extraction failed\n`);
436
+ }
437
+ continue;
438
+ }
439
+ }
440
+ catch (error) {
441
+ if (isEncrypted) {
442
+ process.stderr.write(`neounzip: ${filename}: decryption failed\n`);
443
+ }
444
+ else {
445
+ process.stderr.write(`neounzip: ${filename}: extraction failed\n`);
446
+ }
447
+ continue;
448
+ }
449
+ // Write file content to stdout
450
+ if (data) {
451
+ process.stdout.write(data);
452
+ }
453
+ }
454
+ catch (error) {
455
+ process.stderr.write(`neounzip: ${filename}: ${error instanceof Error ? error.message : String(error)}\n`);
456
+ }
457
+ }
458
+ }
459
+ /**
460
+ * Display archive comment only
461
+ */
462
+ async function showCommentOnly(zip, archivePath, options) {
463
+ // Load the central directory completely to ensure comment is available
464
+ await getDirectory(zip);
465
+ // Always show the archive name first
466
+ console.log(`Archive: ${archivePath}`);
467
+ // Get archive comment
468
+ const archiveComment = zip.getZipComment();
469
+ if (archiveComment && archiveComment.trim()) {
470
+ // Show the comment after the archive name
471
+ console.log(archiveComment);
472
+ }
473
+ // If no comment, just show the archive name (no additional output)
474
+ }
475
+ /**
476
+ * List archive contents in table format (using neolist default format)
477
+ */
478
+ async function listArchive(zip, archivePath, options) {
479
+ const entries = await getDirectory(zip, false);
480
+ // Always show listing output, even in quiet mode
481
+ console.log(`Archive: ${archivePath}`);
482
+ // Detailed verbose format matching neolist default
483
+ const sep = ' ';
484
+ const lengthW = 8, sizeW = 7, cmprW = 4, methodW = 6, dateW = 10, timeW = 5, crcW = 8;
485
+ console.log(padLeft('Length', lengthW) + sep +
486
+ padRight('Method', methodW) + sep +
487
+ padLeft('Size', sizeW) + ' ' +
488
+ padRight('Cmpr', cmprW) + ' ' +
489
+ padRight('Date', dateW) + ' ' +
490
+ padRight('Time', timeW) + ' ' +
491
+ padRight('CRC-32', crcW) + sep +
492
+ 'Name');
493
+ console.log('-------- ------ ------- ---- ---------- ----- -------- ----');
494
+ let totalLen = 0;
495
+ let totalCmp = 0;
496
+ entries.forEach(entry => {
497
+ const unc = entry.uncompressedSize || 0;
498
+ const cmp = entry.compressedSize || 0;
499
+ totalLen += unc;
500
+ totalCmp += cmp;
501
+ const cmpr = unc > 0 ? padRight(String(Math.max(0, Math.round((1 - cmp / unc) * 100))) + '%', cmprW) : padRight('0%', cmprW);
502
+ const modifiedDate = entry.parseDateTime(entry.timeDateDOS) || new Date();
503
+ const { date, time } = toDateParts(modifiedDate);
504
+ const crc = ((entry.crc || 0) >>> 0).toString(16).padStart(8, '0');
505
+ const filename = entry.filename || '';
506
+ console.log(padLeft(String(unc), lengthW) + sep +
507
+ padRight(methodLabel(getMethodName(entry.cmpMethod)), methodW) + sep +
508
+ padLeft(String(cmp), sizeW) + ' ' +
509
+ cmpr + ' ' +
510
+ padRight(date, dateW) + ' ' +
511
+ padRight(time, timeW) + ' ' +
512
+ padRight(crc, crcW) + sep +
513
+ filename);
514
+ // Show file comment below the file entry
515
+ if (entry.comment) {
516
+ console.log(entry.comment);
517
+ }
518
+ });
519
+ console.log('-------- ------ ------- ---- ---------- ----- -------- ----');
520
+ const filesCount = entries.length;
521
+ const compressionRatio = totalLen > 0 ? Math.round((1 - totalCmp / totalLen) * 100) : 0;
522
+ console.log(padLeft(String(totalLen), lengthW) + ' ' +
523
+ padLeft(String(totalCmp), sizeW) + ' ' +
524
+ padRight(`${compressionRatio}%`, 4) + ' ' +
525
+ padRight(`${filesCount} files`, 0));
526
+ // Show archive comment by default
527
+ const archiveComment = zip.getZipComment();
528
+ if (archiveComment && archiveComment.trim()) {
529
+ console.log(archiveComment);
530
+ }
531
+ }
532
+ /**
533
+ * Test archive without extracting
534
+ */
535
+ async function testArchive(zip, options) {
536
+ const entries = await getDirectory(zip, false);
537
+ let failures = 0;
538
+ // Set password on zip instance if provided (needed for decryption)
539
+ if (options.password) {
540
+ zip.password = options.password;
541
+ }
542
+ // Check if ZIP is file-based or buffer-based
543
+ const fileBased = isFileBased(zip);
544
+ const bufferBased = isBufferBased(zip);
545
+ // For merkle root calculation: collect verified SHA-256 hashes during test extraction
546
+ const hashAccumulator = new neozipkit_1.HashCalculator({ enableAccumulation: true });
547
+ let allFilesValid = true;
548
+ const failedFiles = [];
549
+ // Filter out metadata files for merkle root calculation
550
+ const contentEntriesForMerkle = entries.filter(entry => {
551
+ const filename = entry.filename || '';
552
+ return filename !== neozipkit_1.TOKENIZED_METADATA &&
553
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
554
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED &&
555
+ !entry.isDirectory;
556
+ });
557
+ for (const entry of entries) {
558
+ const name = entry.filename || '';
559
+ // Skip directories
560
+ if (entry.isDirectory) {
561
+ continue;
562
+ }
563
+ if (options.showFiles && !options.quiet) {
564
+ log(`testing: ${name}`, options);
565
+ }
566
+ try {
567
+ if (bufferBased) {
568
+ // Buffer-based: use extract() which performs CRC/SHA checks and returns data in memory
569
+ await extractEntry(zip, entry, options.skipBlockchain);
570
+ }
571
+ else if (fileBased) {
572
+ // File-based: use testEntry() to validate without extracting to disk
573
+ // testEntry() now returns the verified hash, eliminating the need for temp files
574
+ const testResult = await zip.testEntry(entry, {
575
+ skipHashCheck: options.skipBlockchain
576
+ });
577
+ // Collect verified SHA-256 hash for merkle root calculation if available
578
+ if (testResult.verifiedHash && !options.skipBlockchain) {
579
+ const isContentFile = name !== neozipkit_1.TOKENIZED_METADATA &&
580
+ name !== neozipkit_1.TIMESTAMP_METADATA &&
581
+ name !== neozipkit_1.TIMESTAMP_SUBMITTED &&
582
+ !entry.isDirectory;
583
+ if (isContentFile) {
584
+ // Use the verified hash returned from testEntry()
585
+ const hashBuffer = Buffer.from(testResult.verifiedHash, 'hex');
586
+ hashAccumulator.addHash(hashBuffer);
587
+ }
588
+ }
589
+ }
590
+ else {
591
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
592
+ }
593
+ // Report success
594
+ if (entry.sha256 && !options.skipBlockchain) {
595
+ console.log(`testing: ${name} ...OK SHA-256`);
596
+ }
597
+ else if (entry.sha256 && options.skipBlockchain) {
598
+ console.log(`testing: ${name} ...OK (SHA-256 skipped)`);
599
+ }
600
+ else {
601
+ console.log(`testing: ${name} ...OK`);
602
+ }
603
+ }
604
+ catch (err) {
605
+ failures += 1;
606
+ let msg = err?.message || String(err) || 'ERROR';
607
+ // Check if entry is encrypted to provide better error messages
608
+ // Check bitFlags for encryption flag (bit 0)
609
+ const isEncrypted = entry.isEncrypted || (entry.bitFlags & 0x01) !== 0;
610
+ // Track failed files for merkle root calculation
611
+ const isContentFile = name !== neozipkit_1.TOKENIZED_METADATA &&
612
+ name !== neozipkit_1.TIMESTAMP_METADATA &&
613
+ name !== neozipkit_1.TIMESTAMP_SUBMITTED &&
614
+ !entry.isDirectory;
615
+ if (isEncrypted) {
616
+ // For encrypted files, decompression errors after decryption usually mean wrong password
617
+ // Check for pako inflate errors which indicate corrupted/decompressed data
618
+ if (msg.includes('CRC32') || msg.includes('INVALID_CRC') || msg.includes('CRC-32') || msg.includes('CRC32 checksum')) {
619
+ msg = 'Incorrect Password';
620
+ }
621
+ else if (msg.includes('SHA-256') || msg.includes('INVALID_SHA256') || msg.includes('SHA-256 hash')) {
622
+ msg = 'Incorrect Password';
623
+ // Track SHA-256 failure for content files
624
+ if (isContentFile && !options.skipBlockchain) {
625
+ allFilesValid = false;
626
+ failedFiles.push(name);
627
+ }
628
+ }
629
+ else if (msg.includes('invalid distance') || msg.includes('invalid block type') ||
630
+ msg.includes('invalid stored block') || msg.includes('invalid code lengths') ||
631
+ msg.includes('invalid distance code') || msg.includes('deflate') ||
632
+ msg.includes('inflate') || msg.includes('corrupted')) {
633
+ // These are decompression errors, which for encrypted files usually mean wrong password
634
+ msg = 'Incorrect Password';
635
+ }
636
+ else if (msg === 'ERROR' || !msg || msg.length === 0) {
637
+ // If we get a generic error on encrypted file, assume password issue
638
+ msg = 'Incorrect Password';
639
+ }
640
+ // Otherwise use the error message as-is for other decryption errors
641
+ }
642
+ else {
643
+ // For non-encrypted files, provide clearer CRC/SHA error messages
644
+ if (msg.includes('CRC32') || msg.includes('INVALID_CRC') || msg.includes('CRC-32') || msg.includes('CRC32 checksum')) {
645
+ msg = 'CRC-32 failed';
646
+ }
647
+ else if (msg.includes('SHA-256') || msg.includes('INVALID_SHA256') || msg.includes('SHA-256 hash')) {
648
+ msg = 'SHA-256 failed';
649
+ // Track SHA-256 failure for content files
650
+ if (isContentFile && !options.skipBlockchain) {
651
+ allFilesValid = false;
652
+ failedFiles.push(name);
653
+ }
654
+ }
655
+ }
656
+ console.log(`testing: ${name} ${msg}`);
657
+ }
658
+ }
659
+ const count = entries.length;
660
+ if (failures === 0) {
661
+ console.log(`No errors detected in compressed data of ${count} file${count === 1 ? '' : 's'}`);
662
+ }
663
+ else {
664
+ console.log(`Errors detected in compressed data (${failures} of ${count} file${count === 1 ? '' : 's'})`);
665
+ }
666
+ // Calculate merkle root from verified hashes
667
+ let merkleRoot = null;
668
+ if (!options.skipBlockchain && contentEntriesForMerkle.length > 0) {
669
+ merkleRoot = hashAccumulator.merkleRoot();
670
+ }
671
+ return {
672
+ failures,
673
+ extractionResult: { merkleRoot, allFilesValid, failedFiles }
674
+ };
675
+ }
676
+ /**
677
+ * Pre-verify archive integrity and blockchain authenticity before extraction
678
+ * Returns success status and any errors
679
+ */
680
+ async function preVerifyArchive(zip, archiveName, options) {
681
+ console.log('\n🔍 Pre-verifying archive before extraction...\n');
682
+ // Step 1: Test archive integrity (CRC/SHA-256)
683
+ const testResult = await testArchive(zip, options);
684
+ if (testResult.failures > 0) {
685
+ console.log(`\n❌ Integrity verification failed: ${testResult.failures} file(s) failed CRC/SHA-256 checks`);
686
+ return {
687
+ success: false,
688
+ integrityPassed: false,
689
+ blockchainPassed: false,
690
+ error: `Integrity check failed: ${testResult.failures} file(s) failed verification`
691
+ };
692
+ }
693
+ if (!testResult.extractionResult.allFilesValid) {
694
+ console.log(`\n❌ Integrity verification failed: SHA-256 verification failed for one or more files`);
695
+ if (testResult.extractionResult.failedFiles.length > 0) {
696
+ console.log(` Failed files: ${testResult.extractionResult.failedFiles.join(', ')}`);
697
+ }
698
+ return {
699
+ success: false,
700
+ integrityPassed: false,
701
+ blockchainPassed: false,
702
+ error: `SHA-256 verification failed for: ${testResult.extractionResult.failedFiles.join(', ')}`
703
+ };
704
+ }
705
+ console.log('✓ Integrity verification passed (CRC/SHA-256)');
706
+ // Step 2: Check for blockchain metadata and verify if present
707
+ const entries = await getDirectory(zip, false);
708
+ const tokenEntry = entries.find((e) => (e.filename || '') === 'META-INF/NZIP.TOKEN');
709
+ if (!tokenEntry) {
710
+ // No blockchain metadata - integrity check is sufficient
711
+ console.log('✓ No blockchain metadata found - integrity verification sufficient');
712
+ return {
713
+ success: true,
714
+ integrityPassed: true,
715
+ blockchainPassed: true // Not applicable
716
+ };
717
+ }
718
+ // Blockchain metadata exists - verify it
719
+ console.log('Blockchain metadata detected - verifying authenticity...');
720
+ if (options.skipBlockchain) {
721
+ console.log('⚠️ Blockchain verification skipped by user request');
722
+ return {
723
+ success: true,
724
+ integrityPassed: true,
725
+ blockchainPassed: false // Skipped
726
+ };
727
+ }
728
+ // Perform blockchain verification
729
+ // We'll use a modified approach that captures the result
730
+ let verificationSuccess = false;
731
+ let verificationError;
732
+ try {
733
+ const verifier = new neozipkit_1.ZipkitVerifier({
734
+ debug: options.verbose,
735
+ skipHash: options.skipBlockchain
736
+ });
737
+ // Extract token metadata
738
+ const fileBased = isFileBased(zip);
739
+ const bufferBased = isBufferBased(zip);
740
+ let tokenBuffer;
741
+ try {
742
+ if (bufferBased) {
743
+ const tokenBuf = await extractEntry(zip, tokenEntry);
744
+ if (!tokenBuf) {
745
+ throw new Error('Cannot read token metadata (no data)');
746
+ }
747
+ tokenBuffer = tokenBuf;
748
+ }
749
+ else if (fileBased) {
750
+ const tmpPath = path.join(require('os').tmpdir(), `neounzip-preverify-${Date.now()}-${tokenEntry.filename?.replace(/[^a-zA-Z0-9]/g, '_') || 'token'}`);
751
+ try {
752
+ await zip.extractToFile(tokenEntry, tmpPath, {
753
+ skipHashCheck: options.skipBlockchain
754
+ });
755
+ tokenBuffer = fs.readFileSync(tmpPath);
756
+ if (fs.existsSync(tmpPath)) {
757
+ fs.unlinkSync(tmpPath);
758
+ }
759
+ }
760
+ catch (extractErr) {
761
+ if (fs.existsSync(tmpPath)) {
762
+ try {
763
+ fs.unlinkSync(tmpPath);
764
+ }
765
+ catch {
766
+ // Ignore cleanup errors
767
+ }
768
+ }
769
+ throw extractErr;
770
+ }
771
+ }
772
+ else {
773
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
774
+ }
775
+ }
776
+ catch (err) {
777
+ throw new Error(`Cannot read token metadata: ${err.message}`);
778
+ }
779
+ // Parse token metadata
780
+ const metadataResult = await verifier.extractTokenMetadata(tokenBuffer);
781
+ if (!metadataResult.success || !metadataResult.metadata) {
782
+ throw new Error(metadataResult.error || 'Failed to parse token metadata');
783
+ }
784
+ // Use pre-calculated merkle root from test
785
+ const calculatedMerkleRoot = testResult.extractionResult.merkleRoot;
786
+ if (!calculatedMerkleRoot) {
787
+ throw new Error('Failed to calculate merkle root from archive contents');
788
+ }
789
+ // Get network config
790
+ const tokenMetadata = metadataResult.metadata;
791
+ const chainId = tokenMetadata.networkChainId;
792
+ let networkConfig = null;
793
+ if (chainId) {
794
+ networkConfig = (0, blockchain_1.getContractConfig)(chainId);
795
+ }
796
+ else {
797
+ const networkName = tokenMetadata.network.toLowerCase();
798
+ if (networkName.includes('base sepolia') || networkName === 'base-sepolia') {
799
+ networkConfig = (0, blockchain_1.getContractConfig)(84532);
800
+ }
801
+ else if (networkName.includes('base mainnet') || networkName === 'base-mainnet' || networkName === 'base') {
802
+ networkConfig = (0, blockchain_1.getContractConfig)(8453);
803
+ }
804
+ else if (networkName.includes('arbitrum sepolia') || networkName === 'arbitrum-sepolia' || (networkName.includes('arbitrum') && networkName.includes('sepolia'))) {
805
+ networkConfig = (0, blockchain_1.getContractConfig)(421614);
806
+ }
807
+ else if (networkName.includes('sepolia testnet') || networkName === 'sepolia-testnet' || (networkName.includes('sepolia') && !networkName.includes('base') && !networkName.includes('arbitrum'))) {
808
+ networkConfig = (0, blockchain_1.getContractConfig)(11155111);
809
+ }
810
+ }
811
+ // Perform verification (non-interactive mode for pre-verify)
812
+ const originalNonInteractive = options.nonInteractive;
813
+ options.nonInteractive = true; // Force non-interactive to avoid prompts during pre-verify
814
+ let verificationResult;
815
+ let currentRpcIndex = 0;
816
+ let maxRetries = 3;
817
+ let retryCount = 0;
818
+ while (true) {
819
+ try {
820
+ verificationResult = await verifier.verifyToken(tokenMetadata, calculatedMerkleRoot, {
821
+ debug: options.verbose,
822
+ skipHash: options.skipBlockchain
823
+ }, currentRpcIndex);
824
+ if (verificationResult.success) {
825
+ verificationSuccess = true;
826
+ break;
827
+ }
828
+ // Check if timeout - in pre-verify mode, we'll try next RPC or fail
829
+ const isTimeout = verificationResult.message?.toLowerCase().includes('timeout') ||
830
+ verificationResult.errorDetails?.errorType === 'CONTRACT_ERROR' &&
831
+ (verificationResult.message?.includes('timeout') ||
832
+ verificationResult.message?.includes('Contract call timeout'));
833
+ if (isTimeout && networkConfig && currentRpcIndex < networkConfig.rpcUrls.length - 1) {
834
+ // Try next RPC
835
+ currentRpcIndex++;
836
+ retryCount = 0;
837
+ continue;
838
+ }
839
+ else {
840
+ // No more RPCs or not a timeout - fail
841
+ verificationError = verificationResult.message || 'Blockchain verification failed';
842
+ break;
843
+ }
844
+ }
845
+ catch (error) {
846
+ const errorMessage = error instanceof Error ? error.message : String(error);
847
+ const isTimeout = errorMessage.toLowerCase().includes('timeout');
848
+ if (isTimeout && networkConfig && currentRpcIndex < networkConfig.rpcUrls.length - 1) {
849
+ currentRpcIndex++;
850
+ retryCount = 0;
851
+ continue;
852
+ }
853
+ else {
854
+ verificationError = `Verification error: ${errorMessage}`;
855
+ break;
856
+ }
857
+ }
858
+ }
859
+ // Restore original non-interactive setting
860
+ options.nonInteractive = originalNonInteractive;
861
+ if (verificationSuccess) {
862
+ console.log('✓ Blockchain verification passed');
863
+ return {
864
+ success: true,
865
+ integrityPassed: true,
866
+ blockchainPassed: true
867
+ };
868
+ }
869
+ else {
870
+ console.log(`\n❌ Blockchain verification failed: ${verificationError}`);
871
+ return {
872
+ success: false,
873
+ integrityPassed: true,
874
+ blockchainPassed: false,
875
+ error: verificationError
876
+ };
877
+ }
878
+ }
879
+ catch (error) {
880
+ const errorMsg = error instanceof Error ? error.message : String(error);
881
+ console.log(`\n❌ Blockchain verification error: ${errorMsg}`);
882
+ return {
883
+ success: false,
884
+ integrityPassed: true,
885
+ blockchainPassed: false,
886
+ error: errorMsg
887
+ };
888
+ }
889
+ }
890
+ /**
891
+ * Extract archive to destination
892
+ * Returns merkle root calculation result for blockchain verification
893
+ */
894
+ async function extractArchive(zip, destination, options) {
895
+ ensureDir(destination);
896
+ // Set password on zip instance if provided (needed for decryption)
897
+ if (options.password) {
898
+ zip.password = options.password;
899
+ }
900
+ const entries = await getDirectory(zip, false);
901
+ // Filter entries using ZipkitNode.filterEntries()
902
+ const filteredEntries = zip.filterEntries(entries, {
903
+ include: options.include,
904
+ exclude: options.exclude,
905
+ skipMetadata: true // Skip META-INF/NZIP.TOKEN automatically
906
+ });
907
+ if (options.verbose && (options.include || options.exclude)) {
908
+ log(`File filtering applied:`, options);
909
+ if (options.include) {
910
+ log(` Include patterns: ${options.include.join(', ')}`, options);
911
+ }
912
+ if (options.exclude) {
913
+ log(` Exclude patterns: ${options.exclude.join(', ')}`, options);
914
+ }
915
+ log(` Original files: ${entries.length}, Filtered files: ${filteredEntries.length}`, options);
916
+ }
917
+ let totalFiles = 0;
918
+ let totalBytes = 0;
919
+ let alwaysOverwrite = false; // Track "always" response from user
920
+ let processedFiles = 0;
921
+ const totalEntries = filteredEntries.length;
922
+ // For merkle root calculation: collect verified SHA-256 hashes during extraction
923
+ const hashAccumulator = new neozipkit_1.HashCalculator({ enableAccumulation: true });
924
+ let allFilesValid = true;
925
+ const failedFiles = [];
926
+ // Filter out metadata files for merkle root calculation
927
+ const contentEntriesForMerkle = filteredEntries.filter(entry => {
928
+ const filename = entry.filename || '';
929
+ return filename !== neozipkit_1.TOKENIZED_METADATA &&
930
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
931
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED &&
932
+ !entry.isDirectory;
933
+ });
934
+ for (const entry of filteredEntries) {
935
+ const filename = entry.filename || '';
936
+ if (options.debug) {
937
+ logDebug(`Processing entry: ${filename}`, options);
938
+ logDebug(` Entry size: ${entry.uncompressedSize || 0} bytes`, options);
939
+ logDebug(` Compressed size: ${entry.compressedSize || 0} bytes`, options);
940
+ logDebug(` Compression method: ${entry.compressionMethod || 'unknown'}`, options);
941
+ logDebug(` Is encrypted: ${entry.isEncrypted || false}`, options);
942
+ logDebug(` CRC32: ${entry.crc || 'unknown'}`, options);
943
+ }
944
+ if (options.progress && !options.quiet) {
945
+ log(`📦 Extracting file ${processedFiles + 1}/${totalEntries}: ${filename}`, options);
946
+ }
947
+ if (options.showFiles && !options.quiet) {
948
+ log(`extracting: ${filename}`, options);
949
+ }
950
+ // Prepare extraction path using ZipkitNode.prepareExtractionPath()
951
+ const outPath = zip.prepareExtractionPath(entry, destination, {
952
+ junkPaths: options.junkPaths
953
+ });
954
+ // Determine if entry should be extracted using ZipkitNode.shouldExtractEntry()
955
+ const shouldExtract = await zip.shouldExtractEntry(entry, outPath, {
956
+ overwrite: options.overwrite || alwaysOverwrite,
957
+ never: options.never,
958
+ freshenOnly: options.freshenOnly,
959
+ updateOnly: options.updateOnly,
960
+ onOverwritePrompt: async (filename) => {
961
+ if (options.quiet) {
962
+ return 'n'; // Skip in quiet mode
963
+ }
964
+ const response = await promptOverwrite(filename);
965
+ if (response === 'a') {
966
+ alwaysOverwrite = true; // Track "always" response
967
+ }
968
+ return response;
969
+ }
970
+ });
971
+ if (!shouldExtract.shouldExtract) {
972
+ if (shouldExtract.reason) {
973
+ if (options.verbose || shouldExtract.reason.includes('abort')) {
974
+ log(shouldExtract.reason, options);
975
+ }
976
+ if (shouldExtract.reason.includes('abort')) {
977
+ return { merkleRoot: null, allFilesValid: false, failedFiles: [] }; // User aborted
978
+ }
979
+ }
980
+ continue;
981
+ }
982
+ // Extract entry using ZipkitNode.extractEntryToPath()
983
+ const result = await zip.extractEntryToPath(entry, outPath, {
984
+ skipHashCheck: options.skipBlockchain,
985
+ preserveTimestamps: true, // Always preserve timestamps
986
+ preservePermissions: options.preservePerms || false,
987
+ symlinks: options.symlinks || false,
988
+ hardLinks: options.hardLinks || false,
989
+ onProgress: options.enableProgress ? (entry, bytes) => {
990
+ // Update progress if needed
991
+ if (options.progress && !options.quiet) {
992
+ // Progress is already shown above
993
+ }
994
+ } : undefined
995
+ });
996
+ if (result.success) {
997
+ totalFiles++;
998
+ totalBytes += result.bytesExtracted;
999
+ const hasSha = entry.sha256 ? true : false;
1000
+ const suffix = hasSha ? (options.skipBlockchain ? ' (SHA-256 skipped)' : ' (SHA-256 OK)') : '';
1001
+ if (options.verbose)
1002
+ log(`extracting: ${filename} (${result.bytesExtracted} bytes)${suffix}`, options);
1003
+ else
1004
+ log(`extracting: ${filename}${suffix}`, options);
1005
+ // Collect verified SHA-256 hash for merkle root calculation (single pass - no double check)
1006
+ // Only if this is a content file (not metadata) and SHA-256 verification was performed
1007
+ if (!options.skipBlockchain && hasSha) {
1008
+ const isContentFile = filename !== neozipkit_1.TOKENIZED_METADATA &&
1009
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
1010
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED &&
1011
+ !entry.isDirectory;
1012
+ if (isContentFile && entry.sha256) {
1013
+ // Use the verified hash from the entry (already verified during extraction)
1014
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
1015
+ hashAccumulator.addHash(hashBuffer);
1016
+ }
1017
+ }
1018
+ }
1019
+ else {
1020
+ log(`error: ${filename} (${result.error || 'unknown error'})`, options);
1021
+ // Track failed files for merkle root calculation
1022
+ const isContentFile = filename !== neozipkit_1.TOKENIZED_METADATA &&
1023
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
1024
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED &&
1025
+ !entry.isDirectory;
1026
+ if (isContentFile && !options.skipBlockchain) {
1027
+ // Check if failure was due to SHA-256 mismatch
1028
+ const errorMsg = result.error || '';
1029
+ if (errorMsg.includes('SHA-256') || errorMsg.includes('INVALID_SHA256')) {
1030
+ allFilesValid = false;
1031
+ failedFiles.push(filename);
1032
+ }
1033
+ }
1034
+ }
1035
+ processedFiles++;
1036
+ }
1037
+ if (totalFiles > 0) {
1038
+ log(`\n✅ Extraction completed: ${totalFiles} files, ${formatBytes(totalBytes)} extracted`, options);
1039
+ }
1040
+ // Calculate merkle root from verified hashes
1041
+ let merkleRoot = null;
1042
+ if (!options.skipBlockchain && contentEntriesForMerkle.length > 0) {
1043
+ merkleRoot = hashAccumulator.merkleRoot();
1044
+ }
1045
+ return { merkleRoot, allFilesValid, failedFiles };
1046
+ }
1047
+ /**
1048
+ * Detect and verify tokenization metadata inside the ZIP against blockchain
1049
+ * @param extractionResult Optional pre-calculated merkle root from extraction/test (avoids double pass)
1050
+ */
1051
+ async function verifyTokenization(zip, archiveName, options, extractionResult) {
1052
+ try {
1053
+ const entries = await getDirectory(zip, false);
1054
+ const tokenEntry = entries.find((e) => (e.filename || '') === 'META-INF/NZIP.TOKEN');
1055
+ if (tokenEntry) {
1056
+ console.log('\nTokenization detected (META-INF/NZIP.TOKEN)');
1057
+ }
1058
+ // Only do token verification if there's a token entry
1059
+ if (tokenEntry) {
1060
+ // Skip blockchain verification if requested
1061
+ if (options.skipBlockchain) {
1062
+ console.log('Blockchain verification skipped by user request');
1063
+ return;
1064
+ }
1065
+ // Initialize blockchain verifier
1066
+ const verifier = new neozipkit_1.ZipkitVerifier({
1067
+ debug: options.verbose, // Enable debug output in verbose mode
1068
+ skipHash: options.skipBlockchain
1069
+ });
1070
+ // Extract token metadata from ZIP
1071
+ // Check if ZIP is file-based or buffer-based and use appropriate extraction method
1072
+ const fileBased = isFileBased(zip);
1073
+ const bufferBased = isBufferBased(zip);
1074
+ let tokenBuffer;
1075
+ try {
1076
+ if (bufferBased) {
1077
+ // Buffer-based: use extract() which performs CRC/SHA checks
1078
+ const tokenBuf = await extractEntry(zip, tokenEntry);
1079
+ if (!tokenBuf) {
1080
+ console.log('✗ Cannot read token metadata (no data)');
1081
+ return;
1082
+ }
1083
+ tokenBuffer = tokenBuf;
1084
+ }
1085
+ else if (fileBased) {
1086
+ // File-based: extract to temporary file, then read it
1087
+ const tmpPath = path.join(require('os').tmpdir(), `neounzip-token-${Date.now()}-${tokenEntry.filename?.replace(/[^a-zA-Z0-9]/g, '_') || 'token'}`);
1088
+ try {
1089
+ await zip.extractToFile(tokenEntry, tmpPath, {
1090
+ skipHashCheck: options.skipBlockchain
1091
+ });
1092
+ // Read the extracted token file
1093
+ tokenBuffer = fs.readFileSync(tmpPath);
1094
+ // Clean up temporary file
1095
+ if (fs.existsSync(tmpPath)) {
1096
+ fs.unlinkSync(tmpPath);
1097
+ }
1098
+ }
1099
+ catch (extractErr) {
1100
+ // Clean up temporary file on error
1101
+ if (fs.existsSync(tmpPath)) {
1102
+ try {
1103
+ fs.unlinkSync(tmpPath);
1104
+ }
1105
+ catch {
1106
+ // Ignore cleanup errors
1107
+ }
1108
+ }
1109
+ throw extractErr;
1110
+ }
1111
+ }
1112
+ else {
1113
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
1114
+ }
1115
+ }
1116
+ catch (err) {
1117
+ console.log('✗ Cannot read token metadata (CRC/IO error)');
1118
+ if (options.verbose) {
1119
+ console.log(` Error: ${err.message}`);
1120
+ }
1121
+ return;
1122
+ }
1123
+ // Parse token metadata
1124
+ const metadataResult = await verifier.extractTokenMetadata(tokenBuffer);
1125
+ if (!metadataResult.success || !metadataResult.metadata) {
1126
+ console.log(`✗ ${metadataResult.error}`);
1127
+ return;
1128
+ }
1129
+ // Calculate merkle root from archive contents
1130
+ // CRITICAL: Use pre-calculated merkle root from extraction if available (single pass - no double extraction)
1131
+ // If not available (e.g., list-only mode), extract files once to calculate merkle root
1132
+ let calculatedMerkleRoot = null;
1133
+ let allFilesValid = true;
1134
+ let failedFiles = [];
1135
+ if (extractionResult) {
1136
+ // Use pre-calculated merkle root from extraction/test (single pass - already verified SHA-256)
1137
+ calculatedMerkleRoot = extractionResult.merkleRoot;
1138
+ allFilesValid = extractionResult.allFilesValid;
1139
+ failedFiles = extractionResult.failedFiles;
1140
+ if (!allFilesValid) {
1141
+ console.log('\n⚠️ WARNING: One or more files failed SHA-256 verification during extraction.');
1142
+ console.log(' Blockchain verification cannot be considered valid.');
1143
+ if (options.verbose && failedFiles.length > 0) {
1144
+ console.log(` Failed files: ${failedFiles.join(', ')}`);
1145
+ }
1146
+ // Skip blockchain verification if files failed SHA-256
1147
+ return;
1148
+ }
1149
+ }
1150
+ else if (!options.skipBlockchain) {
1151
+ // No pre-calculated result (e.g., list-only mode) - extract files once to calculate merkle root
1152
+ try {
1153
+ console.log(' Calculating merkle root from actual file contents...');
1154
+ const hashAccumulator = new neozipkit_1.HashCalculator({ enableAccumulation: true });
1155
+ // Filter out blockchain metadata files to ensure consistent Merkle Root calculation
1156
+ const contentEntries = entries.filter(entry => {
1157
+ const filename = entry.filename || '';
1158
+ return filename !== neozipkit_1.TOKENIZED_METADATA &&
1159
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
1160
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED &&
1161
+ !entry.isDirectory; // Skip directories
1162
+ });
1163
+ // Extract each file once and verify SHA-256 during extraction
1164
+ let hashCount = 0;
1165
+ for (const entry of contentEntries) {
1166
+ try {
1167
+ let fileData = null;
1168
+ // Extract the file to get actual data (single pass - verify SHA-256 during extraction)
1169
+ if (bufferBased) {
1170
+ // Buffer-based: use extract() which verifies SHA-256 during extraction
1171
+ fileData = await zip.extract(entry, false); // false = verify hash
1172
+ }
1173
+ else if (fileBased) {
1174
+ // File-based: extract to temporary file, then read it
1175
+ const tmpPath = path.join(require('os').tmpdir(), `neounzip-verify-${Date.now()}-${(entry.filename || 'file').replace(/[^a-zA-Z0-9]/g, '_')}`);
1176
+ try {
1177
+ await zip.extractToFile(entry, tmpPath, {
1178
+ skipHashCheck: false // Verify hashes during extraction
1179
+ });
1180
+ fileData = fs.readFileSync(tmpPath);
1181
+ // Clean up temporary file
1182
+ if (fs.existsSync(tmpPath)) {
1183
+ fs.unlinkSync(tmpPath);
1184
+ }
1185
+ }
1186
+ catch (extractErr) {
1187
+ // Clean up temporary file on error
1188
+ if (fs.existsSync(tmpPath)) {
1189
+ try {
1190
+ fs.unlinkSync(tmpPath);
1191
+ }
1192
+ catch {
1193
+ // Ignore cleanup errors
1194
+ }
1195
+ }
1196
+ // If extraction fails due to SHA-256, track it
1197
+ const errorMsg = extractErr instanceof Error ? extractErr.message : String(extractErr);
1198
+ if (errorMsg.includes('SHA-256') || errorMsg.includes('INVALID_SHA256')) {
1199
+ allFilesValid = false;
1200
+ failedFiles.push(entry.filename || 'unknown');
1201
+ }
1202
+ // Skip this entry
1203
+ if (options.verbose) {
1204
+ console.log(` Warning: Could not extract ${entry.filename} for merkle root calculation`);
1205
+ }
1206
+ continue;
1207
+ }
1208
+ }
1209
+ else {
1210
+ if (options.verbose) {
1211
+ console.log(` Warning: ZIP backend type unknown, cannot extract ${entry.filename}`);
1212
+ }
1213
+ continue;
1214
+ }
1215
+ if (fileData && entry.sha256) {
1216
+ // Use the verified hash from the entry (already verified during extraction - no double check)
1217
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
1218
+ hashAccumulator.addHash(hashBuffer);
1219
+ hashCount++;
1220
+ }
1221
+ }
1222
+ catch (error) {
1223
+ // If we can't extract a file, track it
1224
+ const errorMsg = error instanceof Error ? error.message : String(error);
1225
+ if (errorMsg.includes('SHA-256') || errorMsg.includes('INVALID_SHA256')) {
1226
+ allFilesValid = false;
1227
+ failedFiles.push(entry.filename || 'unknown');
1228
+ }
1229
+ // Skip this entry
1230
+ if (options.verbose) {
1231
+ console.log(` Warning: Could not calculate hash for ${entry.filename}: ${errorMsg}`);
1232
+ }
1233
+ continue;
1234
+ }
1235
+ }
1236
+ if (hashCount === 0) {
1237
+ if (options.verbose) {
1238
+ console.log(` Error: No files could be extracted for merkle root calculation`);
1239
+ }
1240
+ calculatedMerkleRoot = null;
1241
+ }
1242
+ else {
1243
+ calculatedMerkleRoot = hashAccumulator.merkleRoot();
1244
+ if (options.verbose) {
1245
+ console.log(` Calculated merkle root from ${hashCount} file(s)`);
1246
+ }
1247
+ }
1248
+ // Check if any files failed SHA-256
1249
+ if (!allFilesValid) {
1250
+ console.log('\n⚠️ WARNING: One or more files failed SHA-256 verification.');
1251
+ console.log(' Blockchain verification cannot be considered valid.');
1252
+ if (options.verbose && failedFiles.length > 0) {
1253
+ console.log(` Failed files: ${failedFiles.join(', ')}`);
1254
+ }
1255
+ // Skip blockchain verification if files failed SHA-256
1256
+ return;
1257
+ }
1258
+ }
1259
+ catch (error) {
1260
+ if (options.verbose) {
1261
+ console.log(` Error calculating merkle root: ${error instanceof Error ? error.message : String(error)}`);
1262
+ }
1263
+ calculatedMerkleRoot = null;
1264
+ }
1265
+ }
1266
+ // Perform complete verification with timeout handling and RPC retry logic
1267
+ console.log('Verifying token on blockchain...');
1268
+ // Get network config to access RPC URLs
1269
+ const tokenMetadata = metadataResult.metadata;
1270
+ const chainId = tokenMetadata.networkChainId;
1271
+ let networkConfig = null;
1272
+ if (chainId) {
1273
+ networkConfig = (0, blockchain_1.getContractConfig)(chainId);
1274
+ }
1275
+ else {
1276
+ // Fallback: try to determine chain ID from network name
1277
+ const networkName = tokenMetadata.network.toLowerCase();
1278
+ if (networkName.includes('base sepolia') || networkName === 'base-sepolia') {
1279
+ networkConfig = (0, blockchain_1.getContractConfig)(84532);
1280
+ }
1281
+ else if (networkName.includes('base mainnet') || networkName === 'base-mainnet' || networkName === 'base') {
1282
+ networkConfig = (0, blockchain_1.getContractConfig)(8453);
1283
+ }
1284
+ else if (networkName.includes('arbitrum sepolia') || networkName === 'arbitrum-sepolia' || (networkName.includes('arbitrum') && networkName.includes('sepolia'))) {
1285
+ networkConfig = (0, blockchain_1.getContractConfig)(421614);
1286
+ }
1287
+ else if (networkName.includes('sepolia testnet') || networkName === 'sepolia-testnet' || (networkName.includes('sepolia') && !networkName.includes('base') && !networkName.includes('arbitrum'))) {
1288
+ networkConfig = (0, blockchain_1.getContractConfig)(11155111);
1289
+ }
1290
+ }
1291
+ let verificationResult;
1292
+ let currentRpcIndex = 0;
1293
+ let maxRetries = 3; // Maximum retries per RPC
1294
+ let retryCount = 0;
1295
+ while (true) {
1296
+ try {
1297
+ verificationResult = await verifier.verifyToken(tokenMetadata, calculatedMerkleRoot, {
1298
+ debug: options.verbose,
1299
+ skipHash: options.skipBlockchain
1300
+ }, currentRpcIndex);
1301
+ // If successful, break out of retry loop
1302
+ if (verificationResult.success) {
1303
+ break;
1304
+ }
1305
+ // Check if this is a timeout error
1306
+ const isTimeout = verificationResult.message?.toLowerCase().includes('timeout') ||
1307
+ verificationResult.errorDetails?.errorType === 'CONTRACT_ERROR' &&
1308
+ (verificationResult.message?.includes('timeout') ||
1309
+ verificationResult.message?.includes('Contract call timeout'));
1310
+ if (isTimeout && !options.nonInteractive && networkConfig) {
1311
+ // Display the error first
1312
+ console.log(`❌ ${verificationResult.message}`);
1313
+ // Show verbose error details
1314
+ if (verificationResult.errorDetails) {
1315
+ const errorDetails = verificationResult.errorDetails;
1316
+ console.log(` Error Type: ${errorDetails.errorType}`);
1317
+ if (errorDetails.networkName)
1318
+ console.log(` Network: ${errorDetails.networkName}`);
1319
+ if (errorDetails.rpcUrl)
1320
+ console.log(` RPC URL: ${errorDetails.rpcUrl}`);
1321
+ if (errorDetails.contractAddress)
1322
+ console.log(` Contract: ${errorDetails.contractAddress}`);
1323
+ if (errorDetails.tokenId)
1324
+ console.log(` Token ID: ${errorDetails.tokenId}`);
1325
+ }
1326
+ // Prompt user for action
1327
+ const action = await promptForTimeoutAction(networkConfig, currentRpcIndex, options.nonInteractive || false);
1328
+ if (action === 'exit') {
1329
+ // User chose to exit - exit with appropriate code
1330
+ const errorType = verificationResult.errorDetails?.errorType;
1331
+ if (errorType === 'METADATA_ERROR') {
1332
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_METADATA_ERROR, 'Verification cancelled by user');
1333
+ }
1334
+ else if (errorType === 'MERKLE_ERROR') {
1335
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_MERKLE_MISMATCH, 'Verification cancelled by user');
1336
+ }
1337
+ else if (errorType === 'NETWORK_ERROR') {
1338
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_NETWORK_ERROR, 'Verification cancelled by user');
1339
+ }
1340
+ else {
1341
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_USER_CANCELLED, 'Verification cancelled by user');
1342
+ }
1343
+ }
1344
+ else if (action === 'retry') {
1345
+ // Retry with same RPC
1346
+ retryCount++;
1347
+ if (retryCount >= maxRetries) {
1348
+ console.log(`\n❌ Maximum retries (${maxRetries}) reached for current RPC`);
1349
+ break;
1350
+ }
1351
+ console.log(`\n🔄 Retrying verification (attempt ${retryCount + 1}/${maxRetries})...`);
1352
+ continue;
1353
+ }
1354
+ else if (action === 'next-rpc') {
1355
+ // Try next RPC URL
1356
+ currentRpcIndex++;
1357
+ retryCount = 0; // Reset retry count for new RPC
1358
+ if (currentRpcIndex >= networkConfig.rpcUrls.length) {
1359
+ console.log('\n❌ No more RPC URLs available');
1360
+ break;
1361
+ }
1362
+ console.log(`\n🔄 Trying next RPC URL (${currentRpcIndex + 1}/${networkConfig.rpcUrls.length})...`);
1363
+ continue;
1364
+ }
1365
+ }
1366
+ else {
1367
+ // Not a timeout or non-interactive mode - break and show error
1368
+ break;
1369
+ }
1370
+ }
1371
+ catch (error) {
1372
+ // Handle unexpected errors
1373
+ const errorMessage = error instanceof Error ? error.message : String(error);
1374
+ const isTimeout = errorMessage.toLowerCase().includes('timeout');
1375
+ if (isTimeout && !options.nonInteractive && networkConfig) {
1376
+ // Display the error first
1377
+ console.log(`❌ Verification error: ${errorMessage}`);
1378
+ console.log(` Network: ${tokenMetadata.network}`);
1379
+ console.log(` RPC URL: ${networkConfig.rpcUrls[currentRpcIndex] || 'unknown'}`);
1380
+ const action = await promptForTimeoutAction(networkConfig, currentRpcIndex, options.nonInteractive || false);
1381
+ if (action === 'exit') {
1382
+ // User chose to exit - exit with timeout/network error code
1383
+ if (errorMessage.toLowerCase().includes('timeout')) {
1384
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_TIMEOUT, `Verification cancelled by user: ${errorMessage}`);
1385
+ }
1386
+ else {
1387
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_NETWORK_ERROR, `Verification cancelled by user: ${errorMessage}`);
1388
+ }
1389
+ }
1390
+ else if (action === 'retry') {
1391
+ retryCount++;
1392
+ if (retryCount >= maxRetries) {
1393
+ verificationResult = {
1394
+ success: false,
1395
+ message: `Maximum retries reached: ${errorMessage}`,
1396
+ errorDetails: {
1397
+ errorType: 'CONTRACT_ERROR',
1398
+ networkName: tokenMetadata.network
1399
+ }
1400
+ };
1401
+ break;
1402
+ }
1403
+ console.log(`\n🔄 Retrying verification (attempt ${retryCount + 1}/${maxRetries})...`);
1404
+ continue;
1405
+ }
1406
+ else if (action === 'next-rpc') {
1407
+ currentRpcIndex++;
1408
+ retryCount = 0;
1409
+ if (currentRpcIndex >= networkConfig.rpcUrls.length) {
1410
+ verificationResult = {
1411
+ success: false,
1412
+ message: `No more RPC URLs available: ${errorMessage}`,
1413
+ errorDetails: {
1414
+ errorType: 'CONTRACT_ERROR',
1415
+ networkName: tokenMetadata.network
1416
+ }
1417
+ };
1418
+ break;
1419
+ }
1420
+ console.log(`\n🔄 Trying next RPC URL (${currentRpcIndex + 1}/${networkConfig.rpcUrls.length})...`);
1421
+ continue;
1422
+ }
1423
+ }
1424
+ else {
1425
+ // Not a timeout or non-interactive - return error
1426
+ verificationResult = {
1427
+ success: false,
1428
+ message: `Verification error: ${errorMessage}`,
1429
+ errorDetails: {
1430
+ errorType: 'CONTRACT_ERROR',
1431
+ networkName: tokenMetadata.network
1432
+ }
1433
+ };
1434
+ break;
1435
+ }
1436
+ }
1437
+ }
1438
+ // Display results based on verification outcome
1439
+ if (verificationResult.success && verificationResult.verificationDetails) {
1440
+ // Show Merkle Root verification first
1441
+ if (verificationResult.verificationDetails.merkleRoot && verificationResult.verificationDetails.calculatedMerkleRoot) {
1442
+ const contractRoot = verificationResult.verificationDetails.merkleRoot;
1443
+ const calculatedRoot = verificationResult.verificationDetails.calculatedMerkleRoot;
1444
+ const rootsMatch = contractRoot === calculatedRoot;
1445
+ console.log(`🔐 Merkle Root: ${contractRoot}`);
1446
+ if (rootsMatch) {
1447
+ console.log(`✓ Contract Merkle Root verified against calculated Merkle Root`);
1448
+ }
1449
+ }
1450
+ // Green checkmark for successful verification
1451
+ // NOTE: All verification data (merkle root, mint date) comes from blockchain, not metadata
1452
+ console.log(`✅ ${verificationResult.message}.`);
1453
+ console.log(`The archive, tokenized as NFT #${verificationResult.verificationDetails.tokenId} on the ${verificationResult.verificationDetails.network}, was minted on ${verificationResult.verificationDetails.mintDate}.`);
1454
+ if (options.verbose && verificationResult.verificationDetails) {
1455
+ console.log(` Contract: ${verificationResult.verificationDetails.contractAddress}`);
1456
+ console.log(` Network: ${verificationResult.verificationDetails.network}`);
1457
+ if (verificationResult.verificationDetails.declaredMerkleRoot) {
1458
+ console.log(` Declared: ${verificationResult.verificationDetails.declaredMerkleRoot}`);
1459
+ }
1460
+ }
1461
+ }
1462
+ else {
1463
+ // Red X for failed verification
1464
+ console.log(`❌ ${verificationResult.message}`);
1465
+ // Always show Merkle root details for MERKLE_ERROR type
1466
+ if (verificationResult.errorDetails?.errorType === 'MERKLE_ERROR') {
1467
+ const errorDetails = verificationResult.errorDetails;
1468
+ console.log(`\n📊 Merkle Root Comparison:`);
1469
+ if (errorDetails.calculatedMerkleRoot) {
1470
+ console.log(` Calculated Merkle Root: ${errorDetails.calculatedMerkleRoot}`);
1471
+ }
1472
+ if (errorDetails.onChainMerkleRoot) {
1473
+ console.log(` On-Chain Merkle Root: ${errorDetails.onChainMerkleRoot}`);
1474
+ }
1475
+ if (errorDetails.calculatedMerkleRoot && errorDetails.onChainMerkleRoot) {
1476
+ const match = errorDetails.calculatedMerkleRoot.toLowerCase() === errorDetails.onChainMerkleRoot.toLowerCase();
1477
+ console.log(` Match: ${match ? '✓' : '✗'}`);
1478
+ }
1479
+ }
1480
+ if (options.verbose && verificationResult.errorDetails) {
1481
+ const errorDetails = verificationResult.errorDetails;
1482
+ console.log(`\n Error Type: ${errorDetails.errorType}`);
1483
+ if (errorDetails.networkName)
1484
+ console.log(` Network: ${errorDetails.networkName}`);
1485
+ if (errorDetails.rpcUrl)
1486
+ console.log(` RPC URL: ${errorDetails.rpcUrl}`);
1487
+ if (errorDetails.contractAddress)
1488
+ console.log(` Contract: ${errorDetails.contractAddress}`);
1489
+ if (errorDetails.tokenId)
1490
+ console.log(` Token ID: ${errorDetails.tokenId}`);
1491
+ if (errorDetails.merkleRoot && errorDetails.errorType !== 'MERKLE_ERROR') {
1492
+ // Only show this if we haven't already shown the detailed comparison above
1493
+ console.log(` Merkle Root: ${errorDetails.merkleRoot}`);
1494
+ }
1495
+ }
1496
+ }
1497
+ } // End of token verification if statement
1498
+ // Check for OpenTimestamp verification and upgrade
1499
+ await verifyAndUpgradeOts(zip, archiveName, options);
1500
+ }
1501
+ catch (e) {
1502
+ const msg = e instanceof Error ? e.message : String(e);
1503
+ console.log(`✗ Tokenization verification failed: ${msg}`);
1504
+ }
1505
+ }
1506
+ /**
1507
+ * Verify OpenTimestamp and prompt for upgrade if available
1508
+ */
1509
+ async function verifyAndUpgradeOts(zip, archiveName, options) {
1510
+ try {
1511
+ // Skip OTS verification if blockchain verification is disabled
1512
+ if (options.skipBlockchain) {
1513
+ console.log('OpenTimestamps verification skipped by user request');
1514
+ return;
1515
+ }
1516
+ // Check if this is an OTS timestamped file
1517
+ const entries = await getDirectory(zip, false);
1518
+ const otsEntry = entries.find((e) => (e.filename || '') === 'META-INF/TS-SUBMIT.OTS' ||
1519
+ (e.filename || '') === 'META-INF/TIMESTAMP.OTS');
1520
+ if (!otsEntry) {
1521
+ return;
1522
+ }
1523
+ const res = await (0, neozipkit_1.verifyOtsZip)(zip);
1524
+ console.log('\n🕐 OpenTimestamps Verification:');
1525
+ if (res.status === 'none') {
1526
+ console.log(' - Status: No OpenTimestamps found');
1527
+ return;
1528
+ }
1529
+ if (res.status === 'valid') {
1530
+ if (res.blockHeight && res.attestedAt) {
1531
+ const date = res.attestedAt.toLocaleDateString(undefined, {
1532
+ month: '2-digit',
1533
+ day: '2-digit',
1534
+ year: 'numeric'
1535
+ });
1536
+ const time = res.attestedAt.toLocaleTimeString(undefined, {
1537
+ hour: '2-digit',
1538
+ minute: '2-digit',
1539
+ hour12: false
1540
+ });
1541
+ console.log(` - Status: ✅ VERIFIED`);
1542
+ console.log(` - Attested: Bitcoin block ${res.blockHeight} on ${date} ${time}`);
1543
+ }
1544
+ else {
1545
+ console.log(' - Status: ✅ VERIFIED');
1546
+ }
1547
+ if (res.upgraded && res.upgradedOts) {
1548
+ let doUpgrade = false;
1549
+ // Check automation options
1550
+ if (options.otsSkipUpgrade) {
1551
+ doUpgrade = false;
1552
+ console.log(' - Upgrade: Skipped (--ots-skip-upgrade)');
1553
+ }
1554
+ else if (options.otsAutoUpgrade) {
1555
+ doUpgrade = true;
1556
+ console.log(' - Upgrade: Automatically applying (--ots-auto-upgrade)');
1557
+ }
1558
+ else {
1559
+ // Interactive mode - prompt user
1560
+ doUpgrade = await promptForOtsUpgrade(res, archiveName);
1561
+ }
1562
+ if (doUpgrade) {
1563
+ await performOtsUpgrade(archiveName, res.upgradedOts);
1564
+ }
1565
+ else if (!options.otsSkipUpgrade && !options.otsAutoUpgrade) {
1566
+ console.log(' - Note: You can upgrade later to save the improved timestamp');
1567
+ }
1568
+ }
1569
+ return;
1570
+ }
1571
+ if (res.status === 'pending') {
1572
+ console.log(' - Status: ⏳ PENDING (waiting for Bitcoin confirmation)');
1573
+ if (res.message) {
1574
+ console.log(` - Info: ${res.message}`);
1575
+ }
1576
+ return;
1577
+ }
1578
+ if (res.status === 'error') {
1579
+ console.log(' - Status: ❌ ERROR');
1580
+ if (res.message) {
1581
+ console.log(` - Error: ${res.message}`);
1582
+ }
1583
+ return;
1584
+ }
1585
+ }
1586
+ catch (error) {
1587
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1588
+ console.log('\n🕐 OpenTimestamps Verification:');
1589
+ // Check for network/communication errors
1590
+ if (errorMessage.includes('ESOCKETTIMEDOUT') ||
1591
+ errorMessage.includes('ECONNRESET') ||
1592
+ errorMessage.includes('ENOTFOUND') ||
1593
+ errorMessage.includes('ECONNREFUSED') ||
1594
+ errorMessage.includes('RequestError') ||
1595
+ errorMessage.includes('timeout') ||
1596
+ errorMessage.includes('network')) {
1597
+ console.log(' - Status: 📡 COMMUNICATION ERROR (unable to contact OpenTimestamps servers)');
1598
+ console.log(` - Error: ${errorMessage}`);
1599
+ console.log(' - Note: Check your internet connection or try again later');
1600
+ console.log(' - The archive\'s timestamp data is present but could not be verified due to connectivity issues');
1601
+ }
1602
+ else {
1603
+ console.log(' - Status: ❌ ERROR during verification');
1604
+ console.log(` - Error: ${errorMessage}`);
1605
+ }
1606
+ }
1607
+ }
1608
+ /**
1609
+ * Parse command line arguments
1610
+ */
1611
+ function parseArgs(args) {
1612
+ const options = {
1613
+ dest: '.',
1614
+ test: false,
1615
+ skipBlockchain: false,
1616
+ verbose: false,
1617
+ quiet: false,
1618
+ overwrite: false,
1619
+ testOnly: false,
1620
+ bufferSize: 512 * 1024, // 512KB default (optimal for modern systems)
1621
+ enableProgress: true,
1622
+ junkPaths: false,
1623
+ never: false
1624
+ };
1625
+ let archive = '';
1626
+ let destination = '.';
1627
+ let filePatterns = [];
1628
+ for (let i = 0; i < args.length; i++) {
1629
+ const arg = args[i];
1630
+ switch (arg) {
1631
+ case '-h':
1632
+ showHelp();
1633
+ process.exit(0);
1634
+ break;
1635
+ case '-h2':
1636
+ case '--help-extended':
1637
+ showExtendedHelp();
1638
+ process.exit(0);
1639
+ break;
1640
+ case '--help':
1641
+ showHelp();
1642
+ process.exit(0);
1643
+ break;
1644
+ case '-V':
1645
+ case '--version':
1646
+ console.log(`NeoUnZip Version: ${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})`);
1647
+ process.exit(0);
1648
+ break;
1649
+ case '-v':
1650
+ case '--verbose':
1651
+ options.verbose = true;
1652
+ break;
1653
+ case '-q':
1654
+ case '--quiet':
1655
+ options.quiet = true;
1656
+ break;
1657
+ case '-o':
1658
+ case '--overwrite':
1659
+ options.overwrite = true;
1660
+ break;
1661
+ case '-f':
1662
+ options.freshenOnly = true;
1663
+ break;
1664
+ case '-u':
1665
+ options.updateOnly = true;
1666
+ break;
1667
+ case '-z':
1668
+ options.commentOnly = true;
1669
+ break;
1670
+ case '-t':
1671
+ case '--test':
1672
+ if (options.preVerify) {
1673
+ console.error('Error: -t/--test and -T/--pre-verify cannot be used together');
1674
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1675
+ }
1676
+ options.testOnly = true;
1677
+ break;
1678
+ case '-T':
1679
+ case '--pre-verify':
1680
+ if (options.testOnly) {
1681
+ console.error('Error: -t/--test and -T/--pre-verify cannot be used together');
1682
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1683
+ }
1684
+ options.preVerify = true;
1685
+ break;
1686
+ case '-l':
1687
+ case '--list':
1688
+ options.listOnly = true;
1689
+ break;
1690
+ case '--skip-blockchain':
1691
+ options.skipBlockchain = true;
1692
+ break;
1693
+ case '--buffer-size':
1694
+ const bufferSize = parseInt(args[++i], 10);
1695
+ if (isNaN(bufferSize) || bufferSize < 1024) {
1696
+ console.error('Error: Buffer size must be at least 1024 bytes');
1697
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1698
+ }
1699
+ options.bufferSize = bufferSize;
1700
+ break;
1701
+ case '--no-progress':
1702
+ options.enableProgress = false;
1703
+ break;
1704
+ case '--progress':
1705
+ options.progress = true;
1706
+ options.enableProgress = true;
1707
+ break;
1708
+ case '--debug':
1709
+ options.debug = true;
1710
+ break;
1711
+ case '-sf':
1712
+ case '--show-files':
1713
+ options.showFiles = true;
1714
+ break;
1715
+ case '-p':
1716
+ options.pipeToStdout = true;
1717
+ break;
1718
+ case '-P':
1719
+ case '--password':
1720
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1721
+ // Password provided directly
1722
+ options.password = args[++i];
1723
+ }
1724
+ else {
1725
+ // No password provided, will prompt later
1726
+ options.askPassword = true;
1727
+ }
1728
+ break;
1729
+ case '-x':
1730
+ case '--exclude':
1731
+ if (!options.exclude)
1732
+ options.exclude = [];
1733
+ options.exclude.push(args[++i]);
1734
+ break;
1735
+ case '-i':
1736
+ case '--include':
1737
+ if (!options.include)
1738
+ options.include = [];
1739
+ options.include.push(args[++i]);
1740
+ break;
1741
+ case '-j':
1742
+ case '--junk-paths':
1743
+ options.junkPaths = true;
1744
+ break;
1745
+ case '-n':
1746
+ case '--never':
1747
+ options.never = true;
1748
+ break;
1749
+ case '-d':
1750
+ case '--exdir':
1751
+ options.exdir = args[++i];
1752
+ if (!options.exdir) {
1753
+ console.error('Error: --exdir requires a directory path');
1754
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1755
+ }
1756
+ break;
1757
+ case '-X':
1758
+ case '--preserve-perms':
1759
+ options.preservePerms = true;
1760
+ break;
1761
+ case '-y':
1762
+ case '--symlinks':
1763
+ options.symlinks = true;
1764
+ break;
1765
+ case '-H':
1766
+ case '--hard-links':
1767
+ options.hardLinks = true;
1768
+ break;
1769
+ case '--ots-auto-upgrade':
1770
+ options.otsAutoUpgrade = true;
1771
+ break;
1772
+ case '--ots-skip-upgrade':
1773
+ options.otsSkipUpgrade = true;
1774
+ break;
1775
+ case '--in-memory':
1776
+ options.inMemory = true;
1777
+ break;
1778
+ case '--non-interactive':
1779
+ options.nonInteractive = true;
1780
+ break;
1781
+ default:
1782
+ if (arg.startsWith('-')) {
1783
+ console.error(`Error: Unknown option ${arg}`);
1784
+ showHelp();
1785
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1786
+ }
1787
+ else if (!archive) {
1788
+ archive = arg;
1789
+ }
1790
+ else if (destination === '.' && !options.pipeToStdout) {
1791
+ destination = arg;
1792
+ }
1793
+ else {
1794
+ // Additional arguments are file patterns for extraction/pipe
1795
+ filePatterns.push(arg);
1796
+ }
1797
+ break;
1798
+ }
1799
+ }
1800
+ if (!archive) {
1801
+ console.error('Error: Archive name is required');
1802
+ showHelp();
1803
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1804
+ }
1805
+ if (!fs.existsSync(archive)) {
1806
+ console.error(`Error: Archive not found: ${archive}`);
1807
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.FILES_NOT_FOUND);
1808
+ }
1809
+ // Verify archive is a file, not a directory
1810
+ const stats = fs.statSync(archive);
1811
+ if (!stats.isFile()) {
1812
+ console.error(`Error: Archive path is not a file: ${archive}`);
1813
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.FILES_NOT_FOUND);
1814
+ }
1815
+ return { archive, destination, options, filePatterns };
1816
+ }
1817
+ /**
1818
+ * Show help information
1819
+ */
1820
+ function showHelp() {
1821
+ console.log(`
1822
+ NeoUnZip Version: ${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})
1823
+
1824
+ Usage: neounzip [options] <archive> [destination]
1825
+
1826
+ Options:
1827
+ -h, --help Show this help message
1828
+ -V, --version Show version information
1829
+ -v, --verbose Verbose output
1830
+ -q, --quiet Suppress output
1831
+ -o, --overwrite Overwrite existing files when extracting
1832
+ -f Freshen existing files, create none
1833
+ -u Update files, create if necessary
1834
+ -t, --test Test compressed data without extracting
1835
+ -T, --pre-verify Pre-verify integrity and blockchain before extraction (abort on failure)
1836
+ -z Display archive comment only
1837
+ -l, --list List archive contents without extracting
1838
+ -p Extract files to pipe (stdout), no messages
1839
+ --skip-blockchain Skip all blockchain verification (token and OTS)
1840
+ --non-interactive Skip interactive prompts (exit on timeout)
1841
+ --buffer-size <bytes> Buffer size for streaming (default: 65536)
1842
+ --no-progress Disable progress reporting
1843
+ --progress Enable enhanced progress reporting
1844
+ --debug Enable debug output
1845
+ -sf, --show-files Show files as they are extracted
1846
+ -P, --password [pwd] Password for encrypted files. Provide password or will prompt
1847
+ -x, --exclude <pattern> Exclude files matching pattern (can be used multiple times)
1848
+ -i, --include <pattern> Include only files matching pattern (can be used multiple times)
1849
+ -j, --junk-paths Extract files without directory structure (flatten to destination)
1850
+ -n, --never Never overwrite existing files when extracting
1851
+ -d, --exdir <dir> Extract files to specified directory
1852
+ -X, --preserve-perms Restore file permissions and ownership (UID/GID)
1853
+ -y, --symlinks Restore symbolic links as links (don't follow them)
1854
+ -H, --hard-links Restore hard links efficiently (recreate link relationships)
1855
+ --ots-auto-upgrade Automatically upgrade OTS timestamps when available
1856
+ --ots-skip-upgrade Skip OTS timestamp upgrades (no prompt)
1857
+ --in-memory Force in-memory processing mode (browser compatible)
1858
+
1859
+ Arguments:
1860
+ <archive> ZIP file to extract or test
1861
+ [destination] Output directory (default: current directory)
1862
+
1863
+ Examples:
1864
+ neounzip output/calgary.nzip ./extracted
1865
+ neounzip -t output/calgary.nzip
1866
+ neounzip -l output/calgary.nzip
1867
+ `);
1868
+ }
1869
+ /**
1870
+ * Show extended help information (equivalent to InfoZip's -h2)
1871
+ */
1872
+ function showExtendedHelp() {
1873
+ console.log('Extended Help for NeoUnzip');
1874
+ console.log('');
1875
+ console.log('NeoUnzip is a next-generation ZIP extraction utility that builds upon the strengths');
1876
+ console.log('of the ZIP format while incorporating blockchain verification and modern features.');
1877
+ console.log('');
1878
+ console.log('Basic command line:');
1879
+ console.log(' neounzip [options] archive [destination]');
1880
+ console.log('');
1881
+ console.log('Some examples:');
1882
+ console.log(' Extract all files to current directory: neounzip archive.nzip');
1883
+ console.log(' Extract to specific directory: neounzip archive.nzip ./extracted');
1884
+ console.log(' Extract specific files: neounzip archive.nzip file1.txt file2.txt');
1885
+ console.log(' Test archive integrity: neounzip -t archive.nzip');
1886
+ console.log('');
1887
+ console.log('Basic modes:');
1888
+ console.log(' -t test archive integrity (no extraction)');
1889
+ console.log(' -T pre-verify integrity and blockchain before extraction (abort on failure)');
1890
+ console.log(' -l list archive contents (no extraction)');
1891
+ console.log(' -v verbose listing with details');
1892
+ console.log(' -j junk directory names (extract to flat structure)');
1893
+ console.log('');
1894
+ console.log('Basic options:');
1895
+ console.log(' -q quiet operation');
1896
+ console.log(' -v verbose operation');
1897
+ console.log(' -o overwrite files without prompting');
1898
+ console.log(' -n never overwrite existing files');
1899
+ console.log(' -j junk directory names (extract to flat structure)');
1900
+ console.log(' -x exclude files matching patterns');
1901
+ console.log(' -i include only files matching patterns');
1902
+ console.log('');
1903
+ console.log('Syntax:');
1904
+ console.log(' The full command line syntax is:');
1905
+ console.log('');
1906
+ console.log(' neounzip [options] archive [destination] [file_patterns...]');
1907
+ console.log('');
1908
+ console.log(' Archive is the ZIP file to extract from');
1909
+ console.log(' Destination is the directory to extract to (default: current directory)');
1910
+ console.log(' File patterns specify which files to extract (default: all files)');
1911
+ console.log('');
1912
+ console.log('Options and Values:');
1913
+ console.log(' For short options that take values, use -ovalue or -o value or -o=value');
1914
+ console.log(' For long option values, use either --longoption=value or --longoption value');
1915
+ console.log(' For example:');
1916
+ console.log(' neounzip --password=mypass --exclude "*.tmp" archive.nzip ./extracted');
1917
+ console.log('');
1918
+ console.log('File Patterns:');
1919
+ console.log(' NeoUnzip supports the following patterns:');
1920
+ console.log(' ? matches any single character');
1921
+ console.log(' * matches any number of characters, including zero');
1922
+ console.log(' [list] matches char in list (regex), can do range [a-f], all but [!bf]');
1923
+ console.log(' Examples:');
1924
+ console.log(' neounzip archive.nzip "*.txt" # Extract only .txt files');
1925
+ console.log(' neounzip archive.nzip "test[0-9].*" # Extract test0.txt, test1.txt, etc.');
1926
+ console.log('');
1927
+ console.log('Include and Exclude:');
1928
+ console.log(' -i pattern pattern ... include only files that match a pattern');
1929
+ console.log(' -x pattern pattern ... exclude files that match a pattern');
1930
+ console.log(' Patterns are paths with optional wildcards and match paths as stored in');
1931
+ console.log(' archive. Exclude and include lists end at next option or end of line.');
1932
+ console.log('');
1933
+ console.log('End Of Line Translation (text files only):');
1934
+ console.log(' -a auto-convert text files (convert line endings)');
1935
+ console.log(' -aa convert all files (including binary)');
1936
+ console.log(' -j junk directory names (extract to flat structure)');
1937
+ console.log(' If first buffer read from file contains binary the translation is skipped');
1938
+ console.log('');
1939
+ console.log('File Handling:');
1940
+ console.log(' -o overwrite files without prompting');
1941
+ console.log(' -n never overwrite existing files');
1942
+ console.log(' -f freshen existing files (extract only if newer)');
1943
+ console.log(' -u update existing files (extract if newer or missing)');
1944
+ console.log('');
1945
+ console.log('Symbolic Links and Hard Links:');
1946
+ console.log(' -y restore symbolic links (don\'t follow them)');
1947
+ console.log(' -H restore hard links efficiently');
1948
+ console.log('');
1949
+ console.log('File Permissions:');
1950
+ console.log(' -X restore file permissions and ownership (UID/GID)');
1951
+ console.log(' -P restore file permissions only (not ownership)');
1952
+ console.log('');
1953
+ console.log('Encryption:');
1954
+ console.log(' --password <pass> use password for encrypted archives');
1955
+ console.log(' --pkzip-decrypt use legacy PKZip 2.0 decryption');
1956
+ console.log('');
1957
+ console.log('Blockchain Features:');
1958
+ console.log(' --skip-blockchain disable blockchain verification');
1959
+ console.log(' --non-interactive skip interactive prompts (exit on timeout)');
1960
+ console.log(' --network <net> specify blockchain network (base-sepolia, ethereum, etc.)');
1961
+ console.log(' --wallet <addr> specify wallet address for verification');
1962
+ console.log(' --ots-verify verify OTS (OpenTimestamps) signatures');
1963
+ console.log(' --ots-skip skip OTS verification');
1964
+ console.log('');
1965
+ console.log('Testing and Listing:');
1966
+ console.log(' -t test archive integrity (no extraction)');
1967
+ console.log(' -T pre-verify integrity and blockchain before extraction (abort on failure)');
1968
+ console.log(' -l list archive contents (no extraction)');
1969
+ console.log(' -v verbose listing with details');
1970
+ console.log(' --json output in JSON format');
1971
+ console.log(' --debug show debug information');
1972
+ console.log('');
1973
+ console.log('Progress and Verbosity:');
1974
+ console.log(' -v verbose operation');
1975
+ console.log(' -q quiet operation');
1976
+ console.log(' --progress show progress bar');
1977
+ console.log(' --debug show debug information');
1978
+ console.log('');
1979
+ console.log('More option highlights:');
1980
+ console.log(' --buffer-size <size> set extraction buffer size');
1981
+ console.log(' --show-files show files to be extracted and exit');
1982
+ console.log(' --dry-run show what would be extracted without actually extracting');
1983
+ console.log('');
1984
+ console.log('For more information, visit: https://github.com/NeoWareInc/neozip-cli');
1985
+ console.log('For detailed API documentation, see: https://docs.neoware.io/neozip');
1986
+ }
1987
+ /**
1988
+ * Main execution function
1989
+ */
1990
+ async function main() {
1991
+ // Declare variables outside try block for cleanup in catch
1992
+ let zip = null;
1993
+ let options = null;
1994
+ try {
1995
+ // Parse command line arguments
1996
+ const args = process.argv.slice(2);
1997
+ const parsed = parseArgs(args);
1998
+ const { archive, destination, filePatterns } = parsed;
1999
+ options = parsed.options;
2000
+ // Use exdir option if provided, otherwise use destination
2001
+ const extractDestination = options.exdir || destination;
2002
+ // Load the ZIP file
2003
+ zip = new node_1.default();
2004
+ if (options.inMemory) {
2005
+ // Use Buffer-based loading (browser compatible)
2006
+ const data = fs.readFileSync(archive);
2007
+ zip.loadZip(data);
2008
+ }
2009
+ else {
2010
+ // Use file-based loading (default)
2011
+ await zip.loadZipFile(archive);
2012
+ }
2013
+ // Handle password prompting for decryption
2014
+ if (options.askPassword) {
2015
+ log('🔐 Encrypted archive detected - password required', options);
2016
+ options.password = await promptPassword('Enter password: ');
2017
+ if (!options.password) {
2018
+ logError('❌ Password is required for decryption');
2019
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.BAD_PASSWORD);
2020
+ }
2021
+ log('✅ Password set for decryption', options);
2022
+ }
2023
+ // Display streaming information (suppress in pipe and comment modes)
2024
+ if (!options.pipeToStdout && !options.commentOnly) {
2025
+ log(`🚀 NEOUNZIP v${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})`, options);
2026
+ log(`📦 Archive: ${archive}`, options);
2027
+ log(`📁 Destination: ${extractDestination}`, options);
2028
+ if (options.debug) {
2029
+ // Set global Logger level to 'debug' for debug output
2030
+ // Note: Individual class logging is controlled by their loggingEnabled static property
2031
+ neozipkit_1.Logger.setLevel('debug');
2032
+ logDebug('Debug mode enabled', options);
2033
+ logDebug(`Command line arguments: ${process.argv.join(' ')}`, options);
2034
+ logDebug(`Working directory: ${process.cwd()}`, options);
2035
+ logDebug(`Node.js version: ${process.version}`, options);
2036
+ logDebug(`Platform: ${process.platform} ${process.arch}`, options);
2037
+ logDebug(`Archive file: ${archive}`, options);
2038
+ logDebug(`Extract destination: ${extractDestination}`, options);
2039
+ log('', options);
2040
+ }
2041
+ }
2042
+ if (options.testOnly) {
2043
+ const testResult = await testArchive(zip, options);
2044
+ // Always verify tokenization info after testing so users see results at the end
2045
+ // Pass pre-calculated merkle root to avoid double extraction (single pass)
2046
+ await verifyTokenization(zip, archive, options, testResult.extractionResult);
2047
+ process.exit(testResult.failures === 0 ? exit_codes_1.UNZIP_EXIT_CODES.SUCCESS : exit_codes_1.UNZIP_EXIT_CODES.WARNING);
2048
+ }
2049
+ else if (options.listOnly) {
2050
+ await listArchive(zip, archive, options);
2051
+ // Show tokenization info after listing (no pre-calculated result, will extract once)
2052
+ await verifyTokenization(zip, archive, options);
2053
+ }
2054
+ else if (options.commentOnly) {
2055
+ // Comment mode - show only archive comment, no other output
2056
+ await showCommentOnly(zip, archive, options);
2057
+ // Exit without showing tokenization info
2058
+ return;
2059
+ }
2060
+ else if (options.pipeToStdout) {
2061
+ // Pipe mode - extract to stdout, no messages, no tokenization info
2062
+ await pipeArchive(zip, options, filePatterns.length > 0 ? filePatterns : undefined);
2063
+ // Exit without showing tokenization info
2064
+ return;
2065
+ }
2066
+ else {
2067
+ // Pre-verify before extraction if requested
2068
+ if (options.preVerify) {
2069
+ const preVerifyResult = await preVerifyArchive(zip, archive, options);
2070
+ if (!preVerifyResult.success) {
2071
+ // Determine appropriate exit code based on failure type
2072
+ if (!preVerifyResult.integrityPassed) {
2073
+ // Integrity check failed (CRC/SHA-256)
2074
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.WARNING, `Pre-verification failed: ${preVerifyResult.error || 'Integrity check failed'}`);
2075
+ }
2076
+ else if (!preVerifyResult.blockchainPassed) {
2077
+ // Blockchain verification failed
2078
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR, `Pre-verification failed: ${preVerifyResult.error || 'Blockchain verification failed'}`);
2079
+ }
2080
+ else {
2081
+ // Unknown failure
2082
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR, `Pre-verification failed: ${preVerifyResult.error || 'Unknown error'}`);
2083
+ }
2084
+ }
2085
+ // Pre-verification passed - proceed with extraction
2086
+ console.log('\n✅ Pre-verification passed - proceeding with extraction...\n');
2087
+ }
2088
+ const extractionResult = await extractArchive(zip, extractDestination, options);
2089
+ // Show tokenization info after extraction (pass pre-calculated merkle root to avoid double extraction)
2090
+ // Skip blockchain verification if we already did it during pre-verify
2091
+ if (!options.preVerify) {
2092
+ await verifyTokenization(zip, archive, options, extractionResult);
2093
+ }
2094
+ }
2095
+ log('✓ NeoUnZip completed successfully', options);
2096
+ // Close file handle if file-based ZIP was loaded
2097
+ if (zip && !options.inMemory && isFileBased(zip)) {
2098
+ try {
2099
+ await zip.closeFile();
2100
+ }
2101
+ catch (closeError) {
2102
+ // Ignore cleanup errors - file may already be closed
2103
+ if (options.debug) {
2104
+ logDebug(`Note: Error closing file handle: ${closeError instanceof Error ? closeError.message : String(closeError)}`, options);
2105
+ }
2106
+ }
2107
+ }
2108
+ // Force process exit to avoid waiting for event loop
2109
+ // This prevents delays from lingering async operations (e.g., blockchain RPC connections)
2110
+ process.exit(0);
2111
+ }
2112
+ catch (error) {
2113
+ // Close file handle if file-based ZIP was loaded (cleanup on error)
2114
+ if (zip && options && !options.inMemory && isFileBased(zip)) {
2115
+ try {
2116
+ await zip.closeFile();
2117
+ }
2118
+ catch (closeError) {
2119
+ // Ignore cleanup errors
2120
+ }
2121
+ }
2122
+ const errorMessage = error instanceof Error ? error.message : String(error);
2123
+ logError(`Error: ${errorMessage}`);
2124
+ // Determine appropriate exit code based on error type
2125
+ if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
2126
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.FILES_NOT_FOUND, `Error: ${errorMessage}`);
2127
+ }
2128
+ else if (errorMessage.includes('password') || errorMessage.includes('decrypt')) {
2129
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.BAD_PASSWORD, `Error: ${errorMessage}`);
2130
+ }
2131
+ else if (errorMessage.includes('compression') || errorMessage.includes('unsupported')) {
2132
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.UNSUPPORTED_COMPRESSION, `Error: ${errorMessage}`);
2133
+ }
2134
+ else if (errorMessage.includes('ENOSPC') || errorMessage.includes('disk full')) {
2135
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.DISK_FULL, `Error: ${errorMessage}`);
2136
+ }
2137
+ else if (errorMessage.includes('unexpected end') || errorMessage.includes('truncated')) {
2138
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.UNEXPECTED_END, `Error: ${errorMessage}`);
2139
+ }
2140
+ else if (errorMessage.includes('ZIP format') || errorMessage.includes('corrupt')) {
2141
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.SEVERE_ZIP_ERROR, `Error: ${errorMessage}`);
2142
+ }
2143
+ else if (errorMessage.includes('memory') || errorMessage.includes('allocation')) {
2144
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.MEMORY_ERROR, `Error: ${errorMessage}`);
2145
+ }
2146
+ else {
2147
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR, `Error: ${errorMessage}`);
2148
+ }
2149
+ }
2150
+ }
2151
+ // Always run main when this module is loaded
2152
+ // This allows it to work both when called directly and when imported by index.js
2153
+ main().catch(err => {
2154
+ console.error('Fatal error:', err);
2155
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR);
2156
+ });
2157
+ //# sourceMappingURL=neounzip.js.map