3 const assert = require('assert')
4 const EE = require('events').EventEmitter
5 const Parser = require('./parse.js')
6 const fs = require('fs')
7 const fsm = require('fs-minipass')
8 const path = require('path')
9 const mkdir = require('./mkdir.js')
10 const mkdirSync = mkdir.sync
11 const wc = require('./winchars.js')
13 const ONENTRY = Symbol('onEntry')
14 const CHECKFS = Symbol('checkFs')
15 const ISREUSABLE = Symbol('isReusable')
16 const MAKEFS = Symbol('makeFs')
17 const FILE = Symbol('file')
18 const DIRECTORY = Symbol('directory')
19 const LINK = Symbol('link')
20 const SYMLINK = Symbol('symlink')
21 const HARDLINK = Symbol('hardlink')
22 const UNSUPPORTED = Symbol('unsupported')
23 const UNKNOWN = Symbol('unknown')
24 const CHECKPATH = Symbol('checkPath')
25 const MKDIR = Symbol('mkdir')
26 const ONERROR = Symbol('onError')
27 const PENDING = Symbol('pending')
28 const PEND = Symbol('pend')
29 const UNPEND = Symbol('unpend')
30 const ENDED = Symbol('ended')
31 const MAYBECLOSE = Symbol('maybeClose')
32 const SKIP = Symbol('skip')
33 const DOCHOWN = Symbol('doChown')
34 const UID = Symbol('uid')
35 const GID = Symbol('gid')
36 const crypto = require('crypto')
38 // Unlinks on Windows are not atomic.
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.
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
52 // See: https://github.com/npm/node-tar/issues/183
53 /* istanbul ignore next */
54 const unlinkFile = (path, cb) => {
55 if (process.platform !== 'win32')
56 return fs.unlink(path, cb)
58 const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex')
59 fs.rename(path, name, er => {
66 /* istanbul ignore next */
67 const unlinkFileSync = path => {
68 if (process.platform !== 'win32')
69 return fs.unlinkSync(path)
71 const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex')
72 fs.renameSync(path, name)
76 // this.gid, entry.gid, this.processUid
77 const uint32 = (a, b, c) =>
82 class Unpack extends Parser {
94 this.transform = typeof opt.transform === 'function' ? opt.transform : null
102 this.dirCache = opt.dirCache || new Map()
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)
110 'cannot preserve owner in archive and also set owner explicitly')
117 this.setOwner = false
120 // default true for root
121 if (opt.preserveOwner === undefined && typeof opt.uid !== 'number')
122 this.preserveOwner = process.getuid && process.getuid() === 0
124 this.preserveOwner = !!opt.preserveOwner
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
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
135 // turn ><?| in filenames into 0xf000-higher encoded forms
136 this.win32 = !!opt.win32 || process.platform === 'win32'
138 // do not unpack over files that are newer than what's in the archive
139 this.newer = !!opt.newer
141 // do not unpack over ANY files
142 this.keep = !!opt.keep
144 // do not set mtime/atime of extracted entries
145 this.noMtime = !!opt.noMtime
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
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
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))
167 if (this[ENDED] && this[PENDING] === 0) {
168 this.emit('prefinish')
175 [CHECKPATH] (entry) {
177 const parts = entry.path.split(/\/|\\/)
178 if (parts.length < this.strip)
180 entry.path = parts.slice(this.strip).join('/')
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('/')
189 if (!this.preservePaths) {
191 if (p.match(/(^|\/|\\)\.\.(\\|\/|$)/)) {
192 this.warn('path contains \'..\'', p)
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)
205 // only encode : chars that aren't drive letter indicators
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))
212 if (path.isAbsolute(entry.path))
213 entry.absolute = entry.path
215 entry.absolute = path.resolve(this.cwd, entry.path)
221 if (!this[CHECKPATH](entry))
222 return entry.resume()
224 assert.equal(typeof entry.absolute, 'string')
226 switch (entry.type) {
230 entry.mode = entry.mode | 0o700
234 case 'ContiguousFile':
237 return this[CHECKFS](entry)
239 case 'CharacterDevice':
242 return this[UNSUPPORTED](entry)
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)
253 this.warn(er.message, er)
259 [MKDIR] (dir, mode, cb) {
263 processUid: this.processUid,
264 processGid: this.processGid,
265 umask: this.processUmask,
266 preserve: this.preservePaths,
268 cache: this.dirCache,
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 )
282 ( typeof this.uid === 'number' && this.uid !== this.processUid ||
283 typeof this.gid === 'number' && this.gid !== this.processGid )
287 return uint32(this.uid, entry.uid, this.processUid)
291 return uint32(this.gid, entry.gid, this.processGid)
295 const mode = entry.mode & 0o7777 || this.fmode
296 const stream = new fsm.WriteStream(entry.absolute, {
300 stream.on('error', er => this[ONERROR](er, entry))
305 return this[ONERROR](er, entry)
308 fs.close(stream.fd, _ => this[UNPEND]())
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
318 if (entry.mtime && !this.noMtime) {
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))
327 if (this[DOCHOWN](entry)) {
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))
339 const tx = this.transform ? this.transform(entry) || entry : entry
341 tx.on('error', er => this[ONERROR](er, entry))
347 [DIRECTORY] (entry) {
348 const mode = entry.mode & 0o7777 || this.dmode
349 this[MKDIR](entry.absolute, mode, er => {
351 return this[ONERROR](er, entry)
355 if (--actions === 0) {
361 if (entry.mtime && !this.noMtime) {
363 fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, done)
366 if (this[DOCHOWN](entry)) {
368 fs.chown(entry.absolute, this[UID](entry), this[GID](entry), done)
375 [UNSUPPORTED] (entry) {
376 this.warn('unsupported entry type: ' + entry.type, entry)
381 this[LINK](entry, entry.linkpath, 'symlink')
385 this[LINK](entry, path.resolve(this.cwd, entry.linkpath), 'link')
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' &&
410 process.platform !== 'win32'
413 // check if a thing is there, and if so, try to clobber it
416 this[MKDIR](path.dirname(entry.absolute), this.dmode, 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))
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)
429 fs.chmod(entry.absolute, entry.mode, er => this[MAKEFS](er, entry))
431 fs.rmdir(entry.absolute, er => this[MAKEFS](er, entry))
433 unlinkFile(entry.absolute, er => this[MAKEFS](er, entry))
438 [MAKEFS] (er, entry) {
440 return this[ONERROR](er, entry)
442 switch (entry.type) {
445 case 'ContiguousFile':
446 return this[FILE](entry)
449 return this[HARDLINK](entry)
452 return this[SYMLINK](entry)
456 return this[DIRECTORY](entry)
460 [LINK] (entry, linkpath, link) {
461 // XXX: get the type ('file' or 'dir') for windows
462 fs[link](linkpath, entry.absolute, er => {
464 return this[ONERROR](er, entry)
471 class UnpackSync extends Unpack {
477 const er = this[MKDIR](path.dirname(entry.absolute), this.dmode)
479 return this[ONERROR](er, entry)
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)
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)
493 fs.rmdirSync(entry.absolute)
495 unlinkFileSync(entry.absolute)
496 return this[MAKEFS](null, entry)
498 return this[ONERROR](er, entry)
502 return this[MAKEFS](null, entry)
507 const mode = entry.mode & 0o7777 || this.fmode
510 try { fs.closeSync(fd) } catch (_) {}
512 this[ONERROR](er, entry)
518 fd = fs.openSync(entry.absolute, 'w', mode)
522 const tx = this.transform ? this.transform(entry) || entry : entry
524 tx.on('error', er => this[ONERROR](er, entry))
528 tx.on('data', chunk => {
530 fs.writeSync(fd, chunk, 0, chunk.length)
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
544 fs.futimesSync(fd, atime, mtime)
545 } catch (futimeser) {
547 fs.utimesSync(entry.absolute, atime, mtime)
554 if (this[DOCHOWN](entry)) {
555 const uid = this[UID](entry)
556 const gid = this[GID](entry)
559 fs.fchownSync(fd, uid, gid)
562 fs.chownSync(entry.absolute, uid, gid)
573 [DIRECTORY] (entry) {
574 const mode = entry.mode & 0o7777 || this.dmode
575 const er = this[MKDIR](entry.absolute, mode)
577 return this[ONERROR](er, entry)
578 if (entry.mtime && !this.noMtime) {
580 fs.utimesSync(entry.absolute, entry.atime || new Date(), entry.mtime)
583 if (this[DOCHOWN](entry)) {
585 fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry))
591 [MKDIR] (dir, mode) {
593 return mkdir.sync(dir, {
596 processUid: this.processUid,
597 processGid: this.processGid,
598 umask: this.processUmask,
599 preserve: this.preservePaths,
601 cache: this.dirCache,
610 [LINK] (entry, linkpath, link) {
612 fs[link + 'Sync'](linkpath, entry.absolute)
615 return this[ONERROR](er, entry)
620 Unpack.Sync = UnpackSync
621 module.exports = Unpack