decompress-zip.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. 'use strict';
  2. // The zip file spec is at http://www.pkware.com/documents/casestudies/APPNOTE.TXT
  3. // TODO: There is fair chunk of the spec that I have ignored. Need to add
  4. // assertions everywhere to make sure that we are not dealing with a ZIP type
  5. // that I haven't designed for. Things like spanning archives, non-DEFLATE
  6. // compression, encryption, etc.
  7. var fs = require('graceful-fs');
  8. var Q = require('q');
  9. var path = require('path');
  10. var util = require('util');
  11. var events = require('events');
  12. var structures = require('./structures');
  13. var signatures = require('./signatures');
  14. var extractors = require('./extractors');
  15. var FileDetails = require('./file-details');
  16. var fstat = Q.denodeify(fs.fstat);
  17. var read = Q.denodeify(fs.read);
  18. var fopen = Q.denodeify(fs.open);
  19. function DecompressZip(filename) {
  20. events.EventEmitter.call(this);
  21. this.filename = filename;
  22. this.stats = null;
  23. this.fd = null;
  24. this.chunkSize = 1024 * 1024; // Buffer up to 1Mb at a time
  25. this.dirCache = {};
  26. // When we need a resource, we should check if there is a promise for it
  27. // already and use that. If the promise is already fulfilled we don't do the
  28. // async work again and we get to queue up dependant tasks.
  29. this._p = {}; // _p instead of _promises because it is a lot easier to read
  30. }
  31. util.inherits(DecompressZip, events.EventEmitter);
  32. DecompressZip.prototype.openFile = function () {
  33. return fopen(this.filename, 'r');
  34. };
  35. DecompressZip.prototype.closeFile = function () {
  36. if (this.fd) {
  37. fs.closeSync(this.fd);
  38. this.fd = null;
  39. }
  40. };
  41. DecompressZip.prototype.statFile = function (fd) {
  42. this.fd = fd;
  43. return fstat(fd);
  44. };
  45. DecompressZip.prototype.list = function () {
  46. var self = this;
  47. this.getFiles()
  48. .then(function (files) {
  49. var result = [];
  50. files.forEach(function (file) {
  51. result.push(file.path);
  52. });
  53. self.emit('list', result);
  54. })
  55. .fail(function (error) {
  56. self.emit('error', error);
  57. })
  58. .fin(self.closeFile.bind(self));
  59. return this;
  60. };
  61. DecompressZip.prototype.extract = function (options) {
  62. var self = this;
  63. options = options || {};
  64. options.path = options.path || '.';
  65. options.filter = options.filter || null;
  66. options.follow = !!options.follow;
  67. options.strip = +options.strip || 0;
  68. this.getFiles()
  69. .then(function (files) {
  70. var copies = [];
  71. if (options.filter) {
  72. files = files.filter(options.filter);
  73. }
  74. if (options.follow) {
  75. copies = files.filter(function (file) {
  76. return file.type === 'SymbolicLink';
  77. });
  78. files = files.filter(function (file) {
  79. return file.type !== 'SymbolicLink';
  80. });
  81. }
  82. if (options.strip) {
  83. files = files.map(function (file) {
  84. if (file.type !== 'Directory') {
  85. // we don't use `path.sep` as we're using `/` in Windows too
  86. var dir = file.parent.split('/');
  87. var filename = file.filename;
  88. if (options.strip > dir.length) {
  89. throw new Error('You cannot strip more levels than there are directories');
  90. } else {
  91. dir = dir.slice(options.strip);
  92. }
  93. file.path = path.join(dir.join(path.sep), filename);
  94. return file;
  95. }
  96. });
  97. }
  98. return self.extractFiles(files, options)
  99. .then(self.extractFiles.bind(self, copies, options));
  100. })
  101. .then(function (results) {
  102. self.emit('extract', results);
  103. })
  104. .fail(function (error) {
  105. self.emit('error', error);
  106. })
  107. .fin(self.closeFile.bind(self));
  108. return this;
  109. };
  110. // Utility methods
  111. DecompressZip.prototype.getSearchBuffer = function (stats) {
  112. var size = Math.min(stats.size, this.chunkSize);
  113. this.stats = stats;
  114. return this.getBuffer(stats.size - size, stats.size);
  115. };
  116. DecompressZip.prototype.getBuffer = function (start, end) {
  117. var size = end - start;
  118. return read(this.fd, new Buffer(size), 0, size, start)
  119. .then(function (result) {
  120. return result[1];
  121. });
  122. };
  123. DecompressZip.prototype.findEndOfDirectory = function (buffer) {
  124. var index = buffer.length - 3;
  125. var chunk = '';
  126. // Apparently the ZIP spec is not very good and it is impossible to
  127. // guarantee that you have read a zip file correctly, or to determine
  128. // the location of the CD without hunting.
  129. // Search backwards through the buffer, as it is very likely to be near the
  130. // end of the file.
  131. while (index > Math.max(buffer.length - this.chunkSize, 0) && chunk !== signatures.END_OF_CENTRAL_DIRECTORY) {
  132. index--;
  133. chunk = buffer.readUInt32LE(index);
  134. }
  135. if (chunk !== signatures.END_OF_CENTRAL_DIRECTORY) {
  136. throw new Error('Could not find the End of Central Directory Record');
  137. }
  138. return buffer.slice(index);
  139. };
  140. // Directory here means the ZIP Central Directory, not a folder
  141. DecompressZip.prototype.readDirectory = function (recordBuffer) {
  142. var record = structures.readEndRecord(recordBuffer);
  143. return this.getBuffer(record.directoryOffset, record.directoryOffset + record.directorySize)
  144. .then(structures.readDirectory.bind(null));
  145. };
  146. DecompressZip.prototype.getFiles = function () {
  147. if (!this._p.getFiles) {
  148. this._p.getFiles = this.openFile()
  149. .then(this.statFile.bind(this))
  150. .then(this.getSearchBuffer.bind(this))
  151. .then(this.findEndOfDirectory.bind(this))
  152. .then(this.readDirectory.bind(this))
  153. .then(this.readFileEntries.bind(this));
  154. }
  155. return this._p.getFiles;
  156. };
  157. DecompressZip.prototype.readFileEntries = function (directory) {
  158. var promises = [];
  159. var files = [];
  160. var self = this;
  161. directory.forEach(function (directoryEntry, index) {
  162. var start = directoryEntry.relativeOffsetOfLocalHeader;
  163. var end = Math.min(self.stats.size, start + structures.maxFileEntrySize);
  164. var fileDetails = new FileDetails(directoryEntry);
  165. var promise = self.getBuffer(start, end)
  166. .then(structures.readFileEntry.bind(null))
  167. .then(function (fileEntry) {
  168. var maxSize;
  169. if (fileDetails.compressedSize > 0) {
  170. maxSize = fileDetails.compressedSize;
  171. } else {
  172. maxSize = self.stats.size;
  173. if (index < directory.length - 1) {
  174. maxSize = directory[index + 1].relativeOffsetOfLocalHeader;
  175. }
  176. maxSize -= start + fileEntry.entryLength;
  177. }
  178. fileDetails._offset = start + fileEntry.entryLength;
  179. fileDetails._maxSize = maxSize;
  180. self.emit('file', fileDetails);
  181. files[index] = fileDetails;
  182. });
  183. promises.push(promise);
  184. });
  185. return Q.all(promises)
  186. .then(function () {
  187. return files;
  188. });
  189. };
  190. DecompressZip.prototype.extractFiles = function (files, options, results) {
  191. var promises = [];
  192. var self = this;
  193. results = results || [];
  194. var fileIndex = 0;
  195. files.forEach(function (file) {
  196. var promise = self.extractFile(file, options)
  197. .then(function (result) {
  198. self.emit('progress', fileIndex++, files.length);
  199. results.push(result);
  200. });
  201. promises.push(promise);
  202. });
  203. return Q.all(promises)
  204. .then(function () {
  205. return results;
  206. });
  207. };
  208. DecompressZip.prototype.extractFile = function (file, options) {
  209. var destination = path.join(options.path, file.path);
  210. // Possible compression methods:
  211. // 0 - The file is stored (no compression)
  212. // 1 - The file is Shrunk
  213. // 2 - The file is Reduced with compression factor 1
  214. // 3 - The file is Reduced with compression factor 2
  215. // 4 - The file is Reduced with compression factor 3
  216. // 5 - The file is Reduced with compression factor 4
  217. // 6 - The file is Imploded
  218. // 7 - Reserved for Tokenizing compression algorithm
  219. // 8 - The file is Deflated
  220. // 9 - Enhanced Deflating using Deflate64(tm)
  221. // 10 - PKWARE Data Compression Library Imploding (old IBM TERSE)
  222. // 11 - Reserved by PKWARE
  223. // 12 - File is compressed using BZIP2 algorithm
  224. // 13 - Reserved by PKWARE
  225. // 14 - LZMA (EFS)
  226. // 15 - Reserved by PKWARE
  227. // 16 - Reserved by PKWARE
  228. // 17 - Reserved by PKWARE
  229. // 18 - File is compressed using IBM TERSE (new)
  230. // 19 - IBM LZ77 z Architecture (PFS)
  231. // 97 - WavPack compressed data
  232. // 98 - PPMd version I, Rev 1
  233. if (file.type === 'Directory') {
  234. return extractors.folder(file, destination, this);
  235. }
  236. if (file.type === 'File') {
  237. switch (file.compressionMethod) {
  238. case 0:
  239. return extractors.store(file, destination, this);
  240. case 8:
  241. return extractors.deflate(file, destination, this);
  242. default:
  243. throw new Error('Unsupported compression type');
  244. }
  245. }
  246. if (file.type === 'SymbolicLink') {
  247. if (options.follow) {
  248. return extractors.copy(file, destination, this, options.path);
  249. } else {
  250. return extractors.symlink(file, destination, this, options.path);
  251. }
  252. }
  253. throw new Error('Unsupported file type "' + file.type + '"');
  254. };
  255. module.exports = DecompressZip;