pack.js
  1  var constants = require('fs-constants')
  2  var eos = require('end-of-stream')
  3  var inherits = require('inherits')
  4  var alloc = Buffer.alloc
  5  
  6  var Readable = require('readable-stream').Readable
  7  var Writable = require('readable-stream').Writable
  8  var StringDecoder = require('string_decoder').StringDecoder
  9  
 10  var headers = require('./headers')
 11  
 12  var DMODE = parseInt('755', 8)
 13  var FMODE = parseInt('644', 8)
 14  
 15  var END_OF_TAR = alloc(1024)
 16  
 17  var noop = function () {}
 18  
 19  var overflow = function (self, size) {
 20    size &= 511
 21    if (size) self.push(END_OF_TAR.slice(0, 512 - size))
 22  }
 23  
 24  function modeToType (mode) {
 25    switch (mode & constants.S_IFMT) {
 26      case constants.S_IFBLK: return 'block-device'
 27      case constants.S_IFCHR: return 'character-device'
 28      case constants.S_IFDIR: return 'directory'
 29      case constants.S_IFIFO: return 'fifo'
 30      case constants.S_IFLNK: return 'symlink'
 31    }
 32  
 33    return 'file'
 34  }
 35  
 36  var Sink = function (to) {
 37    Writable.call(this)
 38    this.written = 0
 39    this._to = to
 40    this._destroyed = false
 41  }
 42  
 43  inherits(Sink, Writable)
 44  
 45  Sink.prototype._write = function (data, enc, cb) {
 46    this.written += data.length
 47    if (this._to.push(data)) return cb()
 48    this._to._drain = cb
 49  }
 50  
 51  Sink.prototype.destroy = function () {
 52    if (this._destroyed) return
 53    this._destroyed = true
 54    this.emit('close')
 55  }
 56  
 57  var LinkSink = function () {
 58    Writable.call(this)
 59    this.linkname = ''
 60    this._decoder = new StringDecoder('utf-8')
 61    this._destroyed = false
 62  }
 63  
 64  inherits(LinkSink, Writable)
 65  
 66  LinkSink.prototype._write = function (data, enc, cb) {
 67    this.linkname += this._decoder.write(data)
 68    cb()
 69  }
 70  
 71  LinkSink.prototype.destroy = function () {
 72    if (this._destroyed) return
 73    this._destroyed = true
 74    this.emit('close')
 75  }
 76  
 77  var Void = function () {
 78    Writable.call(this)
 79    this._destroyed = false
 80  }
 81  
 82  inherits(Void, Writable)
 83  
 84  Void.prototype._write = function (data, enc, cb) {
 85    cb(new Error('No body allowed for this entry'))
 86  }
 87  
 88  Void.prototype.destroy = function () {
 89    if (this._destroyed) return
 90    this._destroyed = true
 91    this.emit('close')
 92  }
 93  
 94  var Pack = function (opts) {
 95    if (!(this instanceof Pack)) return new Pack(opts)
 96    Readable.call(this, opts)
 97  
 98    this._drain = noop
 99    this._finalized = false
100    this._finalizing = false
101    this._destroyed = false
102    this._stream = null
103  }
104  
105  inherits(Pack, Readable)
106  
107  Pack.prototype.entry = function (header, buffer, callback) {
108    if (this._stream) throw new Error('already piping an entry')
109    if (this._finalized || this._destroyed) return
110  
111    if (typeof buffer === 'function') {
112      callback = buffer
113      buffer = null
114    }
115  
116    if (!callback) callback = noop
117  
118    var self = this
119  
120    if (!header.size || header.type === 'symlink') header.size = 0
121    if (!header.type) header.type = modeToType(header.mode)
122    if (!header.mode) header.mode = header.type === 'directory' ? DMODE : FMODE
123    if (!header.uid) header.uid = 0
124    if (!header.gid) header.gid = 0
125    if (!header.mtime) header.mtime = new Date()
126  
127    if (typeof buffer === 'string') buffer = Buffer.from(buffer)
128    if (Buffer.isBuffer(buffer)) {
129      header.size = buffer.length
130      this._encode(header)
131      var ok = this.push(buffer)
132      overflow(self, header.size)
133      if (ok) process.nextTick(callback)
134      else this._drain = callback
135      return new Void()
136    }
137  
138    if (header.type === 'symlink' && !header.linkname) {
139      var linkSink = new LinkSink()
140      eos(linkSink, function (err) {
141        if (err) { // stream was closed
142          self.destroy()
143          return callback(err)
144        }
145  
146        header.linkname = linkSink.linkname
147        self._encode(header)
148        callback()
149      })
150  
151      return linkSink
152    }
153  
154    this._encode(header)
155  
156    if (header.type !== 'file' && header.type !== 'contiguous-file') {
157      process.nextTick(callback)
158      return new Void()
159    }
160  
161    var sink = new Sink(this)
162  
163    this._stream = sink
164  
165    eos(sink, function (err) {
166      self._stream = null
167  
168      if (err) { // stream was closed
169        self.destroy()
170        return callback(err)
171      }
172  
173      if (sink.written !== header.size) { // corrupting tar
174        self.destroy()
175        return callback(new Error('size mismatch'))
176      }
177  
178      overflow(self, header.size)
179      if (self._finalizing) self.finalize()
180      callback()
181    })
182  
183    return sink
184  }
185  
186  Pack.prototype.finalize = function () {
187    if (this._stream) {
188      this._finalizing = true
189      return
190    }
191  
192    if (this._finalized) return
193    this._finalized = true
194    this.push(END_OF_TAR)
195    this.push(null)
196  }
197  
198  Pack.prototype.destroy = function (err) {
199    if (this._destroyed) return
200    this._destroyed = true
201  
202    if (err) this.emit('error', err)
203    this.emit('close')
204    if (this._stream && this._stream.destroy) this._stream.destroy()
205  }
206  
207  Pack.prototype._encode = function (header) {
208    if (!header.pax) {
209      var buf = headers.encode(header)
210      if (buf) {
211        this.push(buf)
212        return
213      }
214    }
215    this._encodePax(header)
216  }
217  
218  Pack.prototype._encodePax = function (header) {
219    var paxHeader = headers.encodePax({
220      name: header.name,
221      linkname: header.linkname,
222      pax: header.pax
223    })
224  
225    var newHeader = {
226      name: 'PaxHeader',
227      mode: header.mode,
228      uid: header.uid,
229      gid: header.gid,
230      size: paxHeader.length,
231      mtime: header.mtime,
232      type: 'pax-header',
233      linkname: header.linkname && 'PaxHeader',
234      uname: header.uname,
235      gname: header.gname,
236      devmajor: header.devmajor,
237      devminor: header.devminor
238    }
239  
240    this.push(headers.encode(newHeader))
241    this.push(paxHeader)
242    overflow(this, paxHeader.length)
243  
244    newHeader.size = header.size
245    newHeader.type = header.type
246    this.push(headers.encode(newHeader))
247  }
248  
249  Pack.prototype._read = function (n) {
250    var drain = this._drain
251    this._drain = noop
252    drain()
253  }
254  
255  module.exports = Pack