698 lines
28 KiB
JavaScript
698 lines
28 KiB
JavaScript
|
var fs = require("fs");
|
||
|
var Transform = require("stream").Transform;
|
||
|
var PassThrough = require("stream").PassThrough;
|
||
|
var zlib = require("zlib");
|
||
|
var util = require("util");
|
||
|
var EventEmitter = require("events").EventEmitter;
|
||
|
var crc32 = require("buffer-crc32");
|
||
|
|
||
|
exports.ZipFile = ZipFile;
|
||
|
exports.dateToDosDateTime = dateToDosDateTime;
|
||
|
|
||
|
util.inherits(ZipFile, EventEmitter);
|
||
|
function ZipFile() {
|
||
|
this.outputStream = new PassThrough();
|
||
|
this.entries = [];
|
||
|
this.outputStreamCursor = 0;
|
||
|
this.ended = false; // .end() sets this
|
||
|
this.allDone = false; // set when we've written the last bytes
|
||
|
this.forceZip64Eocd = false; // configurable in .end()
|
||
|
}
|
||
|
|
||
|
ZipFile.prototype.addFile = function(realPath, metadataPath, options) {
|
||
|
var self = this;
|
||
|
metadataPath = validateMetadataPath(metadataPath, false);
|
||
|
if (options == null) options = {};
|
||
|
|
||
|
var entry = new Entry(metadataPath, false, options);
|
||
|
self.entries.push(entry);
|
||
|
fs.stat(realPath, function(err, stats) {
|
||
|
if (err) return self.emit("error", err);
|
||
|
if (!stats.isFile()) return self.emit("error", new Error("not a file: " + realPath));
|
||
|
entry.uncompressedSize = stats.size;
|
||
|
if (options.mtime == null) entry.setLastModDate(stats.mtime);
|
||
|
if (options.mode == null) entry.setFileAttributesMode(stats.mode);
|
||
|
entry.setFileDataPumpFunction(function() {
|
||
|
var readStream = fs.createReadStream(realPath);
|
||
|
entry.state = Entry.FILE_DATA_IN_PROGRESS;
|
||
|
readStream.on("error", function(err) {
|
||
|
self.emit("error", err);
|
||
|
});
|
||
|
pumpFileDataReadStream(self, entry, readStream);
|
||
|
});
|
||
|
pumpEntries(self);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
ZipFile.prototype.addReadStream = function(readStream, metadataPath, options) {
|
||
|
var self = this;
|
||
|
metadataPath = validateMetadataPath(metadataPath, false);
|
||
|
if (options == null) options = {};
|
||
|
var entry = new Entry(metadataPath, false, options);
|
||
|
self.entries.push(entry);
|
||
|
entry.setFileDataPumpFunction(function() {
|
||
|
entry.state = Entry.FILE_DATA_IN_PROGRESS;
|
||
|
pumpFileDataReadStream(self, entry, readStream);
|
||
|
});
|
||
|
pumpEntries(self);
|
||
|
};
|
||
|
|
||
|
ZipFile.prototype.addBuffer = function(buffer, metadataPath, options) {
|
||
|
var self = this;
|
||
|
metadataPath = validateMetadataPath(metadataPath, false);
|
||
|
if (buffer.length > 0x3fffffff) throw new Error("buffer too large: " + buffer.length + " > " + 0x3fffffff);
|
||
|
if (options == null) options = {};
|
||
|
if (options.size != null) throw new Error("options.size not allowed");
|
||
|
var entry = new Entry(metadataPath, false, options);
|
||
|
entry.uncompressedSize = buffer.length;
|
||
|
entry.crc32 = crc32.unsigned(buffer);
|
||
|
entry.crcAndFileSizeKnown = true;
|
||
|
self.entries.push(entry);
|
||
|
if (!entry.compress) {
|
||
|
setCompressedBuffer(buffer);
|
||
|
} else {
|
||
|
zlib.deflateRaw(buffer, function(err, compressedBuffer) {
|
||
|
setCompressedBuffer(compressedBuffer);
|
||
|
});
|
||
|
}
|
||
|
function setCompressedBuffer(compressedBuffer) {
|
||
|
entry.compressedSize = compressedBuffer.length;
|
||
|
entry.setFileDataPumpFunction(function() {
|
||
|
writeToOutputStream(self, compressedBuffer);
|
||
|
writeToOutputStream(self, entry.getDataDescriptor());
|
||
|
entry.state = Entry.FILE_DATA_DONE;
|
||
|
|
||
|
// don't call pumpEntries() recursively.
|
||
|
// (also, don't call process.nextTick recursively.)
|
||
|
setImmediate(function() {
|
||
|
pumpEntries(self);
|
||
|
});
|
||
|
});
|
||
|
pumpEntries(self);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ZipFile.prototype.addEmptyDirectory = function(metadataPath, options) {
|
||
|
var self = this;
|
||
|
metadataPath = validateMetadataPath(metadataPath, true);
|
||
|
if (options == null) options = {};
|
||
|
if (options.size != null) throw new Error("options.size not allowed");
|
||
|
if (options.compress != null) throw new Error("options.compress not allowed");
|
||
|
var entry = new Entry(metadataPath, true, options);
|
||
|
self.entries.push(entry);
|
||
|
entry.setFileDataPumpFunction(function() {
|
||
|
writeToOutputStream(self, entry.getDataDescriptor());
|
||
|
entry.state = Entry.FILE_DATA_DONE;
|
||
|
pumpEntries(self);
|
||
|
});
|
||
|
pumpEntries(self);
|
||
|
};
|
||
|
|
||
|
var eocdrSignatureBuffer = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
||
|
|
||
|
ZipFile.prototype.end = function(options, finalSizeCallback) {
|
||
|
if (typeof options === "function") {
|
||
|
finalSizeCallback = options;
|
||
|
options = null;
|
||
|
}
|
||
|
if (options == null) options = {};
|
||
|
if (this.ended) return;
|
||
|
this.ended = true;
|
||
|
this.finalSizeCallback = finalSizeCallback;
|
||
|
this.forceZip64Eocd = !!options.forceZip64Format;
|
||
|
if (options.comment) {
|
||
|
if (typeof options.comment === "string") {
|
||
|
this.comment = encodeCp437(options.comment);
|
||
|
} else {
|
||
|
// It should be a Buffer
|
||
|
this.comment = options.comment;
|
||
|
}
|
||
|
if (this.comment.length > 0xffff) throw new Error("comment is too large");
|
||
|
// gotta check for this, because the zipfile format is actually ambiguous.
|
||
|
if (this.comment.includes(eocdrSignatureBuffer)) throw new Error("comment contains end of central directory record signature");
|
||
|
} else {
|
||
|
// no comment.
|
||
|
this.comment = EMPTY_BUFFER;
|
||
|
}
|
||
|
pumpEntries(this);
|
||
|
};
|
||
|
|
||
|
function writeToOutputStream(self, buffer) {
|
||
|
self.outputStream.write(buffer);
|
||
|
self.outputStreamCursor += buffer.length;
|
||
|
}
|
||
|
|
||
|
function pumpFileDataReadStream(self, entry, readStream) {
|
||
|
var crc32Watcher = new Crc32Watcher();
|
||
|
var uncompressedSizeCounter = new ByteCounter();
|
||
|
var compressor = entry.compress ? new zlib.DeflateRaw() : new PassThrough();
|
||
|
var compressedSizeCounter = new ByteCounter();
|
||
|
readStream.pipe(crc32Watcher)
|
||
|
.pipe(uncompressedSizeCounter)
|
||
|
.pipe(compressor)
|
||
|
.pipe(compressedSizeCounter)
|
||
|
.pipe(self.outputStream, {end: false});
|
||
|
compressedSizeCounter.on("end", function() {
|
||
|
entry.crc32 = crc32Watcher.crc32;
|
||
|
if (entry.uncompressedSize == null) {
|
||
|
entry.uncompressedSize = uncompressedSizeCounter.byteCount;
|
||
|
} else {
|
||
|
if (entry.uncompressedSize !== uncompressedSizeCounter.byteCount) return self.emit("error", new Error("file data stream has unexpected number of bytes"));
|
||
|
}
|
||
|
entry.compressedSize = compressedSizeCounter.byteCount;
|
||
|
self.outputStreamCursor += entry.compressedSize;
|
||
|
writeToOutputStream(self, entry.getDataDescriptor());
|
||
|
entry.state = Entry.FILE_DATA_DONE;
|
||
|
pumpEntries(self);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function pumpEntries(self) {
|
||
|
if (self.allDone) return;
|
||
|
// first check if finalSize is finally known
|
||
|
if (self.ended && self.finalSizeCallback != null) {
|
||
|
var finalSize = calculateFinalSize(self);
|
||
|
if (finalSize != null) {
|
||
|
// we have an answer
|
||
|
self.finalSizeCallback(finalSize);
|
||
|
self.finalSizeCallback = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// pump entries
|
||
|
var entry = getFirstNotDoneEntry();
|
||
|
function getFirstNotDoneEntry() {
|
||
|
for (var i = 0; i < self.entries.length; i++) {
|
||
|
var entry = self.entries[i];
|
||
|
if (entry.state < Entry.FILE_DATA_DONE) return entry;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
if (entry != null) {
|
||
|
// this entry is not done yet
|
||
|
if (entry.state < Entry.READY_TO_PUMP_FILE_DATA) return; // input file not open yet
|
||
|
if (entry.state === Entry.FILE_DATA_IN_PROGRESS) return; // we'll get there
|
||
|
// start with local file header
|
||
|
entry.relativeOffsetOfLocalHeader = self.outputStreamCursor;
|
||
|
var localFileHeader = entry.getLocalFileHeader();
|
||
|
writeToOutputStream(self, localFileHeader);
|
||
|
entry.doFileDataPump();
|
||
|
} else {
|
||
|
// all cought up on writing entries
|
||
|
if (self.ended) {
|
||
|
// head for the exit
|
||
|
self.offsetOfStartOfCentralDirectory = self.outputStreamCursor;
|
||
|
self.entries.forEach(function(entry) {
|
||
|
var centralDirectoryRecord = entry.getCentralDirectoryRecord();
|
||
|
writeToOutputStream(self, centralDirectoryRecord);
|
||
|
});
|
||
|
writeToOutputStream(self, getEndOfCentralDirectoryRecord(self));
|
||
|
self.outputStream.end();
|
||
|
self.allDone = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function calculateFinalSize(self) {
|
||
|
var pretendOutputCursor = 0;
|
||
|
var centralDirectorySize = 0;
|
||
|
for (var i = 0; i < self.entries.length; i++) {
|
||
|
var entry = self.entries[i];
|
||
|
// compression is too hard to predict
|
||
|
if (entry.compress) return -1;
|
||
|
if (entry.state >= Entry.READY_TO_PUMP_FILE_DATA) {
|
||
|
// if addReadStream was called without providing the size, we can't predict the final size
|
||
|
if (entry.uncompressedSize == null) return -1;
|
||
|
} else {
|
||
|
// if we're still waiting for fs.stat, we might learn the size someday
|
||
|
if (entry.uncompressedSize == null) return null;
|
||
|
}
|
||
|
// we know this for sure, and this is important to know if we need ZIP64 format.
|
||
|
entry.relativeOffsetOfLocalHeader = pretendOutputCursor;
|
||
|
var useZip64Format = entry.useZip64Format();
|
||
|
|
||
|
pretendOutputCursor += LOCAL_FILE_HEADER_FIXED_SIZE + entry.utf8FileName.length;
|
||
|
pretendOutputCursor += entry.uncompressedSize;
|
||
|
if (!entry.crcAndFileSizeKnown) {
|
||
|
// use a data descriptor
|
||
|
if (useZip64Format) {
|
||
|
pretendOutputCursor += ZIP64_DATA_DESCRIPTOR_SIZE;
|
||
|
} else {
|
||
|
pretendOutputCursor += DATA_DESCRIPTOR_SIZE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
centralDirectorySize += CENTRAL_DIRECTORY_RECORD_FIXED_SIZE + entry.utf8FileName.length + entry.fileComment.length;
|
||
|
if (useZip64Format) {
|
||
|
centralDirectorySize += ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD_SIZE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var endOfCentralDirectorySize = 0;
|
||
|
if (self.forceZip64Eocd ||
|
||
|
self.entries.length >= 0xffff ||
|
||
|
centralDirectorySize >= 0xffff ||
|
||
|
pretendOutputCursor >= 0xffffffff) {
|
||
|
// use zip64 end of central directory stuff
|
||
|
endOfCentralDirectorySize += ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE + ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE;
|
||
|
}
|
||
|
endOfCentralDirectorySize += END_OF_CENTRAL_DIRECTORY_RECORD_SIZE + self.comment.length;
|
||
|
return pretendOutputCursor + centralDirectorySize + endOfCentralDirectorySize;
|
||
|
}
|
||
|
|
||
|
var ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE = 56;
|
||
|
var ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE = 20;
|
||
|
var END_OF_CENTRAL_DIRECTORY_RECORD_SIZE = 22;
|
||
|
function getEndOfCentralDirectoryRecord(self, actuallyJustTellMeHowLongItWouldBe) {
|
||
|
var needZip64Format = false;
|
||
|
var normalEntriesLength = self.entries.length;
|
||
|
if (self.forceZip64Eocd || self.entries.length >= 0xffff) {
|
||
|
normalEntriesLength = 0xffff;
|
||
|
needZip64Format = true;
|
||
|
}
|
||
|
var sizeOfCentralDirectory = self.outputStreamCursor - self.offsetOfStartOfCentralDirectory;
|
||
|
var normalSizeOfCentralDirectory = sizeOfCentralDirectory;
|
||
|
if (self.forceZip64Eocd || sizeOfCentralDirectory >= 0xffffffff) {
|
||
|
normalSizeOfCentralDirectory = 0xffffffff;
|
||
|
needZip64Format = true;
|
||
|
}
|
||
|
var normalOffsetOfStartOfCentralDirectory = self.offsetOfStartOfCentralDirectory;
|
||
|
if (self.forceZip64Eocd || self.offsetOfStartOfCentralDirectory >= 0xffffffff) {
|
||
|
normalOffsetOfStartOfCentralDirectory = 0xffffffff;
|
||
|
needZip64Format = true;
|
||
|
}
|
||
|
if (actuallyJustTellMeHowLongItWouldBe) {
|
||
|
if (needZip64Format) {
|
||
|
return (
|
||
|
ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE +
|
||
|
ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE +
|
||
|
END_OF_CENTRAL_DIRECTORY_RECORD_SIZE
|
||
|
);
|
||
|
} else {
|
||
|
return END_OF_CENTRAL_DIRECTORY_RECORD_SIZE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var eocdrBuffer = Buffer.allocUnsafe(END_OF_CENTRAL_DIRECTORY_RECORD_SIZE + self.comment.length);
|
||
|
// end of central dir signature 4 bytes (0x06054b50)
|
||
|
eocdrBuffer.writeUInt32LE(0x06054b50, 0);
|
||
|
// number of this disk 2 bytes
|
||
|
eocdrBuffer.writeUInt16LE(0, 4);
|
||
|
// number of the disk with the start of the central directory 2 bytes
|
||
|
eocdrBuffer.writeUInt16LE(0, 6);
|
||
|
// total number of entries in the central directory on this disk 2 bytes
|
||
|
eocdrBuffer.writeUInt16LE(normalEntriesLength, 8);
|
||
|
// total number of entries in the central directory 2 bytes
|
||
|
eocdrBuffer.writeUInt16LE(normalEntriesLength, 10);
|
||
|
// size of the central directory 4 bytes
|
||
|
eocdrBuffer.writeUInt32LE(normalSizeOfCentralDirectory, 12);
|
||
|
// offset of start of central directory with respect to the starting disk number 4 bytes
|
||
|
eocdrBuffer.writeUInt32LE(normalOffsetOfStartOfCentralDirectory, 16);
|
||
|
// .ZIP file comment length 2 bytes
|
||
|
eocdrBuffer.writeUInt16LE(self.comment.length, 20);
|
||
|
// .ZIP file comment (variable size)
|
||
|
self.comment.copy(eocdrBuffer, 22);
|
||
|
|
||
|
if (!needZip64Format) return eocdrBuffer;
|
||
|
|
||
|
// ZIP64 format
|
||
|
// ZIP64 End of Central Directory Record
|
||
|
var zip64EocdrBuffer = Buffer.allocUnsafe(ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE);
|
||
|
// zip64 end of central dir signature 4 bytes (0x06064b50)
|
||
|
zip64EocdrBuffer.writeUInt32LE(0x06064b50, 0);
|
||
|
// size of zip64 end of central directory record 8 bytes
|
||
|
writeUInt64LE(zip64EocdrBuffer, ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE - 12, 4);
|
||
|
// version made by 2 bytes
|
||
|
zip64EocdrBuffer.writeUInt16LE(VERSION_MADE_BY, 12);
|
||
|
// version needed to extract 2 bytes
|
||
|
zip64EocdrBuffer.writeUInt16LE(VERSION_NEEDED_TO_EXTRACT_ZIP64, 14);
|
||
|
// number of this disk 4 bytes
|
||
|
zip64EocdrBuffer.writeUInt32LE(0, 16);
|
||
|
// number of the disk with the start of the central directory 4 bytes
|
||
|
zip64EocdrBuffer.writeUInt32LE(0, 20);
|
||
|
// total number of entries in the central directory on this disk 8 bytes
|
||
|
writeUInt64LE(zip64EocdrBuffer, self.entries.length, 24);
|
||
|
// total number of entries in the central directory 8 bytes
|
||
|
writeUInt64LE(zip64EocdrBuffer, self.entries.length, 32);
|
||
|
// size of the central directory 8 bytes
|
||
|
writeUInt64LE(zip64EocdrBuffer, sizeOfCentralDirectory, 40);
|
||
|
// offset of start of central directory with respect to the starting disk number 8 bytes
|
||
|
writeUInt64LE(zip64EocdrBuffer, self.offsetOfStartOfCentralDirectory, 48);
|
||
|
// zip64 extensible data sector (variable size)
|
||
|
// nothing in the zip64 extensible data sector
|
||
|
|
||
|
|
||
|
// ZIP64 End of Central Directory Locator
|
||
|
var zip64EocdlBuffer = Buffer.allocUnsafe(ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE);
|
||
|
// zip64 end of central dir locator signature 4 bytes (0x07064b50)
|
||
|
zip64EocdlBuffer.writeUInt32LE(0x07064b50, 0);
|
||
|
// number of the disk with the start of the zip64 end of central directory 4 bytes
|
||
|
zip64EocdlBuffer.writeUInt32LE(0, 4);
|
||
|
// relative offset of the zip64 end of central directory record 8 bytes
|
||
|
writeUInt64LE(zip64EocdlBuffer, self.outputStreamCursor, 8);
|
||
|
// total number of disks 4 bytes
|
||
|
zip64EocdlBuffer.writeUInt32LE(1, 16);
|
||
|
|
||
|
|
||
|
return Buffer.concat([
|
||
|
zip64EocdrBuffer,
|
||
|
zip64EocdlBuffer,
|
||
|
eocdrBuffer,
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
function validateMetadataPath(metadataPath, isDirectory) {
|
||
|
if (metadataPath === "") throw new Error("empty metadataPath");
|
||
|
metadataPath = metadataPath.replace(/\\/g, "/");
|
||
|
if (/^[a-zA-Z]:/.test(metadataPath) || /^\//.test(metadataPath)) throw new Error("absolute path: " + metadataPath);
|
||
|
if (metadataPath.split("/").indexOf("..") !== -1) throw new Error("invalid relative path: " + metadataPath);
|
||
|
var looksLikeDirectory = /\/$/.test(metadataPath);
|
||
|
if (isDirectory) {
|
||
|
// append a trailing '/' if necessary.
|
||
|
if (!looksLikeDirectory) metadataPath += "/";
|
||
|
} else {
|
||
|
if (looksLikeDirectory) throw new Error("file path cannot end with '/': " + metadataPath);
|
||
|
}
|
||
|
return metadataPath;
|
||
|
}
|
||
|
|
||
|
var EMPTY_BUFFER = Buffer.allocUnsafe(0);
|
||
|
|
||
|
// this class is not part of the public API
|
||
|
function Entry(metadataPath, isDirectory, options) {
|
||
|
this.utf8FileName = Buffer.from(metadataPath);
|
||
|
if (this.utf8FileName.length > 0xffff) throw new Error("utf8 file name too long. " + utf8FileName.length + " > " + 0xffff);
|
||
|
this.isDirectory = isDirectory;
|
||
|
this.state = Entry.WAITING_FOR_METADATA;
|
||
|
this.setLastModDate(options.mtime != null ? options.mtime : new Date());
|
||
|
if (options.mode != null) {
|
||
|
this.setFileAttributesMode(options.mode);
|
||
|
} else {
|
||
|
this.setFileAttributesMode(isDirectory ? 0o40775 : 0o100664);
|
||
|
}
|
||
|
if (isDirectory) {
|
||
|
this.crcAndFileSizeKnown = true;
|
||
|
this.crc32 = 0;
|
||
|
this.uncompressedSize = 0;
|
||
|
this.compressedSize = 0;
|
||
|
} else {
|
||
|
// unknown so far
|
||
|
this.crcAndFileSizeKnown = false;
|
||
|
this.crc32 = null;
|
||
|
this.uncompressedSize = null;
|
||
|
this.compressedSize = null;
|
||
|
if (options.size != null) this.uncompressedSize = options.size;
|
||
|
}
|
||
|
if (isDirectory) {
|
||
|
this.compress = false;
|
||
|
} else {
|
||
|
this.compress = true; // default
|
||
|
if (options.compress != null) this.compress = !!options.compress;
|
||
|
}
|
||
|
this.forceZip64Format = !!options.forceZip64Format;
|
||
|
if (options.fileComment) {
|
||
|
if (typeof options.fileComment === "string") {
|
||
|
this.fileComment = Buffer.from(options.fileComment, "utf-8");
|
||
|
} else {
|
||
|
// It should be a Buffer
|
||
|
this.fileComment = options.fileComment;
|
||
|
}
|
||
|
if (this.fileComment.length > 0xffff) throw new Error("fileComment is too large");
|
||
|
} else {
|
||
|
// no comment.
|
||
|
this.fileComment = EMPTY_BUFFER;
|
||
|
}
|
||
|
}
|
||
|
Entry.WAITING_FOR_METADATA = 0;
|
||
|
Entry.READY_TO_PUMP_FILE_DATA = 1;
|
||
|
Entry.FILE_DATA_IN_PROGRESS = 2;
|
||
|
Entry.FILE_DATA_DONE = 3;
|
||
|
Entry.prototype.setLastModDate = function(date) {
|
||
|
var dosDateTime = dateToDosDateTime(date);
|
||
|
this.lastModFileTime = dosDateTime.time;
|
||
|
this.lastModFileDate = dosDateTime.date;
|
||
|
};
|
||
|
Entry.prototype.setFileAttributesMode = function(mode) {
|
||
|
if ((mode & 0xffff) !== mode) throw new Error("invalid mode. expected: 0 <= " + mode + " <= " + 0xffff);
|
||
|
// http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute/14727#14727
|
||
|
this.externalFileAttributes = (mode << 16) >>> 0;
|
||
|
};
|
||
|
// doFileDataPump() should not call pumpEntries() directly. see issue #9.
|
||
|
Entry.prototype.setFileDataPumpFunction = function(doFileDataPump) {
|
||
|
this.doFileDataPump = doFileDataPump;
|
||
|
this.state = Entry.READY_TO_PUMP_FILE_DATA;
|
||
|
};
|
||
|
Entry.prototype.useZip64Format = function() {
|
||
|
return (
|
||
|
(this.forceZip64Format) ||
|
||
|
(this.uncompressedSize != null && this.uncompressedSize > 0xfffffffe) ||
|
||
|
(this.compressedSize != null && this.compressedSize > 0xfffffffe) ||
|
||
|
(this.relativeOffsetOfLocalHeader != null && this.relativeOffsetOfLocalHeader > 0xfffffffe)
|
||
|
);
|
||
|
}
|
||
|
var LOCAL_FILE_HEADER_FIXED_SIZE = 30;
|
||
|
var VERSION_NEEDED_TO_EXTRACT_UTF8 = 20;
|
||
|
var VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45;
|
||
|
// 3 = unix. 63 = spec version 6.3
|
||
|
var VERSION_MADE_BY = (3 << 8) | 63;
|
||
|
var FILE_NAME_IS_UTF8 = 1 << 11;
|
||
|
var UNKNOWN_CRC32_AND_FILE_SIZES = 1 << 3;
|
||
|
Entry.prototype.getLocalFileHeader = function() {
|
||
|
var crc32 = 0;
|
||
|
var compressedSize = 0;
|
||
|
var uncompressedSize = 0;
|
||
|
if (this.crcAndFileSizeKnown) {
|
||
|
crc32 = this.crc32;
|
||
|
compressedSize = this.compressedSize;
|
||
|
uncompressedSize = this.uncompressedSize;
|
||
|
}
|
||
|
|
||
|
var fixedSizeStuff = Buffer.allocUnsafe(LOCAL_FILE_HEADER_FIXED_SIZE);
|
||
|
var generalPurposeBitFlag = FILE_NAME_IS_UTF8;
|
||
|
if (!this.crcAndFileSizeKnown) generalPurposeBitFlag |= UNKNOWN_CRC32_AND_FILE_SIZES;
|
||
|
|
||
|
// local file header signature 4 bytes (0x04034b50)
|
||
|
fixedSizeStuff.writeUInt32LE(0x04034b50, 0);
|
||
|
// version needed to extract 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(VERSION_NEEDED_TO_EXTRACT_UTF8, 4);
|
||
|
// general purpose bit flag 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(generalPurposeBitFlag, 6);
|
||
|
// compression method 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.getCompressionMethod(), 8);
|
||
|
// last mod file time 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.lastModFileTime, 10);
|
||
|
// last mod file date 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.lastModFileDate, 12);
|
||
|
// crc-32 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(crc32, 14);
|
||
|
// compressed size 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(compressedSize, 18);
|
||
|
// uncompressed size 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(uncompressedSize, 22);
|
||
|
// file name length 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.utf8FileName.length, 26);
|
||
|
// extra field length 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(0, 28);
|
||
|
return Buffer.concat([
|
||
|
fixedSizeStuff,
|
||
|
// file name (variable size)
|
||
|
this.utf8FileName,
|
||
|
// extra field (variable size)
|
||
|
// no extra fields
|
||
|
]);
|
||
|
};
|
||
|
var DATA_DESCRIPTOR_SIZE = 16;
|
||
|
var ZIP64_DATA_DESCRIPTOR_SIZE = 24;
|
||
|
Entry.prototype.getDataDescriptor = function() {
|
||
|
if (this.crcAndFileSizeKnown) {
|
||
|
// the Mac Archive Utility requires this not be present unless we set general purpose bit 3
|
||
|
return Buffer.allocUnsafe(0);
|
||
|
}
|
||
|
if (!this.useZip64Format()) {
|
||
|
var buffer = Buffer.allocUnsafe(DATA_DESCRIPTOR_SIZE);
|
||
|
// optional signature (required according to Archive Utility)
|
||
|
buffer.writeUInt32LE(0x08074b50, 0);
|
||
|
// crc-32 4 bytes
|
||
|
buffer.writeUInt32LE(this.crc32, 4);
|
||
|
// compressed size 4 bytes
|
||
|
buffer.writeUInt32LE(this.compressedSize, 8);
|
||
|
// uncompressed size 4 bytes
|
||
|
buffer.writeUInt32LE(this.uncompressedSize, 12);
|
||
|
return buffer;
|
||
|
} else {
|
||
|
// ZIP64 format
|
||
|
var buffer = Buffer.allocUnsafe(ZIP64_DATA_DESCRIPTOR_SIZE);
|
||
|
// optional signature (unknown if anyone cares about this)
|
||
|
buffer.writeUInt32LE(0x08074b50, 0);
|
||
|
// crc-32 4 bytes
|
||
|
buffer.writeUInt32LE(this.crc32, 4);
|
||
|
// compressed size 8 bytes
|
||
|
writeUInt64LE(buffer, this.compressedSize, 8);
|
||
|
// uncompressed size 8 bytes
|
||
|
writeUInt64LE(buffer, this.uncompressedSize, 16);
|
||
|
return buffer;
|
||
|
}
|
||
|
};
|
||
|
var CENTRAL_DIRECTORY_RECORD_FIXED_SIZE = 46;
|
||
|
var ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD_SIZE = 28;
|
||
|
Entry.prototype.getCentralDirectoryRecord = function() {
|
||
|
var fixedSizeStuff = Buffer.allocUnsafe(CENTRAL_DIRECTORY_RECORD_FIXED_SIZE);
|
||
|
var generalPurposeBitFlag = FILE_NAME_IS_UTF8;
|
||
|
if (!this.crcAndFileSizeKnown) generalPurposeBitFlag |= UNKNOWN_CRC32_AND_FILE_SIZES;
|
||
|
|
||
|
var normalCompressedSize = this.compressedSize;
|
||
|
var normalUncompressedSize = this.uncompressedSize;
|
||
|
var normalRelativeOffsetOfLocalHeader = this.relativeOffsetOfLocalHeader;
|
||
|
var versionNeededToExtract;
|
||
|
var zeiefBuffer;
|
||
|
if (this.useZip64Format()) {
|
||
|
normalCompressedSize = 0xffffffff;
|
||
|
normalUncompressedSize = 0xffffffff;
|
||
|
normalRelativeOffsetOfLocalHeader = 0xffffffff;
|
||
|
versionNeededToExtract = VERSION_NEEDED_TO_EXTRACT_ZIP64;
|
||
|
|
||
|
// ZIP64 extended information extra field
|
||
|
zeiefBuffer = Buffer.allocUnsafe(ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD_SIZE);
|
||
|
// 0x0001 2 bytes Tag for this "extra" block type
|
||
|
zeiefBuffer.writeUInt16LE(0x0001, 0);
|
||
|
// Size 2 bytes Size of this "extra" block
|
||
|
zeiefBuffer.writeUInt16LE(ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD_SIZE - 4, 2);
|
||
|
// Original Size 8 bytes Original uncompressed file size
|
||
|
writeUInt64LE(zeiefBuffer, this.uncompressedSize, 4);
|
||
|
// Compressed Size 8 bytes Size of compressed data
|
||
|
writeUInt64LE(zeiefBuffer, this.compressedSize, 12);
|
||
|
// Relative Header Offset 8 bytes Offset of local header record
|
||
|
writeUInt64LE(zeiefBuffer, this.relativeOffsetOfLocalHeader, 20);
|
||
|
// Disk Start Number 4 bytes Number of the disk on which this file starts
|
||
|
// (omit)
|
||
|
} else {
|
||
|
versionNeededToExtract = VERSION_NEEDED_TO_EXTRACT_UTF8;
|
||
|
zeiefBuffer = Buffer.allocUnsafe(0);
|
||
|
}
|
||
|
|
||
|
// central file header signature 4 bytes (0x02014b50)
|
||
|
fixedSizeStuff.writeUInt32LE(0x02014b50, 0);
|
||
|
// version made by 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(VERSION_MADE_BY, 4);
|
||
|
// version needed to extract 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(versionNeededToExtract, 6);
|
||
|
// general purpose bit flag 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(generalPurposeBitFlag, 8);
|
||
|
// compression method 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.getCompressionMethod(), 10);
|
||
|
// last mod file time 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.lastModFileTime, 12);
|
||
|
// last mod file date 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.lastModFileDate, 14);
|
||
|
// crc-32 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(this.crc32, 16);
|
||
|
// compressed size 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(normalCompressedSize, 20);
|
||
|
// uncompressed size 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(normalUncompressedSize, 24);
|
||
|
// file name length 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.utf8FileName.length, 28);
|
||
|
// extra field length 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(zeiefBuffer.length, 30);
|
||
|
// file comment length 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(this.fileComment.length, 32);
|
||
|
// disk number start 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(0, 34);
|
||
|
// internal file attributes 2 bytes
|
||
|
fixedSizeStuff.writeUInt16LE(0, 36);
|
||
|
// external file attributes 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(this.externalFileAttributes, 38);
|
||
|
// relative offset of local header 4 bytes
|
||
|
fixedSizeStuff.writeUInt32LE(normalRelativeOffsetOfLocalHeader, 42);
|
||
|
|
||
|
return Buffer.concat([
|
||
|
fixedSizeStuff,
|
||
|
// file name (variable size)
|
||
|
this.utf8FileName,
|
||
|
// extra field (variable size)
|
||
|
zeiefBuffer,
|
||
|
// file comment (variable size)
|
||
|
this.fileComment,
|
||
|
]);
|
||
|
};
|
||
|
Entry.prototype.getCompressionMethod = function() {
|
||
|
var NO_COMPRESSION = 0;
|
||
|
var DEFLATE_COMPRESSION = 8;
|
||
|
return this.compress ? DEFLATE_COMPRESSION : NO_COMPRESSION;
|
||
|
};
|
||
|
|
||
|
function dateToDosDateTime(jsDate) {
|
||
|
var date = 0;
|
||
|
date |= jsDate.getDate() & 0x1f; // 1-31
|
||
|
date |= ((jsDate.getMonth() + 1) & 0xf) << 5; // 0-11, 1-12
|
||
|
date |= ((jsDate.getFullYear() - 1980) & 0x7f) << 9; // 0-128, 1980-2108
|
||
|
|
||
|
var time = 0;
|
||
|
time |= Math.floor(jsDate.getSeconds() / 2); // 0-59, 0-29 (lose odd numbers)
|
||
|
time |= (jsDate.getMinutes() & 0x3f) << 5; // 0-59
|
||
|
time |= (jsDate.getHours() & 0x1f) << 11; // 0-23
|
||
|
|
||
|
return {date: date, time: time};
|
||
|
}
|
||
|
|
||
|
function writeUInt64LE(buffer, n, offset) {
|
||
|
// can't use bitshift here, because JavaScript only allows bitshifting on 32-bit integers.
|
||
|
var high = Math.floor(n / 0x100000000);
|
||
|
var low = n % 0x100000000;
|
||
|
buffer.writeUInt32LE(low, offset);
|
||
|
buffer.writeUInt32LE(high, offset + 4);
|
||
|
}
|
||
|
|
||
|
function defaultCallback(err) {
|
||
|
if (err) throw err;
|
||
|
}
|
||
|
|
||
|
util.inherits(ByteCounter, Transform);
|
||
|
function ByteCounter(options) {
|
||
|
Transform.call(this, options);
|
||
|
this.byteCount = 0;
|
||
|
}
|
||
|
ByteCounter.prototype._transform = function(chunk, encoding, cb) {
|
||
|
this.byteCount += chunk.length;
|
||
|
cb(null, chunk);
|
||
|
};
|
||
|
|
||
|
util.inherits(Crc32Watcher, Transform);
|
||
|
function Crc32Watcher(options) {
|
||
|
Transform.call(this, options);
|
||
|
this.crc32 = 0;
|
||
|
}
|
||
|
Crc32Watcher.prototype._transform = function(chunk, encoding, cb) {
|
||
|
this.crc32 = crc32.unsigned(chunk, this.crc32);
|
||
|
cb(null, chunk);
|
||
|
};
|
||
|
|
||
|
var cp437 = '\u0000☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ';
|
||
|
if (cp437.length !== 256) throw new Error("assertion failure");
|
||
|
var reverseCp437 = null;
|
||
|
|
||
|
function encodeCp437(string) {
|
||
|
if (/^[\x20-\x7e]*$/.test(string)) {
|
||
|
// CP437, ASCII, and UTF-8 overlap in this range.
|
||
|
return Buffer.from(string, "utf-8");
|
||
|
}
|
||
|
|
||
|
// This is the slow path.
|
||
|
if (reverseCp437 == null) {
|
||
|
// cache this once
|
||
|
reverseCp437 = {};
|
||
|
for (var i = 0; i < cp437.length; i++) {
|
||
|
reverseCp437[cp437[i]] = i;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var result = Buffer.allocUnsafe(string.length);
|
||
|
for (var i = 0; i < string.length; i++) {
|
||
|
var b = reverseCp437[string[i]];
|
||
|
if (b == null) throw new Error("character not encodable in CP437: " + JSON.stringify(string[i]));
|
||
|
result[i] = b;
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|