blob: 72d96a0c778147c44fa31ebc1fbb80b3858d9e4e [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001var constants = require('fs-constants')
2var eos = require('end-of-stream')
3var util = require('util')
4var alloc = require('buffer-alloc')
5var toBuffer = require('to-buffer')
6
7var Readable = require('readable-stream').Readable
8var Writable = require('readable-stream').Writable
9var StringDecoder = require('string_decoder').StringDecoder
10
11var headers = require('./headers')
12
13var DMODE = parseInt('755', 8)
14var FMODE = parseInt('644', 8)
15
16var END_OF_TAR = alloc(1024)
17
18var noop = function () {}
19
20var overflow = function (self, size) {
21 size &= 511
22 if (size) self.push(END_OF_TAR.slice(0, 512 - size))
23}
24
25function modeToType (mode) {
26 switch (mode & constants.S_IFMT) {
27 case constants.S_IFBLK: return 'block-device'
28 case constants.S_IFCHR: return 'character-device'
29 case constants.S_IFDIR: return 'directory'
30 case constants.S_IFIFO: return 'fifo'
31 case constants.S_IFLNK: return 'symlink'
32 }
33
34 return 'file'
35}
36
37var Sink = function (to) {
38 Writable.call(this)
39 this.written = 0
40 this._to = to
41 this._destroyed = false
42}
43
44util.inherits(Sink, Writable)
45
46Sink.prototype._write = function (data, enc, cb) {
47 this.written += data.length
48 if (this._to.push(data)) return cb()
49 this._to._drain = cb
50}
51
52Sink.prototype.destroy = function () {
53 if (this._destroyed) return
54 this._destroyed = true
55 this.emit('close')
56}
57
58var LinkSink = function () {
59 Writable.call(this)
60 this.linkname = ''
61 this._decoder = new StringDecoder('utf-8')
62 this._destroyed = false
63}
64
65util.inherits(LinkSink, Writable)
66
67LinkSink.prototype._write = function (data, enc, cb) {
68 this.linkname += this._decoder.write(data)
69 cb()
70}
71
72LinkSink.prototype.destroy = function () {
73 if (this._destroyed) return
74 this._destroyed = true
75 this.emit('close')
76}
77
78var Void = function () {
79 Writable.call(this)
80 this._destroyed = false
81}
82
83util.inherits(Void, Writable)
84
85Void.prototype._write = function (data, enc, cb) {
86 cb(new Error('No body allowed for this entry'))
87}
88
89Void.prototype.destroy = function () {
90 if (this._destroyed) return
91 this._destroyed = true
92 this.emit('close')
93}
94
95var Pack = function (opts) {
96 if (!(this instanceof Pack)) return new Pack(opts)
97 Readable.call(this, opts)
98
99 this._drain = noop
100 this._finalized = false
101 this._finalizing = false
102 this._destroyed = false
103 this._stream = null
104}
105
106util.inherits(Pack, Readable)
107
108Pack.prototype.entry = function (header, buffer, callback) {
109 if (this._stream) throw new Error('already piping an entry')
110 if (this._finalized || this._destroyed) return
111
112 if (typeof buffer === 'function') {
113 callback = buffer
114 buffer = null
115 }
116
117 if (!callback) callback = noop
118
119 var self = this
120
121 if (!header.size || header.type === 'symlink') header.size = 0
122 if (!header.type) header.type = modeToType(header.mode)
123 if (!header.mode) header.mode = header.type === 'directory' ? DMODE : FMODE
124 if (!header.uid) header.uid = 0
125 if (!header.gid) header.gid = 0
126 if (!header.mtime) header.mtime = new Date()
127
128 if (typeof buffer === 'string') buffer = toBuffer(buffer)
129 if (Buffer.isBuffer(buffer)) {
130 header.size = buffer.length
131 this._encode(header)
132 this.push(buffer)
133 overflow(self, header.size)
134 process.nextTick(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
186Pack.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
198Pack.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
207Pack.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
218Pack.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
249Pack.prototype._read = function (n) {
250 var drain = this._drain
251 this._drain = noop
252 drain()
253}
254
255module.exports = Pack