stegdoc 5.6.0 → 5.7.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 +1 -2
- package/src/commands/encode.js +2 -164
- package/src/lib/xlsx-handler.js +22 -321
- package/src/lib/xlsx-writer.js +298 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stegdoc",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.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": {
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"chalk": "^4.1.2",
|
|
51
51
|
"commander": "^14.0.2",
|
|
52
52
|
"docx": "^9.5.1",
|
|
53
|
-
"exceljs": "^4.4.0",
|
|
54
53
|
"fast-xml-parser": "^5.3.3",
|
|
55
54
|
"file-type": "^16.5.4",
|
|
56
55
|
"inquirer": "^8.2.6",
|
package/src/commands/encode.js
CHANGED
|
@@ -5,7 +5,7 @@ const chalk = require('chalk');
|
|
|
5
5
|
const ora = require('ora');
|
|
6
6
|
const AdmZip = require('adm-zip');
|
|
7
7
|
const { createDocxWithBase64, createDocxV5 } = require('../lib/docx-handler');
|
|
8
|
-
const {
|
|
8
|
+
const { createXlsxPartV5 } = require('../lib/xlsx-handler');
|
|
9
9
|
const { createMetadata, serializeMetadata } = require('../lib/metadata');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const { generateHash, parseSizeToBytes, formatBytes, generateFilename } = require('../lib/utils');
|
|
@@ -143,7 +143,7 @@ async function encodeCommand(inputFile, options) {
|
|
|
143
143
|
if (format === 'docx') {
|
|
144
144
|
await encodeLegacyDocx(streamSource, filename, extension, fileSize, options, useCompression, useEncryption, spinner, quiet, createdFiles);
|
|
145
145
|
} else {
|
|
146
|
-
|
|
146
|
+
throw new Error('Legacy XLSX format (--legacy) is no longer supported. Use v5 format (default) or legacy DOCX (-f docx --legacy).');
|
|
147
147
|
}
|
|
148
148
|
if (tempZipPath) cleanupTemp(tempZipPath);
|
|
149
149
|
return;
|
|
@@ -356,168 +356,6 @@ async function encodeCommand(inputFile, options) {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
// ─── Legacy v4 XLSX Pipeline ────────────────────────────────────────────────
|
|
360
|
-
|
|
361
|
-
async function encodeLegacyXlsx(inputPath, filename, extension, fileSize, options, useCompression, useEncryption, spinner, quiet, createdFiles) {
|
|
362
|
-
const hash = generateHash();
|
|
363
|
-
const outputDir = options.outputDir || process.cwd();
|
|
364
|
-
const format = 'xlsx';
|
|
365
|
-
const chunkInput = (options.chunkSize || '').toString().trim();
|
|
366
|
-
|
|
367
|
-
// Parse chunk size
|
|
368
|
-
const chunkInputLower = chunkInput.toLowerCase();
|
|
369
|
-
let chunkSizeBytes;
|
|
370
|
-
|
|
371
|
-
if (chunkInputLower === '0' || chunkInputLower === 'max' || chunkInputLower === 'single' || chunkInputLower === 'none' || chunkInputLower === '') {
|
|
372
|
-
chunkSizeBytes = Infinity;
|
|
373
|
-
} else if (/^\d+\s*parts?$/i.test(chunkInput)) {
|
|
374
|
-
const numParts = parseInt(chunkInput, 10);
|
|
375
|
-
if (numParts < 1) throw new Error('Number of parts must be at least 1');
|
|
376
|
-
const estimatedBase64Size = Math.ceil(fileSize * 4 / 3);
|
|
377
|
-
chunkSizeBytes = Math.ceil(estimatedBase64Size / numParts);
|
|
378
|
-
spinner.info && spinner.info(`Splitting into ~${numParts} parts (~${formatBytes(chunkSizeBytes)} content each)`);
|
|
379
|
-
} else if (chunkInput) {
|
|
380
|
-
chunkSizeBytes = parseSizeToBytes(chunkInput);
|
|
381
|
-
} else {
|
|
382
|
-
chunkSizeBytes = 5 * 1024 * 1024;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
spinner.text = 'Computing file hash...';
|
|
386
|
-
const contentHash = await computeFileHash(inputPath);
|
|
387
|
-
|
|
388
|
-
const sessionSalt = useEncryption ? generateSalt() : null;
|
|
389
|
-
|
|
390
|
-
spinner.text = useCompression ? 'Compressing and encoding (legacy v4)...' : 'Encoding (legacy v4)...';
|
|
391
|
-
|
|
392
|
-
const partFiles = [];
|
|
393
|
-
|
|
394
|
-
if (useEncryption) {
|
|
395
|
-
const binaryChunkSize = chunkSizeBytes === Infinity ? Infinity : Math.floor(chunkSizeBytes * 3 / 4);
|
|
396
|
-
|
|
397
|
-
const onBinaryChunkReady = async (binaryBuffer, index) => {
|
|
398
|
-
const partNumber = index + 1;
|
|
399
|
-
const partSpinner = quiet ? spinner : ora(`Creating part ${partNumber}...`).start();
|
|
400
|
-
|
|
401
|
-
const { stream: cipher, iv, salt, getAuthTag } = createEncryptStream(options.password, sessionSalt);
|
|
402
|
-
const encrypted = Buffer.concat([cipher.update(binaryBuffer), cipher.final()]);
|
|
403
|
-
const authTag = getAuthTag();
|
|
404
|
-
const base64Chunk = encrypted.toString('base64');
|
|
405
|
-
|
|
406
|
-
const metadata = createMetadata({
|
|
407
|
-
originalFilename: filename,
|
|
408
|
-
originalExtension: extension,
|
|
409
|
-
hash,
|
|
410
|
-
partNumber,
|
|
411
|
-
totalParts: null,
|
|
412
|
-
originalSize: fileSize,
|
|
413
|
-
format,
|
|
414
|
-
encrypted: true,
|
|
415
|
-
compressed: useCompression,
|
|
416
|
-
contentHash,
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
const encryptionMeta = packEncryptionMeta({ iv, salt, authTag });
|
|
420
|
-
const outputFilename = generateFilename(hash, partNumber, null, format);
|
|
421
|
-
const outputPath = path.join(outputDir, outputFilename);
|
|
422
|
-
|
|
423
|
-
if (fs.existsSync(outputPath) && !options.force) {
|
|
424
|
-
throw new Error(`File already exists: ${outputPath}. Use --force to overwrite.`);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
await createXlsxPartStreaming({
|
|
428
|
-
base64Content: base64Chunk,
|
|
429
|
-
encryptionMeta,
|
|
430
|
-
metadataJson: serializeMetadata(metadata),
|
|
431
|
-
outputPath,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
partFiles.push(outputPath);
|
|
435
|
-
createdFiles.push(outputPath);
|
|
436
|
-
partSpinner.succeed && partSpinner.succeed(`Created: ${outputFilename} (${formatBytes(base64Chunk.length)} encoded)`);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const collector = new BinaryChunkCollector(binaryChunkSize, onBinaryChunkReady);
|
|
440
|
-
const streams = [fs.createReadStream(inputPath)];
|
|
441
|
-
if (!quiet) {
|
|
442
|
-
streams.push(new ProgressTransform(fileSize, (processed, total) => {
|
|
443
|
-
const pct = Math.min(100, Math.round((processed / total) * 100));
|
|
444
|
-
spinner.text = `Compressing (legacy v4)... ${formatBytes(processed)} / ${formatBytes(total)} (${pct}%)`;
|
|
445
|
-
}));
|
|
446
|
-
}
|
|
447
|
-
if (useCompression) streams.push(createCompressStream());
|
|
448
|
-
streams.push(collector);
|
|
449
|
-
await pipeline(...streams);
|
|
450
|
-
} else {
|
|
451
|
-
const onChunkReady = async (base64Chunk, index) => {
|
|
452
|
-
const partNumber = index + 1;
|
|
453
|
-
const partSpinner = quiet ? spinner : ora(`Creating part ${partNumber}...`).start();
|
|
454
|
-
|
|
455
|
-
const metadata = createMetadata({
|
|
456
|
-
originalFilename: filename,
|
|
457
|
-
originalExtension: extension,
|
|
458
|
-
hash,
|
|
459
|
-
partNumber,
|
|
460
|
-
totalParts: null,
|
|
461
|
-
originalSize: fileSize,
|
|
462
|
-
format,
|
|
463
|
-
encrypted: false,
|
|
464
|
-
compressed: useCompression,
|
|
465
|
-
contentHash,
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
const outputFilename = generateFilename(hash, partNumber, null, format);
|
|
469
|
-
const outputPath = path.join(outputDir, outputFilename);
|
|
470
|
-
|
|
471
|
-
if (fs.existsSync(outputPath) && !options.force) {
|
|
472
|
-
throw new Error(`File already exists: ${outputPath}. Use --force to overwrite.`);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
await createXlsxPartStreaming({
|
|
476
|
-
base64Content: base64Chunk,
|
|
477
|
-
encryptionMeta: '',
|
|
478
|
-
metadataJson: serializeMetadata(metadata),
|
|
479
|
-
outputPath,
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
partFiles.push(outputPath);
|
|
483
|
-
createdFiles.push(outputPath);
|
|
484
|
-
partSpinner.succeed && partSpinner.succeed(`Created: ${outputFilename} (${formatBytes(base64Chunk.length)} encoded)`);
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
const collector = new ChunkCollector(chunkSizeBytes, onChunkReady);
|
|
488
|
-
const streams = [fs.createReadStream(inputPath)];
|
|
489
|
-
if (!quiet) {
|
|
490
|
-
streams.push(new ProgressTransform(fileSize, (processed, total) => {
|
|
491
|
-
const pct = Math.min(100, Math.round((processed / total) * 100));
|
|
492
|
-
spinner.text = `Encoding (legacy v4)... ${formatBytes(processed)} / ${formatBytes(total)} (${pct}%)`;
|
|
493
|
-
}));
|
|
494
|
-
}
|
|
495
|
-
if (useCompression) streams.push(createCompressStream());
|
|
496
|
-
streams.push(new Base64EncodeTransform());
|
|
497
|
-
streams.push(collector);
|
|
498
|
-
await pipeline(...streams);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const totalParts = partFiles.length;
|
|
502
|
-
spinner.succeed && spinner.succeed('Encoding complete!');
|
|
503
|
-
|
|
504
|
-
if (!quiet) {
|
|
505
|
-
console.log();
|
|
506
|
-
console.log(chalk.green.bold('✓ File encoded successfully!'));
|
|
507
|
-
console.log(chalk.cyan(` Format: XLSX (v4 legacy)`));
|
|
508
|
-
console.log(chalk.cyan(` Hash: ${hash}`));
|
|
509
|
-
if (totalParts > 1) {
|
|
510
|
-
console.log(chalk.cyan(` Parts: ${totalParts}`));
|
|
511
|
-
}
|
|
512
|
-
console.log(chalk.cyan(` Encrypted: ${useEncryption ? 'Yes' : 'No'}`));
|
|
513
|
-
console.log(chalk.cyan(` Compressed: ${useCompression ? 'Yes (gzip)' : 'No'}`));
|
|
514
|
-
console.log(chalk.cyan(` Location: ${outputDir}`));
|
|
515
|
-
if (useEncryption) {
|
|
516
|
-
console.log(chalk.yellow(` Remember your password - it cannot be recovered!`));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
359
|
// ─── Legacy v4 DOCX Pipeline ────────────────────────────────────────────────
|
|
522
360
|
|
|
523
361
|
async function encodeLegacyDocx(inputPath, filename, extension, fileSize, options, useCompression, useEncryption, spinner, quiet, createdFiles) {
|
package/src/lib/xlsx-handler.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
const ExcelJS = require('exceljs');
|
|
2
1
|
const fs = require('fs');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
const AdmZip = require('adm-zip');
|
|
5
|
-
const { generateDecoyHeaders, generateDecoyData, calculateDecoyRowCount, rowToArray, resetTimeWindow } = require('./decoy-generator');
|
|
6
4
|
const { generateLogHeaders, encodePayloadToLogLines, decodeLogLines, resetTimeState } = require('./log-generator');
|
|
7
5
|
const { parseXmlFromZip, ensureArray, extractTextContent } = require('./xml-utils');
|
|
6
|
+
const { createXlsxRaw } = require('./xlsx-writer');
|
|
8
7
|
|
|
9
|
-
// Constants for data storage
|
|
10
|
-
const HIDDEN_SHEET_NAME = 'Data';
|
|
11
|
-
const VISIBLE_SHEET_NAME = 'Server Metrics';
|
|
12
8
|
const V5_SHEET_NAME = 'Access Logs';
|
|
13
|
-
const CELL_CHUNK_SIZE = 32000; // Max characters per cell (~32KB)
|
|
14
9
|
|
|
15
10
|
// ─── v5 Log-Embed Format ───────────────────────────────────────────────────
|
|
16
11
|
|
|
@@ -29,88 +24,23 @@ const CELL_CHUNK_SIZE = 32000; // Max characters per cell (~32KB)
|
|
|
29
24
|
async function createXlsxPartV5(options) {
|
|
30
25
|
const { payloadBuffer, encryptionMeta, metadataJson, outputPath } = options;
|
|
31
26
|
|
|
32
|
-
// Ensure output directory exists
|
|
33
|
-
const outputDir = path.dirname(outputPath);
|
|
34
|
-
if (!fs.existsSync(outputDir)) {
|
|
35
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
|
|
39
|
-
filename: outputPath,
|
|
40
|
-
useSharedStrings: false,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
workbook.creator = 'Microsoft Excel';
|
|
44
|
-
workbook.lastModifiedBy = 'Microsoft Excel';
|
|
45
|
-
workbook.created = new Date();
|
|
46
|
-
workbook.modified = new Date();
|
|
47
|
-
|
|
48
|
-
// === Single Sheet: Access Logs ===
|
|
49
|
-
const sheet = workbook.addWorksheet(V5_SHEET_NAME, {
|
|
50
|
-
properties: { tabColor: { argb: '2F5496' } },
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Column widths for log data
|
|
54
|
-
sheet.columns = [
|
|
55
|
-
{ width: 16 }, // Remote Address
|
|
56
|
-
{ width: 28 }, // Timestamp
|
|
57
|
-
{ width: 8 }, // Method
|
|
58
|
-
{ width: 90 }, // Request (contains URL with payload)
|
|
59
|
-
{ width: 7 }, // Status
|
|
60
|
-
{ width: 8 }, // Bytes
|
|
61
|
-
{ width: 65 }, // Referer
|
|
62
|
-
{ width: 85 }, // User-Agent
|
|
63
|
-
{ width: 38 }, // X-Request-ID
|
|
64
|
-
{ width: 34 }, // X-Trace-ID
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
// Header row with styling
|
|
68
|
-
const headers = generateLogHeaders();
|
|
69
|
-
const headerRow = sheet.addRow(headers);
|
|
70
|
-
for (let col = 1; col <= headers.length; col++) {
|
|
71
|
-
const cell = headerRow.getCell(col);
|
|
72
|
-
cell.font = { bold: true, size: 10, color: { argb: 'FFFFFF' }, name: 'Consolas' };
|
|
73
|
-
cell.fill = {
|
|
74
|
-
type: 'pattern',
|
|
75
|
-
pattern: 'solid',
|
|
76
|
-
fgColor: { argb: '2F5496' },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
headerRow.commit();
|
|
80
|
-
|
|
81
27
|
// Generate all log lines (header + data + filler)
|
|
82
28
|
const { headerRows, dataRows, fillerRows } = encodePayloadToLogLines(
|
|
83
29
|
payloadBuffer, metadataJson, encryptionMeta
|
|
84
30
|
);
|
|
85
31
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Write filler lines (realistic padding)
|
|
99
|
-
for (const row of fillerRows) {
|
|
100
|
-
const r = sheet.addRow(row);
|
|
101
|
-
r.commit();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const totalRows = headerRows.length + dataRows.length + fillerRows.length;
|
|
105
|
-
|
|
106
|
-
// Add filters
|
|
107
|
-
sheet.autoFilter = {
|
|
108
|
-
from: 'A1',
|
|
109
|
-
to: `J${totalRows + 1}`,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
await sheet.commit();
|
|
113
|
-
await workbook.commit();
|
|
32
|
+
const headers = generateLogHeaders();
|
|
33
|
+
const allRows = [...headerRows, ...dataRows, ...fillerRows];
|
|
34
|
+
|
|
35
|
+
// Build XLSX from raw XML (no ExcelJS — passes air gap filters)
|
|
36
|
+
createXlsxRaw({
|
|
37
|
+
headers,
|
|
38
|
+
rows: allRows,
|
|
39
|
+
sheetName: V5_SHEET_NAME,
|
|
40
|
+
outputPath,
|
|
41
|
+
colWidths: [16, 28, 8, 90, 7, 8, 65, 85, 38, 34],
|
|
42
|
+
autoFilter: true,
|
|
43
|
+
});
|
|
114
44
|
|
|
115
45
|
return outputPath;
|
|
116
46
|
}
|
|
@@ -130,17 +60,17 @@ async function readXlsxV5(xlsxPath) {
|
|
|
130
60
|
const fileBuffer = fs.readFileSync(xlsxPath);
|
|
131
61
|
const zip = new AdmZip(fileBuffer);
|
|
132
62
|
|
|
133
|
-
// Parse shared strings
|
|
63
|
+
// Parse shared strings — handles both normal and namespace-prefixed XML (ns0:si, ns0:t)
|
|
134
64
|
let sharedStrings = null;
|
|
135
65
|
const ssEntry = zip.getEntry('xl/sharedStrings.xml');
|
|
136
66
|
if (ssEntry) {
|
|
137
67
|
sharedStrings = [];
|
|
138
68
|
const ssXml = ssEntry.getData().toString('utf8');
|
|
139
|
-
//
|
|
140
|
-
const siRegex = /<si><t[^>]*>([^<]*)<\/t><\/si>/g;
|
|
69
|
+
// Match <si><t>...</t></si> or <ns0:si><ns0:t>...</ns0:t></ns0:si> (any nsN prefix)
|
|
70
|
+
const siRegex = /<(?:\w+:)?si><(?:\w+:)?t[^>]*>([^<]*)<\/(?:\w+:)?t><\/(?:\w+:)?si>/g;
|
|
141
71
|
let match;
|
|
142
72
|
while ((match = siRegex.exec(ssXml)) !== null) {
|
|
143
|
-
sharedStrings.push(match[1]);
|
|
73
|
+
sharedStrings.push(decodeXmlEntities(match[1]));
|
|
144
74
|
}
|
|
145
75
|
}
|
|
146
76
|
|
|
@@ -151,10 +81,10 @@ async function readXlsxV5(xlsxPath) {
|
|
|
151
81
|
}
|
|
152
82
|
const sheetXml = sheetEntry.getData().toString('utf8');
|
|
153
83
|
|
|
154
|
-
// Fast row extraction using regex —
|
|
84
|
+
// Fast row extraction using regex — handles both normal and namespace-prefixed XML
|
|
155
85
|
const allRows = [];
|
|
156
|
-
const rowRegex = /<row [^>]*>(.*?)<\/row>/gs;
|
|
157
|
-
const cellRegex = /<c r="([A-Z]+)\d+"(?: t="([^"]*)")?[^>]*>(?:<v>([^<]*)<\/v>|<is><t[^>]*>([^<]*)<\/t><\/is>)?<\/c>/g;
|
|
86
|
+
const rowRegex = /<(?:\w+:)?row [^>]*>(.*?)<\/(?:\w+:)?row>/gs;
|
|
87
|
+
const cellRegex = /<(?:\w+:)?c r="([A-Z]+)\d+"(?: t="([^"]*)")?[^>]*>(?:<(?:\w+:)?v>([^<]*)<\/(?:\w+:)?v>|<(?:\w+:)?is><(?:\w+:)?t[^>]*>([^<]*)<\/(?:\w+:)?t><\/(?:\w+:)?is>)?<\/(?:\w+:)?c>/g;
|
|
158
88
|
|
|
159
89
|
let rowMatch;
|
|
160
90
|
while ((rowMatch = rowRegex.exec(sheetXml)) !== null) {
|
|
@@ -251,201 +181,7 @@ function detectXlsxVersion(xlsxPath) {
|
|
|
251
181
|
return 'v5';
|
|
252
182
|
}
|
|
253
183
|
|
|
254
|
-
// ─── v3/v4 Legacy
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Create an XLSX file using streaming WorkbookWriter (v4 format).
|
|
258
|
-
* Memory-efficient: rows are freed after commit.
|
|
259
|
-
* @param {object} options
|
|
260
|
-
* @param {string} options.base64Content - Base64 content to store in this part
|
|
261
|
-
* @param {string} options.encryptionMeta - Packed encryption metadata (iv:salt:authTag) or ''
|
|
262
|
-
* @param {string} options.metadataJson - Serialized metadata JSON string
|
|
263
|
-
* @param {string} options.outputPath - Output file path
|
|
264
|
-
* @returns {Promise<string>} Path to created file
|
|
265
|
-
*/
|
|
266
|
-
async function createXlsxPartStreaming(options) {
|
|
267
|
-
const { base64Content, encryptionMeta, metadataJson, outputPath } = options;
|
|
268
|
-
|
|
269
|
-
// Ensure output directory exists
|
|
270
|
-
const outputDir = path.dirname(outputPath);
|
|
271
|
-
if (!fs.existsSync(outputDir)) {
|
|
272
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
|
|
276
|
-
filename: outputPath,
|
|
277
|
-
useSharedStrings: false, // Inline strings for easier post-processing
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
workbook.creator = 'Microsoft Excel';
|
|
281
|
-
workbook.lastModifiedBy = 'Microsoft Excel';
|
|
282
|
-
workbook.created = new Date();
|
|
283
|
-
workbook.modified = new Date();
|
|
284
|
-
|
|
285
|
-
// === Sheet 1: Visible decoy data (server metrics) ===
|
|
286
|
-
const visibleSheet = workbook.addWorksheet(VISIBLE_SHEET_NAME, {
|
|
287
|
-
properties: { tabColor: { argb: '4472C4' } },
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// Set column widths before adding rows
|
|
291
|
-
visibleSheet.columns = [
|
|
292
|
-
{ width: 20 }, // Timestamp
|
|
293
|
-
{ width: 16 }, // Server ID
|
|
294
|
-
{ width: 12 }, // Status
|
|
295
|
-
{ width: 8 }, // CPU %
|
|
296
|
-
{ width: 10 }, // Memory %
|
|
297
|
-
{ width: 8 }, // Disk %
|
|
298
|
-
{ width: 14 }, // Network (MB/s)
|
|
299
|
-
{ width: 10 }, // Requests
|
|
300
|
-
{ width: 14 }, // Resp Time (ms)
|
|
301
|
-
{ width: 12 }, // Uptime (hrs)
|
|
302
|
-
];
|
|
303
|
-
|
|
304
|
-
// Add headers
|
|
305
|
-
const headers = generateDecoyHeaders();
|
|
306
|
-
const headerRow = visibleSheet.addRow(headers);
|
|
307
|
-
|
|
308
|
-
const headerCount = headers.length;
|
|
309
|
-
for (let col = 1; col <= headerCount; col++) {
|
|
310
|
-
const cell = headerRow.getCell(col);
|
|
311
|
-
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
|
|
312
|
-
cell.fill = {
|
|
313
|
-
type: 'pattern',
|
|
314
|
-
pattern: 'solid',
|
|
315
|
-
fgColor: { argb: '2E7D32' },
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
headerRow.commit();
|
|
319
|
-
|
|
320
|
-
// Generate and write decoy data rows, committing each to free memory
|
|
321
|
-
const payloadSize = base64Content.length;
|
|
322
|
-
const rowCount = calculateDecoyRowCount(payloadSize);
|
|
323
|
-
const decoyData = generateDecoyData(rowCount);
|
|
324
|
-
|
|
325
|
-
for (const row of decoyData) {
|
|
326
|
-
const dataRow = visibleSheet.addRow(rowToArray(row));
|
|
327
|
-
dataRow.commit();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Add filters
|
|
331
|
-
visibleSheet.autoFilter = {
|
|
332
|
-
from: 'A1',
|
|
333
|
-
to: `J${rowCount + 1}`,
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
await visibleSheet.commit();
|
|
337
|
-
|
|
338
|
-
// === Sheet 2: Hidden payload (veryHidden) ===
|
|
339
|
-
const hiddenSheet = workbook.addWorksheet(HIDDEN_SHEET_NAME, {
|
|
340
|
-
state: 'veryHidden',
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// Split base64 content into cell-sized chunks
|
|
344
|
-
const chunks = splitIntoChunks(base64Content, CELL_CHUNK_SIZE);
|
|
345
|
-
|
|
346
|
-
// Row 1: metadata
|
|
347
|
-
const metaRow = hiddenSheet.getRow(1);
|
|
348
|
-
metaRow.getCell(1).value = encryptionMeta;
|
|
349
|
-
metaRow.getCell(2).value = metadataJson;
|
|
350
|
-
metaRow.getCell(3).value = chunks.length.toString();
|
|
351
|
-
metaRow.commit();
|
|
352
|
-
|
|
353
|
-
// Write cell chunks in rows starting from row 2, columns A-Z
|
|
354
|
-
const totalChunks = chunks.length;
|
|
355
|
-
const totalRows = Math.ceil(totalChunks / 26);
|
|
356
|
-
|
|
357
|
-
for (let rowIdx = 0; rowIdx < totalRows; rowIdx++) {
|
|
358
|
-
const sheetRow = hiddenSheet.getRow(rowIdx + 2);
|
|
359
|
-
const startChunk = rowIdx * 26;
|
|
360
|
-
const endChunk = Math.min(startChunk + 26, totalChunks);
|
|
361
|
-
|
|
362
|
-
for (let i = startChunk; i < endChunk; i++) {
|
|
363
|
-
const col = (i % 26) + 1;
|
|
364
|
-
sheetRow.getCell(col).value = chunks[i];
|
|
365
|
-
}
|
|
366
|
-
sheetRow.commit();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
await hiddenSheet.commit();
|
|
370
|
-
await workbook.commit();
|
|
371
|
-
|
|
372
|
-
// WorkbookWriter natively supports veryHidden state — no post-processing needed.
|
|
373
|
-
return outputPath;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Create an XLSX file with encrypted base64 content hidden in a veryHidden sheet (legacy v3)
|
|
378
|
-
*/
|
|
379
|
-
async function createXlsxWithBase64(options) {
|
|
380
|
-
const { base64Content, encryptionMeta, metadata, outputPath } = options;
|
|
381
|
-
|
|
382
|
-
const workbook = new ExcelJS.Workbook();
|
|
383
|
-
|
|
384
|
-
workbook.creator = 'Microsoft Excel';
|
|
385
|
-
workbook.lastModifiedBy = 'Microsoft Excel';
|
|
386
|
-
workbook.created = new Date();
|
|
387
|
-
workbook.modified = new Date();
|
|
388
|
-
|
|
389
|
-
const visibleSheet = workbook.addWorksheet(VISIBLE_SHEET_NAME, {
|
|
390
|
-
properties: { tabColor: { argb: '4472C4' } },
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const headers = generateDecoyHeaders();
|
|
394
|
-
const headerRow = visibleSheet.addRow(headers);
|
|
395
|
-
|
|
396
|
-
const headerCount = headers.length;
|
|
397
|
-
for (let col = 1; col <= headerCount; col++) {
|
|
398
|
-
const cell = headerRow.getCell(col);
|
|
399
|
-
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
|
|
400
|
-
cell.fill = {
|
|
401
|
-
type: 'pattern',
|
|
402
|
-
pattern: 'solid',
|
|
403
|
-
fgColor: { argb: '2E7D32' },
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const payloadSize = base64Content.length;
|
|
408
|
-
const rowCount = calculateDecoyRowCount(payloadSize);
|
|
409
|
-
|
|
410
|
-
const decoyData = generateDecoyData(rowCount);
|
|
411
|
-
decoyData.forEach((row) => {
|
|
412
|
-
visibleSheet.addRow(rowToArray(row));
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
visibleSheet.columns = [
|
|
416
|
-
{ width: 20 }, { width: 16 }, { width: 12 }, { width: 8 },
|
|
417
|
-
{ width: 10 }, { width: 8 }, { width: 14 }, { width: 10 },
|
|
418
|
-
{ width: 14 }, { width: 12 },
|
|
419
|
-
];
|
|
420
|
-
|
|
421
|
-
visibleSheet.autoFilter = { from: 'A1', to: `J${rowCount + 1}` };
|
|
422
|
-
|
|
423
|
-
const hiddenSheet = workbook.addWorksheet(HIDDEN_SHEET_NAME, {
|
|
424
|
-
state: 'veryHidden',
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
hiddenSheet.getCell('A1').value = encryptionMeta;
|
|
428
|
-
hiddenSheet.getCell('B1').value = metadata;
|
|
429
|
-
|
|
430
|
-
const chunks = splitIntoChunks(base64Content, CELL_CHUNK_SIZE);
|
|
431
|
-
hiddenSheet.getCell('C1').value = chunks.length.toString();
|
|
432
|
-
|
|
433
|
-
chunks.forEach((chunk, index) => {
|
|
434
|
-
const row = Math.floor(index / 26) + 2;
|
|
435
|
-
const col = (index % 26) + 1;
|
|
436
|
-
hiddenSheet.getCell(row, col).value = chunk;
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
const outputDir = path.dirname(outputPath);
|
|
440
|
-
if (!fs.existsSync(outputDir)) {
|
|
441
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
await workbook.xlsx.writeFile(outputPath);
|
|
445
|
-
await ensureVeryHidden(outputPath);
|
|
446
|
-
|
|
447
|
-
return outputPath;
|
|
448
|
-
}
|
|
184
|
+
// ─── v3/v4 Legacy XLSX Creation (removed — no longer passes air gap filters)
|
|
449
185
|
|
|
450
186
|
// ─── Unified Reader ─────────────────────────────────────────────────────────
|
|
451
187
|
|
|
@@ -561,38 +297,6 @@ async function extractFromXml(xlsxPath) {
|
|
|
561
297
|
|
|
562
298
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
563
299
|
|
|
564
|
-
async function ensureVeryHidden(xlsxPath) {
|
|
565
|
-
const zip = new AdmZip(xlsxPath);
|
|
566
|
-
|
|
567
|
-
const workbookEntry = zip.getEntry('xl/workbook.xml');
|
|
568
|
-
if (workbookEntry) {
|
|
569
|
-
let workbookXml = workbookEntry.getData().toString('utf8');
|
|
570
|
-
|
|
571
|
-
workbookXml = workbookXml.replace(
|
|
572
|
-
/(<sheet[^>]*name="Data"[^>]*)\s+state="[^"]*"([^>]*\/>)/gi,
|
|
573
|
-
'$1 state="veryHidden"$2'
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
if (!workbookXml.match(/<sheet[^>]*name="Data"[^>]*state="/i)) {
|
|
577
|
-
workbookXml = workbookXml.replace(
|
|
578
|
-
/(<sheet[^>]*name="Data")([^>]*\/>)/gi,
|
|
579
|
-
'$1 state="veryHidden"$2'
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
zip.updateFile('xl/workbook.xml', Buffer.from(workbookXml, 'utf8'));
|
|
584
|
-
zip.writeZip(xlsxPath);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function splitIntoChunks(str, size) {
|
|
589
|
-
const chunks = [];
|
|
590
|
-
for (let i = 0; i < str.length; i += size) {
|
|
591
|
-
chunks.push(str.slice(i, i + size));
|
|
592
|
-
}
|
|
593
|
-
return chunks;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
300
|
function columnToLetter(col) {
|
|
597
301
|
let letter = '';
|
|
598
302
|
while (col > 0) {
|
|
@@ -608,9 +312,6 @@ module.exports = {
|
|
|
608
312
|
createXlsxPartV5,
|
|
609
313
|
readXlsxV5,
|
|
610
314
|
detectXlsxVersion,
|
|
611
|
-
//
|
|
612
|
-
createXlsxPartStreaming,
|
|
613
|
-
createXlsxWithBase64,
|
|
614
|
-
// Unified reader
|
|
315
|
+
// Unified reader (auto-detects v5 vs legacy)
|
|
615
316
|
readXlsxBase64,
|
|
616
317
|
};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw XLSX Writer (v5.x)
|
|
3
|
+
*
|
|
4
|
+
* Builds XLSX files from raw XML — no ExcelJS dependency.
|
|
5
|
+
* Produces output identical to genuine Microsoft Excel, which passes
|
|
6
|
+
* air gap filters that reject programmatically-generated files.
|
|
7
|
+
*
|
|
8
|
+
* Architecture: XML templates + shared string table + AdmZip packaging.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const zlib = require('zlib');
|
|
15
|
+
|
|
16
|
+
// ─── XML Escape ──────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function escapeXml(s) {
|
|
19
|
+
return String(s)
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Column Letter Helper ───────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function colLetter(idx) {
|
|
29
|
+
let s = '', n = idx + 1;
|
|
30
|
+
while (n > 0) { n--; s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26); }
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Shared Strings Table ───────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function buildSharedStrings(allValues) {
|
|
37
|
+
// Collect string values in order, build index map
|
|
38
|
+
let count = 0;
|
|
39
|
+
const unique = [];
|
|
40
|
+
const map = new Map();
|
|
41
|
+
for (const val of allValues) {
|
|
42
|
+
if (typeof val !== 'number') {
|
|
43
|
+
const s = String(val);
|
|
44
|
+
count++;
|
|
45
|
+
if (!map.has(s)) {
|
|
46
|
+
map.set(s, unique.length);
|
|
47
|
+
unique.push(s);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build XML using array join to avoid O(n²) string concatenation
|
|
53
|
+
const parts = [`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${count}" uniqueCount="${unique.length}">`];
|
|
54
|
+
for (const s of unique) {
|
|
55
|
+
parts.push(`<si><t>${escapeXml(s)}</t></si>`);
|
|
56
|
+
}
|
|
57
|
+
parts.push('</sst>');
|
|
58
|
+
return { xml: parts.join(''), map };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Sheet XML Builder ──────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function buildSheetXml(headers, rows, ssMap, opts = {}) {
|
|
64
|
+
const totalRows = rows.length + 1;
|
|
65
|
+
const totalCols = headers.length;
|
|
66
|
+
const lastCol = colLetter(totalCols - 1);
|
|
67
|
+
const uid = `{${crypto.randomUUID().toUpperCase()}}`;
|
|
68
|
+
|
|
69
|
+
// Pre-compute column letters for all columns
|
|
70
|
+
const colLetters = [];
|
|
71
|
+
for (let i = 0; i < totalCols; i++) colLetters.push(colLetter(i));
|
|
72
|
+
|
|
73
|
+
// Build XML using array join for O(n) performance
|
|
74
|
+
const parts = [];
|
|
75
|
+
parts.push(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac xr xr2 xr3" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xr:uid="${uid}">`);
|
|
76
|
+
parts.push(`<dimension ref="A1:${lastCol}${totalRows}"/>`);
|
|
77
|
+
parts.push(`<sheetViews><sheetView tabSelected="1" workbookViewId="0"><selection activeCell="A1" sqref="A1"/></sheetView></sheetViews>`);
|
|
78
|
+
parts.push(`<sheetFormatPr defaultRowHeight="15" x14ac:dyDescent="0.25"/>`);
|
|
79
|
+
|
|
80
|
+
if (opts.colWidths) {
|
|
81
|
+
parts.push('<cols>');
|
|
82
|
+
opts.colWidths.forEach((w, i) => parts.push(`<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`));
|
|
83
|
+
parts.push('</cols>');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
parts.push('<sheetData>');
|
|
87
|
+
|
|
88
|
+
// Header row
|
|
89
|
+
parts.push(`<row r="1" spans="1:${totalCols}" x14ac:dyDescent="0.25">`);
|
|
90
|
+
for (let i = 0; i < totalCols; i++) {
|
|
91
|
+
parts.push(`<c r="${colLetters[i]}1" t="s"><v>${ssMap.get(String(headers[i]))}</v></c>`);
|
|
92
|
+
}
|
|
93
|
+
parts.push('</row>');
|
|
94
|
+
|
|
95
|
+
// Data rows
|
|
96
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
97
|
+
const row = rows[ri];
|
|
98
|
+
const rowNum = ri + 2;
|
|
99
|
+
parts.push(`<row r="${rowNum}" spans="1:${totalCols}" x14ac:dyDescent="0.25">`);
|
|
100
|
+
for (let ci = 0; ci < row.length; ci++) {
|
|
101
|
+
const val = row[ci];
|
|
102
|
+
const ref = `${colLetters[ci]}${rowNum}`;
|
|
103
|
+
if (typeof val === 'number') {
|
|
104
|
+
parts.push(`<c r="${ref}"><v>${val}</v></c>`);
|
|
105
|
+
} else {
|
|
106
|
+
const si = ssMap.get(String(val));
|
|
107
|
+
if (si !== undefined) {
|
|
108
|
+
parts.push(`<c r="${ref}" t="s"><v>${si}</v></c>`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
parts.push('</row>');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
parts.push('</sheetData>');
|
|
116
|
+
if (opts.autoFilter) parts.push(`<autoFilter ref="A1:${lastCol}${totalRows}"/>`);
|
|
117
|
+
parts.push('<pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>');
|
|
118
|
+
parts.push('</worksheet>');
|
|
119
|
+
return parts.join('');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Static OOXML Parts ─────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
const CONTENT_TYPES = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
125
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
|
|
126
|
+
|
|
127
|
+
const RELS = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
128
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`;
|
|
129
|
+
|
|
130
|
+
const WORKBOOK_RELS = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
131
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/></Relationships>`;
|
|
132
|
+
|
|
133
|
+
function makeWorkbook(sheetName) {
|
|
134
|
+
const docId = `8_{${crypto.randomUUID().toUpperCase()}}`;
|
|
135
|
+
const viewUid = `{${crypto.randomUUID().toUpperCase()}}`;
|
|
136
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
137
|
+
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x15 xr xr6 xr10 xr2" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2"><fileVersion appName="xl" lastEdited="7" lowestEdited="7" rupBuild="29725"/><workbookPr defaultThemeVersion="202300"/><xr:revisionPtr revIDLastSave="0" documentId="${docId}" xr6:coauthVersionLast="47" xr6:coauthVersionMax="47" xr10:uidLastSave="{00000000-0000-0000-0000-000000000000}"/><bookViews><workbookView xWindow="-120" yWindow="-120" windowWidth="29040" windowHeight="17520" xr2:uid="${viewUid}"/></bookViews><sheets><sheet name="${escapeXml(sheetName)}" sheetId="1" r:id="rId1"/></sheets><calcPr calcId="191029"/><extLst><ext uri="{140A7094-0E35-4892-8432-C4D2E57EDEB5}" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"><x15:workbookPr chartTrackingRefBase="1"/></ext><ext uri="{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}" xmlns:xcalcf="http://schemas.microsoft.com/office/spreadsheetml/2018/calcfeatures"><xcalcf:calcFeatures><xcalcf:feature name="microsoft.com:RD"/><xcalcf:feature name="microsoft.com:Single"/><xcalcf:feature name="microsoft.com:FV"/><xcalcf:feature name="microsoft.com:CNMTM"/><xcalcf:feature name="microsoft.com:LET_WF"/><xcalcf:feature name="microsoft.com:LAMBDA_WF"/><xcalcf:feature name="microsoft.com:ARRAYTEXT_WF"/></xcalcf:calcFeatures></ext></extLst></workbook>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const STYLES = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
141
|
+
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac x16r2 xr" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision"><fonts count="1" x14ac:knownFonts="1"><font><sz val="11"/><color theme="1"/><name val="Aptos Narrow"/><family val="2"/><scheme val="minor"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles><dxfs count="0"/><tableStyles count="0" defaultTableStyle="TableStyleMedium2" defaultPivotStyle="PivotStyleLight16"/><extLst><ext uri="{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"><x14:slicerStyles defaultSlicerStyle="SlicerStyleLight1"/></ext><ext uri="{9260A510-F301-46a8-8635-F512D64BE5F5}" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"><x15:timelineStyles defaultTimelineStyle="TimeSlicerStyleLight1"/></ext></extLst></styleSheet>`;
|
|
142
|
+
|
|
143
|
+
// Theme extracted from genuine Microsoft Excel 2024 output
|
|
144
|
+
const THEME = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
145
|
+
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="0E2841"/></a:dk2><a:lt2><a:srgbClr val="E8E8E8"/></a:lt2><a:accent1><a:srgbClr val="156082"/></a:accent1><a:accent2><a:srgbClr val="E97132"/></a:accent2><a:accent3><a:srgbClr val="196B24"/></a:accent3><a:accent4><a:srgbClr val="0F9ED5"/></a:accent4><a:accent5><a:srgbClr val="A02B93"/></a:accent5><a:accent6><a:srgbClr val="4EA72E"/></a:accent6><a:hlink><a:srgbClr val="467886"/></a:hlink><a:folHlink><a:srgbClr val="96607D"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Aptos Display" panose="02110004020202020204"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游ゴシック Light"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线 Light"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:majorFont><a:minorFont><a:latin typeface="Aptos Narrow" panose="02110004020202020204"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游ゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults><a:lnDef><a:spPr/><a:bodyPr/><a:lstStyle/><a:style><a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef><a:fillRef idx="0"><a:schemeClr val="accent1"/></a:fillRef><a:effectRef idx="1"><a:schemeClr val="accent1"/></a:effectRef><a:fontRef idx="minor"><a:schemeClr val="tx1"/></a:fontRef></a:style></a:lnDef></a:objectDefaults><a:extraClrSchemeLst/><a:extLst><a:ext uri="{05A4C25C-085E-4340-85A3-A5531E510DB2}"><thm15:themeFamily xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" name="Office Theme" id="{2E142A2C-CD16-42D6-873A-C26D2A0506FA}" vid="{1BDDFF52-6CD6-40A5-AB3C-68EB2F1E4D0A}"/></a:ext></a:extLst></a:theme>`;
|
|
146
|
+
|
|
147
|
+
function makeCoreProps() {
|
|
148
|
+
const now = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
|
|
149
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
150
|
+
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:creator>Microsoft Excel</dc:creator><cp:lastModifiedBy>Microsoft Excel</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function makeAppProps(sheetName) {
|
|
154
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
155
|
+
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>Microsoft Excel</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="2" baseType="variant"><vt:variant><vt:lpstr>Worksheets</vt:lpstr></vt:variant><vt:variant><vt:i4>1</vt:i4></vt:variant></vt:vector></HeadingPairs><TitlesOfParts><vt:vector size="1" baseType="lpstr"><vt:lpstr>${escapeXml(sheetName)}</vt:lpstr></vt:vector></TitlesOfParts><Company></Company><LinksUpToDate>false</LinksUpToDate><SharedDoc>false</SharedDoc><HyperlinksChanged>false</HyperlinksChanged><AppVersion>16.0300</AppVersion></Properties>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── CRC-32 ──────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
const crcTable = (() => {
|
|
161
|
+
const t = new Uint32Array(256);
|
|
162
|
+
for (let i = 0; i < 256; i++) {
|
|
163
|
+
let c = i;
|
|
164
|
+
for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
165
|
+
t[i] = c;
|
|
166
|
+
}
|
|
167
|
+
return t;
|
|
168
|
+
})();
|
|
169
|
+
|
|
170
|
+
function crc32(buf) {
|
|
171
|
+
let crc = 0xFFFFFFFF;
|
|
172
|
+
for (let i = 0; i < buf.length; i++) crc = crcTable[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
|
|
173
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Ordered ZIP Writer ──────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function writeZipOrdered(entries, outputPath) {
|
|
179
|
+
const localParts = [];
|
|
180
|
+
const centralParts = [];
|
|
181
|
+
let offset = 0;
|
|
182
|
+
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const nameBuffer = Buffer.from(entry.name, 'utf8');
|
|
185
|
+
const uncompressed = entry.data;
|
|
186
|
+
const compressed = zlib.deflateRawSync(uncompressed);
|
|
187
|
+
const crc = crc32(uncompressed);
|
|
188
|
+
|
|
189
|
+
// Local file header
|
|
190
|
+
const local = Buffer.alloc(30 + nameBuffer.length);
|
|
191
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
192
|
+
local.writeUInt16LE(20, 4);
|
|
193
|
+
local.writeUInt16LE(0, 6);
|
|
194
|
+
local.writeUInt16LE(8, 8); // deflate
|
|
195
|
+
local.writeUInt16LE(0, 10);
|
|
196
|
+
local.writeUInt16LE(0, 12);
|
|
197
|
+
local.writeUInt32LE(crc, 14);
|
|
198
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
199
|
+
local.writeUInt32LE(uncompressed.length, 22);
|
|
200
|
+
local.writeUInt16LE(nameBuffer.length, 26);
|
|
201
|
+
local.writeUInt16LE(0, 28);
|
|
202
|
+
nameBuffer.copy(local, 30);
|
|
203
|
+
|
|
204
|
+
// Central directory
|
|
205
|
+
const central = Buffer.alloc(46 + nameBuffer.length);
|
|
206
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
207
|
+
central.writeUInt16LE(20, 4);
|
|
208
|
+
central.writeUInt16LE(20, 6);
|
|
209
|
+
central.writeUInt16LE(0, 8);
|
|
210
|
+
central.writeUInt16LE(8, 10);
|
|
211
|
+
central.writeUInt16LE(0, 12);
|
|
212
|
+
central.writeUInt16LE(0, 14);
|
|
213
|
+
central.writeUInt32LE(crc, 16);
|
|
214
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
215
|
+
central.writeUInt32LE(uncompressed.length, 24);
|
|
216
|
+
central.writeUInt16LE(nameBuffer.length, 28);
|
|
217
|
+
central.writeUInt16LE(0, 30);
|
|
218
|
+
central.writeUInt16LE(0, 32);
|
|
219
|
+
central.writeUInt16LE(0, 34);
|
|
220
|
+
central.writeUInt16LE(0, 36);
|
|
221
|
+
central.writeUInt32LE(0, 38);
|
|
222
|
+
central.writeUInt32LE(offset, 42);
|
|
223
|
+
nameBuffer.copy(central, 46);
|
|
224
|
+
|
|
225
|
+
centralParts.push(central);
|
|
226
|
+
localParts.push(local, compressed);
|
|
227
|
+
offset += local.length + compressed.length;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const centralBuf = Buffer.concat(centralParts);
|
|
231
|
+
const eocd = Buffer.alloc(22);
|
|
232
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
233
|
+
eocd.writeUInt16LE(0, 4);
|
|
234
|
+
eocd.writeUInt16LE(0, 6);
|
|
235
|
+
eocd.writeUInt16LE(entries.length, 8);
|
|
236
|
+
eocd.writeUInt16LE(entries.length, 10);
|
|
237
|
+
eocd.writeUInt32LE(centralBuf.length, 12);
|
|
238
|
+
eocd.writeUInt32LE(offset, 16);
|
|
239
|
+
eocd.writeUInt16LE(0, 20);
|
|
240
|
+
|
|
241
|
+
const dir = path.dirname(outputPath);
|
|
242
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
243
|
+
|
|
244
|
+
fs.writeFileSync(outputPath, Buffer.concat([...localParts, centralBuf, eocd]));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create an XLSX file with log data embedded as shared strings.
|
|
251
|
+
* Produces output matching genuine Microsoft Excel format.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} options
|
|
254
|
+
* @param {string[]} options.headers - Column header strings
|
|
255
|
+
* @param {Array<Array>} options.rows - Row arrays (strings and numbers)
|
|
256
|
+
* @param {string} options.sheetName - Worksheet name
|
|
257
|
+
* @param {string} options.outputPath - Output file path
|
|
258
|
+
* @param {number[]} [options.colWidths] - Column widths
|
|
259
|
+
* @param {boolean} [options.autoFilter] - Add autoFilter
|
|
260
|
+
* @returns {string} Path to created file
|
|
261
|
+
*/
|
|
262
|
+
function createXlsxRaw(options) {
|
|
263
|
+
const { headers, rows, sheetName, outputPath, colWidths, autoFilter } = options;
|
|
264
|
+
|
|
265
|
+
// Collect all cell values for shared strings
|
|
266
|
+
const allValues = [...headers];
|
|
267
|
+
for (const row of rows) {
|
|
268
|
+
for (const val of row) allValues.push(val);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { xml: ssXml, map: ssMap } = buildSharedStrings(allValues);
|
|
272
|
+
const sheetXml = buildSheetXml(headers, rows, ssMap, { colWidths, autoFilter });
|
|
273
|
+
|
|
274
|
+
// Assemble ZIP in the same entry order as real Excel
|
|
275
|
+
const entries = [
|
|
276
|
+
{ name: '[Content_Types].xml', data: Buffer.from(CONTENT_TYPES, 'utf8') },
|
|
277
|
+
{ name: '_rels/.rels', data: Buffer.from(RELS, 'utf8') },
|
|
278
|
+
{ name: 'xl/workbook.xml', data: Buffer.from(makeWorkbook(sheetName), 'utf8') },
|
|
279
|
+
{ name: 'xl/_rels/workbook.xml.rels', data: Buffer.from(WORKBOOK_RELS, 'utf8') },
|
|
280
|
+
{ name: 'xl/worksheets/sheet1.xml', data: Buffer.from(sheetXml, 'utf8') },
|
|
281
|
+
{ name: 'xl/theme/theme1.xml', data: Buffer.from(THEME, 'utf8') },
|
|
282
|
+
{ name: 'xl/styles.xml', data: Buffer.from(STYLES, 'utf8') },
|
|
283
|
+
{ name: 'xl/sharedStrings.xml', data: Buffer.from(ssXml, 'utf8') },
|
|
284
|
+
{ name: 'docProps/core.xml', data: Buffer.from(makeCoreProps(), 'utf8') },
|
|
285
|
+
{ name: 'docProps/app.xml', data: Buffer.from(makeAppProps(sheetName), 'utf8') },
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
writeZipOrdered(entries, outputPath);
|
|
289
|
+
return outputPath;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
createXlsxRaw,
|
|
294
|
+
buildSharedStrings,
|
|
295
|
+
buildSheetXml,
|
|
296
|
+
escapeXml,
|
|
297
|
+
colLetter,
|
|
298
|
+
};
|