| Leo Repp | 58b9f11 | 2021-11-22 11:57:47 +0100 | [diff] [blame^] | 1 | 'use strict'; |
| 2 | const path = require('path'); |
| 3 | const fs = require('graceful-fs'); |
| 4 | const decompressTar = require('decompress-tar'); |
| 5 | const decompressTarbz2 = require('decompress-tarbz2'); |
| 6 | const decompressTargz = require('decompress-targz'); |
| 7 | const decompressUnzip = require('decompress-unzip'); |
| 8 | const makeDir = require('make-dir'); |
| 9 | const pify = require('pify'); |
| 10 | const stripDirs = require('strip-dirs'); |
| 11 | |
| 12 | const fsP = pify(fs); |
| 13 | |
| 14 | const runPlugins = (input, opts) => { |
| 15 | if (opts.plugins.length === 0) { |
| 16 | return Promise.resolve([]); |
| 17 | } |
| 18 | |
| 19 | return Promise.all(opts.plugins.map(x => x(input, opts))).then(files => files.reduce((a, b) => a.concat(b))); |
| 20 | }; |
| 21 | |
| 22 | const safeMakeDir = (dir, realOutputPath) => { |
| 23 | return fsP.realpath(dir) |
| 24 | .catch(_ => { |
| 25 | const parent = path.dirname(dir); |
| 26 | return safeMakeDir(parent, realOutputPath); |
| 27 | }) |
| 28 | .then(realParentPath => { |
| 29 | if (realParentPath.indexOf(realOutputPath) !== 0) { |
| 30 | throw (new Error('Refusing to create a directory outside the output path.')); |
| 31 | } |
| 32 | |
| 33 | return makeDir(dir).then(fsP.realpath); |
| 34 | }); |
| 35 | }; |
| 36 | |
| 37 | const preventWritingThroughSymlink = (destination, realOutputPath) => { |
| 38 | return fsP.readlink(destination) |
| 39 | .catch(_ => { |
| 40 | // Either no file exists, or it's not a symlink. In either case, this is |
| 41 | // not an escape we need to worry about in this phase. |
| 42 | return null; |
| 43 | }) |
| 44 | .then(symlinkPointsTo => { |
| 45 | if (symlinkPointsTo) { |
| 46 | throw new Error('Refusing to write into a symlink'); |
| 47 | } |
| 48 | |
| 49 | // No symlink exists at `destination`, so we can continue |
| 50 | return realOutputPath; |
| 51 | }); |
| 52 | }; |
| 53 | |
| 54 | const extractFile = (input, output, opts) => runPlugins(input, opts).then(files => { |
| 55 | if (opts.strip > 0) { |
| 56 | files = files |
| 57 | .map(x => { |
| 58 | x.path = stripDirs(x.path, opts.strip); |
| 59 | return x; |
| 60 | }) |
| 61 | .filter(x => x.path !== '.'); |
| 62 | } |
| 63 | |
| 64 | if (typeof opts.filter === 'function') { |
| 65 | files = files.filter(opts.filter); |
| 66 | } |
| 67 | |
| 68 | if (typeof opts.map === 'function') { |
| 69 | files = files.map(opts.map); |
| 70 | } |
| 71 | |
| 72 | if (!output) { |
| 73 | return files; |
| 74 | } |
| 75 | |
| 76 | return Promise.all(files.map(x => { |
| 77 | const dest = path.join(output, x.path); |
| 78 | const mode = x.mode & ~process.umask(); |
| 79 | const now = new Date(); |
| 80 | |
| 81 | if (x.type === 'directory') { |
| 82 | return makeDir(output) |
| 83 | .then(outputPath => fsP.realpath(outputPath)) |
| 84 | .then(realOutputPath => safeMakeDir(dest, realOutputPath)) |
| 85 | .then(() => fsP.utimes(dest, now, x.mtime)) |
| 86 | .then(() => x); |
| 87 | } |
| 88 | |
| 89 | return makeDir(output) |
| 90 | .then(outputPath => fsP.realpath(outputPath)) |
| 91 | .then(realOutputPath => { |
| 92 | // Attempt to ensure parent directory exists (failing if it's outside the output dir) |
| 93 | return safeMakeDir(path.dirname(dest), realOutputPath) |
| 94 | .then(() => realOutputPath); |
| 95 | }) |
| 96 | .then(realOutputPath => { |
| 97 | if (x.type === 'file') { |
| 98 | return preventWritingThroughSymlink(dest, realOutputPath); |
| 99 | } |
| 100 | |
| 101 | return realOutputPath; |
| 102 | }) |
| 103 | .then(realOutputPath => { |
| 104 | return fsP.realpath(path.dirname(dest)) |
| 105 | .then(realDestinationDir => { |
| 106 | if (realDestinationDir.indexOf(realOutputPath) !== 0) { |
| 107 | throw (new Error('Refusing to write outside output directory: ' + realDestinationDir)); |
| 108 | } |
| 109 | }); |
| 110 | }) |
| 111 | .then(() => { |
| 112 | if (x.type === 'link') { |
| 113 | return fsP.link(x.linkname, dest); |
| 114 | } |
| 115 | |
| 116 | if (x.type === 'symlink' && process.platform === 'win32') { |
| 117 | return fsP.link(x.linkname, dest); |
| 118 | } |
| 119 | |
| 120 | if (x.type === 'symlink') { |
| 121 | return fsP.symlink(x.linkname, dest); |
| 122 | } |
| 123 | |
| 124 | return fsP.writeFile(dest, x.data, {mode}); |
| 125 | }) |
| 126 | .then(() => x.type === 'file' && fsP.utimes(dest, now, x.mtime)) |
| 127 | .then(() => x); |
| 128 | })); |
| 129 | }); |
| 130 | |
| 131 | module.exports = (input, output, opts) => { |
| 132 | if (typeof input !== 'string' && !Buffer.isBuffer(input)) { |
| 133 | return Promise.reject(new TypeError('Input file required')); |
| 134 | } |
| 135 | |
| 136 | if (typeof output === 'object') { |
| 137 | opts = output; |
| 138 | output = null; |
| 139 | } |
| 140 | |
| 141 | opts = Object.assign({plugins: [ |
| 142 | decompressTar(), |
| 143 | decompressTarbz2(), |
| 144 | decompressTargz(), |
| 145 | decompressUnzip() |
| 146 | ]}, opts); |
| 147 | |
| 148 | const read = typeof input === 'string' ? fsP.readFile(input) : Promise.resolve(input); |
| 149 | |
| 150 | return read.then(buf => extractFile(buf, output, opts)); |
| 151 | }; |