UNPKG

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