directory.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. const PullStream = require('../PullStream');
  2. const unzip = require('./unzip');
  3. const BufferStream = require('../BufferStream');
  4. const parseExtraField = require('../parseExtraField');
  5. const path = require('path');
  6. const fs = require('fs-extra');
  7. const parseDateTime = require('../parseDateTime');
  8. const parseBuffer = require('../parseBuffer');
  9. const Bluebird = require('bluebird');
  10. const signature = Buffer.alloc(4);
  11. signature.writeUInt32LE(0x06054b50, 0);
  12. function getCrxHeader(source) {
  13. const sourceStream = source.stream(0).pipe(PullStream());
  14. return sourceStream.pull(4).then(function(data) {
  15. const signature = data.readUInt32LE(0);
  16. if (signature === 0x34327243) {
  17. let crxHeader;
  18. return sourceStream.pull(12).then(function(data) {
  19. crxHeader = parseBuffer.parse(data, [
  20. ['version', 4],
  21. ['pubKeyLength', 4],
  22. ['signatureLength', 4],
  23. ]);
  24. }).then(function() {
  25. return sourceStream.pull(crxHeader.pubKeyLength +crxHeader.signatureLength);
  26. }).then(function(data) {
  27. crxHeader.publicKey = data.slice(0, crxHeader.pubKeyLength);
  28. crxHeader.signature = data.slice(crxHeader.pubKeyLength);
  29. crxHeader.size = 16 + crxHeader.pubKeyLength +crxHeader.signatureLength;
  30. return crxHeader;
  31. });
  32. }
  33. });
  34. }
  35. // Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
  36. function getZip64CentralDirectory(source, zip64CDL) {
  37. const d64loc = parseBuffer.parse(zip64CDL, [
  38. ['signature', 4],
  39. ['diskNumber', 4],
  40. ['offsetToStartOfCentralDirectory', 8],
  41. ['numberOfDisks', 4],
  42. ]);
  43. if (d64loc.signature != 0x07064b50) {
  44. throw new Error('invalid zip64 end of central dir locator signature (0x07064b50): 0x' + d64loc.signature.toString(16));
  45. }
  46. const dir64 = PullStream();
  47. source.stream(d64loc.offsetToStartOfCentralDirectory).pipe(dir64);
  48. return dir64.pull(56);
  49. }
  50. // Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
  51. function parseZip64DirRecord (dir64record) {
  52. const vars = parseBuffer.parse(dir64record, [
  53. ['signature', 4],
  54. ['sizeOfCentralDirectory', 8],
  55. ['version', 2],
  56. ['versionsNeededToExtract', 2],
  57. ['diskNumber', 4],
  58. ['diskStart', 4],
  59. ['numberOfRecordsOnDisk', 8],
  60. ['numberOfRecords', 8],
  61. ['sizeOfCentralDirectory', 8],
  62. ['offsetToStartOfCentralDirectory', 8],
  63. ]);
  64. if (vars.signature != 0x06064b50) {
  65. throw new Error('invalid zip64 end of central dir locator signature (0x06064b50): 0x0' + vars.signature.toString(16));
  66. }
  67. return vars;
  68. }
  69. module.exports = function centralDirectory(source, options) {
  70. const endDir = PullStream();
  71. const records = PullStream();
  72. const tailSize = (options && options.tailSize) || 80;
  73. let sourceSize,
  74. crxHeader,
  75. startOffset,
  76. vars;
  77. if (options && options.crx)
  78. crxHeader = getCrxHeader(source);
  79. return source.size()
  80. .then(function(size) {
  81. sourceSize = size;
  82. source.stream(Math.max(0, size-tailSize))
  83. .on('error', function (error) { endDir.emit('error', error); })
  84. .pipe(endDir);
  85. return endDir.pull(signature);
  86. })
  87. .then(function() {
  88. return Bluebird.props({directory: endDir.pull(22), crxHeader: crxHeader});
  89. })
  90. .then(function(d) {
  91. const data = d.directory;
  92. startOffset = d.crxHeader && d.crxHeader.size || 0;
  93. vars = parseBuffer.parse(data, [
  94. ['signature', 4],
  95. ['diskNumber', 2],
  96. ['diskStart', 2],
  97. ['numberOfRecordsOnDisk', 2],
  98. ['numberOfRecords', 2],
  99. ['sizeOfCentralDirectory', 4],
  100. ['offsetToStartOfCentralDirectory', 4],
  101. ['commentLength', 2],
  102. ]);
  103. // Is this zip file using zip64 format? Use same check as Go:
  104. // https://github.com/golang/go/blob/master/src/archive/zip/reader.go#L503
  105. // For zip64 files, need to find zip64 central directory locator header to extract
  106. // relative offset for zip64 central directory record.
  107. if (vars.diskNumber == 0xffff || vars.numberOfRecords == 0xffff ||
  108. vars.offsetToStartOfCentralDirectory == 0xffffffff) {
  109. // Offset to zip64 CDL is 20 bytes before normal CDR
  110. const zip64CDLSize = 20;
  111. const zip64CDLOffset = sourceSize - (tailSize - endDir.match + zip64CDLSize);
  112. const zip64CDLStream = PullStream();
  113. source.stream(zip64CDLOffset).pipe(zip64CDLStream);
  114. return zip64CDLStream.pull(zip64CDLSize)
  115. .then(function (d) { return getZip64CentralDirectory(source, d); })
  116. .then(function (dir64record) {
  117. vars = parseZip64DirRecord(dir64record);
  118. });
  119. } else {
  120. vars.offsetToStartOfCentralDirectory += startOffset;
  121. }
  122. })
  123. .then(function() {
  124. if (vars.commentLength) return endDir.pull(vars.commentLength).then(function(comment) {
  125. vars.comment = comment.toString('utf8');
  126. });
  127. })
  128. .then(function() {
  129. source.stream(vars.offsetToStartOfCentralDirectory).pipe(records);
  130. vars.extract = function(opts) {
  131. if (!opts || !opts.path) throw new Error('PATH_MISSING');
  132. // make sure path is normalized before using it
  133. opts.path = path.resolve(path.normalize(opts.path));
  134. return vars.files.then(function(files) {
  135. return Bluebird.map(files, async function(entry) {
  136. // to avoid zip slip (writing outside of the destination), we resolve
  137. // the target path, and make sure it's nested in the intended
  138. // destination, or not extract it otherwise.
  139. const extractPath = path.join(opts.path, entry.path);
  140. if (extractPath.indexOf(opts.path) != 0) {
  141. return;
  142. }
  143. if (entry.type == 'Directory') {
  144. await fs.ensureDir(extractPath);
  145. return;
  146. }
  147. await fs.ensureDir(path.dirname(extractPath));
  148. const writer = opts.getWriter ? opts.getWriter({path: extractPath}) : fs.createWriteStream(extractPath);
  149. return new Promise(function(resolve, reject) {
  150. entry.stream(opts.password)
  151. .on('error', reject)
  152. .pipe(writer)
  153. .on('close', resolve)
  154. .on('error', reject);
  155. });
  156. }, { concurrency: opts.concurrency > 1 ? opts.concurrency : 1 });
  157. });
  158. };
  159. vars.files = Bluebird.mapSeries(Array(vars.numberOfRecords), function() {
  160. return records.pull(46).then(function(data) {
  161. const vars = parseBuffer.parse(data, [
  162. ['signature', 4],
  163. ['versionMadeBy', 2],
  164. ['versionsNeededToExtract', 2],
  165. ['flags', 2],
  166. ['compressionMethod', 2],
  167. ['lastModifiedTime', 2],
  168. ['lastModifiedDate', 2],
  169. ['crc32', 4],
  170. ['compressedSize', 4],
  171. ['uncompressedSize', 4],
  172. ['fileNameLength', 2],
  173. ['extraFieldLength', 2],
  174. ['fileCommentLength', 2],
  175. ['diskNumber', 2],
  176. ['internalFileAttributes', 2],
  177. ['externalFileAttributes', 4],
  178. ['offsetToLocalFileHeader', 4],
  179. ]);
  180. vars.offsetToLocalFileHeader += startOffset;
  181. vars.lastModifiedDateTime = parseDateTime(vars.lastModifiedDate, vars.lastModifiedTime);
  182. return records.pull(vars.fileNameLength).then(function(fileNameBuffer) {
  183. vars.pathBuffer = fileNameBuffer;
  184. vars.path = fileNameBuffer.toString('utf8');
  185. vars.isUnicode = (vars.flags & 0x800) != 0;
  186. return records.pull(vars.extraFieldLength);
  187. })
  188. .then(function(extraField) {
  189. vars.extra = parseExtraField(extraField, vars);
  190. return records.pull(vars.fileCommentLength);
  191. })
  192. .then(function(comment) {
  193. vars.comment = comment;
  194. vars.type = (vars.uncompressedSize === 0 && /[/\\]$/.test(vars.path)) ? 'Directory' : 'File';
  195. const padding = options && options.padding || 1000;
  196. vars.stream = function(_password) {
  197. const totalSize = 30
  198. + padding // add an extra buffer
  199. + (vars.extraFieldLength || 0)
  200. + (vars.fileNameLength || 0)
  201. + vars.compressedSize;
  202. return unzip(source, vars.offsetToLocalFileHeader, _password, vars, totalSize);
  203. };
  204. vars.buffer = function(_password) {
  205. return BufferStream(vars.stream(_password));
  206. };
  207. return vars;
  208. });
  209. });
  210. });
  211. return Bluebird.props(vars);
  212. });
  213. };