directory.js 8.2 KB

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