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