voltjs-framework 1.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/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Excel Module
|
|
3
|
+
*
|
|
4
|
+
* Import and export Excel files (XLSX, CSV) with zero external dependencies.
|
|
5
|
+
* Uses native Node.js buffers to read/write Excel format.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { Excel } = require('voltjs');
|
|
9
|
+
*
|
|
10
|
+
* // Export to CSV
|
|
11
|
+
* const csv = Excel.toCSV([
|
|
12
|
+
* { name: 'John', age: 30, email: 'john@test.com' },
|
|
13
|
+
* { name: 'Jane', age: 25, email: 'jane@test.com' },
|
|
14
|
+
* ]);
|
|
15
|
+
*
|
|
16
|
+
* // Import from CSV
|
|
17
|
+
* const data = Excel.fromCSV(csvString);
|
|
18
|
+
*
|
|
19
|
+
* // Generate XLSX
|
|
20
|
+
* const buffer = Excel.createXLSX(data, { sheetName: 'Users' });
|
|
21
|
+
* fs.writeFileSync('users.xlsx', buffer);
|
|
22
|
+
*
|
|
23
|
+
* // Parse XLSX
|
|
24
|
+
* const rows = Excel.parseXLSX(buffer);
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const zlib = require('zlib');
|
|
32
|
+
|
|
33
|
+
class Excel {
|
|
34
|
+
// ===== CSV OPERATIONS =====
|
|
35
|
+
|
|
36
|
+
/** Convert array of objects to CSV string */
|
|
37
|
+
static toCSV(data, options = {}) {
|
|
38
|
+
if (!Array.isArray(data) || data.length === 0) return '';
|
|
39
|
+
|
|
40
|
+
const delimiter = options.delimiter || ',';
|
|
41
|
+
const lineBreak = options.lineBreak || '\n';
|
|
42
|
+
const headers = options.headers || Object.keys(data[0]);
|
|
43
|
+
const includeHeaders = options.includeHeaders !== false;
|
|
44
|
+
|
|
45
|
+
const escapeValue = (val) => {
|
|
46
|
+
if (val === null || val === undefined) return '';
|
|
47
|
+
const str = String(val);
|
|
48
|
+
if (str.includes(delimiter) || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
49
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
50
|
+
}
|
|
51
|
+
return str;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const lines = [];
|
|
55
|
+
if (includeHeaders) {
|
|
56
|
+
lines.push(headers.map(escapeValue).join(delimiter));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const row of data) {
|
|
60
|
+
lines.push(headers.map(h => escapeValue(row[h])).join(delimiter));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return lines.join(lineBreak);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parse CSV string to array of objects */
|
|
67
|
+
static fromCSV(csvString, options = {}) {
|
|
68
|
+
const delimiter = options.delimiter || ',';
|
|
69
|
+
const hasHeaders = options.hasHeaders !== false;
|
|
70
|
+
|
|
71
|
+
const lines = Excel._parseCSVLines(csvString, delimiter);
|
|
72
|
+
if (lines.length === 0) return [];
|
|
73
|
+
|
|
74
|
+
if (hasHeaders) {
|
|
75
|
+
const headers = lines[0];
|
|
76
|
+
return lines.slice(1).map(row => {
|
|
77
|
+
const obj = {};
|
|
78
|
+
headers.forEach((h, i) => {
|
|
79
|
+
obj[h] = row[i] !== undefined ? row[i] : '';
|
|
80
|
+
});
|
|
81
|
+
return obj;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Parse CSV with proper quote handling */
|
|
89
|
+
static _parseCSVLines(text, delimiter = ',') {
|
|
90
|
+
const lines = [];
|
|
91
|
+
let current = [];
|
|
92
|
+
let field = '';
|
|
93
|
+
let inQuotes = false;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < text.length; i++) {
|
|
96
|
+
const ch = text[i];
|
|
97
|
+
const next = text[i + 1];
|
|
98
|
+
|
|
99
|
+
if (inQuotes) {
|
|
100
|
+
if (ch === '"' && next === '"') {
|
|
101
|
+
field += '"';
|
|
102
|
+
i++;
|
|
103
|
+
} else if (ch === '"') {
|
|
104
|
+
inQuotes = false;
|
|
105
|
+
} else {
|
|
106
|
+
field += ch;
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
if (ch === '"') {
|
|
110
|
+
inQuotes = true;
|
|
111
|
+
} else if (ch === delimiter) {
|
|
112
|
+
current.push(field.trim());
|
|
113
|
+
field = '';
|
|
114
|
+
} else if (ch === '\n' || (ch === '\r' && next === '\n')) {
|
|
115
|
+
current.push(field.trim());
|
|
116
|
+
if (current.some(f => f !== '')) lines.push(current);
|
|
117
|
+
current = [];
|
|
118
|
+
field = '';
|
|
119
|
+
if (ch === '\r') i++;
|
|
120
|
+
} else {
|
|
121
|
+
field += ch;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Last field
|
|
127
|
+
current.push(field.trim());
|
|
128
|
+
if (current.some(f => f !== '')) lines.push(current);
|
|
129
|
+
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Read CSV file */
|
|
134
|
+
static readCSV(filePath, options = {}) {
|
|
135
|
+
const content = fs.readFileSync(filePath, options.encoding || 'utf-8');
|
|
136
|
+
return Excel.fromCSV(content, options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Write CSV file */
|
|
140
|
+
static writeCSV(filePath, data, options = {}) {
|
|
141
|
+
const csv = Excel.toCSV(data, options);
|
|
142
|
+
fs.writeFileSync(filePath, csv, options.encoding || 'utf-8');
|
|
143
|
+
return filePath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ===== XLSX OPERATIONS =====
|
|
147
|
+
|
|
148
|
+
/** Create a simple XLSX file (ZIP-based XML spreadsheet) */
|
|
149
|
+
static createXLSX(data, options = {}) {
|
|
150
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
151
|
+
throw new Error('Data must be a non-empty array of objects');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sheetName = options.sheetName || 'Sheet1';
|
|
155
|
+
const headers = options.headers || Object.keys(data[0]);
|
|
156
|
+
|
|
157
|
+
// Build the sheet XML
|
|
158
|
+
let sheetData = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
159
|
+
sheetData += '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">\n';
|
|
160
|
+
sheetData += '<sheetData>\n';
|
|
161
|
+
|
|
162
|
+
// Header row
|
|
163
|
+
sheetData += '<row r="1">\n';
|
|
164
|
+
headers.forEach((h, i) => {
|
|
165
|
+
const col = Excel._columnLetter(i);
|
|
166
|
+
sheetData += `<c r="${col}1" t="inlineStr"><is><t>${Excel._escapeXML(String(h))}</t></is></c>\n`;
|
|
167
|
+
});
|
|
168
|
+
sheetData += '</row>\n';
|
|
169
|
+
|
|
170
|
+
// Data rows
|
|
171
|
+
data.forEach((row, rowIndex) => {
|
|
172
|
+
const r = rowIndex + 2;
|
|
173
|
+
sheetData += `<row r="${r}">\n`;
|
|
174
|
+
headers.forEach((h, colIndex) => {
|
|
175
|
+
const col = Excel._columnLetter(colIndex);
|
|
176
|
+
const val = row[h];
|
|
177
|
+
if (val === null || val === undefined) {
|
|
178
|
+
sheetData += `<c r="${col}${r}" t="inlineStr"><is><t></t></is></c>\n`;
|
|
179
|
+
} else if (typeof val === 'number') {
|
|
180
|
+
sheetData += `<c r="${col}${r}"><v>${val}</v></c>\n`;
|
|
181
|
+
} else {
|
|
182
|
+
sheetData += `<c r="${col}${r}" t="inlineStr"><is><t>${Excel._escapeXML(String(val))}</t></is></c>\n`;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
sheetData += '</row>\n';
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
sheetData += '</sheetData>\n';
|
|
189
|
+
sheetData += '</worksheet>';
|
|
190
|
+
|
|
191
|
+
// Build XLSX (ZIP format with required XML files)
|
|
192
|
+
const files = {
|
|
193
|
+
'[Content_Types].xml': Excel._contentTypes(),
|
|
194
|
+
'_rels/.rels': Excel._rootRels(),
|
|
195
|
+
'xl/_rels/workbook.xml.rels': Excel._workbookRels(),
|
|
196
|
+
'xl/workbook.xml': Excel._workbook(sheetName),
|
|
197
|
+
'xl/worksheets/sheet1.xml': sheetData,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return Excel._createZip(files);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Parse XLSX file to array of objects */
|
|
204
|
+
static parseXLSX(bufferOrPath) {
|
|
205
|
+
let buffer;
|
|
206
|
+
if (typeof bufferOrPath === 'string') {
|
|
207
|
+
buffer = fs.readFileSync(bufferOrPath);
|
|
208
|
+
} else {
|
|
209
|
+
buffer = bufferOrPath;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Extract ZIP entries
|
|
213
|
+
const entries = Excel._extractZip(buffer);
|
|
214
|
+
|
|
215
|
+
// Find the sheet XML
|
|
216
|
+
const sheetEntry = entries['xl/worksheets/sheet1.xml'] || entries['xl/worksheets/Sheet1.xml'];
|
|
217
|
+
if (!sheetEntry) {
|
|
218
|
+
throw new Error('No worksheet found in XLSX file');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const sheetXML = sheetEntry.toString('utf-8');
|
|
222
|
+
|
|
223
|
+
// Check for shared strings
|
|
224
|
+
let sharedStrings = [];
|
|
225
|
+
const sharedStringsEntry = entries['xl/sharedStrings.xml'];
|
|
226
|
+
if (sharedStringsEntry) {
|
|
227
|
+
const ssXML = sharedStringsEntry.toString('utf-8');
|
|
228
|
+
const ssMatches = ssXML.matchAll(/<t[^>]*>([^<]*)<\/t>/g);
|
|
229
|
+
for (const m of ssMatches) {
|
|
230
|
+
sharedStrings.push(m[1]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Parse rows
|
|
235
|
+
const rows = [];
|
|
236
|
+
const rowMatches = sheetXML.matchAll(/<row[^>]*>([\s\S]*?)<\/row>/g);
|
|
237
|
+
|
|
238
|
+
for (const rowMatch of rowMatches) {
|
|
239
|
+
const row = [];
|
|
240
|
+
const cellMatches = rowMatch[1].matchAll(/<c[^>]*(?:t="([^"]*)")?[^>]*>(?:<(?:is|v)>(?:<t>)?([^<]*)(?:<\/t>)?<\/(?:is|v)>)?<\/c>/g);
|
|
241
|
+
|
|
242
|
+
for (const cellMatch of cellMatches) {
|
|
243
|
+
const type = cellMatch[1];
|
|
244
|
+
let value = cellMatch[2] || '';
|
|
245
|
+
|
|
246
|
+
if (type === 's' && sharedStrings.length > 0) {
|
|
247
|
+
value = sharedStrings[parseInt(value)] || value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
value = Excel._unescapeXML(value);
|
|
251
|
+
row.push(value);
|
|
252
|
+
}
|
|
253
|
+
rows.push(row);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (rows.length === 0) return [];
|
|
257
|
+
|
|
258
|
+
// First row as headers
|
|
259
|
+
const headers = rows[0];
|
|
260
|
+
return rows.slice(1).map(row => {
|
|
261
|
+
const obj = {};
|
|
262
|
+
headers.forEach((h, i) => {
|
|
263
|
+
obj[h] = row[i] !== undefined ? row[i] : '';
|
|
264
|
+
});
|
|
265
|
+
return obj;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Write XLSX file */
|
|
270
|
+
static writeXLSX(filePath, data, options = {}) {
|
|
271
|
+
const buffer = Excel.createXLSX(data, options);
|
|
272
|
+
fs.writeFileSync(filePath, buffer);
|
|
273
|
+
return filePath;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ===== TSV OPERATIONS =====
|
|
277
|
+
|
|
278
|
+
/** Convert to TSV (Tab-Separated Values) */
|
|
279
|
+
static toTSV(data, options = {}) {
|
|
280
|
+
return Excel.toCSV(data, { ...options, delimiter: '\t' });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Parse TSV */
|
|
284
|
+
static fromTSV(tsvString, options = {}) {
|
|
285
|
+
return Excel.fromCSV(tsvString, { ...options, delimiter: '\t' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ===== JSON OPERATIONS =====
|
|
289
|
+
|
|
290
|
+
/** Convert to formatted JSON */
|
|
291
|
+
static toJSON(data, pretty = true) {
|
|
292
|
+
return JSON.stringify(data, null, pretty ? 2 : 0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Convert to JSON file */
|
|
296
|
+
static writeJSON(filePath, data, pretty = true) {
|
|
297
|
+
fs.writeFileSync(filePath, Excel.toJSON(data, pretty));
|
|
298
|
+
return filePath;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Read JSON file */
|
|
302
|
+
static readJSON(filePath) {
|
|
303
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ===== HELPERS =====
|
|
307
|
+
|
|
308
|
+
static _columnLetter(index) {
|
|
309
|
+
let letter = '';
|
|
310
|
+
while (index >= 0) {
|
|
311
|
+
letter = String.fromCharCode(65 + (index % 26)) + letter;
|
|
312
|
+
index = Math.floor(index / 26) - 1;
|
|
313
|
+
}
|
|
314
|
+
return letter;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static _escapeXML(str) {
|
|
318
|
+
return str
|
|
319
|
+
.replace(/&/g, '&')
|
|
320
|
+
.replace(/</g, '<')
|
|
321
|
+
.replace(/>/g, '>')
|
|
322
|
+
.replace(/"/g, '"')
|
|
323
|
+
.replace(/'/g, ''');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
static _unescapeXML(str) {
|
|
327
|
+
return str
|
|
328
|
+
.replace(/&/g, '&')
|
|
329
|
+
.replace(/</g, '<')
|
|
330
|
+
.replace(/>/g, '>')
|
|
331
|
+
.replace(/"/g, '"')
|
|
332
|
+
.replace(/'/g, "'");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
static _contentTypes() {
|
|
336
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
337
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">\n' +
|
|
338
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>\n' +
|
|
339
|
+
'<Default Extension="xml" ContentType="application/xml"/>\n' +
|
|
340
|
+
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>\n' +
|
|
341
|
+
'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>\n' +
|
|
342
|
+
'</Types>';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static _rootRels() {
|
|
346
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
347
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n' +
|
|
348
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>\n' +
|
|
349
|
+
'</Relationships>';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
static _workbookRels() {
|
|
353
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
354
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n' +
|
|
355
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>\n' +
|
|
356
|
+
'</Relationships>';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
static _workbook(sheetName) {
|
|
360
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
361
|
+
'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">\n' +
|
|
362
|
+
'<sheets>\n' +
|
|
363
|
+
`<sheet name="${sheetName}" sheetId="1" r:id="rId1"/>\n` +
|
|
364
|
+
'</sheets>\n' +
|
|
365
|
+
'</workbook>';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Create a simple ZIP file from an object of {path: content} */
|
|
369
|
+
static _createZip(files) {
|
|
370
|
+
// Simple ZIP file creator (PKZIP format)
|
|
371
|
+
const entries = [];
|
|
372
|
+
const centralDir = [];
|
|
373
|
+
let offset = 0;
|
|
374
|
+
|
|
375
|
+
for (const [filepath, content] of Object.entries(files)) {
|
|
376
|
+
const data = Buffer.from(content, 'utf-8');
|
|
377
|
+
const compressed = zlib.deflateRawSync(data);
|
|
378
|
+
|
|
379
|
+
const filenameBuffer = Buffer.from(filepath, 'utf-8');
|
|
380
|
+
|
|
381
|
+
// Local file header
|
|
382
|
+
const localHeader = Buffer.alloc(30);
|
|
383
|
+
localHeader.writeUInt32LE(0x04034b50, 0); // Local file header signature
|
|
384
|
+
localHeader.writeUInt16LE(20, 4); // Version needed
|
|
385
|
+
localHeader.writeUInt16LE(0, 6); // General purpose flag
|
|
386
|
+
localHeader.writeUInt16LE(8, 8); // Compression method (deflate)
|
|
387
|
+
localHeader.writeUInt16LE(0, 10); // Last mod time
|
|
388
|
+
localHeader.writeUInt16LE(0, 12); // Last mod date
|
|
389
|
+
localHeader.writeUInt32LE(Excel._crc32(data), 14); // CRC-32
|
|
390
|
+
localHeader.writeUInt32LE(compressed.length, 18); // Compressed size
|
|
391
|
+
localHeader.writeUInt32LE(data.length, 22); // Uncompressed size
|
|
392
|
+
localHeader.writeUInt16LE(filenameBuffer.length, 26); // Filename length
|
|
393
|
+
|
|
394
|
+
const entry = Buffer.concat([localHeader, filenameBuffer, compressed]);
|
|
395
|
+
entries.push(entry);
|
|
396
|
+
|
|
397
|
+
// Central directory header
|
|
398
|
+
const cdHeader = Buffer.alloc(46);
|
|
399
|
+
cdHeader.writeUInt32LE(0x02014b50, 0); // Central directory signature
|
|
400
|
+
cdHeader.writeUInt16LE(20, 4); // Version made by
|
|
401
|
+
cdHeader.writeUInt16LE(20, 6); // Version needed
|
|
402
|
+
cdHeader.writeUInt16LE(0, 8); // General purpose flag
|
|
403
|
+
cdHeader.writeUInt16LE(8, 10); // Compression method
|
|
404
|
+
cdHeader.writeUInt16LE(0, 12); // Last mod time
|
|
405
|
+
cdHeader.writeUInt16LE(0, 14); // Last mod date
|
|
406
|
+
cdHeader.writeUInt32LE(Excel._crc32(data), 16);
|
|
407
|
+
cdHeader.writeUInt32LE(compressed.length, 20);
|
|
408
|
+
cdHeader.writeUInt32LE(data.length, 24);
|
|
409
|
+
cdHeader.writeUInt16LE(filenameBuffer.length, 28);
|
|
410
|
+
cdHeader.writeUInt32LE(offset, 42); // Relative offset
|
|
411
|
+
|
|
412
|
+
centralDir.push(Buffer.concat([cdHeader, filenameBuffer]));
|
|
413
|
+
offset += entry.length;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const centralDirBuffer = Buffer.concat(centralDir);
|
|
417
|
+
const centralDirOffset = offset;
|
|
418
|
+
|
|
419
|
+
// End of central directory
|
|
420
|
+
const eocd = Buffer.alloc(22);
|
|
421
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
422
|
+
eocd.writeUInt16LE(Object.keys(files).length, 8); // Total entries
|
|
423
|
+
eocd.writeUInt16LE(Object.keys(files).length, 10);
|
|
424
|
+
eocd.writeUInt32LE(centralDirBuffer.length, 12);
|
|
425
|
+
eocd.writeUInt32LE(centralDirOffset, 16);
|
|
426
|
+
|
|
427
|
+
return Buffer.concat([...entries, centralDirBuffer, eocd]);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Extract files from a ZIP buffer */
|
|
431
|
+
static _extractZip(buffer) {
|
|
432
|
+
const entries = {};
|
|
433
|
+
let offset = 0;
|
|
434
|
+
|
|
435
|
+
while (offset < buffer.length - 4) {
|
|
436
|
+
const signature = buffer.readUInt32LE(offset);
|
|
437
|
+
|
|
438
|
+
if (signature === 0x04034b50) { // Local file header
|
|
439
|
+
const compressionMethod = buffer.readUInt16LE(offset + 8);
|
|
440
|
+
const compressedSize = buffer.readUInt32LE(offset + 18);
|
|
441
|
+
const uncompressedSize = buffer.readUInt32LE(offset + 22);
|
|
442
|
+
const filenameLen = buffer.readUInt16LE(offset + 26);
|
|
443
|
+
const extraLen = buffer.readUInt16LE(offset + 28);
|
|
444
|
+
|
|
445
|
+
const filename = buffer.slice(offset + 30, offset + 30 + filenameLen).toString('utf-8');
|
|
446
|
+
const dataStart = offset + 30 + filenameLen + extraLen;
|
|
447
|
+
const data = buffer.slice(dataStart, dataStart + compressedSize);
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
if (compressionMethod === 8) {
|
|
451
|
+
entries[filename] = zlib.inflateRawSync(data);
|
|
452
|
+
} else {
|
|
453
|
+
entries[filename] = data;
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
entries[filename] = data;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
offset = dataStart + compressedSize;
|
|
460
|
+
} else {
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return entries;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** CRC-32 calculation */
|
|
469
|
+
static _crc32(data) {
|
|
470
|
+
let crc = 0xFFFFFFFF;
|
|
471
|
+
for (let i = 0; i < data.length; i++) {
|
|
472
|
+
crc ^= data[i];
|
|
473
|
+
for (let j = 0; j < 8; j++) {
|
|
474
|
+
if (crc & 1) crc = (crc >>> 1) ^ 0xEDB88320;
|
|
475
|
+
else crc >>>= 1;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = { Excel };
|