From 6dcda692477d8f9cdb7b91cdd558bea27600ee4a Mon Sep 17 00:00:00 2001 From: Mike Ryan Date: Thu, 30 May 2019 15:29:05 -0500 Subject: [PATCH] Improve gnu/oldgnu format support - gnu/oldgnu formats do not have a prefix field. - Explicitly validate magic and version when parsing. Throw an exception if unsupported/invalid magic is encountered. --- headers.js | 30 +++++++--- test/extract.js | 89 ++++++++++++++++++++++++++++++ test/fixtures/gnu-incremental.tar | Bin 0 -> 10240 bytes test/fixtures/gnu.tar | Bin 0 -> 10240 bytes test/fixtures/index.js | 6 ++ test/fixtures/v7.tar | Bin 0 -> 10240 bytes 6 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/gnu-incremental.tar create mode 100644 test/fixtures/gnu.tar create mode 100644 test/fixtures/v7.tar 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 0000000000000000000000000000000000000000..0e62cfea528fabb0abb5a8dd1c4202114feee6bd GIT binary patch literal 10240 zcmeIuK?=e!5QX8aJw@D$Owws`1+O6(+_Y5E6z%OTCI-$>7g&zY^`a>p-*{^7l(Gd z=Wci`^0nZzkk{zq&vI-1JLQ+uG~LNQyWHH4hn+-nl;>YXATM literal 0 HcmV?d00001 diff --git a/test/fixtures/gnu.tar b/test/fixtures/gnu.tar new file mode 100644 index 0000000000000000000000000000000000000000..efd641bfb31141cb2665b384c38ff87c6e121da7 GIT binary patch literal 10240 zcmeIuF%H5o425COoFeQ9IjKpmz%`JlSW*?Gsi3!~L)E2ohl>AA@f#xhtL;#)^;>1f z=$vRKs*P&$qCw4KnCB%vif=5L`WVtp)CcoPobI^O`t>-}M{!QNX&l