index.js 4.0 KB

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