Comparing version 2.2.1 to 3.0.0
@@ -1,94 +0,125 @@ | ||
// give it a tarball and a path, and it'll dump the contents | ||
'use strict' | ||
module.exports = Extract | ||
// tar -x | ||
const hlo = require('./high-level-opt.js') | ||
const Unpack = require('./unpack.js') | ||
const fs = require('fs') | ||
const path = require('path') | ||
var tar = require("../tar.js") | ||
, fstream = require("fstream") | ||
, inherits = require("inherits") | ||
, path = require("path") | ||
const x = module.exports = (opt_, files, cb) => { | ||
if (typeof opt_ === 'function') | ||
cb = opt_, files = [], opt_ = {} | ||
else if (Array.isArray(opt_)) | ||
files = opt_, opt_ = {} | ||
function Extract (opts) { | ||
if (!(this instanceof Extract)) return new Extract(opts) | ||
tar.Parse.apply(this) | ||
if (typeof files === 'function') | ||
cb = files, files = [] | ||
if (typeof opts !== "object") { | ||
opts = { path: opts } | ||
} | ||
if (!files) | ||
files = [] | ||
// better to drop in cwd? seems more standard. | ||
opts.path = opts.path || path.resolve("node-tar-extract") | ||
opts.type = "Directory" | ||
opts.Directory = true | ||
const opt = hlo(opt_) | ||
// similar to --strip or --strip-components | ||
opts.strip = +opts.strip | ||
if (!opts.strip || opts.strip <= 0) opts.strip = 0 | ||
if (opt.sync && typeof cb === 'function') | ||
throw new TypeError('callback not supported for sync tar functions') | ||
this._fst = fstream.Writer(opts) | ||
if (!opt.file && typeof cb === 'function') | ||
throw new TypeError('callback only supported with file option') | ||
this.pause() | ||
var me = this | ||
if (files.length) | ||
filesFilter(opt, files) | ||
// Hardlinks in tarballs are relative to the root | ||
// of the tarball. So, they need to be resolved against | ||
// the target directory in order to be created properly. | ||
me.on("entry", function (entry) { | ||
// if there's a "strip" argument, then strip off that many | ||
// path components. | ||
if (opts.strip) { | ||
var p = entry.path.split("/").slice(opts.strip).join("/") | ||
entry.path = entry.props.path = p | ||
if (entry.linkpath) { | ||
var lp = entry.linkpath.split("/").slice(opts.strip).join("/") | ||
entry.linkpath = entry.props.linkpath = lp | ||
} | ||
} | ||
if (entry.type === "Link") { | ||
entry.linkpath = entry.props.linkpath = | ||
path.join(opts.path, path.join("/", entry.props.linkpath)) | ||
} | ||
return opt.file && opt.sync ? extractFileSync(opt) | ||
: opt.file ? extractFile(opt, cb) | ||
: opt.sync ? extractSync(opt) | ||
: extract(opt) | ||
} | ||
if (entry.type === "SymbolicLink") { | ||
var dn = path.dirname(entry.path) || "" | ||
var linkpath = entry.props.linkpath | ||
var target = path.resolve(opts.path, dn, linkpath) | ||
if (target.indexOf(opts.path) !== 0) { | ||
linkpath = path.join(opts.path, path.join("/", linkpath)) | ||
// construct a filter that limits the file entries listed | ||
// include child entries if a dir is included | ||
const filesFilter = (opt, files) => { | ||
const map = new Map(files.map(f => [f.replace(/\/+$/, ''), true])) | ||
const filter = opt.filter | ||
const mapHas = (file, r) => { | ||
const root = r || path.parse(file).root || '.' | ||
const ret = file === root ? false | ||
: map.has(file) ? map.get(file) | ||
: mapHas(path.dirname(file), root) | ||
map.set(file, ret) | ||
return ret | ||
} | ||
opt.filter = filter | ||
? (file, entry) => filter(file, entry) && mapHas(file.replace(/\/+$/, '')) | ||
: file => mapHas(file.replace(/\/+$/, '')) | ||
} | ||
const extractFileSync = opt => { | ||
const u = new Unpack.Sync(opt) | ||
const file = opt.file | ||
let threw = true | ||
let fd | ||
try { | ||
const stat = fs.statSync(file) | ||
const readSize = opt.maxReadSize || 16*1024*1024 | ||
if (stat.size < readSize) | ||
u.end(fs.readFileSync(file)) | ||
else { | ||
let pos = 0 | ||
const buf = Buffer.allocUnsafe(readSize) | ||
fd = fs.openSync(file, 'r') | ||
while (pos < stat.size) { | ||
let bytesRead = fs.readSync(fd, buf, 0, readSize, pos) | ||
pos += bytesRead | ||
u.write(buf.slice(0, bytesRead)) | ||
} | ||
entry.linkpath = entry.props.linkpath = linkpath | ||
u.end() | ||
fs.closeSync(fd) | ||
} | ||
}) | ||
threw = false | ||
} finally { | ||
if (threw && fd) | ||
try { fs.closeSync(fd) } catch (er) {} | ||
} | ||
} | ||
this._fst.on("ready", function () { | ||
me.pipe(me._fst, { end: false }) | ||
me.resume() | ||
}) | ||
const extractFile = (opt, cb) => { | ||
const u = new Unpack(opt) | ||
const readSize = opt.maxReadSize || 16*1024*1024 | ||
this._fst.on('error', function(err) { | ||
me.emit('error', err) | ||
}) | ||
const file = opt.file | ||
const p = new Promise((resolve, reject) => { | ||
u.on('error', reject) | ||
u.on('close', resolve) | ||
this._fst.on('drain', function() { | ||
me.emit('drain') | ||
fs.stat(file, (er, stat) => { | ||
if (er) | ||
reject(er) | ||
else if (stat.size < readSize) | ||
fs.readFile(file, (er, data) => { | ||
if (er) | ||
return reject(er) | ||
u.end(data) | ||
}) | ||
else { | ||
const stream = fs.createReadStream(file, { | ||
highWaterMark: readSize | ||
}) | ||
stream.on('error', reject) | ||
stream.pipe(u) | ||
} | ||
}) | ||
}) | ||
return cb ? p.then(cb, cb) : p | ||
} | ||
// this._fst.on("end", function () { | ||
// console.error("\nEEEE Extract End", me._fst.path) | ||
// }) | ||
this._fst.on("close", function () { | ||
// console.error("\nEEEE Extract End", me._fst.path) | ||
me.emit("finish") | ||
me.emit("end") | ||
me.emit("close") | ||
}) | ||
const extractSync = opt => { | ||
return new Unpack.Sync(opt) | ||
} | ||
inherits(Extract, tar.Parse) | ||
Extract.prototype._streamEnd = function () { | ||
var me = this | ||
if (!me._ended || me._entry) me.error("unexpected eof") | ||
me._fst.end() | ||
// my .end() is coming later. | ||
const extract = opt => { | ||
return new Unpack(opt) | ||
} |
@@ -0,385 +1,272 @@ | ||
'use strict' | ||
// parse a 512-byte header block to a data object, or vice-versa | ||
// If the data won't fit nicely in a simple header, then generate | ||
// the appropriate extended header file, and return that. | ||
// encode returns `true` if a pax extended header is needed, because | ||
// the data could not be faithfully encoded in a simple header. | ||
// (Also, check header.needPax to see if it needs a pax header.) | ||
module.exports = TarHeader | ||
const types = require('./types.js') | ||
const pathModule = require('path') | ||
const large = require('./large-numbers.js') | ||
var tar = require("../tar.js") | ||
, fields = tar.fields | ||
, fieldOffs = tar.fieldOffs | ||
, fieldEnds = tar.fieldEnds | ||
, fieldSize = tar.fieldSize | ||
, numeric = tar.numeric | ||
, assert = require("assert").ok | ||
, space = " ".charCodeAt(0) | ||
, slash = "/".charCodeAt(0) | ||
, bslash = process.platform === "win32" ? "\\".charCodeAt(0) : null | ||
const TYPE = Symbol('type') | ||
function TarHeader (block) { | ||
if (!(this instanceof TarHeader)) return new TarHeader(block) | ||
if (block) this.decode(block) | ||
} | ||
class Header { | ||
constructor (data, off) { | ||
this.cksumValid = false | ||
this.needPax = false | ||
this.nullBlock = false | ||
TarHeader.prototype = | ||
{ decode : decode | ||
, encode: encode | ||
, calcSum: calcSum | ||
, checkSum: checkSum | ||
} | ||
this.block = null | ||
this.path = null | ||
this.mode = null | ||
this.uid = null | ||
this.gid = null | ||
this.size = null | ||
this.mtime = null | ||
this.cksum = null | ||
this[TYPE] = '0' | ||
this.linkpath = null | ||
this.uname = null | ||
this.gname = null | ||
this.devmaj = 0 | ||
this.devmin = 0 | ||
this.atime = null | ||
this.ctime = null | ||
TarHeader.parseNumeric = parseNumeric | ||
TarHeader.encode = encode | ||
TarHeader.decode = decode | ||
// note that this will only do the normal ustar header, not any kind | ||
// of extended posix header file. If something doesn't fit comfortably, | ||
// then it will set obj.needExtended = true, and set the block to | ||
// the closest approximation. | ||
function encode (obj) { | ||
if (!obj && !(this instanceof TarHeader)) throw new Error( | ||
"encode must be called on a TarHeader, or supplied an object") | ||
obj = obj || this | ||
var block = obj.block = new Buffer(512) | ||
// if the object has a "prefix", then that's actually an extension of | ||
// the path field. | ||
if (obj.prefix) { | ||
// console.error("%% header encoding, got a prefix", obj.prefix) | ||
obj.path = obj.prefix + "/" + obj.path | ||
// console.error("%% header encoding, prefixed path", obj.path) | ||
obj.prefix = "" | ||
if (Buffer.isBuffer(data)) { | ||
this.decode(data, off || 0) | ||
} else if (data) | ||
this.set(data) | ||
} | ||
obj.needExtended = false | ||
decode (buf, off) { | ||
if (!off) | ||
off = 0 | ||
if (obj.mode) { | ||
if (typeof obj.mode === "string") obj.mode = parseInt(obj.mode, 8) | ||
obj.mode = obj.mode & 0777 | ||
} | ||
if (!buf || !(buf.length >= off + 512)) | ||
throw new Error('need 512 bytes for header') | ||
for (var f = 0; fields[f] !== null; f ++) { | ||
var field = fields[f] | ||
, off = fieldOffs[f] | ||
, end = fieldEnds[f] | ||
, ret | ||
this.path = decString(buf, off, 100) | ||
this.mode = decNumber(buf, off + 100, 8) | ||
this.uid = decNumber(buf, off + 108, 8) | ||
this.gid = decNumber(buf, off + 116, 8) | ||
this.size = decNumber(buf, off + 124, 12) | ||
this.mtime = decDate(buf, off + 136, 12) | ||
this.cksum = decNumber(buf, off + 148, 12) | ||
switch (field) { | ||
case "cksum": | ||
// special, done below, after all the others | ||
break | ||
// old tar versions marked dirs as a file with a trailing / | ||
this[TYPE] = decString(buf, off + 156, 1) | ||
if (this[TYPE] === '') | ||
this[TYPE] = '0' | ||
if (this[TYPE] === '0' && this.path.substr(-1) === '/') | ||
this[TYPE] = '5' | ||
case "prefix": | ||
// special, this is an extension of the "path" field. | ||
// console.error("%% header encoding, skip prefix later") | ||
break | ||
// tar implementations sometimes incorrectly put the stat(dir).size | ||
// as the size in the tarball, even though Directory entries are | ||
// not able to have any body at all. In the very rare chance that | ||
// it actually DOES have a body, we weren't going to do anything with | ||
// it anyway, and it'll just be a warning about an invalid header. | ||
if (this[TYPE] === '5') | ||
this.size = 0 | ||
case "type": | ||
// convert from long name to a single char. | ||
var type = obj.type || "0" | ||
if (type.length > 1) { | ||
type = tar.types[obj.type] | ||
if (!type) type = "0" | ||
} | ||
writeText(block, off, end, type) | ||
break | ||
this.linkpath = decString(buf, off + 157, 100) | ||
if (buf.slice(off + 257, off + 265).toString() === 'ustar\u000000') { | ||
this.uname = decString(buf, off + 265, 32) | ||
this.gname = decString(buf, off + 297, 32) | ||
this.devmaj = decNumber(buf, off + 329, 8) | ||
this.devmin = decNumber(buf, off + 337, 8) | ||
if (buf[off + 475] !== 0) { | ||
// definitely a prefix, definitely >130 chars. | ||
const prefix = decString(buf, off + 345, 155) | ||
this.path = prefix + '/' + this.path | ||
} else { | ||
const prefix = decString(buf, off + 345, 130) | ||
if (prefix) | ||
this.path = prefix + '/' + this.path | ||
this.atime = decDate(buf, off + 476, 12) | ||
this.ctime = decDate(buf, off + 488, 12) | ||
} | ||
} | ||
case "path": | ||
// uses the "prefix" field if > 100 bytes, but <= 255 | ||
var pathLen = Buffer.byteLength(obj.path) | ||
, pathFSize = fieldSize[fields.path] | ||
, prefFSize = fieldSize[fields.prefix] | ||
// paths between 100 and 255 should use the prefix field. | ||
// longer than 255 | ||
if (pathLen > pathFSize && | ||
pathLen <= pathFSize + prefFSize) { | ||
// need to find a slash somewhere in the middle so that | ||
// path and prefix both fit in their respective fields | ||
var searchStart = pathLen - 1 - pathFSize | ||
, searchEnd = prefFSize | ||
, found = false | ||
, pathBuf = new Buffer(obj.path) | ||
for ( var s = searchStart | ||
; (s <= searchEnd) | ||
; s ++ ) { | ||
if (pathBuf[s] === slash || pathBuf[s] === bslash) { | ||
found = s | ||
break | ||
} | ||
} | ||
if (found !== false) { | ||
prefix = pathBuf.slice(0, found).toString("utf8") | ||
path = pathBuf.slice(found + 1).toString("utf8") | ||
ret = writeText(block, off, end, path) | ||
off = fieldOffs[fields.prefix] | ||
end = fieldEnds[fields.prefix] | ||
// console.error("%% header writing prefix", off, end, prefix) | ||
ret = writeText(block, off, end, prefix) || ret | ||
break | ||
} | ||
} | ||
// paths less than 100 chars don't need a prefix | ||
// and paths longer than 255 need an extended header and will fail | ||
// on old implementations no matter what we do here. | ||
// Null out the prefix, and fallthrough to default. | ||
// console.error("%% header writing no prefix") | ||
var poff = fieldOffs[fields.prefix] | ||
, pend = fieldEnds[fields.prefix] | ||
writeText(block, poff, pend, "") | ||
// fallthrough | ||
// all other fields are numeric or text | ||
default: | ||
ret = numeric[field] | ||
? writeNumeric(block, off, end, obj[field]) | ||
: writeText(block, off, end, obj[field] || "") | ||
break | ||
let sum = 8 * 0x20 | ||
for (let i = off; i < off + 148; i++) { | ||
sum += buf[i] | ||
} | ||
obj.needExtended = obj.needExtended || ret | ||
for (let i = off + 156; i < off + 512; i++) { | ||
sum += buf[i] | ||
} | ||
this.cksumValid = sum === this.cksum | ||
if (this.cksum === null && sum === 8 * 0x20) | ||
this.nullBlock = true | ||
} | ||
var off = fieldOffs[fields.cksum] | ||
, end = fieldEnds[fields.cksum] | ||
encode (buf, off) { | ||
if (!buf) { | ||
buf = this.block = Buffer.alloc(512) | ||
off = 0 | ||
} | ||
writeNumeric(block, off, end, calcSum.call(this, block)) | ||
if (!off) | ||
off = 0 | ||
return block | ||
} | ||
if (!(buf.length >= off + 512)) | ||
throw new Error('need 512 bytes for header') | ||
// if it's a negative number, or greater than will fit, | ||
// then use write256. | ||
var MAXNUM = { 12: 077777777777 | ||
, 11: 07777777777 | ||
, 8 : 07777777 | ||
, 7 : 0777777 } | ||
function writeNumeric (block, off, end, num) { | ||
var writeLen = end - off | ||
, maxNum = MAXNUM[writeLen] || 0 | ||
const prefixSize = this.ctime || this.atime ? 130 : 155 | ||
const split = splitPrefix(this.path || '', prefixSize) | ||
const path = split[0] | ||
const prefix = split[1] | ||
this.needPax = split[2] | ||
num = num || 0 | ||
// console.error(" numeric", num) | ||
this.needPax = encString(buf, off, 100, path) || this.needPax | ||
this.needPax = encNumber(buf, off + 100, 8, this.mode) || this.needPax | ||
this.needPax = encNumber(buf, off + 108, 8, this.uid) || this.needPax | ||
this.needPax = encNumber(buf, off + 116, 8, this.gid) || this.needPax | ||
this.needPax = encNumber(buf, off + 124, 12, this.size) || this.needPax | ||
this.needPax = encDate(buf, off + 136, 12, this.mtime) || this.needPax | ||
buf[off + 156] = this[TYPE].charCodeAt(0) | ||
this.needPax = encString(buf, off + 157, 100, this.linkpath) || this.needPax | ||
buf.write('ustar\u000000', off + 257, 8) | ||
this.needPax = encString(buf, off + 265, 32, this.uname) || this.needPax | ||
this.needPax = encString(buf, off + 297, 32, this.gname) || this.needPax | ||
this.needPax = encNumber(buf, off + 329, 8, this.devmaj) || this.needPax | ||
this.needPax = encNumber(buf, off + 337, 8, this.devmin) || this.needPax | ||
this.needPax = encString(buf, off + 345, prefixSize, prefix) || this.needPax | ||
if (buf[off + 475] !== 0) | ||
this.needPax = encString(buf, off + 345, 155, prefix) || this.needPax | ||
else { | ||
this.needPax = encString(buf, off + 345, 130, prefix) || this.needPax | ||
this.needPax = encDate(buf, off + 476, 12, this.atime) || this.needPax | ||
this.needPax = encDate(buf, off + 488, 12, this.ctime) || this.needPax | ||
} | ||
if (num instanceof Date || | ||
Object.prototype.toString.call(num) === "[object Date]") { | ||
num = num.getTime() / 1000 | ||
} | ||
let sum = 8 * 0x20 | ||
for (let i = off; i < off + 148; i++) { | ||
sum += buf[i] | ||
} | ||
for (let i = off + 156; i < off + 512; i++) { | ||
sum += buf[i] | ||
} | ||
this.cksum = sum | ||
encNumber(buf, off + 148, 8, this.cksum) | ||
this.cksumValid = true | ||
if (num > maxNum || num < 0) { | ||
write256(block, off, end, num) | ||
// need an extended header if negative or too big. | ||
return true | ||
return this.needPax | ||
} | ||
// god, tar is so annoying | ||
// if the string is small enough, you should put a space | ||
// between the octal string and the \0, but if it doesn't | ||
// fit, then don't. | ||
var numStr = Math.floor(num).toString(8) | ||
if (num < MAXNUM[writeLen - 1]) numStr += " " | ||
// pad with "0" chars | ||
if (numStr.length < writeLen) { | ||
numStr = (new Array(writeLen - numStr.length).join("0")) + numStr | ||
set (data) { | ||
for (let i in data) { | ||
if (data[i] !== null && data[i] !== undefined) | ||
this[i] = data[i] | ||
} | ||
} | ||
if (numStr.length !== writeLen - 1) { | ||
throw new Error("invalid length: " + JSON.stringify(numStr) + "\n" + | ||
"expected: "+writeLen) | ||
get type () { | ||
return types.name.get(this[TYPE]) || this[TYPE] | ||
} | ||
block.write(numStr, off, writeLen, "utf8") | ||
block[end - 1] = 0 | ||
} | ||
function write256 (block, off, end, num) { | ||
var buf = block.slice(off, end) | ||
var positive = num >= 0 | ||
buf[0] = positive ? 0x80 : 0xFF | ||
// get the number as a base-256 tuple | ||
if (!positive) num *= -1 | ||
var tuple = [] | ||
do { | ||
var n = num % 256 | ||
tuple.push(n) | ||
num = (num - n) / 256 | ||
} while (num) | ||
var bytes = tuple.length | ||
var fill = buf.length - bytes | ||
for (var i = 1; i < fill; i ++) { | ||
buf[i] = positive ? 0 : 0xFF | ||
get typeKey () { | ||
return this[TYPE] | ||
} | ||
// tuple is a base256 number, with [0] as the *least* significant byte | ||
// if it's negative, then we need to flip all the bits once we hit the | ||
// first non-zero bit. The 2's-complement is (0x100 - n), and the 1's- | ||
// complement is (0xFF - n). | ||
var zero = true | ||
for (i = bytes; i > 0; i --) { | ||
var byte = tuple[bytes - i] | ||
if (positive) buf[fill + i] = byte | ||
else if (zero && byte === 0) buf[fill + i] = 0 | ||
else if (zero) { | ||
zero = false | ||
buf[fill + i] = 0x100 - byte | ||
} else buf[fill + i] = 0xFF - byte | ||
set type (type) { | ||
if (types.code.has(type)) | ||
this[TYPE] = types.code.get(type) | ||
else | ||
this[TYPE] = type | ||
} | ||
} | ||
function writeText (block, off, end, str) { | ||
// strings are written as utf8, then padded with \0 | ||
var strLen = Buffer.byteLength(str) | ||
, writeLen = Math.min(strLen, end - off) | ||
// non-ascii fields need extended headers | ||
// long fields get truncated | ||
, needExtended = strLen !== str.length || strLen > writeLen | ||
const splitPrefix = (p, prefixSize) => { | ||
const pathSize = 100 | ||
let pp = p | ||
let prefix = '' | ||
let ret | ||
const root = pathModule.parse(p).root || '.' | ||
// write the string, and null-pad | ||
if (writeLen > 0) block.write(str, off, writeLen, "utf8") | ||
for (var i = off + writeLen; i < end; i ++) block[i] = 0 | ||
if (Buffer.byteLength(pp) < pathSize) | ||
ret = [pp, prefix, false] | ||
else { | ||
// first set prefix to the dir, and path to the base | ||
prefix = pathModule.dirname(pp) | ||
pp = pathModule.basename(pp) | ||
return needExtended | ||
} | ||
do { | ||
// both fit! | ||
if (Buffer.byteLength(pp) <= pathSize && | ||
Buffer.byteLength(prefix) <= prefixSize) | ||
ret = [pp, prefix, false] | ||
function calcSum (block) { | ||
block = block || this.block | ||
assert(Buffer.isBuffer(block) && block.length === 512) | ||
// prefix fits in prefix, but path doesn't fit in path | ||
else if (Buffer.byteLength(pp) > pathSize && | ||
Buffer.byteLength(prefix) <= prefixSize) | ||
ret = [pp.substr(0, pathSize - 1), prefix, true] | ||
if (!block) throw new Error("Need block to checksum") | ||
else { | ||
// make path take a bit from prefix | ||
pp = pathModule.join(pathModule.basename(prefix), pp) | ||
prefix = pathModule.dirname(prefix) | ||
} | ||
} while (prefix !== root && !ret) | ||
// now figure out what it would be if the cksum was " " | ||
var sum = 0 | ||
, start = fieldOffs[fields.cksum] | ||
, end = fieldEnds[fields.cksum] | ||
for (var i = 0; i < fieldOffs[fields.cksum]; i ++) { | ||
sum += block[i] | ||
// at this point, found no resolution, just truncate | ||
if (!ret) | ||
ret = [p.substr(0, pathSize - 1), '', true] | ||
} | ||
for (var i = start; i < end; i ++) { | ||
sum += space | ||
} | ||
for (var i = end; i < 512; i ++) { | ||
sum += block[i] | ||
} | ||
return sum | ||
return ret | ||
} | ||
const decString = (buf, off, size) => | ||
buf.slice(off, off + size).toString('utf8').replace(/\0.*/, '') | ||
function checkSum (block) { | ||
var sum = calcSum.call(this, block) | ||
block = block || this.block | ||
const decDate = (buf, off, size) => | ||
numToDate(decNumber(buf, off, size)) | ||
var cksum = block.slice(fieldOffs[fields.cksum], fieldEnds[fields.cksum]) | ||
cksum = parseNumeric(cksum) | ||
const numToDate = num => num === null ? null : new Date(num * 1000) | ||
return cksum === sum | ||
} | ||
const decNumber = (buf, off, size) => | ||
buf[off] & 0x80 ? large.parse(buf.slice(off, off + size)) | ||
: decSmallNumber(buf, off, size) | ||
function decode (block) { | ||
block = block || this.block | ||
assert(Buffer.isBuffer(block) && block.length === 512) | ||
const nanNull = value => isNaN(value) ? null : value | ||
this.block = block | ||
this.cksumValid = this.checkSum() | ||
const decSmallNumber = (buf, off, size) => | ||
nanNull(parseInt( | ||
buf.slice(off, off + size) | ||
.toString('utf8').replace(/\0.*$/, '').trim(), 8)) | ||
var prefix = null | ||
// slice off each field. | ||
for (var f = 0; fields[f] !== null; f ++) { | ||
var field = fields[f] | ||
, val = block.slice(fieldOffs[f], fieldEnds[f]) | ||
switch (field) { | ||
case "ustar": | ||
// if not ustar, then everything after that is just padding. | ||
if (val.toString() !== "ustar\0") { | ||
this.ustar = false | ||
return | ||
} else { | ||
// console.error("ustar:", val, val.toString()) | ||
this.ustar = val.toString() | ||
} | ||
break | ||
// prefix is special, since it might signal the xstar header | ||
case "prefix": | ||
var atime = parseNumeric(val.slice(131, 131 + 12)) | ||
, ctime = parseNumeric(val.slice(131 + 12, 131 + 12 + 12)) | ||
if ((val[130] === 0 || val[130] === space) && | ||
typeof atime === "number" && | ||
typeof ctime === "number" && | ||
val[131 + 12] === space && | ||
val[131 + 12 + 12] === space) { | ||
this.atime = atime | ||
this.ctime = ctime | ||
val = val.slice(0, 130) | ||
} | ||
prefix = val.toString("utf8").replace(/\0+$/, "") | ||
// console.error("%% header reading prefix", prefix) | ||
break | ||
// all other fields are null-padding text | ||
// or a number. | ||
default: | ||
if (numeric[field]) { | ||
this[field] = parseNumeric(val) | ||
} else { | ||
this[field] = val.toString("utf8").replace(/\0+$/, "") | ||
} | ||
break | ||
} | ||
} | ||
// if we got a prefix, then prepend it to the path. | ||
if (prefix) { | ||
this.path = prefix + "/" + this.path | ||
// console.error("%% header got a prefix", this.path) | ||
} | ||
// the maximum encodable as a null-terminated octal, by field size | ||
const MAXNUM = { | ||
12: 0o77777777777, | ||
8 : 0o7777777 | ||
} | ||
function parse256 (buf) { | ||
// first byte MUST be either 80 or FF | ||
// 80 for positive, FF for 2's comp | ||
var positive | ||
if (buf[0] === 0x80) positive = true | ||
else if (buf[0] === 0xFF) positive = false | ||
else return null | ||
const encNumber = (buf, off, size, number) => | ||
number === null ? false : | ||
number > MAXNUM[size] || number < 0 | ||
? (large.encode(number, buf.slice(off, off + size)), true) | ||
: (encSmallNumber(buf, off, size, number), false) | ||
// build up a base-256 tuple from the least sig to the highest | ||
var zero = false | ||
, tuple = [] | ||
for (var i = buf.length - 1; i > 0; i --) { | ||
var byte = buf[i] | ||
if (positive) tuple.push(byte) | ||
else if (zero && byte === 0) tuple.push(0) | ||
else if (zero) { | ||
zero = false | ||
tuple.push(0x100 - byte) | ||
} else tuple.push(0xFF - byte) | ||
} | ||
const encSmallNumber = (buf, off, size, number) => | ||
buf.write(octalString(number, size), off, size, 'ascii') | ||
for (var sum = 0, i = 0, l = tuple.length; i < l; i ++) { | ||
sum += tuple[i] * Math.pow(256, i) | ||
} | ||
const octalString = (number, size) => | ||
padOctal(Math.floor(number).toString(8), size) | ||
return positive ? sum : -1 * sum | ||
} | ||
const padOctal = (string, size) => | ||
(string.length === size - 1 ? string | ||
: new Array(size - string.length - 1).join('0') + string + ' ') + '\0' | ||
function parseNumeric (f) { | ||
if (f[0] & 0x80) return parse256(f) | ||
const encDate = (buf, off, size, date) => | ||
date === null ? false : | ||
encNumber(buf, off, size, date.getTime() / 1000) | ||
var str = f.toString("utf8").split("\0")[0].trim() | ||
, res = parseInt(str, 8) | ||
// enough to fill the longest string we've got | ||
const NULLS = new Array(156).join('\0') | ||
// pad with nulls, return true if it's longer or non-ascii | ||
const encString = (buf, off, size, string) => | ||
string === null ? false : | ||
(buf.write(string + NULLS, off, size, 'utf8'), | ||
string.length !== Buffer.byteLength(string) || string.length > size) | ||
return isNaN(res) ? null : res | ||
} | ||
module.exports = Header |
484
lib/pack.js
@@ -1,236 +0,356 @@ | ||
// pipe in an fstream, and it'll make a tarball. | ||
// key-value pair argument is global extended header props. | ||
'use strict' | ||
module.exports = Pack | ||
// A readable tar stream creator | ||
// Technically, this is a transform stream that you write paths into, | ||
// and tar format comes out of. | ||
// The `add()` method is like `write()` but returns this, | ||
// and end() return `this` as well, so you can | ||
// do `new Pack(opt).add('files').add('dir').end().pipe(output) | ||
// You could also do something like: | ||
// streamOfPaths().pipe(new Pack()).pipe(new fs.WriteStream('out.tar')) | ||
var EntryWriter = require("./entry-writer.js") | ||
, Stream = require("stream").Stream | ||
, path = require("path") | ||
, inherits = require("inherits") | ||
, GlobalHeaderWriter = require("./global-header-writer.js") | ||
, collect = require("fstream").collect | ||
, eof = new Buffer(512) | ||
class PackJob { | ||
constructor (path, absolute) { | ||
this.path = path || './' | ||
this.absolute = absolute | ||
this.realpath = null | ||
this.stat = null | ||
this.readdir = null | ||
this.pending = false | ||
this.ignore = false | ||
this.piped = false | ||
} | ||
} | ||
for (var i = 0; i < 512; i ++) eof[i] = 0 | ||
const MiniPass = require('minipass') | ||
const zlib = require('minizlib') | ||
const WriteEntry = require('./write-entry.js') | ||
const WriteEntrySync = WriteEntry.Sync | ||
const Yallist = require('yallist') | ||
const EOF = Buffer.alloc(1024) | ||
const ONSTAT = Symbol('onStat') | ||
const ENDED = Symbol('ended') | ||
const QUEUE = Symbol('queue') | ||
const CURRENT = Symbol('current') | ||
const PROCESS = Symbol('process') | ||
const PROCESSING = Symbol('processing') | ||
const PROCESSJOB = Symbol('processJob') | ||
const JOBS = Symbol('jobs') | ||
const JOBDONE = Symbol('jobDone') | ||
const ADDENTRY = Symbol('addEntry') | ||
const STAT = Symbol('stat') | ||
const READDIR = Symbol('readdir') | ||
const ONREADDIR = Symbol('onreaddir') | ||
const PIPE = Symbol('pipe') | ||
const ENTRY = Symbol('entry') | ||
const WRITEENTRYCLASS = Symbol('writeEntryClass') | ||
const WRITE = Symbol('write') | ||
const ONDRAIN = Symbol('ondrain') | ||
inherits(Pack, Stream) | ||
const fs = require('fs') | ||
const path = require('path') | ||
function Pack (props) { | ||
// console.error("-- p ctor") | ||
var me = this | ||
if (!(me instanceof Pack)) return new Pack(props) | ||
class Pack extends MiniPass { | ||
constructor (opt) { | ||
super(opt) | ||
opt = opt || Object.create(null) | ||
this.opt = opt | ||
this.cwd = opt.cwd || process.cwd() | ||
this.maxReadSize = opt.maxReadSize | ||
this.preservePaths = !!opt.preservePaths | ||
this.strict = !!opt.strict | ||
this.prefix = (opt.prefix || '').replace(/(\\|\/)+$/, '') | ||
this.linkCache = opt.linkCache || new Map() | ||
this.statCache = opt.statCache || new Map() | ||
this.readdirCache = opt.readdirCache || new Map() | ||
this[WRITEENTRYCLASS] = WriteEntry | ||
if (typeof opt.onwarn === 'function') | ||
this.on('warn', opt.onwarn) | ||
if (props) me._noProprietary = props.noProprietary | ||
else me._noProprietary = false | ||
this.zip = null | ||
if (opt.gzip) { | ||
if (typeof opt.gzip !== 'object') | ||
opt.gzip = {} | ||
this.zip = new zlib.Gzip(opt.gzip) | ||
this.zip.on('data', chunk => super.write(chunk)) | ||
this.zip.on('end', _ => super.end()) | ||
this.zip.on('drain', _ => { | ||
this[ONDRAIN]() | ||
}) | ||
} else | ||
this.on('drain', this[ONDRAIN]) | ||
me._global = props | ||
this.portable = !!opt.portable | ||
this.noDirRecurse = !!opt.noDirRecurse | ||
this.follow = !!opt.follow | ||
me.readable = true | ||
me.writable = true | ||
me._buffer = [] | ||
// console.error("-- -- set current to null in ctor") | ||
me._currentEntry = null | ||
me._processing = false | ||
this.filter = typeof opt.filter === 'function' ? opt.filter : _ => true | ||
me._pipeRoot = null | ||
me.on("pipe", function (src) { | ||
if (src.root === me._pipeRoot) return | ||
me._pipeRoot = src | ||
src.on("end", function () { | ||
me._pipeRoot = null | ||
}) | ||
me.add(src) | ||
}) | ||
} | ||
this[QUEUE] = new Yallist | ||
this[JOBS] = 0 | ||
this.jobs = +opt.jobs || 4 | ||
this[PROCESSING] = false | ||
this[ENDED] = false | ||
} | ||
Pack.prototype.addGlobal = function (props) { | ||
// console.error("-- p addGlobal") | ||
if (this._didGlobal) return | ||
this._didGlobal = true | ||
[WRITE] (chunk) { | ||
return super.write(chunk) | ||
} | ||
var me = this | ||
GlobalHeaderWriter(props) | ||
.on("data", function (c) { | ||
me.emit("data", c) | ||
}) | ||
.end() | ||
} | ||
add (path) { | ||
this.write(path) | ||
return this | ||
} | ||
Pack.prototype.add = function (stream) { | ||
if (this._global && !this._didGlobal) this.addGlobal(this._global) | ||
end (path) { | ||
if (path) | ||
this.write(path) | ||
this[ENDED] = true | ||
this[PROCESS]() | ||
return this | ||
} | ||
if (this._ended) return this.emit("error", new Error("add after end")) | ||
write (path) { | ||
if (this[ENDED]) | ||
throw new Error('write after end') | ||
collect(stream) | ||
this._buffer.push(stream) | ||
this._process() | ||
this._needDrain = this._buffer.length > 0 | ||
return !this._needDrain | ||
} | ||
this[ADDENTRY](path) | ||
return this.flowing | ||
} | ||
Pack.prototype.pause = function () { | ||
this._paused = true | ||
if (this._currentEntry) this._currentEntry.pause() | ||
this.emit("pause") | ||
} | ||
[ADDENTRY] (p) { | ||
const absolute = path.resolve(this.cwd, p) | ||
if (this.prefix) | ||
p = this.prefix + '/' + p | ||
this[QUEUE].push(new PackJob(p, absolute)) | ||
this[PROCESS]() | ||
} | ||
Pack.prototype.resume = function () { | ||
this._paused = false | ||
if (this._currentEntry) this._currentEntry.resume() | ||
this.emit("resume") | ||
this._process() | ||
} | ||
[STAT] (job) { | ||
job.pending = true | ||
this[JOBS] += 1 | ||
const stat = this.follow ? 'stat' : 'lstat' | ||
fs[stat](job.absolute, (er, stat) => { | ||
job.pending = false | ||
this[JOBS] -= 1 | ||
if (er) | ||
return this.emit('error', er) | ||
this[ONSTAT](job, stat) | ||
}) | ||
} | ||
Pack.prototype.end = function () { | ||
this._ended = true | ||
this._buffer.push(eof) | ||
this._process() | ||
} | ||
[ONSTAT] (job, stat) { | ||
this.statCache.set(job.absolute, stat) | ||
job.stat = stat | ||
Pack.prototype._process = function () { | ||
var me = this | ||
if (me._paused || me._processing) { | ||
return | ||
} | ||
// now we have the stat, we can filter it. | ||
if (!this.filter(job.path, stat)) | ||
job.ignore = true | ||
var entry = me._buffer.shift() | ||
if (!entry) { | ||
if (me._needDrain) { | ||
me.emit("drain") | ||
} | ||
return | ||
this[PROCESS]() | ||
} | ||
if (entry.ready === false) { | ||
// console.error("-- entry is not ready", entry) | ||
me._buffer.unshift(entry) | ||
entry.on("ready", function () { | ||
// console.error("-- -- ready!", entry) | ||
me._process() | ||
[READDIR] (job) { | ||
job.pending = true | ||
this[JOBS] += 1 | ||
fs.readdir(job.absolute, (er, entries) => { | ||
job.pending = false | ||
this[JOBS] -= 1 | ||
if (er) | ||
return this.emit('error', er) | ||
this[ONREADDIR](job, entries) | ||
}) | ||
return | ||
} | ||
me._processing = true | ||
[ONREADDIR] (job, entries) { | ||
this.readdirCache.set(job.absolute, entries) | ||
job.readdir = entries | ||
this[PROCESS]() | ||
} | ||
if (entry === eof) { | ||
// need 2 ending null blocks. | ||
me.emit("data", eof) | ||
me.emit("data", eof) | ||
me.emit("end") | ||
me.emit("close") | ||
return | ||
[PROCESS] () { | ||
if (this[PROCESSING]) | ||
return | ||
this[PROCESSING] = true | ||
for (let w = this[QUEUE].head; | ||
w !== null && this[JOBS] < this.jobs; | ||
w = w.next) { | ||
this[PROCESSJOB](w.value) | ||
} | ||
this[PROCESSING] = false | ||
if (this[ENDED] && !this[QUEUE].length && this[JOBS] === 0) { | ||
if (this.zip) | ||
this.zip.end(EOF) | ||
else { | ||
super.write(EOF) | ||
super.end() | ||
} | ||
} | ||
} | ||
// Change the path to be relative to the root dir that was | ||
// added to the tarball. | ||
// | ||
// XXX This should be more like how -C works, so you can | ||
// explicitly set a root dir, and also explicitly set a pathname | ||
// in the tarball to use. That way we can skip a lot of extra | ||
// work when resolving symlinks for bundled dependencies in npm. | ||
get [CURRENT] () { | ||
return this[QUEUE] && this[QUEUE].head && this[QUEUE].head.value | ||
} | ||
var root = path.dirname((entry.root || entry).path); | ||
if (me._global && me._global.fromBase && entry.root && entry.root.path) { | ||
// user set 'fromBase: true' indicating tar root should be directory itself | ||
root = entry.root.path; | ||
[JOBDONE] (job) { | ||
this[QUEUE].shift() | ||
this[JOBS] -= 1 | ||
this[PROCESS]() | ||
} | ||
var wprops = {} | ||
[PROCESSJOB] (job) { | ||
if (job.pending) | ||
return | ||
Object.keys(entry.props || {}).forEach(function (k) { | ||
wprops[k] = entry.props[k] | ||
}) | ||
if (!job.stat) { | ||
if (this.statCache.has(job.absolute)) | ||
this[ONSTAT](job, this.statCache.get(job.absolute)) | ||
else | ||
this[STAT](job) | ||
} | ||
if (!job.stat) | ||
return | ||
if (me._noProprietary) wprops.noProprietary = true | ||
// filtered out! | ||
if (job.ignore) { | ||
if (job === this[CURRENT]) | ||
this[QUEUE].shift() | ||
return | ||
} | ||
wprops.path = path.relative(root, entry.path || '') | ||
if (!this.noDirRecurse && job.stat.isDirectory() && !job.readdir) { | ||
if (this.readdirCache.has(job.absolute)) | ||
this[ONREADDIR](job, this.readdirCache.get(job.absolute)) | ||
else | ||
this[READDIR](job) | ||
if (!job.readdir) | ||
return | ||
} | ||
// actually not a matter of opinion or taste. | ||
if (process.platform === "win32") { | ||
wprops.path = wprops.path.replace(/\\/g, "/") | ||
if (!job.entry) { | ||
job.entry = this[ENTRY](job) | ||
if (!job.entry) { | ||
job.ignore = true | ||
return | ||
} | ||
} | ||
if (job === this[CURRENT] && !job.piped) | ||
this[PIPE](job) | ||
} | ||
if (!wprops.type) | ||
wprops.type = 'Directory' | ||
warn (msg, data) { | ||
return this.emit('warn', msg, data) | ||
} | ||
switch (wprops.type) { | ||
// sockets not supported | ||
case "Socket": | ||
return | ||
[ENTRY] (job) { | ||
this[JOBS] += 1 | ||
try { | ||
return new this[WRITEENTRYCLASS](job.path, { | ||
onwarn: (msg, data) => { | ||
this.warn(msg, data) | ||
}, | ||
cwd: this.cwd, | ||
absolute: job.absolute, | ||
preservePaths: this.preservePaths, | ||
maxReadSize: this.maxReadSize, | ||
strict: this.strict, | ||
portable: this.portable, | ||
linkCache: this.linkCache, | ||
statCache: this.statCache | ||
}).on('end', _ => { | ||
this[JOBDONE](job) | ||
}) | ||
} catch (er) { | ||
this.emit('error', er) | ||
} | ||
} | ||
case "Directory": | ||
wprops.path += "/" | ||
wprops.size = 0 | ||
break | ||
[ONDRAIN] () { | ||
if (this[CURRENT] && this[CURRENT].entry) | ||
this[CURRENT].entry.resume() | ||
} | ||
case "Link": | ||
var lp = path.resolve(path.dirname(entry.path), entry.linkpath) | ||
wprops.linkpath = path.relative(root, lp) || "." | ||
wprops.size = 0 | ||
break | ||
// like .pipe() but using super, because our write() is special | ||
[PIPE] (job) { | ||
job.piped = true | ||
case "SymbolicLink": | ||
var lp = path.resolve(path.dirname(entry.path), entry.linkpath) | ||
wprops.linkpath = path.relative(path.dirname(entry.path), lp) || "." | ||
wprops.size = 0 | ||
break | ||
} | ||
if (job.readdir) | ||
job.readdir.forEach(entry => { | ||
const base = job.path === './' ? '' : job.path.replace(/\/*$/, '/') | ||
this[ADDENTRY](base + entry) | ||
}) | ||
// console.error("-- new writer", wprops) | ||
// if (!wprops.type) { | ||
// // console.error("-- no type?", entry.constructor.name, entry) | ||
// } | ||
const source = job.entry | ||
const zip = this.zip | ||
// console.error("-- -- set current to new writer", wprops.path) | ||
var writer = me._currentEntry = EntryWriter(wprops) | ||
if (zip) | ||
source.on('data', chunk => { | ||
if (!zip.write(chunk)) | ||
source.pause() | ||
}) | ||
else | ||
source.on('data', chunk => { | ||
if (!super.write(chunk)) | ||
source.pause() | ||
}) | ||
} | ||
writer.parent = me | ||
resume () { | ||
if (this.zip) | ||
this.zip.resume() | ||
return super.resume() | ||
} | ||
// writer.on("end", function () { | ||
// // console.error("-- -- writer end", writer.path) | ||
// }) | ||
pause () { | ||
if (this.zip) | ||
this.zip.pause() | ||
return super.pause() | ||
} | ||
} | ||
writer.on("data", function (c) { | ||
me.emit("data", c) | ||
}) | ||
class PackSync extends Pack { | ||
constructor (opt) { | ||
super(opt) | ||
this[WRITEENTRYCLASS] = WriteEntrySync | ||
} | ||
writer.on("header", function () { | ||
Buffer.prototype.toJSON = function () { | ||
return this.toString().split(/\0/).join(".") | ||
} | ||
// console.error("-- -- writer header %j", writer.props) | ||
if (writer.props.size === 0) nextEntry() | ||
}) | ||
writer.on("close", nextEntry) | ||
// pause/resume are no-ops in sync streams. | ||
pause () {} | ||
resume () {} | ||
var ended = false | ||
function nextEntry () { | ||
if (ended) return | ||
ended = true | ||
[STAT] (job) { | ||
const stat = this.follow ? 'statSync' : 'lstatSync' | ||
this[ONSTAT](job, fs[stat](job.absolute)) | ||
} | ||
// console.error("-- -- writer close", writer.path) | ||
// console.error("-- -- set current to null", wprops.path) | ||
me._currentEntry = null | ||
me._processing = false | ||
me._process() | ||
[READDIR] (job, stat) { | ||
this[ONREADDIR](job, fs.readdirSync(job.absolute)) | ||
} | ||
writer.on("error", function (er) { | ||
// console.error("-- -- writer error", writer.path) | ||
me.emit("error", er) | ||
}) | ||
// gotta get it all in this tick | ||
[PIPE] (job) { | ||
const source = job.entry | ||
const zip = this.zip | ||
// if it's the root, then there's no need to add its entries, | ||
// or data, since they'll be added directly. | ||
if (entry === me._pipeRoot) { | ||
// console.error("-- is the root, don't auto-add") | ||
writer.add = null | ||
if (job.readdir) | ||
job.readdir.forEach(entry => { | ||
this[ADDENTRY](job.path + '/' + entry) | ||
}) | ||
if (zip) | ||
source.on('data', chunk => { | ||
zip.write(chunk) | ||
}) | ||
else | ||
source.on('data', chunk => { | ||
super[WRITE](chunk) | ||
}) | ||
} | ||
entry.pipe(writer) | ||
} | ||
Pack.prototype.destroy = function () {} | ||
Pack.prototype.write = function () {} | ||
Pack.Sync = PackSync | ||
module.exports = Pack |
583
lib/parse.js
@@ -0,275 +1,412 @@ | ||
'use strict' | ||
// A writable stream. | ||
// It emits "entry" events, which provide a readable stream that has | ||
// header info attached. | ||
// this[BUFFER] is the remainder of a chunk if we're waiting for | ||
// the full 512 bytes of a header to come in. We will Buffer.concat() | ||
// it to the next write(), which is a mem copy, but a small one. | ||
// | ||
// this[QUEUE] is a Yallist of entries that haven't been emitted | ||
// yet this can only get filled up if the user keeps write()ing after | ||
// a write() returns false, or does a write() with more than one entry | ||
// | ||
// We don't buffer chunks, we always parse them and either create an | ||
// entry, or push it into the active entry. The ReadEntry class knows | ||
// to throw data away if .ignore=true | ||
// | ||
// Shift entry off the buffer when it emits 'end', and emit 'entry' for | ||
// the next one in the list. | ||
// | ||
// At any time, we're pushing body chunks into the entry at WRITEENTRY, | ||
// and waiting for 'end' on the entry at READENTRY | ||
// | ||
// ignored entries get .resume() called on them straight away | ||
module.exports = Parse.create = Parse | ||
const path = require('path') | ||
const Header = require('./header.js') | ||
const EE = require('events') | ||
const Yallist = require('yallist') | ||
const maxMetaEntrySize = 1024 * 1024 | ||
const Entry = require('./read-entry.js') | ||
const Pax = require('./pax.js') | ||
const zlib = require('minizlib') | ||
var stream = require("stream") | ||
, Stream = stream.Stream | ||
, BlockStream = require("block-stream") | ||
, tar = require("../tar.js") | ||
, TarHeader = require("./header.js") | ||
, Entry = require("./entry.js") | ||
, BufferEntry = require("./buffer-entry.js") | ||
, ExtendedHeader = require("./extended-header.js") | ||
, assert = require("assert").ok | ||
, inherits = require("inherits") | ||
, fstream = require("fstream") | ||
const gzipHeader = new Buffer([0x1f, 0x8b]) | ||
const STATE = Symbol('state') | ||
const WRITEENTRY = Symbol('writeEntry') | ||
const READENTRY = Symbol('readEntry') | ||
const NEXTENTRY = Symbol('nextEntry') | ||
const PROCESSENTRY = Symbol('processEntry') | ||
const EX = Symbol('extendedHeader') | ||
const GEX = Symbol('globalExtendedHeader') | ||
const META = Symbol('meta') | ||
const EMITMETA = Symbol('emitMeta') | ||
const BUFFER = Symbol('buffer') | ||
const QUEUE = Symbol('queue') | ||
const ENDED = Symbol('ended') | ||
const EMITTEDEND = Symbol('emittedEnd') | ||
const EMIT = Symbol('emit') | ||
const UNZIP = Symbol('unzip') | ||
const CONSUMECHUNK = Symbol('consumeChunk') | ||
const CONSUMECHUNKSUB = Symbol('consumeChunkSub') | ||
const CONSUMEBODY = Symbol('consumeBody') | ||
const CONSUMEMETA = Symbol('consumeMeta') | ||
const CONSUMEHEADER = Symbol('consumeHeader') | ||
const CONSUMING = Symbol('consuming') | ||
const BUFFERCONCAT = Symbol('bufferConcat') | ||
const MAYBEEND = Symbol('maybeEnd') | ||
const WRITING = Symbol('writing') | ||
const ABORTED = Symbol('aborted') | ||
// reading a tar is a lot like reading a directory | ||
// However, we're actually not going to run the ctor, | ||
// since it does a stat and various other stuff. | ||
// This inheritance gives us the pause/resume/pipe | ||
// behavior that is desired. | ||
inherits(Parse, fstream.Reader) | ||
function noop () { return true } | ||
function Parse () { | ||
var me = this | ||
if (!(me instanceof Parse)) return new Parse() | ||
module.exports = class Parser extends EE { | ||
constructor (opt) { | ||
const start = process.hrtime() | ||
opt = opt || {} | ||
super(opt) | ||
// doesn't apply fstream.Reader ctor? | ||
// no, becasue we don't want to stat/etc, we just | ||
// want to get the entry/add logic from .pipe() | ||
Stream.apply(me) | ||
this.strict = !!opt.strict | ||
this.maxMetaEntrySize = opt.maxMetaEntrySize || maxMetaEntrySize | ||
this.filter = typeof opt.filter === 'function' ? opt.filter : noop | ||
me.writable = true | ||
me.readable = true | ||
me._stream = new BlockStream(512) | ||
me.position = 0 | ||
me._ended = false | ||
this[QUEUE] = new Yallist() | ||
this[BUFFER] = null | ||
this[READENTRY] = null | ||
this[WRITEENTRY] = null | ||
this[STATE] = 'begin' | ||
this[META] = '' | ||
this[EX] = null | ||
this[GEX] = null | ||
this[ENDED] = false | ||
this[UNZIP] = null | ||
this[ABORTED] = false | ||
if (typeof opt.onwarn === 'function') | ||
this.on('warn', opt.onwarn) | ||
if (typeof opt.onentry === 'function') | ||
this.on('entry', opt.onentry) | ||
} | ||
me._stream.on("error", function (e) { | ||
me.emit("error", e) | ||
}) | ||
[CONSUMEHEADER] (chunk, position) { | ||
const header = new Header(chunk, position) | ||
me._stream.on("data", function (c) { | ||
me._process(c) | ||
}) | ||
if (header.nullBlock) | ||
this[EMIT]('nullBlock') | ||
else if (!header.cksumValid) | ||
this.warn('invalid entry', header) | ||
else if (!header.path) | ||
this.warn('invalid: path is required', header) | ||
else { | ||
const type = header.type | ||
if (/^(Symbolic)?Link$/.test(type) && !header.linkpath) | ||
this.warn('invalid: linkpath required', header) | ||
else if (!/^(Symbolic)?Link$/.test(type) && header.linkpath) | ||
this.warn('invalid: linkpath forbidden', header) | ||
else { | ||
const entry = this[WRITEENTRY] = new Entry(header, this[EX], this[GEX]) | ||
me._stream.on("end", function () { | ||
me._streamEnd() | ||
}) | ||
if (entry.meta) { | ||
if (entry.size > this.maxMetaEntrySize) { | ||
entry.ignore = true | ||
this[EMIT]('ignoredEntry', entry) | ||
this[STATE] = 'ignore' | ||
} else if (entry.size > 0) { | ||
this[META] = '' | ||
entry.on('data', c => this[META] += c) | ||
this[STATE] = 'meta' | ||
} | ||
} else { | ||
me._stream.on("drain", function () { | ||
me.emit("drain") | ||
}) | ||
} | ||
this[EX] = null | ||
entry.ignore = entry.ignore || !this.filter(entry.path, entry) | ||
if (entry.ignore) { | ||
this[EMIT]('ignoredEntry', entry) | ||
this[STATE] = entry.remain ? 'ignore' : 'begin' | ||
} else { | ||
if (entry.remain) | ||
this[STATE] = 'body' | ||
else { | ||
this[STATE] = 'begin' | ||
entry.end() | ||
} | ||
// overridden in Extract class, since it needs to | ||
// wait for its DirWriter part to finish before | ||
// emitting "end" | ||
Parse.prototype._streamEnd = function () { | ||
var me = this | ||
if (!me._ended || me._entry) me.error("unexpected eof") | ||
me.emit("end") | ||
} | ||
if (!this[READENTRY]) { | ||
this[QUEUE].push(entry) | ||
this[NEXTENTRY]() | ||
} else | ||
this[QUEUE].push(entry) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// a tar reader is actually a filter, not just a readable stream. | ||
// So, you should pipe a tarball stream into it, and it needs these | ||
// write/end methods to do that. | ||
Parse.prototype.write = function (c) { | ||
if (this._ended) { | ||
// gnutar puts a LOT of nulls at the end. | ||
// you can keep writing these things forever. | ||
// Just ignore them. | ||
for (var i = 0, l = c.length; i > l; i ++) { | ||
if (c[i] !== 0) return this.error("write() after end()") | ||
[PROCESSENTRY] (entry) { | ||
let go = true | ||
if (!entry) { | ||
this[READENTRY] = null | ||
go = false | ||
} else if (Array.isArray(entry)) | ||
this.emit.apply(this, entry) | ||
else { | ||
this[READENTRY] = entry | ||
this.emit('entry', entry) | ||
if (!entry.emittedEnd) { | ||
entry.on('end', _ => this[NEXTENTRY]()) | ||
go = false | ||
} | ||
} | ||
return | ||
return go | ||
} | ||
return this._stream.write(c) | ||
} | ||
Parse.prototype.end = function (c) { | ||
this._ended = true | ||
return this._stream.end(c) | ||
} | ||
[NEXTENTRY] () { | ||
do {} while (this[PROCESSENTRY](this[QUEUE].shift())) | ||
// don't need to do anything, since we're just | ||
// proxying the data up from the _stream. | ||
// Just need to override the parent's "Not Implemented" | ||
// error-thrower. | ||
Parse.prototype._read = function () {} | ||
if (!this[QUEUE].length) { | ||
// At this point, there's nothing in the queue, but we may have an | ||
// entry which is being consumed (readEntry). | ||
// If we don't, then we definitely can handle more data. | ||
// If we do, and either it's flowing, or it has never had any data | ||
// written to it, then it needs more. | ||
// The only other possibility is that it has returned false from a | ||
// write() call, so we wait for the next drain to continue. | ||
const re = this[READENTRY] | ||
const drainNow = !re || re.flowing || re.size === re.remain | ||
if (drainNow) { | ||
if (!this[WRITING]) | ||
this.emit('drain') | ||
} else | ||
re.once('drain', _ => this.emit('drain')) | ||
} | ||
} | ||
Parse.prototype._process = function (c) { | ||
assert(c && c.length === 512, "block size should be 512") | ||
[CONSUMEBODY] (chunk, position) { | ||
// write up to but no more than writeEntry.blockRemain | ||
const entry = this[WRITEENTRY] | ||
const br = entry.blockRemain | ||
const c = (br >= chunk.length && position === 0) ? chunk | ||
: chunk.slice(position, position + br) | ||
// one of three cases. | ||
// 1. A new header | ||
// 2. A part of a file/extended header | ||
// 3. One of two or more EOF null blocks | ||
entry.write(c) | ||
if (this._entry) { | ||
var entry = this._entry | ||
if(!entry._abort) entry.write(c) | ||
else { | ||
entry._remaining -= c.length | ||
if(entry._remaining < 0) entry._remaining = 0 | ||
} | ||
if (entry._remaining === 0) { | ||
if (!entry.blockRemain) { | ||
this[STATE] = 'begin' | ||
this[WRITEENTRY] = null | ||
entry.end() | ||
this._entry = null | ||
} | ||
} else { | ||
// either zeroes or a header | ||
var zero = true | ||
for (var i = 0; i < 512 && zero; i ++) { | ||
zero = c[i] === 0 | ||
} | ||
// eof is *at least* 2 blocks of nulls, and then the end of the | ||
// file. you can put blocks of nulls between entries anywhere, | ||
// so appending one tarball to another is technically valid. | ||
// ending without the eof null blocks is not allowed, however. | ||
if (zero) { | ||
if (this._eofStarted) | ||
this._ended = true | ||
this._eofStarted = true | ||
} else { | ||
this._eofStarted = false | ||
this._startEntry(c) | ||
} | ||
return c.length | ||
} | ||
this.position += 512 | ||
} | ||
[CONSUMEMETA] (chunk, position) { | ||
const entry = this[WRITEENTRY] | ||
const ret = this[CONSUMEBODY](chunk, position) | ||
// take a header chunk, start the right kind of entry. | ||
Parse.prototype._startEntry = function (c) { | ||
var header = new TarHeader(c) | ||
, self = this | ||
, entry | ||
, ev | ||
, EntryType | ||
, onend | ||
, meta = false | ||
// if we finished, then the entry is reset | ||
if (!this[WRITEENTRY]) | ||
this[EMITMETA](entry) | ||
if (null === header.size || !header.cksumValid) { | ||
var e = new Error("invalid tar file") | ||
e.header = header | ||
e.tar_file_offset = this.position | ||
e.tar_block = this.position / 512 | ||
return this.emit("error", e) | ||
return ret | ||
} | ||
switch (tar.types[header.type]) { | ||
case "File": | ||
case "OldFile": | ||
case "Link": | ||
case "SymbolicLink": | ||
case "CharacterDevice": | ||
case "BlockDevice": | ||
case "Directory": | ||
case "FIFO": | ||
case "ContiguousFile": | ||
case "GNUDumpDir": | ||
// start a file. | ||
// pass in any extended headers | ||
// These ones consumers are typically most interested in. | ||
EntryType = Entry | ||
ev = "entry" | ||
break | ||
[EMIT] (ev, data, extra) { | ||
if (!this[QUEUE].length && !this[READENTRY]) | ||
this.emit(ev, data, extra) | ||
else | ||
this[QUEUE].push([ev, data, extra]) | ||
} | ||
case "GlobalExtendedHeader": | ||
// extended headers that apply to the rest of the tarball | ||
EntryType = ExtendedHeader | ||
onend = function () { | ||
self._global = self._global || {} | ||
Object.keys(entry.fields).forEach(function (k) { | ||
self._global[k] = entry.fields[k] | ||
[EMITMETA] (entry) { | ||
this[EMIT]('meta', this[META]) | ||
switch (entry.type) { | ||
case 'ExtendedHeader': | ||
case 'OldExtendedHeader': | ||
this[EX] = Pax.parse(this[META], this[EX], false) | ||
break | ||
case 'GlobalExtendedHeader': | ||
this[GEX] = Pax.parse(this[META], this[GEX], true) | ||
break | ||
case 'NextFileHasLongPath': | ||
case 'OldGnuLongPath': | ||
this[EX] = this[EX] || Object.create(null) | ||
this[EX].path = this[META] | ||
break | ||
case 'NextFileHasLongLinkpath': | ||
this[EX] = this[EX] || Object.create(null) | ||
this[EX].linkpath = this[META] | ||
break | ||
/* istanbul ignore next */ | ||
default: throw new Error('unknown meta: ' + entry.type) | ||
} | ||
} | ||
abort (msg, error) { | ||
this[ABORTED] = true | ||
this.warn(msg, error) | ||
this.emit('abort') | ||
} | ||
write (chunk) { | ||
if (this[ABORTED]) | ||
return | ||
// first write, might be gzipped | ||
if (this[UNZIP] === null && chunk) { | ||
if (this[BUFFER]) { | ||
chunk = Buffer.concat([this[BUFFER], chunk]) | ||
this[BUFFER] = null | ||
} | ||
if (chunk.length < gzipHeader.length) { | ||
this[BUFFER] = chunk | ||
return true | ||
} | ||
for (let i = 0; this[UNZIP] === null && i < gzipHeader.length; i++) { | ||
if (chunk[i] !== gzipHeader[i]) | ||
this[UNZIP] = false | ||
} | ||
if (this[UNZIP] === null) { | ||
const ended = this[ENDED] | ||
this[ENDED] = false | ||
this[UNZIP] = new zlib.Unzip() | ||
this[UNZIP].on('data', chunk => this[CONSUMECHUNK](chunk)) | ||
this[UNZIP].on('error', er => | ||
this.abort('zlib error: ' + er.message, er)) | ||
this[UNZIP].on('end', _ => { | ||
this[ENDED] = true | ||
this[CONSUMECHUNK]() | ||
}) | ||
return ended ? this[UNZIP].end(chunk) : this[UNZIP].write(chunk) | ||
} | ||
ev = "globalExtendedHeader" | ||
meta = true | ||
break | ||
} | ||
case "ExtendedHeader": | ||
case "OldExtendedHeader": | ||
// extended headers that apply to the next entry | ||
EntryType = ExtendedHeader | ||
onend = function () { | ||
self._extended = entry.fields | ||
} | ||
ev = "extendedHeader" | ||
meta = true | ||
break | ||
this[WRITING] = true | ||
if (this[UNZIP]) | ||
this[UNZIP].write(chunk) | ||
else | ||
this[CONSUMECHUNK](chunk) | ||
this[WRITING] = false | ||
case "NextFileHasLongLinkpath": | ||
// set linkpath=<contents> in extended header | ||
EntryType = BufferEntry | ||
onend = function () { | ||
self._extended = self._extended || {} | ||
self._extended.linkpath = entry.body | ||
} | ||
ev = "longLinkpath" | ||
meta = true | ||
break | ||
// return false if there's a queue, or if the current entry isn't flowing | ||
const ret = | ||
this[QUEUE].length ? false : | ||
this[READENTRY] ? this[READENTRY].flowing : | ||
true | ||
case "NextFileHasLongPath": | ||
case "OldGnuLongPath": | ||
// set path=<contents> in file-extended header | ||
EntryType = BufferEntry | ||
onend = function () { | ||
self._extended = self._extended || {} | ||
self._extended.path = entry.body | ||
} | ||
ev = "longPath" | ||
meta = true | ||
break | ||
// if we have no queue, then that means a clogged READENTRY | ||
if (!ret && !this[QUEUE].length) | ||
this[READENTRY].once('drain', _ => this.emit('drain')) | ||
default: | ||
// all the rest we skip, but still set the _entry | ||
// member, so that we can skip over their data appropriately. | ||
// emit an event to say that this is an ignored entry type? | ||
EntryType = Entry | ||
ev = "ignoredEntry" | ||
break | ||
return ret | ||
} | ||
var global, extended | ||
if (meta) { | ||
global = extended = null | ||
} else { | ||
var global = this._global | ||
var extended = this._extended | ||
[BUFFERCONCAT] (c) { | ||
if (c && !this[ABORTED]) | ||
this[BUFFER] = this[BUFFER] ? Buffer.concat([this[BUFFER], c]) : c | ||
} | ||
// extendedHeader only applies to one entry, so once we start | ||
// an entry, it's over. | ||
this._extended = null | ||
[MAYBEEND] () { | ||
if (this[ENDED] && !this[EMITTEDEND] && !this[ABORTED]) { | ||
this[EMITTEDEND] = true | ||
const entry = this[WRITEENTRY] | ||
if (entry && entry.blockRemain) { | ||
const have = this[BUFFER] ? this[BUFFER].length : 0 | ||
this.warn('Truncated input (needed ' + entry.blockRemain + | ||
' more bytes, only ' + have + ' available)', entry) | ||
if (this[BUFFER]) | ||
entry.write(this[BUFFER]) | ||
entry.end() | ||
} | ||
this[EMIT]('end') | ||
} | ||
} | ||
entry = new EntryType(header, extended, global) | ||
entry.meta = meta | ||
// only proxy data events of normal files. | ||
if (!meta) { | ||
entry.on("data", function (c) { | ||
me.emit("data", c) | ||
}) | ||
[CONSUMECHUNK] (chunk) { | ||
if (this[CONSUMING]) { | ||
this[BUFFERCONCAT](chunk) | ||
} else if (!chunk && !this[BUFFER]) { | ||
this[MAYBEEND]() | ||
} else { | ||
this[CONSUMING] = true | ||
if (this[BUFFER]) { | ||
this[BUFFERCONCAT](chunk) | ||
const c = this[BUFFER] | ||
this[BUFFER] = null | ||
this[CONSUMECHUNKSUB](c) | ||
} else { | ||
this[CONSUMECHUNKSUB](chunk) | ||
} | ||
while (this[BUFFER] && this[BUFFER].length >= 512 && !this[ABORTED]) { | ||
const c = this[BUFFER] | ||
this[BUFFER] = null | ||
this[CONSUMECHUNKSUB](c) | ||
} | ||
this[CONSUMING] = false | ||
} | ||
if (!this[BUFFER] || this[ENDED]) | ||
this[MAYBEEND]() | ||
} | ||
if (onend) entry.on("end", onend) | ||
[CONSUMECHUNKSUB] (chunk) { | ||
// we know that we are in CONSUMING mode, so anything written goes into | ||
// the buffer. Advance the position and put any remainder in the buffer. | ||
let position = 0 | ||
let length = chunk.length | ||
while (position + 512 <= length && !this[ABORTED]) { | ||
switch (this[STATE]) { | ||
case 'begin': | ||
this[CONSUMEHEADER](chunk, position) | ||
position += 512 | ||
break | ||
this._entry = entry | ||
var me = this | ||
case 'ignore': | ||
case 'body': | ||
position += this[CONSUMEBODY](chunk, position) | ||
break | ||
entry.on("pause", function () { | ||
me.pause() | ||
}) | ||
case 'meta': | ||
position += this[CONSUMEMETA](chunk, position) | ||
break | ||
entry.on("resume", function () { | ||
me.resume() | ||
}) | ||
/* istanbul ignore next */ | ||
default: | ||
throw new Error('invalid state: ' + this[STATE]) | ||
} | ||
} | ||
if (this.listeners("*").length) { | ||
this.emit("*", ev, entry) | ||
if (position < length) { | ||
if (this[BUFFER]) | ||
this[BUFFER] = Buffer.concat([chunk.slice(position), this[BUFFER]]) | ||
else | ||
this[BUFFER] = chunk.slice(position) | ||
} | ||
} | ||
this.emit(ev, entry) | ||
warn (msg, data) { | ||
if (!this.strict) | ||
this.emit('warn', msg, data) | ||
else if (data instanceof Error) | ||
this.emit('error', data) | ||
else { | ||
const er = new Error(msg) | ||
er.data = data | ||
this[EMIT]('error', er) | ||
} | ||
} | ||
// Zero-byte entry. End immediately. | ||
if (entry.props.size === 0) { | ||
entry.end() | ||
this._entry = null | ||
end (chunk) { | ||
if (!this[ABORTED]) { | ||
if (this[UNZIP]) | ||
this[UNZIP].end(chunk) | ||
else { | ||
this[ENDED] = true | ||
this.write(chunk) | ||
} | ||
} | ||
} | ||
} |
@@ -5,23 +5,36 @@ { | ||
"description": "tar for node", | ||
"version": "2.2.1", | ||
"version": "3.0.0", | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/isaacs/node-tar.git" | ||
"url": "https://github.com/npm/node-tar.git" | ||
}, | ||
"main": "tar.js", | ||
"scripts": { | ||
"test": "tap test/*.js" | ||
"test": "tap test/*.js --100 -J --coverage-report=text", | ||
"preversion": "npm test", | ||
"postversion": "npm publish", | ||
"postpublish": "git push origin --all; git push origin --tags", | ||
"genparse": "node scripts/generate-parse-fixtures.js", | ||
"bench": "for i in benchmarks/*/*.js; do echo $i; for j in {1..5}; do node $i || break; done; done" | ||
}, | ||
"dependencies": { | ||
"block-stream": "*", | ||
"fstream": "^1.0.2", | ||
"inherits": "2" | ||
"minipass": "^2.0.1", | ||
"minizlib": "^1.0.3", | ||
"mkdirp": "^0.5.0", | ||
"yallist": "^3.0.2" | ||
}, | ||
"devDependencies": { | ||
"graceful-fs": "^4.1.2", | ||
"chmodr": "^1.0.2", | ||
"events-to-array": "^1.1.2", | ||
"mutate-fs": "^1.1.0", | ||
"rimraf": "1.x", | ||
"tap": "0.x", | ||
"mkdirp": "^0.5.0" | ||
"tap": "^10.3.1", | ||
"tar-fs": "^1.15.2", | ||
"tar-stream": "^1.5.2" | ||
}, | ||
"license": "ISC" | ||
"license": "ISC", | ||
"files": [ | ||
"tar.js", | ||
"lib/" | ||
] | ||
} |
811
README.md
# node-tar | ||
Tar for Node.js. | ||
[Fast](./benchmarks) and full-featured Tar for Node.js | ||
[![NPM](https://nodei.co/npm/tar.png)](https://nodei.co/npm/tar/) | ||
The API is designed to mimic the behavior of `tar(1)` on unix systems. | ||
If you are familiar with how tar works, most of this will hopefully be | ||
straightforward for you. If not, then hopefully this module can teach | ||
you useful unix skills that may come in handy someday :) | ||
## API | ||
## Background | ||
See `examples/` for usage examples. | ||
A "tar file" or "tarball" is an archive of file system entries | ||
(directories, files, links, etc.) The name comes from "tape archive". | ||
If you run `man tar` on almost any Unix command line, you'll learn | ||
quite a bit about what it can do, and its history. | ||
### var tar = require('tar') | ||
Tar has 5 main top-level commands: | ||
Returns an object with `.Pack`, `.Extract` and `.Parse` methods. | ||
* `c` Create an archive | ||
* `r` Replace entries within an archive | ||
* `u` Update entries within an archive (ie, replace if they're newer) | ||
* `t` List out the contents of an archive | ||
* `x` Extract an archive to disk | ||
### tar.Pack([properties]) | ||
The other flags and options modify how this top level function works. | ||
Returns a through stream. Use | ||
[fstream](https://npmjs.org/package/fstream) to write files into the | ||
pack stream and you will receive tar archive data from the pack | ||
stream. | ||
## High-Level API | ||
This only works with directories, it does not work with individual files. | ||
These 5 functions are the high-level API. All of them have a | ||
single-character name (for unix nerds familiar with `tar(1)`) as well | ||
as a long name (for everyone else). | ||
The optional `properties` object are used to set properties in the tar | ||
'Global Extended Header'. If the `fromBase` property is set to true, | ||
the tar will contain files relative to the path passed, and not with | ||
the path included. | ||
All the high-level functions take the following arguments, all three | ||
of which are optional and may be omitted. | ||
### tar.Extract([options]) | ||
1. `options` - An optional object specifying various options | ||
2. `paths` - An array of paths to add or extract | ||
3. `callback` - Called when the command is completed, if async. (If | ||
sync or no file specified, providing a callback throws a | ||
`TypeError`.) | ||
Returns a through stream. Write tar data to the stream and the files | ||
in the tarball will be extracted onto the filesystem. | ||
If the command is sync (ie, if `options.sync=true`), then the | ||
callback is not allowed, since the action will be completed immediately. | ||
`options` can be: | ||
If a `file` argument is specified, and the command is async, then a | ||
`Promise` is returned. In this case, if async, a callback may be | ||
provided which is called when the command is completed. | ||
If a `file` option is not specified, then a stream is returned. For | ||
`create`, this is a readable stream of the generated archive. For | ||
`list` and `extract` this is a writable stream that an archive should | ||
be written into. If a file is not specified, then a callback is not | ||
allowed, because you're already getting a stream to work with. | ||
`replace` and `update` only work on existing archives, and so require | ||
a `file` argument. | ||
Sync commands without a file argument return a stream that acts on its | ||
input immediately in the same tick. For readable streams, this means | ||
that all of the data is immediately available by calling | ||
`stream.read()`. For writable streams, it will be acted upon as soon | ||
as it is provided, but this can be at any time. | ||
### Warnings | ||
Some things cause tar to emit a warning, but should usually not cause | ||
the entire operation to fail. There are three ways to handle | ||
warnings: | ||
1. **Ignore them** (default) Invalid entries won't be put in the | ||
archive, and invalid entries won't be unpacked. This is usually | ||
fine, but can hide failures that you might care about. | ||
2. **Notice them** Add an `onwarn` function to the options, or listen | ||
to the `'warn'` event on any tar stream. The function will get | ||
called as `onwarn(message, data)`. Handle as appropriate. | ||
3. **Explode them.** Set `strict: true` in the options object, and | ||
`warn` messages will be emitted as `'error'` events instead. If | ||
there's no `error` handler, this causes the program to crash. If | ||
used with a promise-returning/callback-taking method, then it'll | ||
send the error to the promise/callback. | ||
### Examples | ||
The API mimics the `tar(1)` command line functionality, with aliases | ||
for more human-readable option and function names. The goal is that | ||
if you know how to use `tar(1)` in Unix, then you know how to use | ||
`require('tar')` in JavaScript. | ||
To replicate `tar czf my-tarball.tgz files and folders`, you'd do: | ||
```js | ||
{ | ||
path: '/path/to/extract/tar/into', | ||
strip: 0, // how many path segments to strip from the root when extracting | ||
} | ||
tar.c( | ||
{ | ||
gzip: <true|gzip options>, | ||
file: 'my-tarball.tgz' | ||
}, | ||
['some', 'files', 'and', 'folders'] | ||
).then(_ => { .. tarball has been created .. }) | ||
``` | ||
`options` also get passed to the `fstream.Writer` instance that `tar` | ||
uses internally. | ||
To replicate `tar cz files and folders > my-tarball.tgz`, you'd do: | ||
### tar.Parse() | ||
```js | ||
tar.c( // or tar.create | ||
{ | ||
gzip: <true|gzip options> | ||
}, | ||
['some', 'files', 'and', 'folders'] | ||
).pipe(fs.createWriteStream('my-tarball.tgz') | ||
``` | ||
Returns a writable stream. Write tar data to it and it will emit | ||
`entry` events for each entry parsed from the tarball. This is used by | ||
`tar.Extract`. | ||
To replicate `tar xf my-tarball.tgz` you'd do: | ||
```js | ||
tar.x( // or tar.extract( | ||
{ | ||
file: 'my-tarball.tgz' | ||
} | ||
).then(_=> { .. tarball has been dumped in cwd .. }) | ||
``` | ||
To replicate `cat my-tarball.tgz | tar x -C some-dir --strip=1`: | ||
```js | ||
fs.createReadStream('my-tarball.tgz').pipe( | ||
tar.x({ | ||
strip: 1, | ||
C: 'some-dir' // alias for cwd:'some-dir', also ok | ||
}) | ||
) | ||
``` | ||
To replicate `tar tf my-tarball.tgz`, do this: | ||
```js | ||
tar.t({ | ||
file: 'my-tarball.tgz', | ||
onentry: entry => { .. do whatever with it .. } | ||
}) | ||
``` | ||
To replicate `cat my-tarball.tgz | tar t` do: | ||
```js | ||
fs.createReadStream('my-tarball.tgz') | ||
.pipe(tar.t()) | ||
.on('entry', entry => { .. do whatever with it .. }) | ||
``` | ||
To do anything synchronous, add `sync: true` to the options. Note | ||
that sync functions don't take a callback and don't return a promise. | ||
When the function returns, it's already done. Sync methods without a | ||
file argument return a sync stream, which flushes immediately. But, | ||
of course, it still won't be done until you `.end()` it. | ||
To filter entries, add `filter: <function>` to the options. | ||
Tar-creating methods call the filter with `filter(path, stat)`. | ||
Tar-reading methods (including extraction) call the filter with | ||
`filter(path, entry)`. The filter is called in the `this`-context of | ||
the `Pack` or `Unpack` stream object. | ||
The arguments list to `tar t` and `tar x` specify a list of filenames | ||
to extract or list, so they're equivalent to a filter that tests if | ||
the file is in the list. | ||
For those who _aren't_ fans of tar's single-character command names: | ||
``` | ||
tar.c === tar.create | ||
tar.r === tar.replace (appends to archive, file is required) | ||
tar.u === tar.update (appends if newer, file is required) | ||
tar.x === tar.extract | ||
tar.t === tar.list | ||
``` | ||
Keep reading for all the command descriptions and options, as well as | ||
the low-level API that they are built on. | ||
### tar.c(options, fileList, callback) [alias: tar.create] | ||
Create a tarball archive. | ||
The `fileList` is an array of paths to add to the tarball. Adding a | ||
directory also adds its children recursively. | ||
The following options are supported: | ||
- `file` Write the tarball archive to the specified filename. If this | ||
is specified, then the callback will be fired when the file has been | ||
written, and a promise will be returned that resolves when the file | ||
is written. If a filename is not specified, then a Readable Stream | ||
will be returned which will emit the file data. [Alias: `f`] | ||
- `sync` Act synchronously. If this is set, then any provided file | ||
will be fully written after the call to `tar.c`. If this is set, | ||
and a file is not provided, then the resulting stream will already | ||
have the data ready to `read` or `emit('data')` as soon as you | ||
request it. | ||
- `onwarn` A function that will get called with `(message, data)` for | ||
any warnings encountered. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `cwd` The current working directory for creating the archive. | ||
Defaults to `process.cwd()`. [Alias: `C`] | ||
- `prefix` A path portion to prefix onto the entries in the archive. | ||
- `gzip` Set to any truthy value to create a gzipped archive, or an | ||
object with settings for `zlib.Gzip()` [Alias: `z`] | ||
- `filter` A function that gets called with `(path, stat)` for each | ||
entry being added. Return `true` to add the entry to the archive, | ||
or `false` to omit it. | ||
- `portable` Omit metadata that is system-specific: `ctime`, `atime`, | ||
`uid`, `gid`, `uname`, `gname`, `dev`, `ino`, and `nlink`. Note | ||
that `mtime` is still included, because this is necessary other | ||
time-based operations. | ||
- `preservePaths` Allow absolute paths and paths containing `..`. By | ||
default, `/` is stripped from absolute paths, `..` paths are not | ||
added to the archive. [Alias: `P`] | ||
- `mode` The mode to set on the created file archive | ||
- `noDirRecurse` Do not recursively archive the contents of | ||
directories. [Alias: `n`] | ||
- `follow` Set to true to pack the targets of symbolic links. Without | ||
this option, symbolic links are archived as such. [Alias: `L`, `h`] | ||
The following options are mostly internal, but can be modified in some | ||
advanced use cases, such as re-using caches between runs. | ||
- `linkCache` A Map object containing the device and inode value for | ||
any file whose nlink is > 1, to identify hard links. | ||
- `statCache` A Map object that caches calls `lstat`. | ||
- `readdirCache` A Map object that caches calls to `readdir`. | ||
- `jobs` A number specifying how many concurrent jobs to run. | ||
Defaults to 4. | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 1 MB. | ||
### tar.x(options, fileList, callback) [alias: tar.extract] | ||
Extract a tarball archive. | ||
The `fileList` is an array of paths to extract from the tarball. If | ||
no paths are provided, then all the entries are extracted. | ||
If the archive is gzipped, then tar will detect this and unzip it. | ||
Note that all directories that are created will be forced to be | ||
writable, readable, and listable by their owner, to avoid cases where | ||
a directory prevents extraction of child entries by virtue of its | ||
mode. | ||
The following options are supported: | ||
- `cwd` Extract files relative to the specified directory. Defaults | ||
to `process.cwd()`. [Alias: `C`] | ||
- `file` The archive file to extract. If not specified, then a | ||
Writable stream is returned where the archive data should be | ||
written. [Alias: `f`] | ||
- `sync` Create files and directories synchronously. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `filter` A function that gets called with `(path, entry)` for each | ||
entry being unpacked. Return `true` to unpack the entry from the | ||
archive, or `false` to skip it. | ||
- `newer` Set to true to keep the existing file on disk if it's newer | ||
than the file in the archive. [Alias: `keep-newer`, | ||
`keep-newer-files`] | ||
- `keep` Do not overwrite existing files. In particular, if a file | ||
appears more than once in an archive, later copies will not | ||
overwrite earlier copies. [Alias: `k`, `keep-existing`] | ||
- `preservePaths` Allow absolute paths, paths containing `..`, and | ||
extracting through symbolic links. By default, `/` is stripped from | ||
absolute paths, `..` paths are not extracted, and any file whose | ||
location would be modified by a symbolic link is not extracted. | ||
[Alias: `P`] | ||
- `unlink` Unlink files before creating them. Without this option, | ||
tar overwrites existing files, which preserves existing hardlinks. | ||
With this option, existing hardlinks will be broken, as will any | ||
symlink that would affect the location of an extracted file. [Alias: | ||
`U`] | ||
- `strip` Remove the specified number of leading path elements. | ||
Pathnames with fewer elements will be silently skipped. Note that | ||
the pathname is edited after applying the filter, but before | ||
security checks. [Alias: `strip-components`, `stripComponents`] | ||
- `onwarn` A function that will get called with `(message, data)` for | ||
any warnings encountered. | ||
- `preserveOwner` If true, tar will set the `uid` and `gid` of | ||
extracted entries to the `uid` and `gid` fields in the archive. | ||
This defaults to true when run as root, and false otherwise. If | ||
false, then files and directories will be set with the owner and | ||
group of the user running the process. This is similar to `-p` in | ||
`tar(1)`, but ACLs and other system-specific data is never unpacked | ||
in this implementation, and modes are set by default already. | ||
[Alias: `p`] | ||
The following options are mostly internal, but can be modified in some | ||
advanced use cases, such as re-using caches between runs. | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 16 MB. | ||
- `umask` Filter the modes of entries like `process.umask()`. | ||
- `dmode` Default mode for directories | ||
- `fmode` Default mode for files | ||
- `dirCache` A Map object of which directories exist. | ||
- `maxMetaEntrySize` The maximum size of meta entries that is | ||
supported. Defaults to 1 MB. | ||
### tar.t(options, fileList, callback) [alias: tar.list] | ||
List the contents of a tarball archive. | ||
The `fileList` is an array of paths to list from the tarball. If | ||
no paths are provided, then all the entries are listed. | ||
If the archive is gzipped, then tar will detect this and unzip it. | ||
Returns an event emitter that emits `entry` events with | ||
`tar.ReadEntry` objects. However, they don't emit `'data'` or `'end'` | ||
events. (If you want to get actual readable entries, use the | ||
`tar.Parse` class instead.) | ||
The following options are supported: | ||
- `cwd` Extract files relative to the specified directory. Defaults | ||
to `process.cwd()`. [Alias: `C`] | ||
- `file` The archive file to list. If not specified, then a | ||
Writable stream is returned where the archive data should be | ||
written. [Alias: `f`] | ||
- `sync` Read the specified file synchronously. (This has no effect | ||
when a file option isn't specified, because entries are emitted as | ||
fast as they are parsed from the stream anyway.) | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `filter` A function that gets called with `(path, entry)` for each | ||
entry being listed. Return `true` to emit the entry from the | ||
archive, or `false` to skip it. | ||
- `onentry` A function that gets called with `(entry)` for each entry | ||
that passes the filter. This is important for when both `file` and | ||
`sync` are set, because it will be called synchronously. | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 16 MB. | ||
### tar.u(options, fileList, callback) [alias: tar.update] | ||
Add files to an archive if they are newer than the entry already in | ||
the tarball archive. | ||
The `fileList` is an array of paths to add to the tarball. Adding a | ||
directory also adds its children recursively. | ||
The following options are supported: | ||
- `file` Required. Write the tarball archive to the specified | ||
filename. [Alias: `f`] | ||
- `sync` Act synchronously. If this is set, then any provided file | ||
will be fully written after the call to `tar.c`. | ||
- `onwarn` A function that will get called with `(message, data)` for | ||
any warnings encountered. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `cwd` The current working directory for adding entries to the | ||
archive. Defaults to `process.cwd()`. [Alias: `C`] | ||
- `prefix` A path portion to prefix onto the entries in the archive. | ||
- `gzip` Set to any truthy value to create a gzipped archive, or an | ||
object with settings for `zlib.Gzip()` [Alias: `z`] | ||
- `filter` A function that gets called with `(path, stat)` for each | ||
entry being added. Return `true` to add the entry to the archive, | ||
or `false` to omit it. | ||
- `portable` Omit metadata that is system-specific: `ctime`, `atime`, | ||
`uid`, `gid`, `uname`, `gname`, `dev`, `ino`, and `nlink`. Note | ||
that `mtime` is still included, because this is necessary other | ||
time-based operations. | ||
- `preservePaths` Allow absolute paths and paths containing `..`. By | ||
default, `/` is stripped from absolute paths, `..` paths are not | ||
added to the archive. [Alias: `P`] | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 16 MB. | ||
- `noDirRecurse` Do not recursively archive the contents of | ||
directories. [Alias: `n`] | ||
- `follow` Set to true to pack the targets of symbolic links. Without | ||
this option, symbolic links are archived as such. [Alias: `L`, `h`] | ||
### tar.r(options, fileList, callback) [alias: tar.replace] | ||
Add files to an existing archive. Because later entries override | ||
earlier entries, this effectively replaces any existing entries. | ||
The `fileList` is an array of paths to add to the tarball. Adding a | ||
directory also adds its children recursively. | ||
The following options are supported: | ||
- `file` Required. Write the tarball archive to the specified | ||
filename. [Alias: `f`] | ||
- `sync` Act synchronously. If this is set, then any provided file | ||
will be fully written after the call to `tar.c`. | ||
- `onwarn` A function that will get called with `(message, data)` for | ||
any warnings encountered. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `cwd` The current working directory for adding entries to the | ||
archive. Defaults to `process.cwd()`. [Alias: `C`] | ||
- `prefix` A path portion to prefix onto the entries in the archive. | ||
- `gzip` Set to any truthy value to create a gzipped archive, or an | ||
object with settings for `zlib.Gzip()` [Alias: `z`] | ||
- `filter` A function that gets called with `(path, stat)` for each | ||
entry being added. Return `true` to add the entry to the archive, | ||
or `false` to omit it. | ||
- `portable` Omit metadata that is system-specific: `ctime`, `atime`, | ||
`uid`, `gid`, `uname`, `gname`, `dev`, `ino`, and `nlink`. Note | ||
that `mtime` is still included, because this is necessary other | ||
time-based operations. | ||
- `preservePaths` Allow absolute paths and paths containing `..`. By | ||
default, `/` is stripped from absolute paths, `..` paths are not | ||
added to the archive. [Alias: `P`] | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 16 MB. | ||
- `noDirRecurse` Do not recursively archive the contents of | ||
directories. [Alias: `n`] | ||
- `follow` Set to true to pack the targets of symbolic links. Without | ||
this option, symbolic links are archived as such. [Alias: `L`, `h`] | ||
## Low-Level API | ||
### class tar.Pack | ||
A readable tar stream. | ||
Has all the standard readable stream interface stuff. `'data'` and | ||
`'end'` events, `read()` method, `pause()` and `resume()`, etc. | ||
#### constructor(options) | ||
The following options are supported: | ||
- `onwarn` A function that will get called with `(message, data)` for | ||
any warnings encountered. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `cwd` The current working directory for creating the archive. | ||
Defaults to `process.cwd()`. | ||
- `prefix` A path portion to prefix onto the entries in the archive. | ||
- `gzip` Set to any truthy value to create a gzipped archive, or an | ||
object with settings for `zlib.Gzip()` | ||
- `filter` A function that gets called with `(path, stat)` for each | ||
entry being added. Return `true` to add the entry to the archive, | ||
or `false` to omit it. | ||
- `portable` Omit metadata that is system-specific: `ctime`, `atime`, | ||
`uid`, `gid`, `uname`, `gname`, `dev`, `ino`, and `nlink`. Note | ||
that `mtime` is still included, because this is necessary other | ||
time-based operations. | ||
- `preservePaths` Allow absolute paths and paths containing `..`. By | ||
default, `/` is stripped from absolute paths, `..` paths are not | ||
added to the archive. | ||
- `linkCache` A Map object containing the device and inode value for | ||
any file whose nlink is > 1, to identify hard links. | ||
- `statCache` A Map object that caches calls `lstat`. | ||
- `readdirCache` A Map object that caches calls to `readdir`. | ||
- `jobs` A number specifying how many concurrent jobs to run. | ||
Defaults to 4. | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 16 MB. | ||
- `noDirRecurse` Do not recursively archive the contents of | ||
directories. | ||
- `follow` Set to true to pack the targets of symbolic links. Without | ||
this option, symbolic links are archived as such. | ||
#### add(path) | ||
Adds an entry to the archive. Returns the Pack stream. | ||
#### write(path) | ||
Adds an entry to the archive. Returns true if flushed. | ||
#### end() | ||
Finishes the archive. | ||
### class tar.Pack.Sync | ||
Synchronous version of `tar.Pack`. | ||
### class tar.Unpack | ||
A writable stream that unpacks a tar archive onto the file system. | ||
All the normal writable stream stuff is supported. `write()` and | ||
`end()` methods, `'drain'` events, etc. | ||
Note that all directories that are created will be forced to be | ||
writable, readable, and listable by their owner, to avoid cases where | ||
a directory prevents extraction of child entries by virtue of its | ||
mode. | ||
`'close'` is emitted when it's done writing stuff to the file system. | ||
#### constructor(options) | ||
- `cwd` Extract files relative to the specified directory. Defaults | ||
to `process.cwd()`. | ||
- `filter` A function that gets called with `(path, entry)` for each | ||
entry being unpacked. Return `true` to unpack the entry from the | ||
archive, or `false` to skip it. | ||
- `newer` Set to true to keep the existing file on disk if it's newer | ||
than the file in the archive. | ||
- `keep` Do not overwrite existing files. In particular, if a file | ||
appears more than once in an archive, later copies will not | ||
overwrite earlier copies. | ||
- `preservePaths` Allow absolute paths, paths containing `..`, and | ||
extracting through symbolic links. By default, `/` is stripped from | ||
absolute paths, `..` paths are not extracted, and any file whose | ||
location would be modified by a symbolic link is not extracted. | ||
- `unlink` Unlink files before creating them. Without this option, | ||
tar overwrites existing files, which preserves existing hardlinks. | ||
With this option, existing hardlinks will be broken, as will any | ||
symlink that would affect the location of an extracted file. | ||
- `strip` Remove the specified number of leading path elements. | ||
Pathnames with fewer elements will be silently skipped. Note that | ||
the pathname is edited after applying the filter, but before | ||
security checks. | ||
- `onwarn` A function that will get called with `(message, data)` for | ||
any warnings encountered. | ||
- `umask` Filter the modes of entries like `process.umask()`. | ||
- `dmode` Default mode for directories | ||
- `fmode` Default mode for files | ||
- `dirCache` A Map object of which directories exist. | ||
- `maxMetaEntrySize` The maximum size of meta entries that is | ||
supported. Defaults to 1 MB. | ||
- `preserveOwner` If true, tar will set the `uid` and `gid` of | ||
extracted entries to the `uid` and `gid` fields in the archive. | ||
This defaults to true when run as root, and false otherwise. If | ||
false, then files and directories will be set with the owner and | ||
group of the user running the process. This is similar to `-p` in | ||
`tar(1)`, but ACLs and other system-specific data is never unpacked | ||
in this implementation, and modes are set by default already. | ||
### class tar.Unpack.Sync | ||
Synchronous version of `tar.Unpack`. | ||
### class tar.Parse | ||
A writable stream that parses a tar archive stream. All the standard | ||
writable stream stuff is supported. | ||
If the archive is gzipped, then tar will detect this and unzip it. | ||
Emits `'entry'` events with `tar.ReadEntry` objects, which are | ||
themselves readable streams that you can pipe wherever. | ||
Each `entry` will not emit until the one before it is flushed through, | ||
so make sure to either consume the data (with `on('data', ...)` or | ||
`.pipe(...)`) or throw it away with `.resume()` to keep the stream | ||
flowing. | ||
#### constructor(options) | ||
Returns an event emitter that emits `entry` events with | ||
`tar.ReadEntry` objects. | ||
The following options are supported: | ||
- `cwd` Extract files relative to the specified directory. Defaults | ||
to `process.cwd()`. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `filter` A function that gets called with `(path, entry)` for each | ||
entry being listed. Return `true` to emit the entry from the | ||
archive, or `false` to skip it. | ||
- `onentry` A function that gets called with `(entry)` for each entry | ||
that passes the filter. | ||
#### abort(message, error) | ||
Stop all parsing activities. This is called when there are zlib | ||
errors. It also emits a warning with the message and error provided. | ||
### class tar.ReadEntry extends [MiniPass](http://npm.im/minipass) | ||
A representation of an entry that is being read out of a tar archive. | ||
It has the following fields: | ||
- `extended` The extended metadata object provided to the constructor. | ||
- `globalExtended` The global extended metadata object provided to the | ||
constructor. | ||
- `remain` The number of bytes remaining to be written into the | ||
stream. | ||
- `blockRemain` The number of 512-byte blocks remaining to be written | ||
into the stream. | ||
- `ignore` Whether this entry should be ignored. | ||
- `meta` True if this represents metadata about the next entry, false | ||
if it represents a filesystem object. | ||
- All the fields from the header, extended header, and global extended | ||
header are added to the ReadEntry object. So it has `path`, `type`, | ||
`size, `mode`, and so on. | ||
#### constructor(header, extended, globalExtended) | ||
Create a new ReadEntry object with the specified header, extended | ||
header, and global extended header values. | ||
### class tar.WriteEntry extends [MiniPass](http://npm.im/minipass) | ||
A representation of an entry that is being written from the file | ||
system into a tar archive. | ||
Emits data for the Header, and for the Pax Extended Header if one is | ||
required, as well as any body data. | ||
Creating a WriteEntry for a directory does not also create | ||
WriteEntry objects for all of the directory contents. | ||
It has the following fields: | ||
- `path` The path field that will be written to the archive. By | ||
default, this is also the path from the cwd to the file system | ||
object. | ||
- `portable` Omit metadata that is system-specific: `ctime`, `atime`, | ||
`uid`, `gid`, `uname`, `gname`, `dev`, `ino`, and `nlink`. Note | ||
that `mtime` is still included, because this is necessary other | ||
time-based operations. | ||
- `myuid` If supported, the uid of the user running the current | ||
process. | ||
- `myuser` The `env.USER` string if set, or `''`. Set as the entry | ||
`uname` field if the file's `uid` matches `this.myuid`. | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 1 MB. | ||
- `linkCache` A Map object containing the device and inode value for | ||
any file whose nlink is > 1, to identify hard links. | ||
- `statCache` A Map object that caches calls `lstat`. | ||
- `preservePaths` Allow absolute paths and paths containing `..`. By | ||
default, `/` is stripped from absolute paths, `..` paths are not | ||
added to the archive. | ||
- `cwd` The current working directory for creating the archive. | ||
Defaults to `process.cwd()`. | ||
- `absolute` The absolute path to the entry on the filesystem. By | ||
default, this is `path.resolve(this.cwd, this.path)`, but it can be | ||
overridden explicitly. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `win32` True if on a windows platform. Causes behavior where paths | ||
replace `\` with `/`. | ||
#### constructor(path, options) | ||
`path` is the path of the entry as it is written in the archive. | ||
The following options are supported: | ||
- `portable` Omit metadata that is system-specific: `ctime`, `atime`, | ||
`uid`, `gid`, `uname`, `gname`, `dev`, `ino`, and `nlink`. Note | ||
that `mtime` is still included, because this is necessary other | ||
time-based operations. | ||
- `maxReadSize` The maximum buffer size for `fs.read()` operations. | ||
Defaults to 1 MB. | ||
- `linkCache` A Map object containing the device and inode value for | ||
any file whose nlink is > 1, to identify hard links. | ||
- `statCache` A Map object that caches calls `lstat`. | ||
- `preservePaths` Allow absolute paths and paths containing `..`. By | ||
default, `/` is stripped from absolute paths, `..` paths are not | ||
added to the archive. | ||
- `cwd` The current working directory for creating the archive. | ||
Defaults to `process.cwd()`. | ||
- `absolute` The absolute path to the entry on the filesystem. By | ||
default, this is `path.resolve(this.cwd, this.path)`, but it can be | ||
overridden explicitly. | ||
- `strict` Treat warnings as crash-worthy errors. Default false. | ||
- `win32` True if on a windows platform. Causes behavior where paths | ||
replace `\` with `/`. | ||
- `onwarn` A function that will get called with `(message, data)` for | ||
any warnings encountered. | ||
#### warn(message, data) | ||
If strict, emit an error with the provided message. | ||
Othewise, emit a `'warn'` event with the provided message and data. | ||
### class tar.WriteEntry.Sync | ||
Synchronous version of tar.WriteEntry | ||
### class tar.Header | ||
A class for reading and writing header blocks. | ||
It has the following fields: | ||
- `nullBlock` True if decoding a block which is entirely composed of | ||
`0x00` null bytes. (Useful because tar files are terminated by | ||
at least 2 null blocks.) | ||
- `cksumValid` True if the checksum in the header is valid, false | ||
otherwise. | ||
- `needPax` True if the values, as encoded, will require a Pax | ||
extended header. | ||
- `path` The path of the entry. | ||
- `mode` The 4 lowest-order octal digits of the file mode. That is, | ||
read/write/execute permissions for world, group, and owner, and the | ||
setuid, setgid, and sticky bits. | ||
- `uid` Numeric user id of the file owner | ||
- `gid` Numeric group id of the file owner | ||
- `size` Size of the file in bytes | ||
- `mtime` Modified time of the file | ||
- `cksum` The checksum of the header. This is generated by adding all | ||
the bytes of the header block, treating the checksum field itself as | ||
all ascii space characters (that is, `0x20`). | ||
- `type` The human-readable name of the type of entry this represents, | ||
or the alphanumeric key if unknown. | ||
- `typeKey` The alphanumeric key for the type of entry this header | ||
represents. | ||
- `linkpath` The target of Link and SymbolicLink entries. | ||
- `uname` Human-readable user name of the file owner | ||
- `gname` Human-readable group name of the file owner | ||
- `devmaj` The major portion of the device number. Always `0` for | ||
files, directories, and links. | ||
- `devmin` The minor portion of the device number. Always `0` for | ||
files, directories, and links. | ||
- `atime` File access time. | ||
- `ctime` File change time. | ||
#### constructor(data, [offset=0]) | ||
`data` is optional. It is either a Buffer that should be interpreted | ||
as a tar Header starting at the specified offset and continuing for | ||
512 bytes, or a data object of keys and values to set on the header | ||
object, and eventually encode as a tar Header. | ||
#### decode(block, offset) | ||
Decode the provided buffer starting at the specified offset. | ||
Buffer length must be greater than 512 bytes. | ||
#### set(data) | ||
Set the fields in the data object. | ||
#### encode(buffer, offset) | ||
Encode the header fields into the buffer at the specified offset. | ||
Returns `this.needPax` to indicate whether a Pax Extended Header is | ||
required to properly encode the specified data. | ||
### class tar.Pax | ||
An object representing a set of key-value pairs in an Pax extended | ||
header entry. | ||
It has the following fields. Where the same name is used, they have | ||
the same semantics as the tar.Header field of the same name. | ||
- `global` True if this represents a global extended header, or false | ||
if it is for a single entry. | ||
- `atime` | ||
- `charset` | ||
- `comment` | ||
- `ctime` | ||
- `gid` | ||
- `gname` | ||
- `linkpath` | ||
- `mtime` | ||
- `path` | ||
- `size` | ||
- `uid` | ||
- `uname` | ||
- `dev` | ||
- `ino` | ||
- `nlink` | ||
#### constructor(object, global) | ||
Set the fields set in the object. `global` is a boolean that defaults | ||
to false. | ||
#### encode() | ||
Return a Buffer containing the header and body for the Pax extended | ||
header entry, or `null` if there is nothing to encode. | ||
#### encodeBody() | ||
Return a string representing the body of the pax extended header | ||
entry. | ||
#### encodeField(fieldName) | ||
Return a string representing the key/value encoding for the specified | ||
fieldName, or `''` if the field is unset. | ||
### tar.Pax.parse(string, extended, global) | ||
Return a new Pax object created by parsing the contents of the string | ||
provided. | ||
If the `extended` object is set, then also add the fields from that | ||
object. (This is necessary because multiple metadata entries can | ||
occur in sequence.) | ||
### tar.types | ||
A translation table for the `type` field in tar headers. | ||
#### tar.types.name.get(code) | ||
Get the human-readable name for a given alphanumeric code. | ||
#### tar.types.code.get(name) | ||
Get the alphanumeric code for a given human-readable name. |
187
tar.js
@@ -1,173 +0,18 @@ | ||
// field paths that every tar file must have. | ||
// header is padded to 512 bytes. | ||
var f = 0 | ||
, fields = {} | ||
, path = fields.path = f++ | ||
, mode = fields.mode = f++ | ||
, uid = fields.uid = f++ | ||
, gid = fields.gid = f++ | ||
, size = fields.size = f++ | ||
, mtime = fields.mtime = f++ | ||
, cksum = fields.cksum = f++ | ||
, type = fields.type = f++ | ||
, linkpath = fields.linkpath = f++ | ||
, headerSize = 512 | ||
, blockSize = 512 | ||
, fieldSize = [] | ||
'use strict' | ||
fieldSize[path] = 100 | ||
fieldSize[mode] = 8 | ||
fieldSize[uid] = 8 | ||
fieldSize[gid] = 8 | ||
fieldSize[size] = 12 | ||
fieldSize[mtime] = 12 | ||
fieldSize[cksum] = 8 | ||
fieldSize[type] = 1 | ||
fieldSize[linkpath] = 100 | ||
// high-level commands | ||
exports.c = exports.create = require('./lib/create.js') | ||
exports.r = exports.replace = require('./lib/replace.js') | ||
exports.t = exports.list = require('./lib/list.js') | ||
exports.u = exports.update = require('./lib/update.js') | ||
exports.x = exports.extract = require('./lib/extract.js') | ||
// "ustar\0" may introduce another bunch of headers. | ||
// these are optional, and will be nulled out if not present. | ||
var ustar = fields.ustar = f++ | ||
, ustarver = fields.ustarver = f++ | ||
, uname = fields.uname = f++ | ||
, gname = fields.gname = f++ | ||
, devmaj = fields.devmaj = f++ | ||
, devmin = fields.devmin = f++ | ||
, prefix = fields.prefix = f++ | ||
, fill = fields.fill = f++ | ||
// terminate fields. | ||
fields[f] = null | ||
fieldSize[ustar] = 6 | ||
fieldSize[ustarver] = 2 | ||
fieldSize[uname] = 32 | ||
fieldSize[gname] = 32 | ||
fieldSize[devmaj] = 8 | ||
fieldSize[devmin] = 8 | ||
fieldSize[prefix] = 155 | ||
fieldSize[fill] = 12 | ||
// nb: prefix field may in fact be 130 bytes of prefix, | ||
// a null char, 12 bytes for atime, 12 bytes for ctime. | ||
// | ||
// To recognize this format: | ||
// 1. prefix[130] === ' ' or '\0' | ||
// 2. atime and ctime are octal numeric values | ||
// 3. atime and ctime have ' ' in their last byte | ||
var fieldEnds = {} | ||
, fieldOffs = {} | ||
, fe = 0 | ||
for (var i = 0; i < f; i ++) { | ||
fieldOffs[i] = fe | ||
fieldEnds[i] = (fe += fieldSize[i]) | ||
} | ||
// build a translation table of field paths. | ||
Object.keys(fields).forEach(function (f) { | ||
if (fields[f] !== null) fields[fields[f]] = f | ||
}) | ||
// different values of the 'type' field | ||
// paths match the values of Stats.isX() functions, where appropriate | ||
var types = | ||
{ 0: "File" | ||
, "\0": "OldFile" // like 0 | ||
, "": "OldFile" | ||
, 1: "Link" | ||
, 2: "SymbolicLink" | ||
, 3: "CharacterDevice" | ||
, 4: "BlockDevice" | ||
, 5: "Directory" | ||
, 6: "FIFO" | ||
, 7: "ContiguousFile" // like 0 | ||
// posix headers | ||
, g: "GlobalExtendedHeader" // k=v for the rest of the archive | ||
, x: "ExtendedHeader" // k=v for the next file | ||
// vendor-specific stuff | ||
, A: "SolarisACL" // skip | ||
, D: "GNUDumpDir" // like 5, but with data, which should be skipped | ||
, I: "Inode" // metadata only, skip | ||
, K: "NextFileHasLongLinkpath" // data = link path of next file | ||
, L: "NextFileHasLongPath" // data = path of next file | ||
, M: "ContinuationFile" // skip | ||
, N: "OldGnuLongPath" // like L | ||
, S: "SparseFile" // skip | ||
, V: "TapeVolumeHeader" // skip | ||
, X: "OldExtendedHeader" // like x | ||
} | ||
Object.keys(types).forEach(function (t) { | ||
types[types[t]] = types[types[t]] || t | ||
}) | ||
// values for the mode field | ||
var modes = | ||
{ suid: 04000 // set uid on extraction | ||
, sgid: 02000 // set gid on extraction | ||
, svtx: 01000 // set restricted deletion flag on dirs on extraction | ||
, uread: 0400 | ||
, uwrite: 0200 | ||
, uexec: 0100 | ||
, gread: 040 | ||
, gwrite: 020 | ||
, gexec: 010 | ||
, oread: 4 | ||
, owrite: 2 | ||
, oexec: 1 | ||
, all: 07777 | ||
} | ||
var numeric = | ||
{ mode: true | ||
, uid: true | ||
, gid: true | ||
, size: true | ||
, mtime: true | ||
, devmaj: true | ||
, devmin: true | ||
, cksum: true | ||
, atime: true | ||
, ctime: true | ||
, dev: true | ||
, ino: true | ||
, nlink: true | ||
} | ||
Object.keys(modes).forEach(function (t) { | ||
modes[modes[t]] = modes[modes[t]] || t | ||
}) | ||
var knownExtended = | ||
{ atime: true | ||
, charset: true | ||
, comment: true | ||
, ctime: true | ||
, gid: true | ||
, gname: true | ||
, linkpath: true | ||
, mtime: true | ||
, path: true | ||
, realtime: true | ||
, security: true | ||
, size: true | ||
, uid: true | ||
, uname: true } | ||
exports.fields = fields | ||
exports.fieldSize = fieldSize | ||
exports.fieldOffs = fieldOffs | ||
exports.fieldEnds = fieldEnds | ||
exports.types = types | ||
exports.modes = modes | ||
exports.numeric = numeric | ||
exports.headerSize = headerSize | ||
exports.blockSize = blockSize | ||
exports.knownExtended = knownExtended | ||
exports.Pack = require("./lib/pack.js") | ||
exports.Parse = require("./lib/parse.js") | ||
exports.Extract = require("./lib/extract.js") | ||
// classes | ||
exports.Pack = require('./lib/pack.js') | ||
exports.Unpack = require('./lib/unpack.js') | ||
exports.Parse = require('./lib/parse.js') | ||
exports.ReadEntry = require('./lib/read-entry.js') | ||
exports.WriteEntry = require('./lib/write-entry.js') | ||
exports.Header = require('./lib/header.js') | ||
exports.Pax = require('./lib/pax.js') | ||
exports.types = require('./lib/types.js') |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 3 instances in 1 package
0
802
9
1
107234
4
7
20
2422
+ Addedminipass@^2.0.1
+ Addedminizlib@^1.0.3
+ Addedmkdirp@^0.5.0
+ Addedyallist@^3.0.2
+ Addedminipass@2.9.0(transitive)
+ Addedminizlib@1.3.3(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedyallist@3.1.1(transitive)
- Removedblock-stream@*
- Removedfstream@^1.0.2
- Removedinherits@2
- Removedbalanced-match@1.0.2(transitive)
- Removedblock-stream@0.0.9(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedfstream@1.0.12(transitive)
- Removedglob@7.2.3(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinherits@2.0.4(transitive)
- Removedminimatch@3.1.2(transitive)
- Removedonce@1.4.0(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedrimraf@2.7.1(transitive)
- Removedwrappy@1.0.2(transitive)