UNPKG

15.1 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.ZipperEntryExtraField = exports.ZipperEntry = exports.Zipper = void 0;
7var _nodeZlib = require("node:zlib");
8var _iconEncoder = require("@shockpkg/icon-encoder");
9/* eslint-disable max-classes-per-file */
10
11/**
12 * Zipper write stream interface.
13 * A subset of Writable.
14 */
15
16/**
17 * Zipper Entry Extra Field object.
18 */
19class ZipperEntryExtraField {
20 /**
21 * Type ID.
22 */
23 type = 0;
24
25 /**
26 * Data for the type.
27 */
28 data = new Uint8Array(0);
29
30 /**
31 * Zipper Entry Extra Field constructor.
32 */
33 constructor() {}
34
35 /**
36 * Get the encode size.
37 *
38 * @returns Encode size.
39 */
40 sizeof() {
41 return 4 + this.data.length;
42 }
43
44 /**
45 * Encode type and data as data.
46 *
47 * @returns Encoded data.
48 */
49 encode() {
50 const {
51 data,
52 type
53 } = this;
54 const d = new Uint8Array(this.sizeof());
55 const v = new DataView(d.buffer, d.byteOffset, d.byteLength);
56 v.setUint16(0, type, true);
57 v.setUint16(2, data.length, true);
58 d.set(data, 4);
59 return d;
60 }
61
62 /**
63 * Init Info-ZIP UNIX type 2 data, local header.
64 *
65 * @param uid User ID.
66 * @param gid Group ID.
67 */
68 initInfoZipUnix2Local(uid = 0, gid = 0) {
69 this._initInfoZipUnix2(true, uid, gid);
70 }
71
72 /**
73 * Init Info-ZIP UNIX type 2 data, central header.
74 *
75 * @param uid User ID.
76 * @param gid Group ID.
77 */
78 initInfoZipUnix2Central(uid = 0, gid = 0) {
79 this._initInfoZipUnix2(false, uid, gid);
80 }
81
82 /**
83 * Init Extended Timestamp data, local header.
84 *
85 * @param mtime Modification time.
86 * @param atime Access time.
87 * @param ctime Creation time.
88 */
89 initExtendedTimestampLocal(mtime = null, atime = null, ctime = null) {
90 this._initExtendedTimestamp(true, mtime, atime, ctime);
91 }
92
93 /**
94 * Init Extended Timestamp data, central header.
95 *
96 * @param mtime Modification time.
97 * @param atime Access time.
98 * @param ctime Creation time.
99 */
100 initExtendedTimestampCentral(mtime = null, atime = null, ctime = null) {
101 this._initExtendedTimestamp(false, mtime, atime, ctime);
102 }
103
104 /**
105 * Init Info-ZIP UNIX type 2 data.
106 *
107 * @param local Local header or central.
108 * @param uid User ID.
109 * @param gid Group ID.
110 */
111 _initInfoZipUnix2(local, uid, gid) {
112 const d = local ? new Uint8Array(4) : new Uint8Array(0);
113 if (local) {
114 const v = new DataView(d.buffer, d.byteOffset, d.byteLength);
115 v.setUint16(0, uid, true);
116 v.setUint16(2, gid, true);
117 }
118
119 // Type: 'Ux'
120 this.type = 0x7855;
121 this.data = d;
122 }
123
124 /**
125 * Init Extended Timestamp data.
126 *
127 * @param local Local header or central.
128 * @param mtime Modification time.
129 * @param atime Access time.
130 * @param ctime Creation time.
131 */
132 _initExtendedTimestamp(local, mtime, atime, ctime) {
133 let flags = 0;
134 const times = [];
135 [mtime, atime, ctime].forEach((v, i) => {
136 if (v === null) {
137 return;
138 }
139
140 // eslint-disable-next-line no-bitwise
141 flags |= 1 << i;
142 if (local || i) {
143 times.push(typeof v === 'number' ? v : Math.round(v.getTime() / 1000));
144 }
145 });
146 const d = new Uint8Array(1 + times.length * 4);
147 let i = 0;
148 d[i++] = flags;
149 const v = new DataView(d.buffer, d.byteOffset, d.byteLength);
150 for (const time of times) {
151 v.setUint32(i, time, true);
152 i += 4;
153 }
154
155 // Type: 'UT'
156 this.type = 0x5455;
157 this.data = d;
158 }
159}
160
161/**
162 * Zipper Entry object.
163 */
164exports.ZipperEntryExtraField = ZipperEntryExtraField;
165class ZipperEntry {
166 /**
167 * Tag signature, local header.
168 */
169 signatureLocal = 0x4034b50;
170
171 /**
172 * Tag signature, central header.
173 */
174 signatureCentral = 0x2014b50;
175
176 /**
177 * Extract version.
178 */
179 extractVersion = 0x14;
180
181 /**
182 * Extract host OS.
183 */
184 extractHostOS = 0;
185
186 /**
187 * Create version.
188 */
189 createVersion = 0x14;
190
191 /**
192 * Create host OS.
193 */
194 createHostOS = 0;
195
196 /**
197 * Extract flags.
198 */
199 flags = 0;
200
201 /**
202 * Compression type.
203 */
204 compression = 0;
205
206 /**
207 * DOS time.
208 */
209 time = 0;
210
211 /**
212 * DOS date.
213 */
214 date = 0;
215
216 /**
217 * Data CRC32.
218 */
219 crc32 = 0;
220
221 /**
222 * Size compressed.
223 */
224 sizeCompressed = 0;
225
226 /**
227 * Size uncompressed.
228 */
229 sizeUncompressed = 0;
230
231 /**
232 * Disk number start.
233 */
234 diskNumberStart = 0;
235
236 /**
237 * Internal attributes.
238 */
239 internalAttributes = 0;
240
241 /**
242 * External attributes.
243 */
244 externalAttributes = 0;
245
246 /**
247 * Header offset, local header.
248 */
249 headerOffsetLocal = 0;
250
251 /**
252 * Entry path.
253 */
254 path = new Uint8Array(0);
255
256 /**
257 * Entry comment.
258 */
259 comment = new Uint8Array(0);
260
261 /**
262 * Extra fields, local header.
263 */
264 extraFieldsLocal = [];
265
266 /**
267 * Extra fields, central header.
268 */
269 extraFieldsCentral = [];
270
271 /**
272 * Zipper Entry constructor.
273 */
274 constructor() {}
275
276 /**
277 * Get the file record extra fields size.
278 *
279 * @returns Extra fields size.
280 */
281 sizeofExtraFieldsLocal() {
282 let r = 0;
283 for (const ef of this.extraFieldsLocal) {
284 r += ef.sizeof();
285 }
286 return r;
287 }
288
289 /**
290 * Get the file record extra fields size.
291 *
292 * @returns Extra fields size.
293 */
294 sizeofLocal() {
295 return 30 + this.path.length + this.sizeofExtraFieldsLocal();
296 }
297
298 /**
299 * Get the file record extra fields size.
300 *
301 * @returns Extra fields size.
302 */
303 sizeofExtraFieldsCentral() {
304 let r = 0;
305 for (const ef of this.extraFieldsCentral) {
306 r += ef.sizeof();
307 }
308 return r;
309 }
310
311 /**
312 * Get the central record extra fields size.
313 *
314 * @returns Extra fields size.
315 */
316 sizeofCentral() {
317 return 46 + this.path.length + this.comment.length + this.sizeofExtraFieldsCentral();
318 }
319
320 /**
321 * Create new ZipperEntryExtraField object.
322 *
323 * @returns ZipperEntryExtraField object.
324 */
325 createExtraField() {
326 return new ZipperEntryExtraField();
327 }
328
329 /**
330 * Set date from a date object or timestamp.
331 *
332 * @param date Date object or timestamp.
333 */
334 setDate(date) {
335 const dosTime = this._dateToDosTime(date);
336 this.date = dosTime.date;
337 this.time = dosTime.time;
338 }
339
340 /**
341 * Get local record data.
342 *
343 * @returns Local record data.
344 */
345 encodeLocal() {
346 const {
347 path,
348 extraFieldsLocal
349 } = this;
350 const d = new Uint8Array(this.sizeofLocal());
351 const v = new DataView(d.buffer, d.byteOffset, d.byteLength);
352 let i = 0;
353 v.setUint32(i, this.signatureLocal, true);
354 i += 4;
355 v.setUint8(i++, this.extractVersion);
356 v.setUint8(i++, this.extractHostOS);
357 v.setUint16(i, this.flags, true);
358 i += 2;
359 v.setUint16(i, this.compression, true);
360 i += 2;
361 v.setUint16(i, this.time, true);
362 i += 2;
363 v.setUint16(i, this.date, true);
364 i += 2;
365 v.setUint32(i, this.crc32, true);
366 i += 4;
367 v.setUint32(i, this.sizeCompressed, true);
368 i += 4;
369 v.setUint32(i, this.sizeUncompressed, true);
370 i += 4;
371 v.setUint16(i, path.length, true);
372 i += 2;
373 v.setUint16(i, this.sizeofExtraFieldsLocal(), true);
374 i += 2;
375 d.set(path, i);
376 i += path.length;
377 for (const ef of extraFieldsLocal) {
378 const e = ef.encode();
379 d.set(e, i);
380 i += e.length;
381 }
382 return d;
383 }
384
385 /**
386 * Get central record data.
387 *
388 * @returns Central entry data.
389 */
390 encodeCentral() {
391 const {
392 path,
393 comment,
394 extraFieldsCentral
395 } = this;
396 const d = new Uint8Array(this.sizeofCentral());
397 const v = new DataView(d.buffer, d.byteOffset, d.byteLength);
398 let i = 0;
399 v.setUint32(i, this.signatureCentral, true);
400 i += 4;
401 v.setUint8(i++, this.createVersion);
402 v.setUint8(i++, this.createHostOS);
403 v.setUint8(i++, this.extractVersion);
404 v.setUint8(i++, this.extractHostOS);
405 v.setUint16(i, this.flags, true);
406 i += 2;
407 v.setUint16(i, this.compression, true);
408 i += 2;
409 v.setUint16(i, this.time, true);
410 i += 2;
411 v.setUint16(i, this.date, true);
412 i += 2;
413 v.setUint32(i, this.crc32, true);
414 i += 4;
415 v.setUint32(i, this.sizeCompressed, true);
416 i += 4;
417 v.setUint32(i, this.sizeUncompressed, true);
418 i += 4;
419 v.setUint16(i, path.length, true);
420 i += 2;
421 v.setUint16(i, this.sizeofExtraFieldsCentral(), true);
422 i += 2;
423 v.setUint16(i, comment.length, true);
424 i += 2;
425 v.setUint16(i, this.diskNumberStart, true);
426 i += 2;
427 v.setUint16(i, this.internalAttributes, true);
428 i += 2;
429 v.setUint32(i, this.externalAttributes, true);
430 i += 4;
431 v.setUint32(i, this.headerOffsetLocal, true);
432 i += 4;
433 d.set(path, i);
434 i += path.length;
435 for (const ef of extraFieldsCentral) {
436 const e = ef.encode();
437 d.set(e, i);
438 i += e.length;
439 }
440 d.set(comment, i);
441 return d;
442 }
443
444 /**
445 * Setup data for entry.
446 *
447 * @param data Data for the entry.
448 * @param compress Compress option, true to force, false to disable.
449 * @returns Resulting data, or null if no data passed.
450 */
451 async initData(data, compress = null) {
452 this.compression = 0;
453 this.crc32 = 0;
454 this.sizeCompressed = 0;
455 this.sizeUncompressed = 0;
456 if (!data) {
457 return null;
458 }
459 const crc32 = this._crc32(data);
460 if (compress === false) {
461 this.crc32 = crc32;
462 this.sizeCompressed = this.sizeUncompressed = data.length;
463 this.compression = 0;
464 return data;
465 }
466 if (compress === true) {
467 const comp = await this._zlibDeflateRaw(data);
468 this.crc32 = crc32;
469 this.sizeUncompressed = data.length;
470 this.sizeCompressed = comp.length;
471 this.compression = 8;
472 return comp;
473 }
474 const comp = await this._zlibDeflateRaw(data);
475 const r = comp.length < data.length ? comp : data;
476 this.crc32 = crc32;
477 this.sizeUncompressed = data.length;
478 this.sizeCompressed = r.length;
479 this.compression = r === data ? 0 : 8;
480 return r;
481 }
482
483 /**
484 * Add extra fields for Extended Timestamp.
485 *
486 * @param mtime Modification time.
487 * @param atime Access time.
488 * @param ctime Creation time.
489 */
490 addExtraFieldsExtendedTimestamp(mtime = null, atime = null, ctime = null) {
491 const efl = this.createExtraField();
492 efl.initExtendedTimestampLocal(mtime, atime, ctime);
493 this.extraFieldsLocal.push(efl);
494 const efc = this.createExtraField();
495 efc.initExtendedTimestampCentral(mtime, atime, ctime);
496 this.extraFieldsCentral.push(efc);
497 }
498
499 /**
500 * Add extra fields for Info-ZIP UNIX type 2.
501 *
502 * @param uid User ID.
503 * @param gid Group ID.
504 */
505 addExtraFieldsInfoZipUnix2(uid = 0, gid = 0) {
506 const efl = this.createExtraField();
507 efl.initInfoZipUnix2Local(uid, gid);
508 this.extraFieldsLocal.push(efl);
509 const efc = this.createExtraField();
510 efc.initInfoZipUnix2Central(uid, gid);
511 this.extraFieldsCentral.push(efc);
512 }
513
514 /**
515 * Convert date from a date object or timestamp.
516 *
517 * @param date Date object or timestamp.
518 * @returns DOS time.
519 */
520 _dateToDosTime(date) {
521 const d = typeof date === 'number' ? new Date(date * 1000) : date;
522 return {
523 date:
524 // eslint-disable-next-line no-bitwise
525 d.getDate() & 0x1f |
526 // eslint-disable-next-line no-bitwise
527 (d.getMonth() + 1 & 0xf) << 5 |
528 // eslint-disable-next-line no-bitwise
529 (d.getFullYear() - 1980 & 0x7f) << 9,
530 time:
531 // eslint-disable-next-line no-bitwise
532 Math.floor(d.getSeconds() / 2) |
533 // eslint-disable-next-line no-bitwise
534 (d.getMinutes() & 0x3f) << 5 |
535 // eslint-disable-next-line no-bitwise
536 (d.getHours() & 0x1f) << 11
537 };
538 }
539
540 /**
541 * Calculate the CRC32 hash for data.
542 *
543 * @param data Data to be hashed.
544 * @returns CRC32 hash.
545 */
546 _crc32(data) {
547 // eslint-disable-next-line no-bitwise
548 return (0, _iconEncoder.crc32)(data) >>> 0;
549 }
550
551 /**
552 * Zlib deflate raw data.
553 *
554 * @param data Data to be compressed.
555 * @returns Compressed data.
556 */
557 async _zlibDeflateRaw(data) {
558 return new Promise((resolve, reject) => {
559 (0, _nodeZlib.deflateRaw)(data, (err, d) => {
560 if (err) {
561 reject(err);
562 return;
563 }
564 resolve(new Uint8Array(d.buffer, d.byteOffset, d.byteLength));
565 });
566 });
567 }
568}
569
570/**
571 * Zipper, a low-level ZIP file writter.
572 */
573exports.ZipperEntry = ZipperEntry;
574class Zipper {
575 /**
576 * Tag signature.
577 */
578 signature = 0x6054b50;
579
580 /**
581 * Archive comment.
582 */
583 comment = new Uint8Array(0);
584
585 /**
586 * Added entries.
587 */
588 entries = [];
589
590 /**
591 * Current offset.
592 */
593 _offset = 0;
594
595 /**
596 * Output stream.
597 */
598
599 /**
600 * Zipper constructor.
601 *
602 * @param output Writable stream.
603 */
604 constructor(output) {
605 this._output = output;
606 }
607
608 /**
609 * Create new ZipperEntry object.
610 *
611 * @returns ZipperEntry object.
612 */
613 createEntry() {
614 return new ZipperEntry();
615 }
616
617 /**
618 * Add Entry and any associated data.
619 *
620 * @param entry Entry object.
621 * @param data Data from the entry initData method.
622 */
623 async addEntry(entry, data = null) {
624 const {
625 _offset
626 } = this;
627 const {
628 sizeCompressed
629 } = entry;
630 if (data) {
631 if (data.length !== sizeCompressed) {
632 throw new Error('Data length and compressed size must match');
633 }
634 } else if (sizeCompressed) {
635 throw new Error('Data required when compressed size not zero');
636 }
637 entry.headerOffsetLocal = _offset;
638 this.entries.push(entry);
639 await this._writeOutput(entry.encodeLocal());
640 if (data) {
641 await this._writeOutput(data);
642 }
643 }
644
645 /**
646 * Close stream.
647 */
648 async close() {
649 const {
650 _offset,
651 entries,
652 comment
653 } = this;
654 let size = 0;
655 for (const e of entries) {
656 const d = e.encodeCentral();
657 // eslint-disable-next-line no-await-in-loop
658 await this._writeOutput(d);
659 size += d.length;
660 }
661 const d = new Uint8Array(22 + comment.length);
662 const v = new DataView(d.buffer, d.byteOffset, d.byteLength);
663 let i = 0;
664 v.setUint32(i, this.signature, true);
665 i += 4;
666 v.setUint16(i, 0, true);
667 i += 2;
668 v.setUint16(i, 0, true);
669 i += 2;
670 v.setUint16(i, entries.length, true);
671 i += 2;
672 v.setUint16(i, entries.length, true);
673 i += 2;
674 v.setUint32(i, size, true);
675 i += 4;
676 v.setUint32(i, _offset, true);
677 i += 4;
678 v.setUint16(i, comment.length, true);
679 i += 2;
680 d.set(comment, i);
681 await this._writeOutput(d);
682 await new Promise((resolve, reject) => {
683 this._output.end(err => {
684 if (err) {
685 reject(err);
686 return;
687 }
688 resolve();
689 });
690 });
691 }
692
693 /**
694 * Write data buffer to output stream.
695 *
696 * @param data Output data.
697 */
698 async _writeOutput(data) {
699 await new Promise((resolve, reject) => {
700 this._output.write(data, err => {
701 if (err) {
702 reject(err);
703 return;
704 }
705 resolve();
706 });
707 });
708 this._offset += data.length;
709 }
710}
711exports.Zipper = Zipper;
712//# sourceMappingURL=zipper.js.map
\No newline at end of file