2 var StringReader = require('./stringReader');
3 var utils = require('./utils');
4 var CompressedObject = require('./compressedObject');
5 var jszipProto = require('./object');
7 var MADE_BY_DOS = 0x00;
8 var MADE_BY_UNIX = 0x03;
12 * An entry in the zip file.
14 * @param {Object} options Options of the current file.
15 * @param {Object} loadOptions Options for loading the stream.
17 function ZipEntry(options, loadOptions) {
18 this.options = options;
19 this.loadOptions = loadOptions;
21 ZipEntry.prototype = {
23 * say if the file is encrypted.
24 * @return {boolean} true if the file is encrypted, false otherwise.
26 isEncrypted: function() {
28 return (this.bitFlag & 0x0001) === 0x0001;
31 * say if the file has utf-8 filename/comment.
32 * @return {boolean} true if the filename/comment is in utf-8, false otherwise.
36 return (this.bitFlag & 0x0800) === 0x0800;
39 * Prepare the function used to generate the compressed content from this ZipFile.
40 * @param {DataReader} reader the reader to use.
41 * @param {number} from the offset from where we should read the data.
42 * @param {number} length the length of the data to read.
43 * @return {Function} the callback to get the compressed content (the type depends of the DataReader class).
45 prepareCompressedContent: function(reader, from, length) {
47 var previousIndex = reader.index;
48 reader.setIndex(from);
49 var compressedFileData = reader.readData(length);
50 reader.setIndex(previousIndex);
52 return compressedFileData;
56 * Prepare the function used to generate the uncompressed content from this ZipFile.
57 * @param {DataReader} reader the reader to use.
58 * @param {number} from the offset from where we should read the data.
59 * @param {number} length the length of the data to read.
60 * @param {JSZip.compression} compression the compression used on this file.
61 * @param {number} uncompressedSize the uncompressed size to expect.
62 * @return {Function} the callback to get the uncompressed content (the type depends of the DataReader class).
64 prepareContent: function(reader, from, length, compression, uncompressedSize) {
67 var compressedFileData = utils.transformTo(compression.uncompressInputType, this.getCompressedContent());
68 var uncompressedFileData = compression.uncompress(compressedFileData);
70 if (uncompressedFileData.length !== uncompressedSize) {
71 throw new Error("Bug : uncompressed data size mismatch");
74 return uncompressedFileData;
78 * Read the local part of a zip file and add the info in this object.
79 * @param {DataReader} reader the reader to use.
81 readLocalPart: function(reader) {
82 var compression, localExtraFieldsLength;
84 // we already know everything from the central dir !
85 // If the central dir data are false, we are doomed.
86 // On the bright side, the local part is scary : zip64, data descriptors, both, etc.
87 // The less data we get here, the more reliable this should be.
88 // Let's skip the whole header and dash to the data !
90 // in some zip created on windows, the filename stored in the central dir contains \ instead of /.
91 // Strangely, the filename here is OK.
92 // I would love to treat these zip files as corrupted (see http://www.info-zip.org/FAQ.html#backslashes
93 // or APPNOTE#4.4.17.1, "All slashes MUST be forward slashes '/'") but there are a lot of bad zip generators...
94 // Search "unzip mismatching "local" filename continuing with "central" filename version" on
97 // I think I see the logic here : the central directory is used to display
98 // content and the local directory is used to extract the files. Mixing / and \
99 // may be used to display \ to windows users and use / when extracting the files.
100 // Unfortunately, this lead also to some issues : http://seclists.org/fulldisclosure/2009/Sep/394
101 this.fileNameLength = reader.readInt(2);
102 localExtraFieldsLength = reader.readInt(2); // can't be sure this will be the same as the central dir
103 this.fileName = reader.readString(this.fileNameLength);
104 reader.skip(localExtraFieldsLength);
106 if (this.compressedSize == -1 || this.uncompressedSize == -1) {
107 throw new Error("Bug or corrupted zip : didn't get enough informations from the central directory " + "(compressedSize == -1 || uncompressedSize == -1)");
110 compression = utils.findCompression(this.compressionMethod);
111 if (compression === null) { // no compression found
112 throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + this.fileName + ")");
114 this.decompressed = new CompressedObject();
115 this.decompressed.compressedSize = this.compressedSize;
116 this.decompressed.uncompressedSize = this.uncompressedSize;
117 this.decompressed.crc32 = this.crc32;
118 this.decompressed.compressionMethod = this.compressionMethod;
119 this.decompressed.getCompressedContent = this.prepareCompressedContent(reader, reader.index, this.compressedSize, compression);
120 this.decompressed.getContent = this.prepareContent(reader, reader.index, this.compressedSize, compression, this.uncompressedSize);
122 // we need to compute the crc32...
123 if (this.loadOptions.checkCRC32) {
124 this.decompressed = utils.transformTo("string", this.decompressed.getContent());
125 if (jszipProto.crc32(this.decompressed) !== this.crc32) {
126 throw new Error("Corrupted zip : CRC32 mismatch");
132 * Read the central part of a zip file and add the info in this object.
133 * @param {DataReader} reader the reader to use.
135 readCentralPart: function(reader) {
136 this.versionMadeBy = reader.readInt(2);
137 this.versionNeeded = reader.readInt(2);
138 this.bitFlag = reader.readInt(2);
139 this.compressionMethod = reader.readString(2);
140 this.date = reader.readDate();
141 this.crc32 = reader.readInt(4);
142 this.compressedSize = reader.readInt(4);
143 this.uncompressedSize = reader.readInt(4);
144 this.fileNameLength = reader.readInt(2);
145 this.extraFieldsLength = reader.readInt(2);
146 this.fileCommentLength = reader.readInt(2);
147 this.diskNumberStart = reader.readInt(2);
148 this.internalFileAttributes = reader.readInt(2);
149 this.externalFileAttributes = reader.readInt(4);
150 this.localHeaderOffset = reader.readInt(4);
152 if (this.isEncrypted()) {
153 throw new Error("Encrypted zip are not supported");
156 this.fileName = reader.readString(this.fileNameLength);
157 this.readExtraFields(reader);
158 this.parseZIP64ExtraField(reader);
159 this.fileComment = reader.readString(this.fileCommentLength);
163 * Parse the external file attributes and get the unix/dos permissions.
165 processAttributes: function () {
166 this.unixPermissions = null;
167 this.dosPermissions = null;
168 var madeBy = this.versionMadeBy >> 8;
170 // Check if we have the DOS directory flag set.
171 // We look for it in the DOS and UNIX permissions
172 // but some unknown platform could set it as a compatibility flag.
173 this.dir = this.externalFileAttributes & 0x0010 ? true : false;
175 if(madeBy === MADE_BY_DOS) {
176 // first 6 bits (0 to 5)
177 this.dosPermissions = this.externalFileAttributes & 0x3F;
180 if(madeBy === MADE_BY_UNIX) {
181 this.unixPermissions = (this.externalFileAttributes >> 16) & 0xFFFF;
182 // the octal permissions are in (this.unixPermissions & 0x01FF).toString(8);
185 // fail safe : if the name ends with a / it probably means a folder
186 if (!this.dir && this.fileName.slice(-1) === '/') {
192 * Parse the ZIP64 extra field and merge the info in the current ZipEntry.
193 * @param {DataReader} reader the reader to use.
195 parseZIP64ExtraField: function(reader) {
197 if (!this.extraFields[0x0001]) {
201 // should be something, preparing the extra reader
202 var extraReader = new StringReader(this.extraFields[0x0001].value);
204 // I really hope that these 64bits integer can fit in 32 bits integer, because js
205 // won't let us have more.
206 if (this.uncompressedSize === utils.MAX_VALUE_32BITS) {
207 this.uncompressedSize = extraReader.readInt(8);
209 if (this.compressedSize === utils.MAX_VALUE_32BITS) {
210 this.compressedSize = extraReader.readInt(8);
212 if (this.localHeaderOffset === utils.MAX_VALUE_32BITS) {
213 this.localHeaderOffset = extraReader.readInt(8);
215 if (this.diskNumberStart === utils.MAX_VALUE_32BITS) {
216 this.diskNumberStart = extraReader.readInt(4);
220 * Read the central part of a zip file and add the info in this object.
221 * @param {DataReader} reader the reader to use.
223 readExtraFields: function(reader) {
224 var start = reader.index,
229 this.extraFields = this.extraFields || {};
231 while (reader.index < start + this.extraFieldsLength) {
232 extraFieldId = reader.readInt(2);
233 extraFieldLength = reader.readInt(2);
234 extraFieldValue = reader.readString(extraFieldLength);
236 this.extraFields[extraFieldId] = {
238 length: extraFieldLength,
239 value: extraFieldValue
244 * Apply an UTF8 transformation if needed.
246 handleUTF8: function() {
247 if (this.useUTF8()) {
248 this.fileName = jszipProto.utf8decode(this.fileName);
249 this.fileComment = jszipProto.utf8decode(this.fileComment);
251 var upath = this.findExtraFieldUnicodePath();
252 if (upath !== null) {
253 this.fileName = upath;
255 var ucomment = this.findExtraFieldUnicodeComment();
256 if (ucomment !== null) {
257 this.fileComment = ucomment;
263 * Find the unicode path declared in the extra field, if any.
264 * @return {String} the unicode path, null otherwise.
266 findExtraFieldUnicodePath: function() {
267 var upathField = this.extraFields[0x7075];
269 var extraReader = new StringReader(upathField.value);
272 if (extraReader.readInt(1) !== 1) {
276 // the crc of the filename changed, this field is out of date.
277 if (jszipProto.crc32(this.fileName) !== extraReader.readInt(4)) {
281 return jszipProto.utf8decode(extraReader.readString(upathField.length - 5));
287 * Find the unicode comment declared in the extra field, if any.
288 * @return {String} the unicode comment, null otherwise.
290 findExtraFieldUnicodeComment: function() {
291 var ucommentField = this.extraFields[0x6375];
293 var extraReader = new StringReader(ucommentField.value);
296 if (extraReader.readInt(1) !== 1) {
300 // the crc of the comment changed, this field is out of date.
301 if (jszipProto.crc32(this.fileComment) !== extraReader.readInt(4)) {
305 return jszipProto.utf8decode(extraReader.readString(ucommentField.length - 5));
310 module.exports = ZipEntry;