blob: 19a4255a305ac8798f20259177113d74ccb5ae38 [file] [log] [blame]
var util = require('util')
var bl = require('bl')
var xtend = require('xtend')
var headers = require('./headers')
var Writable = require('readable-stream').Writable
var PassThrough = require('readable-stream').PassThrough
var noop = function () {}
var overflow = function (size) {
size &= 511
return size && 512 - size
}
var emptyStream = function (self, offset) {
var s = new Source(self, offset)
s.end()
return s
}
var mixinPax = function (header, pax) {
if (pax.path) header.name = pax.path
if (pax.linkpath) header.linkname = pax.linkpath
if (pax.size) header.size = parseInt(pax.size, 10)
header.pax = pax
return header
}
var Source = function (self, offset) {
this._parent = self
this.offset = offset
PassThrough.call(this)
}
util.inherits(Source, PassThrough)
Source.prototype.destroy = function (err) {
this._parent.destroy(err)
}
var Extract = function (opts) {
if (!(this instanceof Extract)) return new Extract(opts)
Writable.call(this, opts)
opts = opts || {}
this._offset = 0
this._buffer = bl()
this._missing = 0
this._partial = false
this._onparse = noop
this._header = null
this._stream = null
this._overflow = null
this._cb = null
this._locked = false
this._destroyed = false
this._pax = null
this._paxGlobal = null
this._gnuLongPath = null
this._gnuLongLinkPath = null
var self = this
var b = self._buffer
var oncontinue = function () {
self._continue()
}
var onunlock = function (err) {
self._locked = false
if (err) return self.destroy(err)
if (!self._stream) oncontinue()
}
var onstreamend = function () {
self._stream = null
var drain = overflow(self._header.size)
if (drain) self._parse(drain, ondrain)
else self._parse(512, onheader)
if (!self._locked) oncontinue()
}
var ondrain = function () {
self._buffer.consume(overflow(self._header.size))
self._parse(512, onheader)
oncontinue()
}
var onpaxglobalheader = function () {
var size = self._header.size
self._paxGlobal = headers.decodePax(b.slice(0, size))
b.consume(size)
onstreamend()
}
var onpaxheader = function () {
var size = self._header.size
self._pax = headers.decodePax(b.slice(0, size))
if (self._paxGlobal) self._pax = xtend(self._paxGlobal, self._pax)
b.consume(size)
onstreamend()
}
var ongnulongpath = function () {
var size = self._header.size
this._gnuLongPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding)
b.consume(size)
onstreamend()
}
var ongnulonglinkpath = function () {
var size = self._header.size
this._gnuLongLinkPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding)
b.consume(size)
onstreamend()
}
var onheader = function () {
var offset = self._offset
var header
try {
header = self._header = headers.decode(b.slice(0, 512), opts.filenameEncoding)
} catch (err) {
self.emit('error', err)
}
b.consume(512)
if (!header) {
self._parse(512, onheader)
oncontinue()
return
}
if (header.type === 'gnu-long-path') {
self._parse(header.size, ongnulongpath)
oncontinue()
return
}
if (header.type === 'gnu-long-link-path') {
self._parse(header.size, ongnulonglinkpath)
oncontinue()
return
}
if (header.type === 'pax-global-header') {
self._parse(header.size, onpaxglobalheader)
oncontinue()
return
}
if (header.type === 'pax-header') {
self._parse(header.size, onpaxheader)
oncontinue()
return
}
if (self._gnuLongPath) {
header.name = self._gnuLongPath
self._gnuLongPath = null
}
if (self._gnuLongLinkPath) {
header.linkname = self._gnuLongLinkPath
self._gnuLongLinkPath = null
}
if (self._pax) {
self._header = header = mixinPax(header, self._pax)
self._pax = null
}
self._locked = true
if (!header.size || header.type === 'directory') {
self._parse(512, onheader)
self.emit('entry', header, emptyStream(self, offset), onunlock)
return
}
self._stream = new Source(self, offset)
self.emit('entry', header, self._stream, onunlock)
self._parse(header.size, onstreamend)
oncontinue()
}
this._onheader = onheader
this._parse(512, onheader)
}
util.inherits(Extract, Writable)
Extract.prototype.destroy = function (err) {
if (this._destroyed) return
this._destroyed = true
if (err) this.emit('error', err)
this.emit('close')
if (this._stream) this._stream.emit('close')
}
Extract.prototype._parse = function (size, onparse) {
if (this._destroyed) return
this._offset += size
this._missing = size
if (onparse === this._onheader) this._partial = false
this._onparse = onparse
}
Extract.prototype._continue = function () {
if (this._destroyed) return
var cb = this._cb
this._cb = noop
if (this._overflow) this._write(this._overflow, undefined, cb)
else cb()
}
Extract.prototype._write = function (data, enc, cb) {
if (this._destroyed) return
var s = this._stream
var b = this._buffer
var missing = this._missing
if (data.length) this._partial = true
// we do not reach end-of-chunk now. just forward it
if (data.length < missing) {
this._missing -= data.length
this._overflow = null
if (s) return s.write(data, cb)
b.append(data)
return cb()
}
// end-of-chunk. the parser should call cb.
this._cb = cb
this._missing = 0
var overflow = null
if (data.length > missing) {
overflow = data.slice(missing)
data = data.slice(0, missing)
}
if (s) s.end(data)
else b.append(data)
this._overflow = overflow
this._onparse()
}
Extract.prototype._final = function (cb) {
if (this._partial) return this.destroy(new Error('Unexpected end of data'))
cb()
}
module.exports = Extract