'use strict' const assert = require('assert') const EE = require('events').EventEmitter const Parser = require('./parse.js') const fs = require('fs') const path = require('path') const mkdir = require('./mkdir.js') const mkdirSync = mkdir.sync const wc = require('./winchars.js') const ONENTRY = Symbol('onEntry') const CHECKFS = Symbol('checkFs') const MAKEFS = Symbol('makeFs') const FILE = Symbol('file') const DIRECTORY = Symbol('directory') const LINK = Symbol('link') const SYMLINK = Symbol('symlink') const HARDLINK = Symbol('hardlink') const UNSUPPORTED = Symbol('unsupported') const UNKNOWN = Symbol('unknown') const CHECKPATH = Symbol('checkPath') const MKDIR = Symbol('mkdir') const ONERROR = Symbol('onError') const PENDING = Symbol('pending') const PEND = Symbol('pend') const UNPEND = Symbol('unpend') const ENDED = Symbol('ended') const MAYBECLOSE = Symbol('maybeClose') const SKIP = Symbol('skip') const DOCHOWN = Symbol('doChown') const UID = Symbol('uid') const GID = Symbol('gid') class Unpack extends Parser { constructor (opt) { if (!opt) opt = {} opt.ondone = _ => { this[ENDED] = true this[MAYBECLOSE]() } super(opt) this.writable = true this.readable = false this[PENDING] = 0 this[ENDED] = false this.dirCache = opt.dirCache || new Map() if (typeof opt.uid === 'number' || typeof opt.gid === 'number') { // need both or neither if (typeof opt.uid !== 'number' || typeof opt.gid !== 'number') throw new TypeError('cannot set owner without number uid and gid') if (opt.preserveOwner) throw new TypeError( 'cannot preserve owner in archive and also set owner explicitly') this.uid = opt.uid this.gid = opt.gid this.setOwner = true } else { this.uid = null this.gid = null this.setOwner = false } // default true for root if (opt.preserveOwner === undefined && typeof opt.uid !== 'number') this.preserveOwner = process.getuid && process.getuid() === 0 else this.preserveOwner = !!opt.preserveOwner this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ? process.getuid() : null this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ? process.getgid() : null // turn > this[ONENTRY](entry)) } [MAYBECLOSE] () { if (this[ENDED] && this[PENDING] === 0) { this.emit('prefinish') this.emit('finish') this.emit('end') this.emit('close') } } [CHECKPATH] (entry) { if (this.strip) { const parts = entry.path.split(/\/|\\/) if (parts.length < this.strip) return false entry.path = parts.slice(this.strip).join('/') } if (!this.preservePaths) { const p = entry.path if (p.match(/(^|\/|\\)\.\.(\\|\/|$)/)) { this.warn('path contains \'..\'', p) return false } // absolutes on posix are also absolutes on win32 // so we only need to test this one to get both if (path.win32.isAbsolute(p)) { const parsed = path.win32.parse(p) this.warn('stripping ' + parsed.root + ' from absolute path', p) entry.path = p.substr(parsed.root.length) } } // only encode : chars that aren't drive letter indicators if (this.win32) { const parsed = path.win32.parse(entry.path) entry.path = parsed.root === '' ? wc.encode(entry.path) : parsed.root + wc.encode(entry.path.substr(parsed.root.length)) } if (path.isAbsolute(entry.path)) entry.absolute = entry.path else entry.absolute = path.resolve(this.cwd, entry.path) return true } [ONENTRY] (entry) { if (!this[CHECKPATH](entry)) return entry.resume() assert.equal(typeof entry.absolute, 'string') switch (entry.type) { case 'Directory': case 'GNUDumpDir': if (entry.mode) entry.mode = entry.mode | 0o700 case 'File': case 'OldFile': case 'ContiguousFile': case 'Link': case 'SymbolicLink': return this[CHECKFS](entry) case 'CharacterDevice': case 'BlockDevice': case 'FIFO': return this[UNSUPPORTED](entry) } } [ONERROR] (er, entry) { this.warn(er.message, er) this[UNPEND]() entry.resume() } [MKDIR] (dir, mode, cb) { mkdir(dir, { uid: this.uid, gid: this.gid, processUid: this.processUid, processGid: this.processGid, umask: this.processUmask, preserve: this.preservePaths, unlink: this.unlink, cache: this.dirCache, cwd: this.cwd, mode: mode }, cb) } [DOCHOWN] (entry) { // in preserve owner mode, chown if the entry doesn't match process // in set owner mode, chown if setting doesn't match process return this.preserveOwner && ( typeof entry.uid === 'number' && entry.uid !== this.processUid || typeof entry.gid === 'number' && entry.gid !== this.processGid ) || ( typeof this.uid === 'number' && this.uid !== this.processUid || typeof this.gid === 'number' && this.gid !== this.processGid ) } [UID] (entry) { return typeof this.uid === 'number' ? this.uid : typeof entry.uid === 'number' ? entry.uid : this.processUid } [GID] (entry) { return typeof this.gid === 'number' ? this.gid : typeof entry.gid === 'number' ? entry.gid : this.processGid } [FILE] (entry) { const mode = entry.mode & 0o7777 || this.fmode const stream = fs.createWriteStream(entry.absolute, { mode: mode }) stream.on('error', er => this[ONERROR](er, entry)) const queue = [] const processQueue = _ => { const action = queue.shift() if (action) action(processQueue) else this[UNPEND]() } stream.on('close', _ => { if (entry.mtime && !this.noMtime) queue.push(cb => fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, cb)) if (this[DOCHOWN](entry)) queue.push(cb => fs.chown(entry.absolute, this[UID](entry), this[GID](entry), cb)) processQueue() }) entry.pipe(stream) } [DIRECTORY] (entry) { const mode = entry.mode & 0o7777 || this.dmode this[MKDIR](entry.absolute, mode, er => { if (er) return this[ONERROR](er, entry) const queue = [] const processQueue = _ => { const action = queue.shift() if (action) action(processQueue) else { this[UNPEND]() entry.resume() } } if (entry.mtime && !this.noMtime) queue.push(cb => fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, cb)) if (this[DOCHOWN](entry)) queue.push(cb => fs.chown(entry.absolute, this[UID](entry), this[GID](entry), cb)) processQueue() }) } [UNSUPPORTED] (entry) { this.warn('unsupported entry type: ' + entry.type, entry) entry.resume() } [SYMLINK] (entry) { this[LINK](entry, entry.linkpath, 'symlink') } [HARDLINK] (entry) { this[LINK](entry, path.resolve(this.cwd, entry.linkpath), 'link') } [PEND] () { this[PENDING]++ } [UNPEND] () { this[PENDING]-- this[MAYBECLOSE]() } [SKIP] (entry) { this[UNPEND]() entry.resume() } // check if a thing is there, and if so, try to clobber it [CHECKFS] (entry) { this[PEND]() this[MKDIR](path.dirname(entry.absolute), this.dmode, er => { if (er) return this[ONERROR](er, entry) fs.lstat(entry.absolute, (er, st) => { if (st && (this.keep || this.newer && st.mtime > entry.mtime)) this[SKIP](entry) else if (er || (entry.type === 'File' && !this.unlink && st.isFile())) this[MAKEFS](null, entry) else if (st.isDirectory()) { if (entry.type === 'Directory') { if (!entry.mode || (st.mode & 0o7777) === entry.mode) this[MAKEFS](null, entry) else fs.chmod(entry.absolute, entry.mode, er => this[MAKEFS](er, entry)) } else fs.rmdir(entry.absolute, er => this[MAKEFS](er, entry)) } else fs.unlink(entry.absolute, er => this[MAKEFS](er, entry)) }) }) } [MAKEFS] (er, entry) { if (er) return this[ONERROR](er, entry) switch (entry.type) { case 'File': case 'OldFile': case 'ContiguousFile': return this[FILE](entry) case 'Link': return this[HARDLINK](entry) case 'SymbolicLink': return this[SYMLINK](entry) case 'Directory': case 'GNUDumpDir': return this[DIRECTORY](entry) } } [LINK] (entry, linkpath, link) { // XXX: get the type ('file' or 'dir') for windows fs[link](linkpath, entry.absolute, er => { if (er) return this[ONERROR](er, entry) this[UNPEND]() entry.resume() }) } } class UnpackSync extends Unpack { constructor (opt) { super(opt) } [CHECKFS] (entry) { const er = this[MKDIR](path.dirname(entry.absolute), this.dmode) if (er) return this[ONERROR](er, entry) try { const st = fs.lstatSync(entry.absolute) if (this.keep || this.newer && st.mtime > entry.mtime) return this[SKIP](entry) else if (entry.type === 'File' && !this.unlink && st.isFile()) return this[MAKEFS](null, entry) else { try { if (st.isDirectory()) { if (entry.type === 'Directory') { if (entry.mode && (st.mode & 0o7777) !== entry.mode) fs.chmodSync(entry.absolute, entry.mode) } else fs.rmdirSync(entry.absolute) } else fs.unlinkSync(entry.absolute) return this[MAKEFS](null, entry) } catch (er) { return this[ONERROR](er, entry) } } } catch (er) { return this[MAKEFS](null, entry) } } [FILE] (entry) { const mode = entry.mode & 0o7777 || this.fmode try { const fd = fs.openSync(entry.absolute, 'w', mode) entry.on('data', buf => fs.writeSync(fd, buf, 0, buf.length, null)) entry.on('end', _ => { if (entry.mtime && !this.noMtime) { try { fs.futimesSync(fd, entry.atime || new Date(), entry.mtime) } catch (er) {} } if (this[DOCHOWN](entry)) { try { fs.fchownSync(fd, this[UID](entry), this[GID](entry)) } catch (er) {} } try { fs.closeSync(fd) } catch (er) { this[ONERROR](er, entry) } }) } catch (er) { this[ONERROR](er, entry) } } [DIRECTORY] (entry) { const mode = entry.mode & 0o7777 || this.dmode const er = this[MKDIR](entry.absolute, mode) if (er) return this[ONERROR](er, entry) if (entry.mtime && !this.noMtime) { try { fs.utimesSync(entry.absolute, entry.atime || new Date(), entry.mtime) } catch (er) {} } if (this[DOCHOWN](entry)) { try { fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry)) } catch (er) {} } entry.resume() } [MKDIR] (dir, mode) { try { return mkdir.sync(dir, { uid: this.uid, gid: this.gid, processUid: this.processUid, processGid: this.processGid, umask: this.processUmask, preserve: this.preservePaths, unlink: this.unlink, cache: this.dirCache, cwd: this.cwd, mode: mode }) } catch (er) { return er } } [LINK] (entry, linkpath, link) { try { fs[link + 'Sync'](linkpath, entry.absolute) entry.resume() } catch (er) { return this[ONERROR](er, entry) } } } Unpack.Sync = UnpackSync module.exports = Unpack