const b4a = require('b4a') const ZEROS = '0000000000000000000' const SEVENS = '7777777777777777777' const ZERO_OFFSET = '0'.charCodeAt(0) const USTAR_MAGIC = b4a.from([0x75, 0x73, 0x74, 0x61, 0x72, 0x00]) // ustar\x00 const USTAR_VER = b4a.from([ZERO_OFFSET, ZERO_OFFSET]) const GNU_MAGIC = b4a.from([0x75, 0x73, 0x74, 0x61, 0x72, 0x20]) // ustar\x20 const GNU_VER = b4a.from([0x20, 0x00]) const MASK = 0o7777 const MAGIC_OFFSET = 257 const VERSION_OFFSET = 263 exports.decodeLongPath = function decodeLongPath (buf, encoding) { return decodeStr(buf, 0, buf.length, encoding) } exports.encodePax = function encodePax (opts) { // TODO: encode more stuff in pax let result = '' if (opts.name) result += addLength(' path=' + opts.name + '\n') if (opts.linkname) result += addLength(' linkpath=' + opts.linkname + '\n') const pax = opts.pax if (pax) { for (const key in pax) { result += addLength(' ' + key + '=' + pax[key] + '\n') } } return b4a.from(result) } exports.decodePax = function decodePax (buf) { const result = {} while (buf.length) { let i = 0 while (i < buf.length && buf[i] !== 32) i++ const len = parseInt(b4a.toString(buf.subarray(0, i)), 10) if (!len) return result const b = b4a.toString(buf.subarray(i + 1, len - 1)) const keyIndex = b.indexOf('=') if (keyIndex === -1) return result result[b.slice(0, keyIndex)] = b.slice(keyIndex + 1) buf = buf.subarray(len) } return result } exports.encode = function encode (opts) { const buf = b4a.alloc(512) let name = opts.name let prefix = '' if (opts.typeflag === 5 && name[name.length - 1] !== '/') name += '/' if (b4a.byteLength(name) !== name.length) return null // utf-8 while (b4a.byteLength(name) > 100) { const i = name.indexOf('/') if (i === -1) return null prefix += prefix ? '/' + name.slice(0, i) : name.slice(0, i) name = name.slice(i + 1) } if (b4a.byteLength(name) > 100 || b4a.byteLength(prefix) > 155) return null if (opts.linkname && b4a.byteLength(opts.linkname) > 100) return null b4a.write(buf, name) b4a.write(buf, encodeOct(opts.mode & MASK, 6), 100) b4a.write(buf, encodeOct(opts.uid, 6), 108) b4a.write(buf, encodeOct(opts.gid, 6), 116) encodeSize(opts.size, buf, 124) b4a.write(buf, encodeOct((opts.mtime.getTime() / 1000) | 0, 11), 136) buf[156] = ZERO_OFFSET + toTypeflag(opts.type) if (opts.linkname) b4a.write(buf, opts.linkname, 157) b4a.copy(USTAR_MAGIC, buf, MAGIC_OFFSET) b4a.copy(USTAR_VER, buf, VERSION_OFFSET) if (opts.uname) b4a.write(buf, opts.uname, 265) if (opts.gname) b4a.write(buf, opts.gname, 297) b4a.write(buf, encodeOct(opts.devmajor || 0, 6), 329) b4a.write(buf, encodeOct(opts.devminor || 0, 6), 337) if (prefix) b4a.write(buf, prefix, 345) b4a.write(buf, encodeOct(cksum(buf), 6), 148) return buf } exports.decode = function decode (buf, filenameEncoding, allowUnknownFormat) { let typeflag = buf[156] === 0 ? 0 : buf[156] - ZERO_OFFSET let name = decodeStr(buf, 0, 100, filenameEncoding) const mode = decodeOct(buf, 100, 8) const uid = decodeOct(buf, 108, 8) const gid = decodeOct(buf, 116, 8) const size = decodeOct(buf, 124, 12) const mtime = decodeOct(buf, 136, 12) const type = toType(typeflag) const linkname = buf[157] === 0 ? null : decodeStr(buf, 157, 100, filenameEncoding) const uname = decodeStr(buf, 265, 32) const gname = decodeStr(buf, 297, 32) const devmajor = decodeOct(buf, 329, 8) const devminor = decodeOct(buf, 337, 8) const c = cksum(buf) // checksum is still initial value if header was null. if (c === 8 * 32) return null // 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 (isUSTAR(buf)) { // ustar (posix) format. // prepend prefix, if present. if (buf[345]) name = decodeStr(buf, 345, 155, filenameEncoding) + '/' + name } else if (isGNU(buf)) { // 'gnu'/'oldgnu' format. Similar to ustar, but has support for incremental and // multi-volume tarballs. } else { if (!allowUnknownFormat) { 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, mode, uid, gid, size, mtime: new Date(1000 * mtime), type, linkname, uname, gname, devmajor, devminor, pax: null } } function isUSTAR (buf) { return b4a.equals(USTAR_MAGIC, buf.subarray(MAGIC_OFFSET, MAGIC_OFFSET + 6)) } function isGNU (buf) { return b4a.equals(GNU_MAGIC, buf.subarray(MAGIC_OFFSET, MAGIC_OFFSET + 6)) && b4a.equals(GNU_VER, buf.subarray(VERSION_OFFSET, VERSION_OFFSET + 2)) } function clamp (index, len, defaultValue) { if (typeof index !== 'number') return defaultValue index = ~~index // Coerce to integer. if (index >= len) return len if (index >= 0) return index index += len if (index >= 0) return index return 0 } function toType (flag) { switch (flag) { case 0: return 'file' case 1: return 'link' case 2: return 'symlink' case 3: return 'character-device' case 4: return 'block-device' case 5: return 'directory' case 6: return 'fifo' case 7: return 'contiguous-file' case 72: return 'pax-header' case 55: return 'pax-global-header' case 27: return 'gnu-long-link-path' case 28: case 30: return 'gnu-long-path' } return null } function toTypeflag (flag) { switch (flag) { case 'file': return 0 case 'link': return 1 case 'symlink': return 2 case 'character-device': return 3 case 'block-device': return 4 case 'directory': return 5 case 'fifo': return 6 case 'contiguous-file': return 7 case 'pax-header': return 72 } return 0 } function indexOf (block, num, offset, end) { for (; offset < end; offset++) { if (block[offset] === num) return offset } return end } function cksum (block) { let sum = 8 * 32 for (let i = 0; i < 148; i++) sum += block[i] for (let j = 156; j < 512; j++) sum += block[j] return sum } function encodeOct (val, n) { val = val.toString(8) if (val.length > n) return SEVENS.slice(0, n) + ' ' return ZEROS.slice(0, n - val.length) + val + ' ' } function encodeSizeBin (num, buf, off) { buf[off] = 0x80 for (let i = 11; i > 0; i--) { buf[off + i] = num & 0xff num = Math.floor(num / 0x100) } } function encodeSize (num, buf, off) { if (num.toString(8).length > 11) { encodeSizeBin(num, buf, off) } else { b4a.write(buf, encodeOct(num, 11), off) } } /* Copied from the node-tar repo and modified to meet * tar-stream coding standard. * * Source: https://github.com/npm/node-tar/blob/51b6627a1f357d2eb433e7378e5f05e83b7aa6cd/lib/header.js#L349 */ function parse256 (buf) { // first byte MUST be either 80 or FF // 80 for positive, FF for 2's comp let positive if (buf[0] === 0x80) positive = true else if (buf[0] === 0xFF) positive = false else return null // build up a base-256 tuple from the least sig to the highest const tuple = [] let i for (i = buf.length - 1; i > 0; i--) { const byte = buf[i] if (positive) tuple.push(byte) else tuple.push(0xFF - byte) } let sum = 0 const l = tuple.length for (i = 0; i < l; i++) { sum += tuple[i] * Math.pow(256, i) } return positive ? sum : -1 * sum } function decodeOct (val, offset, length) { val = val.subarray(offset, offset + length) offset = 0 // If prefixed with 0x80 then parse as a base-256 integer if (val[offset] & 0x80) { return parse256(val) } else { // Older versions of tar can prefix with spaces while (offset < val.length && val[offset] === 32) offset++ const end = clamp(indexOf(val, 32, offset, val.length), val.length, val.length) while (offset < end && val[offset] === 0) offset++ if (end === offset) return 0 return parseInt(b4a.toString(val.subarray(offset, end)), 8) } } function decodeStr (val, offset, length, encoding) { return b4a.toString(val.subarray(offset, indexOf(val, 0, offset, offset + length)), encoding) } function addLength (str) { const len = b4a.byteLength(str) let digits = Math.floor(Math.log(len) / Math.log(10)) + 1 if (len + digits >= Math.pow(10, digits)) digits++ return (len + digits) + str }