You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					232 lines
				
				8.2 KiB
			
		
		
			
		
	
	
					232 lines
				
				8.2 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								var binary = require('binary');
							 | 
						||
| 
								 | 
							
								var PullStream = require('../PullStream');
							 | 
						||
| 
								 | 
							
								var unzip = require('./unzip');
							 | 
						||
| 
								 | 
							
								var Promise = require('bluebird');
							 | 
						||
| 
								 | 
							
								var BufferStream = require('../BufferStream');
							 | 
						||
| 
								 | 
							
								var parseExtraField = require('../parseExtraField');
							 | 
						||
| 
								 | 
							
								var Buffer = require('../Buffer');
							 | 
						||
| 
								 | 
							
								var path = require('path');
							 | 
						||
| 
								 | 
							
								var Writer = require('fstream').Writer;
							 | 
						||
| 
								 | 
							
								var parseDateTime = require('../parseDateTime');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								var signature = Buffer.alloc(4);
							 | 
						||
| 
								 | 
							
								signature.writeUInt32LE(0x06054b50,0);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function getCrxHeader(source) {
							 | 
						||
| 
								 | 
							
								  var sourceStream = source.stream(0).pipe(PullStream());
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return sourceStream.pull(4).then(function(data) {
							 | 
						||
| 
								 | 
							
								    var signature = data.readUInt32LE(0);
							 | 
						||
| 
								 | 
							
								    if (signature === 0x34327243) {
							 | 
						||
| 
								 | 
							
								      var crxHeader;
							 | 
						||
| 
								 | 
							
								      return sourceStream.pull(12).then(function(data) {
							 | 
						||
| 
								 | 
							
								        crxHeader = binary.parse(data)
							 | 
						||
| 
								 | 
							
								          .word32lu('version')
							 | 
						||
| 
								 | 
							
								          .word32lu('pubKeyLength')
							 | 
						||
| 
								 | 
							
								          .word32lu('signatureLength')
							 | 
						||
| 
								 | 
							
								          .vars;
							 | 
						||
| 
								 | 
							
								      }).then(function() {
							 | 
						||
| 
								 | 
							
								        return sourceStream.pull(crxHeader.pubKeyLength +crxHeader.signatureLength);
							 | 
						||
| 
								 | 
							
								      }).then(function(data) {
							 | 
						||
| 
								 | 
							
								        crxHeader.publicKey = data.slice(0,crxHeader.pubKeyLength);
							 | 
						||
| 
								 | 
							
								        crxHeader.signature = data.slice(crxHeader.pubKeyLength);
							 | 
						||
| 
								 | 
							
								        crxHeader.size = 16 + crxHeader.pubKeyLength +crxHeader.signatureLength;
							 | 
						||
| 
								 | 
							
								        return crxHeader;
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
							 | 
						||
| 
								 | 
							
								function getZip64CentralDirectory(source, zip64CDL) {
							 | 
						||
| 
								 | 
							
								  var d64loc = binary.parse(zip64CDL)
							 | 
						||
| 
								 | 
							
								    .word32lu('signature')
							 | 
						||
| 
								 | 
							
								    .word32lu('diskNumber')
							 | 
						||
| 
								 | 
							
								    .word64lu('offsetToStartOfCentralDirectory')
							 | 
						||
| 
								 | 
							
								    .word32lu('numberOfDisks')
							 | 
						||
| 
								 | 
							
								    .vars;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (d64loc.signature != 0x07064b50) {
							 | 
						||
| 
								 | 
							
								    throw new Error('invalid zip64 end of central dir locator signature (0x07064b50): 0x' + d64loc.signature.toString(16));
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  var dir64 = PullStream();
							 | 
						||
| 
								 | 
							
								  source.stream(d64loc.offsetToStartOfCentralDirectory).pipe(dir64);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return dir64.pull(56)
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
							 | 
						||
| 
								 | 
							
								function parseZip64DirRecord (dir64record) {
							 | 
						||
| 
								 | 
							
								  var vars = binary.parse(dir64record)
							 | 
						||
| 
								 | 
							
								    .word32lu('signature')
							 | 
						||
| 
								 | 
							
								    .word64lu('sizeOfCentralDirectory')
							 | 
						||
| 
								 | 
							
								    .word16lu('version')
							 | 
						||
| 
								 | 
							
								    .word16lu('versionsNeededToExtract')
							 | 
						||
| 
								 | 
							
								    .word32lu('diskNumber')
							 | 
						||
| 
								 | 
							
								    .word32lu('diskStart')
							 | 
						||
| 
								 | 
							
								    .word64lu('numberOfRecordsOnDisk')
							 | 
						||
| 
								 | 
							
								    .word64lu('numberOfRecords')
							 | 
						||
| 
								 | 
							
								    .word64lu('sizeOfCentralDirectory')
							 | 
						||
| 
								 | 
							
								    .word64lu('offsetToStartOfCentralDirectory')
							 | 
						||
| 
								 | 
							
								    .vars;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (vars.signature != 0x06064b50) {
							 | 
						||
| 
								 | 
							
								    throw new Error('invalid zip64 end of central dir locator signature (0x06064b50): 0x0' + vars.signature.toString(16));
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return vars
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = function centralDirectory(source, options) {
							 | 
						||
| 
								 | 
							
								  var endDir = PullStream(),
							 | 
						||
| 
								 | 
							
								      records = PullStream(),
							 | 
						||
| 
								 | 
							
								      tailSize = (options && options.tailSize) || 80,
							 | 
						||
| 
								 | 
							
								      sourceSize,
							 | 
						||
| 
								 | 
							
								      crxHeader,
							 | 
						||
| 
								 | 
							
								      startOffset,
							 | 
						||
| 
								 | 
							
								      vars;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (options && options.crx)
							 | 
						||
| 
								 | 
							
								    crxHeader = getCrxHeader(source);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return source.size()
							 | 
						||
| 
								 | 
							
								    .then(function(size) {
							 | 
						||
| 
								 | 
							
								      sourceSize = size;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      source.stream(Math.max(0,size-tailSize))
							 | 
						||
| 
								 | 
							
								        .on('error', function (error) { endDir.emit('error', error) })
							 | 
						||
| 
								 | 
							
								        .pipe(endDir);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      return endDir.pull(signature);
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								    .then(function() {
							 | 
						||
| 
								 | 
							
								      return Promise.props({directory: endDir.pull(22), crxHeader: crxHeader});
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								    .then(function(d) {
							 | 
						||
| 
								 | 
							
								      var data = d.directory;
							 | 
						||
| 
								 | 
							
								      startOffset = d.crxHeader && d.crxHeader.size || 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      vars = binary.parse(data)
							 | 
						||
| 
								 | 
							
								        .word32lu('signature')
							 | 
						||
| 
								 | 
							
								        .word16lu('diskNumber')
							 | 
						||
| 
								 | 
							
								        .word16lu('diskStart')
							 | 
						||
| 
								 | 
							
								        .word16lu('numberOfRecordsOnDisk')
							 | 
						||
| 
								 | 
							
								        .word16lu('numberOfRecords')
							 | 
						||
| 
								 | 
							
								        .word32lu('sizeOfCentralDirectory')
							 | 
						||
| 
								 | 
							
								        .word32lu('offsetToStartOfCentralDirectory')
							 | 
						||
| 
								 | 
							
								        .word16lu('commentLength')
							 | 
						||
| 
								 | 
							
								        .vars;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // Is this zip file using zip64 format? Use same check as Go:
							 | 
						||
| 
								 | 
							
								      // https://github.com/golang/go/blob/master/src/archive/zip/reader.go#L503
							 | 
						||
| 
								 | 
							
								      // For zip64 files, need to find zip64 central directory locator header to extract
							 | 
						||
| 
								 | 
							
								      // relative offset for zip64 central directory record.
							 | 
						||
| 
								 | 
							
								      if (vars.numberOfRecords == 0xffff|| vars.numberOfRecords == 0xffff ||
							 | 
						||
| 
								 | 
							
								        vars.offsetToStartOfCentralDirectory == 0xffffffff) {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // Offset to zip64 CDL is 20 bytes before normal CDR
							 | 
						||
| 
								 | 
							
								        const zip64CDLSize = 20
							 | 
						||
| 
								 | 
							
								        const zip64CDLOffset = sourceSize - (tailSize - endDir.match + zip64CDLSize)
							 | 
						||
| 
								 | 
							
								        const zip64CDLStream = PullStream();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        source.stream(zip64CDLOffset).pipe(zip64CDLStream);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return zip64CDLStream.pull(zip64CDLSize)
							 | 
						||
| 
								 | 
							
								          .then(function (d) { return getZip64CentralDirectory(source, d) })
							 | 
						||
| 
								 | 
							
								          .then(function (dir64record) {
							 | 
						||
| 
								 | 
							
								            vars = parseZip64DirRecord(dir64record)
							 | 
						||
| 
								 | 
							
								          })
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        vars.offsetToStartOfCentralDirectory += startOffset;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								    .then(function() {
							 | 
						||
| 
								 | 
							
								      if (vars.commentLength) return endDir.pull(vars.commentLength).then(function(comment) {
							 | 
						||
| 
								 | 
							
								        vars.comment = comment.toString('utf8');
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								    .then(function() {
							 | 
						||
| 
								 | 
							
								      source.stream(vars.offsetToStartOfCentralDirectory).pipe(records);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      vars.extract = function(opts) {
							 | 
						||
| 
								 | 
							
								        if (!opts || !opts.path) throw new Error('PATH_MISSING');
							 | 
						||
| 
								 | 
							
								        // make sure path is normalized before using it
							 | 
						||
| 
								 | 
							
								        opts.path = path.resolve(path.normalize(opts.path));
							 | 
						||
| 
								 | 
							
								        return vars.files.then(function(files) {
							 | 
						||
| 
								 | 
							
								          return Promise.map(files, function(entry) {
							 | 
						||
| 
								 | 
							
								            if (entry.type == 'Directory') return;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            // to avoid zip slip (writing outside of the destination), we resolve
							 | 
						||
| 
								 | 
							
								            // the target path, and make sure it's nested in the intended
							 | 
						||
| 
								 | 
							
								            // destination, or not extract it otherwise.
							 | 
						||
| 
								 | 
							
								            var extractPath = path.join(opts.path, entry.path);
							 | 
						||
| 
								 | 
							
								            if (extractPath.indexOf(opts.path) != 0) {
							 | 
						||
| 
								 | 
							
								              return;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            var writer = opts.getWriter ? opts.getWriter({path: extractPath}) :  Writer({ path: extractPath });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            return new Promise(function(resolve, reject) {
							 | 
						||
| 
								 | 
							
								              entry.stream(opts.password)
							 | 
						||
| 
								 | 
							
								                .on('error',reject)
							 | 
						||
| 
								 | 
							
								                .pipe(writer)
							 | 
						||
| 
								 | 
							
								                .on('close',resolve)
							 | 
						||
| 
								 | 
							
								                .on('error',reject);
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								          }, { concurrency: opts.concurrency > 1 ? opts.concurrency : 1 });
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      vars.files = Promise.mapSeries(Array(vars.numberOfRecords),function() {
							 | 
						||
| 
								 | 
							
								        return records.pull(46).then(function(data) {    
							 | 
						||
| 
								 | 
							
								          var vars = binary.parse(data)
							 | 
						||
| 
								 | 
							
								            .word32lu('signature')
							 | 
						||
| 
								 | 
							
								            .word16lu('versionMadeBy')
							 | 
						||
| 
								 | 
							
								            .word16lu('versionsNeededToExtract')
							 | 
						||
| 
								 | 
							
								            .word16lu('flags')
							 | 
						||
| 
								 | 
							
								            .word16lu('compressionMethod')
							 | 
						||
| 
								 | 
							
								            .word16lu('lastModifiedTime')
							 | 
						||
| 
								 | 
							
								            .word16lu('lastModifiedDate')
							 | 
						||
| 
								 | 
							
								            .word32lu('crc32')
							 | 
						||
| 
								 | 
							
								            .word32lu('compressedSize')
							 | 
						||
| 
								 | 
							
								            .word32lu('uncompressedSize')
							 | 
						||
| 
								 | 
							
								            .word16lu('fileNameLength')
							 | 
						||
| 
								 | 
							
								            .word16lu('extraFieldLength')
							 | 
						||
| 
								 | 
							
								            .word16lu('fileCommentLength')
							 | 
						||
| 
								 | 
							
								            .word16lu('diskNumber')
							 | 
						||
| 
								 | 
							
								            .word16lu('internalFileAttributes')
							 | 
						||
| 
								 | 
							
								            .word32lu('externalFileAttributes')
							 | 
						||
| 
								 | 
							
								            .word32lu('offsetToLocalFileHeader')
							 | 
						||
| 
								 | 
							
								            .vars;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        vars.offsetToLocalFileHeader += startOffset;
							 | 
						||
| 
								 | 
							
								        vars.lastModifiedDateTime = parseDateTime(vars.lastModifiedDate, vars.lastModifiedTime);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return records.pull(vars.fileNameLength).then(function(fileNameBuffer) {
							 | 
						||
| 
								 | 
							
								          vars.pathBuffer = fileNameBuffer;
							 | 
						||
| 
								 | 
							
								          vars.path = fileNameBuffer.toString('utf8');
							 | 
						||
| 
								 | 
							
								          vars.isUnicode = (vars.flags & 0x800) != 0;
							 | 
						||
| 
								 | 
							
								          return records.pull(vars.extraFieldLength);
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								        .then(function(extraField) {
							 | 
						||
| 
								 | 
							
								          vars.extra = parseExtraField(extraField, vars);
							 | 
						||
| 
								 | 
							
								          return records.pull(vars.fileCommentLength);
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								        .then(function(comment) {
							 | 
						||
| 
								 | 
							
								          vars.comment = comment;
							 | 
						||
| 
								 | 
							
								          vars.type = (vars.uncompressedSize === 0 && /[\/\\]$/.test(vars.path)) ? 'Directory' : 'File';
							 | 
						||
| 
								 | 
							
								          vars.stream = function(_password) {
							 | 
						||
| 
								 | 
							
								            return unzip(source, vars.offsetToLocalFileHeader,_password, vars);
							 | 
						||
| 
								 | 
							
								          };
							 | 
						||
| 
								 | 
							
								          vars.buffer = function(_password) {
							 | 
						||
| 
								 | 
							
								            return BufferStream(vars.stream(_password));
							 | 
						||
| 
								 | 
							
								          };
							 | 
						||
| 
								 | 
							
								          return vars;
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return Promise.props(vars);
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								};
							 |