'use strict' const MiniPass = require('minipass') const Pax = require('./pax.js') const Header = require('./header.js') const ReadEntry = require('./read-entry.js') const fs = require('fs') const path = require('path') const types = require('./types.js') 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 warner = require('./warn-mixin.js') const winchars = require('./winchars.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 if (typeof opt.onwarn === 'function') this.on('warn', opt.onwarn) 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.warn('stripping ' + parsed.root + ' from absolute path', p) this.path = p.substr(parsed.root.length) } 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 (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() } } [HEADER] () { 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.stat.mode & 0o7777, uid: this.portable ? null : this.stat.uid, gid: this.portable ? null : this.stat.gid, size: this.stat.size, mtime: this.type === 'Directory' && this.portable ? null : 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.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 this[HEADER]() this.end() } [HARDLINK] (linkpath) { this.type = 'Link' this.linkpath = path.relative(this.cwd, linkpath) 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) 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('unexpected EOF') er.path = this.absolute er.syscall = 'read' er.code = 'EOF' 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)) this.end() this[CLOSE](fd, _ => _) return } 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 { constructor (path, opt) { super(path, opt) } [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 { if (threw) try { this[CLOSE](fd) } catch (er) {} } } [CLOSE] (fd) { fs.closeSync(fd) } } const WriteEntryTar = warner(class WriteEntryTar extends MiniPass { constructor (readEntry, opt) { opt = opt || {} super(opt) this.readEntry = readEntry this.type = readEntry.type this.path = readEntry.path this.mode = readEntry.mode if (this.mode) this.mode = this.mode & 0o7777 this.uid = readEntry.uid this.gid = readEntry.gid this.uname = readEntry.uname this.gname = readEntry.gname this.size = readEntry.size this.mtime = readEntry.mtime this.atime = readEntry.atime this.ctime = readEntry.ctime this.linkpath = readEntry.linkpath this.uname = readEntry.uname this.gname = readEntry.gname this.preservePaths = !!opt.preservePaths this.portable = !!opt.portable this.strict = !!opt.strict this.noPax = !!opt.noPax if (typeof opt.onwarn === 'function') this.on('warn', opt.onwarn) if (path.isAbsolute(this.path) && !this.preservePaths) { const parsed = path.parse(this.path) this.warn( 'stripping ' + parsed.root + ' from absolute path', this.path ) 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.mtime, type: this.type, uname: this.portable ? null : this.uname, atime: this.portable ? null : this.atime, ctime: this.portable ? null : this.ctime }) 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.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) } 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