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