123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- '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 normPath = require('./normalize-windows-path.js')
- const stripSlash = require('./strip-trailing-slashes.js')
-
- const prefixPath = (path, prefix) => {
- if (!prefix) {
- return normPath(path)
- }
- path = normPath(path).replace(/^\.(\/|$)/, '')
- return stripSlash(prefix) + '/' + 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 AWAITDRAIN = Symbol('awaitDrain')
- const ONDRAIN = Symbol('ondrain')
- const PREFIX = Symbol('prefix')
- const HAD_ERROR = Symbol('hadError')
- const warner = require('./warn-mixin.js')
- const winchars = require('./winchars.js')
- const stripAbsolutePath = require('./strip-absolute-path.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 = normPath(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() || 0
- 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 = normPath(opt.cwd || process.cwd())
- this.strict = !!opt.strict
- this.noPax = !!opt.noPax
- this.noMtime = !!opt.noMtime
- this.mtime = opt.mtime || null
- this.prefix = opt.prefix ? normPath(opt.prefix) : null
-
- this.fd = null
- this.blockLen = null
- this.blockRemain = null
- this.buf = null
- this.offset = null
- this.length = null
- this.pos = null
- this.remain = null
-
- if (typeof opt.onwarn === 'function') {
- this.on('warn', opt.onwarn)
- }
-
- let pathWarn = false
- if (!this.preservePaths) {
- const [root, stripped] = stripAbsolutePath(this.path)
- if (root) {
- this.path = stripped
- pathWarn = root
- }
- }
-
- this.win32 = !!opt.win32 || process.platform === 'win32'
- if (this.win32) {
- // force the \ to / normalization, since we might not *actually*
- // be on windows, but want \ to be considered a path separator.
- this.path = winchars.decode(this.path.replace(/\\/g, '/'))
- p = p.replace(/\\/g, '/')
- }
-
- this.absolute = normPath(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]()
- }
- }
-
- emit (ev, ...data) {
- if (ev === 'error') {
- this[HAD_ERROR] = true
- }
- return super.emit(ev, ...data)
- }
-
- [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)
- }
-
- [PREFIX] (path) {
- return prefixPath(path, this.prefix)
- }
-
- [HEADER] () {
- if (this.type === 'Directory' && this.portable) {
- this.noMtime = true
- }
-
- this.header = new Header({
- path: this[PREFIX](this.path),
- // only apply the prefix to hard links.
- linkpath: this.type === 'Link' ? this[PREFIX](this.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) {
- super.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[PREFIX](this.path),
- linkpath: this.type === 'Link' ? this[PREFIX](this.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())
- }
- super.write(this.header.block)
- }
-
- [DIRECTORY] () {
- if (this.path.slice(-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 = normPath(linkpath)
- this[HEADER]()
- this.end()
- }
-
- [HARDLINK] (linkpath) {
- this.type = 'Link'
- this.linkpath = normPath(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) {
- this.fd = fd
- if (this[HAD_ERROR]) {
- return this[CLOSE]()
- }
-
- this.blockLen = 512 * Math.ceil(this.stat.size / 512)
- this.blockRemain = this.blockLen
- const bufLen = Math.min(this.blockLen, this.maxReadSize)
- this.buf = Buffer.allocUnsafe(bufLen)
- this.offset = 0
- this.pos = 0
- this.remain = this.stat.size
- this.length = this.buf.length
- this[READ]()
- }
-
- [READ] () {
- const { fd, buf, offset, length, pos } = this
- 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](() => this.emit('error', er))
- }
- this[ONREAD](bytesRead)
- })
- }
-
- [CLOSE] (cb) {
- fs.close(this.fd, cb)
- }
-
- [ONREAD] (bytesRead) {
- if (bytesRead <= 0 && this.remain > 0) {
- const er = new Error('encountered unexpected EOF')
- er.path = this.absolute
- er.syscall = 'read'
- er.code = 'EOF'
- return this[CLOSE](() => this.emit('error', er))
- }
-
- if (bytesRead > this.remain) {
- const er = new Error('did not encounter expected EOF')
- er.path = this.absolute
- er.syscall = 'read'
- er.code = 'EOF'
- return this[CLOSE](() => this.emit('error', er))
- }
-
- // null out the rest of the buffer, if we could fit the block padding
- // at the end of this loop, we've incremented bytesRead and this.remain
- // to be incremented up to the blockRemain level, as if we had expected
- // to get a null-padded file, and read it until the end. then we will
- // decrement both remain and blockRemain by bytesRead, and know that we
- // reached the expected EOF, without any null buffer to append.
- if (bytesRead === this.remain) {
- for (let i = bytesRead; i < this.length && bytesRead < this.blockRemain; i++) {
- this.buf[i + this.offset] = 0
- bytesRead++
- this.remain++
- }
- }
-
- const writeBuf = this.offset === 0 && bytesRead === this.buf.length ?
- this.buf : this.buf.slice(this.offset, this.offset + bytesRead)
-
- const flushed = this.write(writeBuf)
- if (!flushed) {
- this[AWAITDRAIN](() => this[ONDRAIN]())
- } else {
- this[ONDRAIN]()
- }
- }
-
- [AWAITDRAIN] (cb) {
- this.once('drain', cb)
- }
-
- write (writeBuf) {
- if (this.blockRemain < writeBuf.length) {
- const er = new Error('writing more data than expected')
- er.path = this.absolute
- return this.emit('error', er)
- }
- this.remain -= writeBuf.length
- this.blockRemain -= writeBuf.length
- this.pos += writeBuf.length
- this.offset += writeBuf.length
- return super.write(writeBuf)
- }
-
- [ONDRAIN] () {
- if (!this.remain) {
- if (this.blockRemain) {
- super.write(Buffer.alloc(this.blockRemain))
- }
- return this[CLOSE](er => er ? this.emit('error', er) : this.end())
- }
-
- if (this.offset >= this.length) {
- // if we only have a smaller bit left to read, alloc a smaller buffer
- // otherwise, keep it the same length it was before.
- this.buf = Buffer.allocUnsafe(Math.min(this.blockRemain, this.buf.length))
- this.offset = 0
- }
- this.length = this.buf.length - this.offset
- this[READ]()
- }
- })
-
- 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] () {
- let threw = true
- try {
- const { fd, buf, offset, length, pos } = this
- const bytesRead = fs.readSync(fd, buf, offset, length, pos)
- this[ONREAD](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](() => {})
- } catch (er) {}
- }
- }
- }
-
- [AWAITDRAIN] (cb) {
- cb()
- }
-
- [CLOSE] (cb) {
- fs.closeSync(this.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.prefix = opt.prefix || null
-
- this.path = normPath(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 = normPath(readEntry.linkpath)
-
- if (typeof opt.onwarn === 'function') {
- this.on('warn', opt.onwarn)
- }
-
- let pathWarn = false
- if (!this.preservePaths) {
- const [root, stripped] = stripAbsolutePath(this.path)
- if (root) {
- this.path = stripped
- pathWarn = root
- }
- }
-
- this.remain = readEntry.size
- this.blockRemain = readEntry.startBlockSize
-
- this.header = new Header({
- path: this[PREFIX](this.path),
- linkpath: this.type === 'Link' ? this[PREFIX](this.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[PREFIX](this.path),
- linkpath: this.type === 'Link' ? this[PREFIX](this.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)
- }
-
- [PREFIX] (path) {
- return prefixPath(path, this.prefix)
- }
-
- [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) {
- super.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
|