zipFile.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. const ZipEntry = require("./zipEntry");
  2. const Headers = require("./headers");
  3. const Utils = require("./util");
  4. module.exports = function (/*Buffer|null*/ inBuffer, /** object */ options) {
  5. var entryList = [],
  6. entryTable = {},
  7. _comment = Buffer.alloc(0),
  8. mainHeader = new Headers.MainHeader(),
  9. loadedEntries = false;
  10. // assign options
  11. const opts = Object.assign(Object.create(null), options);
  12. const { noSort } = opts;
  13. if (inBuffer) {
  14. // is a memory buffer
  15. readMainHeader(opts.readEntries);
  16. } else {
  17. // none. is a new file
  18. loadedEntries = true;
  19. }
  20. function iterateEntries(callback) {
  21. const totalEntries = mainHeader.diskEntries; // total number of entries
  22. let index = mainHeader.offset; // offset of first CEN header
  23. for (let i = 0; i < totalEntries; i++) {
  24. let tmp = index;
  25. const entry = new ZipEntry(inBuffer);
  26. entry.header = inBuffer.slice(tmp, (tmp += Utils.Constants.CENHDR));
  27. entry.entryName = inBuffer.slice(tmp, (tmp += entry.header.fileNameLength));
  28. index += entry.header.entryHeaderSize;
  29. callback(entry);
  30. }
  31. }
  32. function readEntries() {
  33. loadedEntries = true;
  34. entryTable = {};
  35. entryList = new Array(mainHeader.diskEntries); // total number of entries
  36. var index = mainHeader.offset; // offset of first CEN header
  37. for (var i = 0; i < entryList.length; i++) {
  38. var tmp = index,
  39. entry = new ZipEntry(inBuffer);
  40. entry.header = inBuffer.slice(tmp, (tmp += Utils.Constants.CENHDR));
  41. entry.entryName = inBuffer.slice(tmp, (tmp += entry.header.fileNameLength));
  42. if (entry.header.extraLength) {
  43. entry.extra = inBuffer.slice(tmp, (tmp += entry.header.extraLength));
  44. }
  45. if (entry.header.commentLength) entry.comment = inBuffer.slice(tmp, tmp + entry.header.commentLength);
  46. index += entry.header.entryHeaderSize;
  47. entryList[i] = entry;
  48. entryTable[entry.entryName] = entry;
  49. }
  50. }
  51. function readMainHeader(/*Boolean*/ readNow) {
  52. var i = inBuffer.length - Utils.Constants.ENDHDR, // END header size
  53. max = Math.max(0, i - 0xffff), // 0xFFFF is the max zip file comment length
  54. n = max,
  55. endStart = inBuffer.length,
  56. endOffset = -1, // Start offset of the END header
  57. commentEnd = 0;
  58. for (i; i >= n; i--) {
  59. if (inBuffer[i] !== 0x50) continue; // quick check that the byte is 'P'
  60. if (inBuffer.readUInt32LE(i) === Utils.Constants.ENDSIG) {
  61. // "PK\005\006"
  62. endOffset = i;
  63. commentEnd = i;
  64. endStart = i + Utils.Constants.ENDHDR;
  65. // We already found a regular signature, let's look just a bit further to check if there's any zip64 signature
  66. n = i - Utils.Constants.END64HDR;
  67. continue;
  68. }
  69. if (inBuffer.readUInt32LE(i) === Utils.Constants.END64SIG) {
  70. // Found a zip64 signature, let's continue reading the whole zip64 record
  71. n = max;
  72. continue;
  73. }
  74. if (inBuffer.readUInt32LE(i) === Utils.Constants.ZIP64SIG) {
  75. // Found the zip64 record, let's determine it's size
  76. endOffset = i;
  77. endStart = i + Utils.readBigUInt64LE(inBuffer, i + Utils.Constants.ZIP64SIZE) + Utils.Constants.ZIP64LEAD;
  78. break;
  79. }
  80. }
  81. if (!~endOffset) throw new Error(Utils.Errors.INVALID_FORMAT);
  82. mainHeader.loadFromBinary(inBuffer.slice(endOffset, endStart));
  83. if (mainHeader.commentLength) {
  84. _comment = inBuffer.slice(commentEnd + Utils.Constants.ENDHDR);
  85. }
  86. if (readNow) readEntries();
  87. }
  88. function sortEntries() {
  89. if (entryList.length > 1 && !noSort) {
  90. entryList.sort((a, b) => a.entryName.toLowerCase().localeCompare(b.entryName.toLowerCase()));
  91. }
  92. }
  93. return {
  94. /**
  95. * Returns an array of ZipEntry objects existent in the current opened archive
  96. * @return Array
  97. */
  98. get entries() {
  99. if (!loadedEntries) {
  100. readEntries();
  101. }
  102. return entryList;
  103. },
  104. /**
  105. * Archive comment
  106. * @return {String}
  107. */
  108. get comment() {
  109. return _comment.toString();
  110. },
  111. set comment(val) {
  112. _comment = Utils.toBuffer(val);
  113. mainHeader.commentLength = _comment.length;
  114. },
  115. getEntryCount: function () {
  116. if (!loadedEntries) {
  117. return mainHeader.diskEntries;
  118. }
  119. return entryList.length;
  120. },
  121. forEach: function (callback) {
  122. if (!loadedEntries) {
  123. iterateEntries(callback);
  124. return;
  125. }
  126. entryList.forEach(callback);
  127. },
  128. /**
  129. * Returns a reference to the entry with the given name or null if entry is inexistent
  130. *
  131. * @param entryName
  132. * @return ZipEntry
  133. */
  134. getEntry: function (/*String*/ entryName) {
  135. if (!loadedEntries) {
  136. readEntries();
  137. }
  138. return entryTable[entryName] || null;
  139. },
  140. /**
  141. * Adds the given entry to the entry list
  142. *
  143. * @param entry
  144. */
  145. setEntry: function (/*ZipEntry*/ entry) {
  146. if (!loadedEntries) {
  147. readEntries();
  148. }
  149. entryList.push(entry);
  150. entryTable[entry.entryName] = entry;
  151. mainHeader.totalEntries = entryList.length;
  152. },
  153. /**
  154. * Removes the entry with the given name from the entry list.
  155. *
  156. * If the entry is a directory, then all nested files and directories will be removed
  157. * @param entryName
  158. */
  159. deleteEntry: function (/*String*/ entryName) {
  160. if (!loadedEntries) {
  161. readEntries();
  162. }
  163. var entry = entryTable[entryName];
  164. if (entry && entry.isDirectory) {
  165. var _self = this;
  166. this.getEntryChildren(entry).forEach(function (child) {
  167. if (child.entryName !== entryName) {
  168. _self.deleteEntry(child.entryName);
  169. }
  170. });
  171. }
  172. entryList.splice(entryList.indexOf(entry), 1);
  173. delete entryTable[entryName];
  174. mainHeader.totalEntries = entryList.length;
  175. },
  176. /**
  177. * Iterates and returns all nested files and directories of the given entry
  178. *
  179. * @param entry
  180. * @return Array
  181. */
  182. getEntryChildren: function (/*ZipEntry*/ entry) {
  183. if (!loadedEntries) {
  184. readEntries();
  185. }
  186. if (entry && entry.isDirectory) {
  187. const list = [];
  188. const name = entry.entryName;
  189. const len = name.length;
  190. entryList.forEach(function (zipEntry) {
  191. if (zipEntry.entryName.substr(0, len) === name) {
  192. list.push(zipEntry);
  193. }
  194. });
  195. return list;
  196. }
  197. return [];
  198. },
  199. /**
  200. * Returns the zip file
  201. *
  202. * @return Buffer
  203. */
  204. compressToBuffer: function () {
  205. if (!loadedEntries) {
  206. readEntries();
  207. }
  208. sortEntries();
  209. const dataBlock = [];
  210. const entryHeaders = [];
  211. let totalSize = 0;
  212. let dindex = 0;
  213. mainHeader.size = 0;
  214. mainHeader.offset = 0;
  215. for (const entry of entryList) {
  216. // compress data and set local and entry header accordingly. Reason why is called first
  217. const compressedData = entry.getCompressedData();
  218. // 1. construct data header
  219. entry.header.offset = dindex;
  220. const dataHeader = entry.header.dataHeaderToBinary();
  221. const entryNameLen = entry.rawEntryName.length;
  222. // 1.2. postheader - data after data header
  223. const postHeader = Buffer.alloc(entryNameLen + entry.extra.length);
  224. entry.rawEntryName.copy(postHeader, 0);
  225. postHeader.copy(entry.extra, entryNameLen);
  226. // 2. offsets
  227. const dataLength = dataHeader.length + postHeader.length + compressedData.length;
  228. dindex += dataLength;
  229. // 3. store values in sequence
  230. dataBlock.push(dataHeader);
  231. dataBlock.push(postHeader);
  232. dataBlock.push(compressedData);
  233. // 4. construct entry header
  234. const entryHeader = entry.packHeader();
  235. entryHeaders.push(entryHeader);
  236. // 5. update main header
  237. mainHeader.size += entryHeader.length;
  238. totalSize += dataLength + entryHeader.length;
  239. }
  240. totalSize += mainHeader.mainHeaderSize; // also includes zip file comment length
  241. // point to end of data and beginning of central directory first record
  242. mainHeader.offset = dindex;
  243. dindex = 0;
  244. const outBuffer = Buffer.alloc(totalSize);
  245. // write data blocks
  246. for (const content of dataBlock) {
  247. content.copy(outBuffer, dindex);
  248. dindex += content.length;
  249. }
  250. // write central directory entries
  251. for (const content of entryHeaders) {
  252. content.copy(outBuffer, dindex);
  253. dindex += content.length;
  254. }
  255. // write main header
  256. const mh = mainHeader.toBinary();
  257. if (_comment) {
  258. _comment.copy(mh, Utils.Constants.ENDHDR); // add zip file comment
  259. }
  260. mh.copy(outBuffer, dindex);
  261. return outBuffer;
  262. },
  263. toAsyncBuffer: function (/*Function*/ onSuccess, /*Function*/ onFail, /*Function*/ onItemStart, /*Function*/ onItemEnd) {
  264. try {
  265. if (!loadedEntries) {
  266. readEntries();
  267. }
  268. sortEntries();
  269. const dataBlock = [];
  270. const entryHeaders = [];
  271. let totalSize = 0;
  272. let dindex = 0;
  273. mainHeader.size = 0;
  274. mainHeader.offset = 0;
  275. const compress2Buffer = function (entryLists) {
  276. if (entryLists.length) {
  277. const entry = entryLists.pop();
  278. const name = entry.entryName + entry.extra.toString();
  279. if (onItemStart) onItemStart(name);
  280. entry.getCompressedDataAsync(function (compressedData) {
  281. if (onItemEnd) onItemEnd(name);
  282. entry.header.offset = dindex;
  283. // data header
  284. const dataHeader = entry.header.dataHeaderToBinary();
  285. const postHeader = Buffer.alloc(name.length, name);
  286. const dataLength = dataHeader.length + postHeader.length + compressedData.length;
  287. dindex += dataLength;
  288. dataBlock.push(dataHeader);
  289. dataBlock.push(postHeader);
  290. dataBlock.push(compressedData);
  291. const entryHeader = entry.packHeader();
  292. entryHeaders.push(entryHeader);
  293. mainHeader.size += entryHeader.length;
  294. totalSize += dataLength + entryHeader.length;
  295. compress2Buffer(entryLists);
  296. });
  297. } else {
  298. totalSize += mainHeader.mainHeaderSize; // also includes zip file comment length
  299. // point to end of data and beginning of central directory first record
  300. mainHeader.offset = dindex;
  301. dindex = 0;
  302. const outBuffer = Buffer.alloc(totalSize);
  303. dataBlock.forEach(function (content) {
  304. content.copy(outBuffer, dindex); // write data blocks
  305. dindex += content.length;
  306. });
  307. entryHeaders.forEach(function (content) {
  308. content.copy(outBuffer, dindex); // write central directory entries
  309. dindex += content.length;
  310. });
  311. const mh = mainHeader.toBinary();
  312. if (_comment) {
  313. _comment.copy(mh, Utils.Constants.ENDHDR); // add zip file comment
  314. }
  315. mh.copy(outBuffer, dindex); // write main header
  316. onSuccess(outBuffer);
  317. }
  318. };
  319. compress2Buffer(entryList);
  320. } catch (e) {
  321. onFail(e);
  322. }
  323. }
  324. };
  325. };