1'use strict'
2
3// the PEND/UNPEND stuff tracks whether we're ready to emit end/close yet.
4// but the path reservations are required to avoid race conditions where
5// parallelized unpack ops may mess with one another, due to dependencies
6// (like a Link depending on its target) or destructive operations (like
7// clobbering an fs object to create one of a different type.)
8
9const assert = require('assert')
10const Parser = require('./parse.js')
11const fs = require('fs')
12const fsm = require('fs-minipass')
13const path = require('path')
14const mkdir = require('./mkdir.js')
15const wc = require('./winchars.js')
16const pathReservations = require('./path-reservations.js')
17const stripAbsolutePath = require('./strip-absolute-path.js')
18const normPath = require('./normalize-windows-path.js')
19const stripSlash = require('./strip-trailing-slashes.js')
20const normalize = require('./normalize-unicode.js')
21
22const ONENTRY = Symbol('onEntry')
23const CHECKFS = Symbol('checkFs')
24const CHECKFS2 = Symbol('checkFs2')
25const PRUNECACHE = Symbol('pruneCache')
26const ISREUSABLE = Symbol('isReusable')
27const MAKEFS = Symbol('makeFs')
28const FILE = Symbol('file')
29const DIRECTORY = Symbol('directory')
30const LINK = Symbol('link')
31const SYMLINK = Symbol('symlink')
32const HARDLINK = Symbol('hardlink')
33const UNSUPPORTED = Symbol('unsupported')
34const CHECKPATH = Symbol('checkPath')
35const MKDIR = Symbol('mkdir')
36const ONERROR = Symbol('onError')
37const PENDING = Symbol('pending')
38const PEND = Symbol('pend')
39const UNPEND = Symbol('unpend')
40const ENDED = Symbol('ended')
41const MAYBECLOSE = Symbol('maybeClose')
42const SKIP = Symbol('skip')
43const DOCHOWN = Symbol('doChown')
44const UID = Symbol('uid')
45const GID = Symbol('gid')
46const CHECKED_CWD = Symbol('checkedCwd')
47const crypto = require('crypto')
48const getFlag = require('./get-write-flag.js')
49const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform
50const isWindows = platform === 'win32'
51
52// Unlinks on Windows are not atomic.
53//
54// This means that if you have a file entry, followed by another
55// file entry with an identical name, and you cannot re-use the file
56// (because it's a hardlink, or because unlink:true is set, or it's
57// Windows, which does not have useful nlink values), then the unlink
58// will be committed to the disk AFTER the new file has been written
59// over the old one, deleting the new file.
60//
61// To work around this, on Windows systems, we rename the file and then
62// delete the renamed file.  It's a sloppy kludge, but frankly, I do not
63// know of a better way to do this, given windows' non-atomic unlink
64// semantics.
65//
66// See: https://github.com/npm/node-tar/issues/183
67/* istanbul ignore next */
68const unlinkFile = (path, cb) => {
69  if (!isWindows) {
70    return fs.unlink(path, cb)
71  }
72
73  const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex')
74  fs.rename(path, name, er => {
75    if (er) {
76      return cb(er)
77    }
78    fs.unlink(name, cb)
79  })
80}
81
82/* istanbul ignore next */
83const unlinkFileSync = path => {
84  if (!isWindows) {
85    return fs.unlinkSync(path)
86  }
87
88  const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex')
89  fs.renameSync(path, name)
90  fs.unlinkSync(name)
91}
92
93// this.gid, entry.gid, this.processUid
94const uint32 = (a, b, c) =>
95  a === a >>> 0 ? a
96  : b === b >>> 0 ? b
97  : c
98
99// clear the cache if it's a case-insensitive unicode-squashing match.
100// we can't know if the current file system is case-sensitive or supports
101// unicode fully, so we check for similarity on the maximally compatible
102// representation.  Err on the side of pruning, since all it's doing is
103// preventing lstats, and it's not the end of the world if we get a false
104// positive.
105// Note that on windows, we always drop the entire cache whenever a
106// symbolic link is encountered, because 8.3 filenames are impossible
107// to reason about, and collisions are hazards rather than just failures.
108const cacheKeyNormalize = path => stripSlash(normPath(normalize(path)))
109  .toLowerCase()
110
111const pruneCache = (cache, abs) => {
112  abs = cacheKeyNormalize(abs)
113  for (const path of cache.keys()) {
114    const pnorm = cacheKeyNormalize(path)
115    if (pnorm === abs || pnorm.indexOf(abs + '/') === 0) {
116      cache.delete(path)
117    }
118  }
119}
120
121const dropCache = cache => {
122  for (const key of cache.keys()) {
123    cache.delete(key)
124  }
125}
126
127class Unpack extends Parser {
128  constructor (opt) {
129    if (!opt) {
130      opt = {}
131    }
132
133    opt.ondone = _ => {
134      this[ENDED] = true
135      this[MAYBECLOSE]()
136    }
137
138    super(opt)
139
140    this[CHECKED_CWD] = false
141
142    this.reservations = pathReservations()
143
144    this.transform = typeof opt.transform === 'function' ? opt.transform : null
145
146    this.writable = true
147    this.readable = false
148
149    this[PENDING] = 0
150    this[ENDED] = false
151
152    this.dirCache = opt.dirCache || new Map()
153
154    if (typeof opt.uid === 'number' || typeof opt.gid === 'number') {
155      // need both or neither
156      if (typeof opt.uid !== 'number' || typeof opt.gid !== 'number') {
157        throw new TypeError('cannot set owner without number uid and gid')
158      }
159      if (opt.preserveOwner) {
160        throw new TypeError(
161          'cannot preserve owner in archive and also set owner explicitly')
162      }
163      this.uid = opt.uid
164      this.gid = opt.gid
165      this.setOwner = true
166    } else {
167      this.uid = null
168      this.gid = null
169      this.setOwner = false
170    }
171
172    // default true for root
173    if (opt.preserveOwner === undefined && typeof opt.uid !== 'number') {
174      this.preserveOwner = process.getuid && process.getuid() === 0
175    } else {
176      this.preserveOwner = !!opt.preserveOwner
177    }
178
179    this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ?
180      process.getuid() : null
181    this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ?
182      process.getgid() : null
183
184    // mostly just for testing, but useful in some cases.
185    // Forcibly trigger a chown on every entry, no matter what
186    this.forceChown = opt.forceChown === true
187
188    // turn ><?| in filenames into 0xf000-higher encoded forms
189    this.win32 = !!opt.win32 || isWindows
190
191    // do not unpack over files that are newer than what's in the archive
192    this.newer = !!opt.newer
193
194    // do not unpack over ANY files
195    this.keep = !!opt.keep
196
197    // do not set mtime/atime of extracted entries
198    this.noMtime = !!opt.noMtime
199
200    // allow .., absolute path entries, and unpacking through symlinks
201    // without this, warn and skip .., relativize absolutes, and error
202    // on symlinks in extraction path
203    this.preservePaths = !!opt.preservePaths
204
205    // unlink files and links before writing. This breaks existing hard
206    // links, and removes symlink directories rather than erroring
207    this.unlink = !!opt.unlink
208
209    this.cwd = normPath(path.resolve(opt.cwd || process.cwd()))
210    this.strip = +opt.strip || 0
211    // if we're not chmodding, then we don't need the process umask
212    this.processUmask = opt.noChmod ? 0 : process.umask()
213    this.umask = typeof opt.umask === 'number' ? opt.umask : this.processUmask
214
215    // default mode for dirs created as parents
216    this.dmode = opt.dmode || (0o0777 & (~this.umask))
217    this.fmode = opt.fmode || (0o0666 & (~this.umask))
218
219    this.on('entry', entry => this[ONENTRY](entry))
220  }
221
222  // a bad or damaged archive is a warning for Parser, but an error
223  // when extracting.  Mark those errors as unrecoverable, because
224  // the Unpack contract cannot be met.
225  warn (code, msg, data = {}) {
226    if (code === 'TAR_BAD_ARCHIVE' || code === 'TAR_ABORT') {
227      data.recoverable = false
228    }
229    return super.warn(code, msg, data)
230  }
231
232  [MAYBECLOSE] () {
233    if (this[ENDED] && this[PENDING] === 0) {
234      this.emit('prefinish')
235      this.emit('finish')
236      this.emit('end')
237    }
238  }
239
240  [CHECKPATH] (entry) {
241    if (this.strip) {
242      const parts = normPath(entry.path).split('/')
243      if (parts.length < this.strip) {
244        return false
245      }
246      entry.path = parts.slice(this.strip).join('/')
247
248      if (entry.type === 'Link') {
249        const linkparts = normPath(entry.linkpath).split('/')
250        if (linkparts.length >= this.strip) {
251          entry.linkpath = linkparts.slice(this.strip).join('/')
252        } else {
253          return false
254        }
255      }
256    }
257
258    if (!this.preservePaths) {
259      const p = normPath(entry.path)
260      const parts = p.split('/')
261      if (parts.includes('..') || isWindows && /^[a-z]:\.\.$/i.test(parts[0])) {
262        this.warn('TAR_ENTRY_ERROR', `path contains '..'`, {
263          entry,
264          path: p,
265        })
266        return false
267      }
268
269      // strip off the root
270      const [root, stripped] = stripAbsolutePath(p)
271      if (root) {
272        entry.path = stripped
273        this.warn('TAR_ENTRY_INFO', `stripping ${root} from absolute path`, {
274          entry,
275          path: p,
276        })
277      }
278    }
279
280    if (path.isAbsolute(entry.path)) {
281      entry.absolute = normPath(path.resolve(entry.path))
282    } else {
283      entry.absolute = normPath(path.resolve(this.cwd, entry.path))
284    }
285
286    // if we somehow ended up with a path that escapes the cwd, and we are
287    // not in preservePaths mode, then something is fishy!  This should have
288    // been prevented above, so ignore this for coverage.
289    /* istanbul ignore if - defense in depth */
290    if (!this.preservePaths &&
291        entry.absolute.indexOf(this.cwd + '/') !== 0 &&
292        entry.absolute !== this.cwd) {
293      this.warn('TAR_ENTRY_ERROR', 'path escaped extraction target', {
294        entry,
295        path: normPath(entry.path),
296        resolvedPath: entry.absolute,
297        cwd: this.cwd,
298      })
299      return false
300    }
301
302    // an archive can set properties on the extraction directory, but it
303    // may not replace the cwd with a different kind of thing entirely.
304    if (entry.absolute === this.cwd &&
305        entry.type !== 'Directory' &&
306        entry.type !== 'GNUDumpDir') {
307      return false
308    }
309
310    // only encode : chars that aren't drive letter indicators
311    if (this.win32) {
312      const { root: aRoot } = path.win32.parse(entry.absolute)
313      entry.absolute = aRoot + wc.encode(entry.absolute.slice(aRoot.length))
314      const { root: pRoot } = path.win32.parse(entry.path)
315      entry.path = pRoot + wc.encode(entry.path.slice(pRoot.length))
316    }
317
318    return true
319  }
320
321  [ONENTRY] (entry) {
322    if (!this[CHECKPATH](entry)) {
323      return entry.resume()
324    }
325
326    assert.equal(typeof entry.absolute, 'string')
327
328    switch (entry.type) {
329      case 'Directory':
330      case 'GNUDumpDir':
331        if (entry.mode) {
332          entry.mode = entry.mode | 0o700
333        }
334
335      // eslint-disable-next-line no-fallthrough
336      case 'File':
337      case 'OldFile':
338      case 'ContiguousFile':
339      case 'Link':
340      case 'SymbolicLink':
341        return this[CHECKFS](entry)
342
343      case 'CharacterDevice':
344      case 'BlockDevice':
345      case 'FIFO':
346      default:
347        return this[UNSUPPORTED](entry)
348    }
349  }
350
351  [ONERROR] (er, entry) {
352    // Cwd has to exist, or else nothing works. That's serious.
353    // Other errors are warnings, which raise the error in strict
354    // mode, but otherwise continue on.
355    if (er.name === 'CwdError') {
356      this.emit('error', er)
357    } else {
358      this.warn('TAR_ENTRY_ERROR', er, { entry })
359      this[UNPEND]()
360      entry.resume()
361    }
362  }
363
364  [MKDIR] (dir, mode, cb) {
365    mkdir(normPath(dir), {
366      uid: this.uid,
367      gid: this.gid,
368      processUid: this.processUid,
369      processGid: this.processGid,
370      umask: this.processUmask,
371      preserve: this.preservePaths,
372      unlink: this.unlink,
373      cache: this.dirCache,
374      cwd: this.cwd,
375      mode: mode,
376      noChmod: this.noChmod,
377    }, cb)
378  }
379
380  [DOCHOWN] (entry) {
381    // in preserve owner mode, chown if the entry doesn't match process
382    // in set owner mode, chown if setting doesn't match process
383    return this.forceChown ||
384      this.preserveOwner &&
385      (typeof entry.uid === 'number' && entry.uid !== this.processUid ||
386        typeof entry.gid === 'number' && entry.gid !== this.processGid)
387      ||
388      (typeof this.uid === 'number' && this.uid !== this.processUid ||
389        typeof this.gid === 'number' && this.gid !== this.processGid)
390  }
391
392  [UID] (entry) {
393    return uint32(this.uid, entry.uid, this.processUid)
394  }
395
396  [GID] (entry) {
397    return uint32(this.gid, entry.gid, this.processGid)
398  }
399
400  [FILE] (entry, fullyDone) {
401    const mode = entry.mode & 0o7777 || this.fmode
402    const stream = new fsm.WriteStream(entry.absolute, {
403      flags: getFlag(entry.size),
404      mode: mode,
405      autoClose: false,
406    })
407    stream.on('error', er => {
408      if (stream.fd) {
409        fs.close(stream.fd, () => {})
410      }
411
412      // flush all the data out so that we aren't left hanging
413      // if the error wasn't actually fatal.  otherwise the parse
414      // is blocked, and we never proceed.
415      stream.write = () => true
416      this[ONERROR](er, entry)
417      fullyDone()
418    })
419
420    let actions = 1
421    const done = er => {
422      if (er) {
423        /* istanbul ignore else - we should always have a fd by now */
424        if (stream.fd) {
425          fs.close(stream.fd, () => {})
426        }
427
428        this[ONERROR](er, entry)
429        fullyDone()
430        return
431      }
432
433      if (--actions === 0) {
434        fs.close(stream.fd, er => {
435          if (er) {
436            this[ONERROR](er, entry)
437          } else {
438            this[UNPEND]()
439          }
440          fullyDone()
441        })
442      }
443    }
444
445    stream.on('finish', _ => {
446      // if futimes fails, try utimes
447      // if utimes fails, fail with the original error
448      // same for fchown/chown
449      const abs = entry.absolute
450      const fd = stream.fd
451
452      if (entry.mtime && !this.noMtime) {
453        actions++
454        const atime = entry.atime || new Date()
455        const mtime = entry.mtime
456        fs.futimes(fd, atime, mtime, er =>
457          er ? fs.utimes(abs, atime, mtime, er2 => done(er2 && er))
458          : done())
459      }
460
461      if (this[DOCHOWN](entry)) {
462        actions++
463        const uid = this[UID](entry)
464        const gid = this[GID](entry)
465        fs.fchown(fd, uid, gid, er =>
466          er ? fs.chown(abs, uid, gid, er2 => done(er2 && er))
467          : done())
468      }
469
470      done()
471    })
472
473    const tx = this.transform ? this.transform(entry) || entry : entry
474    if (tx !== entry) {
475      tx.on('error', er => {
476        this[ONERROR](er, entry)
477        fullyDone()
478      })
479      entry.pipe(tx)
480    }
481    tx.pipe(stream)
482  }
483
484  [DIRECTORY] (entry, fullyDone) {
485    const mode = entry.mode & 0o7777 || this.dmode
486    this[MKDIR](entry.absolute, mode, er => {
487      if (er) {
488        this[ONERROR](er, entry)
489        fullyDone()
490        return
491      }
492
493      let actions = 1
494      const done = _ => {
495        if (--actions === 0) {
496          fullyDone()
497          this[UNPEND]()
498          entry.resume()
499        }
500      }
501
502      if (entry.mtime && !this.noMtime) {
503        actions++
504        fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, done)
505      }
506
507      if (this[DOCHOWN](entry)) {
508        actions++
509        fs.chown(entry.absolute, this[UID](entry), this[GID](entry), done)
510      }
511
512      done()
513    })
514  }
515
516  [UNSUPPORTED] (entry) {
517    entry.unsupported = true
518    this.warn('TAR_ENTRY_UNSUPPORTED',
519      `unsupported entry type: ${entry.type}`, { entry })
520    entry.resume()
521  }
522
523  [SYMLINK] (entry, done) {
524    this[LINK](entry, entry.linkpath, 'symlink', done)
525  }
526
527  [HARDLINK] (entry, done) {
528    const linkpath = normPath(path.resolve(this.cwd, entry.linkpath))
529    this[LINK](entry, linkpath, 'link', done)
530  }
531
532  [PEND] () {
533    this[PENDING]++
534  }
535
536  [UNPEND] () {
537    this[PENDING]--
538    this[MAYBECLOSE]()
539  }
540
541  [SKIP] (entry) {
542    this[UNPEND]()
543    entry.resume()
544  }
545
546  // Check if we can reuse an existing filesystem entry safely and
547  // overwrite it, rather than unlinking and recreating
548  // Windows doesn't report a useful nlink, so we just never reuse entries
549  [ISREUSABLE] (entry, st) {
550    return entry.type === 'File' &&
551      !this.unlink &&
552      st.isFile() &&
553      st.nlink <= 1 &&
554      !isWindows
555  }
556
557  // check if a thing is there, and if so, try to clobber it
558  [CHECKFS] (entry) {
559    this[PEND]()
560    const paths = [entry.path]
561    if (entry.linkpath) {
562      paths.push(entry.linkpath)
563    }
564    this.reservations.reserve(paths, done => this[CHECKFS2](entry, done))
565  }
566
567  [PRUNECACHE] (entry) {
568    // if we are not creating a directory, and the path is in the dirCache,
569    // then that means we are about to delete the directory we created
570    // previously, and it is no longer going to be a directory, and neither
571    // is any of its children.
572    // If a symbolic link is encountered, all bets are off.  There is no
573    // reasonable way to sanitize the cache in such a way we will be able to
574    // avoid having filesystem collisions.  If this happens with a non-symlink
575    // entry, it'll just fail to unpack, but a symlink to a directory, using an
576    // 8.3 shortname or certain unicode attacks, can evade detection and lead
577    // to arbitrary writes to anywhere on the system.
578    if (entry.type === 'SymbolicLink') {
579      dropCache(this.dirCache)
580    } else if (entry.type !== 'Directory') {
581      pruneCache(this.dirCache, entry.absolute)
582    }
583  }
584
585  [CHECKFS2] (entry, fullyDone) {
586    this[PRUNECACHE](entry)
587
588    const done = er => {
589      this[PRUNECACHE](entry)
590      fullyDone(er)
591    }
592
593    const checkCwd = () => {
594      this[MKDIR](this.cwd, this.dmode, er => {
595        if (er) {
596          this[ONERROR](er, entry)
597          done()
598          return
599        }
600        this[CHECKED_CWD] = true
601        start()
602      })
603    }
604
605    const start = () => {
606      if (entry.absolute !== this.cwd) {
607        const parent = normPath(path.dirname(entry.absolute))
608        if (parent !== this.cwd) {
609          return this[MKDIR](parent, this.dmode, er => {
610            if (er) {
611              this[ONERROR](er, entry)
612              done()
613              return
614            }
615            afterMakeParent()
616          })
617        }
618      }
619      afterMakeParent()
620    }
621
622    const afterMakeParent = () => {
623      fs.lstat(entry.absolute, (lstatEr, st) => {
624        if (st && (this.keep || this.newer && st.mtime > entry.mtime)) {
625          this[SKIP](entry)
626          done()
627          return
628        }
629        if (lstatEr || this[ISREUSABLE](entry, st)) {
630          return this[MAKEFS](null, entry, done)
631        }
632
633        if (st.isDirectory()) {
634          if (entry.type === 'Directory') {
635            const needChmod = !this.noChmod &&
636              entry.mode &&
637              (st.mode & 0o7777) !== entry.mode
638            const afterChmod = er => this[MAKEFS](er, entry, done)
639            if (!needChmod) {
640              return afterChmod()
641            }
642            return fs.chmod(entry.absolute, entry.mode, afterChmod)
643          }
644          // Not a dir entry, have to remove it.
645          // NB: the only way to end up with an entry that is the cwd
646          // itself, in such a way that == does not detect, is a
647          // tricky windows absolute path with UNC or 8.3 parts (and
648          // preservePaths:true, or else it will have been stripped).
649          // In that case, the user has opted out of path protections
650          // explicitly, so if they blow away the cwd, c'est la vie.
651          if (entry.absolute !== this.cwd) {
652            return fs.rmdir(entry.absolute, er =>
653              this[MAKEFS](er, entry, done))
654          }
655        }
656
657        // not a dir, and not reusable
658        // don't remove if the cwd, we want that error
659        if (entry.absolute === this.cwd) {
660          return this[MAKEFS](null, entry, done)
661        }
662
663        unlinkFile(entry.absolute, er =>
664          this[MAKEFS](er, entry, done))
665      })
666    }
667
668    if (this[CHECKED_CWD]) {
669      start()
670    } else {
671      checkCwd()
672    }
673  }
674
675  [MAKEFS] (er, entry, done) {
676    if (er) {
677      this[ONERROR](er, entry)
678      done()
679      return
680    }
681
682    switch (entry.type) {
683      case 'File':
684      case 'OldFile':
685      case 'ContiguousFile':
686        return this[FILE](entry, done)
687
688      case 'Link':
689        return this[HARDLINK](entry, done)
690
691      case 'SymbolicLink':
692        return this[SYMLINK](entry, done)
693
694      case 'Directory':
695      case 'GNUDumpDir':
696        return this[DIRECTORY](entry, done)
697    }
698  }
699
700  [LINK] (entry, linkpath, link, done) {
701    // XXX: get the type ('symlink' or 'junction') for windows
702    fs[link](linkpath, entry.absolute, er => {
703      if (er) {
704        this[ONERROR](er, entry)
705      } else {
706        this[UNPEND]()
707        entry.resume()
708      }
709      done()
710    })
711  }
712}
713
714const callSync = fn => {
715  try {
716    return [null, fn()]
717  } catch (er) {
718    return [er, null]
719  }
720}
721class UnpackSync extends Unpack {
722  [MAKEFS] (er, entry) {
723    return super[MAKEFS](er, entry, () => {})
724  }
725
726  [CHECKFS] (entry) {
727    this[PRUNECACHE](entry)
728
729    if (!this[CHECKED_CWD]) {
730      const er = this[MKDIR](this.cwd, this.dmode)
731      if (er) {
732        return this[ONERROR](er, entry)
733      }
734      this[CHECKED_CWD] = true
735    }
736
737    // don't bother to make the parent if the current entry is the cwd,
738    // we've already checked it.
739    if (entry.absolute !== this.cwd) {
740      const parent = normPath(path.dirname(entry.absolute))
741      if (parent !== this.cwd) {
742        const mkParent = this[MKDIR](parent, this.dmode)
743        if (mkParent) {
744          return this[ONERROR](mkParent, entry)
745        }
746      }
747    }
748
749    const [lstatEr, st] = callSync(() => fs.lstatSync(entry.absolute))
750    if (st && (this.keep || this.newer && st.mtime > entry.mtime)) {
751      return this[SKIP](entry)
752    }
753
754    if (lstatEr || this[ISREUSABLE](entry, st)) {
755      return this[MAKEFS](null, entry)
756    }
757
758    if (st.isDirectory()) {
759      if (entry.type === 'Directory') {
760        const needChmod = !this.noChmod &&
761          entry.mode &&
762          (st.mode & 0o7777) !== entry.mode
763        const [er] = needChmod ? callSync(() => {
764          fs.chmodSync(entry.absolute, entry.mode)
765        }) : []
766        return this[MAKEFS](er, entry)
767      }
768      // not a dir entry, have to remove it
769      const [er] = callSync(() => fs.rmdirSync(entry.absolute))
770      this[MAKEFS](er, entry)
771    }
772
773    // not a dir, and not reusable.
774    // don't remove if it's the cwd, since we want that error.
775    const [er] = entry.absolute === this.cwd ? []
776      : callSync(() => unlinkFileSync(entry.absolute))
777    this[MAKEFS](er, entry)
778  }
779
780  [FILE] (entry, done) {
781    const mode = entry.mode & 0o7777 || this.fmode
782
783    const oner = er => {
784      let closeError
785      try {
786        fs.closeSync(fd)
787      } catch (e) {
788        closeError = e
789      }
790      if (er || closeError) {
791        this[ONERROR](er || closeError, entry)
792      }
793      done()
794    }
795
796    let fd
797    try {
798      fd = fs.openSync(entry.absolute, getFlag(entry.size), mode)
799    } catch (er) {
800      return oner(er)
801    }
802    const tx = this.transform ? this.transform(entry) || entry : entry
803    if (tx !== entry) {
804      tx.on('error', er => this[ONERROR](er, entry))
805      entry.pipe(tx)
806    }
807
808    tx.on('data', chunk => {
809      try {
810        fs.writeSync(fd, chunk, 0, chunk.length)
811      } catch (er) {
812        oner(er)
813      }
814    })
815
816    tx.on('end', _ => {
817      let er = null
818      // try both, falling futimes back to utimes
819      // if either fails, handle the first error
820      if (entry.mtime && !this.noMtime) {
821        const atime = entry.atime || new Date()
822        const mtime = entry.mtime
823        try {
824          fs.futimesSync(fd, atime, mtime)
825        } catch (futimeser) {
826          try {
827            fs.utimesSync(entry.absolute, atime, mtime)
828          } catch (utimeser) {
829            er = futimeser
830          }
831        }
832      }
833
834      if (this[DOCHOWN](entry)) {
835        const uid = this[UID](entry)
836        const gid = this[GID](entry)
837
838        try {
839          fs.fchownSync(fd, uid, gid)
840        } catch (fchowner) {
841          try {
842            fs.chownSync(entry.absolute, uid, gid)
843          } catch (chowner) {
844            er = er || fchowner
845          }
846        }
847      }
848
849      oner(er)
850    })
851  }
852
853  [DIRECTORY] (entry, done) {
854    const mode = entry.mode & 0o7777 || this.dmode
855    const er = this[MKDIR](entry.absolute, mode)
856    if (er) {
857      this[ONERROR](er, entry)
858      done()
859      return
860    }
861    if (entry.mtime && !this.noMtime) {
862      try {
863        fs.utimesSync(entry.absolute, entry.atime || new Date(), entry.mtime)
864      } catch (er) {}
865    }
866    if (this[DOCHOWN](entry)) {
867      try {
868        fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry))
869      } catch (er) {}
870    }
871    done()
872    entry.resume()
873  }
874
875  [MKDIR] (dir, mode) {
876    try {
877      return mkdir.sync(normPath(dir), {
878        uid: this.uid,
879        gid: this.gid,
880        processUid: this.processUid,
881        processGid: this.processGid,
882        umask: this.processUmask,
883        preserve: this.preservePaths,
884        unlink: this.unlink,
885        cache: this.dirCache,
886        cwd: this.cwd,
887        mode: mode,
888      })
889    } catch (er) {
890      return er
891    }
892  }
893
894  [LINK] (entry, linkpath, link, done) {
895    try {
896      fs[link + 'Sync'](linkpath, entry.absolute)
897      done()
898      entry.resume()
899    } catch (er) {
900      return this[ONERROR](er, entry)
901    }
902  }
903}
904
905Unpack.Sync = UnpackSync
906module.exports = Unpack
907