'use strict' const MiniPass = require('minipass') const Pax = require('./pax.js') const Header = require('./header.js') const fs = require('fs') const path = require('path') const maxReadSize = 16 * 1024 * 1024 const PROCESS = Symbol('process') const FILE = Symbol('file') const DIRECTORY = Symbol('directory') const SYMLINK = Symbol('symlink') const HARDLINK = Symbol('hardlink') const HEADER = Symbol('header') const READ = Symbol('read') const LSTAT = Symbol('lstat') const ONLSTAT = Symbol('onlstat') const ONREAD = Symbol('onread') const ONREADLINK = Symbol('onreadlink') const OPENFILE = Symbol('openfile') const ONOPENFILE = Symbol('onopenfile') const CLOSE = Symbol('close') const MODE = Symbol('mode') const warner = require('./warn-mixin.js') const winchars = require('./winchars.js') const modeFix = require('./mode-fix.js') const WriteEntry = warner(class WriteEntry extends MiniPass { constructor (p, opt) { opt = opt || {} super(opt) if (typeof p !== 'string') throw new TypeError('path is required') this.path = p // suppress atime, ctime, uid, gid, uname, gname this.portable = !!opt.portable // until node has builtin pwnam functions, this'll have to do this.myuid = process.getuid && process.getuid() this.myuser = process.env.USER || '' this.maxReadSize = opt.maxReadSize || maxReadSize this.linkCache = opt.linkCache || new Map() this.statCache = opt.statCache || new Map() this.preservePaths = !!opt.preservePaths this.cwd = opt.cwd || process.cwd() this.strict = !!opt.strict this.noPax = !!opt.noPax this.noMtime = !!opt.noMtime this.mtime = opt.mtime || null if (typeof opt.onwarn === 'function') this.on('warn', opt.onwarn) let pathWarn = false if (!this.preservePaths && path.win32.isAbsolute(p)) { // absolutes on posix are also absolutes on win32 // so we only need to test this one to get both const parsed = path.win32.parse(p) this.path = p.substr(parsed.root.length) pathWarn = parsed.root } this.win32 = !!opt.win32 || process.platform === 'win32' if (this.win32) { this.path = winchars.decode(this.path.replace(/\\/g, '/')) p = p.replace(/\\/g, '/') } this.absolute = opt.absolute || path.resolve(this.cwd, p) if (this.path === '') this.path = './' if (pathWarn) { this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, { entry: this, path: pathWarn + this.path, }) } if (this.statCache.has(this.absolute)) this[ONLSTAT](this.statCache.get(this.absolute)) else this[LSTAT]() } [LSTAT] () { fs.lstat(this.absolute, (er, stat) => { if (er) return this.emit('error', er) this[ONLSTAT](stat) }) } [ONLSTAT] (stat) { this.statCache.set(this.absolute, stat) this.stat = stat if (!stat.isFile()) stat.size = 0 this.type = getType(stat) this.emit('stat', stat) this[PROCESS]() } [PROCESS] () { switch (this.type) { case 'File': return this[FILE]() case 'Directory': return this[DIRECTORY]() case 'SymbolicLink': return this[SYMLINK]() // unsupported types are ignored. default: return this.end() } } [MODE] (mode) { return modeFix(mode, this.type === 'Directory', this.portable) } [HEADER] () { if (this.type === 'Directory' && this.portable) this.noMtime = true this.header = new Header({ path: this.path, linkpath: this.linkpath, // only the permissions and setuid/setgid/sticky bitflags // not the higher-order bits that specify file type mode: this[MODE](this.stat.mode), uid: this.portable ? null : this.stat.uid, gid: this.portable ? null : this.stat.gid, size: this.stat.size, mtime: this.noMtime ? null : this.mtime || this.stat.mtime, type: this.type, uname: this.portable ? null : this.stat.uid === this.myuid ? this.myuser : '', atime: this.portable ? null : this.stat.atime, ctime: this.portable ? null : this.stat.ctime, }) if (this.header.encode() && !this.noPax) { this.write(new Pax({ atime: this.portable ? null : this.header.atime, ctime: this.portable ? null : this.header.ctime, gid: this.portable ? null : this.header.gid, mtime: this.noMtime ? null : this.mtime || this.header.mtime, path: this.path, linkpath: this.linkpath, size: this.header.size, uid: this.portable ? null : this.header.uid, uname: this.portable ? null : this.header.uname, dev: this.portable ? null : this.stat.dev, ino: this.portable ? null : this.stat.ino, nlink: this.portable ? null : this.stat.nlink, }).encode()) } this.write(this.header.block) } [DIRECTORY] () { if (this.path.substr(-1) !== '/') this.path += '/' this.stat.size = 0 this[HEADER]() this.end() } [SYMLINK] () { fs.readlink(this.absolute, (er, linkpath) => { if (er) return this.emit('error', er) this[ONREADLINK](linkpath) }) } [ONREADLINK] (linkpath) { this.linkpath = linkpath.replace(/\\/g, '/') this[HEADER]() this.end() } [HARDLINK] (linkpath) { this.type = 'Link' this.linkpath = path.relative(this.cwd, linkpath).replace(/\\/g, '/') this.stat.size = 0 this[HEADER]() this.end() } [FILE] () { if (this.stat.nlink > 1) { const linkKey = this.stat.dev + ':' + this.stat.ino if (this.linkCache.has(linkKey)) { const linkpath = this.linkCache.get(linkKey) if (linkpath.indexOf(this.cwd) === 0) return this[HARDLINK](linkpath) } this.linkCache.set(linkKey, this.absolute) } this[HEADER]() if (this.stat.size === 0) return this.end() this[OPENFILE]() } [OPENFILE] () { fs.open(this.absolute, 'r', (er, fd) => { if (er) return this.emit('error', er) this[ONOPENFILE](fd) }) } [ONOPENFILE] (fd) { const blockLen = 512 * Math.ceil(this.stat.size / 512) const bufLen = Math.min(blockLen, this.maxReadSize) const buf = Buffer.allocUnsafe(bufLen) this[READ](fd, buf, 0, buf.length, 0, this.stat.size, blockLen) } [READ] (fd, buf, offset, length, pos, remain, blockRemain) { fs.read(fd, buf, offset, length, pos, (er, bytesRead) => { if (er) { // ignoring the error from close(2) is a bad practice, but at // this point we already have an error, don't need another one return this[CLOSE](fd, () => this.emit('error', er)) } this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead) }) } [CLOSE] (fd, cb) { fs.close(fd, cb) } [ONREAD] (fd, buf, offset, length, pos, remain, blockRemain, bytesRead) { if (bytesRead <= 0 && remain > 0) { const er = new Error('encountered unexpected EOF') er.path = this.absolute er.syscall = 'read' er.code = 'EOF' return this[CLOSE](fd, () => this.emit('error', er)) } if (bytesRead > remain) { const er = new Error('did not encounter expected EOF') er.path = this.absolute er.syscall = 'read' er.code = 'EOF' return this[CLOSE](fd, () => this.emit('error', er)) } // null out the rest of the buffer, if we could fit the block padding if (bytesRead === remain) { for (let i = bytesRead; i < length && bytesRead < blockRemain; i++) { buf[i + offset] = 0 bytesRead++ remain++ } } const writeBuf = offset === 0 && bytesRead === buf.length ? buf : buf.slice(offset, offset + bytesRead) remain -= bytesRead blockRemain -= bytesRead pos += bytesRead offset += bytesRead this.write(writeBuf) if (!remain) { if (blockRemain) this.write(Buffer.alloc(blockRemain)) return this[CLOSE](fd, er => er ? this.emit('error', er) : this.end()) } if (offset >= length) { buf = Buffer.allocUnsafe(length) offset = 0 } length = buf.length - offset this[READ](fd, buf, offset, length, pos, remain, blockRemain) } }) class WriteEntrySync extends WriteEntry { [LSTAT] () { this[ONLSTAT](fs.lstatSync(this.absolute)) } [SYMLINK] () { this[ONREADLINK](fs.readlinkSync(this.absolute)) } [OPENFILE] () { this[ONOPENFILE](fs.openSync(this.absolute, 'r')) } [READ] (fd, buf, offset, length, pos, remain, blockRemain) { let threw = true try { const bytesRead = fs.readSync(fd, buf, offset, length, pos) this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead) threw = false } finally { // ignoring the error from close(2) is a bad practice, but at // this point we already have an error, don't need another one if (threw) { try { this[CLOSE](fd, () => {}) } catch (er) {} } } } [CLOSE] (fd, cb) { fs.closeSync(fd) cb() } } const WriteEntryTar = warner(class WriteEntryTar extends MiniPass { constructor (readEntry, opt) { opt = opt || {} super(opt) this.preservePaths = !!opt.preservePaths this.portable = !!opt.portable this.strict = !!opt.strict this.noPax = !!opt.noPax this.noMtime = !!opt.noMtime this.readEntry = readEntry this.type = readEntry.type if (this.type === 'Directory' && this.portable) this.noMtime = true this.path = readEntry.path this.mode = this[MODE](readEntry.mode) this.uid = this.portable ? null : readEntry.uid this.gid = this.portable ? null : readEntry.gid this.uname = this.portable ? null : readEntry.uname this.gname = this.portable ? null : readEntry.gname this.size = readEntry.size this.mtime = this.noMtime ? null : opt.mtime || readEntry.mtime this.atime = this.portable ? null : readEntry.atime this.ctime = this.portable ? null : readEntry.ctime this.linkpath = readEntry.linkpath if (typeof opt.onwarn === 'function') this.on('warn', opt.onwarn) let pathWarn = false if (path.isAbsolute(this.path) && !this.preservePaths) { const parsed = path.parse(this.path) pathWarn = parsed.root this.path = this.path.substr(parsed.root.length) } this.remain = readEntry.size this.blockRemain = readEntry.startBlockSize this.header = new Header({ path: this.path, linkpath: this.linkpath, // only the permissions and setuid/setgid/sticky bitflags // not the higher-order bits that specify file type mode: this.mode, uid: this.portable ? null : this.uid, gid: this.portable ? null : this.gid, size: this.size, mtime: this.noMtime ? null : this.mtime, type: this.type, uname: this.portable ? null : this.uname, atime: this.portable ? null : this.atime, ctime: this.portable ? null : this.ctime, }) if (pathWarn) { this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, { entry: this, path: pathWarn + this.path, }) } if (this.header.encode() && !this.noPax) { super.write(new Pax({ atime: this.portable ? null : this.atime, ctime: this.portable ? null : this.ctime, gid: this.portable ? null : this.gid, mtime: this.noMtime ? null : this.mtime, path: this.path, linkpath: this.linkpath, size: this.size, uid: this.portable ? null : this.uid, uname: this.portable ? null : this.uname, dev: this.portable ? null : this.readEntry.dev, ino: this.portable ? null : this.readEntry.ino, nlink: this.portable ? null : this.readEntry.nlink, }).encode()) } super.write(this.header.block) readEntry.pipe(this) } [MODE] (mode) { return modeFix(mode, this.type === 'Directory', this.portable) } write (data) { const writeLen = data.length if (writeLen > this.blockRemain) throw new Error('writing more to entry than is appropriate') this.blockRemain -= writeLen return super.write(data) } end () { if (this.blockRemain) this.write(Buffer.alloc(this.blockRemain)) return super.end() } }) WriteEntry.Sync = WriteEntrySync WriteEntry.Tar = WriteEntryTar const getType = stat => stat.isFile() ? 'File' : stat.isDirectory() ? 'Directory' : stat.isSymbolicLink() ? 'SymbolicLink' : 'Unsupported' module.exports = WriteEntry