| const { Readable, Writable, getStreamError } = require('streamx') |
| const b4a = require('b4a') |
| |
| const constants = require('./constants') |
| const headers = require('./headers') |
| |
| const DMODE = 0o755 |
| const FMODE = 0o644 |
| |
| const END_OF_TAR = b4a.alloc(1024) |
| |
| class Sink extends Writable { |
| constructor (pack, header, callback) { |
| super({ mapWritable, eagerOpen: true }) |
| |
| this.written = 0 |
| this.header = header |
| |
| this._callback = callback |
| this._linkname = null |
| this._isLinkname = header.type === 'symlink' && !header.linkname |
| this._isVoid = header.type !== 'file' && header.type !== 'contiguous-file' |
| this._finished = false |
| this._pack = pack |
| this._openCallback = null |
| |
| if (this._pack._stream === null) this._pack._stream = this |
| else this._pack._pending.push(this) |
| } |
| |
| _open (cb) { |
| this._openCallback = cb |
| if (this._pack._stream === this) this._continueOpen() |
| } |
| |
| _continuePack (err) { |
| if (this._callback === null) return |
| |
| const callback = this._callback |
| this._callback = null |
| |
| callback(err) |
| } |
| |
| _continueOpen () { |
| if (this._pack._stream === null) this._pack._stream = this |
| |
| const cb = this._openCallback |
| this._openCallback = null |
| if (cb === null) return |
| |
| if (this._pack.destroying) return cb(new Error('pack stream destroyed')) |
| if (this._pack._finalized) return cb(new Error('pack stream is already finalized')) |
| |
| this._pack._stream = this |
| |
| if (!this._isLinkname) { |
| this._pack._encode(this.header) |
| } |
| |
| if (this._isVoid) { |
| this._finish() |
| this._continuePack(null) |
| } |
| |
| cb(null) |
| } |
| |
| _write (data, cb) { |
| if (this._isLinkname) { |
| this._linkname = this._linkname ? b4a.concat([this._linkname, data]) : data |
| return cb(null) |
| } |
| |
| if (this._isVoid) { |
| if (data.byteLength > 0) { |
| return cb(new Error('No body allowed for this entry')) |
| } |
| return cb() |
| } |
| |
| this.written += data.byteLength |
| if (this._pack.push(data)) return cb() |
| this._pack._drain = cb |
| } |
| |
| _finish () { |
| if (this._finished) return |
| this._finished = true |
| |
| if (this._isLinkname) { |
| this.header.linkname = this._linkname ? b4a.toString(this._linkname, 'utf-8') : '' |
| this._pack._encode(this.header) |
| } |
| |
| overflow(this._pack, this.header.size) |
| |
| this._pack._done(this) |
| } |
| |
| _final (cb) { |
| if (this.written !== this.header.size) { // corrupting tar |
| return cb(new Error('Size mismatch')) |
| } |
| |
| this._finish() |
| cb(null) |
| } |
| |
| _getError () { |
| return getStreamError(this) || new Error('tar entry destroyed') |
| } |
| |
| _predestroy () { |
| this._pack.destroy(this._getError()) |
| } |
| |
| _destroy (cb) { |
| this._pack._done(this) |
| |
| this._continuePack(this._finished ? null : this._getError()) |
| |
| cb() |
| } |
| } |
| |
| class Pack extends Readable { |
| constructor (opts) { |
| super(opts) |
| this._drain = noop |
| this._finalized = false |
| this._finalizing = false |
| this._pending = [] |
| this._stream = null |
| } |
| |
| entry (header, buffer, callback) { |
| if (this._finalized || this.destroying) throw new Error('already finalized or destroyed') |
| |
| if (typeof buffer === 'function') { |
| callback = buffer |
| buffer = null |
| } |
| |
| if (!callback) callback = noop |
| |
| if (!header.size || header.type === 'symlink') header.size = 0 |
| if (!header.type) header.type = modeToType(header.mode) |
| if (!header.mode) header.mode = header.type === 'directory' ? DMODE : FMODE |
| if (!header.uid) header.uid = 0 |
| if (!header.gid) header.gid = 0 |
| if (!header.mtime) header.mtime = new Date() |
| |
| if (typeof buffer === 'string') buffer = b4a.from(buffer) |
| |
| const sink = new Sink(this, header, callback) |
| |
| if (b4a.isBuffer(buffer)) { |
| header.size = buffer.byteLength |
| sink.write(buffer) |
| sink.end() |
| return sink |
| } |
| |
| if (sink._isVoid) { |
| return sink |
| } |
| |
| return sink |
| } |
| |
| finalize () { |
| if (this._stream || this._pending.length > 0) { |
| this._finalizing = true |
| return |
| } |
| |
| if (this._finalized) return |
| this._finalized = true |
| |
| this.push(END_OF_TAR) |
| this.push(null) |
| } |
| |
| _done (stream) { |
| if (stream !== this._stream) return |
| |
| this._stream = null |
| |
| if (this._finalizing) this.finalize() |
| if (this._pending.length) this._pending.shift()._continueOpen() |
| } |
| |
| _encode (header) { |
| if (!header.pax) { |
| const buf = headers.encode(header) |
| if (buf) { |
| this.push(buf) |
| return |
| } |
| } |
| this._encodePax(header) |
| } |
| |
| _encodePax (header) { |
| const paxHeader = headers.encodePax({ |
| name: header.name, |
| linkname: header.linkname, |
| pax: header.pax |
| }) |
| |
| const newHeader = { |
| name: 'PaxHeader', |
| mode: header.mode, |
| uid: header.uid, |
| gid: header.gid, |
| size: paxHeader.byteLength, |
| mtime: header.mtime, |
| type: 'pax-header', |
| linkname: header.linkname && 'PaxHeader', |
| uname: header.uname, |
| gname: header.gname, |
| devmajor: header.devmajor, |
| devminor: header.devminor |
| } |
| |
| this.push(headers.encode(newHeader)) |
| this.push(paxHeader) |
| overflow(this, paxHeader.byteLength) |
| |
| newHeader.size = header.size |
| newHeader.type = header.type |
| this.push(headers.encode(newHeader)) |
| } |
| |
| _doDrain () { |
| const drain = this._drain |
| this._drain = noop |
| drain() |
| } |
| |
| _predestroy () { |
| const err = getStreamError(this) |
| |
| if (this._stream) this._stream.destroy(err) |
| |
| while (this._pending.length) { |
| const stream = this._pending.shift() |
| stream.destroy(err) |
| stream._continueOpen() |
| } |
| |
| this._doDrain() |
| } |
| |
| _read (cb) { |
| this._doDrain() |
| cb() |
| } |
| } |
| |
| module.exports = function pack (opts) { |
| return new Pack(opts) |
| } |
| |
| function modeToType (mode) { |
| switch (mode & constants.S_IFMT) { |
| case constants.S_IFBLK: return 'block-device' |
| case constants.S_IFCHR: return 'character-device' |
| case constants.S_IFDIR: return 'directory' |
| case constants.S_IFIFO: return 'fifo' |
| case constants.S_IFLNK: return 'symlink' |
| } |
| |
| return 'file' |
| } |
| |
| function noop () {} |
| |
| function overflow (self, size) { |
| size &= 511 |
| if (size) self.push(END_OF_TAR.subarray(0, 512 - size)) |
| } |
| |
| function mapWritable (buf) { |
| return b4a.isBuffer(buf) ? buf : b4a.from(buf) |
| } |