ZipFileWorker.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. 'use strict';
  2. var utils = require('../utils');
  3. var GenericWorker = require('../stream/GenericWorker');
  4. var utf8 = require('../utf8');
  5. var crc32 = require('../crc32');
  6. var signature = require('../signature');
  7. /**
  8. * Transform an integer into a string in hexadecimal.
  9. * @private
  10. * @param {number} dec the number to convert.
  11. * @param {number} bytes the number of bytes to generate.
  12. * @returns {string} the result.
  13. */
  14. var decToHex = function(dec, bytes) {
  15. var hex = "", i;
  16. for (i = 0; i < bytes; i++) {
  17. hex += String.fromCharCode(dec & 0xff);
  18. dec = dec >>> 8;
  19. }
  20. return hex;
  21. };
  22. /**
  23. * Generate the UNIX part of the external file attributes.
  24. * @param {Object} unixPermissions the unix permissions or null.
  25. * @param {Boolean} isDir true if the entry is a directory, false otherwise.
  26. * @return {Number} a 32 bit integer.
  27. *
  28. * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute :
  29. *
  30. * TTTTsstrwxrwxrwx0000000000ADVSHR
  31. * ^^^^____________________________ file type, see zipinfo.c (UNX_*)
  32. * ^^^_________________________ setuid, setgid, sticky
  33. * ^^^^^^^^^________________ permissions
  34. * ^^^^^^^^^^______ not used ?
  35. * ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only
  36. */
  37. var generateUnixExternalFileAttr = function (unixPermissions, isDir) {
  38. var result = unixPermissions;
  39. if (!unixPermissions) {
  40. // I can't use octal values in strict mode, hence the hexa.
  41. // 040775 => 0x41fd
  42. // 0100664 => 0x81b4
  43. result = isDir ? 0x41fd : 0x81b4;
  44. }
  45. return (result & 0xFFFF) << 16;
  46. };
  47. /**
  48. * Generate the DOS part of the external file attributes.
  49. * @param {Object} dosPermissions the dos permissions or null.
  50. * @param {Boolean} isDir true if the entry is a directory, false otherwise.
  51. * @return {Number} a 32 bit integer.
  52. *
  53. * Bit 0 Read-Only
  54. * Bit 1 Hidden
  55. * Bit 2 System
  56. * Bit 3 Volume Label
  57. * Bit 4 Directory
  58. * Bit 5 Archive
  59. */
  60. var generateDosExternalFileAttr = function (dosPermissions, isDir) {
  61. // the dir flag is already set for compatibility
  62. return (dosPermissions || 0) & 0x3F;
  63. };
  64. /**
  65. * Generate the various parts used in the construction of the final zip file.
  66. * @param {Object} streamInfo the hash with information about the compressed file.
  67. * @param {Boolean} streamedContent is the content streamed ?
  68. * @param {Boolean} streamingEnded is the stream finished ?
  69. * @param {number} offset the current offset from the start of the zip file.
  70. * @param {String} platform let's pretend we are this platform (change platform dependents fields)
  71. * @param {Function} encodeFileName the function to encode the file name / comment.
  72. * @return {Object} the zip parts.
  73. */
  74. var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) {
  75. var file = streamInfo['file'],
  76. compression = streamInfo['compression'],
  77. useCustomEncoding = encodeFileName !== utf8.utf8encode,
  78. encodedFileName = utils.transformTo("string", encodeFileName(file.name)),
  79. utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)),
  80. comment = file.comment,
  81. encodedComment = utils.transformTo("string", encodeFileName(comment)),
  82. utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)),
  83. useUTF8ForFileName = utfEncodedFileName.length !== file.name.length,
  84. useUTF8ForComment = utfEncodedComment.length !== comment.length,
  85. dosTime,
  86. dosDate,
  87. extraFields = "",
  88. unicodePathExtraField = "",
  89. unicodeCommentExtraField = "",
  90. dir = file.dir,
  91. date = file.date;
  92. var dataInfo = {
  93. crc32 : 0,
  94. compressedSize : 0,
  95. uncompressedSize : 0
  96. };
  97. // if the content is streamed, the sizes/crc32 are only available AFTER
  98. // the end of the stream.
  99. if (!streamedContent || streamingEnded) {
  100. dataInfo.crc32 = streamInfo['crc32'];
  101. dataInfo.compressedSize = streamInfo['compressedSize'];
  102. dataInfo.uncompressedSize = streamInfo['uncompressedSize'];
  103. }
  104. var bitflag = 0;
  105. if (streamedContent) {
  106. // Bit 3: the sizes/crc32 are set to zero in the local header.
  107. // The correct values are put in the data descriptor immediately
  108. // following the compressed data.
  109. bitflag |= 0x0008;
  110. }
  111. if (!useCustomEncoding && (useUTF8ForFileName || useUTF8ForComment)) {
  112. // Bit 11: Language encoding flag (EFS).
  113. bitflag |= 0x0800;
  114. }
  115. var extFileAttr = 0;
  116. var versionMadeBy = 0;
  117. if (dir) {
  118. // dos or unix, we set the dos dir flag
  119. extFileAttr |= 0x00010;
  120. }
  121. if(platform === "UNIX") {
  122. versionMadeBy = 0x031E; // UNIX, version 3.0
  123. extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir);
  124. } else { // DOS or other, fallback to DOS
  125. versionMadeBy = 0x0014; // DOS, version 2.0
  126. extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir);
  127. }
  128. // date
  129. // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html
  130. // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html
  131. // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html
  132. dosTime = date.getUTCHours();
  133. dosTime = dosTime << 6;
  134. dosTime = dosTime | date.getUTCMinutes();
  135. dosTime = dosTime << 5;
  136. dosTime = dosTime | date.getUTCSeconds() / 2;
  137. dosDate = date.getUTCFullYear() - 1980;
  138. dosDate = dosDate << 4;
  139. dosDate = dosDate | (date.getUTCMonth() + 1);
  140. dosDate = dosDate << 5;
  141. dosDate = dosDate | date.getUTCDate();
  142. if (useUTF8ForFileName) {
  143. // set the unicode path extra field. unzip needs at least one extra
  144. // field to correctly handle unicode path, so using the path is as good
  145. // as any other information. This could improve the situation with
  146. // other archive managers too.
  147. // This field is usually used without the utf8 flag, with a non
  148. // unicode path in the header (winrar, winzip). This helps (a bit)
  149. // with the messy Windows' default compressed folders feature but
  150. // breaks on p7zip which doesn't seek the unicode path extra field.
  151. // So for now, UTF-8 everywhere !
  152. unicodePathExtraField =
  153. // Version
  154. decToHex(1, 1) +
  155. // NameCRC32
  156. decToHex(crc32(encodedFileName), 4) +
  157. // UnicodeName
  158. utfEncodedFileName;
  159. extraFields +=
  160. // Info-ZIP Unicode Path Extra Field
  161. "\x75\x70" +
  162. // size
  163. decToHex(unicodePathExtraField.length, 2) +
  164. // content
  165. unicodePathExtraField;
  166. }
  167. if(useUTF8ForComment) {
  168. unicodeCommentExtraField =
  169. // Version
  170. decToHex(1, 1) +
  171. // CommentCRC32
  172. decToHex(crc32(encodedComment), 4) +
  173. // UnicodeName
  174. utfEncodedComment;
  175. extraFields +=
  176. // Info-ZIP Unicode Path Extra Field
  177. "\x75\x63" +
  178. // size
  179. decToHex(unicodeCommentExtraField.length, 2) +
  180. // content
  181. unicodeCommentExtraField;
  182. }
  183. var header = "";
  184. // version needed to extract
  185. header += "\x0A\x00";
  186. // general purpose bit flag
  187. header += decToHex(bitflag, 2);
  188. // compression method
  189. header += compression.magic;
  190. // last mod file time
  191. header += decToHex(dosTime, 2);
  192. // last mod file date
  193. header += decToHex(dosDate, 2);
  194. // crc-32
  195. header += decToHex(dataInfo.crc32, 4);
  196. // compressed size
  197. header += decToHex(dataInfo.compressedSize, 4);
  198. // uncompressed size
  199. header += decToHex(dataInfo.uncompressedSize, 4);
  200. // file name length
  201. header += decToHex(encodedFileName.length, 2);
  202. // extra field length
  203. header += decToHex(extraFields.length, 2);
  204. var fileRecord = signature.LOCAL_FILE_HEADER + header + encodedFileName + extraFields;
  205. var dirRecord = signature.CENTRAL_FILE_HEADER +
  206. // version made by (00: DOS)
  207. decToHex(versionMadeBy, 2) +
  208. // file header (common to file and central directory)
  209. header +
  210. // file comment length
  211. decToHex(encodedComment.length, 2) +
  212. // disk number start
  213. "\x00\x00" +
  214. // internal file attributes TODO
  215. "\x00\x00" +
  216. // external file attributes
  217. decToHex(extFileAttr, 4) +
  218. // relative offset of local header
  219. decToHex(offset, 4) +
  220. // file name
  221. encodedFileName +
  222. // extra field
  223. extraFields +
  224. // file comment
  225. encodedComment;
  226. return {
  227. fileRecord: fileRecord,
  228. dirRecord: dirRecord
  229. };
  230. };
  231. /**
  232. * Generate the EOCD record.
  233. * @param {Number} entriesCount the number of entries in the zip file.
  234. * @param {Number} centralDirLength the length (in bytes) of the central dir.
  235. * @param {Number} localDirLength the length (in bytes) of the local dir.
  236. * @param {String} comment the zip file comment as a binary string.
  237. * @param {Function} encodeFileName the function to encode the comment.
  238. * @return {String} the EOCD record.
  239. */
  240. var generateCentralDirectoryEnd = function (entriesCount, centralDirLength, localDirLength, comment, encodeFileName) {
  241. var dirEnd = "";
  242. var encodedComment = utils.transformTo("string", encodeFileName(comment));
  243. // end of central dir signature
  244. dirEnd = signature.CENTRAL_DIRECTORY_END +
  245. // number of this disk
  246. "\x00\x00" +
  247. // number of the disk with the start of the central directory
  248. "\x00\x00" +
  249. // total number of entries in the central directory on this disk
  250. decToHex(entriesCount, 2) +
  251. // total number of entries in the central directory
  252. decToHex(entriesCount, 2) +
  253. // size of the central directory 4 bytes
  254. decToHex(centralDirLength, 4) +
  255. // offset of start of central directory with respect to the starting disk number
  256. decToHex(localDirLength, 4) +
  257. // .ZIP file comment length
  258. decToHex(encodedComment.length, 2) +
  259. // .ZIP file comment
  260. encodedComment;
  261. return dirEnd;
  262. };
  263. /**
  264. * Generate data descriptors for a file entry.
  265. * @param {Object} streamInfo the hash generated by a worker, containing information
  266. * on the file entry.
  267. * @return {String} the data descriptors.
  268. */
  269. var generateDataDescriptors = function (streamInfo) {
  270. var descriptor = "";
  271. descriptor = signature.DATA_DESCRIPTOR +
  272. // crc-32 4 bytes
  273. decToHex(streamInfo['crc32'], 4) +
  274. // compressed size 4 bytes
  275. decToHex(streamInfo['compressedSize'], 4) +
  276. // uncompressed size 4 bytes
  277. decToHex(streamInfo['uncompressedSize'], 4);
  278. return descriptor;
  279. };
  280. /**
  281. * A worker to concatenate other workers to create a zip file.
  282. * @param {Boolean} streamFiles `true` to stream the content of the files,
  283. * `false` to accumulate it.
  284. * @param {String} comment the comment to use.
  285. * @param {String} platform the platform to use, "UNIX" or "DOS".
  286. * @param {Function} encodeFileName the function to encode file names and comments.
  287. */
  288. function ZipFileWorker(streamFiles, comment, platform, encodeFileName) {
  289. GenericWorker.call(this, "ZipFileWorker");
  290. // The number of bytes written so far. This doesn't count accumulated chunks.
  291. this.bytesWritten = 0;
  292. // The comment of the zip file
  293. this.zipComment = comment;
  294. // The platform "generating" the zip file.
  295. this.zipPlatform = platform;
  296. // the function to encode file names and comments.
  297. this.encodeFileName = encodeFileName;
  298. // Should we stream the content of the files ?
  299. this.streamFiles = streamFiles;
  300. // If `streamFiles` is false, we will need to accumulate the content of the
  301. // files to calculate sizes / crc32 (and write them *before* the content).
  302. // This boolean indicates if we are accumulating chunks (it will change a lot
  303. // during the lifetime of this worker).
  304. this.accumulate = false;
  305. // The buffer receiving chunks when accumulating content.
  306. this.contentBuffer = [];
  307. // The list of generated directory records.
  308. this.dirRecords = [];
  309. // The offset (in bytes) from the beginning of the zip file for the current source.
  310. this.currentSourceOffset = 0;
  311. // The total number of entries in this zip file.
  312. this.entriesCount = 0;
  313. // the name of the file currently being added, null when handling the end of the zip file.
  314. // Used for the emitted metadata.
  315. this.currentFile = null;
  316. this._sources = [];
  317. }
  318. utils.inherits(ZipFileWorker, GenericWorker);
  319. /**
  320. * @see GenericWorker.push
  321. */
  322. ZipFileWorker.prototype.push = function (chunk) {
  323. var currentFilePercent = chunk.meta.percent || 0;
  324. var entriesCount = this.entriesCount;
  325. var remainingFiles = this._sources.length;
  326. if(this.accumulate) {
  327. this.contentBuffer.push(chunk);
  328. } else {
  329. this.bytesWritten += chunk.data.length;
  330. GenericWorker.prototype.push.call(this, {
  331. data : chunk.data,
  332. meta : {
  333. currentFile : this.currentFile,
  334. percent : entriesCount ? (currentFilePercent + 100 * (entriesCount - remainingFiles - 1)) / entriesCount : 100
  335. }
  336. });
  337. }
  338. };
  339. /**
  340. * The worker started a new source (an other worker).
  341. * @param {Object} streamInfo the streamInfo object from the new source.
  342. */
  343. ZipFileWorker.prototype.openedSource = function (streamInfo) {
  344. this.currentSourceOffset = this.bytesWritten;
  345. this.currentFile = streamInfo['file'].name;
  346. var streamedContent = this.streamFiles && !streamInfo['file'].dir;
  347. // don't stream folders (because they don't have any content)
  348. if(streamedContent) {
  349. var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
  350. this.push({
  351. data : record.fileRecord,
  352. meta : {percent:0}
  353. });
  354. } else {
  355. // we need to wait for the whole file before pushing anything
  356. this.accumulate = true;
  357. }
  358. };
  359. /**
  360. * The worker finished a source (an other worker).
  361. * @param {Object} streamInfo the streamInfo object from the finished source.
  362. */
  363. ZipFileWorker.prototype.closedSource = function (streamInfo) {
  364. this.accumulate = false;
  365. var streamedContent = this.streamFiles && !streamInfo['file'].dir;
  366. var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
  367. this.dirRecords.push(record.dirRecord);
  368. if(streamedContent) {
  369. // after the streamed file, we put data descriptors
  370. this.push({
  371. data : generateDataDescriptors(streamInfo),
  372. meta : {percent:100}
  373. });
  374. } else {
  375. // the content wasn't streamed, we need to push everything now
  376. // first the file record, then the content
  377. this.push({
  378. data : record.fileRecord,
  379. meta : {percent:0}
  380. });
  381. while(this.contentBuffer.length) {
  382. this.push(this.contentBuffer.shift());
  383. }
  384. }
  385. this.currentFile = null;
  386. };
  387. /**
  388. * @see GenericWorker.flush
  389. */
  390. ZipFileWorker.prototype.flush = function () {
  391. var localDirLength = this.bytesWritten;
  392. for(var i = 0; i < this.dirRecords.length; i++) {
  393. this.push({
  394. data : this.dirRecords[i],
  395. meta : {percent:100}
  396. });
  397. }
  398. var centralDirLength = this.bytesWritten - localDirLength;
  399. var dirEnd = generateCentralDirectoryEnd(this.dirRecords.length, centralDirLength, localDirLength, this.zipComment, this.encodeFileName);
  400. this.push({
  401. data : dirEnd,
  402. meta : {percent:100}
  403. });
  404. };
  405. /**
  406. * Prepare the next source to be read.
  407. */
  408. ZipFileWorker.prototype.prepareNextSource = function () {
  409. this.previous = this._sources.shift();
  410. this.openedSource(this.previous.streamInfo);
  411. if (this.isPaused) {
  412. this.previous.pause();
  413. } else {
  414. this.previous.resume();
  415. }
  416. };
  417. /**
  418. * @see GenericWorker.registerPrevious
  419. */
  420. ZipFileWorker.prototype.registerPrevious = function (previous) {
  421. this._sources.push(previous);
  422. var self = this;
  423. previous.on('data', function (chunk) {
  424. self.processChunk(chunk);
  425. });
  426. previous.on('end', function () {
  427. self.closedSource(self.previous.streamInfo);
  428. if(self._sources.length) {
  429. self.prepareNextSource();
  430. } else {
  431. self.end();
  432. }
  433. });
  434. previous.on('error', function (e) {
  435. self.error(e);
  436. });
  437. return this;
  438. };
  439. /**
  440. * @see GenericWorker.resume
  441. */
  442. ZipFileWorker.prototype.resume = function () {
  443. if(!GenericWorker.prototype.resume.call(this)) {
  444. return false;
  445. }
  446. if (!this.previous && this._sources.length) {
  447. this.prepareNextSource();
  448. return true;
  449. }
  450. if (!this.previous && !this._sources.length && !this.generatedError) {
  451. this.end();
  452. return true;
  453. }
  454. };
  455. /**
  456. * @see GenericWorker.error
  457. */
  458. ZipFileWorker.prototype.error = function (e) {
  459. var sources = this._sources;
  460. if(!GenericWorker.prototype.error.call(this, e)) {
  461. return false;
  462. }
  463. for(var i = 0; i < sources.length; i++) {
  464. try {
  465. sources[i].error(e);
  466. } catch(e) {
  467. // the `error` exploded, nothing to do
  468. }
  469. }
  470. return true;
  471. };
  472. /**
  473. * @see GenericWorker.lock
  474. */
  475. ZipFileWorker.prototype.lock = function () {
  476. GenericWorker.prototype.lock.call(this);
  477. var sources = this._sources;
  478. for(var i = 0; i < sources.length; i++) {
  479. sources[i].lock();
  480. }
  481. };
  482. module.exports = ZipFileWorker;