• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const assert = require('assert')
4const EE = require('events').EventEmitter
5const Parser = require('./parse.js')
6const fs = require('fs')
7const fsm = require('fs-minipass')
8const path = require('path')
9const mkdir = require('./mkdir.js')
10const mkdirSync = mkdir.sync
11const wc = require('./winchars.js')
12
13const ONENTRY = Symbol('onEntry')
14const CHECKFS = Symbol('checkFs')
15const ISREUSABLE = Symbol('isReusable')
16const MAKEFS = Symbol('makeFs')
17const FILE = Symbol('file')
18const DIRECTORY = Symbol('directory')
19const LINK = Symbol('link')
20const SYMLINK = Symbol('symlink')
21const HARDLINK = Symbol('hardlink')
22const UNSUPPORTED = Symbol('unsupported')
23const UNKNOWN = Symbol('unknown')
24const CHECKPATH = Symbol('checkPath')
25const MKDIR = Symbol('mkdir')
26const ONERROR = Symbol('onError')
27const PENDING = Symbol('pending')
28const PEND = Symbol('pend')
29const UNPEND = Symbol('unpend')
30const ENDED = Symbol('ended')
31const MAYBECLOSE = Symbol('maybeClose')
32const SKIP = Symbol('skip')
33const DOCHOWN = Symbol('doChown')
34const UID = Symbol('uid')
35const GID = Symbol('gid')
36const crypto = require('crypto')
37
38// Unlinks on Windows are not atomic.
39//
40// This means that if you have a file entry, followed by another
41// file entry with an identical name, and you cannot re-use the file
42// (because it's a hardlink, or because unlink:true is set, or it's
43// Windows, which does not have useful nlink values), then the unlink
44// will be committed to the disk AFTER the new file has been written
45// over the old one, deleting the new file.
46//
47// To work around this, on Windows systems, we rename the file and then
48// delete the renamed file.  It's a sloppy kludge, but frankly, I do not
49// know of a better way to do this, given windows' non-atomic unlink
50// semantics.
51//
52// See: https://github.com/npm/node-tar/issues/183
53/* istanbul ignore next */
54const unlinkFile = (path, cb) => {
55  if (process.platform !== 'win32')
56    return fs.unlink(path, cb)
57
58  const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex')
59  fs.rename(path, name, er => {
60    if (er)
61      return cb(er)
62    fs.unlink(name, cb)
63  })
64}
65
66/* istanbul ignore next */
67const unlinkFileSync = path => {
68  if (process.platform !== 'win32')
69    return fs.unlinkSync(path)
70
71  const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex')
72  fs.renameSync(path, name)
73  fs.unlinkSync(name)
74}
75
76// this.gid, entry.gid, this.processUid
77const uint32 = (a, b, c) =>
78  a === a >>> 0 ? a
79  : b === b >>> 0 ? b
80  : c
81
82class Unpack extends Parser {
83  constructor (opt) {
84    if (!opt)
85      opt = {}
86
87    opt.ondone = _ => {
88      this[ENDED] = true
89      this[MAYBECLOSE]()
90    }
91
92    super(opt)
93
94    this.transform = typeof opt.transform === 'function' ? opt.transform : null
95
96    this.writable = true
97    this.readable = false
98
99    this[PENDING] = 0
100    this[ENDED] = false
101
102    this.dirCache = opt.dirCache || new Map()
103
104    if (typeof opt.uid === 'number' || typeof opt.gid === 'number') {
105      // need both or neither
106      if (typeof opt.uid !== 'number' || typeof opt.gid !== 'number')
107        throw new TypeError('cannot set owner without number uid and gid')
108      if (opt.preserveOwner)
109        throw new TypeError(
110          'cannot preserve owner in archive and also set owner explicitly')
111      this.uid = opt.uid
112      this.gid = opt.gid
113      this.setOwner = true
114    } else {
115      this.uid = null
116      this.gid = null
117      this.setOwner = false
118    }
119
120    // default true for root
121    if (opt.preserveOwner === undefined && typeof opt.uid !== 'number')
122      this.preserveOwner = process.getuid && process.getuid() === 0
123    else
124      this.preserveOwner = !!opt.preserveOwner
125
126    this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ?
127      process.getuid() : null
128    this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ?
129      process.getgid() : null
130
131    // mostly just for testing, but useful in some cases.
132    // Forcibly trigger a chown on every entry, no matter what
133    this.forceChown = opt.forceChown === true
134
135    // turn ><?| in filenames into 0xf000-higher encoded forms
136    this.win32 = !!opt.win32 || process.platform === 'win32'
137
138    // do not unpack over files that are newer than what's in the archive
139    this.newer = !!opt.newer
140
141    // do not unpack over ANY files
142    this.keep = !!opt.keep
143
144    // do not set mtime/atime of extracted entries
145    this.noMtime = !!opt.noMtime
146
147    // allow .., absolute path entries, and unpacking through symlinks
148    // without this, warn and skip .., relativize absolutes, and error
149    // on symlinks in extraction path
150    this.preservePaths = !!opt.preservePaths
151
152    // unlink files and links before writing. This breaks existing hard
153    // links, and removes symlink directories rather than erroring
154    this.unlink = !!opt.unlink
155
156    this.cwd = path.resolve(opt.cwd || process.cwd())
157    this.strip = +opt.strip || 0
158    this.processUmask = process.umask()
159    this.umask = typeof opt.umask === 'number' ? opt.umask : this.processUmask
160    // default mode for dirs created as parents
161    this.dmode = opt.dmode || (0o0777 & (~this.umask))
162    this.fmode = opt.fmode || (0o0666 & (~this.umask))
163    this.on('entry', entry => this[ONENTRY](entry))
164  }
165
166  [MAYBECLOSE] () {
167    if (this[ENDED] && this[PENDING] === 0) {
168      this.emit('prefinish')
169      this.emit('finish')
170      this.emit('end')
171      this.emit('close')
172    }
173  }
174
175  [CHECKPATH] (entry) {
176    if (this.strip) {
177      const parts = entry.path.split(/\/|\\/)
178      if (parts.length < this.strip)
179        return false
180      entry.path = parts.slice(this.strip).join('/')
181
182      if (entry.type === 'Link') {
183        const linkparts = entry.linkpath.split(/\/|\\/)
184        if (linkparts.length >= this.strip)
185          entry.linkpath = linkparts.slice(this.strip).join('/')
186      }
187    }
188
189    if (!this.preservePaths) {
190      const p = entry.path
191      if (p.match(/(^|\/|\\)\.\.(\\|\/|$)/)) {
192        this.warn('path contains \'..\'', p)
193        return false
194      }
195
196      // absolutes on posix are also absolutes on win32
197      // so we only need to test this one to get both
198      if (path.win32.isAbsolute(p)) {
199        const parsed = path.win32.parse(p)
200        this.warn('stripping ' + parsed.root + ' from absolute path', p)
201        entry.path = p.substr(parsed.root.length)
202      }
203    }
204
205    // only encode : chars that aren't drive letter indicators
206    if (this.win32) {
207      const parsed = path.win32.parse(entry.path)
208      entry.path = parsed.root === '' ? wc.encode(entry.path)
209        : parsed.root + wc.encode(entry.path.substr(parsed.root.length))
210    }
211
212    if (path.isAbsolute(entry.path))
213      entry.absolute = entry.path
214    else
215      entry.absolute = path.resolve(this.cwd, entry.path)
216
217    return true
218  }
219
220  [ONENTRY] (entry) {
221    if (!this[CHECKPATH](entry))
222      return entry.resume()
223
224    assert.equal(typeof entry.absolute, 'string')
225
226    switch (entry.type) {
227      case 'Directory':
228      case 'GNUDumpDir':
229        if (entry.mode)
230          entry.mode = entry.mode | 0o700
231
232      case 'File':
233      case 'OldFile':
234      case 'ContiguousFile':
235      case 'Link':
236      case 'SymbolicLink':
237        return this[CHECKFS](entry)
238
239      case 'CharacterDevice':
240      case 'BlockDevice':
241      case 'FIFO':
242        return this[UNSUPPORTED](entry)
243    }
244  }
245
246  [ONERROR] (er, entry) {
247    // Cwd has to exist, or else nothing works. That's serious.
248    // Other errors are warnings, which raise the error in strict
249    // mode, but otherwise continue on.
250    if (er.name === 'CwdError')
251      this.emit('error', er)
252    else {
253      this.warn(er.message, er)
254      this[UNPEND]()
255      entry.resume()
256    }
257  }
258
259  [MKDIR] (dir, mode, cb) {
260    mkdir(dir, {
261      uid: this.uid,
262      gid: this.gid,
263      processUid: this.processUid,
264      processGid: this.processGid,
265      umask: this.processUmask,
266      preserve: this.preservePaths,
267      unlink: this.unlink,
268      cache: this.dirCache,
269      cwd: this.cwd,
270      mode: mode
271    }, cb)
272  }
273
274  [DOCHOWN] (entry) {
275    // in preserve owner mode, chown if the entry doesn't match process
276    // in set owner mode, chown if setting doesn't match process
277    return this.forceChown ||
278      this.preserveOwner &&
279      ( typeof entry.uid === 'number' && entry.uid !== this.processUid ||
280        typeof entry.gid === 'number' && entry.gid !== this.processGid )
281      ||
282      ( typeof this.uid === 'number' && this.uid !== this.processUid ||
283        typeof this.gid === 'number' && this.gid !== this.processGid )
284  }
285
286  [UID] (entry) {
287    return uint32(this.uid, entry.uid, this.processUid)
288  }
289
290  [GID] (entry) {
291    return uint32(this.gid, entry.gid, this.processGid)
292  }
293
294  [FILE] (entry) {
295    const mode = entry.mode & 0o7777 || this.fmode
296    const stream = new fsm.WriteStream(entry.absolute, {
297      mode: mode,
298      autoClose: false
299    })
300    stream.on('error', er => this[ONERROR](er, entry))
301
302    let actions = 1
303    const done = er => {
304      if (er)
305        return this[ONERROR](er, entry)
306
307      if (--actions === 0)
308        fs.close(stream.fd, _ => this[UNPEND]())
309    }
310
311    stream.on('finish', _ => {
312      // if futimes fails, try utimes
313      // if utimes fails, fail with the original error
314      // same for fchown/chown
315      const abs = entry.absolute
316      const fd = stream.fd
317
318      if (entry.mtime && !this.noMtime) {
319        actions++
320        const atime = entry.atime || new Date()
321        const mtime = entry.mtime
322        fs.futimes(fd, atime, mtime, er =>
323          er ? fs.utimes(abs, atime, mtime, er2 => done(er2 && er))
324          : done())
325      }
326
327      if (this[DOCHOWN](entry)) {
328        actions++
329        const uid = this[UID](entry)
330        const gid = this[GID](entry)
331        fs.fchown(fd, uid, gid, er =>
332          er ? fs.chown(abs, uid, gid, er2 => done(er2 && er))
333          : done())
334      }
335
336      done()
337    })
338
339    const tx = this.transform ? this.transform(entry) || entry : entry
340    if (tx !== entry) {
341      tx.on('error', er => this[ONERROR](er, entry))
342      entry.pipe(tx)
343    }
344    tx.pipe(stream)
345  }
346
347  [DIRECTORY] (entry) {
348    const mode = entry.mode & 0o7777 || this.dmode
349    this[MKDIR](entry.absolute, mode, er => {
350      if (er)
351        return this[ONERROR](er, entry)
352
353      let actions = 1
354      const done = _ => {
355        if (--actions === 0) {
356          this[UNPEND]()
357          entry.resume()
358        }
359      }
360
361      if (entry.mtime && !this.noMtime) {
362        actions++
363        fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, done)
364      }
365
366      if (this[DOCHOWN](entry)) {
367        actions++
368        fs.chown(entry.absolute, this[UID](entry), this[GID](entry), done)
369      }
370
371      done()
372    })
373  }
374
375  [UNSUPPORTED] (entry) {
376    this.warn('unsupported entry type: ' + entry.type, entry)
377    entry.resume()
378  }
379
380  [SYMLINK] (entry) {
381    this[LINK](entry, entry.linkpath, 'symlink')
382  }
383
384  [HARDLINK] (entry) {
385    this[LINK](entry, path.resolve(this.cwd, entry.linkpath), 'link')
386  }
387
388  [PEND] () {
389    this[PENDING]++
390  }
391
392  [UNPEND] () {
393    this[PENDING]--
394    this[MAYBECLOSE]()
395  }
396
397  [SKIP] (entry) {
398    this[UNPEND]()
399    entry.resume()
400  }
401
402  // Check if we can reuse an existing filesystem entry safely and
403  // overwrite it, rather than unlinking and recreating
404  // Windows doesn't report a useful nlink, so we just never reuse entries
405  [ISREUSABLE] (entry, st) {
406    return entry.type === 'File' &&
407      !this.unlink &&
408      st.isFile() &&
409      st.nlink <= 1 &&
410      process.platform !== 'win32'
411  }
412
413  // check if a thing is there, and if so, try to clobber it
414  [CHECKFS] (entry) {
415    this[PEND]()
416    this[MKDIR](path.dirname(entry.absolute), this.dmode, er => {
417      if (er)
418        return this[ONERROR](er, entry)
419      fs.lstat(entry.absolute, (er, st) => {
420        if (st && (this.keep || this.newer && st.mtime > entry.mtime))
421          this[SKIP](entry)
422        else if (er || this[ISREUSABLE](entry, st))
423          this[MAKEFS](null, entry)
424        else if (st.isDirectory()) {
425          if (entry.type === 'Directory') {
426            if (!entry.mode || (st.mode & 0o7777) === entry.mode)
427              this[MAKEFS](null, entry)
428            else
429              fs.chmod(entry.absolute, entry.mode, er => this[MAKEFS](er, entry))
430          } else
431            fs.rmdir(entry.absolute, er => this[MAKEFS](er, entry))
432        } else
433          unlinkFile(entry.absolute, er => this[MAKEFS](er, entry))
434      })
435    })
436  }
437
438  [MAKEFS] (er, entry) {
439    if (er)
440      return this[ONERROR](er, entry)
441
442    switch (entry.type) {
443      case 'File':
444      case 'OldFile':
445      case 'ContiguousFile':
446        return this[FILE](entry)
447
448      case 'Link':
449        return this[HARDLINK](entry)
450
451      case 'SymbolicLink':
452        return this[SYMLINK](entry)
453
454      case 'Directory':
455      case 'GNUDumpDir':
456        return this[DIRECTORY](entry)
457    }
458  }
459
460  [LINK] (entry, linkpath, link) {
461    // XXX: get the type ('file' or 'dir') for windows
462    fs[link](linkpath, entry.absolute, er => {
463      if (er)
464        return this[ONERROR](er, entry)
465      this[UNPEND]()
466      entry.resume()
467    })
468  }
469}
470
471class UnpackSync extends Unpack {
472  constructor (opt) {
473    super(opt)
474  }
475
476  [CHECKFS] (entry) {
477    const er = this[MKDIR](path.dirname(entry.absolute), this.dmode)
478    if (er)
479      return this[ONERROR](er, entry)
480    try {
481      const st = fs.lstatSync(entry.absolute)
482      if (this.keep || this.newer && st.mtime > entry.mtime)
483        return this[SKIP](entry)
484      else if (this[ISREUSABLE](entry, st))
485        return this[MAKEFS](null, entry)
486      else {
487        try {
488          if (st.isDirectory()) {
489            if (entry.type === 'Directory') {
490              if (entry.mode && (st.mode & 0o7777) !== entry.mode)
491                fs.chmodSync(entry.absolute, entry.mode)
492            } else
493              fs.rmdirSync(entry.absolute)
494          } else
495            unlinkFileSync(entry.absolute)
496          return this[MAKEFS](null, entry)
497        } catch (er) {
498          return this[ONERROR](er, entry)
499        }
500      }
501    } catch (er) {
502      return this[MAKEFS](null, entry)
503    }
504  }
505
506  [FILE] (entry) {
507    const mode = entry.mode & 0o7777 || this.fmode
508
509    const oner = er => {
510      try { fs.closeSync(fd) } catch (_) {}
511      if (er)
512        this[ONERROR](er, entry)
513    }
514
515    let stream
516    let fd
517    try {
518      fd = fs.openSync(entry.absolute, 'w', mode)
519    } catch (er) {
520      return oner(er)
521    }
522    const tx = this.transform ? this.transform(entry) || entry : entry
523    if (tx !== entry) {
524      tx.on('error', er => this[ONERROR](er, entry))
525      entry.pipe(tx)
526    }
527
528    tx.on('data', chunk => {
529      try {
530        fs.writeSync(fd, chunk, 0, chunk.length)
531      } catch (er) {
532        oner(er)
533      }
534    })
535
536    tx.on('end', _ => {
537      let er = null
538      // try both, falling futimes back to utimes
539      // if either fails, handle the first error
540      if (entry.mtime && !this.noMtime) {
541        const atime = entry.atime || new Date()
542        const mtime = entry.mtime
543        try {
544          fs.futimesSync(fd, atime, mtime)
545        } catch (futimeser) {
546          try {
547            fs.utimesSync(entry.absolute, atime, mtime)
548          } catch (utimeser) {
549            er = futimeser
550          }
551        }
552      }
553
554      if (this[DOCHOWN](entry)) {
555        const uid = this[UID](entry)
556        const gid = this[GID](entry)
557
558        try {
559          fs.fchownSync(fd, uid, gid)
560        } catch (fchowner) {
561          try {
562            fs.chownSync(entry.absolute, uid, gid)
563          } catch (chowner) {
564            er = er || fchowner
565          }
566        }
567      }
568
569      oner(er)
570    })
571  }
572
573  [DIRECTORY] (entry) {
574    const mode = entry.mode & 0o7777 || this.dmode
575    const er = this[MKDIR](entry.absolute, mode)
576    if (er)
577      return this[ONERROR](er, entry)
578    if (entry.mtime && !this.noMtime) {
579      try {
580        fs.utimesSync(entry.absolute, entry.atime || new Date(), entry.mtime)
581      } catch (er) {}
582    }
583    if (this[DOCHOWN](entry)) {
584      try {
585        fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry))
586      } catch (er) {}
587    }
588    entry.resume()
589  }
590
591  [MKDIR] (dir, mode) {
592    try {
593      return mkdir.sync(dir, {
594        uid: this.uid,
595        gid: this.gid,
596        processUid: this.processUid,
597        processGid: this.processGid,
598        umask: this.processUmask,
599        preserve: this.preservePaths,
600        unlink: this.unlink,
601        cache: this.dirCache,
602        cwd: this.cwd,
603        mode: mode
604      })
605    } catch (er) {
606      return er
607    }
608  }
609
610  [LINK] (entry, linkpath, link) {
611    try {
612      fs[link + 'Sync'](linkpath, entry.absolute)
613      entry.resume()
614    } catch (er) {
615      return this[ONERROR](er, entry)
616    }
617  }
618}
619
620Unpack.Sync = UnpackSync
621module.exports = Unpack
622