zipEntry.js 13 KB

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