diff --git a/headers.js b/headers.js index 8571c87..fe7fd36 100644 --- a/headers.js +++ b/headers.js @@ -3,8 +3,13 @@ var alloc = Buffer.alloc var ZEROS = '0000000000000000000' var SEVENS = '7777777777777777777' var ZERO_OFFSET = '0'.charCodeAt(0) -var USTAR = 'ustar\x0000' +var USTAR_MAGIC = Buffer.from('ustar\x00', 'binary') +var USTAR_VER = Buffer.from('00', 'binary') +var GNU_MAGIC = Buffer.from('ustar\x20', 'binary') +var GNU_VER = Buffer.from('\x20\x00', 'binary') var MASK = parseInt('7777', 8) +var MAGIC_OFFSET = 257 +var VERSION_OFFSET = 263 var clamp = function (index, len, defaultValue) { if (typeof index !== 'number') return defaultValue @@ -223,7 +228,8 @@ exports.encode = function (opts) { if (opts.linkname) buf.write(opts.linkname, 157) - buf.write(USTAR, 257) + USTAR_MAGIC.copy(buf, MAGIC_OFFSET) + USTAR_VER.copy(buf, VERSION_OFFSET) if (opts.uname) buf.write(opts.uname, 265) if (opts.gname) buf.write(opts.gname, 297) buf.write(encodeOct(opts.devmajor || 0, 6), 329) @@ -252,11 +258,6 @@ exports.decode = function (buf, filenameEncoding) { var devmajor = decodeOct(buf, 329, 8) var devminor = decodeOct(buf, 337, 8) - if (buf[345]) name = decodeStr(buf, 345, 155, filenameEncoding) + '/' + name - - // to support old tar versions that use trailing / to indicate dirs - if (typeflag === 0 && name && name[name.length - 1] === '/') typeflag = 5 - var c = cksum(buf) // checksum is still initial value if header was null. @@ -265,6 +266,21 @@ exports.decode = function (buf, filenameEncoding) { // valid checksum if (c !== decodeOct(buf, 148, 8)) throw new Error('Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?') + if (USTAR_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0) { + // ustar (posix) format. + // prepend prefix, if present. + if (buf[345]) name = decodeStr(buf, 345, 155, filenameEncoding) + '/' + name + } else if (GNU_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0 && + GNU_VER.compare(buf, VERSION_OFFSET, VERSION_OFFSET + 2) === 0) { + // 'gnu'/'oldgnu' format. Similar to ustar, but has support for incremental and + // multi-volume tarballs. + } else { + throw new Error('Invalid tar header: unknown format.') + } + + // to support old tar versions that use trailing / to indicate dirs + if (typeflag === 0 && name && name[name.length - 1] === '/') typeflag = 5 + return { name: name, mode: mode, diff --git a/test/extract.js b/test/extract.js index 68aeb8b..e2f92bd 100644 --- a/test/extract.js +++ b/test/extract.js @@ -590,3 +590,92 @@ test('incomplete', function (t) { extract.end(fs.readFileSync(fixtures.INCOMPLETE_TAR)) }) + +test('gnu', function (t) { // can correctly unpack gnu-tar format + t.plan(3) + + var extract = tar.extract() + var noEntries = false + + extract.on('entry', function (header, stream, callback) { + t.deepEqual(header, { + name: 'test.txt', + mode: parseInt('644', 8), + uid: 12345, + gid: 67890, + size: 14, + mtime: new Date(1559239869000), + type: 'file', + linkname: null, + uname: 'myuser', + gname: 'mygroup', + devmajor: 0, + devminor: 0 + }) + + stream.pipe(concat(function (data) { + noEntries = true + t.same(data.toString(), 'Hello, world!\n') + callback() + })) + }) + + extract.on('finish', function () { + t.ok(noEntries) + }) + + extract.end(fs.readFileSync(fixtures.GNU_TAR)) +}) + +test('gnu-incremental', function (t) { + // can correctly unpack gnu-tar incremental format. In this situation, + // the tarball will have additional ctime and atime values in the header, + // and without awareness of the 'gnu' tar format, the atime (offset 345) is mistaken + // for a directory prefix (also offset 345). + t.plan(3) + + var extract = tar.extract() + var noEntries = false + + extract.on('entry', function (header, stream, callback) { + t.deepEqual(header, { + name: 'test.txt', + mode: parseInt('644', 8), + uid: 12345, + gid: 67890, + size: 14, + mtime: new Date(1559239869000), + type: 'file', + linkname: null, + uname: 'myuser', + gname: 'mygroup', + devmajor: 0, + devminor: 0 + }) + + stream.pipe(concat(function (data) { + noEntries = true + t.same(data.toString(), 'Hello, world!\n') + callback() + })) + }) + + extract.on('finish', function () { + t.ok(noEntries) + }) + + extract.end(fs.readFileSync(fixtures.GNU_INCREMENTAL_TAR)) +}) + +test('v7 unsupported', function (t) { // correctly fails to parse v7 tarballs + t.plan(1) + + var extract = tar.extract() + + extract.on('error', function (err) { + t.ok(!!err) + extract.destroy() + }) + + extract.end(fs.readFileSync(fixtures.V7_TAR)) +}) diff --git a/test/fixtures/gnu-incremental.tar b/test/fixtures/gnu-incremental.tar new file mode 100644 index 0000000..0e62cfe Binary files /dev/null and b/test/fixtures/gnu-incremental.tar differ diff --git a/test/fixtures/gnu.tar b/test/fixtures/gnu.tar new file mode 100644 index 0000000..efd641b Binary files /dev/null and b/test/fixtures/gnu.tar differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 7d5aa14..879de1f 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -17,3 +17,9 @@ exports.BASE_256_SIZE = path.join(__dirname, 'base-256-size.tar') exports.HUGE = path.join(__dirname, 'huge.tar.gz') exports.LATIN1_TAR = path.join(__dirname, 'latin1.tar') exports.INCOMPLETE_TAR = path.join(__dirname, 'incomplete.tar') +// Created using gnu tar: tar cf gnu-incremental.tar --format gnu --owner=myuser:12345 --group=mygroup:67890 test.txt +exports.GNU_TAR = path.join(__dirname, 'gnu.tar') +// Created using gnu tar: tar cf gnu-incremental.tar -G --format gnu --owner=myuser:12345 --group=mygroup:67890 test.txt +exports.GNU_INCREMENTAL_TAR = path.join(__dirname, 'gnu-incremental.tar') +// Created using gnu tar: tar cf v7.tar --format v7 test.txt +exports.V7_TAR = path.join(__dirname, 'v7.tar') diff --git a/test/fixtures/v7.tar b/test/fixtures/v7.tar new file mode 100644 index 0000000..c76b8ea Binary files /dev/null and b/test/fixtures/v7.tar differ