// Copyright (c) 2014 Max Ogden and other contributors // All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // https://github.com/maxogden/extract-zip // eslint-disable-next-line node/no-unsupported-features/node-builtins const { createWriteStream, promises: fs } = require('original-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 () { 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) { resolve() } }) this.zipfile.on('entry', async entry => { /* istanbul ignore if */ if (this.canceled) { return } 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) this.zipfile.readEntry() } catch (err) { this.canceled = true this.zipfile.close() reject(err) } }) }) } async extractEntry (entry) { /* istanbul ignore if */ if (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) 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 } await fs.mkdir(destDir, mkdirOptions) if (isDir) return const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry) if (symlink) { const link = await getStream(readStream) 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) { 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() }