UNPKG

zip

Version:

An implementation of unzip for JavaScript

462 lines (386 loc) 16.6 kB
// 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 ); }