| const debug = require('debug')('extract-zip') |
| // eslint-disable-next-line node/no-unsupported-features/node-builtins |
| const { 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() |
| } |