stegdoc 5.6.0 → 6.0.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/README.md +231 -106
- package/package.json +25 -3
- package/src/commands/decode.js +97 -149
- package/src/commands/encode.js +152 -173
- package/src/commands/info.js +49 -0
- package/src/commands/verify.js +66 -0
- package/src/index.js +9 -5
- package/src/lib/bindings.js +99 -0
- package/src/lib/log-generator.js +5 -2
- package/src/lib/native.js +68 -0
- package/src/lib/streams.js +0 -107
- package/src/lib/utils.js +20 -0
- package/src/lib/xlsx-handler.js +22 -321
- package/src/lib/xlsx-writer.js +308 -0
- package/src/lib/file-handler.js +0 -113
|
@@ -0,0 +1,308 @@
|
|
|
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
|
+
// Raw OOXML parts, exported so spec/tools/generate-xlsx-static.js can emit
|
|
299
|
+
// the Rust writer's constants from this frozen implementation.
|
|
300
|
+
CONTENT_TYPES,
|
|
301
|
+
RELS,
|
|
302
|
+
WORKBOOK_RELS,
|
|
303
|
+
STYLES,
|
|
304
|
+
THEME,
|
|
305
|
+
makeWorkbook,
|
|
306
|
+
makeCoreProps,
|
|
307
|
+
makeAppProps,
|
|
308
|
+
};
|
package/src/lib/file-handler.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Read a file and encode it to base64
|
|
6
|
-
* @param {string} filePath - Path to the file
|
|
7
|
-
* @returns {object} Object containing base64 string, filename, extension, and size
|
|
8
|
-
*/
|
|
9
|
-
function encodeFileToBase64(filePath) {
|
|
10
|
-
if (!fs.existsSync(filePath)) {
|
|
11
|
-
throw new Error(`File not found: ${filePath}`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const stats = fs.statSync(filePath);
|
|
15
|
-
|
|
16
|
-
if (!stats.isFile()) {
|
|
17
|
-
throw new Error(`Path is not a file: ${filePath}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const fileBuffer = fs.readFileSync(filePath);
|
|
21
|
-
const base64 = fileBuffer.toString('base64');
|
|
22
|
-
const filename = path.basename(filePath);
|
|
23
|
-
const extension = path.extname(filePath);
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
base64,
|
|
27
|
-
filename,
|
|
28
|
-
extension,
|
|
29
|
-
size: stats.size,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Decode base64 string and write to file
|
|
35
|
-
* @param {string} base64 - Base64 encoded string
|
|
36
|
-
* @param {string} outputPath - Output file path
|
|
37
|
-
*/
|
|
38
|
-
function decodeBase64ToFile(base64, outputPath) {
|
|
39
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
40
|
-
|
|
41
|
-
// Ensure output directory exists
|
|
42
|
-
const outputDir = path.dirname(outputPath);
|
|
43
|
-
if (!fs.existsSync(outputDir)) {
|
|
44
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
fs.writeFileSync(outputPath, buffer);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Split base64 string into chunks based on size limit
|
|
52
|
-
* @param {string} base64 - Base64 string to split
|
|
53
|
-
* @param {number} chunkSizeBytes - Maximum size per chunk in bytes
|
|
54
|
-
* @returns {Array<string>} Array of base64 chunks
|
|
55
|
-
*/
|
|
56
|
-
function splitBase64(base64, chunkSizeBytes) {
|
|
57
|
-
const chunks = [];
|
|
58
|
-
let offset = 0;
|
|
59
|
-
|
|
60
|
-
while (offset < base64.length) {
|
|
61
|
-
chunks.push(base64.slice(offset, offset + chunkSizeBytes));
|
|
62
|
-
offset += chunkSizeBytes;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return chunks;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Merge base64 chunks back into a single string
|
|
70
|
-
* @param {Array<string>} chunks - Array of base64 chunks
|
|
71
|
-
* @returns {string} Merged base64 string
|
|
72
|
-
*/
|
|
73
|
-
function mergeBase64Chunks(chunks) {
|
|
74
|
-
return chunks.join('');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Calculate how many chunks will be needed for a file
|
|
79
|
-
* @param {number} fileSize - File size in bytes
|
|
80
|
-
* @param {number} chunkSizeBytes - Chunk size in bytes
|
|
81
|
-
* @returns {number} Number of chunks needed
|
|
82
|
-
*/
|
|
83
|
-
function calculateChunkCount(fileSize, chunkSizeBytes) {
|
|
84
|
-
// Base64 encoding increases size by ~33%
|
|
85
|
-
const base64Size = Math.ceil(fileSize * 4 / 3);
|
|
86
|
-
return Math.ceil(base64Size / chunkSizeBytes);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Validate if a path is writable
|
|
91
|
-
* @param {string} dirPath - Directory path to check
|
|
92
|
-
* @returns {boolean} True if writable
|
|
93
|
-
*/
|
|
94
|
-
function isDirectoryWritable(dirPath) {
|
|
95
|
-
try {
|
|
96
|
-
if (!fs.existsSync(dirPath)) {
|
|
97
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
98
|
-
}
|
|
99
|
-
fs.accessSync(dirPath, fs.constants.W_OK);
|
|
100
|
-
return true;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
module.exports = {
|
|
107
|
-
encodeFileToBase64,
|
|
108
|
-
decodeBase64ToFile,
|
|
109
|
-
splitBase64,
|
|
110
|
-
mergeBase64Chunks,
|
|
111
|
-
calculateChunkCount,
|
|
112
|
-
isDirectoryWritable,
|
|
113
|
-
};
|