stegdoc 5.7.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,44 +3,146 @@ const fs = require('fs');
3
3
  const { pipeline } = require('stream/promises');
4
4
  const chalk = require('chalk');
5
5
  const ora = require('ora');
6
- const AdmZip = require('adm-zip');
7
- const { createDocxWithBase64, createDocxV5 } = require('../lib/docx-handler');
6
+ const { createDocxV5 } = require('../lib/docx-handler');
8
7
  const { createXlsxPartV5 } = require('../lib/xlsx-handler');
9
8
  const { createMetadata, serializeMetadata } = require('../lib/metadata');
10
9
  const crypto = require('crypto');
11
10
  const { generateHash, parseSizeToBytes, formatBytes, generateFilename } = require('../lib/utils');
12
11
  const { packEncryptionMeta, generateSalt, createEncryptStream } = require('../lib/crypto');
13
- const { isCompressedMime, createCompressStream, createBrotliCompressStream } = require('../lib/compression');
12
+ const { createBrotliCompressStream } = require('../lib/compression');
14
13
  const { resetTimeWindow } = require('../lib/decoy-generator');
15
14
  const { resetTimeState, BYTES_PER_DATA_LINE, calculateDataLineCount } = require('../lib/log-generator');
16
15
  const { shouldRunInteractive, promptEncodeOptions } = require('../lib/interactive');
17
- const { Base64EncodeTransform, ChunkCollector, BinaryChunkCollector, ProgressTransform } = require('../lib/streams');
16
+ const { BinaryChunkCollector, ProgressTransform } = require('../lib/streams');
17
+ const { loadNative } = require('../lib/native');
18
18
 
