node-tar/test/unpack.js

3231 lines
79 KiB
JavaScript

'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()
})