zipFile.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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. var password = null;
  11. const temporary = new Set();
  12. // assign options
  13. const opts = options;
  14. const { noSort, decoder } = opts;
  15. if (inBuffer) {
  16. // is a memory buffer
  17. readMainHeader(opts.readEntries);
  18. } else {
  19. // none. is a new file
  20. loadedEntries = true;
  21. }
  22. function makeTemporaryFolders() {
  23. const foldersList = new Set();
  24. // Make list of all folders in file
  25. for (const elem of Object.keys(entryTable)) {
  26. const elements = elem.split("/");
  27. elements.pop(); // filename
  28. if (!elements.length) continue; // no folders
  29. for (let i = 0; i < elements.length; i++) {
  30. const sub = elements.slice(0, i + 1).join("/") + "/";
  31. foldersList.add(sub);
  32. }
  33. }
  34. // create missing folders as temporary
  35. for (const elem of foldersList) {
  36. if (!(elem in entryTable)) {
  37. const tempfolder = new ZipEntry(opts);
  38. tempfolder.entryName = elem;
  39. tempfolder.attr = 0x10;
  40. tempfolder.temporary = true;
  41. entryList.push(tempfolder);
  42. entryTable[tempfolder.entryName] = tempfolder;
  43. temporary.add(tempfolder);
  44. }
  45. }
  46. }
  47. function readEntries() {
  48. loadedEntries = true;
  49. entryTable = {};
  50. if (mainHeader.diskEntries > (inBuffer.length - mainHeader.offset) / Utils.Constants.CENHDR) {
  51. throw Utils.Errors.DISK_ENTRY_TOO_LARGE();
  52. }
  53. entryList = new Array(mainHeader.diskEntries); // total number of entries
  54. var index = mainHeader.offset; // offset of first CEN header
  55. for (var i = 0; i < entryList.length; i++) {
  56. var tmp = index,
  57. entry = new ZipEntry(opts, inBuffer);
  58. entry.header = inBuffer.slice(tmp, (tmp += Utils.Constants.CENHDR));
  59. entry.entryName = inBuffer.slice(tmp, (tmp += entry.header.fileNameLength));
  60. if (entry.header.extraLength) {
  61. entry.extra = inBuffer.slice(tmp, (tmp += entry.header.extraLength));
  62. }
  63. if (entry.header.commentLength) entry.comment = inBuffer.slice(tmp, tmp + entry.header.commentLength);
  64. index += entry.header.centralHeaderSize;
  65. entryList[i] = entry;
  66. entryTable[entry.entryName] = entry;
  67. }
  68. temporary.clear();
  69. makeTemporaryFolders();
  70. }
  71. function readMainHeader(/*Boolean*/ readNow) {
  72. var i = inBuffer.length - Utils.Constants.ENDHDR, // END header size
  73. max = Math.max(0, i - 0xffff), // 0xFFFF is the max zip file comment length
  74. n = max,
  75. endStart = inBuffer.length,
  76. endOffset = -1, // Start offset of the END header
  77. commentEnd = 0;
  78. // option to search header form entire file
  79. const trailingSpace = typeof opts.trailingSpace === "boolean" ? opts.trailingSpace : false;
  80. if (trailingSpace) max = 0;
  81. for (i; i >= n; i--) {
  82. if (inBuffer[i] !== 0x50) continue; // quick check that the byte is 'P'
  83. if (inBuffer.readUInt32LE(i) === Utils.Constants.ENDSIG) {
  84. // "PK\005\006"
  85. endOffset = i;
  86. commentEnd = i;
  87. endStart = i + Utils.Constants.ENDHDR;
  88. // We already found a regular signature, let's look just a bit further to check if there's any zip64 signature
  89. n = i - Utils.Constants.END64HDR;
  90. continue;
  91. }
  92. if (inBuffer.readUInt32LE(i) === Utils.Constants.END64SIG) {
  93. // Found a zip64 signature, let's continue reading the whole zip64 record
  94. n = max;
  95. continue;
  96. }
  97. if (inBuffer.readUInt32LE(i) === Utils.Constants.ZIP64SIG) {
  98. // Found the zip64 record, let's determine it's size
  99. endOffset = i;
  100. endStart = i + Utils.readBigUInt64LE(inBuffer, i + Utils.Constants.ZIP64SIZE) + Utils.Constants.ZIP64LEAD;
  101. break;
  102. }
  103. }
  104. if (endOffset == -1) throw Utils.Errors.INVALID_FORMAT();
  105. mainHeader.loadFromBinary(inBuffer.slice(endOffset, endStart));
  106. if (mainHeader.commentLength) {
  107. _comment = inBuffer.slice(commentEnd + Utils.Constants.ENDHDR);
  108. }
  109. if (readNow) readEntries();
  110. }
  111. function sortEntries() {
  112. if (entryList.length > 1 && !noSort) {
  113. entryList.sort((a, b) => a.entryName.toLowerCase().localeCompare(b.entryName.toLowerCase()));
  114. }
  115. }
  116. return {
  117. /**
  118. * Returns an array of ZipEntry objects existent in the current opened archive
  119. * @return Array
  120. */
  121. get entries() {
  122. if (!loadedEntries) {
  123. readEntries();
  124. }
  125. return entryList.filter((e) => !temporary.has(e));
  126. },
  127. /**
  128. * Archive comment
  129. * @return {String}
  130. */
  131. get comment() {
  132. return decoder.decode(_comment);
  133. },
  134. set comment(val) {
  135. _comment = Utils.toBuffer(val, decoder.encode);
  136. mainHeader.commentLength = _comment.length;
  137. },
  138. getEntryCount: function () {
  139. if (!loadedEntries) {
  140. return mainHeader.diskEntries;
  141. }
  142. return entryList.length;
  143. },
  144. forEach: function (callback) {
  145. this.entries.forEach(callback);
  146. },
  147. /**
  148. * Returns a reference to the entry with the given name or null if entry is inexistent
  149. *
  150. * @param entryName
  151. * @return ZipEntry
  152. */
  153. getEntry: function (/*String*/ entryName) {
  154. if (!loadedEntries) {
  155. readEntries();
  156. }
  157. return entryTable[entryName] || null;
  158. },
  159. /**
  160. * Adds the given entry to the entry list
  161. *
  162. * @param entry
  163. */
  164. setEntry: function (/*ZipEntry*/ entry) {
  165. if (!loadedEntries) {
  166. readEntries();
  167. }
  168. entryList.push(entry);
  169. entryTable[entry.entryName] = entry;
  170. mainHeader.totalEntries = entryList.length;
  171. },
  172. /**
  173. * Removes the file with the given name from the entry list.
  174. *
  175. * If the entry is a directory, then all nested files and directories will be removed
  176. * @param entryName
  177. * @returns {void}
  178. */
  179. deleteFile: function (/*String*/ entryName, withsubfolders = true) {
  180. if (!loadedEntries) {
  181. readEntries();
  182. }
  183. const entry = entryTable[entryName];
  184. const list = this.getEntryChildren(entry, withsubfolders).map((child) => child.entryName);
  185. list.forEach(this.deleteEntry);
  186. },
  187. /**
  188. * Removes the entry with the given name from the entry list.
  189. *
  190. * @param {string} entryName
  191. * @returns {void}
  192. */
  193. deleteEntry: function (/*String*/ entryName) {
  194. if (!loadedEntries) {
  195. readEntries();
  196. }
  197. const entry = entryTable[entryName];
  198. const index = entryList.indexOf(entry);
  199. if (index >= 0) {
  200. entryList.splice(index, 1);
  201. delete entryTable[entryName];
  202. mainHeader.totalEntries = entryList.length;
  203. }
  204. },
  205. /**
  206. * Iterates and returns all nested files and directories of the given entry
  207. *
  208. * @param entry
  209. * @return Array
  210. */
  211. getEntryChildren: function (/*ZipEntry*/ entry, subfolders = true) {
  212. if (!loadedEntries) {
  213. readEntries();
  214. }
  215. if (typeof entry === "object") {
  216. if (entry.isDirectory && subfolders) {
  217. const list = [];
  218. const name = entry.entryName;
  219. for (const zipEntry of entryList) {
  220. if (zipEntry.entryName.startsWith(name)) {
  221. list.push(zipEntry);
  222. }
  223. }
  224. return list;
  225. } else {
  226. return [entry];
  227. }
  228. }
  229. return [];
  230. },
  231. /**
  232. * How many child elements entry has
  233. *
  234. * @param {ZipEntry} entry
  235. * @return {integer}
  236. */
  237. getChildCount: function (entry) {
  238. if (entry && entry.isDirectory) {
  239. const list = this.getEntryChildren(entry);
  240. return list.includes(entry) ? list.length - 1 : list.length;
  241. }
  242. return 0;
  243. },
  244. /**
  245. * Returns the zip file
  246. *
  247. * @return Buffer
  248. */
  249. compressToBuffer: function () {
  250. if (!loadedEntries) {
  251. readEntries();
  252. }
  253. sortEntries();
  254. const dataBlock = [];
  255. const headerBlocks = [];
  256. let totalSize = 0;
  257. let dindex = 0;
  258. mainHeader.size = 0;
  259. mainHeader.offset = 0;
  260. let totalEntries = 0;
  261. for (const entry of this.entries) {
  262. // compress data and set local and entry header accordingly. Reason why is called first
  263. const compressedData = entry.getCompressedData();
  264. entry.header.offset = dindex;
  265. // 1. construct local header
  266. const localHeader = entry.packLocalHeader();
  267. // 2. offsets
  268. const dataLength = localHeader.length + compressedData.length;
  269. dindex += dataLength;
  270. // 3. store values in sequence
  271. dataBlock.push(localHeader);
  272. dataBlock.push(compressedData);
  273. // 4. construct central header
  274. const centralHeader = entry.packCentralHeader();
  275. headerBlocks.push(centralHeader);
  276. // 5. update main header
  277. mainHeader.size += centralHeader.length;
  278. totalSize += dataLength + centralHeader.length;
  279. totalEntries++;
  280. }
  281. totalSize += mainHeader.mainHeaderSize; // also includes zip file comment length
  282. // point to end of data and beginning of central directory first record
  283. mainHeader.offset = dindex;
  284. mainHeader.totalEntries = totalEntries;
  285. dindex = 0;
  286. const outBuffer = Buffer.alloc(totalSize);
  287. // write data blocks
  288. for (const content of dataBlock) {
  289. content.copy(outBuffer, dindex);
  290. dindex += content.length;
  291. }
  292. // write central directory entries
  293. for (const content of headerBlocks) {
  294. content.copy(outBuffer, dindex);
  295. dindex += content.length;
  296. }
  297. // write main header
  298. const mh = mainHeader.toBinary();
  299. if (_comment) {
  300. _comment.copy(mh, Utils.Constants.ENDHDR); // add zip file comment
  301. }
  302. mh.copy(outBuffer, dindex);
  303. // Since we update entry and main header offsets,
  304. // they are no longer valid and we have to reset content
  305. // (Issue 64)
  306. inBuffer = outBuffer;
  307. loadedEntries = false;
  308. return outBuffer;
  309. },
  310. toAsyncBuffer: function (/*Function*/ onSuccess, /*Function*/ onFail, /*Function*/ onItemStart, /*Function*/ onItemEnd) {
  311. try {
  312. if (!loadedEntries) {
  313. readEntries();
  314. }
  315. sortEntries();
  316. const dataBlock = [];
  317. const centralHeaders = [];
  318. let totalSize = 0;
  319. let dindex = 0;
  320. let totalEntries = 0;
  321. mainHeader.size = 0;
  322. mainHeader.offset = 0;
  323. const compress2Buffer = function (entryLists) {
  324. if (entryLists.length > 0) {
  325. const entry = entryLists.shift();
  326. const name = entry.entryName + entry.extra.toString();
  327. if (onItemStart) onItemStart(name);
  328. entry.getCompressedDataAsync(function (compressedData) {
  329. if (onItemEnd) onItemEnd(name);
  330. entry.header.offset = dindex;
  331. // 1. construct local header
  332. const localHeader = entry.packLocalHeader();
  333. // 2. offsets
  334. const dataLength = localHeader.length + compressedData.length;
  335. dindex += dataLength;
  336. // 3. store values in sequence
  337. dataBlock.push(localHeader);
  338. dataBlock.push(compressedData);
  339. // central header
  340. const centalHeader = entry.packCentralHeader();
  341. centralHeaders.push(centalHeader);
  342. mainHeader.size += centalHeader.length;
  343. totalSize += dataLength + centalHeader.length;
  344. totalEntries++;
  345. compress2Buffer(entryLists);
  346. });
  347. } else {
  348. totalSize += mainHeader.mainHeaderSize; // also includes zip file comment length
  349. // point to end of data and beginning of central directory first record
  350. mainHeader.offset = dindex;
  351. mainHeader.totalEntries = totalEntries;
  352. dindex = 0;
  353. const outBuffer = Buffer.alloc(totalSize);
  354. dataBlock.forEach(function (content) {
  355. content.copy(outBuffer, dindex); // write data blocks
  356. dindex += content.length;
  357. });
  358. centralHeaders.forEach(function (content) {
  359. content.copy(outBuffer, dindex); // write central directory entries
  360. dindex += content.length;
  361. });
  362. const mh = mainHeader.toBinary();
  363. if (_comment) {
  364. _comment.copy(mh, Utils.Constants.ENDHDR); // add zip file comment
  365. }
  366. mh.copy(outBuffer, dindex); // write main header
  367. // Since we update entry and main header offsets, they are no
  368. // longer valid and we have to reset content using our new buffer
  369. // (Issue 64)
  370. inBuffer = outBuffer;
  371. loadedEntries = false;
  372. onSuccess(outBuffer);
  373. }
  374. };
  375. compress2Buffer(Array.from(this.entries));
  376. } catch (e) {
  377. onFail(e);
  378. }
  379. }
  380. };
  381. };