uncompress_stream.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. 'use strict';
  2. // https://github.com/thejoshwolfe/yauzl#no-streaming-unzip-api
  3. const yauzl = require('yauzl');
  4. const stream = require('stream');
  5. const UncompressBaseStream = require('../base_write_stream');
  6. const utils = require('../utils');
  7. // lazy load iconv-lite
  8. let iconv;
  9. const YAUZL_CALLBACK = Symbol('ZipUncompressStream#yauzlCallback');
  10. const STRIP_NAME = Symbol('ZipUncompressStream#stripName');
  11. // don't decodeStrings on yauzl, we should handle fileName by ourself
  12. // see validateFileName on https://github.com/thejoshwolfe/yauzl/blob/51010ce4e8c7e6345efe195e1b4150518f37b393/index.js#L607
  13. // - support "absolute path"
  14. const DEFAULTS = { lazyEntries: true, decodeStrings: false };
  15. // from: https://github.com/microsoft/vscode/blob/c0769274fa136b45799edeccc0d0a2f645b75caf/src/vs/base/node/zip.ts#L51
  16. function modeFromEntry(entry) {
  17. const attr = entry.externalFileAttributes >> 16 || 33188;
  18. return [ 448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */ ]
  19. .map(mask => attr & mask)
  20. .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
  21. }
  22. class ZipUncompressStream extends UncompressBaseStream {
  23. constructor(opts) {
  24. opts = opts || {};
  25. super(opts);
  26. this._chunks = [];
  27. this._strip = Number(opts.strip) || 0;
  28. this._zipFileNameEncoding = opts.zipFileNameEncoding || 'utf8';
  29. if (this._zipFileNameEncoding === 'utf-8') {
  30. this._zipFileNameEncoding = 'utf8';
  31. }
  32. this[YAUZL_CALLBACK] = this[YAUZL_CALLBACK].bind(this);
  33. const sourceType = utils.sourceType(opts.source);
  34. const yauzlOpts = this._yauzlOpts = Object.assign({}, DEFAULTS, opts.yauzl);
  35. if (sourceType === 'file') {
  36. yauzl.open(opts.source, yauzlOpts, this[YAUZL_CALLBACK]);
  37. return;
  38. }
  39. if (sourceType === 'buffer') {
  40. yauzl.fromBuffer(opts.source, yauzlOpts, this[YAUZL_CALLBACK]);
  41. return;
  42. }
  43. if (sourceType === 'stream') {
  44. utils.streamToBuffer(opts.source)
  45. .then(buf => yauzl.fromBuffer(buf, yauzlOpts, this[YAUZL_CALLBACK]))
  46. .catch(e => this.emit('error', e));
  47. return;
  48. }
  49. this.on('pipe', srcStream => {
  50. srcStream.unpipe(srcStream);
  51. utils.streamToBuffer(srcStream)
  52. .then(buf => {
  53. this._chunks.push(buf);
  54. buf = Buffer.concat(this._chunks);
  55. yauzl.fromBuffer(buf, yauzlOpts, this[YAUZL_CALLBACK]);
  56. })
  57. .catch(e => this.emit('error', e));
  58. });
  59. }
  60. _write(chunk) {
  61. // push to _chunks array, this will only happen once, for stream will be unpiped.
  62. this._chunks.push(chunk);
  63. }
  64. [YAUZL_CALLBACK](err, zipFile) {
  65. if (err) return this.emit('error', err);
  66. zipFile.readEntry();
  67. zipFile
  68. .on('entry', entry => {
  69. const mode = modeFromEntry(entry);
  70. // fileName is buffer by default because decodeStrings = false
  71. if (Buffer.isBuffer(entry.fileName)) {
  72. if (this._zipFileNameEncoding === 'utf8') {
  73. entry.fileName = entry.fileName.toString();
  74. } else {
  75. if (!iconv) {
  76. iconv = require('iconv-lite');
  77. }
  78. entry.fileName = iconv.decode(entry.fileName, this._zipFileNameEncoding);
  79. }
  80. }
  81. // directory file names end with '/'
  82. const type = /\/$/.test(entry.fileName) ? 'directory' : 'file';
  83. const name = entry.fileName = this[STRIP_NAME](entry.fileName, type);
  84. const header = { name, type, yauzl: entry, mode };
  85. if (type === 'file') {
  86. zipFile.openReadStream(entry, (err, readStream) => {
  87. if (err) return this.emit('error', err);
  88. this.emit('entry', header, readStream, next);
  89. });
  90. } else { // directory
  91. const placeholder = new stream.Readable({ read() {} });
  92. this.emit('entry', header, placeholder, next);
  93. setImmediate(() => placeholder.emit('end'));
  94. }
  95. })
  96. .on('end', () => this.emit('finish'))
  97. .on('error', err => this.emit('error', err));
  98. function next() {
  99. zipFile.readEntry();
  100. }
  101. }
  102. [STRIP_NAME](fileName, type) {
  103. return utils.stripFileName(this._strip, fileName, type);
  104. }
  105. }
  106. module.exports = ZipUncompressStream;