UNPKG

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