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