19
19
  /**
20
- * Zip a folder into a buffer
20
+ * Load the native engine, failing loudly when it is unavailable. Bundling has
21
+ * no JavaScript implementation any more: the archive is built by the core.
22
+ *
23
+ * @returns {object} The native binding.
24
+ */
25
+ function requireNative() {
26
+ const native = loadNative();
27
+ if (!native || typeof native.zip !== 'function') {
28
+ throw new Error(
29
+ 'Bundling inputs requires the native engine, which is not installed. ' +
30
+ 'Reinstall stegdoc, or build it with `pnpm build:native`.'
31
+ );
32
+ }
33
+ return native;
34
+ }
35
+
36
+ /**
37
+ * Recursively collect a directory's files as archive entries under `prefix`.
38
+ *
39
+ * @param {string} dirPath Directory to walk.
40
+ * @param {string} prefix Forward-slash entry path prefix.
41
+ * @param {Array<{name: string, bytes: Buffer}>} entries Accumulator.
42
+ */
43
+ function collectDirectory(dirPath, prefix, entries) {
44
+ for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) {
45
+ const full = path.join(dirPath, dirent.name);
46
+ const name = `${prefix}/${dirent.name}`;
47
+ if (dirent.isDirectory()) {
48
+ collectDirectory(full, name, entries);
49
+ } else {
50
+ entries.push({ name, bytes: fs.readFileSync(full) });
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Zip a folder into a buffer, keeping the folder's basename as a prefix.
57
+ *
58
+ * @param {string} folderPath Folder to archive.
59
+ * @returns {Buffer} The archive bytes.
21
60
  */
22
61
  function zipFolder(folderPath) {
23
- const zip = new AdmZip();
24
- zip.addLocalFolder(folderPath);
25
- return zip.toBuffer();
62
+ const native = requireNative();
63
+ const entries = [];
64
+ collectDirectory(folderPath, path.basename(folderPath), entries);
65
+ return native.zip(entries);
26
66
  }
27
67
 
28
68
  /**
29
- * Detect file type from the first 4KB
69
+ * Pick an archive entry name that is not taken yet, disambiguating inputs that
70
+ * share a basename by inserting a counter before the extension.
71
+ *
72
+ * @param {string} name Preferred entry name.
73
+ * @param {Set<string>} used Names already placed in the archive.
74
+ * @returns {string} A free entry name, which is added to `used`.
30
75
  */
31
- async function detectFileType(filePath) {
32
- try {
33
- const fileType = await import('file-type');
34
- const fromBuffer = fileType.fileTypeFromBuffer || fileType.default?.fromBuffer;
35
- if (!fromBuffer) return null;
36
- const fd = await fs.promises.open(filePath, 'r');
37
- const buf = Buffer.alloc(4100);
38
- await fd.read(buf, 0, 4100, 0);
39
- await fd.close();
40
- return await fromBuffer(buf);
41
- } catch {
42
- return null;
76
+ function uniqueEntryName(name, used) {
77
+ if (!used.has(name)) {
78
+ used.add(name);
79
+ return name;
80
+ }
81
+ const extension = path.extname(name);
82
+ const stem = name.slice(0, name.length - extension.length);
83
+ let counter = 2;
84
+ let candidate = `${stem}-${counter}${extension}`;
85
+ while (used.has(candidate)) {
86
+ counter += 1;
87
+ candidate = `${stem}-${counter}${extension}`;
88
+ }
89
+ used.add(candidate);
90
+ return candidate;
91
+ }
92
+
93
+ /**
94
+ * Zip several inputs into one buffer. Files land at the archive root under
95
+ * their basename; a folder keeps its own name as a prefix.
96
+ *
97
+ * @param {string[]} inputs Absolute or relative paths to files and folders.
98
+ * @returns {Buffer} The archive bytes.
99
+ */
100
+ function zipInputs(inputs) {
101
+ const native = requireNative();
102
+ const entries = [];
103
+ const used = new Set();
104
+ for (const input of inputs) {
105
+ const base = path.basename(input.replace(/[\\/]+$/, ''));
106
+ const entry = uniqueEntryName(base, used);
107
+ if (fs.statSync(input).isDirectory()) {
108
+ collectDirectory(input, entry, entries);
109
+ } else {
110
+ entries.push({ name: entry, bytes: fs.readFileSync(input) });
111
+ }
43
112
  }
113
+ return native.zip(entries);
114
+ }
115
+
116
+ /**
117
+ * Normalise a bundle name: no path segments, always a `.zip` extension.
118
+ *
119
+ * @param {string} name Requested name, possibly empty.
120
+ * @returns {string} The filename recorded in metadata for the bundle.
121
+ */
122
+ function bundleFilename(name) {
123
+ const base = path.basename((name || '').trim());
124
+ if (!base || base === '.' || base === '..') return 'bundle.zip';
125
+ return base.toLowerCase().endsWith('.zip') ? base : `${base}.zip`;
126
+ }
127
+
128
+ /**
129
+ * Extensions whose contents are already compressed, so Brotli only wastes CPU.
130
+ */
131
+ const COMPRESSED_EXTENSIONS = new Set([
132
+ 'zip', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar',
133
+ 'jpg', 'jpeg', 'png', 'gif', 'webp',
134
+ 'mp3', 'mp4', 'm4a', 'mov', 'avi', 'mkv', 'webm', 'ogg', 'opus', 'flac',
135
+ 'pdf', 'apk', 'jar', 'docx', 'xlsx', 'pptx', 'wasm', 'br',
136
+ ]);
137
+
138
+ /**
139
+ * Check whether an extension is already compressed.
140
+ *
141
+ * @param {string} ext Extension, with or without a leading dot.
142
+ * @returns {boolean} True when the format needs no further compression.
143
+ */
144
+ function isCompressedExtension(ext) {
145
+ return COMPRESSED_EXTENSIONS.has(String(ext || '').toLowerCase().replace(/^\./, ''));
44
146
  }
45
147
 
46
148
  /**
@@ -57,12 +159,24 @@ async function computeFileHash(filePath) {
57
159
  }
58
160
 
59
161
  /**
60
- * Encode a file to XLSX/DOCX format with optional AES encryption and compression.
162
+ * Encode one or more inputs to XLSX/DOCX format with optional AES encryption
163
+ * and compression. Several inputs, or a single folder, are zipped first and the
164
+ * archive is what gets encoded, so a decode hands back that zip.
165
+ *
166
+ * @param {string|string[]} input One path, or several to bundle together.
167
+ * @param {object} options Commander options for the encode command.
61
168
  */
62
- async function encodeCommand(inputFile, options) {
169
+ async function encodeCommand(input, options) {
170
+ const inputs = Array.isArray(input) ? input : [input];
171
+ if (inputs.length === 0) {
172
+ throw new Error('No input given.');
173
+ }
174
+ const bundling = inputs.length > 1;
175
+ const bundleName = bundleFilename(options.bundleName);
176
+
63
177
  // Check if we should run interactive mode
64
178
  if (shouldRunInteractive(options, 'encode')) {
65
- const filename = path.basename(inputFile);
179
+ const filename = bundling ? bundleName : path.basename(inputs[0]);
66
180
  console.log(chalk.bold(`\nEncoding: ${filename}`));
67
181
 
68
182
  const interactiveOptions = await promptEncodeOptions(filename);
@@ -71,7 +185,10 @@ async function encodeCommand(inputFile, options) {
71
185
  }
72
186
 
73
187
  const quiet = options.quiet || false;
74
- const legacy = options.legacy || false;
188
+ if (options.v5 && options.v6) {
189
+ throw new Error('--v5 and --v6 cannot be combined.');
190
+ }
191
+ const version = options.v5 ? 'v5' : 'v6';
75
192
  const spinner = quiet ? { start: () => {}, succeed: () => {}, fail: () => {}, info: () => {}, text: '' } : ora('Starting encoding process...').start();
76
193
  const createdFiles = [];
77
194
 
@@ -80,11 +197,14 @@ async function encodeCommand(inputFile, options) {
80
197
  resetTimeState();
81
198
 
82
199
  try {
83
- if (!fs.existsSync(inputFile)) {
84
- throw new Error(`Path not found: ${inputFile}`);
200
+ for (const candidate of inputs) {
201
+ if (!fs.existsSync(candidate)) {
202
+ throw new Error(`Path not found: ${candidate}`);
203
+ }
85
204
  }
86
205
 
87
- const isDirectory = fs.statSync(inputFile).isDirectory();
206
+ const inputFile = inputs[0];
207
+ const isDirectory = !bundling && fs.statSync(inputFile).isDirectory();
88
208
  const format = (options.format || 'xlsx').toLowerCase();
89
209
  if (format !== 'xlsx' && format !== 'docx') {
90
210
  throw new Error('Invalid format. Use "xlsx" or "docx".');
@@ -99,7 +219,23 @@ async function encodeCommand(inputFile, options) {
99
219
  let fileSize;
100
220
  let tempZipPath = null;
101
221
 
102
- if (isDirectory) {
222
+ if (bundling) {
223
+ // One archive keeps the pipeline single-payload: a decode hands back the
224
+ // zip, so nothing downstream has to know about multiple inputs.
225
+ spinner.text = `Bundling ${inputs.length} inputs...`;
226
+ const zipBuffer = zipInputs(inputs);
227
+ filename = bundleName;
228
+ extension = '.zip';
229
+ fileSize = zipBuffer.length;
230
+
231
+ tempZipPath = path.join(require('os').tmpdir(), `stegdoc_${Date.now()}.zip`);
232
+ fs.writeFileSync(tempZipPath, zipBuffer);
233
+ streamSource = tempZipPath;
234
+
235
+ spinner.succeed && spinner.succeed(
236
+ `Bundled ${inputs.length} inputs into ${filename} (${formatBytes(fileSize)})`
237
+ );
238
+ } else if (isDirectory) {
103
239
  spinner.text = 'Zipping folder...';
104
240
  const folderName = path.basename(inputFile);
105
241
  const zipBuffer = zipFolder(inputFile);
@@ -122,34 +258,23 @@ async function encodeCommand(inputFile, options) {
122
258
 
123
259
  spinner.text = 'Checking file type...';
124
260
  let useCompression = true;
125
- const fileType = await detectFileType(streamSource);
261
+ const extensionKey = extension.replace(/^\./, '').toLowerCase();
126
262
 
127
- if (fileType && isCompressedMime(fileType.mime)) {
263
+ if (isCompressedExtension(extensionKey)) {
128
264
  useCompression = false;
129
- spinner.info && spinner.info(`Skipping compression (${fileType.ext} is already compressed)`);
265
+ spinner.info && spinner.info(`Skipping compression (${extensionKey} is already compressed)`);
130
266
  }
131
267
 
132
- // DOCX v5 size limit (not applicable in legacy mode or with --no-limit)
268
+ // DOCX v5 size limit (not applicable with --no-limit)
133
269
  const noLimit = options.noLimit || options.limit === false;
134
- if (format === 'docx' && !legacy && !noLimit && fileSize > 1 * 1024 * 1024) {
270
+ if (format === 'docx' && !noLimit && fileSize > 1 * 1024 * 1024) {
135
271
  throw new Error(
136
272
  `DOCX format is limited to files under 1 MB (yours is ${formatBytes(fileSize)}). ` +
137
273
  `Use XLSX format (-f xlsx) for larger files, or --no-limit to bypass.`
138
274
  );
139
275
  }
140
276
 
141
- // Route to legacy or v5 pipeline
142
- if (legacy) {
143
- if (format === 'docx') {
144
- await encodeLegacyDocx(streamSource, filename, extension, fileSize, options, useCompression, useEncryption, spinner, quiet, createdFiles);
145
- } else {
146
- throw new Error('Legacy XLSX format (--legacy) is no longer supported. Use v5 format (default) or legacy DOCX (-f docx --legacy).');
147
- }
148
- if (tempZipPath) cleanupTemp(tempZipPath);
149
- return;
150
- }
151
-
152
- // === v5 Log-Embed Pipeline ===
277
+ // === Log-Embed Pipeline ===
153
278
  const hash = generateHash();
154
279
  const outputDir = options.outputDir || process.cwd();
155
280
 
@@ -175,6 +300,49 @@ async function encodeCommand(inputFile, options) {
175
300
 
176
301
  // Pre-compute content hash
177
302
  spinner.text = 'Computing file hash...';
303
+
304
+ // Prefer the native engine; the JavaScript encoder only covers v5.
305
+ const native = loadNative();
306
+ if (version === 'v6' && !native) {
307
+ throw new Error(
308
+ 'v6 output requires the native engine (run `pnpm build:native` or install the platform package). Use --v5 for the JavaScript encoder.'
309
+ );
310
+ }
311
+ if (native) {
312
+ spinner.text = 'Encoding...';
313
+ const result = native.encode(streamSource, outputDir, {
314
+ format,
315
+ version,
316
+ password: options.password || undefined,
317
+ chunkSize: chunkSizeBytes === Infinity ? undefined : chunkSizeBytes,
318
+ compress: useCompression,
319
+ force: !!options.force,
320
+ originalFilename: filename,
321
+ originalExtension: extension,
322
+ });
323
+ createdFiles.push(...result.files);
324
+ spinner.succeed && spinner.succeed(`Encoded ${result.partCount} part${result.partCount !== 1 ? 's' : ''}`);
325
+
326
+ if (!quiet) {
327
+ console.log();
328
+ console.log(chalk.green.bold('✓ File encoded successfully!'));
329
+ console.log(chalk.cyan(` Format: ${format.toUpperCase()} (${version} log-embed)`));
330
+ console.log(chalk.cyan(` Hash: ${result.hash}`));
331
+ if (result.partCount > 1) {
332
+ console.log(chalk.cyan(` Parts: ${result.partCount}`));
333
+ }
334
+ console.log(chalk.cyan(` Encrypted: ${result.encrypted ? 'Yes' : 'No'}`));
335
+ console.log(chalk.cyan(` Compressed: ${result.compressed ? 'Yes (Brotli)' : 'No'}`));
336
+ console.log(chalk.cyan(` Location: ${outputDir}`));
337
+ if (result.encrypted) {
338
+ console.log(chalk.yellow(` Remember your password - it cannot be recovered!`));
339
+ }
340
+ }
341
+
342
+ if (tempZipPath) cleanupTemp(tempZipPath);
343
+ return;
344
+ }
345
+
178
346
  const contentHash = await computeFileHash(streamSource);
179
347
 
180
348
  // Generate session salt for encryption
@@ -252,7 +420,6 @@ async function encodeCommand(inputFile, options) {
252
420
  encrypted: useEncryption,
253
421
  compressed: useCompression,
254
422
  contentHash,
255
- stegoMethod: 'log-embed',
256
423
  compressionAlgo: 'brotli',
257
424
  payloadSize: payloadBuffer.length,
258
425
  dataLineCount,
@@ -356,96 +523,6 @@ async function encodeCommand(inputFile, options) {
356
523
  }
357
524
  }
358
525
 
359
- // ─── Legacy v4 DOCX Pipeline ────────────────────────────────────────────────
360
-
361
- async function encodeLegacyDocx(inputPath, filename, extension, fileSize, options, useCompression, useEncryption, spinner, quiet, createdFiles) {
362
- const { compress } = require('../lib/compression');
363
- const { encrypt, packEncryptionMeta: packMeta } = require('../lib/crypto');
364
- const { generateContentHash } = require('../lib/utils');
365
-
366
- const fileBuffer = fs.readFileSync(inputPath);
367
- const contentHash = generateContentHash(fileBuffer);
368
-
369
- let processedBuffer = fileBuffer;
370
- if (useCompression) {
371
- spinner.text = 'Compressing...';
372
- const compressedBuffer = await compress(fileBuffer);
373
- if (compressedBuffer.length < fileBuffer.length) {
374
- processedBuffer = compressedBuffer;
375
- spinner.succeed && spinner.succeed(`Compressed: ${formatBytes(fileBuffer.length)} → ${formatBytes(compressedBuffer.length)}`);
376
- } else {
377
- useCompression = false;
378
- spinner.info && spinner.info('Compression skipped (no size benefit)');
379
- }
380
- }
381
-
382
- const base64 = processedBuffer.toString('base64');
383
-
384
- let contentToStore;
385
- let encryptionMeta = null;
386
-
387
- if (useEncryption) {
388
- spinner.text = 'Encrypting content...';
389
- const { ciphertext, iv, salt, authTag } = encrypt(base64, options.password);
390
- encryptionMeta = packMeta({ iv, salt, authTag });
391
- contentToStore = ciphertext;
392
- spinner.succeed && spinner.succeed('Content encrypted with AES-256-GCM');
393
- } else {
394
- contentToStore = base64;
395
- }
396
-
397
- const hash = generateHash();
398
- const outputDir = options.outputDir || process.cwd();
399
- const format = 'docx';
400
-
401
- const metadata = createMetadata({
402
- originalFilename: filename,
403
- originalExtension: extension,
404
- hash,
405
- partNumber: null,
406
- totalParts: null,
407
- originalSize: fileSize,
408
- format,
409
- encrypted: useEncryption,
410
- compressed: useCompression,
411
- contentHash,
412
- });
413
-
414
- const outputFilename = generateFilename(hash, null, null, format);
415
- const outputPath = path.join(outputDir, outputFilename);
416
-
417
- if (fs.existsSync(outputPath) && !options.force) {
418
- throw new Error(`File already exists: ${outputPath}. Use --force to overwrite.`);
419
- }
420
-
421
- const docxContent = useEncryption
422
- ? `${encryptionMeta}|||${contentToStore}`
423
- : contentToStore;
424
-
425
- await createDocxWithBase64({
426
- base64Content: docxContent,
427
- metadata,
428
- outputPath,
429
- });
430
-
431
- createdFiles.push(outputPath);
432
- spinner.succeed && spinner.succeed('Encoding complete!');
433
-
434
- if (!quiet) {
435
- console.log();
436
- console.log(chalk.green.bold('✓ File encoded successfully!'));
437
- console.log(chalk.cyan(` Format: DOCX (v4 legacy)`));
438
- console.log(chalk.cyan(` Hash: ${hash}`));
439
- console.log(chalk.cyan(` Output: ${outputFilename}`));
440
- console.log(chalk.cyan(` Encrypted: ${useEncryption ? 'Yes' : 'No'}`));
441
- console.log(chalk.cyan(` Compressed: ${useCompression ? 'Yes' : 'No'}`));
442
- console.log(chalk.cyan(` Location: ${outputDir}`));
443
- if (useEncryption) {
444
- console.log(chalk.yellow(` Remember your password - it cannot be recovered!`));
445
- }
446
- }
447
- }
448
-
449
526
  function cleanupTemp(tempPath) {
450
527
  try {
451
528
  if (fs.existsSync(tempPath)) {
@@ -1,11 +1,53 @@
1
- const path = require('path');
2
1
  const chalk = require('chalk');
3
2
  const ora = require('ora');
4
- const { readDocxBase64 } = require('../lib/docx-handler');
5
- const { readXlsxBase64 } = require('../lib/xlsx-handler');
6
- const { validateMetadata, isMultiPart, isLogEmbedFormat } = require('../lib/metadata');
7
- const { detectFormat, formatBytes } = require('../lib/utils');
8
- const { extractContent, findMultiPartFiles } = require('../lib/file-utils');
3
+ const { formatBytes } = require('../lib/utils');
4
+ const { loadNative } = require('../lib/native');
5
+
6
+ const NATIVE_REQUIRED =
7
+ 'Reading file metadata requires the native engine, which is not installed. ' +
8
+ 'Reinstall stegdoc, or build it with `pnpm build:native`.';
9
+
10
+ const LEGACY_UNSUPPORTED =
11
+ 'The legacy v3/v4 format is no longer supported. Re-encode the file with a current version of stegdoc.';
12
+
13
+ /**
14
+ * Print metadata read by the native engine, which handles v5 and v6.
15
+ */
16
+ function printNativeInfo(info) {
17
+ console.log();
18
+ console.log(chalk.bold.white('File Information:'));
19
+ console.log(chalk.cyan(` Format: ${info.format.toUpperCase()} (${info.formatVersion})`));
20
+ console.log(chalk.cyan(` Stego method: Log-embed (${info.formatVersion})`));
21
+ console.log(chalk.cyan(` Authenticated metadata: ${info.authenticated ? 'Yes' : 'No'}`));
22
+ console.log();
23
+ console.log(chalk.bold.white('Original File:'));
24
+ console.log(chalk.cyan(` Filename: ${info.originalFilename}`));
25
+ console.log(chalk.cyan(` Size: ${formatBytes(info.originalSize)}`));
26
+ console.log();
27
+ console.log(chalk.bold.white('Encoding Options:'));
28
+ console.log(chalk.cyan(` Encrypted: ${info.encrypted ? chalk.yellow('Yes') : 'No'}`));
29
+ console.log(chalk.cyan(` Compressed: ${info.compressed ? chalk.green('Yes') : 'No'}`));
30
+ console.log(chalk.cyan(` Encoded on: ${info.encodingDate || 'Unknown'}`));
31
+ if (info.contentHash) {
32
+ console.log(chalk.cyan(` Content hash: ${info.contentHash.slice(0, 16)}...`));
33
+ }
34
+ console.log();
35
+ if (info.partCount > 1) {
36
+ console.log(chalk.bold.white('Multi-part File:'));
37
+ console.log(chalk.cyan(` This is part: ${info.partNumber} of ${info.partCount}`));
38
+ console.log(chalk.cyan(` Hash: ${info.hash}`));
39
+ console.log();
40
+ console.log(chalk.green.bold(`All ${info.partCount} parts required to decode`));
41
+ } else {
42
+ console.log(chalk.bold.white('Single File:'));
43
+ console.log(chalk.cyan(` Hash: ${info.hash}`));
44
+ console.log();
45
+ console.log(chalk.green.bold('Ready to decode'));
46
+ }
47
+ if (info.encrypted) {
48
+ console.log(chalk.yellow('\nNote: Password required for decoding'));
49
+ }
50
+ }
9
51
 
10
52
  /**
11
53
  * Show information about an encoded file without decoding
@@ -14,100 +56,29 @@ async function infoCommand(inputFile, options) {
14
56
  const spinner = ora('Reading file metadata...').start();
15
57
 
16
58
  try {
17
- const format = detectFormat(inputFile);
18
- if (!format) {
19
- throw new Error('Unknown file format. Supported formats: .xlsx, .docx');
59
+ const native = loadNative();
60
+ if (!native) {
61
+ throw new Error(NATIVE_REQUIRED);
20
62
  }
21
63
 
22
- let readResult;
23
- if (format === 'xlsx') {
24
- readResult = await readXlsxBase64(inputFile);
25
- } else {
26
- readResult = await readDocxBase64(inputFile);
64
+ let probe;
65
+ try {
66
+ probe = native.probe(inputFile);
67
+ } catch {
68
+ probe = 'unknown';
27
69
  }
28
-
29
- const extracted = extractContent(readResult, format);
30
- const metadata = extracted.metadata;
31
- const encryptionMeta = extracted.encryptionMeta;
32
-
33
- validateMetadata(metadata);
34
-
35
- const isEncrypted = metadata.encrypted || (encryptionMeta && encryptionMeta.length > 0);
36
- const isCompressed = metadata.compressed || false;
37
- const isV5 = isLogEmbedFormat(metadata);
38
-
39
- spinner.succeed('File metadata read successfully');
40
- console.log();
41
-
42
- console.log(chalk.bold.white('File Information:'));
43
- console.log(chalk.cyan(` Format: ${format.toUpperCase()}`));
44
- console.log(chalk.cyan(` Tool version: ${metadata.version || '1.x'}`));
45
- if (isV5) {
46
- console.log(chalk.cyan(` Stego method: Log-embed (v5)`));
47
- console.log(chalk.cyan(` Compression: ${metadata.compressionAlgo || 'brotli'}`));
70
+ if (probe === 'legacy') {
71
+ throw new Error(LEGACY_UNSUPPORTED);
48
72
  }
49
- console.log();
50
-
51
- console.log(chalk.bold.white('Original File:'));
52
- console.log(chalk.cyan(` Filename: ${metadata.originalFilename}`));
53
- console.log(chalk.cyan(` Extension: ${metadata.originalExtension}`));
54
- console.log(chalk.cyan(` Size: ${formatBytes(metadata.originalSize)}`));
55
- console.log();
56
-
57
- console.log(chalk.bold.white('Encoding Options:'));
58
- console.log(chalk.cyan(` Encrypted: ${isEncrypted ? chalk.yellow('Yes') : 'No'}`));
59
- console.log(chalk.cyan(` Compressed: ${isCompressed ? chalk.green('Yes') : 'No'}`));
60
- console.log(chalk.cyan(` Encoded on: ${metadata.encodingDate || 'Unknown'}`));
61
-
62
- if (metadata.contentHash) {
63
- console.log(chalk.cyan(` Content hash: ${metadata.contentHash.slice(0, 16)}...`));
64
- }
65
-
66
- if (isV5 && metadata.dataLineCount) {
67
- console.log(chalk.cyan(` Data lines: ${metadata.dataLineCount}`));
68
- }
69
- console.log();
70
-
71
- const hasMultipleParts = isMultiPart(metadata) || metadata.partNumber !== null;
72
- if (hasMultipleParts) {
73
- console.log(chalk.bold.white('Multi-part File:'));
74
- const inputDir = path.dirname(inputFile);
75
- const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
76
- const totalParts = metadata.totalParts || allParts.length;
77
-
78
- console.log(chalk.cyan(` This is part: ${metadata.partNumber} of ${totalParts}`));
79
- console.log(chalk.cyan(` Hash: ${metadata.hash}`));
80
-
81
- console.log();
82
- console.log(chalk.bold.white('Parts found in directory:'));
83
-
84
- for (let i = 1; i <= totalParts; i++) {
85
- const part = allParts.find(p => p.partNumber === i);
86
- if (part) {
87
- console.log(chalk.green(` ✓ Part ${i}: ${part.filename}`));
88
- } else {
89
- console.log(chalk.red(` ✗ Part ${i}: MISSING`));
90
- }
91
- }
92
-
93
- if (allParts.length >= totalParts) {
94
- console.log();
95
- console.log(chalk.green.bold('All parts found - ready to decode'));
96
- } else {
97
- console.log();
98
- console.log(chalk.yellow.bold(`Missing ${totalParts - allParts.length} part(s)`));
99
- }
100
- } else {
101
- console.log(chalk.bold.white('Single File:'));
102
- console.log(chalk.cyan(` Hash: ${metadata.hash}`));
103
- console.log();
104
- console.log(chalk.green.bold('Ready to decode'));
105
- }
106
-
107
- if (isEncrypted) {
108
- console.log(chalk.yellow('\nNote: Password required for decoding'));
73
+ if (probe !== 'v5' && probe !== 'archive') {
74
+ throw new Error(
75
+ 'Unknown file format. Supported formats: .xlsx, .docx, or a zip of them'
76
+ );
109
77
  }
110
78
 
79
+ const info = native.info(inputFile);
80
+ spinner.succeed('File metadata read successfully');
81
+ printNativeInfo(info);
111
82
  } catch (error) {
112
83
  spinner.fail('Failed to read file info');
113
84
  console.error(chalk.red(`Error: ${error.message}`));