D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
usr
/
lib
/
node_modules
/
npm
/
node_modules
/
tar
/
lib
/
Filename :
unpack.js
back
Copy
'use strict' const assert = require('assert') const EE = require('events').EventEmitter const Parser = require('./parse.js') const fs = require('fs') const fsm = require('fs-minipass') 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 ISREUSABLE = Symbol('isReusable') 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') const crypto = require('crypto') // Unlinks on Windows are not atomic. // // This means that if you have a file entry, followed by another // file entry with an identical name, and you cannot re-use the file // (because it's a hardlink, or because unlink:true is set, or it's // Windows, which does not have useful nlink values), then the unlink // will be committed to the disk AFTER the new file has been written // over the old one, deleting the new file. // // To work around this, on Windows systems, we rename the file and then // delete the renamed file. It's a sloppy kludge, but frankly, I do not // know of a better way to do this, given windows' non-atomic unlink // semantics. // // See: https://github.com/npm/node-tar/issues/183 /* istanbul ignore next */ const unlinkFile = (path, cb) => { if (process.platform !== 'win32') return fs.unlink(path, cb) const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex') fs.rename(path, name, er => { if (er) return cb(er) fs.unlink(name, cb) }) } /* istanbul ignore next */ const unlinkFileSync = path => { if (process.platform !== 'win32') return fs.unlinkSync(path) const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex') fs.renameSync(path, name) fs.unlinkSync(name) } // this.gid, entry.gid, this.processUid const uint32 = (a, b, c) => a === a >>> 0 ? a : b === b >>> 0 ? b : c class Unpack extends Parser { constructor (opt) { if (!opt) opt = {} opt.ondone = _ => { this[ENDED] = true this[MAYBECLOSE]() } super(opt) this.transform = typeof opt.transform === 'function' ? opt.transform : null 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 // mostly just for testing, but useful in some cases. // Forcibly trigger a chown on every entry, no matter what this.forceChown = opt.forceChown === true // turn ><?| in filenames into 0xf000-higher encoded forms this.win32 = !!opt.win32 || process.platform === 'win32' // do not unpack over files that are newer than what's in the archive this.newer = !!opt.newer // do not unpack over ANY files this.keep = !!opt.keep // do not set mtime/atime of extracted entries this.noMtime = !!opt.noMtime // allow .., absolute path entries, and unpacking through symlinks // without this, warn and skip .., relativize absolutes, and error // on symlinks in extraction path this.preservePaths = !!opt.preservePaths // unlink files and links before writing. This breaks existing hard // links, and removes symlink directories rather than erroring this.unlink = !!opt.unlink this.cwd = path.resolve(opt.cwd || process.cwd()) this.strip = +opt.strip || 0 this.processUmask = process.umask() this.umask = typeof opt.umask === 'number' ? opt.umask : this.processUmask // default mode for dirs created as parents this.dmode = opt.dmode || (0o0777 & (~this.umask)) this.fmode = opt.fmode || (0o0666 & (~this.umask)) this.on('entry', entry => 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 (entry.type === 'Link') { const linkparts = entry.linkpath.split(/\/|\\/) if (linkparts.length >= this.strip) entry.linkpath = linkparts.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) { // Cwd has to exist, or else nothing works. That's serious. // Other errors are warnings, which raise the error in strict // mode, but otherwise continue on. if (er.name === 'CwdError') this.emit('error', er) else { 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.forceChown || 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 uint32(this.uid, entry.uid, this.processUid) } [GID] (entry) { return uint32(this.gid, entry.gid, this.processGid) } [FILE] (entry) { const mode = entry.mode & 0o7777 || this.fmode const stream = new fsm.WriteStream(entry.absolute, { mode: mode, autoClose: false }) stream.on('error', er => this[ONERROR](er, entry)) let actions = 1 const done = er => { if (er) return this[ONERROR](er, entry) if (--actions === 0) fs.close(stream.fd, _ => this[UNPEND]()) } stream.on('finish', _ => { // if futimes fails, try utimes // if utimes fails, fail with the original error // same for fchown/chown const abs = entry.absolute const fd = stream.fd if (entry.mtime && !this.noMtime) { actions++ const atime = entry.atime || new Date() const mtime = entry.mtime fs.futimes(fd, atime, mtime, er => er ? fs.utimes(abs, atime, mtime, er2 => done(er2 && er)) : done()) } if (this[DOCHOWN](entry)) { actions++ const uid = this[UID](entry) const gid = this[GID](entry) fs.fchown(fd, uid, gid, er => er ? fs.chown(abs, uid, gid, er2 => done(er2 && er)) : done()) } done() }) const tx = this.transform ? this.transform(entry) || entry : entry if (tx !== entry) { tx.on('error', er => this[ONERROR](er, entry)) entry.pipe(tx) } tx.pipe(stream) } [DIRECTORY] (entry) { const mode = entry.mode & 0o7777 || this.dmode this[MKDIR](entry.absolute, mode, er => { if (er) return this[ONERROR](er, entry) let actions = 1 const done = _ => { if (--actions === 0) { this[UNPEND]() entry.resume() } } if (entry.mtime && !this.noMtime) { actions++ fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, done) } if (this[DOCHOWN](entry)) { actions++ fs.chown(entry.absolute, this[UID](entry), this[GID](entry), done) } done() }) } [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 we can reuse an existing filesystem entry safely and // overwrite it, rather than unlinking and recreating // Windows doesn't report a useful nlink, so we just never reuse entries [ISREUSABLE] (entry, st) { return entry.type === 'File' && !this.unlink && st.isFile() && st.nlink <= 1 && process.platform !== 'win32' } // 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 || this[ISREUSABLE](entry, st)) 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 unlinkFile(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 (this[ISREUSABLE](entry, st)) 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 unlinkFileSync(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 const oner = er => { try { fs.closeSync(fd) } catch (_) {} if (er) this[ONERROR](er, entry) } let stream let fd try { fd = fs.openSync(entry.absolute, 'w', mode) } catch (er) { return oner(er) } const tx = this.transform ? this.transform(entry) || entry : entry if (tx !== entry) { tx.on('error', er => this[ONERROR](er, entry)) entry.pipe(tx) } tx.on('data', chunk => { try { fs.writeSync(fd, chunk, 0, chunk.length) } catch (er) { oner(er) } }) tx.on('end', _ => { let er = null // try both, falling futimes back to utimes // if either fails, handle the first error if (entry.mtime && !this.noMtime) { const atime = entry.atime || new Date() const mtime = entry.mtime try { fs.futimesSync(fd, atime, mtime) } catch (futimeser) { try { fs.utimesSync(entry.absolute, atime, mtime) } catch (utimeser) { er = futimeser } } } if (this[DOCHOWN](entry)) { const uid = this[UID](entry) const gid = this[GID](entry) try { fs.fchownSync(fd, uid, gid) } catch (fchowner) { try { fs.chownSync(entry.absolute, uid, gid) } catch (chowner) { er = er || fchowner } } } oner(er) }) } [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