zip
Version:
An implementation of unzip for JavaScript
462 lines (386 loc) • 16.6 kB
JavaScript
// Tom Robinson
// Kris Kowal
var INFLATE = require("./inflate");
var bops = require("bops");
var fs = require("fs");
var LOCAL_FILE_HEADER = 0x04034b50;
var CENTRAL_DIRECTORY_FILE_HEADER = 0x02014b50;
var END_OF_CENTRAL_DIRECTORY_RECORD = 0x06054b50;
var MADE_BY_UNIX = 3; // See http://www.pkware.com/documents/casestudies/APPNOTE.TXT
var Reader = exports.Reader = function (data) {
if (!(this instanceof Reader))
return new Reader(data);
if (bops.is(data))
this._source = new BufferSource(data);
else
this._source = new FdSource(data);
this._offset = 0;
}
function FdSource(fd) {
this._fileLength = fs.fstatSync(fd).size;
this.length = function() {
return this._fileLength;
}
this.read = function(start, length) {
var result = bops.create(length);
while (length > 0) {
var pos = 0;
var toRead = length > 8192? 8192: length;
fs.readSync(fd, result, pos, toRead, start);
length -= toRead;
start += toRead;
pos += toRead;
}
return result;
}
}
function BufferSource(buffer) {
this._buffer = buffer;
this.length = function() {
return buffer.length;
}
this.read = function (start, length) {
var bytes = bops.subarray(this._buffer, start, start+length);
return bytes;
}
}
Reader.prototype.length = function () {
return this._source.length();
}
Reader.prototype.position = function () {
return this._offset;
}
Reader.prototype.seek = function (offset) {
this._offset = offset;
}
Reader.prototype.read = function (length) {
var bytes = this._source.read(this._offset, length);
this._offset += length;
return bytes;
}
Reader.prototype.readInteger = function (length, bigEndian) {
if (bigEndian)
return bytesToNumberBE(this.read(length));
else
return bytesToNumberLE(this.read(length));
}
Reader.prototype.readString = function (length, charset) {
return bops.to(this.read(length), charset || "utf8");
}
Reader.prototype.readUncompressed = function (length, method) {
var compressed = this.read(length);
var uncompressed = null;
if (method === 0)
uncompressed = compressed;
else if (method === 8)
uncompressed = INFLATE.inflate(compressed);
else
throw new Error("Unknown compression method: " + structure.compression_method);
return uncompressed;
}
Reader.prototype.readStructure = function () {
var stream = this;
var structure = {};
// local file header signature 4 bytes (0x04034b50)
structure.signature = stream.readInteger(4);
switch (structure.signature) {
case LOCAL_FILE_HEADER :
this.readLocalFileHeader(structure);
break;
case CENTRAL_DIRECTORY_FILE_HEADER :
this.readCentralDirectoryFileHeader(structure);
break;
case END_OF_CENTRAL_DIRECTORY_RECORD :
this.readEndOfCentralDirectoryRecord(structure);
break;
default:
throw new Error("Unknown ZIP structure signature: 0x" + structure.signature.toString(16));
}
return structure;
}
// ZIP local file header
// Offset Bytes Description
// 0 4 Local file header signature = 0x04034b50
// 4 2 Version needed to extract (minimum)
// 6 2 General purpose bit flag
// 8 2 Compression method
// 10 2 File last modification time
// 12 2 File last modification date
// 14 4 CRC-32
// 18 4 Compressed size
// 22 4 Uncompressed size
// 26 2 File name length (n)
// 28 2 Extra field length (m)
// 30 n File name
// 30+n m Extra field
Reader.prototype.readLocalFileHeader = function (structure) {
var stream = this;
structure = structure || {};
if (!structure.signature)
structure.signature = stream.readInteger(4); // Local file header signature = 0x04034b50
if (structure.signature !== LOCAL_FILE_HEADER)
throw new Error("ZIP local file header signature invalid (expects 0x04034b50, actually 0x" + structure.signature.toString(16) +")");
structure.version_needed = stream.readInteger(2); // Version needed to extract (minimum)
structure.flags = stream.readInteger(2); // General purpose bit flag
structure.compression_method = stream.readInteger(2); // Compression method
structure.last_mod_file_time = stream.readInteger(2); // File last modification time
structure.last_mod_file_date = stream.readInteger(2); // File last modification date
structure.crc_32 = stream.readInteger(4); // CRC-32
structure.compressed_size = stream.readInteger(4); // Compressed size
structure.uncompressed_size = stream.readInteger(4); // Uncompressed size
structure.file_name_length = stream.readInteger(2); // File name length (n)
structure.extra_field_length = stream.readInteger(2); // Extra field length (m)
var n = structure.file_name_length;
var m = structure.extra_field_length;
structure.file_name = stream.readString(n); // File name
structure.extra_field = stream.read(m); // Extra fieldFile name
return structure;
}
// ZIP central directory file header
// Offset Bytes Description
// 0 4 Central directory file header signature = 0x02014b50
// 4 2 Version made by
// 6 2 Version needed to extract (minimum)
// 8 2 General purpose bit flag
// 10 2 Compression method
// 12 2 File last modification time
// 14 2 File last modification date
// 16 4 CRC-32
// 20 4 Compressed size
// 24 4 Uncompressed size
// 28 2 File name length (n)
// 30 2 Extra field length (m)
// 32 2 File comment length (k)
// 34 2 Disk number where file starts
// 36 2 Internal file attributes
// 38 4 External file attributes
// 42 4 Relative offset of local file header
// 46 n File name
// 46+n m Extra field
// 46+n+m k File comment
Reader.prototype.readCentralDirectoryFileHeader = function (structure) {
var stream = this;
structure = structure || {};
if (!structure.signature)
structure.signature = stream.readInteger(4); // Central directory file header signature = 0x02014b50
if (structure.signature !== CENTRAL_DIRECTORY_FILE_HEADER)
throw new Error("ZIP central directory file header signature invalid (expects 0x02014b50, actually 0x" + structure.signature.toString(16) +")");
structure.version = stream.readInteger(2); // Version made by
structure.version_needed = stream.readInteger(2); // Version needed to extract (minimum)
structure.flags = stream.readInteger(2); // General purpose bit flag
structure.compression_method = stream.readInteger(2); // Compression method
structure.last_mod_file_time = stream.readInteger(2); // File last modification time
structure.last_mod_file_date = stream.readInteger(2); // File last modification date
structure.crc_32 = stream.readInteger(4); // CRC-32
structure.compressed_size = stream.readInteger(4); // Compressed size
structure.uncompressed_size = stream.readInteger(4); // Uncompressed size
structure.file_name_length = stream.readInteger(2); // File name length (n)
structure.extra_field_length = stream.readInteger(2); // Extra field length (m)
structure.file_comment_length = stream.readInteger(2); // File comment length (k)
structure.disk_number = stream.readInteger(2); // Disk number where file starts
structure.internal_file_attributes = stream.readInteger(2); // Internal file attributes
structure.external_file_attributes = stream.readInteger(4); // External file attributes
structure.local_file_header_offset = stream.readInteger(4); // Relative offset of local file header
var n = structure.file_name_length;
var m = structure.extra_field_length;
var k = structure.file_comment_length;
structure.file_name = stream.readString(n); // File name
structure.extra_field = stream.read(m); // Extra field
structure.file_comment = stream.readString(k); // File comment
structure.mode = stream.detectChmod(structure.version, structure.external_file_attributes); // chmod
return structure;
}
Reader.prototype.detectChmod = function(versionMadeBy, externalFileAttributes) {
var madeBy = versionMadeBy >> 8,
mode = externalFileAttributes >>> 16,
chmod = false;
mode = (mode & 0x1ff);
if (madeBy === MADE_BY_UNIX && (process.platform === 'darwin' || process.platform === 'linux')) {
chmod = mode.toString(8);
}
return chmod;
}
// finds the end of central directory record
// I'd like to slap whoever thought it was a good idea to put a variable length comment field here
Reader.prototype.locateEndOfCentralDirectoryRecord = function () {
var length = this.length();
var minPosition = length - Math.pow(2, 16) - 22;
var position = length - 22 + 1;
while (--position) {
if (position < minPosition)
throw new Error("Unable to find end of central directory record");
this.seek(position);
var possibleSignature = this.readInteger(4);
if (possibleSignature !== END_OF_CENTRAL_DIRECTORY_RECORD)
continue;
this.seek(position + 20);
var possibleFileCommentLength = this.readInteger(2);
if (position + 22 + possibleFileCommentLength === length)
break;
}
this.seek(position);
return position;
};
// ZIP end of central directory record
// Offset Bytes Description
// 0 4 End of central directory signature = 0x06054b50
// 4 2 Number of this disk
// 6 2 Disk where central directory starts
// 8 2 Number of central directory records on this disk
// 10 2 Total number of central directory records
// 12 4 Size of central directory (bytes)
// 16 4 Offset of start of central directory, relative to start of archive
// 20 2 ZIP file comment length (n)
// 22 n ZIP file comment
Reader.prototype.readEndOfCentralDirectoryRecord = function (structure) {
var stream = this;
structure = structure || {};
if (!structure.signature)
structure.signature = stream.readInteger(4); // End of central directory signature = 0x06054b50
if (structure.signature !== END_OF_CENTRAL_DIRECTORY_RECORD)
throw new Error("ZIP end of central directory record signature invalid (expects 0x06054b50, actually 0x" + structure.signature.toString(16) +")");
structure.disk_number = stream.readInteger(2); // Number of this disk
structure.central_dir_disk_number = stream.readInteger(2); // Disk where central directory starts
structure.central_dir_disk_records = stream.readInteger(2); // Number of central directory records on this disk
structure.central_dir_total_records = stream.readInteger(2); // Total number of central directory records
structure.central_dir_size = stream.readInteger(4); // Size of central directory (bytes)
structure.central_dir_offset = stream.readInteger(4); // Offset of start of central directory, relative to start of archive
structure.file_comment_length = stream.readInteger(2); // ZIP file comment length (n)
var n = structure.file_comment_length;
structure.file_comment = stream.readString(n); // ZIP file comment
return structure;
}
Reader.prototype.readDataDescriptor = function () {
var stream = this;
var descriptor = {};
descriptor.crc_32 = stream.readInteger(4);
if (descriptor.crc_32 === 0x08074b50)
descriptor.crc_32 = stream.readInteger(4); // CRC-32
descriptor.compressed_size = stream.readInteger(4); // Compressed size
descriptor.uncompressed_size = stream.readInteger(4); // Uncompressed size
return descriptor;
}
Reader.prototype.iterator = function () {
var stream = this;
// find the end record and read it
stream.locateEndOfCentralDirectoryRecord();
var endRecord = stream.readEndOfCentralDirectoryRecord();
// seek to the beginning of the central directory
stream.seek(endRecord.central_dir_offset);
var count = endRecord.central_dir_disk_records;
return {
next: function () {
if ((count--) === 0)
throw "stop-iteration";
// read the central directory header
var centralHeader = stream.readCentralDirectoryFileHeader();
// save our new position so we can restore it
var saved = stream.position();
// seek to the local header and read it
stream.seek(centralHeader.local_file_header_offset);
var localHeader = stream.readLocalFileHeader();
// dont read the content just save the position for later use
var start = stream.position();
// seek back to the next central directory header
stream.seek(saved);
return new Entry(localHeader, stream, start, centralHeader.compressed_size, centralHeader.compression_method, centralHeader.mode);
}
};
};
Reader.prototype.forEach = function (block, context) {
var iterator = this.iterator();
var next;
while (true) {
try {
next = iterator.next();
} catch (exception) {
if (exception === "stop-iteration")
break;
if (exception === "skip-iteration")
continue;
throw exception;
}
block.call(context, next);
}
};
Reader.prototype.toObject = function (charset) {
var object = {};
this.forEach(function (entry) {
if (entry.isFile()) {
var data = entry.getData();
if (charset)
data = data.toString(charset);
object[entry.getName()] = data;
}
});
return object;
};
Reader.prototype.close = function (mode, options) {
};
var Entry = exports.Entry = function (header, realStream, start, compressedSize, compressionMethod, mode) {
this._mode = mode;
this._header = header;
this._realStream = realStream;
this._stream = null;
this._start = start;
this._compressedSize = compressedSize;
this._compressionMethod = compressionMethod;
};
Entry.prototype.getName = function () {
return this._header.file_name;
};
Entry.prototype.isFile = function () {
return !this.isDirectory();
};
Entry.prototype.isDirectory = function () {
return this.getName().slice(-1) === "/";
};
Entry.prototype.lastModified = function () {
return decodeDateTime(this._header.last_mod_file_date, this._header.last_mod_file_time);
};
Entry.prototype.getData = function () {
if (this._stream == null) {
var bookmark = this._realStream.position();
this._realStream.seek(this._start);
this._stream = this._realStream.readUncompressed(this._compressedSize, this._compressionMethod);
this._realStream.seek(bookmark);
}
return this._stream;
};
Entry.prototype.getMode = function () {
return this._mode;
};
var bytesToNumberLE = function (bytes) {
var acc = 0;
for (var i = 0; i < bytes.length; i++)
acc += bops.readUInt8(bytes, i) << (8*i);
return acc;
};
var bytesToNumberBE = function (bytes) {
var acc = 0;
for (var i = 0; i < bytes.length; i++)
acc = (acc << 8) + bops.readUInt8(bytes, i);
return acc;
};
var numberToBytesLE = function (number, length) {
var bytes = [];
for (var i = 0; i < length; i++)
bytes[i] = (number >> (8*i)) & 0xFF;
return new bops.from(bytes);
};
var numberToBytesBE = function (number, length) {
var bytes = [];
for (var i = 0; i < length; i++)
bytes[length-i-1] = (number >> (8*i)) & 0xFF;
return new bops.from(bytes);
};
var decodeDateTime = function (date, time) {
return new Date(
(date >>> 9) + 1980,
((date >>> 5) & 15) - 1,
(date) & 31,
(time >>> 11) & 31,
(time >>> 5) & 63,
(time & 63) * 2
);
}