| // Tom Robinson |
| // Kris Kowal |
| |
| var INFLATE = require("./inflate"); |
| var Buffer = require("buffer").Buffer; |
| |
| var LOCAL_FILE_HEADER = 0x04034b50; |
| var CENTRAL_DIRECTORY_FILE_HEADER = 0x02014b50; |
| var END_OF_CENTRAL_DIRECTORY_RECORD = 0x06054b50; |
| |
| var Reader = exports.Reader = function (data) { |
| if (!(this instanceof Reader)) |
| return new Reader(data); |
| this._data = data; |
| this._offset = 0; |
| } |
| |
| Reader.prototype.length = function () { |
| return this._data.length; |
| } |
| |
| Reader.prototype.position = function () { |
| return this._offset; |
| } |
| |
| Reader.prototype.seek = function (offset) { |
| this._offset = offset; |
| } |
| |
| Reader.prototype.read = function (length) { |
| var bytes = this._data.slice(this._offset, 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 this.read(length).toString(charset || "UTF-8"); |
| } |
| |
| 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 0x04034b50, 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 |
| |
| return structure; |
| } |
| |
| // 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 0x04034b50, 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(); |
| |
| var uncompressed = null; |
| if (localHeader.file_name.slice(-1) !== "/") { |
| uncompressed = stream.readUncompressed(centralHeader.compressed_size, centralHeader.compression_method); |
| } |
| |
| // seek back to the next central directory header |
| stream.seek(saved); |
| |
| return new Entry(localHeader, uncompressed); |
| } |
| }; |
| }; |
| |
| 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, stream) { |
| this._header = header; |
| this._stream = stream; |
| }; |
| |
| 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.getData = function () { |
| return this._stream; |
| }; |
| |
| var bytesToNumberLE = function (bytes) { |
| var acc = 0; |
| for (var i = 0; i < bytes.length; i++) |
| acc += bytes.get(i) << (8*i); |
| return acc; |
| }; |
| |
| var bytesToNumberBE = function (bytes) { |
| var acc = 0; |
| for (var i = 0; i < bytes.length; i++) |
| acc = (acc << 8) + bytes.get(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 Buffer(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 Buffer(bytes); |
| }; |
| |