acker-dacker 0.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/src/zip.ts ADDED
@@ -0,0 +1,327 @@
1
+ import { lstat, mkdir, open, readdir, readlink } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { deflateRawSync } from 'node:zlib';
4
+ import { compareStableStrings, normalizeArchivePath } from './path-patterns';
5
+
6
+ type ZipEntryKind = 'file' | 'symlink';
7
+
8
+ type ZipFileEntry = {
9
+ readonly absolutePath: string;
10
+ readonly archivePath: string;
11
+ readonly kind: ZipEntryKind;
12
+ readonly linkTarget?: string;
13
+ readonly mode?: number;
14
+ readonly size: number;
15
+ };
16
+
17
+ type WrittenZipEntry = ZipFileEntry & {
18
+ readonly compressedSize: number;
19
+ readonly compressionMethod: number;
20
+ readonly crc32: number;
21
+ readonly localHeaderOffset: number;
22
+ readonly pathBytes: Uint8Array;
23
+ };
24
+
25
+ export type ZipCreationOptions = {
26
+ readonly preserveSymlinks: boolean;
27
+ readonly preserveUnixMetadata: boolean;
28
+ };
29
+
30
+ const ZIP_MAX_UINT32 = 0xffffffff;
31
+ const ZIP_MAX_UINT16 = 0xffff;
32
+ const ZIP_DOS_DATE_1980_01_01 = 0x21;
33
+ const ZIP_DEFLATE_METHOD = 8;
34
+ const ZIP_STORE_METHOD = 0;
35
+ const ZIP_UNIX_CREATOR_VERSION = 0x0314;
36
+ const MAX_BUFFERED_DEFLATE_BYTES = 64 * 1024 * 1024;
37
+
38
+ const CRC32_TABLE = new Uint32Array(256);
39
+ for (let index = 0; index < CRC32_TABLE.length; index += 1) {
40
+ let value = index;
41
+ for (let bit = 0; bit < 8; bit += 1) {
42
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
43
+ }
44
+ CRC32_TABLE[index] = value >>> 0;
45
+ }
46
+
47
+ const updateCrc32 = (crc: number, chunk: Uint8Array): number => {
48
+ let current = crc;
49
+ for (const byte of chunk) {
50
+ current = CRC32_TABLE[(current ^ byte) & 0xff]! ^ (current >>> 8);
51
+ }
52
+ return current >>> 0;
53
+ };
54
+
55
+ const collectZipFiles = async ({
56
+ directory,
57
+ excludedAbsolutePath,
58
+ preserveSymlinks,
59
+ preserveUnixMetadata,
60
+ rootDir,
61
+ }: {
62
+ readonly directory: string;
63
+ readonly excludedAbsolutePath: string;
64
+ readonly preserveSymlinks: boolean;
65
+ readonly preserveUnixMetadata: boolean;
66
+ readonly rootDir: string;
67
+ }): Promise<readonly ZipFileEntry[]> => {
68
+ const entries = await readdir(directory, { withFileTypes: true });
69
+ const files: ZipFileEntry[] = [];
70
+ for (const entry of entries.sort((left, right) => compareStableStrings(left.name, right.name))) {
71
+ const absolutePath = path.join(directory, entry.name);
72
+ if (absolutePath === excludedAbsolutePath) {
73
+ continue;
74
+ }
75
+
76
+ if (entry.isSymbolicLink()) {
77
+ if (!preserveSymlinks) {
78
+ continue;
79
+ }
80
+ const linkTarget = await readlink(absolutePath);
81
+ files.push({
82
+ absolutePath,
83
+ archivePath: normalizeArchivePath(path.relative(rootDir, absolutePath)),
84
+ kind: 'symlink',
85
+ linkTarget,
86
+ mode: 0o120777,
87
+ size: new TextEncoder().encode(linkTarget).byteLength,
88
+ });
89
+ continue;
90
+ }
91
+
92
+ if (entry.isDirectory()) {
93
+ files.push(
94
+ ...(await collectZipFiles({
95
+ directory: absolutePath,
96
+ excludedAbsolutePath,
97
+ preserveSymlinks,
98
+ preserveUnixMetadata,
99
+ rootDir,
100
+ })),
101
+ );
102
+ continue;
103
+ }
104
+ if (!entry.isFile()) {
105
+ continue;
106
+ }
107
+
108
+ const fileStats = await lstat(absolutePath);
109
+ if (fileStats.size > ZIP_MAX_UINT32) {
110
+ throw new Error(`Zip entry exceeds 4 GB limit: ${absolutePath}`);
111
+ }
112
+ files.push({
113
+ absolutePath,
114
+ archivePath: normalizeArchivePath(path.relative(rootDir, absolutePath)),
115
+ kind: 'file',
116
+ mode: preserveUnixMetadata ? fileStats.mode : undefined,
117
+ size: fileStats.size,
118
+ });
119
+ }
120
+ return files;
121
+ };
122
+
123
+ const writeBuffer = async (
124
+ handle: Awaited<ReturnType<typeof open>>,
125
+ buffer: Uint8Array,
126
+ offset: number,
127
+ ): Promise<number> => {
128
+ await handle.write(buffer, 0, buffer.length, offset);
129
+ return offset + buffer.length;
130
+ };
131
+
132
+ const writeZipLocalHeader = async (
133
+ handle: Awaited<ReturnType<typeof open>>,
134
+ entry: ZipFileEntry,
135
+ pathBytes: Uint8Array,
136
+ crc32: number,
137
+ compressedSize: number,
138
+ compressionMethod: number,
139
+ offset: number,
140
+ ): Promise<number> => {
141
+ const header = Buffer.alloc(30);
142
+ header.writeUInt32LE(0x04034b50, 0);
143
+ header.writeUInt16LE(20, 4);
144
+ header.writeUInt16LE(0, 6);
145
+ header.writeUInt16LE(compressionMethod, 8);
146
+ header.writeUInt16LE(0, 10);
147
+ header.writeUInt16LE(ZIP_DOS_DATE_1980_01_01, 12);
148
+ header.writeUInt32LE(crc32, 14);
149
+ header.writeUInt32LE(compressedSize, 18);
150
+ header.writeUInt32LE(entry.size, 22);
151
+ header.writeUInt16LE(pathBytes.length, 26);
152
+ header.writeUInt16LE(0, 28);
153
+ const nextOffset = await writeBuffer(handle, header, offset);
154
+ return await writeBuffer(handle, pathBytes, nextOffset);
155
+ };
156
+
157
+ const computeFileCrc32 = async (absolutePath: string): Promise<number> => {
158
+ const reader = Bun.file(absolutePath).stream().getReader();
159
+ let crc = 0xffffffff;
160
+ try {
161
+ while (true) {
162
+ const { done, value } = await reader.read();
163
+ if (done) {
164
+ break;
165
+ }
166
+ crc = updateCrc32(crc, value);
167
+ }
168
+ } finally {
169
+ reader.releaseLock();
170
+ }
171
+ return (crc ^ 0xffffffff) >>> 0;
172
+ };
173
+
174
+ const prepareFileBytes = async (entry: ZipFileEntry) => {
175
+ if (entry.kind === 'symlink') {
176
+ const source = new TextEncoder().encode(entry.linkTarget ?? '');
177
+ const compressed = deflateRawSync(source, { level: 9 });
178
+ if (compressed.byteLength >= source.byteLength) {
179
+ return {
180
+ bytes: source,
181
+ compressedSize: source.byteLength,
182
+ compressionMethod: ZIP_STORE_METHOD,
183
+ crc32: (updateCrc32(0xffffffff, source) ^ 0xffffffff) >>> 0,
184
+ };
185
+ }
186
+ return {
187
+ bytes: compressed,
188
+ compressedSize: compressed.byteLength,
189
+ compressionMethod: ZIP_DEFLATE_METHOD,
190
+ crc32: (updateCrc32(0xffffffff, source) ^ 0xffffffff) >>> 0,
191
+ };
192
+ }
193
+
194
+ if (entry.size > MAX_BUFFERED_DEFLATE_BYTES) {
195
+ return {
196
+ bytes: null,
197
+ compressedSize: entry.size,
198
+ compressionMethod: ZIP_STORE_METHOD,
199
+ crc32: await computeFileCrc32(entry.absolutePath),
200
+ };
201
+ }
202
+ const source = new Uint8Array(await Bun.file(entry.absolutePath).arrayBuffer());
203
+ const compressed = deflateRawSync(source, { level: 9 });
204
+ if (compressed.byteLength >= source.byteLength) {
205
+ return {
206
+ bytes: source,
207
+ compressedSize: source.byteLength,
208
+ compressionMethod: ZIP_STORE_METHOD,
209
+ crc32: (updateCrc32(0xffffffff, source) ^ 0xffffffff) >>> 0,
210
+ };
211
+ }
212
+ return {
213
+ bytes: compressed,
214
+ compressedSize: compressed.byteLength,
215
+ compressionMethod: ZIP_DEFLATE_METHOD,
216
+ crc32: (updateCrc32(0xffffffff, source) ^ 0xffffffff) >>> 0,
217
+ };
218
+ };
219
+
220
+ export const createZipFile = async (
221
+ rootDir: string,
222
+ outputFile: string,
223
+ options: ZipCreationOptions = { preserveSymlinks: false, preserveUnixMetadata: false },
224
+ ): Promise<void> => {
225
+ const absoluteOutputFile = path.resolve(outputFile);
226
+ const absoluteRootDir = path.resolve(rootDir);
227
+ await mkdir(path.dirname(absoluteOutputFile), { recursive: true });
228
+ const handle = await open(absoluteOutputFile, 'w');
229
+
230
+ try {
231
+ let offset = 0;
232
+ const writtenEntries: WrittenZipEntry[] = [];
233
+ const files = await collectZipFiles({
234
+ directory: absoluteRootDir,
235
+ excludedAbsolutePath: absoluteOutputFile,
236
+ preserveSymlinks: options.preserveSymlinks,
237
+ preserveUnixMetadata: options.preserveUnixMetadata,
238
+ rootDir: absoluteRootDir,
239
+ });
240
+
241
+ for (const entry of files) {
242
+ const pathBytes = new TextEncoder().encode(entry.archivePath);
243
+ if (pathBytes.length > ZIP_MAX_UINT16) {
244
+ throw new Error(`Path exceeds ZIP16 limit: ${entry.archivePath}`);
245
+ }
246
+
247
+ const prepared = await prepareFileBytes(entry);
248
+ const localHeaderOffset = offset;
249
+ offset = await writeZipLocalHeader(
250
+ handle,
251
+ entry,
252
+ pathBytes,
253
+ prepared.crc32,
254
+ prepared.compressedSize,
255
+ prepared.compressionMethod,
256
+ offset,
257
+ );
258
+
259
+ if (prepared.bytes) {
260
+ offset = await writeBuffer(handle, prepared.bytes, offset);
261
+ } else {
262
+ const reader = Bun.file(entry.absolutePath).stream().getReader();
263
+ try {
264
+ while (true) {
265
+ const { done, value } = await reader.read();
266
+ if (done) {
267
+ break;
268
+ }
269
+ offset = await writeBuffer(handle, value, offset);
270
+ }
271
+ } finally {
272
+ reader.releaseLock();
273
+ }
274
+ }
275
+
276
+ writtenEntries.push({
277
+ ...entry,
278
+ compressedSize: prepared.compressedSize,
279
+ compressionMethod: prepared.compressionMethod,
280
+ crc32: prepared.crc32,
281
+ localHeaderOffset,
282
+ pathBytes,
283
+ });
284
+ }
285
+
286
+ const centralDirectoryOffset = offset;
287
+ for (const entry of writtenEntries) {
288
+ const header = Buffer.alloc(46);
289
+ header.writeUInt32LE(0x02014b50, 0);
290
+ header.writeUInt16LE(options.preserveUnixMetadata ? ZIP_UNIX_CREATOR_VERSION : 20, 4);
291
+ header.writeUInt16LE(20, 6);
292
+ header.writeUInt16LE(0, 8);
293
+ header.writeUInt16LE(entry.compressionMethod, 10);
294
+ header.writeUInt16LE(0, 12);
295
+ header.writeUInt16LE(ZIP_DOS_DATE_1980_01_01, 14);
296
+ header.writeUInt32LE(entry.crc32, 16);
297
+ header.writeUInt32LE(entry.compressedSize, 20);
298
+ header.writeUInt32LE(entry.size, 24);
299
+ header.writeUInt16LE(entry.pathBytes.length, 28);
300
+ header.writeUInt16LE(0, 30);
301
+ header.writeUInt16LE(0, 32);
302
+ header.writeUInt16LE(0, 34);
303
+ header.writeUInt16LE(0, 36);
304
+ const externalAttributes = options.preserveUnixMetadata
305
+ ? ((((entry.mode ?? 0o100644) & 0xffff) << 16) >>> 0)
306
+ : 0;
307
+ header.writeUInt32LE(externalAttributes, 38);
308
+ header.writeUInt32LE(entry.localHeaderOffset, 42);
309
+ offset = await writeBuffer(handle, header, offset);
310
+ offset = await writeBuffer(handle, entry.pathBytes, offset);
311
+ }
312
+
313
+ const centralDirectorySize = offset - centralDirectoryOffset;
314
+ const endHeader = Buffer.alloc(22);
315
+ endHeader.writeUInt32LE(0x06054b50, 0);
316
+ endHeader.writeUInt16LE(0, 4);
317
+ endHeader.writeUInt16LE(0, 6);
318
+ endHeader.writeUInt16LE(writtenEntries.length, 8);
319
+ endHeader.writeUInt16LE(writtenEntries.length, 10);
320
+ endHeader.writeUInt32LE(centralDirectorySize, 12);
321
+ endHeader.writeUInt32LE(centralDirectoryOffset, 16);
322
+ endHeader.writeUInt16LE(0, 20);
323
+ await writeBuffer(handle, endHeader, offset);
324
+ } finally {
325
+ await handle.close();
326
+ }
327
+ };