@cj-tech-master/excelts 1.4.3 → 1.4.5
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 +3 -3
- package/README_zh.md +3 -3
- package/dist/browser/excelts.iife.js +12841 -7484
- package/dist/browser/excelts.iife.js.map +1 -1
- package/dist/browser/excelts.iife.min.js +86 -23
- package/dist/cjs/doc/column.js +1 -1
- package/dist/cjs/doc/row.js +9 -4
- package/dist/cjs/doc/worksheet.js +9 -4
- package/dist/cjs/stream/xlsx/workbook-writer.js +3 -2
- package/dist/cjs/utils/unzip/extract.js +166 -0
- package/dist/cjs/utils/unzip/index.js +7 -1
- package/dist/cjs/utils/xml-stream.js +25 -3
- package/dist/cjs/utils/zip/compress.js +261 -0
- package/dist/cjs/utils/zip/crc32.js +154 -0
- package/dist/cjs/utils/zip/index.js +70 -0
- package/dist/cjs/utils/zip/zip-builder.js +378 -0
- package/dist/cjs/utils/zip-stream.js +30 -34
- package/dist/cjs/xlsx/xform/book/defined-name-xform.js +36 -2
- package/dist/cjs/xlsx/xform/list-xform.js +6 -0
- package/dist/cjs/xlsx/xform/sheet/cell-xform.js +6 -1
- package/dist/cjs/xlsx/xform/sheet/row-xform.js +24 -2
- package/dist/cjs/xlsx/xform/table/filter-column-xform.js +4 -0
- package/dist/esm/doc/column.js +1 -1
- package/dist/esm/doc/row.js +9 -4
- package/dist/esm/doc/worksheet.js +9 -4
- package/dist/esm/stream/xlsx/workbook-writer.js +3 -2
- package/dist/esm/utils/unzip/extract.js +160 -0
- package/dist/esm/utils/unzip/index.js +2 -0
- package/dist/esm/utils/xml-stream.js +25 -3
- package/dist/esm/utils/zip/compress.js +220 -0
- package/dist/esm/utils/zip/crc32.js +116 -0
- package/dist/esm/utils/zip/index.js +55 -0
- package/dist/esm/utils/zip/zip-builder.js +372 -0
- package/dist/esm/utils/zip-stream.js +30 -34
- package/dist/esm/xlsx/xform/book/defined-name-xform.js +36 -2
- package/dist/esm/xlsx/xform/list-xform.js +6 -0
- package/dist/esm/xlsx/xform/sheet/cell-xform.js +6 -1
- package/dist/esm/xlsx/xform/sheet/row-xform.js +24 -2
- package/dist/esm/xlsx/xform/table/filter-column-xform.js +4 -0
- package/dist/types/doc/cell.d.ts +10 -6
- package/dist/types/doc/column.d.ts +8 -4
- package/dist/types/doc/row.d.ts +9 -8
- package/dist/types/doc/worksheet.d.ts +2 -2
- package/dist/types/utils/unzip/extract.d.ts +92 -0
- package/dist/types/utils/unzip/index.d.ts +1 -0
- package/dist/types/utils/xml-stream.d.ts +2 -0
- package/dist/types/utils/zip/compress.d.ts +83 -0
- package/dist/types/utils/zip/crc32.d.ts +55 -0
- package/dist/types/utils/zip/index.d.ts +52 -0
- package/dist/types/utils/zip/zip-builder.d.ts +110 -0
- package/dist/types/utils/zip-stream.d.ts +6 -12
- package/dist/types/xlsx/xform/list-xform.d.ts +1 -0
- package/dist/types/xlsx/xform/sheet/row-xform.d.ts +2 -0
- package/package.json +8 -8
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CRC32 calculation utility for ZIP files
|
|
4
|
+
*
|
|
5
|
+
* - Node.js: Uses native zlib.crc32 (C++ implementation, ~100x faster)
|
|
6
|
+
* - Browser: Uses lookup table optimization
|
|
7
|
+
*
|
|
8
|
+
* The polynomial used is the standard CRC-32 IEEE 802.3:
|
|
9
|
+
* x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1
|
|
10
|
+
* Represented as 0xEDB88320 in reversed (LSB-first) form
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.crc32 = crc32;
|
|
47
|
+
exports.ensureCrc32 = ensureCrc32;
|
|
48
|
+
exports.crc32Update = crc32Update;
|
|
49
|
+
exports.crc32Finalize = crc32Finalize;
|
|
50
|
+
// Detect Node.js environment
|
|
51
|
+
const isNode = typeof process !== "undefined" && process.versions?.node;
|
|
52
|
+
// Lazy-loaded zlib module for Node.js
|
|
53
|
+
let _zlib = null;
|
|
54
|
+
let _zlibLoading = null;
|
|
55
|
+
// Auto-initialize zlib in Node.js environment
|
|
56
|
+
if (isNode) {
|
|
57
|
+
_zlibLoading = Promise.resolve().then(() => __importStar(require("zlib"))).then(module => {
|
|
58
|
+
_zlib = module.default ?? module;
|
|
59
|
+
return _zlib;
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {
|
|
62
|
+
_zlib = null;
|
|
63
|
+
return null;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Pre-computed CRC32 lookup table (256 entries)
|
|
68
|
+
* Generated using the standard polynomial 0xEDB88320
|
|
69
|
+
* Used as fallback when native zlib is not available
|
|
70
|
+
*/
|
|
71
|
+
const CRC32_TABLE = /* @__PURE__ */ (() => {
|
|
72
|
+
const table = new Uint32Array(256);
|
|
73
|
+
for (let i = 0; i < 256; i++) {
|
|
74
|
+
let crc = i;
|
|
75
|
+
for (let j = 0; j < 8; j++) {
|
|
76
|
+
crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
|
|
77
|
+
}
|
|
78
|
+
table[i] = crc;
|
|
79
|
+
}
|
|
80
|
+
return table;
|
|
81
|
+
})();
|
|
82
|
+
/**
|
|
83
|
+
* JavaScript fallback CRC32 implementation using lookup table
|
|
84
|
+
*/
|
|
85
|
+
function crc32JS(data) {
|
|
86
|
+
let crc = 0xffffffff;
|
|
87
|
+
for (let i = 0; i < data.length; i++) {
|
|
88
|
+
crc = CRC32_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
|
|
89
|
+
}
|
|
90
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Calculate CRC32 checksum for the given data
|
|
94
|
+
* Uses native zlib.crc32 in Node.js for ~100x better performance
|
|
95
|
+
*
|
|
96
|
+
* @param data - Input data as Uint8Array or Buffer
|
|
97
|
+
* @returns CRC32 checksum as unsigned 32-bit integer
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const data = new TextEncoder().encode("Hello, World!");
|
|
102
|
+
* const checksum = crc32(data);
|
|
103
|
+
* console.log(checksum.toString(16)); // "ec4ac3d0"
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
function crc32(data) {
|
|
107
|
+
// Use native zlib.crc32 if available (Node.js)
|
|
108
|
+
if (_zlib && typeof _zlib.crc32 === "function") {
|
|
109
|
+
return _zlib.crc32(data) >>> 0;
|
|
110
|
+
}
|
|
111
|
+
// Fallback to JS implementation
|
|
112
|
+
return crc32JS(data);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ensure zlib is loaded (for use before calling crc32)
|
|
116
|
+
*/
|
|
117
|
+
async function ensureCrc32() {
|
|
118
|
+
if (_zlibLoading) {
|
|
119
|
+
await _zlibLoading;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Calculate CRC32 incrementally (useful for streaming)
|
|
124
|
+
* Call with initial crc of 0xffffffff, then finalize with crc32Finalize
|
|
125
|
+
* Note: This always uses JS implementation for consistency in streaming
|
|
126
|
+
*
|
|
127
|
+
* @param crc - Current CRC value (start with 0xffffffff)
|
|
128
|
+
* @param data - Input data chunk
|
|
129
|
+
* @returns Updated CRC value (not finalized)
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* let crc = 0xffffffff;
|
|
134
|
+
* crc = crc32Update(crc, chunk1);
|
|
135
|
+
* crc = crc32Update(crc, chunk2);
|
|
136
|
+
* const checksum = crc32Finalize(crc);
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
function crc32Update(crc, data) {
|
|
140
|
+
for (let i = 0; i < data.length; i++) {
|
|
141
|
+
crc = CRC32_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
|
|
142
|
+
}
|
|
143
|
+
return crc;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Finalize CRC32 calculation
|
|
147
|
+
* XOR with 0xffffffff and convert to unsigned 32-bit
|
|
148
|
+
*
|
|
149
|
+
* @param crc - CRC value from crc32Update
|
|
150
|
+
* @returns Final CRC32 checksum
|
|
151
|
+
*/
|
|
152
|
+
function crc32Finalize(crc) {
|
|
153
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
154
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Native ZIP utilities - Pure native implementation without third-party dependencies
|
|
4
|
+
*
|
|
5
|
+
* This module provides ZIP file creation using only native platform APIs:
|
|
6
|
+
* - Node.js: Uses native zlib module (C++ implementation, fastest)
|
|
7
|
+
* - Browser: Uses CompressionStream API (Chrome 80+, Firefox 113+, Safari 16.4+)
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Full ZIP format support (Local File Headers, Central Directory, EOCD)
|
|
11
|
+
* - DEFLATE compression (level 0-9 on Node.js, fixed level on browser)
|
|
12
|
+
* - STORE mode (no compression)
|
|
13
|
+
* - UTF-8 filename support
|
|
14
|
+
* - File comments and ZIP comments
|
|
15
|
+
* - Streaming API for large files
|
|
16
|
+
* - Both sync (Node.js) and async APIs
|
|
17
|
+
*
|
|
18
|
+
* @example Basic usage
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createZip } from "./utils/zip/index.js";
|
|
21
|
+
*
|
|
22
|
+
* const zipData = await createZip([
|
|
23
|
+
* { name: "hello.txt", data: new TextEncoder().encode("Hello!") },
|
|
24
|
+
* { name: "folder/nested.txt", data: new TextEncoder().encode("Nested file") }
|
|
25
|
+
* ], { level: 6 });
|
|
26
|
+
*
|
|
27
|
+
* // Write to file (Node.js)
|
|
28
|
+
* fs.writeFileSync("output.zip", zipData);
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example Streaming usage
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { ZipBuilder } from "./utils/zip/index.js";
|
|
34
|
+
*
|
|
35
|
+
* const builder = new ZipBuilder({ level: 1 });
|
|
36
|
+
*
|
|
37
|
+
* // Add files one by one
|
|
38
|
+
* const [header1, data1] = await builder.addFile({
|
|
39
|
+
* name: "file1.txt",
|
|
40
|
+
* data: new TextEncoder().encode("File 1 content")
|
|
41
|
+
* });
|
|
42
|
+
* stream.write(header1);
|
|
43
|
+
* stream.write(data1);
|
|
44
|
+
*
|
|
45
|
+
* // Finalize and write central directory
|
|
46
|
+
* for (const chunk of builder.finalize()) {
|
|
47
|
+
* stream.write(chunk);
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.ZipBuilder = exports.createZipSync = exports.createZip = exports.hasCompressionStream = exports.hasNativeZlib = exports.decompressSync = exports.decompress = exports.compressSync = exports.compress = exports.crc32Finalize = exports.crc32Update = exports.crc32 = void 0;
|
|
53
|
+
// CRC32 utilities
|
|
54
|
+
var crc32_js_1 = require("./crc32");
|
|
55
|
+
Object.defineProperty(exports, "crc32", { enumerable: true, get: function () { return crc32_js_1.crc32; } });
|
|
56
|
+
Object.defineProperty(exports, "crc32Update", { enumerable: true, get: function () { return crc32_js_1.crc32Update; } });
|
|
57
|
+
Object.defineProperty(exports, "crc32Finalize", { enumerable: true, get: function () { return crc32_js_1.crc32Finalize; } });
|
|
58
|
+
// Compression utilities
|
|
59
|
+
var compress_js_1 = require("./compress");
|
|
60
|
+
Object.defineProperty(exports, "compress", { enumerable: true, get: function () { return compress_js_1.compress; } });
|
|
61
|
+
Object.defineProperty(exports, "compressSync", { enumerable: true, get: function () { return compress_js_1.compressSync; } });
|
|
62
|
+
Object.defineProperty(exports, "decompress", { enumerable: true, get: function () { return compress_js_1.decompress; } });
|
|
63
|
+
Object.defineProperty(exports, "decompressSync", { enumerable: true, get: function () { return compress_js_1.decompressSync; } });
|
|
64
|
+
Object.defineProperty(exports, "hasNativeZlib", { enumerable: true, get: function () { return compress_js_1.hasNativeZlib; } });
|
|
65
|
+
Object.defineProperty(exports, "hasCompressionStream", { enumerable: true, get: function () { return compress_js_1.hasCompressionStream; } });
|
|
66
|
+
// ZIP builder
|
|
67
|
+
var zip_builder_js_1 = require("./zip-builder");
|
|
68
|
+
Object.defineProperty(exports, "createZip", { enumerable: true, get: function () { return zip_builder_js_1.createZip; } });
|
|
69
|
+
Object.defineProperty(exports, "createZipSync", { enumerable: true, get: function () { return zip_builder_js_1.createZipSync; } });
|
|
70
|
+
Object.defineProperty(exports, "ZipBuilder", { enumerable: true, get: function () { return zip_builder_js_1.ZipBuilder; } });
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ZIP file format builder
|
|
4
|
+
*
|
|
5
|
+
* Implements ZIP file structure according to PKWARE's APPNOTE.TXT specification
|
|
6
|
+
* https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
|
7
|
+
*
|
|
8
|
+
* ZIP file structure:
|
|
9
|
+
* ┌──────────────────────────┐
|
|
10
|
+
* │ Local File Header 1 │
|
|
11
|
+
* │ File Data 1 │
|
|
12
|
+
* ├──────────────────────────┤
|
|
13
|
+
* │ Local File Header 2 │
|
|
14
|
+
* │ File Data 2 │
|
|
15
|
+
* ├──────────────────────────┤
|
|
16
|
+
* │ ... │
|
|
17
|
+
* ├──────────────────────────┤
|
|
18
|
+
* │ Central Directory 1 │
|
|
19
|
+
* │ Central Directory 2 │
|
|
20
|
+
* │ ... │
|
|
21
|
+
* ├──────────────────────────┤
|
|
22
|
+
* │ End of Central Directory │
|
|
23
|
+
* └──────────────────────────┘
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ZipBuilder = void 0;
|
|
27
|
+
exports.createZip = createZip;
|
|
28
|
+
exports.createZipSync = createZipSync;
|
|
29
|
+
const crc32_js_1 = require("./crc32");
|
|
30
|
+
const compress_js_1 = require("./compress");
|
|
31
|
+
// ZIP signature constants
|
|
32
|
+
const LOCAL_FILE_HEADER_SIG = 0x04034b50;
|
|
33
|
+
const CENTRAL_DIR_HEADER_SIG = 0x02014b50;
|
|
34
|
+
const END_OF_CENTRAL_DIR_SIG = 0x06054b50;
|
|
35
|
+
// ZIP version constants
|
|
36
|
+
const VERSION_NEEDED = 20; // 2.0 - supports DEFLATE
|
|
37
|
+
const VERSION_MADE_BY = 20; // 2.0
|
|
38
|
+
// Compression methods
|
|
39
|
+
const COMPRESSION_STORE = 0;
|
|
40
|
+
const COMPRESSION_DEFLATE = 8;
|
|
41
|
+
/**
|
|
42
|
+
* Convert Date to DOS time format
|
|
43
|
+
* @param date - Date to convert
|
|
44
|
+
* @returns [dosTime, dosDate]
|
|
45
|
+
*/
|
|
46
|
+
function dateToDos(date) {
|
|
47
|
+
const dosTime = ((date.getHours() & 0x1f) << 11) |
|
|
48
|
+
((date.getMinutes() & 0x3f) << 5) |
|
|
49
|
+
((date.getSeconds() >> 1) & 0x1f);
|
|
50
|
+
const dosDate = (((date.getFullYear() - 1980) & 0x7f) << 9) |
|
|
51
|
+
(((date.getMonth() + 1) & 0x0f) << 5) |
|
|
52
|
+
(date.getDate() & 0x1f);
|
|
53
|
+
return [dosTime, dosDate];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Encode string to UTF-8 bytes
|
|
57
|
+
*/
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
function encodeString(str) {
|
|
60
|
+
return encoder.encode(str);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build Local File Header (30 bytes + filename + extra)
|
|
64
|
+
*/
|
|
65
|
+
function buildLocalFileHeader(entry) {
|
|
66
|
+
const header = new Uint8Array(30 + entry.name.length);
|
|
67
|
+
const view = new DataView(header.buffer);
|
|
68
|
+
view.setUint32(0, LOCAL_FILE_HEADER_SIG, true); // Signature
|
|
69
|
+
view.setUint16(4, VERSION_NEEDED, true); // Version needed to extract
|
|
70
|
+
view.setUint16(6, 0x0800, true); // General purpose bit flag (UTF-8 names)
|
|
71
|
+
view.setUint16(8, entry.compressionMethod, true); // Compression method
|
|
72
|
+
view.setUint16(10, entry.modTime, true); // Last mod time
|
|
73
|
+
view.setUint16(12, entry.modDate, true); // Last mod date
|
|
74
|
+
view.setUint32(14, entry.crc, true); // CRC-32
|
|
75
|
+
view.setUint32(18, entry.compressedData.length, true); // Compressed size
|
|
76
|
+
view.setUint32(22, entry.data.length, true); // Uncompressed size
|
|
77
|
+
view.setUint16(26, entry.name.length, true); // Filename length
|
|
78
|
+
view.setUint16(28, 0, true); // Extra field length
|
|
79
|
+
header.set(entry.name, 30);
|
|
80
|
+
return header;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build Central Directory Header (46 bytes + filename + extra + comment)
|
|
84
|
+
*/
|
|
85
|
+
function buildCentralDirHeader(entry) {
|
|
86
|
+
const header = new Uint8Array(46 + entry.name.length + entry.comment.length);
|
|
87
|
+
const view = new DataView(header.buffer);
|
|
88
|
+
view.setUint32(0, CENTRAL_DIR_HEADER_SIG, true); // Signature
|
|
89
|
+
view.setUint16(4, VERSION_MADE_BY, true); // Version made by
|
|
90
|
+
view.setUint16(6, VERSION_NEEDED, true); // Version needed to extract
|
|
91
|
+
view.setUint16(8, 0x0800, true); // General purpose bit flag (UTF-8 names)
|
|
92
|
+
view.setUint16(10, entry.compressionMethod, true); // Compression method
|
|
93
|
+
view.setUint16(12, entry.modTime, true); // Last mod time
|
|
94
|
+
view.setUint16(14, entry.modDate, true); // Last mod date
|
|
95
|
+
view.setUint32(16, entry.crc, true); // CRC-32
|
|
96
|
+
view.setUint32(20, entry.compressedData.length, true); // Compressed size
|
|
97
|
+
view.setUint32(24, entry.data.length, true); // Uncompressed size
|
|
98
|
+
view.setUint16(28, entry.name.length, true); // Filename length
|
|
99
|
+
view.setUint16(30, 0, true); // Extra field length
|
|
100
|
+
view.setUint16(32, entry.comment.length, true); // Comment length
|
|
101
|
+
view.setUint16(34, 0, true); // Disk number start
|
|
102
|
+
view.setUint16(36, 0, true); // Internal file attributes
|
|
103
|
+
view.setUint32(38, 0, true); // External file attributes
|
|
104
|
+
view.setUint32(42, entry.offset, true); // Relative offset of local header
|
|
105
|
+
header.set(entry.name, 46);
|
|
106
|
+
if (entry.comment.length > 0) {
|
|
107
|
+
header.set(entry.comment, 46 + entry.name.length);
|
|
108
|
+
}
|
|
109
|
+
return header;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build End of Central Directory Record (22 bytes + comment)
|
|
113
|
+
*/
|
|
114
|
+
function buildEndOfCentralDir(entryCount, centralDirSize, centralDirOffset, comment) {
|
|
115
|
+
const record = new Uint8Array(22 + comment.length);
|
|
116
|
+
const view = new DataView(record.buffer);
|
|
117
|
+
view.setUint32(0, END_OF_CENTRAL_DIR_SIG, true); // Signature
|
|
118
|
+
view.setUint16(4, 0, true); // Number of this disk
|
|
119
|
+
view.setUint16(6, 0, true); // Disk where central dir starts
|
|
120
|
+
view.setUint16(8, entryCount, true); // Number of entries on this disk
|
|
121
|
+
view.setUint16(10, entryCount, true); // Total number of entries
|
|
122
|
+
view.setUint32(12, centralDirSize, true); // Size of central directory
|
|
123
|
+
view.setUint32(16, centralDirOffset, true); // Offset of central directory
|
|
124
|
+
view.setUint16(20, comment.length, true); // Comment length
|
|
125
|
+
if (comment.length > 0) {
|
|
126
|
+
record.set(comment, 22);
|
|
127
|
+
}
|
|
128
|
+
return record;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create a ZIP file from entries (async)
|
|
132
|
+
*
|
|
133
|
+
* @param entries - Files to include in ZIP
|
|
134
|
+
* @param options - ZIP options
|
|
135
|
+
* @returns ZIP file as Uint8Array
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* const zip = await createZip([
|
|
140
|
+
* { name: "hello.txt", data: new TextEncoder().encode("Hello!") },
|
|
141
|
+
* { name: "folder/file.txt", data: new TextEncoder().encode("Nested!") }
|
|
142
|
+
* ], { level: 6 });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
async function createZip(entries, options = {}) {
|
|
146
|
+
const level = options.level ?? 6;
|
|
147
|
+
const zipComment = encodeString(options.comment ?? "");
|
|
148
|
+
const now = new Date();
|
|
149
|
+
// Process entries
|
|
150
|
+
const processedEntries = [];
|
|
151
|
+
let currentOffset = 0;
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const nameBytes = encodeString(entry.name);
|
|
154
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
155
|
+
const modDate = entry.modTime ?? now;
|
|
156
|
+
const [dosTime, dosDate] = dateToDos(modDate);
|
|
157
|
+
// Compress data
|
|
158
|
+
const isCompressed = level > 0 && entry.data.length > 0;
|
|
159
|
+
const compressedData = isCompressed ? await (0, compress_js_1.compress)(entry.data, { level }) : entry.data;
|
|
160
|
+
const processedEntry = {
|
|
161
|
+
name: nameBytes,
|
|
162
|
+
data: entry.data,
|
|
163
|
+
compressedData,
|
|
164
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
165
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
166
|
+
modTime: dosTime,
|
|
167
|
+
modDate: dosDate,
|
|
168
|
+
comment: commentBytes,
|
|
169
|
+
offset: currentOffset
|
|
170
|
+
};
|
|
171
|
+
// Calculate offset for next entry
|
|
172
|
+
currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
173
|
+
processedEntries.push(processedEntry);
|
|
174
|
+
}
|
|
175
|
+
// Build ZIP structure
|
|
176
|
+
const chunks = [];
|
|
177
|
+
// Local file headers and data
|
|
178
|
+
for (const entry of processedEntries) {
|
|
179
|
+
chunks.push(buildLocalFileHeader(entry));
|
|
180
|
+
chunks.push(entry.compressedData);
|
|
181
|
+
}
|
|
182
|
+
const centralDirOffset = currentOffset;
|
|
183
|
+
// Central directory
|
|
184
|
+
const centralDirChunks = [];
|
|
185
|
+
for (const entry of processedEntries) {
|
|
186
|
+
centralDirChunks.push(buildCentralDirHeader(entry));
|
|
187
|
+
}
|
|
188
|
+
chunks.push(...centralDirChunks);
|
|
189
|
+
const centralDirSize = centralDirChunks.reduce((sum, c) => sum + c.length, 0);
|
|
190
|
+
// End of central directory
|
|
191
|
+
chunks.push(buildEndOfCentralDir(processedEntries.length, centralDirSize, centralDirOffset, zipComment));
|
|
192
|
+
// Combine all chunks
|
|
193
|
+
const totalSize = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
194
|
+
const result = new Uint8Array(totalSize);
|
|
195
|
+
let offset = 0;
|
|
196
|
+
for (const chunk of chunks) {
|
|
197
|
+
result.set(chunk, offset);
|
|
198
|
+
offset += chunk.length;
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create a ZIP file from entries (sync, Node.js only)
|
|
204
|
+
*
|
|
205
|
+
* @param entries - Files to include in ZIP
|
|
206
|
+
* @param options - ZIP options
|
|
207
|
+
* @returns ZIP file as Uint8Array
|
|
208
|
+
* @throws Error if not in Node.js environment
|
|
209
|
+
*/
|
|
210
|
+
function createZipSync(entries, options = {}) {
|
|
211
|
+
const level = options.level ?? 6;
|
|
212
|
+
const zipComment = encodeString(options.comment ?? "");
|
|
213
|
+
const now = new Date();
|
|
214
|
+
// Process entries
|
|
215
|
+
const processedEntries = [];
|
|
216
|
+
let currentOffset = 0;
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const nameBytes = encodeString(entry.name);
|
|
219
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
220
|
+
const modDate = entry.modTime ?? now;
|
|
221
|
+
const [dosTime, dosDate] = dateToDos(modDate);
|
|
222
|
+
// Compress data
|
|
223
|
+
const isCompressed = level > 0 && entry.data.length > 0;
|
|
224
|
+
const compressedData = isCompressed ? (0, compress_js_1.compressSync)(entry.data, { level }) : entry.data;
|
|
225
|
+
const processedEntry = {
|
|
226
|
+
name: nameBytes,
|
|
227
|
+
data: entry.data,
|
|
228
|
+
compressedData,
|
|
229
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
230
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
231
|
+
modTime: dosTime,
|
|
232
|
+
modDate: dosDate,
|
|
233
|
+
comment: commentBytes,
|
|
234
|
+
offset: currentOffset
|
|
235
|
+
};
|
|
236
|
+
currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
237
|
+
processedEntries.push(processedEntry);
|
|
238
|
+
}
|
|
239
|
+
// Build ZIP structure
|
|
240
|
+
const chunks = [];
|
|
241
|
+
// Local file headers and data
|
|
242
|
+
for (const entry of processedEntries) {
|
|
243
|
+
chunks.push(buildLocalFileHeader(entry));
|
|
244
|
+
chunks.push(entry.compressedData);
|
|
245
|
+
}
|
|
246
|
+
const centralDirOffset = currentOffset;
|
|
247
|
+
// Central directory
|
|
248
|
+
const centralDirChunks = [];
|
|
249
|
+
for (const entry of processedEntries) {
|
|
250
|
+
centralDirChunks.push(buildCentralDirHeader(entry));
|
|
251
|
+
}
|
|
252
|
+
chunks.push(...centralDirChunks);
|
|
253
|
+
const centralDirSize = centralDirChunks.reduce((sum, c) => sum + c.length, 0);
|
|
254
|
+
// End of central directory
|
|
255
|
+
chunks.push(buildEndOfCentralDir(processedEntries.length, centralDirSize, centralDirOffset, zipComment));
|
|
256
|
+
// Combine all chunks
|
|
257
|
+
const totalSize = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
258
|
+
const result = new Uint8Array(totalSize);
|
|
259
|
+
let offset = 0;
|
|
260
|
+
for (const chunk of chunks) {
|
|
261
|
+
result.set(chunk, offset);
|
|
262
|
+
offset += chunk.length;
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Streaming ZIP builder for large files
|
|
268
|
+
* Writes chunks to a callback as they are generated
|
|
269
|
+
*/
|
|
270
|
+
class ZipBuilder {
|
|
271
|
+
/**
|
|
272
|
+
* Create a new ZIP builder
|
|
273
|
+
* @param options - ZIP options
|
|
274
|
+
*/
|
|
275
|
+
constructor(options = {}) {
|
|
276
|
+
this.entries = [];
|
|
277
|
+
this.currentOffset = 0;
|
|
278
|
+
this.finalized = false;
|
|
279
|
+
this.level = options.level ?? 6;
|
|
280
|
+
this.zipComment = encodeString(options.comment ?? "");
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Add a file to the ZIP (async)
|
|
284
|
+
* @param entry - File entry
|
|
285
|
+
* @returns Local file header and compressed data chunks
|
|
286
|
+
*/
|
|
287
|
+
async addFile(entry) {
|
|
288
|
+
if (this.finalized) {
|
|
289
|
+
throw new Error("Cannot add files after finalizing");
|
|
290
|
+
}
|
|
291
|
+
const nameBytes = encodeString(entry.name);
|
|
292
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
293
|
+
const [dosTime, dosDate] = dateToDos(entry.modTime ?? new Date());
|
|
294
|
+
// Compress data
|
|
295
|
+
const isCompressed = this.level > 0 && entry.data.length > 0;
|
|
296
|
+
const compressedData = isCompressed
|
|
297
|
+
? await (0, compress_js_1.compress)(entry.data, { level: this.level })
|
|
298
|
+
: entry.data;
|
|
299
|
+
const processedEntry = {
|
|
300
|
+
name: nameBytes,
|
|
301
|
+
data: entry.data,
|
|
302
|
+
compressedData,
|
|
303
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
304
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
305
|
+
modTime: dosTime,
|
|
306
|
+
modDate: dosDate,
|
|
307
|
+
comment: commentBytes,
|
|
308
|
+
offset: this.currentOffset
|
|
309
|
+
};
|
|
310
|
+
this.entries.push(processedEntry);
|
|
311
|
+
this.currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
312
|
+
return [buildLocalFileHeader(processedEntry), compressedData];
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Add a file to the ZIP (sync, Node.js only)
|
|
316
|
+
* @param entry - File entry
|
|
317
|
+
* @returns Local file header and compressed data chunks
|
|
318
|
+
*/
|
|
319
|
+
addFileSync(entry) {
|
|
320
|
+
if (this.finalized) {
|
|
321
|
+
throw new Error("Cannot add files after finalizing");
|
|
322
|
+
}
|
|
323
|
+
const nameBytes = encodeString(entry.name);
|
|
324
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
325
|
+
const [dosTime, dosDate] = dateToDos(entry.modTime ?? new Date());
|
|
326
|
+
// Compress data
|
|
327
|
+
const isCompressed = this.level > 0 && entry.data.length > 0;
|
|
328
|
+
const compressedData = isCompressed
|
|
329
|
+
? (0, compress_js_1.compressSync)(entry.data, { level: this.level })
|
|
330
|
+
: entry.data;
|
|
331
|
+
const processedEntry = {
|
|
332
|
+
name: nameBytes,
|
|
333
|
+
data: entry.data,
|
|
334
|
+
compressedData,
|
|
335
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
336
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
337
|
+
modTime: dosTime,
|
|
338
|
+
modDate: dosDate,
|
|
339
|
+
comment: commentBytes,
|
|
340
|
+
offset: this.currentOffset
|
|
341
|
+
};
|
|
342
|
+
this.entries.push(processedEntry);
|
|
343
|
+
this.currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
344
|
+
return [buildLocalFileHeader(processedEntry), compressedData];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Finalize the ZIP and return central directory + end record
|
|
348
|
+
* @returns Central directory and end of central directory chunks
|
|
349
|
+
*/
|
|
350
|
+
finalize() {
|
|
351
|
+
if (this.finalized) {
|
|
352
|
+
throw new Error("ZIP already finalized");
|
|
353
|
+
}
|
|
354
|
+
this.finalized = true;
|
|
355
|
+
const chunks = [];
|
|
356
|
+
// Central directory
|
|
357
|
+
for (const entry of this.entries) {
|
|
358
|
+
chunks.push(buildCentralDirHeader(entry));
|
|
359
|
+
}
|
|
360
|
+
const centralDirSize = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
361
|
+
// End of central directory
|
|
362
|
+
chunks.push(buildEndOfCentralDir(this.entries.length, centralDirSize, this.currentOffset, this.zipComment));
|
|
363
|
+
return chunks;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get current number of entries
|
|
367
|
+
*/
|
|
368
|
+
get entryCount() {
|
|
369
|
+
return this.entries.length;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get current ZIP data size (without central directory)
|
|
373
|
+
*/
|
|
374
|
+
get dataSize() {
|
|
375
|
+
return this.currentOffset;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
exports.ZipBuilder = ZipBuilder;
|