pack.js•6.29 kB
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)
}