| 'use strict'; |
| const path = require('path'); |
| const fs = require('graceful-fs'); |
| const decompressTar = require('decompress-tar'); |
| const decompressTarbz2 = require('decompress-tarbz2'); |
| const decompressTargz = require('decompress-targz'); |
| const decompressUnzip = require('decompress-unzip'); |
| const makeDir = require('make-dir'); |
| const pify = require('pify'); |
| const stripDirs = require('strip-dirs'); |
| |
| const fsP = pify(fs); |
| |
| const runPlugins = (input, opts) => { |
| if (opts.plugins.length === 0) { |
| return Promise.resolve([]); |
| } |
| |
| return Promise.all(opts.plugins.map(x => x(input, opts))).then(files => files.reduce((a, b) => a.concat(b))); |
| }; |
| |
| const safeMakeDir = (dir, realOutputPath) => { |
| return fsP.realpath(dir) |
| .catch(_ => { |
| const parent = path.dirname(dir); |
| return safeMakeDir(parent, realOutputPath); |
| }) |
| .then(realParentPath => { |
| if (realParentPath.indexOf(realOutputPath) !== 0) { |
| throw (new Error('Refusing to create a directory outside the output path.')); |
| } |
| |
| return makeDir(dir).then(fsP.realpath); |
| }); |
| }; |
| |
| const preventWritingThroughSymlink = (destination, realOutputPath) => { |
| return fsP.readlink(destination) |
| .catch(_ => { |
| // Either no file exists, or it's not a symlink. In either case, this is |
| // not an escape we need to worry about in this phase. |
| return null; |
| }) |
| .then(symlinkPointsTo => { |
| if (symlinkPointsTo) { |
| throw new Error('Refusing to write into a symlink'); |
| } |
| |
| // No symlink exists at `destination`, so we can continue |
| return realOutputPath; |
| }); |
| }; |
| |
| const extractFile = (input, output, opts) => runPlugins(input, opts).then(files => { |
| if (opts.strip > 0) { |
| files = files |
| .map(x => { |
| x.path = stripDirs(x.path, opts.strip); |
| return x; |
| }) |
| .filter(x => x.path !== '.'); |
| } |
| |
| if (typeof opts.filter === 'function') { |
| files = files.filter(opts.filter); |
| } |
| |
| if (typeof opts.map === 'function') { |
| files = files.map(opts.map); |
| } |
| |
| if (!output) { |
| return files; |
| } |
| |
| return Promise.all(files.map(x => { |
| const dest = path.join(output, x.path); |
| const mode = x.mode & ~process.umask(); |
| const now = new Date(); |
| |
| if (x.type === 'directory') { |
| return makeDir(output) |
| .then(outputPath => fsP.realpath(outputPath)) |
| .then(realOutputPath => safeMakeDir(dest, realOutputPath)) |
| .then(() => fsP.utimes(dest, now, x.mtime)) |
| .then(() => x); |
| } |
| |
| return makeDir(output) |
| .then(outputPath => fsP.realpath(outputPath)) |
| .then(realOutputPath => { |
| // Attempt to ensure parent directory exists (failing if it's outside the output dir) |
| return safeMakeDir(path.dirname(dest), realOutputPath) |
| .then(() => realOutputPath); |
| }) |
| .then(realOutputPath => { |
| if (x.type === 'file') { |
| return preventWritingThroughSymlink(dest, realOutputPath); |
| } |
| |
| return realOutputPath; |
| }) |
| .then(realOutputPath => { |
| return fsP.realpath(path.dirname(dest)) |
| .then(realDestinationDir => { |
| if (realDestinationDir.indexOf(realOutputPath) !== 0) { |
| throw (new Error('Refusing to write outside output directory: ' + realDestinationDir)); |
| } |
| }); |
| }) |
| .then(() => { |
| if (x.type === 'link') { |
| return fsP.link(x.linkname, dest); |
| } |
| |
| if (x.type === 'symlink' && process.platform === 'win32') { |
| return fsP.link(x.linkname, dest); |
| } |
| |
| if (x.type === 'symlink') { |
| return fsP.symlink(x.linkname, dest); |
| } |
| |
| return fsP.writeFile(dest, x.data, {mode}); |
| }) |
| .then(() => x.type === 'file' && fsP.utimes(dest, now, x.mtime)) |
| .then(() => x); |
| })); |
| }); |
| |
| module.exports = (input, output, opts) => { |
| if (typeof input !== 'string' && !Buffer.isBuffer(input)) { |
| return Promise.reject(new TypeError('Input file required')); |
| } |
| |
| if (typeof output === 'object') { |
| opts = output; |
| output = null; |
| } |
| |
| opts = Object.assign({plugins: [ |
| decompressTar(), |
| decompressTarbz2(), |
| decompressTargz(), |
| decompressUnzip() |
| ]}, opts); |
| |
| const read = typeof input === 'string' ? fsP.readFile(input) : Promise.resolve(input); |
| |
| return read.then(buf => extractFile(buf, output, opts)); |
| }; |