• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2const { Minipass } = require('minipass')
3const Pax = require('./pax.js')
4const Header = require('./header.js')
5const fs = require('fs')
6const path = require('path')
7const normPath = require('./normalize-windows-path.js')
8const stripSlash = require('./strip-trailing-slashes.js')
9
10const prefixPath = (path, prefix) => {
11  if (!prefix) {
12    return normPath(path)
13  }
14  path = normPath(path).replace(/^\.(\/|$)/, '')
15  return stripSlash(prefix) + '/' + path
16}
17
18const maxReadSize = 16 * 1024 * 1024
19const PROCESS = Symbol('process')
20const FILE = Symbol('file')
21const DIRECTORY = Symbol('directory')
22const SYMLINK = Symbol('symlink')
23const HARDLINK = Symbol('hardlink')
24const HEADER = Symbol('header')
25const READ = Symbol('read')
26const LSTAT = Symbol('lstat')
27const ONLSTAT = Symbol('onlstat')
28const ONREAD = Symbol('onread')
29const ONREADLINK = Symbol('onreadlink')
30const OPENFILE = Symbol('openfile')
31const ONOPENFILE = Symbol('onopenfile')
32const CLOSE = Symbol('close')
33const MODE = Symbol('mode')
34const AWAITDRAIN = Symbol('awaitDrain')
35const ONDRAIN = Symbol('ondrain')
36const PREFIX = Symbol('prefix')
37const HAD_ERROR = Symbol('hadError')
38const warner = require('./warn-mixin.js')
39const winchars = require('./winchars.js')
40const stripAbsolutePath = require('./strip-absolute-path.js')
41
42const modeFix = require('./mode-fix.js')
43
44const WriteEntry = warner(class WriteEntry extends Minipass {
45  constructor (p, opt) {
46    opt = opt || {}
47    super(opt)
48    if (typeof p !== 'string') {
49      throw new TypeError('path is required')
50    }
51    this.path = normPath(p)
52    // suppress atime, ctime, uid, gid, uname, gname
53    this.portable = !!opt.portable
54    // until node has builtin pwnam functions, this'll have to do
55    this.myuid = process.getuid && process.getuid() || 0
56    this.myuser = process.env.USER || ''
57    this.maxReadSize = opt.maxReadSize || maxReadSize
58    this.linkCache = opt.linkCache || new Map()
59    this.statCache = opt.statCache || new Map()
60    this.preservePaths = !!opt.preservePaths
61    this.cwd = normPath(opt.cwd || process.cwd())
62    this.strict = !!opt.strict
63    this.noPax = !!opt.noPax
64    this.noMtime = !!opt.noMtime
65    this.mtime = opt.mtime || null
66    this.prefix = opt.prefix ? normPath(opt.prefix) : null
67
68    this.fd = null
69    this.blockLen = null
70    this.blockRemain = null
71    this.buf = null
72    this.offset = null
73    this.length = null
74    this.pos = null
75    this.remain = null
76
77    if (typeof opt.onwarn === 'function') {
78      this.on('warn', opt.onwarn)
79    }
80
81    let pathWarn = false
82    if (!this.preservePaths) {
83      const [root, stripped] = stripAbsolutePath(this.path)
84      if (root) {
85        this.path = stripped
86        pathWarn = root
87      }
88    }
89
90    this.win32 = !!opt.win32 || process.platform === 'win32'
91    if (this.win32) {
92      // force the \ to / normalization, since we might not *actually*
93      // be on windows, but want \ to be considered a path separator.
94      this.path = winchars.decode(this.path.replace(/\\/g, '/'))
95      p = p.replace(/\\/g, '/')
96    }
97
98    this.absolute = normPath(opt.absolute || path.resolve(this.cwd, p))
99
100    if (this.path === '') {
101      this.path = './'
102    }
103
104    if (pathWarn) {
105      this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
106        entry: this,
107        path: pathWarn + this.path,
108      })
109    }
110
111    if (this.statCache.has(this.absolute)) {
112      this[ONLSTAT](this.statCache.get(this.absolute))
113    } else {
114      this[LSTAT]()
115    }
116  }
117
118  emit (ev, ...data) {
119    if (ev === 'error') {
120      this[HAD_ERROR] = true
121    }
122    return super.emit(ev, ...data)
123  }
124
125  [LSTAT] () {
126    fs.lstat(this.absolute, (er, stat) => {
127      if (er) {
128        return this.emit('error', er)
129      }
130      this[ONLSTAT](stat)
131    })
132  }
133
134  [ONLSTAT] (stat) {
135    this.statCache.set(this.absolute, stat)
136    this.stat = stat
137    if (!stat.isFile()) {
138      stat.size = 0
139    }
140    this.type = getType(stat)
141    this.emit('stat', stat)
142    this[PROCESS]()
143  }
144
145  [PROCESS] () {
146    switch (this.type) {
147      case 'File': return this[FILE]()
148      case 'Directory': return this[DIRECTORY]()
149      case 'SymbolicLink': return this[SYMLINK]()
150      // unsupported types are ignored.
151      default: return this.end()
152    }
153  }
154
155  [MODE] (mode) {
156    return modeFix(mode, this.type === 'Directory', this.portable)
157  }
158
159  [PREFIX] (path) {
160    return prefixPath(path, this.prefix)
161  }
162
163  [HEADER] () {
164    if (this.type === 'Directory' && this.portable) {
165      this.noMtime = true
166    }
167
168    this.header = new Header({
169      path: this[PREFIX](this.path),
170      // only apply the prefix to hard links.
171      linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
172      : this.linkpath,
173      // only the permissions and setuid/setgid/sticky bitflags
174      // not the higher-order bits that specify file type
175      mode: this[MODE](this.stat.mode),
176      uid: this.portable ? null : this.stat.uid,
177      gid: this.portable ? null : this.stat.gid,
178      size: this.stat.size,
179      mtime: this.noMtime ? null : this.mtime || this.stat.mtime,
180      type: this.type,
181      uname: this.portable ? null :
182      this.stat.uid === this.myuid ? this.myuser : '',
183      atime: this.portable ? null : this.stat.atime,
184      ctime: this.portable ? null : this.stat.ctime,
185    })
186
187    if (this.header.encode() && !this.noPax) {
188      super.write(new Pax({
189        atime: this.portable ? null : this.header.atime,
190        ctime: this.portable ? null : this.header.ctime,
191        gid: this.portable ? null : this.header.gid,
192        mtime: this.noMtime ? null : this.mtime || this.header.mtime,
193        path: this[PREFIX](this.path),
194        linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
195        : this.linkpath,
196        size: this.header.size,
197        uid: this.portable ? null : this.header.uid,
198        uname: this.portable ? null : this.header.uname,
199        dev: this.portable ? null : this.stat.dev,
200        ino: this.portable ? null : this.stat.ino,
201        nlink: this.portable ? null : this.stat.nlink,
202      }).encode())
203    }
204    super.write(this.header.block)
205  }
206
207  [DIRECTORY] () {
208    if (this.path.slice(-1) !== '/') {
209      this.path += '/'
210    }
211    this.stat.size = 0
212    this[HEADER]()
213    this.end()
214  }
215
216  [SYMLINK] () {
217    fs.readlink(this.absolute, (er, linkpath) => {
218      if (er) {
219        return this.emit('error', er)
220      }
221      this[ONREADLINK](linkpath)
222    })
223  }
224
225  [ONREADLINK] (linkpath) {
226    this.linkpath = normPath(linkpath)
227    this[HEADER]()
228    this.end()
229  }
230
231  [HARDLINK] (linkpath) {
232    this.type = 'Link'
233    this.linkpath = normPath(path.relative(this.cwd, linkpath))
234    this.stat.size = 0
235    this[HEADER]()
236    this.end()
237  }
238
239  [FILE] () {
240    if (this.stat.nlink > 1) {
241      const linkKey = this.stat.dev + ':' + this.stat.ino
242      if (this.linkCache.has(linkKey)) {
243        const linkpath = this.linkCache.get(linkKey)
244        if (linkpath.indexOf(this.cwd) === 0) {
245          return this[HARDLINK](linkpath)
246        }
247      }
248      this.linkCache.set(linkKey, this.absolute)
249    }
250
251    this[HEADER]()
252    if (this.stat.size === 0) {
253      return this.end()
254    }
255
256    this[OPENFILE]()
257  }
258
259  [OPENFILE] () {
260    fs.open(this.absolute, 'r', (er, fd) => {
261      if (er) {
262        return this.emit('error', er)
263      }
264      this[ONOPENFILE](fd)
265    })
266  }
267
268  [ONOPENFILE] (fd) {
269    this.fd = fd
270    if (this[HAD_ERROR]) {
271      return this[CLOSE]()
272    }
273
274    this.blockLen = 512 * Math.ceil(this.stat.size / 512)
275    this.blockRemain = this.blockLen
276    const bufLen = Math.min(this.blockLen, this.maxReadSize)
277    this.buf = Buffer.allocUnsafe(bufLen)
278    this.offset = 0
279    this.pos = 0
280    this.remain = this.stat.size
281    this.length = this.buf.length
282    this[READ]()
283  }
284
285  [READ] () {
286    const { fd, buf, offset, length, pos } = this
287    fs.read(fd, buf, offset, length, pos, (er, bytesRead) => {
288      if (er) {
289        // ignoring the error from close(2) is a bad practice, but at
290        // this point we already have an error, don't need another one
291        return this[CLOSE](() => this.emit('error', er))
292      }
293      this[ONREAD](bytesRead)
294    })
295  }
296
297  [CLOSE] (cb) {
298    fs.close(this.fd, cb)
299  }
300
301  [ONREAD] (bytesRead) {
302    if (bytesRead <= 0 && this.remain > 0) {
303      const er = new Error('encountered unexpected EOF')
304      er.path = this.absolute
305      er.syscall = 'read'
306      er.code = 'EOF'
307      return this[CLOSE](() => this.emit('error', er))
308    }
309
310    if (bytesRead > this.remain) {
311      const er = new Error('did not encounter expected EOF')
312      er.path = this.absolute
313      er.syscall = 'read'
314      er.code = 'EOF'
315      return this[CLOSE](() => this.emit('error', er))
316    }
317
318    // null out the rest of the buffer, if we could fit the block padding
319    // at the end of this loop, we've incremented bytesRead and this.remain
320    // to be incremented up to the blockRemain level, as if we had expected
321    // to get a null-padded file, and read it until the end.  then we will
322    // decrement both remain and blockRemain by bytesRead, and know that we
323    // reached the expected EOF, without any null buffer to append.
324    if (bytesRead === this.remain) {
325      for (let i = bytesRead; i < this.length && bytesRead < this.blockRemain; i++) {
326        this.buf[i + this.offset] = 0
327        bytesRead++
328        this.remain++
329      }
330    }
331
332    const writeBuf = this.offset === 0 && bytesRead === this.buf.length ?
333      this.buf : this.buf.slice(this.offset, this.offset + bytesRead)
334
335    const flushed = this.write(writeBuf)
336    if (!flushed) {
337      this[AWAITDRAIN](() => this[ONDRAIN]())
338    } else {
339      this[ONDRAIN]()
340    }
341  }
342
343  [AWAITDRAIN] (cb) {
344    this.once('drain', cb)
345  }
346
347  write (writeBuf) {
348    if (this.blockRemain < writeBuf.length) {
349      const er = new Error('writing more data than expected')
350      er.path = this.absolute
351      return this.emit('error', er)
352    }
353    this.remain -= writeBuf.length
354    this.blockRemain -= writeBuf.length
355    this.pos += writeBuf.length
356    this.offset += writeBuf.length
357    return super.write(writeBuf)
358  }
359
360  [ONDRAIN] () {
361    if (!this.remain) {
362      if (this.blockRemain) {
363        super.write(Buffer.alloc(this.blockRemain))
364      }
365      return this[CLOSE](er => er ? this.emit('error', er) : this.end())
366    }
367
368    if (this.offset >= this.length) {
369      // if we only have a smaller bit left to read, alloc a smaller buffer
370      // otherwise, keep it the same length it was before.
371      this.buf = Buffer.allocUnsafe(Math.min(this.blockRemain, this.buf.length))
372      this.offset = 0
373    }
374    this.length = this.buf.length - this.offset
375    this[READ]()
376  }
377})
378
379class WriteEntrySync extends WriteEntry {
380  [LSTAT] () {
381    this[ONLSTAT](fs.lstatSync(this.absolute))
382  }
383
384  [SYMLINK] () {
385    this[ONREADLINK](fs.readlinkSync(this.absolute))
386  }
387
388  [OPENFILE] () {
389    this[ONOPENFILE](fs.openSync(this.absolute, 'r'))
390  }
391
392  [READ] () {
393    let threw = true
394    try {
395      const { fd, buf, offset, length, pos } = this
396      const bytesRead = fs.readSync(fd, buf, offset, length, pos)
397      this[ONREAD](bytesRead)
398      threw = false
399    } finally {
400      // ignoring the error from close(2) is a bad practice, but at
401      // this point we already have an error, don't need another one
402      if (threw) {
403        try {
404          this[CLOSE](() => {})
405        } catch (er) {}
406      }
407    }
408  }
409
410  [AWAITDRAIN] (cb) {
411    cb()
412  }
413
414  [CLOSE] (cb) {
415    fs.closeSync(this.fd)
416    cb()
417  }
418}
419
420const WriteEntryTar = warner(class WriteEntryTar extends Minipass {
421  constructor (readEntry, opt) {
422    opt = opt || {}
423    super(opt)
424    this.preservePaths = !!opt.preservePaths
425    this.portable = !!opt.portable
426    this.strict = !!opt.strict
427    this.noPax = !!opt.noPax
428    this.noMtime = !!opt.noMtime
429
430    this.readEntry = readEntry
431    this.type = readEntry.type
432    if (this.type === 'Directory' && this.portable) {
433      this.noMtime = true
434    }
435
436    this.prefix = opt.prefix || null
437
438    this.path = normPath(readEntry.path)
439    this.mode = this[MODE](readEntry.mode)
440    this.uid = this.portable ? null : readEntry.uid
441    this.gid = this.portable ? null : readEntry.gid
442    this.uname = this.portable ? null : readEntry.uname
443    this.gname = this.portable ? null : readEntry.gname
444    this.size = readEntry.size
445    this.mtime = this.noMtime ? null : opt.mtime || readEntry.mtime
446    this.atime = this.portable ? null : readEntry.atime
447    this.ctime = this.portable ? null : readEntry.ctime
448    this.linkpath = normPath(readEntry.linkpath)
449
450    if (typeof opt.onwarn === 'function') {
451      this.on('warn', opt.onwarn)
452    }
453
454    let pathWarn = false
455    if (!this.preservePaths) {
456      const [root, stripped] = stripAbsolutePath(this.path)
457      if (root) {
458        this.path = stripped
459        pathWarn = root
460      }
461    }
462
463    this.remain = readEntry.size
464    this.blockRemain = readEntry.startBlockSize
465
466    this.header = new Header({
467      path: this[PREFIX](this.path),
468      linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
469      : this.linkpath,
470      // only the permissions and setuid/setgid/sticky bitflags
471      // not the higher-order bits that specify file type
472      mode: this.mode,
473      uid: this.portable ? null : this.uid,
474      gid: this.portable ? null : this.gid,
475      size: this.size,
476      mtime: this.noMtime ? null : this.mtime,
477      type: this.type,
478      uname: this.portable ? null : this.uname,
479      atime: this.portable ? null : this.atime,
480      ctime: this.portable ? null : this.ctime,
481    })
482
483    if (pathWarn) {
484      this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
485        entry: this,
486        path: pathWarn + this.path,
487      })
488    }
489
490    if (this.header.encode() && !this.noPax) {
491      super.write(new Pax({
492        atime: this.portable ? null : this.atime,
493        ctime: this.portable ? null : this.ctime,
494        gid: this.portable ? null : this.gid,
495        mtime: this.noMtime ? null : this.mtime,
496        path: this[PREFIX](this.path),
497        linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
498        : this.linkpath,
499        size: this.size,
500        uid: this.portable ? null : this.uid,
501        uname: this.portable ? null : this.uname,
502        dev: this.portable ? null : this.readEntry.dev,
503        ino: this.portable ? null : this.readEntry.ino,
504        nlink: this.portable ? null : this.readEntry.nlink,
505      }).encode())
506    }
507
508    super.write(this.header.block)
509    readEntry.pipe(this)
510  }
511
512  [PREFIX] (path) {
513    return prefixPath(path, this.prefix)
514  }
515
516  [MODE] (mode) {
517    return modeFix(mode, this.type === 'Directory', this.portable)
518  }
519
520  write (data) {
521    const writeLen = data.length
522    if (writeLen > this.blockRemain) {
523      throw new Error('writing more to entry than is appropriate')
524    }
525    this.blockRemain -= writeLen
526    return super.write(data)
527  }
528
529  end () {
530    if (this.blockRemain) {
531      super.write(Buffer.alloc(this.blockRemain))
532    }
533    return super.end()
534  }
535})
536
537WriteEntry.Sync = WriteEntrySync
538WriteEntry.Tar = WriteEntryTar
539
540const getType = stat =>
541  stat.isFile() ? 'File'
542  : stat.isDirectory() ? 'Directory'
543  : stat.isSymbolicLink() ? 'SymbolicLink'
544  : 'Unsupported'
545
546module.exports = WriteEntry
547