| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 | const debug = require('debug')('extract-zip')// eslint-disable-next-line node/no-unsupported-features/node-builtinsconst { createWriteStream, promises: fs } = require('fs')const getStream = require('get-stream')const path = require('path')const { promisify } = require('util')const stream = require('stream')const yauzl = require('yauzl')const openZip = promisify(yauzl.open)const pipeline = promisify(stream.pipeline)class Extractor {  constructor (zipPath, opts) {    this.zipPath = zipPath    this.opts = opts  }  async extract () {    debug('opening', this.zipPath, 'with opts', this.opts)    this.zipfile = await openZip(this.zipPath, { lazyEntries: true })    this.canceled = false    return new Promise((resolve, reject) => {      this.zipfile.on('error', err => {        this.canceled = true        reject(err)      })      this.zipfile.readEntry()      this.zipfile.on('close', () => {        if (!this.canceled) {          debug('zip extraction complete')          resolve()        }      })      this.zipfile.on('entry', async entry => {        /* istanbul ignore if */        if (this.canceled) {          debug('skipping entry', entry.fileName, { cancelled: this.canceled })          return        }        debug('zipfile entry', entry.fileName)        if (entry.fileName.startsWith('__MACOSX/')) {          this.zipfile.readEntry()          return        }        const destDir = path.dirname(path.join(this.opts.dir, entry.fileName))        try {          await fs.mkdir(destDir, { recursive: true })          const canonicalDestDir = await fs.realpath(destDir)          const relativeDestDir = path.relative(this.opts.dir, canonicalDestDir)          if (relativeDestDir.split(path.sep).includes('..')) {            throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`)          }          await this.extractEntry(entry)          debug('finished processing', entry.fileName)          this.zipfile.readEntry()        } catch (err) {          this.canceled = true          this.zipfile.close()          reject(err)        }      })    })  }  async extractEntry (entry) {    /* istanbul ignore if */    if (this.canceled) {      debug('skipping entry extraction', entry.fileName, { cancelled: this.canceled })      return    }    if (this.opts.onEntry) {      this.opts.onEntry(entry, this.zipfile)    }    const dest = path.join(this.opts.dir, entry.fileName)    // convert external file attr int into a fs stat mode int    const mode = (entry.externalFileAttributes >> 16) & 0xFFFF    // check if it's a symlink or dir (using stat mode constants)    const IFMT = 61440    const IFDIR = 16384    const IFLNK = 40960    const symlink = (mode & IFMT) === IFLNK    let isDir = (mode & IFMT) === IFDIR    // Failsafe, borrowed from jsZip    if (!isDir && entry.fileName.endsWith('/')) {      isDir = true    }    // check for windows weird way of specifying a directory    // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566    const madeBy = entry.versionMadeBy >> 8    if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16)    debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink })    const procMode = this.getExtractedMode(mode, isDir) & 0o777    // always ensure folders are created    const destDir = isDir ? dest : path.dirname(dest)    const mkdirOptions = { recursive: true }    if (isDir) {      mkdirOptions.mode = procMode    }    debug('mkdir', { dir: destDir, ...mkdirOptions })    await fs.mkdir(destDir, mkdirOptions)    if (isDir) return    debug('opening read stream', dest)    const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry)    if (symlink) {      const link = await getStream(readStream)      debug('creating symlink', link, dest)      await fs.symlink(link, dest)    } else {      await pipeline(readStream, createWriteStream(dest, { mode: procMode }))    }  }  getExtractedMode (entryMode, isDir) {    let mode = entryMode    // Set defaults, if necessary    if (mode === 0) {      if (isDir) {        if (this.opts.defaultDirMode) {          mode = parseInt(this.opts.defaultDirMode, 10)        }        if (!mode) {          mode = 0o755        }      } else {        if (this.opts.defaultFileMode) {          mode = parseInt(this.opts.defaultFileMode, 10)        }        if (!mode) {          mode = 0o644        }      }    }    return mode  }}module.exports = async function (zipPath, opts) {  debug('creating target directory', opts.dir)  if (!path.isAbsolute(opts.dir)) {    throw new Error('Target directory is expected to be absolute')  }  await fs.mkdir(opts.dir, { recursive: true })  opts.dir = await fs.realpath(opts.dir)  return new Extractor(zipPath, opts).extract()}
 |