zipEntry.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. 'use strict';
  2. var StringReader = require('./stringReader');
  3. var utils = require('./utils');
  4. var CompressedObject = require('./compressedObject');
  5. var jszipProto = require('./object');
  6. // class ZipEntry {{{
  7. /**
  8. * An entry in the zip file.
  9. * @constructor
  10. * @param {Object} options Options of the current file.
  11. * @param {Object} loadOptions Options for loading the stream.
  12. */
  13. function ZipEntry(options, loadOptions) {
  14. this.options = options;
  15. this.loadOptions = loadOptions;
  16. }
  17. ZipEntry.prototype = {
  18. /**
  19. * say if the file is encrypted.
  20. * @return {boolean} true if the file is encrypted, false otherwise.
  21. */
  22. isEncrypted: function() {
  23. // bit 1 is set
  24. return (this.bitFlag & 0x0001) === 0x0001;
  25. },
  26. /**
  27. * say if the file has utf-8 filename/comment.
  28. * @return {boolean} true if the filename/comment is in utf-8, false otherwise.
  29. */
  30. useUTF8: function() {
  31. // bit 11 is set
  32. return (this.bitFlag & 0x0800) === 0x0800;
  33. },
  34. /**
  35. * Prepare the function used to generate the compressed content from this ZipFile.
  36. * @param {DataReader} reader the reader to use.
  37. * @param {number} from the offset from where we should read the data.
  38. * @param {number} length the length of the data to read.
  39. * @return {Function} the callback to get the compressed content (the type depends of the DataReader class).
  40. */
  41. prepareCompressedContent: function(reader, from, length) {
  42. return function() {
  43. var previousIndex = reader.index;
  44. reader.setIndex(from);
  45. var compressedFileData = reader.readData(length);
  46. reader.setIndex(previousIndex);
  47. return compressedFileData;
  48. };
  49. },
  50. /**
  51. * Prepare the function used to generate the uncompressed content from this ZipFile.
  52. * @param {DataReader} reader the reader to use.
  53. * @param {number} from the offset from where we should read the data.
  54. * @param {number} length the length of the data to read.
  55. * @param {JSZip.compression} compression the compression used on this file.
  56. * @param {number} uncompressedSize the uncompressed size to expect.
  57. * @return {Function} the callback to get the uncompressed content (the type depends of the DataReader class).
  58. */
  59. prepareContent: function(reader, from, length, compression, uncompressedSize) {
  60. return function() {
  61. var compressedFileData = utils.transformTo(compression.uncompressInputType, this.getCompressedContent());
  62. var uncompressedFileData = compression.uncompress(compressedFileData);
  63. if (uncompressedFileData.length !== uncompressedSize) {
  64. throw new Error("Bug : uncompressed data size mismatch");
  65. }
  66. return uncompressedFileData;
  67. };
  68. },
  69. /**
  70. * Read the local part of a zip file and add the info in this object.
  71. * @param {DataReader} reader the reader to use.
  72. */
  73. readLocalPart: function(reader) {
  74. var compression, localExtraFieldsLength;
  75. // we already know everything from the central dir !
  76. // If the central dir data are false, we are doomed.
  77. // On the bright side, the local part is scary : zip64, data descriptors, both, etc.
  78. // The less data we get here, the more reliable this should be.
  79. // Let's skip the whole header and dash to the data !
  80. reader.skip(22);
  81. // in some zip created on windows, the filename stored in the central dir contains \ instead of /.
  82. // Strangely, the filename here is OK.
  83. // I would love to treat these zip files as corrupted (see http://www.info-zip.org/FAQ.html#backslashes
  84. // or APPNOTE#4.4.17.1, "All slashes MUST be forward slashes '/'") but there are a lot of bad zip generators...
  85. // Search "unzip mismatching "local" filename continuing with "central" filename version" on
  86. // the internet.
  87. //
  88. // I think I see the logic here : the central directory is used to display
  89. // content and the local directory is used to extract the files. Mixing / and \
  90. // may be used to display \ to windows users and use / when extracting the files.
  91. // Unfortunately, this lead also to some issues : http://seclists.org/fulldisclosure/2009/Sep/394
  92. this.fileNameLength = reader.readInt(2);
  93. localExtraFieldsLength = reader.readInt(2); // can't be sure this will be the same as the central dir
  94. this.fileName = reader.readString(this.fileNameLength);
  95. reader.skip(localExtraFieldsLength);
  96. if (this.compressedSize == -1 || this.uncompressedSize == -1) {
  97. throw new Error("Bug or corrupted zip : didn't get enough informations from the central directory " + "(compressedSize == -1 || uncompressedSize == -1)");
  98. }
  99. compression = utils.findCompression(this.compressionMethod);
  100. if (compression === null) { // no compression found
  101. throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + this.fileName + ")");
  102. }
  103. this.decompressed = new CompressedObject();
  104. this.decompressed.compressedSize = this.compressedSize;
  105. this.decompressed.uncompressedSize = this.uncompressedSize;
  106. this.decompressed.crc32 = this.crc32;
  107. this.decompressed.compressionMethod = this.compressionMethod;
  108. this.decompressed.getCompressedContent = this.prepareCompressedContent(reader, reader.index, this.compressedSize, compression);
  109. this.decompressed.getContent = this.prepareContent(reader, reader.index, this.compressedSize, compression, this.uncompressedSize);
  110. // we need to compute the crc32...
  111. if (this.loadOptions.checkCRC32) {
  112. this.decompressed = utils.transformTo("string", this.decompressed.getContent());
  113. if (jszipProto.crc32(this.decompressed) !== this.crc32) {
  114. throw new Error("Corrupted zip : CRC32 mismatch");
  115. }
  116. }
  117. },
  118. /**
  119. * Read the central part of a zip file and add the info in this object.
  120. * @param {DataReader} reader the reader to use.
  121. */
  122. readCentralPart: function(reader) {
  123. this.versionMadeBy = reader.readString(2);
  124. this.versionNeeded = reader.readInt(2);
  125. this.bitFlag = reader.readInt(2);
  126. this.compressionMethod = reader.readString(2);
  127. this.date = reader.readDate();
  128. this.crc32 = reader.readInt(4);
  129. this.compressedSize = reader.readInt(4);
  130. this.uncompressedSize = reader.readInt(4);
  131. this.fileNameLength = reader.readInt(2);
  132. this.extraFieldsLength = reader.readInt(2);
  133. this.fileCommentLength = reader.readInt(2);
  134. this.diskNumberStart = reader.readInt(2);
  135. this.internalFileAttributes = reader.readInt(2);
  136. this.externalFileAttributes = reader.readInt(4);
  137. this.localHeaderOffset = reader.readInt(4);
  138. if (this.isEncrypted()) {
  139. throw new Error("Encrypted zip are not supported");
  140. }
  141. this.fileName = reader.readString(this.fileNameLength);
  142. this.readExtraFields(reader);
  143. this.parseZIP64ExtraField(reader);
  144. this.fileComment = reader.readString(this.fileCommentLength);
  145. // warning, this is true only for zip with madeBy == DOS (plateform dependent feature)
  146. this.dir = this.externalFileAttributes & 0x00000010 ? true : false;
  147. },
  148. /**
  149. * Parse the ZIP64 extra field and merge the info in the current ZipEntry.
  150. * @param {DataReader} reader the reader to use.
  151. */
  152. parseZIP64ExtraField: function(reader) {
  153. if (!this.extraFields[0x0001]) {
  154. return;
  155. }
  156. // should be something, preparing the extra reader
  157. var extraReader = new StringReader(this.extraFields[0x0001].value);
  158. // I really hope that these 64bits integer can fit in 32 bits integer, because js
  159. // won't let us have more.
  160. if (this.uncompressedSize === utils.MAX_VALUE_32BITS) {
  161. this.uncompressedSize = extraReader.readInt(8);
  162. }
  163. if (this.compressedSize === utils.MAX_VALUE_32BITS) {
  164. this.compressedSize = extraReader.readInt(8);
  165. }
  166. if (this.localHeaderOffset === utils.MAX_VALUE_32BITS) {
  167. this.localHeaderOffset = extraReader.readInt(8);
  168. }
  169. if (this.diskNumberStart === utils.MAX_VALUE_32BITS) {
  170. this.diskNumberStart = extraReader.readInt(4);
  171. }
  172. },
  173. /**
  174. * Read the central part of a zip file and add the info in this object.
  175. * @param {DataReader} reader the reader to use.
  176. */
  177. readExtraFields: function(reader) {
  178. var start = reader.index,
  179. extraFieldId,
  180. extraFieldLength,
  181. extraFieldValue;
  182. this.extraFields = this.extraFields || {};
  183. while (reader.index < start + this.extraFieldsLength) {
  184. extraFieldId = reader.readInt(2);
  185. extraFieldLength = reader.readInt(2);
  186. extraFieldValue = reader.readString(extraFieldLength);
  187. this.extraFields[extraFieldId] = {
  188. id: extraFieldId,
  189. length: extraFieldLength,
  190. value: extraFieldValue
  191. };
  192. }
  193. },
  194. /**
  195. * Apply an UTF8 transformation if needed.
  196. */
  197. handleUTF8: function() {
  198. if (this.useUTF8()) {
  199. this.fileName = jszipProto.utf8decode(this.fileName);
  200. this.fileComment = jszipProto.utf8decode(this.fileComment);
  201. } else {
  202. var upath = this.findExtraFieldUnicodePath();
  203. if (upath !== null) {
  204. this.fileName = upath;
  205. }
  206. var ucomment = this.findExtraFieldUnicodeComment();
  207. if (ucomment !== null) {
  208. this.fileComment = ucomment;
  209. }
  210. }
  211. },
  212. /**
  213. * Find the unicode path declared in the extra field, if any.
  214. * @return {String} the unicode path, null otherwise.
  215. */
  216. findExtraFieldUnicodePath: function() {
  217. var upathField = this.extraFields[0x7075];
  218. if (upathField) {
  219. var extraReader = new StringReader(upathField.value);
  220. // wrong version
  221. if (extraReader.readInt(1) !== 1) {
  222. return null;
  223. }
  224. // the crc of the filename changed, this field is out of date.
  225. if (jszipProto.crc32(this.fileName) !== extraReader.readInt(4)) {
  226. return null;
  227. }
  228. return jszipProto.utf8decode(extraReader.readString(upathField.length - 5));
  229. }
  230. return null;
  231. },
  232. /**
  233. * Find the unicode comment declared in the extra field, if any.
  234. * @return {String} the unicode comment, null otherwise.
  235. */
  236. findExtraFieldUnicodeComment: function() {
  237. var ucommentField = this.extraFields[0x6375];
  238. if (ucommentField) {
  239. var extraReader = new StringReader(ucommentField.value);
  240. // wrong version
  241. if (extraReader.readInt(1) !== 1) {
  242. return null;
  243. }
  244. // the crc of the comment changed, this field is out of date.
  245. if (jszipProto.crc32(this.fileComment) !== extraReader.readInt(4)) {
  246. return null;
  247. }
  248. return jszipProto.utf8decode(extraReader.readString(ucommentField.length - 5));
  249. }
  250. return null;
  251. }
  252. };
  253. module.exports = ZipEntry;