698 lines
16 KiB
JavaScript
698 lines
16 KiB
JavaScript
'use strict'
|
|
const t = require('tap')
|
|
const Parse = require('../lib/parse.js')
|
|
|
|
const makeTar = require('./make-tar.js')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const tardir = path.resolve(__dirname, 'fixtures/tars')
|
|
const zlib = require('zlib')
|
|
const MiniPass = require('minipass')
|
|
const Header = require('../lib/header.js')
|
|
const EE = require('events').EventEmitter
|
|
|
|
t.test('fixture tests', t => {
|
|
class ByteStream extends MiniPass {
|
|
write (chunk) {
|
|
for (let i = 0; i < chunk.length - 1; i++) {
|
|
super.write(chunk.slice(i, i + 1))
|
|
}
|
|
|
|
return super.write(chunk.slice(chunk.length - 1, chunk.length))
|
|
}
|
|
}
|
|
|
|
const trackEvents = (t, expect, p, slow) => {
|
|
let ok = true
|
|
let cursor = 0
|
|
p.on('entry', entry => {
|
|
ok = ok && t.match(['entry', entry], expect[cursor++], entry.path)
|
|
if (slow) {
|
|
setTimeout(_ => entry.resume())
|
|
} else {
|
|
entry.resume()
|
|
}
|
|
})
|
|
p.on('ignoredEntry', entry => {
|
|
ok = ok && t.match(['ignoredEntry', entry], expect[cursor++],
|
|
'ignored: ' + entry.path)
|
|
})
|
|
p.on('warn', (c, message, data) => {
|
|
ok = ok && t.match(['warn', c, message], expect[cursor++], 'warn')
|
|
})
|
|
p.on('nullBlock', _ => {
|
|
ok = ok && t.match(['nullBlock'], expect[cursor++], 'null')
|
|
})
|
|
p.on('error', er => {
|
|
ok = ok && t.match(['error', er], expect[cursor++], 'error')
|
|
})
|
|
p.on('meta', meta => {
|
|
ok = ok && t.match(['meta', meta], expect[cursor++], 'meta')
|
|
})
|
|
p.on('eof', _ => {
|
|
ok = ok && t.match(['eof'], expect[cursor++], 'eof')
|
|
})
|
|
p.on('end', _ => {
|
|
ok = ok && t.match(['end'], expect[cursor++], 'end')
|
|
t.end()
|
|
})
|
|
}
|
|
|
|
t.jobs = 4
|
|
const path = require('path')
|
|
const parsedir = path.resolve(__dirname, 'fixtures/parse')
|
|
const files = fs.readdirSync(tardir)
|
|
const maxMetaOpt = [250, null]
|
|
const filterOpt = [true, false]
|
|
const strictOpt = [true, false]
|
|
const runTest = (file, maxMeta, filter, strict) => {
|
|
const tardata = fs.readFileSync(file)
|
|
const base = path.basename(file, '.tar')
|
|
t.test('file=' + base + '.tar' +
|
|
' maxmeta=' + maxMeta +
|
|
' filter=' + filter +
|
|
' strict=' + strict, t => {
|
|
const o =
|
|
(maxMeta ? '-meta-' + maxMeta : '') +
|
|
(filter ? '-filter' : '') +
|
|
(strict ? '-strict' : '')
|
|
const tail = (o ? '-' + o : '') + '.json'
|
|
const eventsFile = parsedir + '/' + base + tail
|
|
const expect = require(eventsFile)
|
|
|
|
t.test('one byte at a time', t => {
|
|
const bs = new ByteStream()
|
|
const opt = (maxMeta || filter || strict) ? {
|
|
maxMetaEntrySize: maxMeta,
|
|
filter: filter ? (path, entry) => entry.size % 2 !== 0 : null,
|
|
strict: strict,
|
|
} : null
|
|
const bp = new Parse(opt)
|
|
trackEvents(t, expect, bp)
|
|
bs.pipe(bp)
|
|
bs.end(tardata)
|
|
})
|
|
|
|
t.test('all at once', t => {
|
|
const p = new Parse({
|
|
maxMetaEntrySize: maxMeta,
|
|
filter: filter ? (path, entry) => entry.size % 2 !== 0 : null,
|
|
strict: strict,
|
|
})
|
|
trackEvents(t, expect, p)
|
|
p.end(tardata)
|
|
})
|
|
|
|
t.test('gzipped all at once', t => {
|
|
const p = new Parse({
|
|
maxMetaEntrySize: maxMeta,
|
|
filter: filter ? (path, entry) => entry.size % 2 !== 0 : null,
|
|
strict: strict,
|
|
})
|
|
trackEvents(t, expect, p)
|
|
p.end(zlib.gzipSync(tardata))
|
|
})
|
|
|
|
t.test('gzipped byte at a time', t => {
|
|
const bs = new ByteStream()
|
|
const bp = new Parse({
|
|
maxMetaEntrySize: maxMeta,
|
|
filter: filter ? (path, entry) => entry.size % 2 !== 0 : null,
|
|
strict: strict,
|
|
})
|
|
trackEvents(t, expect, bp)
|
|
bs.pipe(bp)
|
|
bs.end(zlib.gzipSync(tardata))
|
|
})
|
|
|
|
t.test('async chunks', t => {
|
|
const p = new Parse({
|
|
maxMetaEntrySize: maxMeta,
|
|
filter: filter ? (path, entry) => entry.size % 2 !== 0 : null,
|
|
strict: strict,
|
|
})
|
|
trackEvents(t, expect, p, true)
|
|
p.write(tardata.slice(0, Math.floor(tardata.length / 2)))
|
|
process.nextTick(_ => p.end(tardata.slice(Math.floor(tardata.length / 2))))
|
|
})
|
|
|
|
t.end()
|
|
})
|
|
}
|
|
|
|
files
|
|
.map(f => path.resolve(tardir, f)).forEach(file =>
|
|
maxMetaOpt.forEach(maxMeta =>
|
|
strictOpt.forEach(strict =>
|
|
filterOpt.forEach(filter =>
|
|
runTest(file, maxMeta, filter, strict)))))
|
|
t.end()
|
|
})
|
|
|
|
t.test('strict warn with an error emits that error', t => {
|
|
t.plan(1)
|
|
const p = new Parse({
|
|
strict: true,
|
|
})
|
|
p.on('error', emitted => t.equal(emitted, er))
|
|
const er = new Error('yolo')
|
|
p.warn('TAR_TEST', er)
|
|
})
|
|
|
|
t.test('onwarn gets added to the warn event', t => {
|
|
t.plan(1)
|
|
const p = new Parse({
|
|
onwarn (code, message) {
|
|
t.equal(message, 'this is fine')
|
|
},
|
|
})
|
|
p.warn('TAR_TEST', 'this is fine')
|
|
})
|
|
|
|
t.test('onentry gets added to entry event', t => {
|
|
t.plan(1)
|
|
const p = new Parse({
|
|
onentry: entry => t.equal(entry, 'yes hello this is dog'),
|
|
})
|
|
p.emit('entry', 'yes hello this is dog')
|
|
})
|
|
|
|
t.test('drain event timings', t => {
|
|
let sawOndone = false
|
|
const ondone = function () {
|
|
sawOndone = true
|
|
this.emit('prefinish')
|
|
this.emit('finish')
|
|
this.emit('end')
|
|
this.emit('close')
|
|
}
|
|
|
|
// write 1 header and body, write 2 header, verify false return
|
|
// wait for drain event before continuing.
|
|
// write 2 body, 3 header and body, 4 header, verify false return
|
|
// wait for drain event
|
|
// write 4 body and null blocks
|
|
|
|
const data = [
|
|
[
|
|
{
|
|
path: 'one',
|
|
size: 513,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('1'),
|
|
'1',
|
|
{
|
|
path: 'two',
|
|
size: 513,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('2'),
|
|
'2',
|
|
{
|
|
path: 'three',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
],
|
|
[
|
|
new Array(513).join('3'),
|
|
new Array(513).join('3'),
|
|
{
|
|
path: 'four',
|
|
size: 513,
|
|
type: 'File',
|
|
},
|
|
],
|
|
[
|
|
new Array(513).join('4'),
|
|
'4',
|
|
{
|
|
path: 'five',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('5'),
|
|
new Array(513).join('5'),
|
|
{
|
|
path: 'six',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('6'),
|
|
new Array(513).join('6'),
|
|
{
|
|
path: 'seven',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('7'),
|
|
new Array(513).join('7'),
|
|
{
|
|
path: 'eight',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('8'),
|
|
new Array(513).join('8'),
|
|
{
|
|
path: 'four',
|
|
size: 513,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('4'),
|
|
'4',
|
|
{
|
|
path: 'five',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('5'),
|
|
new Array(513).join('5'),
|
|
{
|
|
path: 'six',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('6'),
|
|
new Array(513).join('6'),
|
|
{
|
|
path: 'seven',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('7'),
|
|
new Array(513).join('7'),
|
|
{
|
|
path: 'eight',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('8'),
|
|
],
|
|
[
|
|
new Array(513).join('8'),
|
|
{
|
|
path: 'nine',
|
|
size: 1537,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('9'),
|
|
],
|
|
[new Array(513).join('9')],
|
|
[new Array(513).join('9')],
|
|
['9'],
|
|
].map(chunks => makeTar(chunks))
|
|
|
|
const expect = [
|
|
'one', 'two', 'three',
|
|
'four', 'five', 'six', 'seven', 'eight',
|
|
'four', 'five', 'six', 'seven', 'eight',
|
|
'nine',
|
|
'one', 'two', 'three',
|
|
'four', 'five', 'six', 'seven', 'eight',
|
|
'four', 'five', 'six', 'seven', 'eight',
|
|
'nine',
|
|
]
|
|
|
|
class SlowStream extends EE {
|
|
write () {
|
|
setTimeout(_ => this.emit('drain'))
|
|
return false
|
|
}
|
|
|
|
end () {
|
|
return this.write()
|
|
}
|
|
}
|
|
|
|
let currentEntry
|
|
const autoPipe = true
|
|
const p = new Parse({
|
|
ondone,
|
|
onentry: entry => {
|
|
t.equal(entry.path, expect.shift())
|
|
currentEntry = entry
|
|
if (autoPipe) {
|
|
setTimeout(_ => entry.pipe(new SlowStream()))
|
|
}
|
|
},
|
|
})
|
|
|
|
data.forEach(d => {
|
|
if (!t.equal(p.write(d), false, 'write should return false')) {
|
|
return t.end()
|
|
}
|
|
})
|
|
|
|
let interval
|
|
const go = _ => {
|
|
const d = data.shift()
|
|
if (d === undefined) {
|
|
return p.end()
|
|
}
|
|
|
|
let paused
|
|
if (currentEntry) {
|
|
currentEntry.pause()
|
|
paused = true
|
|
}
|
|
|
|
const hunklen = Math.floor(d.length / 2)
|
|
const hunks = [
|
|
d.slice(0, hunklen),
|
|
d.slice(hunklen),
|
|
]
|
|
p.write(hunks[0])
|
|
|
|
if (currentEntry && !paused) {
|
|
console.error('has current entry')
|
|
currentEntry.pause()
|
|
paused = true
|
|
}
|
|
|
|
if (!t.equal(p.write(hunks[1]), false, 'write should return false: ' + d)) {
|
|
return t.end()
|
|
}
|
|
|
|
p.once('drain', go)
|
|
|
|
if (paused) {
|
|
currentEntry.resume()
|
|
}
|
|
}
|
|
|
|
p.once('drain', go)
|
|
p.on('end', _ => {
|
|
clearInterval(interval)
|
|
t.ok(sawOndone)
|
|
t.end()
|
|
})
|
|
go()
|
|
})
|
|
|
|
t.test('consume while consuming', t => {
|
|
const data = makeTar([
|
|
{
|
|
path: 'one',
|
|
size: 0,
|
|
type: 'File',
|
|
},
|
|
{
|
|
path: 'zero',
|
|
size: 0,
|
|
type: 'File',
|
|
},
|
|
{
|
|
path: 'two',
|
|
size: 513,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('2'),
|
|
'2',
|
|
{
|
|
path: 'three',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('3'),
|
|
new Array(513).join('3'),
|
|
{
|
|
path: 'zero',
|
|
size: 0,
|
|
type: 'File',
|
|
},
|
|
{
|
|
path: 'zero',
|
|
size: 0,
|
|
type: 'File',
|
|
},
|
|
{
|
|
path: 'four',
|
|
size: 1024,
|
|
type: 'File',
|
|
},
|
|
new Array(513).join('4'),
|
|
new Array(513).join('4'),
|
|
{
|
|
path: 'zero',
|
|
size: 0,
|
|
type: 'File',
|
|
},
|
|
{
|
|
path: 'zero',
|
|
size: 0,
|
|
type: 'File',
|
|
},
|
|
])
|
|
|
|
const runTest = (t, size) => {
|
|
const p = new Parse()
|
|
const first = data.slice(0, size)
|
|
const rest = data.slice(size)
|
|
p.once('entry', entry => {
|
|
for (let pos = 0; pos < rest.length; pos += size) {
|
|
p.write(rest.slice(pos, pos + size))
|
|
}
|
|
|
|
p.end()
|
|
})
|
|
.on('entry', entry => entry.resume())
|
|
.on('end', _ => t.end())
|
|
.write(first)
|
|
}
|
|
|
|
// one that aligns, and another that doesn't, so that we
|
|
// get some cases where there's leftover chunk and a buffer
|
|
t.test('size=1000', t => runTest(t, 1000))
|
|
t.test('size=1024', t => runTest(t, 4096))
|
|
t.end()
|
|
})
|
|
|
|
t.test('truncated input', t => {
|
|
const data = makeTar([
|
|
{
|
|
path: 'foo/',
|
|
type: 'Directory',
|
|
},
|
|
{
|
|
path: 'foo/bar',
|
|
type: 'File',
|
|
size: 18,
|
|
},
|
|
])
|
|
|
|
t.test('truncated at block boundary', t => {
|
|
const warnings = []
|
|
const p = new Parse({ onwarn: (c, message) => warnings.push(message) })
|
|
p.end(data)
|
|
t.same(warnings, [
|
|
'Truncated input (needed 512 more bytes, only 0 available)',
|
|
])
|
|
t.end()
|
|
})
|
|
|
|
t.test('truncated mid-block', t => {
|
|
const warnings = []
|
|
const p = new Parse({ onwarn: (c, message) => warnings.push(message) })
|
|
p.write(data)
|
|
p.end(Buffer.from('not a full block'))
|
|
t.same(warnings, [
|
|
'Truncated input (needed 512 more bytes, only 16 available)',
|
|
])
|
|
t.end()
|
|
})
|
|
|
|
t.end()
|
|
})
|
|
|
|
t.test('truncated gzip input', t => {
|
|
const raw = makeTar([
|
|
{
|
|
path: 'foo/',
|
|
type: 'Directory',
|
|
},
|
|
{
|
|
path: 'foo/bar',
|
|
type: 'File',
|
|
size: 18,
|
|
},
|
|
new Array(19).join('x'),
|
|
'',
|
|
'',
|
|
])
|
|
const tgz = zlib.gzipSync(raw)
|
|
const split = Math.floor(tgz.length * 2 / 3)
|
|
const trunc = tgz.slice(0, split)
|
|
|
|
const skipEarlyEnd = process.version.match(/^v4\./)
|
|
t.test('early end', {
|
|
skip: skipEarlyEnd ? 'not a zlib error on v4' : false,
|
|
}, t => {
|
|
const warnings = []
|
|
const p = new Parse()
|
|
p.on('error', er => warnings.push(er.message))
|
|
let aborted = false
|
|
p.on('abort', _ => aborted = true)
|
|
p.end(trunc)
|
|
t.equal(aborted, true, 'aborted writing')
|
|
t.same(warnings, ['zlib: unexpected end of file'])
|
|
t.end()
|
|
})
|
|
|
|
t.test('just wrong', t => {
|
|
const warnings = []
|
|
const p = new Parse()
|
|
p.on('error', er => warnings.push(er.message))
|
|
let aborted = false
|
|
p.on('abort', _ => aborted = true)
|
|
p.write(trunc)
|
|
p.write(trunc)
|
|
p.write(tgz.slice(split))
|
|
p.end()
|
|
t.equal(aborted, true, 'aborted writing')
|
|
t.match(warnings, [/^zlib: /])
|
|
t.end()
|
|
})
|
|
|
|
t.end()
|
|
})
|
|
|
|
t.test('end while consuming', t => {
|
|
// https://github.com/npm/node-tar/issues/157
|
|
const data = zlib.gzipSync(makeTar([
|
|
{
|
|
path: 'package/package.json',
|
|
type: 'File',
|
|
size: 130,
|
|
},
|
|
new Array(131).join('x'),
|
|
{
|
|
path: 'package/node_modules/@c/d/node_modules/e/package.json',
|
|
type: 'File',
|
|
size: 30,
|
|
},
|
|
new Array(31).join('e'),
|
|
{
|
|
path: 'package/node_modules/@c/d/package.json',
|
|
type: 'File',
|
|
size: 33,
|
|
},
|
|
new Array(34).join('d'),
|
|
{
|
|
path: 'package/node_modules/a/package.json',
|
|
type: 'File',
|
|
size: 59,
|
|
},
|
|
new Array(60).join('a'),
|
|
{
|
|
path: 'package/node_modules/b/package.json',
|
|
type: 'File',
|
|
size: 30,
|
|
},
|
|
new Array(31).join('b'),
|
|
'',
|
|
'',
|
|
]))
|
|
|
|
const actual = []
|
|
const expect = [
|
|
'package/package.json',
|
|
'package/node_modules/@c/d/node_modules/e/package.json',
|
|
'package/node_modules/@c/d/package.json',
|
|
'package/node_modules/a/package.json',
|
|
'package/node_modules/b/package.json',
|
|
]
|
|
|
|
const mp = new MiniPass()
|
|
const p = new Parse({
|
|
onentry: entry => {
|
|
actual.push(entry.path)
|
|
entry.resume()
|
|
},
|
|
onwarn: (c, m, data) => t.fail(`${c}: ${m}`, data),
|
|
})
|
|
p.on('end', () => {
|
|
t.same(actual, expect)
|
|
t.end()
|
|
})
|
|
mp.end(data)
|
|
mp.pipe(p)
|
|
})
|
|
|
|
t.test('bad archives', t => {
|
|
const p = new Parse()
|
|
const warnings = []
|
|
p.on('warn', (code, msg, data) => {
|
|
warnings.push([code, msg, data])
|
|
})
|
|
p.on('end', () => {
|
|
// last one should be 'this archive sucks'
|
|
t.match(warnings.pop(), [
|
|
'TAR_BAD_ARCHIVE',
|
|
'Unrecognized archive format',
|
|
{ code: 'TAR_BAD_ARCHIVE', tarCode: 'TAR_BAD_ARCHIVE' },
|
|
])
|
|
t.end()
|
|
})
|
|
// javascript test is not a tarball.
|
|
p.end(fs.readFileSync(__filename))
|
|
})
|
|
|
|
t.test('header that throws', t => {
|
|
const p = new Parse()
|
|
p.on('warn', (c, m, d) => {
|
|
t.equal(m, 'invalid base256 encoding')
|
|
t.match(d, {
|
|
code: 'TAR_ENTRY_INVALID',
|
|
})
|
|
t.end()
|
|
})
|
|
const h = new Header({
|
|
path: 'path',
|
|
mode: 0o07777, // gonna make this one invalid
|
|
uid: 1234,
|
|
gid: 4321,
|
|
type: 'File',
|
|
size: 1,
|
|
})
|
|
h.encode()
|
|
const buf = h.block
|
|
const bad = Buffer.from([0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
|
|
bad.copy(buf, 100)
|
|
t.throws(() => new Header(buf), 'the header with that buffer throws')
|
|
p.write(buf)
|
|
})
|
|
|
|
t.test('warnings that are not so bad', t => {
|
|
const p = new Parse()
|
|
const warnings = []
|
|
p.on('warn', (code, m, d) => {
|
|
warnings.push([code, m, d])
|
|
t.fail('should get no warnings')
|
|
})
|
|
// the parser doesn't actually decide what's "ok" or "supported",
|
|
// it just parses. So we have to set it ourselves like unpack does
|
|
p.once('entry', entry => entry.invalid = true)
|
|
p.on('entry', entry => entry.resume())
|
|
const data = makeTar([
|
|
{
|
|
path: '/a/b/c',
|
|
type: 'File',
|
|
size: 1,
|
|
},
|
|
'a',
|
|
{
|
|
path: 'a/b/c',
|
|
type: 'Directory',
|
|
},
|
|
'',
|
|
'',
|
|
])
|
|
p.on('end', () => {
|
|
t.same(warnings, [])
|
|
t.end()
|
|
})
|
|
p.end(data)
|
|
})
|