123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- 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()
- }
|