stegdoc 5.2.0 → 5.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stegdoc",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Hide files inside Office documents (XLSX/DOCX) with AES-256 encryption and steganography",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -178,16 +178,52 @@ async function encodeCommand(inputFile, options) {
178
178
  // Generate session salt for encryption
179
179
  const sessionSalt = useEncryption ? generateSalt() : null;
180
180
 
181
- spinner.text = useCompression ? 'Compressing (Brotli) and encoding...' : 'Encoding...';
181
+ // === Phase 1: Compress and collect all chunks ===
182
+ spinner.text = useCompression ? 'Compressing...' : 'Reading...';
182
183
 
183
184
  const partFiles = [];
184
-
185
- // v5 pipeline: compress (brotli) → collect binary chunks → encrypt per-part → embed in log lines
186
185
  const binaryChunkSize = chunkSizeBytes === Infinity ? Infinity : Math.floor(chunkSizeBytes * 3 / 4);
187
186
 
188
- const onBinaryChunkReady = async (binaryBuffer, index) => {
187
+ // Collect all chunks first (enables parallel part creation + known total)
188
+ const collectedChunks = [];
189
+ const collectChunk = async (binaryBuffer, index) => {
190
+ collectedChunks.push({ buffer: Buffer.from(binaryBuffer), index });
191
+ };
192
+
193
+ const collector = new BinaryChunkCollector(binaryChunkSize, collectChunk);
194
+
195
+ const streams = [fs.createReadStream(streamSource)];
196
+
197
+ // Add progress tracking (tracks input bytes read)
198
+ if (!quiet) {
199
+ const progressStream = new ProgressTransform(fileSize, (processed, total) => {
200
+ const pct = Math.min(100, Math.round((processed / total) * 100));
201
+ const processedFmt = formatBytes(processed);
202
+ const totalFmt = formatBytes(total);
203
+ const phase = useCompression ? 'Compressing' : 'Reading';
204
+ spinner.text = `${phase}... ${processedFmt} / ${totalFmt} (${pct}%)`;
205
+ });
206
+ streams.push(progressStream);
207
+ }
208
+
209
+ if (useCompression) {
210
+ streams.push(createBrotliCompressStream());
211
+ }
212
+ streams.push(collector);
213
+
214
+ await pipeline(...streams);
215
+
216
+ const totalParts = collectedChunks.length;
217
+ spinner.succeed && spinner.succeed(`${useCompression ? 'Compressed' : 'Read'}: ${totalParts} part${totalParts !== 1 ? 's' : ''} to create`);
218
+
219
+ // === Phase 2: Create parts in parallel ===
220
+ const os = require('os');
221
+ const concurrency = Math.min(totalParts, Math.max(1, os.cpus().length));
222
+ let completedParts = 0;
223
+ const partSpinner = quiet ? spinner : ora(`Creating parts... 0/${totalParts}`).start();
224
+
225
+ const createPart = async ({ buffer: binaryBuffer, index }) => {
189
226
  const partNumber = index + 1;
190
- const partSpinner = quiet ? spinner : ora(`Creating part ${partNumber}...`).start();
191
227
 
192
228
  let payloadBuffer;
193
229
  let encryptionMeta = '';
@@ -208,7 +244,7 @@ async function encodeCommand(inputFile, options) {
208
244
  originalExtension: extension,
209
245
  hash,
210
246
  partNumber,
211
- totalParts: null,
247
+ totalParts,
212
248
  originalSize: fileSize,
213
249
  format,
214
250
  encrypted: useEncryption,
@@ -262,35 +298,26 @@ async function encodeCommand(inputFile, options) {
262
298
 
263
299
  partFiles.push(outputPath);
264
300
  createdFiles.push(outputPath);
265
- partSpinner.succeed && partSpinner.succeed(`Created: ${outputFilename} (${formatBytes(payloadBuffer.length)} payload, ${dataLineCount} data lines)`);
266
- };
267
-
268
- const collector = new BinaryChunkCollector(binaryChunkSize, onBinaryChunkReady);
269
301
 
270
- const streams = [fs.createReadStream(streamSource)];
302
+ completedParts++;
303
+ const pct = Math.round((completedParts / totalParts) * 100);
304
+ partSpinner.text = `Creating parts... ${completedParts}/${totalParts} (${pct}%)`;
305
+ };
271
306
 
272
- // Add progress tracking before compression (tracks input bytes read)
273
- if (!quiet) {
274
- const progressStream = new ProgressTransform(fileSize, (processed, total) => {
275
- const pct = Math.min(100, Math.round((processed / total) * 100));
276
- const processedFmt = formatBytes(processed);
277
- const totalFmt = formatBytes(total);
278
- const phase = useCompression ? 'Compressing' : 'Processing';
279
- spinner.text = `${phase}... ${processedFmt} / ${totalFmt} (${pct}%)`;
280
- });
281
- streams.push(progressStream);
282
- }
307
+ // Run part creation in parallel with limited concurrency
308
+ const chunks = [...collectedChunks];
309
+ const executing = new Set();
283
310
 
284
- if (useCompression) {
285
- streams.push(createBrotliCompressStream());
311
+ for (const chunk of chunks) {
312
+ const promise = createPart(chunk).then(() => executing.delete(promise));
313
+ executing.add(promise);
314
+ if (executing.size >= concurrency) {
315
+ await Promise.race(executing);
316
+ }
286
317
  }
287
- streams.push(collector);
288
-
289
- await pipeline(...streams);
290
-
291
- const totalParts = partFiles.length;
318
+ await Promise.all(executing);
292
319
 
293
- spinner.succeed && spinner.succeed('Encoding complete!');
320
+ partSpinner.succeed && partSpinner.succeed(`Created ${totalParts} part${totalParts !== 1 ? 's' : ''}`);
294
321
 
295
322
  if (!quiet) {
296
323
  console.log();
package/src/index.js CHANGED
@@ -11,7 +11,7 @@ const verifyCommand = require('./commands/verify');
11
11
  program
12
12
  .name('stegdoc')
13
13
  .description('CLI tool to encode files into Office documents with AES-256 encryption')
14
- .version('5.2.0');
14
+ .version('5.3.0');
15
15
 
16
16
  // Encode command
17
17
  program