stegdoc 5.3.0 → 5.5.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.3.0",
3
+ "version": "5.5.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": {
@@ -198,7 +198,8 @@ async function decodeV5(inputFile, format, firstReadResult, options, spinner, qu
198
198
  spinner.succeed && spinner.succeed(`Found all ${totalPartsFound} parts`);
199
199
 
200
200
  for (let i = 0; i < allParts.length; i++) {
201
- const partSpinner = quiet ? spinner : ora(`Decoding part ${i + 1} of ${totalPartsFound}...`).start();
201
+ const pct = Math.round(((i + 1) / totalPartsFound) * 100);
202
+ const partSpinner = quiet ? spinner : ora(`Decoding part ${i + 1}/${totalPartsFound} (${pct}%)...`).start();
202
203
 
203
204
  const partResult = await readFile(allParts[i].path, format);
204
205
 
@@ -228,7 +229,7 @@ async function decodeV5(inputFile, format, firstReadResult, options, spinner, qu
228
229
  writeTarget.write(partPayload);
229
230
  }
230
231
 
231
- partSpinner.succeed && partSpinner.succeed(`Part ${i + 1} decoded`);
232
+ partSpinner.succeed && partSpinner.succeed(`Part ${i + 1}/${totalPartsFound} decoded`);
232
233
  }
233
234
  } else {
234
235
  // Single file
@@ -30,12 +30,14 @@ function zipFolder(folderPath) {
30
30
  */
31
31
  async function detectFileType(filePath) {
32
32
  try {
33
- const { fileTypeFromBuffer } = await import('file-type');
33
+ const fileType = await import('file-type');
34
+ const fromBuffer = fileType.fileTypeFromBuffer || fileType.default?.fromBuffer;
35
+ if (!fromBuffer) return null;
34
36
  const fd = await fs.promises.open(filePath, 'r');
35
37
  const buf = Buffer.alloc(4100);
36
38
  await fd.read(buf, 0, 4100, 0);
37
39
  await fd.close();
38
- return await fileTypeFromBuffer(buf);
40
+ return await fromBuffer(buf);
39
41
  } catch {
40
42
  return null;
41
43
  }
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.3.0');
14
+ .version('5.5.0');
15
15
 
16
16
  // Encode command
17
17
  program
@@ -117,6 +117,7 @@ async function createXlsxPartV5(options) {
117
117
 
118
118
  /**
119
119
  * Read a v5 log-embed XLSX file and extract payload.
120
+ * Uses fast regex-based XML scanning instead of full DOM parsing for speed.
120
121
  * @param {string} xlsxPath - Path to XLSX file
121
122
  * @returns {object} { payloadBuffer, metadataJson, encryptionMeta, metadata }
122
123
  */
@@ -125,62 +126,67 @@ async function readXlsxV5(xlsxPath) {
125
126
  throw new Error(`XLSX file not found: ${xlsxPath}`);
126
127
  }
127
128
 
128
- // Parse shared strings
129
- let sharedStrings = [];
130
- const ssParsed = parseXmlFromZip(xlsxPath, 'xl/sharedStrings.xml');
131
- if (ssParsed && ssParsed.sst && ssParsed.sst.si) {
132
- const siArray = ensureArray(ssParsed.sst.si);
133
- sharedStrings = siArray.map((si) => extractTextContent(si.t));
129
+ // Read file once and reuse the zip instance
130
+ const fileBuffer = fs.readFileSync(xlsxPath);
131
+ const zip = new AdmZip(fileBuffer);
132
+
133
+ // Parse shared strings (only if present — v5 files created with useSharedStrings:false may not have them)
134
+ let sharedStrings = null;
135
+ const ssEntry = zip.getEntry('xl/sharedStrings.xml');
136
+ if (ssEntry) {
137
+ sharedStrings = [];
138
+ const ssXml = ssEntry.getData().toString('utf8');
139
+ // Fast extract: match each <si><t>...</t></si> or <si><t ...>...</t></si>
140
+ const siRegex = /<si><t[^>]*>([^<]*)<\/t><\/si>/g;
141
+ let match;
142
+ while ((match = siRegex.exec(ssXml)) !== null) {
143
+ sharedStrings.push(match[1]);
144
+ }
134
145
  }
135
146
 
136
- // Parse sheet1 (the only sheet in v5)
137
- const sheetParsed = parseXmlFromZip(xlsxPath, 'xl/worksheets/sheet1.xml');
138
- if (!sheetParsed) {
147
+ // Extract sheet1.xml raw string
148
+ const sheetEntry = zip.getEntry('xl/worksheets/sheet1.xml');
149
+ if (!sheetEntry) {
139
150
  throw new Error('Sheet not found in XLSX file.');
140
151
  }
152
+ const sheetXml = sheetEntry.getData().toString('utf8');
141
153
 
142
- // Extract all rows as arrays of cell values
154
+ // Fast row extraction using regex much faster than full DOM parsing
143
155
  const allRows = [];
144
- const sheetData = sheetParsed.worksheet?.sheetData;
145
-
146
- if (sheetData && sheetData.row) {
147
- const rows = ensureArray(sheetData.row);
148
-
149
- for (const row of rows) {
150
- if (!row.c) continue;
151
- const cells = ensureArray(row.c);
152
-
153
- // Build a sparse array for this row
154
- const rowValues = [];
155
- for (const cell of cells) {
156
- const cellRef = cell['@_r'];
157
- if (!cellRef) continue;
158
-
159
- // Parse column index from cell reference (e.g., "A2" -> col 0, "J2" -> col 9)
160
- const colMatch = cellRef.match(/^([A-Z]+)/);
161
- if (!colMatch) continue;
162
- const colIdx = colLetterToIndex(colMatch[1]);
163
-
164
- const cellType = cell['@_t'];
165
- const cellValue = cell.v;
166
-
167
- let value;
168
- if (cellType === 's' && cellValue !== undefined) {
169
- const ssIndex = parseInt(cellValue, 10);
170
- value = ssIndex < sharedStrings.length ? sharedStrings[ssIndex] : '';
171
- } else if (cellType === 'inlineStr' && cell.is) {
172
- value = extractTextContent(cell.is.t);
173
- } else if (cellValue !== undefined) {
174
- value = String(cellValue);
175
- } else {
176
- value = '';
177
- }
178
-
179
- rowValues[colIdx] = value;
156
+ const rowRegex = /<row [^>]*>(.*?)<\/row>/gs;
157
+ const cellRegex = /<c r="([A-Z]+)\d+"(?: t="([^"]*)")?[^>]*>(?:<v>([^<]*)<\/v>|<is><t[^>]*>([^<]*)<\/t><\/is>)?<\/c>/g;
158
+
159
+ let rowMatch;
160
+ while ((rowMatch = rowRegex.exec(sheetXml)) !== null) {
161
+ const rowXml = rowMatch[1];
162
+ const rowValues = [];
163
+
164
+ let cellMatch;
165
+ cellRegex.lastIndex = 0;
166
+ while ((cellMatch = cellRegex.exec(rowXml)) !== null) {
167
+ const colLetter = cellMatch[1];
168
+ const cellType = cellMatch[2] || '';
169
+ const vValue = cellMatch[3];
170
+ const inlineValue = cellMatch[4];
171
+
172
+ const colIdx = colLetterToIndex(colLetter);
173
+
174
+ let value;
175
+ if (cellType === 's' && vValue !== undefined && sharedStrings) {
176
+ const ssIndex = parseInt(vValue, 10);
177
+ value = ssIndex < sharedStrings.length ? sharedStrings[ssIndex] : '';
178
+ } else if (inlineValue !== undefined) {
179
+ value = decodeXmlEntities(inlineValue);
180
+ } else if (vValue !== undefined) {
181
+ value = decodeXmlEntities(vValue);
182
+ } else {
183
+ value = '';
180
184
  }
181
185
 
182
- allRows.push(rowValues);
186
+ rowValues[colIdx] = value;
183
187
  }
188
+
189
+ allRows.push(rowValues);
184
190
  }
185
191
 
186
192
  // Skip the first row (column headers)
@@ -194,6 +200,19 @@ async function readXlsxV5(xlsxPath) {
194
200
  return decodeLogLines(dataRows);
195
201
  }
196
202
 
203
+ /**
204
+ * Decode XML entities in a string value
205
+ */
206
+ function decodeXmlEntities(str) {
207
+ if (!str || !str.includes('&')) return str;
208
+ return str
209
+ .replace(/&amp;/g, '&')
210
+ .replace(/&lt;/g, '<')
211
+ .replace(/&gt;/g, '>')
212
+ .replace(/&apos;/g, "'")
213
+ .replace(/&quot;/g, '"');
214
+ }
215
+
197
216
  /**
198
217
  * Convert column letter to 0-based index (A=0, B=1, ..., Z=25, AA=26)
199
218
  */