| 'use strict'; |
| var readerFor = require('./reader/readerFor'); |
| var utils = require('./utils'); |
| var CompressedObject = require('./compressedObject'); |
| var crc32fn = require('./crc32'); |
| var utf8 = require('./utf8'); |
| var compressions = require('./compressions'); |
| var support = require('./support'); |
| |
| var MADE_BY_DOS = 0x00; |
| var MADE_BY_UNIX = 0x03; |
| |
| /** |
| * Find a compression registered in JSZip. |
| * @param {string} compressionMethod the method magic to find. |
| * @return {Object|null} the JSZip compression object, null if none found. |
| */ |
| var findCompression = function(compressionMethod) { |
| for (var method in compressions) { |
| if (!compressions.hasOwnProperty(method)) { |
| continue; |
| } |
| if (compressions[method].magic === compressionMethod) { |
| return compressions[method]; |
| } |
| } |
| return null; |
| }; |
| |
| // class ZipEntry {{{ |
| /** |
| * An entry in the zip file. |
| * @constructor |
| * @param {Object} options Options of the current file. |
| * @param {Object} loadOptions Options for loading the stream. |
| */ |
| function ZipEntry(options, loadOptions) { |
| this.options = options; |
| this.loadOptions = loadOptions; |
| } |
| ZipEntry.prototype = { |
| /** |
| * say if the file is encrypted. |
| * @return {boolean} true if the file is encrypted, false otherwise. |
| */ |
| isEncrypted: function() { |
| // bit 1 is set |
| return (this.bitFlag & 0x0001) === 0x0001; |
| }, |
| /** |
| * say if the file has utf-8 filename/comment. |
| * @return {boolean} true if the filename/comment is in utf-8, false otherwise. |
| */ |
| useUTF8: function() { |
| // bit 11 is set |
| return (this.bitFlag & 0x0800) === 0x0800; |
| }, |
| /** |
| * Read the local part of a zip file and add the info in this object. |
| * @param {DataReader} reader the reader to use. |
| */ |
| readLocalPart: function(reader) { |
| var compression, localExtraFieldsLength; |
| |
| // we already know everything from the central dir ! |
| // If the central dir data are false, we are doomed. |
| // On the bright side, the local part is scary : zip64, data descriptors, both, etc. |
| // The less data we get here, the more reliable this should be. |
| // Let's skip the whole header and dash to the data ! |
| reader.skip(22); |
| // in some zip created on windows, the filename stored in the central dir contains \ instead of /. |
| // Strangely, the filename here is OK. |
| // I would love to treat these zip files as corrupted (see http://www.info-zip.org/FAQ.html#backslashes |
| // or APPNOTE#4.4.17.1, "All slashes MUST be forward slashes '/'") but there are a lot of bad zip generators... |
| // Search "unzip mismatching "local" filename continuing with "central" filename version" on |
| // the internet. |
| // |
| // I think I see the logic here : the central directory is used to display |
| // content and the local directory is used to extract the files. Mixing / and \ |
| // may be used to display \ to windows users and use / when extracting the files. |
| // Unfortunately, this lead also to some issues : http://seclists.org/fulldisclosure/2009/Sep/394 |
| this.fileNameLength = reader.readInt(2); |
| localExtraFieldsLength = reader.readInt(2); // can't be sure this will be the same as the central dir |
| // the fileName is stored as binary data, the handleUTF8 method will take care of the encoding. |
| this.fileName = reader.readData(this.fileNameLength); |
| reader.skip(localExtraFieldsLength); |
| |
| if (this.compressedSize === -1 || this.uncompressedSize === -1) { |
| throw new Error("Bug or corrupted zip : didn't get enough informations from the central directory " + "(compressedSize === -1 || uncompressedSize === -1)"); |
| } |
| |
| compression = findCompression(this.compressionMethod); |
| if (compression === null) { // no compression found |
| throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")"); |
| } |
| this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize)); |
| }, |
| |
| /** |
| * Read the central part of a zip file and add the info in this object. |
| * @param {DataReader} reader the reader to use. |
| */ |
| readCentralPart: function(reader) { |
| this.versionMadeBy = reader.readInt(2); |
| reader.skip(2); |
| // this.versionNeeded = reader.readInt(2); |
| this.bitFlag = reader.readInt(2); |
| this.compressionMethod = reader.readString(2); |
| this.date = reader.readDate(); |
| this.crc32 = reader.readInt(4); |
| this.compressedSize = reader.readInt(4); |
| this.uncompressedSize = reader.readInt(4); |
| var fileNameLength = reader.readInt(2); |
| this.extraFieldsLength = reader.readInt(2); |
| this.fileCommentLength = reader.readInt(2); |
| this.diskNumberStart = reader.readInt(2); |
| this.internalFileAttributes = reader.readInt(2); |
| this.externalFileAttributes = reader.readInt(4); |
| this.localHeaderOffset = reader.readInt(4); |
| |
| if (this.isEncrypted()) { |
| throw new Error("Encrypted zip are not supported"); |
| } |
| |
| // will be read in the local part, see the comments there |
| reader.skip(fileNameLength); |
| this.readExtraFields(reader); |
| this.parseZIP64ExtraField(reader); |
| this.fileComment = reader.readData(this.fileCommentLength); |
| }, |
| |
| /** |
| * Parse the external file attributes and get the unix/dos permissions. |
| */ |
| processAttributes: function () { |
| this.unixPermissions = null; |
| this.dosPermissions = null; |
| var madeBy = this.versionMadeBy >> 8; |
| |
| // Check if we have the DOS directory flag set. |
| // We look for it in the DOS and UNIX permissions |
| // but some unknown platform could set it as a compatibility flag. |
| this.dir = this.externalFileAttributes & 0x0010 ? true : false; |
| |
| if(madeBy === MADE_BY_DOS) { |
| // first 6 bits (0 to 5) |
| this.dosPermissions = this.externalFileAttributes & 0x3F; |
| } |
| |
| if(madeBy === MADE_BY_UNIX) { |
| this.unixPermissions = (this.externalFileAttributes >> 16) & 0xFFFF; |
| // the octal permissions are in (this.unixPermissions & 0x01FF).toString(8); |
| } |
| |
| // fail safe : if the name ends with a / it probably means a folder |
| if (!this.dir && this.fileNameStr.slice(-1) === '/') { |
| this.dir = true; |
| } |
| }, |
| |
| /** |
| * Parse the ZIP64 extra field and merge the info in the current ZipEntry. |
| * @param {DataReader} reader the reader to use. |
| */ |
| parseZIP64ExtraField: function(reader) { |
| |
| if (!this.extraFields[0x0001]) { |
| return; |
| } |
| |
| // should be something, preparing the extra reader |
| var extraReader = readerFor(this.extraFields[0x0001].value); |
| |
| // I really hope that these 64bits integer can fit in 32 bits integer, because js |
| // won't let us have more. |
| if (this.uncompressedSize === utils.MAX_VALUE_32BITS) { |
| this.uncompressedSize = extraReader.readInt(8); |
| } |
| if (this.compressedSize === utils.MAX_VALUE_32BITS) { |
| this.compressedSize = extraReader.readInt(8); |
| } |
| if (this.localHeaderOffset === utils.MAX_VALUE_32BITS) { |
| this.localHeaderOffset = extraReader.readInt(8); |
| } |
| if (this.diskNumberStart === utils.MAX_VALUE_32BITS) { |
| this.diskNumberStart = extraReader.readInt(4); |
| } |
| }, |
| /** |
| * Read the central part of a zip file and add the info in this object. |
| * @param {DataReader} reader the reader to use. |
| */ |
| readExtraFields: function(reader) { |
| var end = reader.index + this.extraFieldsLength, |
| extraFieldId, |
| extraFieldLength, |
| extraFieldValue; |
| |
| if (!this.extraFields) { |
| this.extraFields = {}; |
| } |
| |
| while (reader.index < end) { |
| extraFieldId = reader.readInt(2); |
| extraFieldLength = reader.readInt(2); |
| extraFieldValue = reader.readData(extraFieldLength); |
| |
| this.extraFields[extraFieldId] = { |
| id: extraFieldId, |
| length: extraFieldLength, |
| value: extraFieldValue |
| }; |
| } |
| }, |
| /** |
| * Apply an UTF8 transformation if needed. |
| */ |
| handleUTF8: function() { |
| var decodeParamType = support.uint8array ? "uint8array" : "array"; |
| if (this.useUTF8()) { |
| this.fileNameStr = utf8.utf8decode(this.fileName); |
| this.fileCommentStr = utf8.utf8decode(this.fileComment); |
| } else { |
| var upath = this.findExtraFieldUnicodePath(); |
| if (upath !== null) { |
| this.fileNameStr = upath; |
| } else { |
| // ASCII text or unsupported code page |
| var fileNameByteArray = utils.transformTo(decodeParamType, this.fileName); |
| this.fileNameStr = this.loadOptions.decodeFileName(fileNameByteArray); |
| } |
| |
| var ucomment = this.findExtraFieldUnicodeComment(); |
| if (ucomment !== null) { |
| this.fileCommentStr = ucomment; |
| } else { |
| // ASCII text or unsupported code page |
| var commentByteArray = utils.transformTo(decodeParamType, this.fileComment); |
| this.fileCommentStr = this.loadOptions.decodeFileName(commentByteArray); |
| } |
| } |
| }, |
| |
| /** |
| * Find the unicode path declared in the extra field, if any. |
| * @return {String} the unicode path, null otherwise. |
| */ |
| findExtraFieldUnicodePath: function() { |
| var upathField = this.extraFields[0x7075]; |
| if (upathField) { |
| var extraReader = readerFor(upathField.value); |
| |
| // wrong version |
| if (extraReader.readInt(1) !== 1) { |
| return null; |
| } |
| |
| // the crc of the filename changed, this field is out of date. |
| if (crc32fn(this.fileName) !== extraReader.readInt(4)) { |
| return null; |
| } |
| |
| return utf8.utf8decode(extraReader.readData(upathField.length - 5)); |
| } |
| return null; |
| }, |
| |
| /** |
| * Find the unicode comment declared in the extra field, if any. |
| * @return {String} the unicode comment, null otherwise. |
| */ |
| findExtraFieldUnicodeComment: function() { |
| var ucommentField = this.extraFields[0x6375]; |
| if (ucommentField) { |
| var extraReader = readerFor(ucommentField.value); |
| |
| // wrong version |
| if (extraReader.readInt(1) !== 1) { |
| return null; |
| } |
| |
| // the crc of the comment changed, this field is out of date. |
| if (crc32fn(this.fileComment) !== extraReader.readInt(4)) { |
| return null; |
| } |
| |
| return utf8.utf8decode(extraReader.readData(ucommentField.length - 5)); |
| } |
| return null; |
| } |
| }; |
| module.exports = ZipEntry; |