stegdoc 5.7.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -106
- package/package.json +27 -5
- package/src/commands/decode.js +78 -436
- package/src/commands/encode.js +214 -137
- package/src/commands/info.js +65 -94
- package/src/commands/verify.js +76 -176
- package/src/index.js +9 -6
- package/src/lib/bindings.js +99 -0
- package/src/lib/compression.js +1 -161
- package/src/lib/crypto.js +0 -107
- package/src/lib/docx-handler.js +2 -278
- package/src/lib/log-generator.js +5 -2
- package/src/lib/metadata.js +7 -88
- package/src/lib/native.js +68 -0
- package/src/lib/streams.js +0 -139
- package/src/lib/utils.js +0 -27
- package/src/lib/xlsx-handler.js +1 -272
- package/src/lib/xlsx-writer.js +10 -0
- package/src/lib/file-handler.js +0 -113
- package/src/lib/file-utils.js +0 -160
- package/src/lib/xml-utils.js +0 -115
package/src/lib/streams.js
CHANGED
|
@@ -1,139 +1,4 @@
|
|
|
1
1
|
const { Transform, Writable } = require('stream');
|
|
2
|
-
const crypto = require('crypto');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Transform stream that converts binary input to base64 text output.
|
|
6
|
-
* Buffers incomplete 3-byte groups across chunk boundaries.
|
|
7
|
-
*/
|
|
8
|
-
class Base64EncodeTransform extends Transform {
|
|
9
|
-
constructor() {
|
|
10
|
-
super();
|
|
11
|
-
this._remainder = Buffer.alloc(0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
_transform(chunk, encoding, callback) {
|
|
15
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
16
|
-
const combined = this._remainder.length > 0 ? Buffer.concat([this._remainder, buf]) : buf;
|
|
17
|
-
const usable = combined.length - (combined.length % 3);
|
|
18
|
-
if (usable > 0) {
|
|
19
|
-
this.push(combined.slice(0, usable).toString('base64'));
|
|
20
|
-
}
|
|
21
|
-
this._remainder = usable < combined.length ? combined.slice(usable) : Buffer.alloc(0);
|
|
22
|
-
callback();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
_flush(callback) {
|
|
26
|
-
if (this._remainder.length > 0) {
|
|
27
|
-
this.push(this._remainder.toString('base64'));
|
|
28
|
-
this._remainder = Buffer.alloc(0);
|
|
29
|
-
}
|
|
30
|
-
callback();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Transform stream that converts base64 text input to binary output.
|
|
36
|
-
* Buffers incomplete 4-char groups across chunk boundaries.
|
|
37
|
-
*/
|
|
38
|
-
class Base64DecodeTransform extends Transform {
|
|
39
|
-
constructor() {
|
|
40
|
-
super();
|
|
41
|
-
this._remainder = '';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
_transform(chunk, encoding, callback) {
|
|
45
|
-
const str = this._remainder + (Buffer.isBuffer(chunk) ? chunk.toString() : chunk);
|
|
46
|
-
const usable = str.length - (str.length % 4);
|
|
47
|
-
if (usable > 0) {
|
|
48
|
-
this.push(Buffer.from(str.slice(0, usable), 'base64'));
|
|
49
|
-
}
|
|
50
|
-
this._remainder = usable < str.length ? str.slice(usable) : '';
|
|
51
|
-
callback();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
_flush(callback) {
|
|
55
|
-
if (this._remainder.length > 0) {
|
|
56
|
-
this.push(Buffer.from(this._remainder, 'base64'));
|
|
57
|
-
this._remainder = '';
|
|
58
|
-
}
|
|
59
|
-
callback();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Transform stream that passes data through unchanged while computing SHA-256 hash.
|
|
65
|
-
* Access the hex hash via .digest after the stream has ended.
|
|
66
|
-
*/
|
|
67
|
-
class HashPassthrough extends Transform {
|
|
68
|
-
constructor() {
|
|
69
|
-
super();
|
|
70
|
-
this._hash = crypto.createHash('sha256');
|
|
71
|
-
this._finalized = false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
_transform(chunk, encoding, callback) {
|
|
75
|
-
this._hash.update(chunk);
|
|
76
|
-
this.push(chunk);
|
|
77
|
-
callback();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
_flush(callback) {
|
|
81
|
-
this._finalized = true;
|
|
82
|
-
callback();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
get digest() {
|
|
86
|
-
if (!this._finalized) {
|
|
87
|
-
throw new Error('Cannot read digest before stream has ended');
|
|
88
|
-
}
|
|
89
|
-
return this._hash.digest('hex');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Writable stream that collects string output up to maxBytes.
|
|
95
|
-
* Calls an async onChunkReady callback when a chunk is full, applying
|
|
96
|
-
* backpressure to pause upstream until the callback resolves.
|
|
97
|
-
*/
|
|
98
|
-
class ChunkCollector extends Writable {
|
|
99
|
-
constructor(maxBytes, onChunkReady) {
|
|
100
|
-
super({ decodeStrings: false });
|
|
101
|
-
this._maxBytes = maxBytes;
|
|
102
|
-
this._buffer = '';
|
|
103
|
-
this._chunkIndex = 0;
|
|
104
|
-
this._onChunkReady = onChunkReady;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async _write(chunk, encoding, callback) {
|
|
108
|
-
try {
|
|
109
|
-
this._buffer += typeof chunk === 'string' ? chunk : chunk.toString();
|
|
110
|
-
while (this._buffer.length >= this._maxBytes) {
|
|
111
|
-
const piece = this._buffer.slice(0, this._maxBytes);
|
|
112
|
-
this._buffer = this._buffer.slice(this._maxBytes);
|
|
113
|
-
await this._onChunkReady(piece, this._chunkIndex++);
|
|
114
|
-
}
|
|
115
|
-
callback();
|
|
116
|
-
} catch (err) {
|
|
117
|
-
callback(err);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async _final(callback) {
|
|
122
|
-
try {
|
|
123
|
-
if (this._buffer.length > 0) {
|
|
124
|
-
await this._onChunkReady(this._buffer, this._chunkIndex++);
|
|
125
|
-
this._buffer = '';
|
|
126
|
-
}
|
|
127
|
-
callback();
|
|
128
|
-
} catch (err) {
|
|
129
|
-
callback(err);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
get totalChunks() {
|
|
134
|
-
return this._chunkIndex;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
2
|
|
|
138
3
|
/**
|
|
139
4
|
* Writable stream that collects binary Buffer output up to maxBytes.
|
|
@@ -221,10 +86,6 @@ class ProgressTransform extends Transform {
|
|
|
221
86
|
}
|
|
222
87
|
|
|
223
88
|
module.exports = {
|
|
224
|
-
Base64EncodeTransform,
|
|
225
|
-
Base64DecodeTransform,
|
|
226
|
-
HashPassthrough,
|
|
227
|
-
ChunkCollector,
|
|
228
89
|
BinaryChunkCollector,
|
|
229
90
|
ProgressTransform,
|
|
230
91
|
};
|
package/src/lib/utils.js
CHANGED
|
@@ -9,15 +9,6 @@ function generateHash(length = 8) {
|
|
|
9
9
|
return crypto.randomBytes(length).toString('hex');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Generate SHA-256 hash of a buffer for integrity verification
|
|
14
|
-
* @param {Buffer} buffer - Data to hash
|
|
15
|
-
* @returns {string} Hexadecimal SHA-256 hash
|
|
16
|
-
*/
|
|
17
|
-
function generateContentHash(buffer) {
|
|
18
|
-
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
12
|
/**
|
|
22
13
|
* Parse size string to bytes
|
|
23
14
|
* @param {string} sizeStr - Size string (e.g., "5MB", "100KB", "1GB")
|
|
@@ -113,13 +104,6 @@ function generateFilename(hash, partNumber = null, totalParts = null, format = '
|
|
|
113
104
|
}
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
/**
|
|
117
|
-
* Legacy alias for backward compatibility
|
|
118
|
-
*/
|
|
119
|
-
function generateDocxFilename(hash, partNumber = null, totalParts = null) {
|
|
120
|
-
return generateFilename(hash, partNumber, totalParts, 'docx').replace(/\.docx$/, '');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
107
|
/**
|
|
124
108
|
* Parse filename to extract hash, part information, and format
|
|
125
109
|
* @param {string} filename - Filename to parse
|
|
@@ -195,13 +179,6 @@ function parseFilename(filename) {
|
|
|
195
179
|
return null;
|
|
196
180
|
}
|
|
197
181
|
|
|
198
|
-
/**
|
|
199
|
-
* Legacy alias for backward compatibility
|
|
200
|
-
*/
|
|
201
|
-
function parseDocxFilename(filename) {
|
|
202
|
-
return parseFilename(filename);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
182
|
/**
|
|
206
183
|
* Detect file format from extension
|
|
207
184
|
* @param {string} filename - Filename to check
|
|
@@ -215,13 +192,9 @@ function detectFormat(filename) {
|
|
|
215
192
|
|
|
216
193
|
module.exports = {
|
|
217
194
|
generateHash,
|
|
218
|
-
generateContentHash,
|
|
219
195
|
parseSizeToBytes,
|
|
220
196
|
formatBytes,
|
|
221
197
|
generateFilename,
|
|
222
198
|
parseFilename,
|
|
223
199
|
detectFormat,
|
|
224
|
-
// Legacy aliases for backward compatibility
|
|
225
|
-
generateDocxFilename,
|
|
226
|
-
parseDocxFilename,
|
|
227
200
|
};
|
package/src/lib/xlsx-handler.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const AdmZip = require('adm-zip');
|
|
4
|
-
const { generateLogHeaders, encodePayloadToLogLines, decodeLogLines, resetTimeState } = require('./log-generator');
|
|
5
|
-
const { parseXmlFromZip, ensureArray, extractTextContent } = require('./xml-utils');
|
|
1
|
+
const { generateLogHeaders, encodePayloadToLogLines } = require('./log-generator');
|
|
6
2
|
const { createXlsxRaw } = require('./xlsx-writer');
|
|
7
3
|
|
|
8
4
|
const V5_SHEET_NAME = 'Access Logs';
|
|
@@ -45,273 +41,6 @@ async function createXlsxPartV5(options) {
|
|
|
45
41
|
return outputPath;
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
/**
|
|
49
|
-
* Read a v5 log-embed XLSX file and extract payload.
|
|
50
|
-
* Uses fast regex-based XML scanning instead of full DOM parsing for speed.
|
|
51
|
-
* @param {string} xlsxPath - Path to XLSX file
|
|
52
|
-
* @returns {object} { payloadBuffer, metadataJson, encryptionMeta, metadata }
|
|
53
|
-
*/
|
|
54
|
-
async function readXlsxV5(xlsxPath) {
|
|
55
|
-
if (!fs.existsSync(xlsxPath)) {
|
|
56
|
-
throw new Error(`XLSX file not found: ${xlsxPath}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Read file once and reuse the zip instance
|
|
60
|
-
const fileBuffer = fs.readFileSync(xlsxPath);
|
|
61
|
-
const zip = new AdmZip(fileBuffer);
|
|
62
|
-
|
|
63
|
-
// Parse shared strings — handles both normal and namespace-prefixed XML (ns0:si, ns0:t)
|
|
64
|
-
let sharedStrings = null;
|
|
65
|
-
const ssEntry = zip.getEntry('xl/sharedStrings.xml');
|
|
66
|
-
if (ssEntry) {
|
|
67
|
-
sharedStrings = [];
|
|
68
|
-
const ssXml = ssEntry.getData().toString('utf8');
|
|
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;
|
|
71
|
-
let match;
|
|
72
|
-
while ((match = siRegex.exec(ssXml)) !== null) {
|
|
73
|
-
sharedStrings.push(decodeXmlEntities(match[1]));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Extract sheet1.xml raw string
|
|
78
|
-
const sheetEntry = zip.getEntry('xl/worksheets/sheet1.xml');
|
|
79
|
-
if (!sheetEntry) {
|
|
80
|
-
throw new Error('Sheet not found in XLSX file.');
|
|
81
|
-
}
|
|
82
|
-
const sheetXml = sheetEntry.getData().toString('utf8');
|
|
83
|
-
|
|
84
|
-
// Fast row extraction using regex — handles both normal and namespace-prefixed XML
|
|
85
|
-
const allRows = [];
|
|
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;
|
|
88
|
-
|
|
89
|
-
let rowMatch;
|
|
90
|
-
while ((rowMatch = rowRegex.exec(sheetXml)) !== null) {
|
|
91
|
-
const rowXml = rowMatch[1];
|
|
92
|
-
const rowValues = [];
|
|
93
|
-
|
|
94
|
-
let cellMatch;
|
|
95
|
-
cellRegex.lastIndex = 0;
|
|
96
|
-
while ((cellMatch = cellRegex.exec(rowXml)) !== null) {
|
|
97
|
-
const colLetter = cellMatch[1];
|
|
98
|
-
const cellType = cellMatch[2] || '';
|
|
99
|
-
const vValue = cellMatch[3];
|
|
100
|
-
const inlineValue = cellMatch[4];
|
|
101
|
-
|
|
102
|
-
const colIdx = colLetterToIndex(colLetter);
|
|
103
|
-
|
|
104
|
-
let value;
|
|
105
|
-
if (cellType === 's' && vValue !== undefined && sharedStrings) {
|
|
106
|
-
const ssIndex = parseInt(vValue, 10);
|
|
107
|
-
value = ssIndex < sharedStrings.length ? sharedStrings[ssIndex] : '';
|
|
108
|
-
} else if (inlineValue !== undefined) {
|
|
109
|
-
value = decodeXmlEntities(inlineValue);
|
|
110
|
-
} else if (vValue !== undefined) {
|
|
111
|
-
value = decodeXmlEntities(vValue);
|
|
112
|
-
} else {
|
|
113
|
-
value = '';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
rowValues[colIdx] = value;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
allRows.push(rowValues);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Skip the first row (column headers)
|
|
123
|
-
if (allRows.length < 2) {
|
|
124
|
-
throw new Error('Not enough rows in XLSX file for v5 format.');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const dataRows = allRows.slice(1); // Skip header row
|
|
128
|
-
|
|
129
|
-
// Decode log lines
|
|
130
|
-
return decodeLogLines(dataRows);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Decode XML entities in a string value
|
|
135
|
-
*/
|
|
136
|
-
function decodeXmlEntities(str) {
|
|
137
|
-
if (!str || !str.includes('&')) return str;
|
|
138
|
-
return str
|
|
139
|
-
.replace(/&/g, '&')
|
|
140
|
-
.replace(/</g, '<')
|
|
141
|
-
.replace(/>/g, '>')
|
|
142
|
-
.replace(/'/g, "'")
|
|
143
|
-
.replace(/"/g, '"');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Convert column letter to 0-based index (A=0, B=1, ..., Z=25, AA=26)
|
|
148
|
-
*/
|
|
149
|
-
function colLetterToIndex(letters) {
|
|
150
|
-
let index = 0;
|
|
151
|
-
for (let i = 0; i < letters.length; i++) {
|
|
152
|
-
index = index * 26 + (letters.charCodeAt(i) - 64);
|
|
153
|
-
}
|
|
154
|
-
return index - 1; // 0-based
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Detect whether an XLSX file is v5 (log-embed) or v3/v4 (hidden sheet) format.
|
|
159
|
-
* @param {string} xlsxPath - Path to XLSX file
|
|
160
|
-
* @returns {string} 'v5' or 'legacy'
|
|
161
|
-
*/
|
|
162
|
-
function detectXlsxVersion(xlsxPath) {
|
|
163
|
-
const zip = new AdmZip(xlsxPath);
|
|
164
|
-
|
|
165
|
-
// v5 files have only sheet1.xml, v3/v4 have sheet2.xml (hidden data sheet)
|
|
166
|
-
const sheet2 = zip.getEntry('xl/worksheets/sheet2.xml');
|
|
167
|
-
if (sheet2) {
|
|
168
|
-
return 'legacy';
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Double-check: look at sheet name in workbook.xml
|
|
172
|
-
const wbEntry = zip.getEntry('xl/workbook.xml');
|
|
173
|
-
if (wbEntry) {
|
|
174
|
-
const wbXml = wbEntry.getData().toString('utf8');
|
|
175
|
-
if (wbXml.includes('Access Logs') || wbXml.includes('access_log')) {
|
|
176
|
-
return 'v5';
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Default: try v5 if only one sheet exists
|
|
181
|
-
return 'v5';
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ─── v3/v4 Legacy XLSX Creation (removed — no longer passes air gap filters)
|
|
185
|
-
|
|
186
|
-
// ─── Unified Reader ─────────────────────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Read an XLSX file and extract content. Auto-detects v5 vs v3/v4 format.
|
|
190
|
-
* @param {string} xlsxPath - Path to XLSX file
|
|
191
|
-
* @returns {Promise<object>} For v5: { payloadBuffer, metadataJson, encryptionMeta, metadata, formatVersion: 'v5' }
|
|
192
|
-
* For legacy: { base64Content, encryptionMeta, metadata, formatVersion: 'legacy' }
|
|
193
|
-
*/
|
|
194
|
-
async function readXlsxBase64(xlsxPath) {
|
|
195
|
-
if (!fs.existsSync(xlsxPath)) {
|
|
196
|
-
throw new Error(`XLSX file not found: ${xlsxPath}`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const version = detectXlsxVersion(xlsxPath);
|
|
200
|
-
|
|
201
|
-
if (version === 'v5') {
|
|
202
|
-
const result = await readXlsxV5(xlsxPath);
|
|
203
|
-
return {
|
|
204
|
-
...result,
|
|
205
|
-
formatVersion: 'v5',
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const result = await extractFromXml(xlsxPath);
|
|
210
|
-
return {
|
|
211
|
-
...result,
|
|
212
|
-
formatVersion: 'legacy',
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ─── Legacy XML Extraction ──────────────────────────────────────────────────
|
|
217
|
-
|
|
218
|
-
async function extractFromXml(xlsxPath) {
|
|
219
|
-
let sharedStrings = [];
|
|
220
|
-
const ssParsed = parseXmlFromZip(xlsxPath, 'xl/sharedStrings.xml');
|
|
221
|
-
|
|
222
|
-
if (ssParsed && ssParsed.sst && ssParsed.sst.si) {
|
|
223
|
-
const siArray = ensureArray(ssParsed.sst.si);
|
|
224
|
-
sharedStrings = siArray.map((si) => extractTextContent(si.t));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const sheetParsed = parseXmlFromZip(xlsxPath, 'xl/worksheets/sheet2.xml');
|
|
228
|
-
|
|
229
|
-
if (!sheetParsed) {
|
|
230
|
-
throw new Error('Hidden sheet not found in XLSX file. This may not be a stegdoc-encoded file.');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const cellValues = new Map();
|
|
234
|
-
const sheetData = sheetParsed.worksheet?.sheetData;
|
|
235
|
-
|
|
236
|
-
if (sheetData && sheetData.row) {
|
|
237
|
-
const rows = ensureArray(sheetData.row);
|
|
238
|
-
|
|
239
|
-
for (const row of rows) {
|
|
240
|
-
if (!row.c) continue;
|
|
241
|
-
const cells = ensureArray(row.c);
|
|
242
|
-
|
|
243
|
-
for (const cell of cells) {
|
|
244
|
-
const cellRef = cell['@_r'];
|
|
245
|
-
const cellType = cell['@_t'];
|
|
246
|
-
const cellValue = cell.v;
|
|
247
|
-
|
|
248
|
-
if (cellRef === undefined) continue;
|
|
249
|
-
|
|
250
|
-
if (cellType === 's' && cellValue !== undefined) {
|
|
251
|
-
const ssIndex = parseInt(cellValue, 10);
|
|
252
|
-
if (ssIndex < sharedStrings.length) {
|
|
253
|
-
cellValues.set(cellRef, sharedStrings[ssIndex]);
|
|
254
|
-
}
|
|
255
|
-
} else if (cellType === 'inlineStr' && cell.is) {
|
|
256
|
-
const text = extractTextContent(cell.is.t);
|
|
257
|
-
if (text !== undefined) {
|
|
258
|
-
cellValues.set(cellRef, text);
|
|
259
|
-
}
|
|
260
|
-
} else if (cellValue !== undefined) {
|
|
261
|
-
cellValues.set(cellRef, String(cellValue));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const encryptionMeta = cellValues.get('A1') || '';
|
|
268
|
-
const metadata = cellValues.get('B1');
|
|
269
|
-
const chunkCountStr = cellValues.get('C1');
|
|
270
|
-
|
|
271
|
-
if (!metadata) {
|
|
272
|
-
throw new Error('No metadata found in XLSX file.');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const chunkCount = parseInt(chunkCountStr, 10);
|
|
276
|
-
if (isNaN(chunkCount) || chunkCount <= 0) {
|
|
277
|
-
throw new Error('Invalid chunk count in XLSX file.');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const chunks = [];
|
|
281
|
-
for (let i = 0; i < chunkCount; i++) {
|
|
282
|
-
const row = Math.floor(i / 26) + 2;
|
|
283
|
-
const col = (i % 26) + 1;
|
|
284
|
-
const cellRef = `${columnToLetter(col)}${row}`;
|
|
285
|
-
const chunk = cellValues.get(cellRef);
|
|
286
|
-
if (chunk) {
|
|
287
|
-
chunks.push(chunk);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
base64Content: chunks.join(''),
|
|
293
|
-
encryptionMeta,
|
|
294
|
-
metadata,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
299
|
-
|
|
300
|
-
function columnToLetter(col) {
|
|
301
|
-
let letter = '';
|
|
302
|
-
while (col > 0) {
|
|
303
|
-
const mod = (col - 1) % 26;
|
|
304
|
-
letter = String.fromCharCode(65 + mod) + letter;
|
|
305
|
-
col = Math.floor((col - 1) / 26);
|
|
306
|
-
}
|
|
307
|
-
return letter;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
44
|
module.exports = {
|
|
311
|
-
// v5
|
|
312
45
|
createXlsxPartV5,
|
|
313
|
-
readXlsxV5,
|
|
314
|
-
detectXlsxVersion,
|
|
315
|
-
// Unified reader (auto-detects v5 vs legacy)
|
|
316
|
-
readXlsxBase64,
|
|
317
46
|
};
|
package/src/lib/xlsx-writer.js
CHANGED
|
@@ -295,4 +295,14 @@ module.exports = {
|
|
|
295
295
|
buildSheetXml,
|
|
296
296
|
escapeXml,
|
|
297
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,
|
|
298
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
|
-
};
|