'use strict' process.umask(0o022) const Unpack = require('../lib/unpack.js') const UnpackSync = Unpack.Sync const t = require('tap') const MiniPass = require('minipass') const makeTar = require('./make-tar.js') const Header = require('../lib/header.js') const z = require('minizlib') const fs = require('fs') const path = require('path') const fixtures = path.resolve(__dirname, 'fixtures') const tars = path.resolve(fixtures, 'tars') const parses = path.resolve(fixtures, 'parse') const unpackdir = path.resolve(fixtures, 'unpack') const {promisify} = require('util') const rimraf = promisify(require('rimraf')) const mkdirp = require('mkdirp') const mutateFS = require('mutate-fs') const eos = require('end-of-stream') const normPath = require('../lib/normalize-windows-path.js') // On Windows in particular, the "really deep folder path" file // often tends to cause problems, which don't indicate a failure // of this library, it's just what happens on Windows with super // long file paths. const isWindows = process.platform === 'win32' const isLongFile = f => f.match(/r.e.a.l.l.y.-.d.e.e.p.-.f.o.l.d.e.r.-.p.a.t.h/) t.teardown(_ => rimraf(unpackdir)) t.before(async () => { await rimraf(unpackdir) await mkdirp(unpackdir) }) t.test('basic file unpack tests', t => { const basedir = path.resolve(unpackdir, 'basic') t.teardown(_ => rimraf(basedir)) const cases = { 'emptypax.tar': { '🌟.txt': '🌟✧✩⭐︎✪✫✬✭✮⚝✯✰✵✶✷✸✹❂⭑⭒★☆✡☪✴︎✦✡️🔯✴️🌠\n', 'one-byte.txt': 'a', }, 'body-byte-counts.tar': { '1024-bytes.txt': new Array(1024).join('x') + '\n', '512-bytes.txt': new Array(512).join('x') + '\n', 'one-byte.txt': 'a', 'zero-byte.txt': '', }, 'utf8.tar': { '🌟.txt': '🌟✧✩⭐︎✪✫✬✭✮⚝✯✰✵✶✷✸✹❂⭑⭒★☆✡☪✴︎✦✡️🔯✴️🌠\n', 'Ω.txt': 'Ω', 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt': 'Ω', }, 'file.tar': { 'one-byte.txt': 'a', }, 'global-header.tar': { 'one-byte.txt': 'a', }, 'long-pax.tar': { '120-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', }, 'long-paths.tar': { '100-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', '120-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', '170-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/a.txt': 'short\n', 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt': 'Ω', }, } const tarfiles = Object.keys(cases) t.plan(tarfiles.length) t.jobs = tarfiles.length tarfiles.forEach(tarfile => { t.test(tarfile, t => { const tf = path.resolve(tars, tarfile) const dir = path.resolve(basedir, tarfile) const linkdir = path.resolve(basedir, tarfile + '.link') t.beforeEach(async () => { await rimraf(dir) await rimraf(linkdir) await mkdirp(dir) fs.symlinkSync(dir, linkdir, 'junction') }) const check = t => { const expect = cases[tarfile] Object.keys(expect).forEach(file => { const f = path.resolve(dir, file) if (isWindows && isLongFile(file)) return t.equal(fs.readFileSync(f, 'utf8'), expect[file], file) }) t.end() } t.plan(2) t.test('async unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new Unpack({ cwd: linkdir, strict: true }) fs.createReadStream(tf).pipe(unpack) eos(unpack, _ => check(t)) }) t.test('loose', t => { const unpack = new Unpack({ cwd: linkdir }) fs.createReadStream(tf).pipe(unpack) eos(unpack, _ => check(t)) }) }) t.test('sync unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new UnpackSync({ cwd: linkdir }) unpack.end(fs.readFileSync(tf)) check(t) }) t.test('loose', t => { const unpack = new UnpackSync({ cwd: linkdir }) unpack.end(fs.readFileSync(tf)) check(t) }) }) }) }) }) t.test('cwd default to process cwd', t => { const u = new Unpack() const us = new UnpackSync() const cwd = normPath(process.cwd()) t.equal(u.cwd, cwd) t.equal(us.cwd, cwd) t.end() }) t.test('links!', t => { const dir = path.resolve(unpackdir, 'links') const data = fs.readFileSync(tars + '/links.tar') const stripData = fs.readFileSync(tars + '/links-strip.tar') t.plan(6) t.beforeEach(() => mkdirp(dir)) t.afterEach(() => rimraf(dir)) const check = t => { const hl1 = fs.lstatSync(dir + '/hardlink-1') const hl2 = fs.lstatSync(dir + '/hardlink-2') t.equal(hl1.dev, hl2.dev) t.equal(hl1.ino, hl2.ino) t.equal(hl1.nlink, 2) t.equal(hl2.nlink, 2) if (!isWindows) { // doesn't work on win32 without special privs const sym = fs.lstatSync(dir + '/symlink') t.ok(sym.isSymbolicLink()) t.equal(fs.readlinkSync(dir + '/symlink'), 'hardlink-2') } t.end() } const checkForStrip = t => { const hl1 = fs.lstatSync(dir + '/hardlink-1') const hl2 = fs.lstatSync(dir + '/hardlink-2') const hl3 = fs.lstatSync(dir + '/1/2/3/hardlink-3') t.equal(hl1.dev, hl2.dev) t.equal(hl1.ino, hl2.ino) t.equal(hl1.dev, hl3.dev) t.equal(hl1.ino, hl3.ino) t.equal(hl1.nlink, 3) t.equal(hl2.nlink, 3) if (!isWindows) { const sym = fs.lstatSync(dir + '/symlink') t.ok(sym.isSymbolicLink()) t.equal(fs.readlinkSync(dir + '/symlink'), 'hardlink-2') } t.end() } const checkForStrip3 = t => { // strips the linkpath entirely, so the link doesn't get extracted. t.throws(() => fs.lstatSync(dir + '/3'), { code: 'ENOENT' }) t.end() } t.test('async', t => { const unpack = new Unpack({ cwd: dir }) let finished = false unpack.on('finish', _ => finished = true) unpack.on('close', _ => t.ok(finished, 'emitted finish before close')) unpack.on('close', _ => check(t)) unpack.end(data) }) t.test('sync', t => { const unpack = new UnpackSync({ cwd: dir }) unpack.end(data) check(t) }) t.test('sync strip', t => { const unpack = new UnpackSync({ cwd: dir, strip: 1 }) unpack.end(stripData) checkForStrip(t) }) t.test('async strip', t => { const unpack = new Unpack({ cwd: dir, strip: 1 }) let finished = false unpack.on('finish', _ => finished = true) unpack.on('close', _ => t.ok(finished, 'emitted finish before close')) unpack.on('close', _ => checkForStrip(t)) unpack.end(stripData) }) t.test('sync strip 3', t => { const unpack = new UnpackSync({ cwd: dir, strip: 3 }) unpack.end(fs.readFileSync(tars + '/links-strip.tar')) checkForStrip3(t) }) t.test('async strip 3', t => { const unpack = new Unpack({ cwd: dir, strip: 3 }) let finished = false unpack.on('finish', _ => finished = true) unpack.on('close', _ => t.ok(finished, 'emitted finish before close')) unpack.on('close', _ => checkForStrip3(t)) unpack.end(stripData) }) }) t.test('links without cleanup (exercise clobbering code)', t => { const dir = path.resolve(unpackdir, 'links') const data = fs.readFileSync(tars + '/links.tar') t.plan(6) mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) t.beforeEach(() => { // clobber this junk try { mkdirp.sync(dir + '/hardlink-1') mkdirp.sync(dir + '/hardlink-2') fs.writeFileSync(dir + '/symlink', 'not a symlink') } catch (er) {} }) const check = t => { const hl1 = fs.lstatSync(dir + '/hardlink-1') const hl2 = fs.lstatSync(dir + '/hardlink-2') t.equal(hl1.dev, hl2.dev) t.equal(hl1.ino, hl2.ino) t.equal(hl1.nlink, 2) t.equal(hl2.nlink, 2) if (!isWindows) { const sym = fs.lstatSync(dir + '/symlink') t.ok(sym.isSymbolicLink()) t.equal(fs.readlinkSync(dir + '/symlink'), 'hardlink-2') } t.end() } t.test('async', t => { const unpack = new Unpack({ cwd: dir }) let prefinished = false unpack.on('prefinish', _ => prefinished = true) unpack.on('finish', _ => t.ok(prefinished, 'emitted prefinish before finish')) unpack.on('close', _ => check(t)) unpack.end(data) }) t.test('sync', t => { const unpack = new UnpackSync({ cwd: dir }) unpack.end(data) check(t) }) t.test('async again', t => { const unpack = new Unpack({ cwd: dir }) eos(unpack, _ => check(t)) unpack.end(data) }) t.test('sync again', t => { const unpack = new UnpackSync({ cwd: dir }) unpack.end(data) check(t) }) t.test('async unlink', t => { const unpack = new Unpack({ cwd: dir, unlink: true }) unpack.on('close', _ => check(t)) unpack.end(data) }) t.test('sync unlink', t => { const unpack = new UnpackSync({ cwd: dir, unlink: true }) unpack.end(data) check(t) }) }) t.test('nested dir dupe', t => { const dir = path.resolve(unpackdir, 'nested-dir') mkdirp.sync(dir + '/d/e/e/p') t.teardown(_ => rimraf(dir)) const expect = { 'd/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/a.txt': 'short\n', 'd/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'd/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'd/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', 'd/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt': 'Ω', } const check = t => { const entries = fs.readdirSync(dir) t.equal(entries.length, 1) t.equal(entries[0], 'd') Object.keys(expect).forEach(f => { const file = dir + '/' + f t.equal(fs.readFileSync(file, 'utf8'), expect[f]) }) t.end() } const unpack = new Unpack({ cwd: dir, strip: 8 }) const data = fs.readFileSync(tars + '/long-paths.tar') // while we're at it, why not use gzip too? const zip = new z.Gzip() zip.pipe(unpack) unpack.on('close', _ => check(t)) zip.end(data) }) t.test('symlink in dir path', { skip: isWindows && 'symlinks not fully supported', }, t => { const dir = path.resolve(unpackdir, 'symlink-junk') t.teardown(_ => rimraf(dir)) t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) }) const data = makeTar([ { path: 'd/i', type: 'Directory', }, { path: 'd/i/r/dir', type: 'Directory', mode: 0o751, mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/file', type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), }, 'a', { path: 'd/i/r/link', type: 'Link', linkpath: 'd/i/r/file', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/symlink', type: 'SymbolicLink', linkpath: './dir', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/symlink/x', type: 'File', size: 0, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, '', '', ]) t.test('no clobbering', t => { const warnings = [] const u = new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]), }) u.on('close', _ => { t.equal(fs.lstatSync(dir + '/d/i').mode & 0o7777, isWindows ? 0o666 : 0o755) t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, isWindows ? 0o666 : 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') if (!isWindows) { t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'got symlink') t.throws(_ => fs.statSync(dir + '/d/i/r/symlink/x')) } t.equal(warnings[0][0], 'TAR_ENTRY_ERROR') if (!isWindows) { t.equal(warnings[0][1], 'Cannot extract through symbolic link') t.match(warnings[0][2], { name: 'SylinkError', path: dir + '/d/i/r/symlink/', symlink: dir + '/d/i/r/symlink', }) } t.equal(warnings.length, 1) t.end() }) u.end(data) }) t.test('no clobbering, sync', t => { const warnings = [] const u = new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]), }) u.end(data) t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, isWindows ? 0o666 : 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') if (!isWindows) { t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'got symlink') t.throws(_ => fs.statSync(dir + '/d/i/r/symlink/x')) } t.equal(warnings.length, 1) t.equal(warnings[0][0], 'TAR_ENTRY_ERROR') t.equal(warnings[0][1], 'Cannot extract through symbolic link') t.match(warnings[0][2], { name: 'SylinkError', path: dir + '/d/i/r/symlink/', symlink: dir + '/d/i/r/symlink', }) t.end() }) t.test('extract through symlink', t => { const warnings = [] const u = new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]), preservePaths: true, }) u.on('close', _ => { t.same(warnings, []) t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'got symlink') t.ok(fs.lstatSync(dir + '/d/i/r/dir/x').isFile(), 'x thru link') t.ok(fs.lstatSync(dir + '/d/i/r/symlink/x').isFile(), 'x thru link') t.end() }) u.end(data) }) t.test('extract through symlink sync', t => { const warnings = [] const u = new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]), preservePaths: true, }) u.end(data) t.same(warnings, []) t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'got symlink') t.ok(fs.lstatSync(dir + '/d/i/r/dir/x').isFile(), 'x thru link') t.ok(fs.lstatSync(dir + '/d/i/r/symlink/x').isFile(), 'x thru link') t.end() }) t.test('clobber through symlink', t => { const warnings = [] const u = new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]), unlink: true, }) u.on('close', _ => { t.same(warnings, []) t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') t.notOk(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'no link') t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isDirectory(), 'sym is dir') t.ok(fs.lstatSync(dir + '/d/i/r/symlink/x').isFile(), 'x thru link') t.end() }) u.end(data) }) t.test('clobber through symlink with busted unlink', t => { const poop = new Error('poop') t.teardown(mutateFS.fail('unlink', poop)) const warnings = [] const u = new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]), unlink: true, }) u.on('close', _ => { t.same(warnings, [['TAR_ENTRY_ERROR', 'poop', poop]]) t.end() }) u.end(data) }) t.test('clobber through symlink sync', t => { const warnings = [] const u = new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]), unlink: true, }) u.end(data) t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') t.notOk(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'no link') t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isDirectory(), 'sym is dir') t.ok(fs.lstatSync(dir + '/d/i/r/symlink/x').isFile(), 'x thru link') t.end() }) t.test('clobber dirs', t => { mkdirp.sync(dir + '/d/i/r/dir') mkdirp.sync(dir + '/d/i/r/file') mkdirp.sync(dir + '/d/i/r/link') mkdirp.sync(dir + '/d/i/r/symlink') const warnings = [] const u = new Unpack({ cwd: dir, onwarn: (c, w, d) => { warnings.push([c, w, d]) }, }) u.on('close', _ => { t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'got symlink') t.throws(_ => fs.statSync(dir + '/d/i/r/symlink/x')) t.equal(warnings.length, 1) t.equal(warnings[0][0], 'TAR_ENTRY_ERROR') t.equal(warnings[0][1], 'Cannot extract through symbolic link') t.match(warnings[0][2], { name: 'SylinkError', path: dir + '/d/i/r/symlink/', symlink: dir + '/d/i/r/symlink', }) t.end() }) u.end(data) }) t.test('clobber dirs sync', t => { mkdirp.sync(dir + '/d/i/r/dir') mkdirp.sync(dir + '/d/i/r/file') mkdirp.sync(dir + '/d/i/r/link') mkdirp.sync(dir + '/d/i/r/symlink') const warnings = [] const u = new UnpackSync({ cwd: dir, onwarn: (c, w, d) => { warnings.push([c, w, d]) }, }) u.end(data) t.equal(fs.lstatSync(dir + '/d/i/r/dir').mode & 0o7777, 0o751) t.ok(fs.lstatSync(dir + '/d/i/r/file').isFile(), 'got file') t.ok(fs.lstatSync(dir + '/d/i/r/symlink').isSymbolicLink(), 'got symlink') t.throws(_ => fs.statSync(dir + '/d/i/r/symlink/x')) t.equal(warnings.length, 1) t.equal(warnings[0][0], 'TAR_ENTRY_ERROR') t.equal(warnings[0][1], 'Cannot extract through symbolic link') t.match(warnings[0][2], { name: 'SylinkError', path: dir + '/d/i/r/symlink/', symlink: dir + '/d/i/r/symlink', }) t.end() }) t.end() }) t.test('unsupported entries', t => { const dir = path.resolve(unpackdir, 'unsupported-entries') mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) const unknown = new Header({ path: 'qux', type: 'File', size: 4 }) unknown.type = 'Z' unknown.encode() const data = makeTar([ { path: 'dev/random', type: 'CharacterDevice', }, { path: 'dev/hd0', type: 'BlockDevice', }, { path: 'dev/fifo0', type: 'FIFO', }, // note: unrecognized types are ignored, so this won't emit a warning. // gnutar and bsdtar treat unrecognized types as 'file', so it may be // worth doing the same thing, but with a warning. unknown.block, 'asdf', '', '', ]) t.test('basic, warns', t => { const warnings = [] const u = new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([c, w, d]) }) const c = 'TAR_ENTRY_UNSUPPORTED' const expect = [ [c, 'unsupported entry type: CharacterDevice', { entry: { path: 'dev/random' }}], [c, 'unsupported entry type: BlockDevice', { entry: { path: 'dev/hd0' }}], [c, 'unsupported entry type: FIFO', { entry: { path: 'dev/fifo0' }}], ] u.on('close', _ => { t.equal(fs.readdirSync(dir).length, 0) t.match(warnings, expect) t.end() }) u.end(data) }) t.test('strict, throws', t => { const warnings = [] const errors = [] const u = new Unpack({ cwd: dir, strict: true, onwarn: (c, w, d) => warnings.push([c, w, d]), }) u.on('error', e => errors.push(e)) u.on('close', _ => { t.equal(fs.readdirSync(dir).length, 0) t.same(warnings, []) t.match(errors, [ { message: 'unsupported entry type: CharacterDevice', entry: { path: 'dev/random' }, }, { message: 'unsupported entry type: BlockDevice', entry: { path: 'dev/hd0' }, }, { message: 'unsupported entry type: FIFO', entry: { path: 'dev/fifo0' }, }, ]) t.end() }) u.end(data) }) t.end() }) t.test('file in dir path', t => { const dir = path.resolve(unpackdir, 'file-junk') t.teardown(_ => rimraf(dir)) t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) }) const data = makeTar([ { path: 'd/i/r/file', type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'a', { path: 'd/i/r/file/a/b/c', type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'b', '', '', ]) t.test('fail because of file', t => { const check = t => { t.equal(fs.readFileSync(dir + '/d/i/r/file', 'utf8'), 'a') t.throws(_ => fs.statSync(dir + '/d/i/r/file/a/b/c')) t.end() } t.plan(2) t.test('async', t => { new Unpack({ cwd: dir }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { new UnpackSync({ cwd: dir }).end(data) check(t) }) }) t.test('clobber on through', t => { const check = t => { t.ok(fs.statSync(dir + '/d/i/r/file').isDirectory()) t.equal(fs.readFileSync(dir + '/d/i/r/file/a/b/c', 'utf8'), 'b') t.end() } t.plan(2) t.test('async', t => { new Unpack({ cwd: dir, unlink: true }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { new UnpackSync({ cwd: dir, unlink: true }).end(data) check(t) }) }) t.end() }) t.test('set umask option', t => { const dir = path.resolve(unpackdir, 'umask') mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) const data = makeTar([ { path: 'd/i/r/dir', type: 'Directory', mode: 0o751, }, '', '', ]) new Unpack({ umask: 0o027, cwd: dir, }).on('close', _ => { t.equal(fs.statSync(dir + '/d/i/r').mode & 0o7777, isWindows ? 0o666 : 0o750) t.equal(fs.statSync(dir + '/d/i/r/dir').mode & 0o7777, isWindows ? 0o666 : 0o751) t.end() }).end(data) }) t.test('absolute paths', t => { const dir = path.join(unpackdir, 'absolute-paths') t.teardown(_ => rimraf(dir)) t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) }) const absolute = path.resolve(dir, 'd/i/r/absolute') const root = path.parse(absolute).root const extraAbsolute = root + root + root + absolute t.ok(path.isAbsolute(extraAbsolute)) t.ok(path.isAbsolute(absolute)) const parsed = path.parse(absolute) const relative = absolute.substr(parsed.root.length) t.notOk(path.isAbsolute(relative)) const data = makeTar([ { path: extraAbsolute, type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'a', '', '', ]) t.test('warn and correct', t => { const check = t => { const r = normPath(root) t.match(warnings, [[ `stripping ${r}${r}${r}${r} from absolute path`, { path: normPath(absolute), code: 'TAR_ENTRY_INFO' }, ]]) t.ok(fs.lstatSync(path.resolve(dir, relative)).isFile(), 'is file') t.end() } const warnings = [] t.test('async', t => { warnings.length = 0 new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { warnings.length = 0 new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t) }) t.end() }) t.test('preserve absolute path', t => { // if we use the extraAbsolute path here, we end up creating a dir // like C:\C:\C:\C:\path\to\absolute, which is both 100% valid on // windows, as well as SUUUUUPER annoying. const data = makeTar([ { path: isWindows ? absolute : extraAbsolute, type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'a', '', '', ]) const check = t => { t.same(warnings, []) t.ok(fs.lstatSync(absolute).isFile(), 'is file') t.end() } const warnings = [] t.test('async', t => { warnings.length = 0 new Unpack({ preservePaths: true, cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { warnings.length = 0 new UnpackSync({ preservePaths: true, cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t) }) t.end() }) t.end() }) t.test('.. paths', t => { const dir = path.join(unpackdir, 'dotted-paths') t.teardown(_ => rimraf(dir)) t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) }) const fmode = 0o755 const dotted = 'a/b/c/../d' const resolved = path.resolve(dir, dotted) const data = makeTar([ { path: dotted, type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'd', '', '', ]) t.test('warn and skip', t => { const check = t => { t.match(warnings, [[ 'path contains \'..\'', { path: dotted, code: 'TAR_ENTRY_ERROR' }, ]]) t.throws(_ => fs.lstatSync(resolved)) t.end() } const warnings = [] t.test('async', t => { warnings.length = 0 new Unpack({ fmode: fmode, cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { warnings.length = 0 new UnpackSync({ fmode: fmode, cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t) }) t.end() }) t.test('preserve dotted path', t => { const check = t => { t.same(warnings, []) t.ok(fs.lstatSync(resolved).isFile(), 'is file') t.equal(fs.lstatSync(resolved).mode & 0o777, isWindows ? 0o666 : fmode) t.end() } const warnings = [] t.test('async', t => { warnings.length = 0 new Unpack({ fmode: fmode, preservePaths: true, cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { warnings.length = 0 new UnpackSync({ fmode: fmode, preservePaths: true, cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t) }) t.end() }) t.end() }) t.test('fail all stats', t => { const poop = new Error('poop') poop.code = 'EPOOP' const dir = normPath(path.join(unpackdir, 'stat-fail')) const { stat, fstat, lstat, statSync, fstatSync, lstatSync, } = fs const unmutate = () => Object.assign(fs, { stat, fstat, lstat, statSync, fstatSync, lstatSync, }) const mutate = () => { fs.stat = fs.lstat = fs.fstat = (...args) => { // don't fail statting the cwd, or we get different errors if (normPath(args[0]) === dir) return lstat(dir, args.pop()) process.nextTick(() => args.pop()(poop)) } fs.statSync = fs.lstatSync = fs.fstatSync = (...args) => { if (normPath(args[0]) === dir) return lstatSync(dir) throw poop } } const warnings = [] t.beforeEach(() => { warnings.length = 0 mkdirp.sync(dir) mutate() }) t.afterEach(async () => { unmutate() await rimraf(dir) }) const data = makeTar([ { path: 'd/i/r/file/', type: 'Directory', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/dir/', type: 'Directory', mode: 0o751, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/file', type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'a', { path: 'd/i/r/link', type: 'Link', linkpath: 'd/i/r/file', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/symlink', type: 'SymbolicLink', linkpath: './dir', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, '', '', ]) const check = (t, expect) => { t.match(warnings, expect) warnings.forEach(w => t.equal(w[0], w[1].message)) t.end() } t.test('async', t => { const expect = [ ['poop', poop], ['poop', poop], ] new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t, expect)).end(data) }) t.test('sync', t => { const expect = [ [ String, { code: 'EISDIR', path: normPath(path.resolve(dir, 'd/i/r/file')), syscall: 'open', }, ], [ String, { dest: normPath(path.resolve(dir, 'd/i/r/link')), path: normPath(path.resolve(dir, 'd/i/r/file')), syscall: 'link', }, ], ] new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t, expect) }) t.end() }) t.test('fail symlink', t => { const poop = new Error('poop') poop.code = 'EPOOP' const unmutate = mutateFS.fail('symlink', poop) const dir = path.join(unpackdir, 'symlink-fail') t.teardown(async _ => { unmutate() await rimraf(dir) }) const warnings = [] t.beforeEach(async () => { warnings.length = 0 await rimraf(dir) await mkdirp(dir) }) const data = makeTar([ { path: 'd/i/r/dir/', type: 'Directory', mode: 0o751, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/symlink', type: 'SymbolicLink', linkpath: './dir', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, '', '', ]) const check = (t, expect) => { t.match(warnings, expect) warnings.forEach(w => t.equal(w[0], w[1].message)) t.end() } t.test('async', t => { const expect = [['poop', poop]] new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t, expect)).end(data) }) t.test('sync', t => { const expect = [['poop', poop]] new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t, expect) }) t.end() }) t.test('fail chmod', t => { const poop = new Error('poop') poop.code = 'EPOOP' const unmutate = mutateFS.fail('chmod', poop) const dir = path.join(unpackdir, 'chmod-fail') t.teardown(async _ => { unmutate() await rimraf(dir) }) const warnings = [] t.beforeEach(async () => { warnings.length = 0 await rimraf(dir) await mkdirp(dir) }) const data = makeTar([ { path: 'd/i/r/dir/', type: 'Directory', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/dir/', type: 'Directory', mode: 0o751, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, '', '', ]) const check = (t, expect) => { t.match(warnings, expect) warnings.forEach(w => t.equal(w[0], w[1].message)) t.end() } t.test('async', t => { const expect = [['poop', poop]] new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t, expect)).end(data) }) t.test('sync', t => { const expect = [['poop', poop]] new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t, expect) }) t.end() }) t.test('fail mkdir', t => { const poop = new Error('poop') poop.code = 'EPOOP' let unmutate const dir = path.join(unpackdir, 'mkdir-fail') t.teardown(_ => rimraf(dir)) const warnings = [] t.beforeEach(async () => { warnings.length = 0 await rimraf(dir) await mkdirp(dir) unmutate = mutateFS.fail('mkdir', poop) }) t.afterEach(() => unmutate()) const data = makeTar([ { path: 'dir/', type: 'Directory', mode: 0o751, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, '', '', ]) const expect = [[ 'ENOENT: no such file or directory', { code: 'ENOENT', syscall: 'lstat', path: normPath(path.resolve(dir, 'dir')), }, ]] const check = t => { t.match(warnings, expect) warnings.forEach(w => t.equal(w[0], w[1].message)) t.end() } t.test('sync', t => { new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t) }) t.test('async', t => { new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t)).end(data) }) t.end() }) t.test('fail write', t => { const poop = new Error('poop') poop.code = 'EPOOP' const unmutate = mutateFS.fail('write', poop) const dir = path.join(unpackdir, 'write-fail') t.teardown(async _ => { unmutate() await rimraf(dir) }) const warnings = [] t.beforeEach(async () => { warnings.length = 0 await rimraf(dir) await mkdirp(dir) }) const data = makeTar([ { path: 'x', type: 'File', size: 1, mode: 0o751, mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'x', '', '', ]) const expect = [['poop', poop]] const check = t => { t.match(warnings, expect) warnings.forEach(w => t.equal(w[0], w[1].message)) t.end() } t.test('async', t => { new Unpack({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { new UnpackSync({ cwd: dir, onwarn: (c, w, d) => warnings.push([w, d]), }).end(data) check(t) }) t.end() }) t.test('skip existing', t => { const dir = path.join(unpackdir, 'skip-newer') t.teardown(_ => rimraf(dir)) const date = new Date('2011-03-27T22:16:31.000Z') t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) fs.writeFileSync(dir + '/x', 'y') fs.utimesSync(dir + '/x', date, date) }) const data = makeTar([ { path: 'x', type: 'File', size: 1, mode: 0o751, mtime: new Date('2013-12-19T17:00:00.000Z'), }, 'x', '', '', ]) const check = t => { const st = fs.lstatSync(dir + '/x') t.equal(st.atime.toISOString(), date.toISOString()) t.equal(st.mtime.toISOString(), date.toISOString()) const data = fs.readFileSync(dir + '/x', 'utf8') t.equal(data, 'y') t.end() } t.test('async', t => { new Unpack({ cwd: dir, keep: true, }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { new UnpackSync({ cwd: dir, keep: true, }).end(data) check(t) }) t.end() }) t.test('skip newer', t => { const dir = path.join(unpackdir, 'skip-newer') t.teardown(_ => rimraf(dir)) const date = new Date('2013-12-19T17:00:00.000Z') t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) fs.writeFileSync(dir + '/x', 'y') fs.utimesSync(dir + '/x', date, date) }) const data = makeTar([ { path: 'x', type: 'File', size: 1, mode: 0o751, mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'x', '', '', ]) const check = t => { const st = fs.lstatSync(dir + '/x') t.equal(st.atime.toISOString(), date.toISOString()) t.equal(st.mtime.toISOString(), date.toISOString()) const data = fs.readFileSync(dir + '/x', 'utf8') t.equal(data, 'y') t.end() } t.test('async', t => { new Unpack({ cwd: dir, newer: true, }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { new UnpackSync({ cwd: dir, newer: true, }).end(data) check(t) }) t.end() }) t.test('no mtime', t => { const dir = path.join(unpackdir, 'skip-newer') t.teardown(_ => rimraf(dir)) t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) }) const date = new Date('2011-03-27T22:16:31.000Z') const data = makeTar([ { path: 'x/', type: 'Directory', size: 0, atime: date, ctime: date, mtime: date, }, { path: 'x/y', type: 'File', size: 1, mode: 0o751, atime: date, ctime: date, mtime: date, }, 'x', '', '', ]) const check = t => { // this may fail if it's run on March 27, 2011 const stx = fs.lstatSync(dir + '/x') t.not(stx.atime.toISOString(), date.toISOString()) t.not(stx.mtime.toISOString(), date.toISOString()) const sty = fs.lstatSync(dir + '/x/y') t.not(sty.atime.toISOString(), date.toISOString()) t.not(sty.mtime.toISOString(), date.toISOString()) const data = fs.readFileSync(dir + '/x/y', 'utf8') t.equal(data, 'x') t.end() } t.test('async', t => { new Unpack({ cwd: dir, noMtime: true, }).on('close', _ => check(t)).end(data) }) t.test('sync', t => { new UnpackSync({ cwd: dir, noMtime: true, }).end(data) check(t) }) t.end() }) t.test('unpack big enough to pause/drain', t => { const dir = path.resolve(unpackdir, 'drain-clog') mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) const stream = fs.createReadStream(fixtures + '/parses.tar') const u = new Unpack({ cwd: dir, strip: 3, strict: true, }) u.on('ignoredEntry', entry => t.fail('should not get ignored entry: ' + entry.path)) u.on('close', _ => { t.pass('extraction finished') const actual = fs.readdirSync(dir) const expected = fs.readdirSync(parses) t.same(actual, expected) t.end() }) stream.pipe(u) }) t.test('set owner', t => { // fake it on platforms that don't have getuid const myUid = 501 const myGid = 1024 const getuid = process.getuid const getgid = process.getgid process.getuid = _ => myUid process.getgid = _ => myGid t.teardown(_ => (process.getuid = getuid, process.getgid = getgid)) // can't actually do this because it requires root, but we can // verify that chown gets called. t.test('as root, defaults to true', t => { const getuid = process.getuid process.getuid = _ => 0 const u = new Unpack() t.equal(u.preserveOwner, true, 'preserveOwner enabled') process.getuid = getuid t.end() }) t.test('as non-root, defaults to false', t => { const getuid = process.getuid process.getuid = _ => 501 const u = new Unpack() t.equal(u.preserveOwner, false, 'preserveOwner disabled') process.getuid = getuid t.end() }) const data = makeTar([ { uid: 2456124561, gid: 813708013, path: 'foo/', type: 'Directory', }, { uid: myUid, gid: 813708013, path: 'foo/my-uid-different-gid', type: 'File', size: 3, }, 'qux', { uid: 2456124561, path: 'foo/different-uid-nogid', type: 'Directory', }, { uid: 2456124561, path: 'foo/different-uid-nogid/bar', type: 'File', size: 3, }, 'qux', { gid: 813708013, path: 'foo/different-gid-nouid/bar', type: 'File', size: 3, }, 'qux', { uid: myUid, gid: myGid, path: 'foo-mine/', type: 'Directory', }, { uid: myUid, gid: myGid, path: 'foo-mine/bar', type: 'File', size: 3, }, 'qux', { uid: myUid, path: 'foo-mine/nogid', type: 'Directory', }, { uid: myUid, path: 'foo-mine/nogid/bar', type: 'File', size: 3, }, 'qux', '', '', ]) t.test('chown failure results in unpack failure', t => { const dir = path.resolve(unpackdir, 'chown') const poop = new Error('expected chown failure') const un = mutateFS.fail('chown', poop) const unl = mutateFS.fail('lchown', poop) const unf = mutateFS.fail('fchown', poop) t.teardown(async () => { un() unf() unl() await rimraf(dir) }) t.test('sync', t => { mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) let warned = false const u = new Unpack.Sync({ cwd: dir, preserveOwner: true, onwarn: (c, m, er) => { if (!warned) { warned = true t.equal(er, poop) t.end() } }, }) u.end(data) }) t.test('async', t => { mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) let warned = false const u = new Unpack({ cwd: dir, preserveOwner: true, onwarn: (c, m, er) => { if (!warned) { warned = true t.equal(er, poop) t.end() } }, }) u.end(data) }) t.end() }) t.test('chown when true', t => { const dir = path.resolve(unpackdir, 'chown') const chown = fs.chown const lchown = fs.lchown const fchown = fs.fchown const chownSync = fs.chownSync const fchownSync = fs.fchownSync const lchownSync = fs.lchownSync let called = 0 fs.fchown = fs.chown = fs.lchown = (path, owner, group, cb) => { called++ cb() } fs.chownSync = fs.lchownSync = fs.fchownSync = _ => called++ t.teardown(_ => { fs.chown = chown fs.fchown = fchown fs.lchown = lchown fs.chownSync = chownSync fs.fchownSync = fchownSync fs.lchownSync = lchownSync }) t.test('sync', t => { mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) called = 0 const u = new Unpack.Sync({ cwd: dir, preserveOwner: true }) u.end(data) t.ok(called >= 5, 'called chowns') t.end() }) t.test('async', t => { mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) called = 0 const u = new Unpack({ cwd: dir, preserveOwner: true }) u.end(data) u.on('close', _ => { t.ok(called >= 5, 'called chowns') t.end() }) }) t.end() }) t.test('no chown when false', t => { const dir = path.resolve(unpackdir, 'nochown') const poop = new Error('poop') const un = mutateFS.fail('chown', poop) const unf = mutateFS.fail('fchown', poop) const unl = mutateFS.fail('lchown', poop) t.teardown(async _ => { un() unf() unl() await rimraf(dir) }) t.beforeEach(() => mkdirp(dir)) t.afterEach(() => rimraf(dir)) const check = t => { const dirStat = fs.statSync(dir + '/foo') t.not(dirStat.uid, 2456124561) t.not(dirStat.gid, 813708013) const fileStat = fs.statSync(dir + '/foo/my-uid-different-gid') t.not(fileStat.uid, 2456124561) t.not(fileStat.gid, 813708013) const dirStat2 = fs.statSync(dir + '/foo/different-uid-nogid') t.not(dirStat2.uid, 2456124561) const fileStat2 = fs.statSync(dir + '/foo/different-uid-nogid/bar') t.not(fileStat2.uid, 2456124561) t.end() } t.test('sync', t => { const u = new Unpack.Sync({ cwd: dir, preserveOwner: false }) u.end(data) check(t) }) t.test('async', t => { const u = new Unpack({ cwd: dir, preserveOwner: false }) u.end(data) u.on('close', _ => check(t)) }) t.end() }) t.end() }) t.test('unpack when dir is not writable', t => { const data = makeTar([ { path: 'a/', type: 'Directory', mode: 0o444, }, { path: 'a/b', type: 'File', size: 1, }, 'a', '', '', ]) const dir = path.resolve(unpackdir, 'nowrite-dir') t.beforeEach(() => mkdirp(dir)) t.afterEach(() => rimraf(dir)) const check = t => { t.equal(fs.statSync(dir + '/a').mode & 0o7777, isWindows ? 0o666 : 0o744) t.equal(fs.readFileSync(dir + '/a/b', 'utf8'), 'a') t.end() } t.test('sync', t => { const u = new Unpack.Sync({ cwd: dir, strict: true }) u.end(data) check(t) }) t.test('async', t => { const u = new Unpack({ cwd: dir, strict: true }) u.end(data) u.on('close', _ => check(t)) }) t.end() }) t.test('transmute chars on windows', t => { const data = makeTar([ { path: '<|>?:.txt', size: 5, type: 'File', }, '<|>?:', '', '', ]) const dir = path.resolve(unpackdir, 'winchars') t.beforeEach(() => mkdirp(dir)) t.afterEach(() => rimraf(dir)) const hex = 'ef80bcef81bcef80beef80bfef80ba2e747874' const uglyName = Buffer.from(hex, 'hex').toString() const ugly = path.resolve(dir, uglyName) const check = t => { t.same(fs.readdirSync(dir), [uglyName]) t.equal(fs.readFileSync(ugly, 'utf8'), '<|>?:') t.end() } t.test('async', t => { const u = new Unpack({ cwd: dir, win32: true, }) u.end(data) u.on('close', _ => check(t)) }) t.test('sync', t => { const u = new Unpack.Sync({ cwd: dir, win32: true, }) u.end(data) check(t) }) t.end() }) t.test('safely transmute chars on windows with absolutes', t => { // don't actually make the directory const poop = new Error('poop') t.teardown(mutateFS.fail('mkdir', poop)) const data = makeTar([ { path: 'c:/x/y/z/<|>?:.txt', size: 5, type: 'File', }, '<|>?:', '', '', ]) const hex = 'ef80bcef81bcef80beef80bfef80ba2e747874' const uglyName = Buffer.from(hex, 'hex').toString() const uglyPath = 'c:/x/y/z/' + uglyName const u = new Unpack({ win32: true, preservePaths: true, }) u.on('entry', entry => { t.equal(entry.path, uglyPath) t.end() }) u.end(data) }) t.test('use explicit chmod when required by umask', t => { process.umask(0o022) const basedir = path.resolve(unpackdir, 'umask-chmod') const data = makeTar([ { path: 'x/y/z', mode: 0o775, type: 'Directory', }, '', '', ]) const check = async t => { const st = fs.statSync(basedir + '/x/y/z') t.equal(st.mode & 0o777, isWindows ? 0o666 : 0o775) await rimraf(basedir) t.end() } t.test('async', t => { mkdirp.sync(basedir) const unpack = new Unpack({ cwd: basedir }) unpack.on('close', _ => check(t)) unpack.end(data) }) return t.test('sync', t => { mkdirp.sync(basedir) const unpack = new Unpack.Sync({ cwd: basedir }) unpack.end(data) check(t) }) }) t.test('dont use explicit chmod if noChmod flag set', t => { process.umask(0o022) const { umask } = process t.teardown(() => process.umask = umask) process.umask = () => { throw new Error('should not call process.umask()') } const basedir = path.resolve(unpackdir, 'umask-no-chmod') const data = makeTar([ { path: 'x/y/z', mode: 0o775, type: 'Directory', }, '', '', ]) const check = async t => { const st = fs.statSync(basedir + '/x/y/z') t.equal(st.mode & 0o777, isWindows ? 0o666 : 0o755) await rimraf(basedir) t.end() } t.test('async', t => { mkdirp.sync(basedir) const unpack = new Unpack({ cwd: basedir, noChmod: true }) unpack.on('close', _ => check(t)) unpack.end(data) }) return t.test('sync', t => { mkdirp.sync(basedir) const unpack = new Unpack.Sync({ cwd: basedir, noChmod: true}) unpack.end(data) check(t) }) }) t.test('chown implicit dirs and also the entries', t => { const basedir = path.resolve(unpackdir, 'chownr') // club these so that the test can run as non-root const chown = fs.chown const chownSync = fs.chownSync const lchown = fs.lchown const lchownSync = fs.lchownSync const fchown = fs.fchown const fchownSync = fs.fchownSync const getuid = process.getuid const getgid = process.getgid t.teardown(_ => { fs.chown = chown fs.chownSync = chownSync fs.lchown = lchown fs.lchownSync = lchownSync fs.fchown = fchown fs.fchownSync = fchownSync process.getgid = getgid }) let chowns = 0 let currentTest = null fs.lchown = fs.fchown = fs.chown = (path, uid, gid, cb) => { currentTest.equal(uid, 420, 'chown(' + path + ') uid') currentTest.equal(gid, 666, 'chown(' + path + ') gid') chowns++ cb() } fs.lchownSync = fs.chownSync = fs.fchownSync = (path, uid, gid) => { currentTest.equal(uid, 420, 'chownSync(' + path + ') uid') currentTest.equal(gid, 666, 'chownSync(' + path + ') gid') chowns++ } const data = makeTar([ { path: 'a/b/c', mode: 0o775, type: 'File', size: 1, uid: null, gid: null, }, '.', { path: 'x/y/z', mode: 0o775, uid: 12345, gid: 54321, type: 'File', size: 1, }, '.', '', '', ]) const check = async t => { currentTest = null t.equal(chowns, 8) chowns = 0 await rimraf(basedir) t.end() } t.test('throws when setting uid/gid improperly', t => { t.throws(_ => new Unpack({ uid: 420 }), TypeError('cannot set owner without number uid and gid')) t.throws(_ => new Unpack({ gid: 666 }), TypeError('cannot set owner without number uid and gid')) t.throws(_ => new Unpack({ uid: 1, gid: 2, preserveOwner: true }), TypeError('cannot preserve owner in archive and also set owner explicitly')) t.end() }) const tests = () => t.test('async', t => { currentTest = t mkdirp.sync(basedir) const unpack = new Unpack({ cwd: basedir, uid: 420, gid: 666 }) unpack.on('close', _ => check(t)) unpack.end(data) }).then(t.test('sync', t => { currentTest = t mkdirp.sync(basedir) const unpack = new Unpack.Sync({ cwd: basedir, uid: 420, gid: 666 }) unpack.end(data) check(t) })) tests() t.test('make it look like processUid is 420', t => { process.getuid = () => 420 t.end() }) tests() t.test('make it look like processGid is 666', t => { process.getuid = getuid process.getgid = () => 666 t.end() }) return tests() }) t.test('bad cwd setting', t => { const basedir = path.resolve(unpackdir, 'bad-cwd') mkdirp.sync(basedir) t.teardown(_ => rimraf(basedir)) const cases = [ // the cwd itself { path: './', type: 'Directory', }, // a file directly in the cwd { path: 'a', type: 'File', }, // a file nested within a subdir of the cwd { path: 'a/b/c', type: 'File', }, ] fs.writeFileSync(basedir + '/file', 'xyz') cases.forEach(c => t.test(c.type + ' ' + c.path, t => { const data = makeTar([ { path: c.path, mode: 0o775, type: c.type, size: 0, uid: null, gid: null, }, '', '', ]) t.test('cwd is a file', t => { const cwd = basedir + '/file' const opt = { cwd: cwd } t.throws(_ => new Unpack.Sync(opt).end(data), { name: 'CwdError', message: 'ENOTDIR: Cannot cd into \'' + normPath(cwd) + '\'', path: normPath(cwd), code: 'ENOTDIR', }) new Unpack(opt).on('error', er => { t.match(er, { name: 'CwdError', message: 'ENOTDIR: Cannot cd into \'' + normPath(cwd) + '\'', path: normPath(cwd), code: 'ENOTDIR', }) t.end() }).end(data) }) return t.test('cwd is missing', t => { const cwd = basedir + '/asdf/asdf/asdf' const opt = { cwd: cwd } t.throws(_ => new Unpack.Sync(opt).end(data), { name: 'CwdError', message: 'ENOENT: Cannot cd into \'' + normPath(cwd) + '\'', path: normPath(cwd), code: 'ENOENT', }) new Unpack(opt).on('error', er => { t.match(er, { name: 'CwdError', message: 'ENOENT: Cannot cd into \'' + normPath(cwd) + '\'', path: normPath(cwd), code: 'ENOENT', }) t.end() }).end(data) }) })) t.end() }) t.test('transform', t => { const basedir = path.resolve(unpackdir, 'transform') t.teardown(_ => rimraf(basedir)) const cases = { 'emptypax.tar': { '🌟.txt': '🌟✧✩⭐︎✪✫✬✭✮⚝✯✰✵✶✷✸✹❂⭑⭒★☆✡☪✴︎✦✡️🔯✴️🌠\n', 'one-byte.txt': '[a]', }, 'body-byte-counts.tar': { '1024-bytes.txt': new Array(1024).join('[x]') + '[\n]', '512-bytes.txt': new Array(512).join('[x]') + '[\n]', 'one-byte.txt': '[a]', 'zero-byte.txt': '', }, 'utf8.tar': { '🌟.txt': '🌟✧✩⭐︎✪✫✬✭✮⚝✯✰✵✶✷✸✹❂⭑⭒★☆✡☪✴︎✦✡️🔯✴️🌠\n', 'Ω.txt': '[Ω]', 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt': '[Ω]', }, } const txFn = entry => { switch (path.basename(entry.path)) { case 'zero-bytes.txt': return entry case 'one-byte.txt': case '1024-bytes.txt': case '512-bytes.txt': case 'Ω.txt': return new Bracer() } } class Bracer extends MiniPass { write (data) { const d = data.toString().split('').map(c => '[' + c + ']').join('') return super.write(d) } } const tarfiles = Object.keys(cases) t.plan(tarfiles.length) t.jobs = tarfiles.length tarfiles.forEach(tarfile => { t.test(tarfile, t => { const tf = path.resolve(tars, tarfile) const dir = path.resolve(basedir, tarfile) t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) }) const check = t => { const expect = cases[tarfile] Object.keys(expect).forEach(file => { const f = path.resolve(dir, file) t.equal(fs.readFileSync(f, 'utf8'), expect[file], file) }) t.end() } t.plan(2) t.test('async unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new Unpack({ cwd: dir, strict: true, transform: txFn }) fs.createReadStream(tf).pipe(unpack) eos(unpack, _ => check(t)) }) t.test('loose', t => { const unpack = new Unpack({ cwd: dir, transform: txFn }) fs.createReadStream(tf).pipe(unpack) eos(unpack, _ => check(t)) }) }) t.test('sync unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new UnpackSync({ cwd: dir, strict: true, transform: txFn }) unpack.end(fs.readFileSync(tf)) check(t) }) t.test('loose', t => { const unpack = new UnpackSync({ cwd: dir, transform: txFn }) unpack.end(fs.readFileSync(tf)) check(t) }) }) }) }) }) t.test('transform error', t => { const dir = path.resolve(unpackdir, 'transform-error') mkdirp.sync(dir) t.teardown(_ => rimraf(dir)) const tarfile = path.resolve(tars, 'body-byte-counts.tar') const tardata = fs.readFileSync(tarfile) const poop = new Error('poop') const txFn = () => { const tx = new MiniPass() tx.write = () => tx.emit('error', poop) tx.resume() return tx } t.test('sync unpack', t => { t.test('strict', t => { const unpack = new UnpackSync({ cwd: dir, strict: true, transform: txFn }) const expect = 3 let actual = 0 unpack.on('error', er => { t.equal(er, poop) actual++ }) unpack.end(tardata) t.equal(actual, expect, 'error count') t.end() }) t.test('loose', t => { const unpack = new UnpackSync({ cwd: dir, transform: txFn }) const expect = 3 let actual = 0 unpack.on('warn', (code, msg, er) => { t.equal(er, poop) actual++ }) unpack.end(tardata) t.equal(actual, expect, 'error count') t.end() }) t.end() }) t.test('async unpack', t => { // the last error is about the folder being deleted, just ignore that one t.test('strict', t => { const unpack = new Unpack({ cwd: dir, strict: true, transform: txFn }) t.plan(3) t.teardown(() => { unpack.removeAllListeners('error') unpack.on('error', () => {}) }) unpack.on('error', er => t.equal(er, poop)) unpack.end(tardata) }) t.test('loose', t => { const unpack = new Unpack({ cwd: dir, transform: txFn }) t.plan(3) t.teardown(() => unpack.removeAllListeners('warn')) unpack.on('warn', (code, msg, er) => t.equal(er, poop)) unpack.end(tardata) }) t.end() }) t.end() }) t.test('futimes/fchown failures', t => { const archive = path.resolve(tars, 'utf8.tar') const dir = path.resolve(unpackdir, 'futimes-fchown-fails') const tardata = fs.readFileSync(archive) const poop = new Error('poop') const second = new Error('second error') t.beforeEach(async () => { await rimraf(dir) await mkdirp(dir) }) t.teardown(() => rimraf(dir)) const methods = ['utimes', 'chown'] methods.forEach(method => { const fc = method === 'chown' t.test(method + ' fallback', t => { t.teardown(mutateFS.fail('f' + method, poop)) // forceChown will fail on systems where the user is not root // and/or the uid/gid in the archive aren't valid. We're just // verifying coverage here, so make the method auto-pass. t.teardown(mutateFS.pass(method)) t.plan(2) t.test('async unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new Unpack({ cwd: dir, strict: true, forceChown: fc }) unpack.on('finish', t.end) unpack.end(tardata) }) t.test('loose', t => { const unpack = new Unpack({ cwd: dir, forceChown: fc }) unpack.on('finish', t.end) unpack.on('warn', t.fail) unpack.end(tardata) }) }) t.test('sync unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new Unpack.Sync({ cwd: dir, strict: true, forceChown: fc }) unpack.end(tardata) t.end() }) t.test('loose', t => { const unpack = new Unpack.Sync({ cwd: dir, forceChown: fc }) unpack.on('warn', t.fail) unpack.end(tardata) t.end() }) }) }) t.test('also fail ' + method, t => { const unmutate = mutateFS.fail('f' + method, poop) const unmutate2 = mutateFS.fail(method, second) t.teardown(() => { unmutate() unmutate2() }) t.plan(2) t.test('async unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new Unpack({ cwd: dir, strict: true, forceChown: fc }) t.plan(3) unpack.on('error', er => t.equal(er, poop)) unpack.end(tardata) }) t.test('loose', t => { const unpack = new Unpack({ cwd: dir, forceChown: fc }) t.plan(3) unpack.on('warn', (code, m, er) => t.equal(er, poop)) unpack.end(tardata) }) }) t.test('sync unpack', t => { t.plan(2) t.test('strict', t => { const unpack = new Unpack.Sync({ cwd: dir, strict: true, forceChown: fc }) t.plan(3) unpack.on('error', er => t.equal(er, poop)) unpack.end(tardata) }) t.test('loose', t => { const unpack = new Unpack.Sync({ cwd: dir, forceChown: fc }) t.plan(3) unpack.on('warn', (c, m, er) => t.equal(er, poop)) unpack.end(tardata) }) }) }) }) t.end() }) t.test('onentry option is preserved', t => { const basedir = path.resolve(unpackdir, 'onentry-method') mkdirp.sync(basedir) t.teardown(() => rimraf(basedir)) let oecalls = 0 const onentry = entry => oecalls++ const data = makeTar([ { path: 'd/i', type: 'Directory', }, { path: 'd/i/r/dir', type: 'Directory', mode: 0o751, mtime: new Date('2011-03-27T22:16:31.000Z'), }, { path: 'd/i/r/file', type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), }, 'a', '', '', ]) const check = t => { t.equal(oecalls, 3) oecalls = 0 t.end() } t.test('sync', t => { const dir = path.join(basedir, 'sync') mkdirp.sync(dir) const unpack = new UnpackSync({ cwd: dir, onentry }) unpack.end(data) check(t) }) t.test('async', t => { const dir = path.join(basedir, 'async') mkdirp.sync(dir) const unpack = new Unpack({ cwd: dir, onentry }) unpack.on('finish', () => check(t)) unpack.end(data) }) t.end() }) t.test('do not reuse hardlinks, only nlink=1 files', t => { const basedir = path.resolve(unpackdir, 'hardlink-reuse') mkdirp.sync(basedir) t.teardown(() => rimraf(basedir)) const now = new Date('2018-04-30T18:30:39.025Z') const data = makeTar([ { path: 'overwriteme', type: 'File', size: 4, mode: 0o644, mtime: now, }, 'foo\n', { path: 'link', linkpath: 'overwriteme', type: 'Link', mode: 0o644, mtime: now, }, { path: 'link', type: 'File', size: 4, mode: 0o644, mtime: now, }, 'bar\n', '', '', ]) const checks = { link: 'bar\n', overwriteme: 'foo\n', } const check = t => { for (const f in checks) { t.equal(fs.readFileSync(basedir + '/' + f, 'utf8'), checks[f], f) t.equal(fs.statSync(basedir + '/' + f).nlink, 1, f) } t.end() } t.test('async', t => { const u = new Unpack({ cwd: basedir }) u.on('close', () => check(t)) u.end(data) }) t.test('sync', t => { const u = new UnpackSync({ cwd: basedir }) u.end(data) check(t) }) t.end() }) t.test('trying to unpack a non-zlib gzip file should fail', t => { const data = Buffer.from('hello this is not gzip data') const dataGzip = Buffer.concat([Buffer.from([0x1f, 0x8b]), data]) const basedir = path.resolve(unpackdir, 'bad-archive') t.test('abort if gzip has an error', t => { t.plan(2) const expect = { message: /^zlib/, errno: Number, code: /^Z/, recoverable: false, cwd: normPath(basedir), tarCode: 'TAR_ABORT', } const opts = { cwd: basedir, gzip: true, } new Unpack(opts) .once('error', er => t.match(er, expect, 'async emits')) .end(dataGzip) const skip = !/^v([0-9]|1[0-3])\./.test(process.version) ? false : 'node prior to v14 did not raise sync zlib errors properly' t.throws(() => new UnpackSync(opts).end(dataGzip), expect, 'sync throws', {skip}) }) t.test('bad archive if no gzip', t => { t.plan(2) const expect = { tarCode: 'TAR_BAD_ARCHIVE', recoverable: false, } const opts = { cwd: basedir } new Unpack(opts) .on('error', er => t.match(er, expect, 'async emits')) .end(data) t.throws(() => new UnpackSync(opts).end(data), expect, 'sync throws') }) t.end() }) t.test('handle errors on fs.close', t => { const poop = new Error('poop') const { close, closeSync } = fs // have to actually close them, or else windows gets mad fs.close = (fd, cb) => close(fd, () => cb(poop)) fs.closeSync = (fd) => { closeSync(fd) throw poop } t.teardown(() => Object.assign(fs, { close, closeSync })) const dir = path.resolve(unpackdir, 'close-fail') mkdirp.sync(dir + '/sync') mkdirp.sync(dir + '/async') const data = makeTar([ { path: 'file', type: 'File', size: 1, atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, 'a', '', '', ]) t.plan(2) new Unpack({ cwd: dir + '/async', strict: true }) .on('error', er => t.equal(er, poop, 'async')) .end(data) t.throws(() => new UnpackSync({ cwd: normPath(dir + '/sync'), strict: true, }).end(data), poop, 'sync') }) t.test('drop entry from dirCache if no longer a directory', { skip: isWindows && 'symlinks not fully supported', }, t => { const dir = path.resolve(unpackdir, 'dir-cache-error') mkdirp.sync(dir + '/sync/y') mkdirp.sync(dir + '/async/y') const data = makeTar([ { path: 'x', type: 'Directory', }, { path: 'x', type: 'SymbolicLink', linkpath: './y', }, { path: 'x/ginkoid', type: 'File', size: 'ginkoid'.length, }, 'ginkoid', '', '', ]) t.plan(2) const WARNINGS = {} const check = (t, path) => { t.equal(fs.statSync(path + '/x').isDirectory(), true) t.equal(fs.lstatSync(path + '/x').isSymbolicLink(), true) t.equal(fs.statSync(path + '/y').isDirectory(), true) t.strictSame(fs.readdirSync(path + '/y'), []) t.throws(() => fs.readFileSync(path + '/x/ginkoid'), { code: 'ENOENT' }) t.strictSame(WARNINGS[path], [ 'TAR_ENTRY_ERROR', 'Cannot extract through symbolic link', ]) t.end() } t.test('async', t => { const path = dir + '/async' new Unpack({ cwd: path }) .on('warn', (code, msg) => WARNINGS[path] = [code, msg]) .on('end', () => check(t, path)) .end(data) }) t.test('sync', t => { const path = dir + '/sync' new UnpackSync({ cwd: path }) .on('warn', (code, msg) => WARNINGS[path] = [code, msg]) .end(data) check(t, path) }) }) t.test('using strip option when top level file exists', t => { const dir = path.resolve(unpackdir, 'strip-with-top-file') mkdirp.sync(dir + '/sync/y') mkdirp.sync(dir + '/async/y') const data = makeTar([ { path: 'top', type: 'File', size: 0, }, { path: 'x', type: 'Directory', }, { path: 'x/a', type: 'File', size: 'a'.length, }, 'a', { path: 'y', type: 'GNUDumpDir', }, { path: 'y/b', type: 'File', size: 'b'.length, }, 'b', '', '', ]) t.plan(2) const check = (t, path) => { t.equal(fs.statSync(path).isDirectory(), true) t.equal(fs.readFileSync(path + '/a', 'utf8'), 'a') t.equal(fs.readFileSync(path + '/b', 'utf8'), 'b') t.throws(() => fs.statSync(path + '/top'), { code: 'ENOENT' }) t.end() } t.test('async', t => { const path = dir + '/async' new Unpack({ cwd: path, strip: 1 }) .on('end', () => check(t, path)) .end(data) }) t.test('sync', t => { const path = dir + '/sync' new UnpackSync({ cwd: path, strip: 1 }).end(data) check(t, path) }) }) t.test('handle EPERMs when creating symlinks', t => { // https://github.com/npm/node-tar/issues/265 const msg = 'You do not have sufficient privilege to perform this operation.' const er = Object.assign(new Error(msg), { code: 'EPERM', }) t.teardown(mutateFS.fail('symlink', er)) const data = makeTar([ { path: 'x', type: 'Directory', }, { path: 'x/y', type: 'File', size: 'hello, world'.length, }, 'hello, world', { path: 'x/link1', type: 'SymbolicLink', linkpath: './y', }, { path: 'x/link2', type: 'SymbolicLink', linkpath: './y', }, { path: 'x/link3', type: 'SymbolicLink', linkpath: './y', }, { path: 'x/z', type: 'File', size: 'hello, world'.length, }, 'hello, world', '', '', ]) const dir = path.resolve(unpackdir, 'eperm-symlinks') mkdirp.sync(`${dir}/sync`) mkdirp.sync(`${dir}/async`) const check = path => { t.match(WARNINGS, [ ['TAR_ENTRY_ERROR', msg], ['TAR_ENTRY_ERROR', msg], ['TAR_ENTRY_ERROR', msg], ], 'got expected warnings') t.equal(WARNINGS.length, 3) WARNINGS.length = 0 t.equal(fs.readFileSync(`${path}/x/y`, 'utf8'), 'hello, world') t.equal(fs.readFileSync(`${path}/x/z`, 'utf8'), 'hello, world') t.throws(() => fs.statSync(`${path}/x/link1`), { code: 'ENOENT' }) t.throws(() => fs.statSync(`${path}/x/link2`), { code: 'ENOENT' }) t.throws(() => fs.statSync(`${path}/x/link3`), { code: 'ENOENT' }) } const WARNINGS = [] const u = new Unpack({ cwd: `${dir}/async`, onwarn: (code, msg, er) => WARNINGS.push([code, msg]), }) u.on('end', () => { check(`${dir}/async`) const u = new UnpackSync({ cwd: `${dir}/sync`, onwarn: (code, msg, er) => WARNINGS.push([code, msg]), }) u.end(data) check(`${dir}/sync`) t.end() }) u.end(data) }) t.test('close fd when error writing', t => { const data = makeTar([ { type: 'Directory', path: 'x', }, { type: 'File', size: 1, path: 'x/y', }, '.', '', '', ]) t.teardown(mutateFS.fail('write', new Error('nope'))) const CLOSES = [] const OPENS = {} const {open} = require('fs') t.teardown(() => fs.open = open) fs.open = (...args) => { const cb = args.pop() args.push((er, fd) => { OPENS[args[0]] = fd cb(er, fd) }) return open.call(fs, ...args) } t.teardown(mutateFS.mutateArgs('close', ([fd]) => { CLOSES.push(fd) return [fd] })) const WARNINGS = [] const dir = path.resolve(unpackdir, 'close-on-write-error') mkdirp.sync(dir) const unpack = new Unpack({ cwd: dir, onwarn: (code, msg) => WARNINGS.push([code, msg]), }) unpack.on('end', () => { for (const [path, fd] of Object.entries(OPENS)) t.equal(CLOSES.includes(fd), true, 'closed fd for ' + path) t.end() }) unpack.end(data) }) t.test('close fd when error setting mtime', t => { const data = makeTar([ { type: 'Directory', path: 'x', }, { type: 'File', size: 1, path: 'x/y', atime: new Date('1979-07-01T19:10:00.000Z'), ctime: new Date('2011-03-27T22:16:31.000Z'), mtime: new Date('2011-03-27T22:16:31.000Z'), }, '.', '', '', ]) // have to clobber these both, because we fall back t.teardown(mutateFS.fail('futimes', new Error('nope'))) t.teardown(mutateFS.fail('utimes', new Error('nooooope'))) const CLOSES = [] const OPENS = {} const {open} = require('fs') t.teardown(() => fs.open = open) fs.open = (...args) => { const cb = args.pop() args.push((er, fd) => { OPENS[args[0]] = fd cb(er, fd) }) return open.call(fs, ...args) } t.teardown(mutateFS.mutateArgs('close', ([fd]) => { CLOSES.push(fd) return [fd] })) const WARNINGS = [] const dir = path.resolve(unpackdir, 'close-on-futimes-error') mkdirp.sync(dir) const unpack = new Unpack({ cwd: dir, onwarn: (code, msg) => WARNINGS.push([code, msg]), }) unpack.on('end', () => { for (const [path, fd] of Object.entries(OPENS)) t.equal(CLOSES.includes(fd), true, 'closed fd for ' + path) t.end() }) unpack.end(data) }) t.test('do not hang on large files that fail to open()', t => { const data = makeTar([ { type: 'Directory', path: 'x', }, { type: 'File', size: 31745, path: 'x/y', }, 'x'.repeat(31745), '', '', ]) t.teardown(mutateFS.fail('open', new Error('nope'))) const dir = path.resolve(unpackdir, 'no-hang-for-large-file-failures') mkdirp.sync(dir) const WARNINGS = [] const unpack = new Unpack({ cwd: dir, onwarn: (code, msg) => WARNINGS.push([code, msg]), }) unpack.on('end', () => { t.strictSame(WARNINGS, [['TAR_ENTRY_ERROR', 'nope']]) t.end() }) unpack.write(data.slice(0, 2048)) setTimeout(() => { unpack.write(data.slice(2048, 4096)) setTimeout(() => { unpack.write(data.slice(4096)) setTimeout(() => { unpack.end() }) }) }) }) t.test('dirCache pruning unicode normalized collisions', { skip: isWindows && 'symlinks not fully supported', }, t => { const data = makeTar([ { type: 'Directory', path: 'foo', }, { type: 'File', path: 'foo/bar', size: 1, }, 'x', { type: 'Directory', // café path: Buffer.from([0x63, 0x61, 0x66, 0xc3, 0xa9]).toString(), }, { type: 'SymbolicLink', // cafe with a ` path: Buffer.from([0x63, 0x61, 0x66, 0x65, 0xcc, 0x81]).toString(), linkpath: 'foo', }, { type: 'Directory', path: 'foo', }, { type: 'File', path: Buffer.from([0x63, 0x61, 0x66, 0xc3, 0xa9]).toString() + '/bar', size: 1, }, 'y', '', '', ]) const check = (path, dirCache, t) => { path = path.replace(/\\/g, '/') t.strictSame([...dirCache.entries()][0], [`${path}/foo`, true]) t.equal(fs.readFileSync(path + '/foo/bar', 'utf8'), 'x') t.end() } t.test('sync', t => { const path = t.testdir() const dirCache = new Map() new UnpackSync({ cwd: path, dirCache }).end(data) check(path, dirCache, t) }) t.test('async', t => { const path = t.testdir() const dirCache = new Map() new Unpack({ cwd: path, dirCache }) .on('close', () => check(path, dirCache, t)) .end(data) }) t.end() }) t.test('dircache prune all on windows when symlink encountered', t => { if (process.platform !== 'win32') { process.env.TESTING_TAR_FAKE_PLATFORM = 'win32' t.teardown(() => { delete process.env.TESTING_TAR_FAKE_PLATFORM }) } const symlinks = [] const Unpack = t.mock('../lib/unpack.js', { fs: { ...fs, symlink: (target, dest, cb) => { symlinks.push(['async', target, dest]) process.nextTick(cb) }, symlinkSync: (target, dest) => symlinks.push(['sync', target, dest]), }, }) const UnpackSync = Unpack.Sync const data = makeTar([ { type: 'Directory', path: 'foo', }, { type: 'File', path: 'foo/bar', size: 1, }, 'x', { type: 'Directory', // café path: Buffer.from([0x63, 0x61, 0x66, 0xc3, 0xa9]).toString(), }, { type: 'SymbolicLink', // cafe with a ` path: Buffer.from([0x63, 0x61, 0x66, 0x65, 0xcc, 0x81]).toString(), linkpath: 'safe/actually/but/cannot/be/too/careful', }, { type: 'File', path: 'bar/baz', size: 1, }, 'z', '', '', ]) const check = (path, dirCache, t) => { // symlink blew away all dirCache entries before it path = path.replace(/\\/g, '/') t.strictSame([...dirCache.entries()], [ [`${path}/bar`, true], ]) t.equal(fs.readFileSync(`${path}/foo/bar`, 'utf8'), 'x') t.equal(fs.readFileSync(`${path}/bar/baz`, 'utf8'), 'z') t.end() } t.test('sync', t => { const path = t.testdir() const dirCache = new Map() new UnpackSync({ cwd: path, dirCache }).end(data) check(path, dirCache, t) }) t.test('async', t => { const path = t.testdir() const dirCache = new Map() new Unpack({ cwd: path, dirCache }) .on('close', () => check(path, dirCache, t)) .end(data) }) t.end() }) t.test('recognize C:.. as a dot path part', t => { if (process.platform !== 'win32') { process.env.TESTING_TAR_FAKE_PLATFORM = 'win32' t.teardown(() => { delete process.env.TESTING_TAR_FAKE_PLATFORM }) } const Unpack = t.mock('../lib/unpack.js', { path: { ...path.win32, win32: path.win32, posix: path.posix, }, }) const UnpackSync = Unpack.Sync const data = makeTar([ { type: 'File', path: 'C:../x/y/z', size: 1, }, 'z', { type: 'File', path: 'x:..\\y\\z', size: 1, }, 'x', { type: 'File', path: 'Y:foo', size: 1, }, 'y', '', '', ]) const check = (path, warnings, t) => { t.equal(fs.readFileSync(`${path}/foo`, 'utf8'), 'y') t.strictSame(warnings, [ [ 'TAR_ENTRY_ERROR', "path contains '..'", 'C:../x/y/z', 'C:../x/y/z', ], ['TAR_ENTRY_ERROR', "path contains '..'", 'x:../y/z', 'x:../y/z'], [ 'TAR_ENTRY_INFO', 'stripping Y: from absolute path', 'Y:foo', 'foo', ], ]) t.end() } t.test('async', t => { const warnings = [] const path = t.testdir() new Unpack({ cwd: path, onwarn: (c, w, { entry, path }) => warnings.push([c, w, path, entry.path]), }) .on('close', () => check(path, warnings, t)) .end(data) }) t.test('sync', t => { const warnings = [] const path = t.testdir() new UnpackSync({ cwd: path, onwarn: (c, w, { entry, path }) => warnings.push([c, w, path, entry.path]), }).end(data) check(path, warnings, t) }) t.end() })