UNPKG

39.7 kBJavaScriptView Raw
1/**
2 * @license node-stream-zip | (c) 2020 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE
3 * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE
4 */
5
6let fs = require('fs');
7const util = require('util');
8const path = require('path');
9const events = require('events');
10const zlib = require('zlib');
11const stream = require('stream');
12
13const consts = {
14 /* The local file header */
15 LOCHDR: 30, // LOC header size
16 LOCSIG: 0x04034b50, // "PK\003\004"
17 LOCVER: 4, // version needed to extract
18 LOCFLG: 6, // general purpose bit flag
19 LOCHOW: 8, // compression method
20 LOCTIM: 10, // modification time (2 bytes time, 2 bytes date)
21 LOCCRC: 14, // uncompressed file crc-32 value
22 LOCSIZ: 18, // compressed size
23 LOCLEN: 22, // uncompressed size
24 LOCNAM: 26, // filename length
25 LOCEXT: 28, // extra field length
26
27 /* The Data descriptor */
28 EXTSIG: 0x08074b50, // "PK\007\008"
29 EXTHDR: 16, // EXT header size
30 EXTCRC: 4, // uncompressed file crc-32 value
31 EXTSIZ: 8, // compressed size
32 EXTLEN: 12, // uncompressed size
33
34 /* The central directory file header */
35 CENHDR: 46, // CEN header size
36 CENSIG: 0x02014b50, // "PK\001\002"
37 CENVEM: 4, // version made by
38 CENVER: 6, // version needed to extract
39 CENFLG: 8, // encrypt, decrypt flags
40 CENHOW: 10, // compression method
41 CENTIM: 12, // modification time (2 bytes time, 2 bytes date)
42 CENCRC: 16, // uncompressed file crc-32 value
43 CENSIZ: 20, // compressed size
44 CENLEN: 24, // uncompressed size
45 CENNAM: 28, // filename length
46 CENEXT: 30, // extra field length
47 CENCOM: 32, // file comment length
48 CENDSK: 34, // volume number start
49 CENATT: 36, // internal file attributes
50 CENATX: 38, // external file attributes (host system dependent)
51 CENOFF: 42, // LOC header offset
52
53 /* The entries in the end of central directory */
54 ENDHDR: 22, // END header size
55 ENDSIG: 0x06054b50, // "PK\005\006"
56 ENDSIGFIRST: 0x50,
57 ENDSUB: 8, // number of entries on this disk
58 ENDTOT: 10, // total number of entries
59 ENDSIZ: 12, // central directory size in bytes
60 ENDOFF: 16, // offset of first CEN header
61 ENDCOM: 20, // zip file comment length
62 MAXFILECOMMENT: 0xffff,
63
64 /* The entries in the end of ZIP64 central directory locator */
65 ENDL64HDR: 20, // ZIP64 end of central directory locator header size
66 ENDL64SIG: 0x07064b50, // ZIP64 end of central directory locator signature
67 ENDL64SIGFIRST: 0x50,
68 ENDL64OFS: 8, // ZIP64 end of central directory offset
69
70 /* The entries in the end of ZIP64 central directory */
71 END64HDR: 56, // ZIP64 end of central directory header size
72 END64SIG: 0x06064b50, // ZIP64 end of central directory signature
73 END64SIGFIRST: 0x50,
74 END64SUB: 24, // number of entries on this disk
75 END64TOT: 32, // total number of entries
76 END64SIZ: 40,
77 END64OFF: 48,
78
79 /* Compression methods */
80 STORED: 0, // no compression
81 SHRUNK: 1, // shrunk
82 REDUCED1: 2, // reduced with compression factor 1
83 REDUCED2: 3, // reduced with compression factor 2
84 REDUCED3: 4, // reduced with compression factor 3
85 REDUCED4: 5, // reduced with compression factor 4
86 IMPLODED: 6, // imploded
87 // 7 reserved
88 DEFLATED: 8, // deflated
89 ENHANCED_DEFLATED: 9, // deflate64
90 PKWARE: 10, // PKWare DCL imploded
91 // 11 reserved
92 BZIP2: 12, // compressed using BZIP2
93 // 13 reserved
94 LZMA: 14, // LZMA
95 // 15-17 reserved
96 IBM_TERSE: 18, // compressed using IBM TERSE
97 IBM_LZ77: 19, //IBM LZ77 z
98
99 /* General purpose bit flag */
100 FLG_ENC: 0, // encrypted file
101 FLG_COMP1: 1, // compression option
102 FLG_COMP2: 2, // compression option
103 FLG_DESC: 4, // data descriptor
104 FLG_ENH: 8, // enhanced deflation
105 FLG_STR: 16, // strong encryption
106 FLG_LNG: 1024, // language encoding
107 FLG_MSK: 4096, // mask header values
108 FLG_ENTRY_ENC: 1,
109
110 /* 4.5 Extensible data fields */
111 EF_ID: 0,
112 EF_SIZE: 2,
113
114 /* Header IDs */
115 ID_ZIP64: 0x0001,
116 ID_AVINFO: 0x0007,
117 ID_PFS: 0x0008,
118 ID_OS2: 0x0009,
119 ID_NTFS: 0x000a,
120 ID_OPENVMS: 0x000c,
121 ID_UNIX: 0x000d,
122 ID_FORK: 0x000e,
123 ID_PATCH: 0x000f,
124 ID_X509_PKCS7: 0x0014,
125 ID_X509_CERTID_F: 0x0015,
126 ID_X509_CERTID_C: 0x0016,
127 ID_STRONGENC: 0x0017,
128 ID_RECORD_MGT: 0x0018,
129 ID_X509_PKCS7_RL: 0x0019,
130 ID_IBM1: 0x0065,
131 ID_IBM2: 0x0066,
132 ID_POSZIP: 0x4690,
133
134 EF_ZIP64_OR_32: 0xffffffff,
135 EF_ZIP64_OR_16: 0xffff,
136};
137
138const StreamZip = function (config) {
139 let fd, fileSize, chunkSize, op, centralDirectory, closed;
140 const ready = false,
141 that = this,
142 entries = config.storeEntries !== false ? {} : null,
143 fileName = config.file;
144
145 open();
146
147 function open() {
148 if (config.fd) {
149 fd = config.fd;
150 readFile();
151 } else {
152 fs.open(fileName, 'r', (err, f) => {
153 if (err) {
154 return that.emit('error', err);
155 }
156 fd = f;
157 readFile();
158 });
159 }
160 }
161
162 function readFile() {
163 fs.fstat(fd, (err, stat) => {
164 if (err) {
165 return that.emit('error', err);
166 }
167 fileSize = stat.size;
168 chunkSize = config.chunkSize || Math.round(fileSize / 1000);
169 chunkSize = Math.max(
170 Math.min(chunkSize, Math.min(128 * 1024, fileSize)),
171 Math.min(1024, fileSize)
172 );
173 readCentralDirectory();
174 });
175 }
176
177 function readUntilFoundCallback(err, bytesRead) {
178 if (err || !bytesRead) {
179 return that.emit('error', err || new Error('Archive read error'));
180 }
181 let pos = op.lastPos;
182 let bufferPosition = pos - op.win.position;
183 const buffer = op.win.buffer;
184 const minPos = op.minPos;
185 while (--pos >= minPos && --bufferPosition >= 0) {
186 if (buffer.length - bufferPosition >= 4 && buffer[bufferPosition] === op.firstByte) {
187 // quick check first signature byte
188 if (buffer.readUInt32LE(bufferPosition) === op.sig) {
189 op.lastBufferPosition = bufferPosition;
190 op.lastBytesRead = bytesRead;
191 op.complete();
192 return;
193 }
194 }
195 }
196 if (pos === minPos) {
197 return that.emit('error', new Error('Bad archive'));
198 }
199 op.lastPos = pos + 1;
200 op.chunkSize *= 2;
201 if (pos <= minPos) {
202 return that.emit('error', new Error('Bad archive'));
203 }
204 const expandLength = Math.min(op.chunkSize, pos - minPos);
205 op.win.expandLeft(expandLength, readUntilFoundCallback);
206 }
207
208 function readCentralDirectory() {
209 const totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, fileSize);
210 op = {
211 win: new FileWindowBuffer(fd),
212 totalReadLength,
213 minPos: fileSize - totalReadLength,
214 lastPos: fileSize,
215 chunkSize: Math.min(1024, chunkSize),
216 firstByte: consts.ENDSIGFIRST,
217 sig: consts.ENDSIG,
218 complete: readCentralDirectoryComplete,
219 };
220 op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback);
221 }
222
223 function readCentralDirectoryComplete() {
224 const buffer = op.win.buffer;
225 const pos = op.lastBufferPosition;
226 try {
227 centralDirectory = new CentralDirectoryHeader();
228 centralDirectory.read(buffer.slice(pos, pos + consts.ENDHDR));
229 centralDirectory.headerOffset = op.win.position + pos;
230 if (centralDirectory.commentLength) {
231 that.comment = buffer
232 .slice(
233 pos + consts.ENDHDR,
234 pos + consts.ENDHDR + centralDirectory.commentLength
235 )
236 .toString();
237 } else {
238 that.comment = null;
239 }
240 that.entriesCount = centralDirectory.volumeEntries;
241 that.centralDirectory = centralDirectory;
242 if (
243 (centralDirectory.volumeEntries === consts.EF_ZIP64_OR_16 &&
244 centralDirectory.totalEntries === consts.EF_ZIP64_OR_16) ||
245 centralDirectory.size === consts.EF_ZIP64_OR_32 ||
246 centralDirectory.offset === consts.EF_ZIP64_OR_32
247 ) {
248 readZip64CentralDirectoryLocator();
249 } else {
250 op = {};
251 readEntries();
252 }
253 } catch (err) {
254 that.emit('error', err);
255 }
256 }
257
258 function readZip64CentralDirectoryLocator() {
259 const length = consts.ENDL64HDR;
260 if (op.lastBufferPosition > length) {
261 op.lastBufferPosition -= length;
262 readZip64CentralDirectoryLocatorComplete();
263 } else {
264 op = {
265 win: op.win,
266 totalReadLength: length,
267 minPos: op.win.position - length,
268 lastPos: op.win.position,
269 chunkSize: op.chunkSize,
270 firstByte: consts.ENDL64SIGFIRST,
271 sig: consts.ENDL64SIG,
272 complete: readZip64CentralDirectoryLocatorComplete,
273 };
274 op.win.read(op.lastPos - op.chunkSize, op.chunkSize, readUntilFoundCallback);
275 }
276 }
277
278 function readZip64CentralDirectoryLocatorComplete() {
279 const buffer = op.win.buffer;
280 const locHeader = new CentralDirectoryLoc64Header();
281 locHeader.read(
282 buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.ENDL64HDR)
283 );
284 const readLength = fileSize - locHeader.headerOffset;
285 op = {
286 win: op.win,
287 totalReadLength: readLength,
288 minPos: locHeader.headerOffset,
289 lastPos: op.lastPos,
290 chunkSize: op.chunkSize,
291 firstByte: consts.END64SIGFIRST,
292 sig: consts.END64SIG,
293 complete: readZip64CentralDirectoryComplete,
294 };
295 op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback);
296 }
297
298 function readZip64CentralDirectoryComplete() {
299 const buffer = op.win.buffer;
300 const zip64cd = new CentralDirectoryZip64Header();
301 zip64cd.read(buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.END64HDR));
302 that.centralDirectory.volumeEntries = zip64cd.volumeEntries;
303 that.centralDirectory.totalEntries = zip64cd.totalEntries;
304 that.centralDirectory.size = zip64cd.size;
305 that.centralDirectory.offset = zip64cd.offset;
306 that.entriesCount = zip64cd.volumeEntries;
307 op = {};
308 readEntries();
309 }
310
311 function readEntries() {
312 op = {
313 win: new FileWindowBuffer(fd),
314 pos: centralDirectory.offset,
315 chunkSize,
316 entriesLeft: centralDirectory.volumeEntries,
317 };
318 op.win.read(op.pos, Math.min(chunkSize, fileSize - op.pos), readEntriesCallback);
319 }
320
321 function readEntriesCallback(err, bytesRead) {
322 if (err || !bytesRead) {
323 return that.emit('error', err || new Error('Entries read error'));
324 }
325 let bufferPos = op.pos - op.win.position;
326 let entry = op.entry;
327 const buffer = op.win.buffer;
328 const bufferLength = buffer.length;
329 try {
330 while (op.entriesLeft > 0) {
331 if (!entry) {
332 entry = new ZipEntry();
333 entry.readHeader(buffer, bufferPos);
334 entry.headerOffset = op.win.position + bufferPos;
335 op.entry = entry;
336 op.pos += consts.CENHDR;
337 bufferPos += consts.CENHDR;
338 }
339 const entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen;
340 const advanceBytes = entryHeaderSize + (op.entriesLeft > 1 ? consts.CENHDR : 0);
341 if (bufferLength - bufferPos < advanceBytes) {
342 op.win.moveRight(chunkSize, readEntriesCallback, bufferPos);
343 op.move = true;
344 return;
345 }
346 entry.read(buffer, bufferPos);
347 if (!config.skipEntryNameValidation) {
348 entry.validateName();
349 }
350 if (entries) {
351 entries[entry.name] = entry;
352 }
353 that.emit('entry', entry);
354 op.entry = entry = null;
355 op.entriesLeft--;
356 op.pos += entryHeaderSize;
357 bufferPos += entryHeaderSize;
358 }
359 that.emit('ready');
360 } catch (err) {
361 that.emit('error', err);
362 }
363 }
364
365 function checkEntriesExist() {
366 if (!entries) {
367 throw new Error('storeEntries disabled');
368 }
369 }
370
371 Object.defineProperty(this, 'ready', {
372 get() {
373 return ready;
374 },
375 });
376
377 this.entry = function (name) {
378 checkEntriesExist();
379 return entries[name];
380 };
381
382 this.entries = function () {
383 checkEntriesExist();
384 return entries;
385 };
386
387 this.stream = function (entry, callback) {
388 return this.openEntry(
389 entry,
390 (err, entry) => {
391 if (err) {
392 return callback(err);
393 }
394 const offset = dataOffset(entry);
395 let entryStream = new EntryDataReaderStream(fd, offset, entry.compressedSize);
396 if (entry.method === consts.STORED) {
397 // nothing to do
398 } else if (entry.method === consts.DEFLATED) {
399 entryStream = entryStream.pipe(zlib.createInflateRaw());
400 } else {
401 return callback(new Error('Unknown compression method: ' + entry.method));
402 }
403 if (canVerifyCrc(entry)) {
404 entryStream = entryStream.pipe(
405 new EntryVerifyStream(entryStream, entry.crc, entry.size)
406 );
407 }
408 callback(null, entryStream);
409 },
410 false
411 );
412 };
413
414 this.entryDataSync = function (entry) {
415 let err = null;
416 this.openEntry(
417 entry,
418 (e, en) => {
419 err = e;
420 entry = en;
421 },
422 true
423 );
424 if (err) {
425 throw err;
426 }
427 let data = Buffer.alloc(entry.compressedSize);
428 new FsRead(fd, data, 0, entry.compressedSize, dataOffset(entry), (e) => {
429 err = e;
430 }).read(true);
431 if (err) {
432 throw err;
433 }
434 if (entry.method === consts.STORED) {
435 // nothing to do
436 } else if (entry.method === consts.DEFLATED || entry.method === consts.ENHANCED_DEFLATED) {
437 data = zlib.inflateRawSync(data);
438 } else {
439 throw new Error('Unknown compression method: ' + entry.method);
440 }
441 if (data.length !== entry.size) {
442 throw new Error('Invalid size');
443 }
444 if (canVerifyCrc(entry)) {
445 const verify = new CrcVerify(entry.crc, entry.size);
446 verify.data(data);
447 }
448 return data;
449 };
450
451 this.openEntry = function (entry, callback, sync) {
452 if (typeof entry === 'string') {
453 checkEntriesExist();
454 entry = entries[entry];
455 if (!entry) {
456 return callback(new Error('Entry not found'));
457 }
458 }
459 if (!entry.isFile) {
460 return callback(new Error('Entry is not file'));
461 }
462 if (!fd) {
463 return callback(new Error('Archive closed'));
464 }
465 const buffer = Buffer.alloc(consts.LOCHDR);
466 new FsRead(fd, buffer, 0, buffer.length, entry.offset, (err) => {
467 if (err) {
468 return callback(err);
469 }
470 let readEx;
471 try {
472 entry.readDataHeader(buffer);
473 if (entry.encrypted) {
474 readEx = new Error('Entry encrypted');
475 }
476 } catch (ex) {
477 readEx = ex;
478 }
479 callback(readEx, entry);
480 }).read(sync);
481 };
482
483 function dataOffset(entry) {
484 return entry.offset + consts.LOCHDR + entry.fnameLen + entry.extraLen;
485 }
486
487 function canVerifyCrc(entry) {
488 // if bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written
489 return (entry.flags & 0x8) !== 0x8;
490 }
491
492 function extract(entry, outPath, callback) {
493 that.stream(entry, (err, stm) => {
494 if (err) {
495 callback(err);
496 } else {
497 let fsStm, errThrown;
498 stm.on('error', (err) => {
499 errThrown = err;
500 if (fsStm) {
501 stm.unpipe(fsStm);
502 fsStm.close(() => {
503 callback(err);
504 });
505 }
506 });
507 fs.open(outPath, 'w', (err, fdFile) => {
508 if (err) {
509 return callback(err);
510 }
511 if (errThrown) {
512 fs.close(fd, () => {
513 callback(errThrown);
514 });
515 return;
516 }
517 fsStm = fs.createWriteStream(outPath, { fd: fdFile });
518 fsStm.on('finish', () => {
519 that.emit('extract', entry, outPath);
520 if (!errThrown) {
521 callback();
522 }
523 });
524 stm.pipe(fsStm);
525 });
526 }
527 });
528 }
529
530 function createDirectories(baseDir, dirs, callback) {
531 if (!dirs.length) {
532 return callback();
533 }
534 let dir = dirs.shift();
535 dir = path.join(baseDir, path.join(...dir));
536 fs.mkdir(dir, (err) => {
537 if (err && err.code !== 'EEXIST') {
538 return callback(err);
539 }
540 createDirectories(baseDir, dirs, callback);
541 });
542 }
543
544 function extractFiles(baseDir, baseRelPath, files, callback, extractedCount) {
545 if (!files.length) {
546 return callback(null, extractedCount);
547 }
548 const file = files.shift();
549 const targetPath = path.join(baseDir, file.name.replace(baseRelPath, ''));
550 extract(file, targetPath, (err) => {
551 if (err) {
552 return callback(err, extractedCount);
553 }
554 extractFiles(baseDir, baseRelPath, files, callback, extractedCount + 1);
555 });
556 }
557
558 this.extract = function (entry, outPath, callback) {
559 let entryName = entry || '';
560 if (typeof entry === 'string') {
561 entry = this.entry(entry);
562 if (entry) {
563 entryName = entry.name;
564 } else {
565 if (entryName.length && entryName[entryName.length - 1] !== '/') {
566 entryName += '/';
567 }
568 }
569 }
570 if (!entry || entry.isDirectory) {
571 const files = [],
572 dirs = [],
573 allDirs = {};
574 for (const e in entries) {
575 if (
576 Object.prototype.hasOwnProperty.call(entries, e) &&
577 e.lastIndexOf(entryName, 0) === 0
578 ) {
579 let relPath = e.replace(entryName, '');
580 const childEntry = entries[e];
581 if (childEntry.isFile) {
582 files.push(childEntry);
583 relPath = path.dirname(relPath);
584 }
585 if (relPath && !allDirs[relPath] && relPath !== '.') {
586 allDirs[relPath] = true;
587 let parts = relPath.split('/').filter((f) => {
588 return f;
589 });
590 if (parts.length) {
591 dirs.push(parts);
592 }
593 while (parts.length > 1) {
594 parts = parts.slice(0, parts.length - 1);
595 const partsPath = parts.join('/');
596 if (allDirs[partsPath] || partsPath === '.') {
597 break;
598 }
599 allDirs[partsPath] = true;
600 dirs.push(parts);
601 }
602 }
603 }
604 }
605 dirs.sort((x, y) => {
606 return x.length - y.length;
607 });
608 if (dirs.length) {
609 createDirectories(outPath, dirs, (err) => {
610 if (err) {
611 callback(err);
612 } else {
613 extractFiles(outPath, entryName, files, callback, 0);
614 }
615 });
616 } else {
617 extractFiles(outPath, entryName, files, callback, 0);
618 }
619 } else {
620 fs.stat(outPath, (err, stat) => {
621 if (stat && stat.isDirectory()) {
622 extract(entry, path.join(outPath, path.basename(entry.name)), callback);
623 } else {
624 extract(entry, outPath, callback);
625 }
626 });
627 }
628 };
629
630 this.close = function (callback) {
631 if (closed || !fd) {
632 closed = true;
633 if (callback) {
634 callback();
635 }
636 } else {
637 closed = true;
638 fs.close(fd, (err) => {
639 fd = null;
640 if (callback) {
641 callback(err);
642 }
643 });
644 }
645 };
646
647 const originalEmit = events.EventEmitter.prototype.emit;
648 this.emit = function (...args) {
649 if (!closed) {
650 return originalEmit.call(this, ...args);
651 }
652 };
653};
654
655StreamZip.setFs = function (customFs) {
656 fs = customFs;
657};
658
659StreamZip.debugLog = (...args) => {
660 if (StreamZip.debug) {
661 // eslint-disable-next-line no-console
662 console.log(...args);
663 }
664};
665
666util.inherits(StreamZip, events.EventEmitter);
667
668const propZip = Symbol('zip');
669
670StreamZip.async = class StreamZipAsync extends events.EventEmitter {
671 constructor(config) {
672 super();
673
674 const zip = new StreamZip(config);
675
676 zip.on('entry', (entry) => this.emit('entry', entry));
677 zip.on('extract', (entry, outPath) => this.emit('extract', entry, outPath));
678
679 this[propZip] = new Promise((resolve, reject) => {
680 zip.on('ready', () => {
681 zip.off('error', reject);
682 resolve(zip);
683 });
684 zip.on('error', reject);
685 });
686 }
687
688 get entriesCount() {
689 return this[propZip].then((zip) => zip.entriesCount);
690 }
691
692 get comment() {
693 return this[propZip].then((zip) => zip.comment);
694 }
695
696 async entry(name) {
697 const zip = await this[propZip];
698 return zip.entry(name);
699 }
700
701 async entries() {
702 const zip = await this[propZip];
703 return zip.entries();
704 }
705
706 async stream(entry) {
707 const zip = await this[propZip];
708 return new Promise((resolve, reject) => {
709 zip.stream(entry, (err, stm) => {
710 if (err) {
711 reject(err);
712 } else {
713 resolve(stm);
714 }
715 });
716 });
717 }
718
719 async entryData(entry) {
720 const stm = await this.stream(entry);
721 return new Promise((resolve, reject) => {
722 const data = [];
723 stm.on('data', (chunk) => data.push(chunk));
724 stm.on('end', () => {
725 resolve(Buffer.concat(data));
726 });
727 stm.on('error', (err) => {
728 stm.removeAllListeners('end');
729 reject(err);
730 });
731 });
732 }
733
734 async extract(entry, outPath) {
735 const zip = await this[propZip];
736 return new Promise((resolve, reject) => {
737 zip.extract(entry, outPath, (err, res) => {
738 if (err) {
739 reject(err);
740 } else {
741 resolve(res);
742 }
743 });
744 });
745 }
746
747 async close() {
748 const zip = await this[propZip];
749 return new Promise((resolve, reject) => {
750 zip.close((err) => {
751 if (err) {
752 reject(err);
753 } else {
754 resolve();
755 }
756 });
757 });
758 }
759};
760
761class CentralDirectoryHeader {
762 read(data) {
763 if (data.length !== consts.ENDHDR || data.readUInt32LE(0) !== consts.ENDSIG) {
764 throw new Error('Invalid central directory');
765 }
766 // number of entries on this volume
767 this.volumeEntries = data.readUInt16LE(consts.ENDSUB);
768 // total number of entries
769 this.totalEntries = data.readUInt16LE(consts.ENDTOT);
770 // central directory size in bytes
771 this.size = data.readUInt32LE(consts.ENDSIZ);
772 // offset of first CEN header
773 this.offset = data.readUInt32LE(consts.ENDOFF);
774 // zip file comment length
775 this.commentLength = data.readUInt16LE(consts.ENDCOM);
776 }
777}
778
779class CentralDirectoryLoc64Header {
780 read(data) {
781 if (data.length !== consts.ENDL64HDR || data.readUInt32LE(0) !== consts.ENDL64SIG) {
782 throw new Error('Invalid zip64 central directory locator');
783 }
784 // ZIP64 EOCD header offset
785 this.headerOffset = readUInt64LE(data, consts.ENDSUB);
786 }
787}
788
789class CentralDirectoryZip64Header {
790 read(data) {
791 if (data.length !== consts.END64HDR || data.readUInt32LE(0) !== consts.END64SIG) {
792 throw new Error('Invalid central directory');
793 }
794 // number of entries on this volume
795 this.volumeEntries = readUInt64LE(data, consts.END64SUB);
796 // total number of entries
797 this.totalEntries = readUInt64LE(data, consts.END64TOT);
798 // central directory size in bytes
799 this.size = readUInt64LE(data, consts.END64SIZ);
800 // offset of first CEN header
801 this.offset = readUInt64LE(data, consts.END64OFF);
802 }
803}
804
805class ZipEntry {
806 readHeader(data, offset) {
807 // data should be 46 bytes and start with "PK 01 02"
808 if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) !== consts.CENSIG) {
809 throw new Error('Invalid entry header');
810 }
811 // version made by
812 this.verMade = data.readUInt16LE(offset + consts.CENVEM);
813 // version needed to extract
814 this.version = data.readUInt16LE(offset + consts.CENVER);
815 // encrypt, decrypt flags
816 this.flags = data.readUInt16LE(offset + consts.CENFLG);
817 // compression method
818 this.method = data.readUInt16LE(offset + consts.CENHOW);
819 // modification time (2 bytes time, 2 bytes date)
820 const timebytes = data.readUInt16LE(offset + consts.CENTIM);
821 const datebytes = data.readUInt16LE(offset + consts.CENTIM + 2);
822 this.time = parseZipTime(timebytes, datebytes);
823
824 // uncompressed file crc-32 value
825 this.crc = data.readUInt32LE(offset + consts.CENCRC);
826 // compressed size
827 this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ);
828 // uncompressed size
829 this.size = data.readUInt32LE(offset + consts.CENLEN);
830 // filename length
831 this.fnameLen = data.readUInt16LE(offset + consts.CENNAM);
832 // extra field length
833 this.extraLen = data.readUInt16LE(offset + consts.CENEXT);
834 // file comment length
835 this.comLen = data.readUInt16LE(offset + consts.CENCOM);
836 // volume number start
837 this.diskStart = data.readUInt16LE(offset + consts.CENDSK);
838 // internal file attributes
839 this.inattr = data.readUInt16LE(offset + consts.CENATT);
840 // external file attributes
841 this.attr = data.readUInt32LE(offset + consts.CENATX);
842 // LOC header offset
843 this.offset = data.readUInt32LE(offset + consts.CENOFF);
844 }
845
846 readDataHeader(data) {
847 // 30 bytes and should start with "PK\003\004"
848 if (data.readUInt32LE(0) !== consts.LOCSIG) {
849 throw new Error('Invalid local header');
850 }
851 // version needed to extract
852 this.version = data.readUInt16LE(consts.LOCVER);
853 // general purpose bit flag
854 this.flags = data.readUInt16LE(consts.LOCFLG);
855 // compression method
856 this.method = data.readUInt16LE(consts.LOCHOW);
857 // modification time (2 bytes time ; 2 bytes date)
858 const timebytes = data.readUInt16LE(consts.LOCTIM);
859 const datebytes = data.readUInt16LE(consts.LOCTIM + 2);
860 this.time = parseZipTime(timebytes, datebytes);
861
862 // uncompressed file crc-32 value
863 this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc;
864 // compressed size
865 const compressedSize = data.readUInt32LE(consts.LOCSIZ);
866 if (compressedSize && compressedSize !== consts.EF_ZIP64_OR_32) {
867 this.compressedSize = compressedSize;
868 }
869 // uncompressed size
870 const size = data.readUInt32LE(consts.LOCLEN);
871 if (size && size !== consts.EF_ZIP64_OR_32) {
872 this.size = size;
873 }
874 // filename length
875 this.fnameLen = data.readUInt16LE(consts.LOCNAM);
876 // extra field length
877 this.extraLen = data.readUInt16LE(consts.LOCEXT);
878 }
879
880 read(data, offset) {
881 this.name = data.slice(offset, (offset += this.fnameLen)).toString();
882 const lastChar = data[offset - 1];
883 this.isDirectory = lastChar === 47 || lastChar === 92;
884
885 if (this.extraLen) {
886 this.readExtra(data, offset);
887 offset += this.extraLen;
888 }
889 this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null;
890 }
891
892 validateName() {
893 if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) {
894 throw new Error('Malicious entry: ' + this.name);
895 }
896 }
897
898 readExtra(data, offset) {
899 let signature, size;
900 const maxPos = offset + this.extraLen;
901 while (offset < maxPos) {
902 signature = data.readUInt16LE(offset);
903 offset += 2;
904 size = data.readUInt16LE(offset);
905 offset += 2;
906 if (consts.ID_ZIP64 === signature) {
907 this.parseZip64Extra(data, offset, size);
908 }
909 offset += size;
910 }
911 }
912
913 parseZip64Extra(data, offset, length) {
914 if (length >= 8 && this.size === consts.EF_ZIP64_OR_32) {
915 this.size = readUInt64LE(data, offset);
916 offset += 8;
917 length -= 8;
918 }
919 if (length >= 8 && this.compressedSize === consts.EF_ZIP64_OR_32) {
920 this.compressedSize = readUInt64LE(data, offset);
921 offset += 8;
922 length -= 8;
923 }
924 if (length >= 8 && this.offset === consts.EF_ZIP64_OR_32) {
925 this.offset = readUInt64LE(data, offset);
926 offset += 8;
927 length -= 8;
928 }
929 if (length >= 4 && this.diskStart === consts.EF_ZIP64_OR_16) {
930 this.diskStart = data.readUInt32LE(offset);
931 // offset += 4; length -= 4;
932 }
933 }
934
935 get encrypted() {
936 return (this.flags & consts.FLG_ENTRY_ENC) === consts.FLG_ENTRY_ENC;
937 }
938
939 get isFile() {
940 return !this.isDirectory;
941 }
942}
943
944class FsRead {
945 constructor(fd, buffer, offset, length, position, callback) {
946 this.fd = fd;
947 this.buffer = buffer;
948 this.offset = offset;
949 this.length = length;
950 this.position = position;
951 this.callback = callback;
952 this.bytesRead = 0;
953 this.waiting = false;
954 }
955
956 read(sync) {
957 StreamZip.debugLog('read', this.position, this.bytesRead, this.length, this.offset);
958 this.waiting = true;
959 let err;
960 if (sync) {
961 let bytesRead = 0;
962 try {
963 bytesRead = fs.readSync(
964 this.fd,
965 this.buffer,
966 this.offset + this.bytesRead,
967 this.length - this.bytesRead,
968 this.position + this.bytesRead
969 );
970 } catch (e) {
971 err = e;
972 }
973 this.readCallback(sync, err, err ? bytesRead : null);
974 } else {
975 fs.read(
976 this.fd,
977 this.buffer,
978 this.offset + this.bytesRead,
979 this.length - this.bytesRead,
980 this.position + this.bytesRead,
981 this.readCallback.bind(this, sync)
982 );
983 }
984 }
985
986 readCallback(sync, err, bytesRead) {
987 if (typeof bytesRead === 'number') {
988 this.bytesRead += bytesRead;
989 }
990 if (err || !bytesRead || this.bytesRead === this.length) {
991 this.waiting = false;
992 return this.callback(err, this.bytesRead);
993 } else {
994 this.read(sync);
995 }
996 }
997}
998
999class FileWindowBuffer {
1000 constructor(fd) {
1001 this.position = 0;
1002 this.buffer = Buffer.alloc(0);
1003 this.fd = fd;
1004 this.fsOp = null;
1005 }
1006
1007 checkOp() {
1008 if (this.fsOp && this.fsOp.waiting) {
1009 throw new Error('Operation in progress');
1010 }
1011 }
1012
1013 read(pos, length, callback) {
1014 this.checkOp();
1015 if (this.buffer.length < length) {
1016 this.buffer = Buffer.alloc(length);
1017 }
1018 this.position = pos;
1019 this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read();
1020 }
1021
1022 expandLeft(length, callback) {
1023 this.checkOp();
1024 this.buffer = Buffer.concat([Buffer.alloc(length), this.buffer]);
1025 this.position -= length;
1026 if (this.position < 0) {
1027 this.position = 0;
1028 }
1029 this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read();
1030 }
1031
1032 expandRight(length, callback) {
1033 this.checkOp();
1034 const offset = this.buffer.length;
1035 this.buffer = Buffer.concat([this.buffer, Buffer.alloc(length)]);
1036 this.fsOp = new FsRead(
1037 this.fd,
1038 this.buffer,
1039 offset,
1040 length,
1041 this.position + offset,
1042 callback
1043 ).read();
1044 }
1045
1046 moveRight(length, callback, shift) {
1047 this.checkOp();
1048 if (shift) {
1049 this.buffer.copy(this.buffer, 0, shift);
1050 } else {
1051 shift = 0;
1052 }
1053 this.position += shift;
1054 this.fsOp = new FsRead(
1055 this.fd,
1056 this.buffer,
1057 this.buffer.length - shift,
1058 shift,
1059 this.position + this.buffer.length - shift,
1060 callback
1061 ).read();
1062 }
1063}
1064
1065class EntryDataReaderStream extends stream.Readable {
1066 constructor(fd, offset, length) {
1067 super();
1068 this.fd = fd;
1069 this.offset = offset;
1070 this.length = length;
1071 this.pos = 0;
1072 this.readCallback = this.readCallback.bind(this);
1073 }
1074
1075 _read(n) {
1076 const buffer = Buffer.alloc(Math.min(n, this.length - this.pos));
1077 if (buffer.length) {
1078 fs.read(this.fd, buffer, 0, buffer.length, this.offset + this.pos, this.readCallback);
1079 } else {
1080 this.push(null);
1081 }
1082 }
1083
1084 readCallback(err, bytesRead, buffer) {
1085 this.pos += bytesRead;
1086 if (err) {
1087 this.emit('error', err);
1088 this.push(null);
1089 } else if (!bytesRead) {
1090 this.push(null);
1091 } else {
1092 if (bytesRead !== buffer.length) {
1093 buffer = buffer.slice(0, bytesRead);
1094 }
1095 this.push(buffer);
1096 }
1097 }
1098}
1099
1100class EntryVerifyStream extends stream.Transform {
1101 constructor(baseStm, crc, size) {
1102 super();
1103 this.verify = new CrcVerify(crc, size);
1104 baseStm.on('error', (e) => {
1105 this.emit('error', e);
1106 });
1107 }
1108
1109 _transform(data, encoding, callback) {
1110 let err;
1111 try {
1112 this.verify.data(data);
1113 } catch (e) {
1114 err = e;
1115 }
1116 callback(err, data);
1117 }
1118}
1119
1120class CrcVerify {
1121 constructor(crc, size) {
1122 this.crc = crc;
1123 this.size = size;
1124 this.state = {
1125 crc: ~0,
1126 size: 0,
1127 };
1128 }
1129
1130 data(data) {
1131 const crcTable = CrcVerify.getCrcTable();
1132 let crc = this.state.crc;
1133 let off = 0;
1134 let len = data.length;
1135 while (--len >= 0) {
1136 crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8);
1137 }
1138 this.state.crc = crc;
1139 this.state.size += data.length;
1140 if (this.state.size >= this.size) {
1141 const buf = Buffer.alloc(4);
1142 buf.writeInt32LE(~this.state.crc & 0xffffffff, 0);
1143 crc = buf.readUInt32LE(0);
1144 if (crc !== this.crc) {
1145 throw new Error('Invalid CRC');
1146 }
1147 if (this.state.size !== this.size) {
1148 throw new Error('Invalid size');
1149 }
1150 }
1151 }
1152
1153 static getCrcTable() {
1154 let crcTable = CrcVerify.crcTable;
1155 if (!crcTable) {
1156 CrcVerify.crcTable = crcTable = [];
1157 const b = Buffer.alloc(4);
1158 for (let n = 0; n < 256; n++) {
1159 let c = n;
1160 for (let k = 8; --k >= 0; ) {
1161 if ((c & 1) !== 0) {
1162 c = 0xedb88320 ^ (c >>> 1);
1163 } else {
1164 c = c >>> 1;
1165 }
1166 }
1167 if (c < 0) {
1168 b.writeInt32LE(c, 0);
1169 c = b.readUInt32LE(0);
1170 }
1171 crcTable[n] = c;
1172 }
1173 }
1174 return crcTable;
1175 }
1176}
1177
1178function parseZipTime(timebytes, datebytes) {
1179 const timebits = toBits(timebytes, 16);
1180 const datebits = toBits(datebytes, 16);
1181
1182 const mt = {
1183 h: parseInt(timebits.slice(0, 5).join(''), 2),
1184 m: parseInt(timebits.slice(5, 11).join(''), 2),
1185 s: parseInt(timebits.slice(11, 16).join(''), 2) * 2,
1186 Y: parseInt(datebits.slice(0, 7).join(''), 2) + 1980,
1187 M: parseInt(datebits.slice(7, 11).join(''), 2),
1188 D: parseInt(datebits.slice(11, 16).join(''), 2),
1189 };
1190 const dt_str = [mt.Y, mt.M, mt.D].join('-') + ' ' + [mt.h, mt.m, mt.s].join(':') + ' GMT+0';
1191 return new Date(dt_str).getTime();
1192}
1193
1194function toBits(dec, size) {
1195 let b = (dec >>> 0).toString(2);
1196 while (b.length < size) {
1197 b = '0' + b;
1198 }
1199 return b.split('');
1200}
1201
1202function readUInt64LE(buffer, offset) {
1203 return buffer.readUInt32LE(offset + 4) * 0x0000000100000000 + buffer.readUInt32LE(offset);
1204}
1205
1206module.exports = StreamZip;