'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.slice(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',
      // café
      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',
      // café
      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()
})
