blob: 6aa67ca9cda4bac1262915ca6e47e97b224d336e [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001'use strict';
2const path = require('path');
3const fs = require('graceful-fs');
4const decompressTar = require('decompress-tar');
5const decompressTarbz2 = require('decompress-tarbz2');
6const decompressTargz = require('decompress-targz');
7const decompressUnzip = require('decompress-unzip');
8const makeDir = require('make-dir');
9const pify = require('pify');
10const stripDirs = require('strip-dirs');
11
12const fsP = pify(fs);
13
14const 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
22const 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
37const 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
54const 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
131module.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};