/** | |
* node-compress-commons | |
* | |
* Copyright (c) 2014 Chris Talkington, contributors. | |
* Licensed under the MIT license. | |
* https://github.com/archiverjs/node-compress-commons/blob/master/LICENSE-MIT | |
*/ | |
var inherits = require('util').inherits; | |
var crc32 = require('buffer-crc32'); | |
var CRC32Stream = require('crc32-stream'); | |
var DeflateCRC32Stream = CRC32Stream.DeflateCRC32Stream; | |
var ArchiveOutputStream = require('../archive-output-stream'); | |
var ZipArchiveEntry = require('./zip-archive-entry'); | |
var GeneralPurposeBit = require('./general-purpose-bit'); | |
var constants = require('./constants'); | |
var util = require('../../util'); | |
var zipUtil = require('./util'); | |
var ZipArchiveOutputStream = module.exports = function(options) { | |
if (!(this instanceof ZipArchiveOutputStream)) { | |
return new ZipArchiveOutputStream(options); | |
} | |
options = this.options = this._defaults(options); | |
ArchiveOutputStream.call(this, options); | |
this._entry = null; | |
this._entries = []; | |
this._archive = { | |
centralLength: 0, | |
centralOffset: 0, | |
comment: '', | |
finish: false, | |
finished: false, | |
processing: false, | |
forceZip64: options.forceZip64, | |
forceLocalTime: options.forceLocalTime | |
}; | |
}; | |
inherits(ZipArchiveOutputStream, ArchiveOutputStream); | |
ZipArchiveOutputStream.prototype._afterAppend = function(ae) { | |
this._entries.push(ae); | |
if (ae.getGeneralPurposeBit().usesDataDescriptor()) { | |
this._writeDataDescriptor(ae); | |
} | |
this._archive.processing = false; | |
this._entry = null; | |
if (this._archive.finish && !this._archive.finished) { | |
this._finish(); | |
} | |
}; | |
ZipArchiveOutputStream.prototype._appendBuffer = function(ae, source, callback) { | |
if (source.length === 0) { | |
ae.setMethod(constants.METHOD_STORED); | |
} | |
var method = ae.getMethod(); | |
if (method === constants.METHOD_STORED) { | |
ae.setSize(source.length); | |
ae.setCompressedSize(source.length); | |
ae.setCrc(crc32.unsigned(source)); | |
} | |
this._writeLocalFileHeader(ae); | |
if (method === constants.METHOD_STORED) { | |
this.write(source); | |
this._afterAppend(ae); | |
callback(null, ae); | |
return; | |
} else if (method === constants.METHOD_DEFLATED) { | |
this._smartStream(ae, callback).end(source); | |
return; | |
} else { | |
callback(new Error('compression method ' + method + ' not implemented')); | |
return; | |
} | |
}; | |
ZipArchiveOutputStream.prototype._appendStream = function(ae, source, callback) { | |
ae.getGeneralPurposeBit().useDataDescriptor(true); | |
ae.setVersionNeededToExtract(constants.MIN_VERSION_DATA_DESCRIPTOR); | |
this._writeLocalFileHeader(ae); | |
var smart = this._smartStream(ae, callback); | |
source.once('error', function(err) { | |
smart.emit('error', err); | |
smart.end(); | |
}) | |
source.pipe(smart); | |
}; | |
ZipArchiveOutputStream.prototype._defaults = function(o) { | |
if (typeof o !== 'object') { | |
o = {}; | |
} | |
if (typeof o.zlib !== 'object') { | |
o.zlib = {}; | |
} | |
if (typeof o.zlib.level !== 'number') { | |
o.zlib.level = constants.ZLIB_BEST_SPEED; | |
} | |
o.forceZip64 = !!o.forceZip64; | |
o.forceLocalTime = !!o.forceLocalTime; | |
return o; | |
}; | |
ZipArchiveOutputStream.prototype._finish = function() { | |
this._archive.centralOffset = this.offset; | |
this._entries.forEach(function(ae) { | |
this._writeCentralFileHeader(ae); | |
}.bind(this)); | |
this._archive.centralLength = this.offset - this._archive.centralOffset; | |
if (this.isZip64()) { | |
this._writeCentralDirectoryZip64(); | |
} | |
this._writeCentralDirectoryEnd(); | |
this._archive.processing = false; | |
this._archive.finish = true; | |
this._archive.finished = true; | |
this.end(); | |
}; | |
ZipArchiveOutputStream.prototype._normalizeEntry = function(ae) { | |
if (ae.getMethod() === -1) { | |
ae.setMethod(constants.METHOD_DEFLATED); | |
} | |
if (ae.getMethod() === constants.METHOD_DEFLATED) { | |
ae.getGeneralPurposeBit().useDataDescriptor(true); | |
ae.setVersionNeededToExtract(constants.MIN_VERSION_DATA_DESCRIPTOR); | |
} | |
if (ae.getTime() === -1) { | |
ae.setTime(new Date(), this._archive.forceLocalTime); | |
} | |
ae._offsets = { | |
file: 0, | |
data: 0, | |
contents: 0, | |
}; | |
}; | |
ZipArchiveOutputStream.prototype._smartStream = function(ae, callback) { | |
var deflate = ae.getMethod() === constants.METHOD_DEFLATED; | |
var process = deflate ? new DeflateCRC32Stream(this.options.zlib) : new CRC32Stream(); | |
var error = null; | |
function handleStuff() { | |
var digest = process.digest().readUInt32BE(0); | |
ae.setCrc(digest); | |
ae.setSize(process.size()); | |
ae.setCompressedSize(process.size(true)); | |
this._afterAppend(ae); | |
callback(error, ae); | |
} | |
process.once('end', handleStuff.bind(this)); | |
process.once('error', function(err) { | |
error = err; | |
}); | |
process.pipe(this, { end: false }); | |
return process; | |
}; | |
ZipArchiveOutputStream.prototype._writeCentralDirectoryEnd = function() { | |
var records = this._entries.length; | |
var size = this._archive.centralLength; | |
var offset = this._archive.centralOffset; | |
if (this.isZip64()) { | |
records = constants.ZIP64_MAGIC_SHORT; | |
size = constants.ZIP64_MAGIC; | |
offset = constants.ZIP64_MAGIC; | |
} | |
// signature | |
this.write(zipUtil.getLongBytes(constants.SIG_EOCD)); | |
// disk numbers | |
this.write(constants.SHORT_ZERO); | |
this.write(constants.SHORT_ZERO); | |
// number of entries | |
this.write(zipUtil.getShortBytes(records)); | |
this.write(zipUtil.getShortBytes(records)); | |
// length and location of CD | |
this.write(zipUtil.getLongBytes(size)); | |
this.write(zipUtil.getLongBytes(offset)); | |
// archive comment | |
var comment = this.getComment(); | |
var commentLength = Buffer.byteLength(comment); | |
this.write(zipUtil.getShortBytes(commentLength)); | |
this.write(comment); | |
}; | |
ZipArchiveOutputStream.prototype._writeCentralDirectoryZip64 = function() { | |
// signature | |
this.write(zipUtil.getLongBytes(constants.SIG_ZIP64_EOCD)); | |
// size of the ZIP64 EOCD record | |
this.write(zipUtil.getEightBytes(44)); | |
// version made by | |
this.write(zipUtil.getShortBytes(constants.MIN_VERSION_ZIP64)); | |
// version to extract | |
this.write(zipUtil.getShortBytes(constants.MIN_VERSION_ZIP64)); | |
// disk numbers | |
this.write(constants.LONG_ZERO); | |
this.write(constants.LONG_ZERO); | |
// number of entries | |
this.write(zipUtil.getEightBytes(this._entries.length)); | |
this.write(zipUtil.getEightBytes(this._entries.length)); | |
// length and location of CD | |
this.write(zipUtil.getEightBytes(this._archive.centralLength)); | |
this.write(zipUtil.getEightBytes(this._archive.centralOffset)); | |
// extensible data sector | |
// not implemented at this time | |
// end of central directory locator | |
this.write(zipUtil.getLongBytes(constants.SIG_ZIP64_EOCD_LOC)); | |
// disk number holding the ZIP64 EOCD record | |
this.write(constants.LONG_ZERO); | |
// relative offset of the ZIP64 EOCD record | |
this.write(zipUtil.getEightBytes(this._archive.centralOffset + this._archive.centralLength)); | |
// total number of disks | |
this.write(zipUtil.getLongBytes(1)); | |
}; | |
ZipArchiveOutputStream.prototype._writeCentralFileHeader = function(ae) { | |
var gpb = ae.getGeneralPurposeBit(); | |
var method = ae.getMethod(); | |
var offsets = ae._offsets; | |
var size = ae.getSize(); | |
var compressedSize = ae.getCompressedSize(); | |
if (ae.isZip64() || offsets.file > constants.ZIP64_MAGIC) { | |
size = constants.ZIP64_MAGIC; | |
compressedSize = constants.ZIP64_MAGIC; | |
ae.setVersionNeededToExtract(constants.MIN_VERSION_ZIP64); | |
var extraBuf = Buffer.concat([ | |
zipUtil.getShortBytes(constants.ZIP64_EXTRA_ID), | |
zipUtil.getShortBytes(24), | |
zipUtil.getEightBytes(ae.getSize()), | |
zipUtil.getEightBytes(ae.getCompressedSize()), | |
zipUtil.getEightBytes(offsets.file) | |
], 28); | |
ae.setExtra(extraBuf); | |
} | |
// signature | |
this.write(zipUtil.getLongBytes(constants.SIG_CFH)); | |
// version made by | |
this.write(zipUtil.getShortBytes((ae.getPlatform() << 8) | constants.VERSION_MADEBY)); | |
// version to extract and general bit flag | |
this.write(zipUtil.getShortBytes(ae.getVersionNeededToExtract())); | |
this.write(gpb.encode()); | |
// compression method | |
this.write(zipUtil.getShortBytes(method)); | |
// datetime | |
this.write(zipUtil.getLongBytes(ae.getTimeDos())); | |
// crc32 checksum | |
this.write(zipUtil.getLongBytes(ae.getCrc())); | |
// sizes | |
this.write(zipUtil.getLongBytes(compressedSize)); | |
this.write(zipUtil.getLongBytes(size)); | |
var name = ae.getName(); | |
var comment = ae.getComment(); | |
var extra = ae.getCentralDirectoryExtra(); | |
if (gpb.usesUTF8ForNames()) { | |
name = new Buffer(name); | |
comment = new Buffer(comment); | |
} | |
// name length | |
this.write(zipUtil.getShortBytes(name.length)); | |
// extra length | |
this.write(zipUtil.getShortBytes(extra.length)); | |
// comments length | |
this.write(zipUtil.getShortBytes(comment.length)); | |
// disk number start | |
this.write(constants.SHORT_ZERO); | |
// internal attributes | |
this.write(zipUtil.getShortBytes(ae.getInternalAttributes())); | |
// external attributes | |
this.write(zipUtil.getLongBytes(ae.getExternalAttributes())); | |
// relative offset of LFH | |
if (offsets.file > constants.ZIP64_MAGIC) { | |
this.write(zipUtil.getLongBytes(constants.ZIP64_MAGIC)); | |
} else { | |
this.write(zipUtil.getLongBytes(offsets.file)); | |
} | |
// name | |
this.write(name); | |
// extra | |
this.write(extra); | |
// comment | |
this.write(comment); | |
}; | |
ZipArchiveOutputStream.prototype._writeDataDescriptor = function(ae) { | |
// signature | |
this.write(zipUtil.getLongBytes(constants.SIG_DD)); | |
// crc32 checksum | |
this.write(zipUtil.getLongBytes(ae.getCrc())); | |
// sizes | |
if (ae.isZip64()) { | |
this.write(zipUtil.getEightBytes(ae.getCompressedSize())); | |
this.write(zipUtil.getEightBytes(ae.getSize())); | |
} else { | |
this.write(zipUtil.getLongBytes(ae.getCompressedSize())); | |
this.write(zipUtil.getLongBytes(ae.getSize())); | |
} | |
}; | |
ZipArchiveOutputStream.prototype._writeLocalFileHeader = function(ae) { | |
var gpb = ae.getGeneralPurposeBit(); | |
var method = ae.getMethod(); | |
var name = ae.getName(); | |
var extra = ae.getLocalFileDataExtra(); | |
if (ae.isZip64()) { | |
gpb.useDataDescriptor(true); | |
ae.setVersionNeededToExtract(constants.MIN_VERSION_ZIP64); | |
} | |
if (gpb.usesUTF8ForNames()) { | |
name = new Buffer(name); | |
} | |
ae._offsets.file = this.offset; | |
// signature | |
this.write(zipUtil.getLongBytes(constants.SIG_LFH)); | |
// version to extract and general bit flag | |
this.write(zipUtil.getShortBytes(ae.getVersionNeededToExtract())); | |
this.write(gpb.encode()); | |
// compression method | |
this.write(zipUtil.getShortBytes(method)); | |
// datetime | |
this.write(zipUtil.getLongBytes(ae.getTimeDos())); | |
ae._offsets.data = this.offset; | |
// crc32 checksum and sizes | |
if (gpb.usesDataDescriptor()) { | |
this.write(constants.LONG_ZERO); | |
this.write(constants.LONG_ZERO); | |
this.write(constants.LONG_ZERO); | |
} else { | |
this.write(zipUtil.getLongBytes(ae.getCrc())); | |
this.write(zipUtil.getLongBytes(ae.getCompressedSize())); | |
this.write(zipUtil.getLongBytes(ae.getSize())); | |
} | |
// name length | |
this.write(zipUtil.getShortBytes(name.length)); | |
// extra length | |
this.write(zipUtil.getShortBytes(extra.length)); | |
// name | |
this.write(name); | |
// extra | |
this.write(extra); | |
ae._offsets.contents = this.offset; | |
}; | |
ZipArchiveOutputStream.prototype.getComment = function(comment) { | |
return this._archive.comment !== null ? this._archive.comment : ''; | |
}; | |
ZipArchiveOutputStream.prototype.isZip64 = function() { | |
return this._archive.forceZip64 || this._entries.length > constants.ZIP64_MAGIC_SHORT || this._archive.centralLength > constants.ZIP64_MAGIC || this._archive.centralOffset > constants.ZIP64_MAGIC; | |
}; | |
ZipArchiveOutputStream.prototype.setComment = function(comment) { | |
this._archive.comment = comment; | |
}; |