1'use strict' 2const { Minipass } = require('minipass') 3const Pax = require('./pax.js') 4const Header = require('./header.js') 5const fs = require('fs') 6const path = require('path') 7const normPath = require('./normalize-windows-path.js') 8const stripSlash = require('./strip-trailing-slashes.js') 9 10const prefixPath = (path, prefix) => { 11 if (!prefix) { 12 return normPath(path) 13 } 14 path = normPath(path).replace(/^\.(\/|$)/, '') 15 return stripSlash(prefix) + '/' + path 16} 17 18const maxReadSize = 16 * 1024 * 1024 19const PROCESS = Symbol('process') 20const FILE = Symbol('file') 21const DIRECTORY = Symbol('directory') 22const SYMLINK = Symbol('symlink') 23const HARDLINK = Symbol('hardlink') 24const HEADER = Symbol('header') 25const READ = Symbol('read') 26const LSTAT = Symbol('lstat') 27const ONLSTAT = Symbol('onlstat') 28const ONREAD = Symbol('onread') 29const ONREADLINK = Symbol('onreadlink') 30const OPENFILE = Symbol('openfile') 31const ONOPENFILE = Symbol('onopenfile') 32const CLOSE = Symbol('close') 33const MODE = Symbol('mode') 34const AWAITDRAIN = Symbol('awaitDrain') 35const ONDRAIN = Symbol('ondrain') 36const PREFIX = Symbol('prefix') 37const HAD_ERROR = Symbol('hadError') 38const warner = require('./warn-mixin.js') 39const winchars = require('./winchars.js') 40const stripAbsolutePath = require('./strip-absolute-path.js') 41 42const modeFix = require('./mode-fix.js') 43 44const WriteEntry = warner(class WriteEntry extends Minipass { 45 constructor (p, opt) { 46 opt = opt || {} 47 super(opt) 48 if (typeof p !== 'string') { 49 throw new TypeError('path is required') 50 } 51 this.path = normPath(p) 52 // suppress atime, ctime, uid, gid, uname, gname 53 this.portable = !!opt.portable 54 // until node has builtin pwnam functions, this'll have to do 55 this.myuid = process.getuid && process.getuid() || 0 56 this.myuser = process.env.USER || '' 57 this.maxReadSize = opt.maxReadSize || maxReadSize 58 this.linkCache = opt.linkCache || new Map() 59 this.statCache = opt.statCache || new Map() 60 this.preservePaths = !!opt.preservePaths 61 this.cwd = normPath(opt.cwd || process.cwd()) 62 this.strict = !!opt.strict 63 this.noPax = !!opt.noPax 64 this.noMtime = !!opt.noMtime 65 this.mtime = opt.mtime || null 66 this.prefix = opt.prefix ? normPath(opt.prefix) : null 67 68 this.fd = null 69 this.blockLen = null 70 this.blockRemain = null 71 this.buf = null 72 this.offset = null 73 this.length = null 74 this.pos = null 75 this.remain = null 76 77 if (typeof opt.onwarn === 'function') { 78 this.on('warn', opt.onwarn) 79 } 80 81 let pathWarn = false 82 if (!this.preservePaths) { 83 const [root, stripped] = stripAbsolutePath(this.path) 84 if (root) { 85 this.path = stripped 86 pathWarn = root 87 } 88 } 89 90 this.win32 = !!opt.win32 || process.platform === 'win32' 91 if (this.win32) { 92 // force the \ to / normalization, since we might not *actually* 93 // be on windows, but want \ to be considered a path separator. 94 this.path = winchars.decode(this.path.replace(/\\/g, '/')) 95 p = p.replace(/\\/g, '/') 96 } 97 98 this.absolute = normPath(opt.absolute || path.resolve(this.cwd, p)) 99 100 if (this.path === '') { 101 this.path = './' 102 } 103 104 if (pathWarn) { 105 this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, { 106 entry: this, 107 path: pathWarn + this.path, 108 }) 109 } 110 111 if (this.statCache.has(this.absolute)) { 112 this[ONLSTAT](this.statCache.get(this.absolute)) 113 } else { 114 this[LSTAT]() 115 } 116 } 117 118 emit (ev, ...data) { 119 if (ev === 'error') { 120 this[HAD_ERROR] = true 121 } 122 return super.emit(ev, ...data) 123 } 124 125 [LSTAT] () { 126 fs.lstat(this.absolute, (er, stat) => { 127 if (er) { 128 return this.emit('error', er) 129 } 130 this[ONLSTAT](stat) 131 }) 132 } 133 134 [ONLSTAT] (stat) { 135 this.statCache.set(this.absolute, stat) 136 this.stat = stat 137 if (!stat.isFile()) { 138 stat.size = 0 139 } 140 this.type = getType(stat) 141 this.emit('stat', stat) 142 this[PROCESS]() 143 } 144 145 [PROCESS] () { 146 switch (this.type) { 147 case 'File': return this[FILE]() 148 case 'Directory': return this[DIRECTORY]() 149 case 'SymbolicLink': return this[SYMLINK]() 150 // unsupported types are ignored. 151 default: return this.end() 152 } 153 } 154 155 [MODE] (mode) { 156 return modeFix(mode, this.type === 'Directory', this.portable) 157 } 158 159 [PREFIX] (path) { 160 return prefixPath(path, this.prefix) 161 } 162 163 [HEADER] () { 164 if (this.type === 'Directory' && this.portable) { 165 this.noMtime = true 166 } 167 168 this.header = new Header({ 169 path: this[PREFIX](this.path), 170 // only apply the prefix to hard links. 171 linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath) 172 : this.linkpath, 173 // only the permissions and setuid/setgid/sticky bitflags 174 // not the higher-order bits that specify file type 175 mode: this[MODE](this.stat.mode), 176 uid: this.portable ? null : this.stat.uid, 177 gid: this.portable ? null : this.stat.gid, 178 size: this.stat.size, 179 mtime: this.noMtime ? null : this.mtime || this.stat.mtime, 180 type: this.type, 181 uname: this.portable ? null : 182 this.stat.uid === this.myuid ? this.myuser : '', 183 atime: this.portable ? null : this.stat.atime, 184 ctime: this.portable ? null : this.stat.ctime, 185 }) 186 187 if (this.header.encode() && !this.noPax) { 188 super.write(new Pax({ 189 atime: this.portable ? null : this.header.atime, 190 ctime: this.portable ? null : this.header.ctime, 191 gid: this.portable ? null : this.header.gid, 192 mtime: this.noMtime ? null : this.mtime || this.header.mtime, 193 path: this[PREFIX](this.path), 194 linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath) 195 : this.linkpath, 196 size: this.header.size, 197 uid: this.portable ? null : this.header.uid, 198 uname: this.portable ? null : this.header.uname, 199 dev: this.portable ? null : this.stat.dev, 200 ino: this.portable ? null : this.stat.ino, 201 nlink: this.portable ? null : this.stat.nlink, 202 }).encode()) 203 } 204 super.write(this.header.block) 205 } 206 207 [DIRECTORY] () { 208 if (this.path.slice(-1) !== '/') { 209 this.path += '/' 210 } 211 this.stat.size = 0 212 this[HEADER]() 213 this.end() 214 } 215 216 [SYMLINK] () { 217 fs.readlink(this.absolute, (er, linkpath) => { 218 if (er) { 219 return this.emit('error', er) 220 } 221 this[ONREADLINK](linkpath) 222 }) 223 } 224 225 [ONREADLINK] (linkpath) { 226 this.linkpath = normPath(linkpath) 227 this[HEADER]() 228 this.end() 229 } 230 231 [HARDLINK] (linkpath) { 232 this.type = 'Link' 233 this.linkpath = normPath(path.relative(this.cwd, linkpath)) 234 this.stat.size = 0 235 this[HEADER]() 236 this.end() 237 } 238 239 [FILE] () { 240 if (this.stat.nlink > 1) { 241 const linkKey = this.stat.dev + ':' + this.stat.ino 242 if (this.linkCache.has(linkKey)) { 243 const linkpath = this.linkCache.get(linkKey) 244 if (linkpath.indexOf(this.cwd) === 0) { 245 return this[HARDLINK](linkpath) 246 } 247 } 248 this.linkCache.set(linkKey, this.absolute) 249 } 250 251 this[HEADER]() 252 if (this.stat.size === 0) { 253 return this.end() 254 } 255 256 this[OPENFILE]() 257 } 258 259 [OPENFILE] () { 260 fs.open(this.absolute, 'r', (er, fd) => { 261 if (er) { 262 return this.emit('error', er) 263 } 264 this[ONOPENFILE](fd) 265 }) 266 } 267 268 [ONOPENFILE] (fd) { 269 this.fd = fd 270 if (this[HAD_ERROR]) { 271 return this[CLOSE]() 272 } 273 274 this.blockLen = 512 * Math.ceil(this.stat.size / 512) 275 this.blockRemain = this.blockLen 276 const bufLen = Math.min(this.blockLen, this.maxReadSize) 277 this.buf = Buffer.allocUnsafe(bufLen) 278 this.offset = 0 279 this.pos = 0 280 this.remain = this.stat.size 281 this.length = this.buf.length 282 this[READ]() 283 } 284 285 [READ] () { 286 const { fd, buf, offset, length, pos } = this 287 fs.read(fd, buf, offset, length, pos, (er, bytesRead) => { 288 if (er) { 289 // ignoring the error from close(2) is a bad practice, but at 290 // this point we already have an error, don't need another one 291 return this[CLOSE](() => this.emit('error', er)) 292 } 293 this[ONREAD](bytesRead) 294 }) 295 } 296 297 [CLOSE] (cb) { 298 fs.close(this.fd, cb) 299 } 300 301 [ONREAD] (bytesRead) { 302 if (bytesRead <= 0 && this.remain > 0) { 303 const er = new Error('encountered unexpected EOF') 304 er.path = this.absolute 305 er.syscall = 'read' 306 er.code = 'EOF' 307 return this[CLOSE](() => this.emit('error', er)) 308 } 309 310 if (bytesRead > this.remain) { 311 const er = new Error('did not encounter expected EOF') 312 er.path = this.absolute 313 er.syscall = 'read' 314 er.code = 'EOF' 315 return this[CLOSE](() => this.emit('error', er)) 316 } 317 318 // null out the rest of the buffer, if we could fit the block padding 319 // at the end of this loop, we've incremented bytesRead and this.remain 320 // to be incremented up to the blockRemain level, as if we had expected 321 // to get a null-padded file, and read it until the end. then we will 322 // decrement both remain and blockRemain by bytesRead, and know that we 323 // reached the expected EOF, without any null buffer to append. 324 if (bytesRead === this.remain) { 325 for (let i = bytesRead; i < this.length && bytesRead < this.blockRemain; i++) { 326 this.buf[i + this.offset] = 0 327 bytesRead++ 328 this.remain++ 329 } 330 } 331 332 const writeBuf = this.offset === 0 && bytesRead === this.buf.length ? 333 this.buf : this.buf.slice(this.offset, this.offset + bytesRead) 334 335 const flushed = this.write(writeBuf) 336 if (!flushed) { 337 this[AWAITDRAIN](() => this[ONDRAIN]()) 338 } else { 339 this[ONDRAIN]() 340 } 341 } 342 343 [AWAITDRAIN] (cb) { 344 this.once('drain', cb) 345 } 346 347 write (writeBuf) { 348 if (this.blockRemain < writeBuf.length) { 349 const er = new Error('writing more data than expected') 350 er.path = this.absolute 351 return this.emit('error', er) 352 } 353 this.remain -= writeBuf.length 354 this.blockRemain -= writeBuf.length 355 this.pos += writeBuf.length 356 this.offset += writeBuf.length 357 return super.write(writeBuf) 358 } 359 360 [ONDRAIN] () { 361 if (!this.remain) { 362 if (this.blockRemain) { 363 super.write(Buffer.alloc(this.blockRemain)) 364 } 365 return this[CLOSE](er => er ? this.emit('error', er) : this.end()) 366 } 367 368 if (this.offset >= this.length) { 369 // if we only have a smaller bit left to read, alloc a smaller buffer 370 // otherwise, keep it the same length it was before. 371 this.buf = Buffer.allocUnsafe(Math.min(this.blockRemain, this.buf.length)) 372 this.offset = 0 373 } 374 this.length = this.buf.length - this.offset 375 this[READ]() 376 } 377}) 378 379class WriteEntrySync extends WriteEntry { 380 [LSTAT] () { 381 this[ONLSTAT](fs.lstatSync(this.absolute)) 382 } 383 384 [SYMLINK] () { 385 this[ONREADLINK](fs.readlinkSync(this.absolute)) 386 } 387 388 [OPENFILE] () { 389 this[ONOPENFILE](fs.openSync(this.absolute, 'r')) 390 } 391 392 [READ] () { 393 let threw = true 394 try { 395 const { fd, buf, offset, length, pos } = this 396 const bytesRead = fs.readSync(fd, buf, offset, length, pos) 397 this[ONREAD](bytesRead) 398 threw = false 399 } finally { 400 // ignoring the error from close(2) is a bad practice, but at 401 // this point we already have an error, don't need another one 402 if (threw) { 403 try { 404 this[CLOSE](() => {}) 405 } catch (er) {} 406 } 407 } 408 } 409 410 [AWAITDRAIN] (cb) { 411 cb() 412 } 413 414 [CLOSE] (cb) { 415 fs.closeSync(this.fd) 416 cb() 417 } 418} 419 420const WriteEntryTar = warner(class WriteEntryTar extends Minipass { 421 constructor (readEntry, opt) { 422 opt = opt || {} 423 super(opt) 424 this.preservePaths = !!opt.preservePaths 425 this.portable = !!opt.portable 426 this.strict = !!opt.strict 427 this.noPax = !!opt.noPax 428 this.noMtime = !!opt.noMtime 429 430 this.readEntry = readEntry 431 this.type = readEntry.type 432 if (this.type === 'Directory' && this.portable) { 433 this.noMtime = true 434 } 435 436 this.prefix = opt.prefix || null 437 438 this.path = normPath(readEntry.path) 439 this.mode = this[MODE](readEntry.mode) 440 this.uid = this.portable ? null : readEntry.uid 441 this.gid = this.portable ? null : readEntry.gid 442 this.uname = this.portable ? null : readEntry.uname 443 this.gname = this.portable ? null : readEntry.gname 444 this.size = readEntry.size 445 this.mtime = this.noMtime ? null : opt.mtime || readEntry.mtime 446 this.atime = this.portable ? null : readEntry.atime 447 this.ctime = this.portable ? null : readEntry.ctime 448 this.linkpath = normPath(readEntry.linkpath) 449 450 if (typeof opt.onwarn === 'function') { 451 this.on('warn', opt.onwarn) 452 } 453 454 let pathWarn = false 455 if (!this.preservePaths) { 456 const [root, stripped] = stripAbsolutePath(this.path) 457 if (root) { 458 this.path = stripped 459 pathWarn = root 460 } 461 } 462 463 this.remain = readEntry.size 464 this.blockRemain = readEntry.startBlockSize 465 466 this.header = new Header({ 467 path: this[PREFIX](this.path), 468 linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath) 469 : this.linkpath, 470 // only the permissions and setuid/setgid/sticky bitflags 471 // not the higher-order bits that specify file type 472 mode: this.mode, 473 uid: this.portable ? null : this.uid, 474 gid: this.portable ? null : this.gid, 475 size: this.size, 476 mtime: this.noMtime ? null : this.mtime, 477 type: this.type, 478 uname: this.portable ? null : this.uname, 479 atime: this.portable ? null : this.atime, 480 ctime: this.portable ? null : this.ctime, 481 }) 482 483 if (pathWarn) { 484 this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, { 485 entry: this, 486 path: pathWarn + this.path, 487 }) 488 } 489 490 if (this.header.encode() && !this.noPax) { 491 super.write(new Pax({ 492 atime: this.portable ? null : this.atime, 493 ctime: this.portable ? null : this.ctime, 494 gid: this.portable ? null : this.gid, 495 mtime: this.noMtime ? null : this.mtime, 496 path: this[PREFIX](this.path), 497 linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath) 498 : this.linkpath, 499 size: this.size, 500 uid: this.portable ? null : this.uid, 501 uname: this.portable ? null : this.uname, 502 dev: this.portable ? null : this.readEntry.dev, 503 ino: this.portable ? null : this.readEntry.ino, 504 nlink: this.portable ? null : this.readEntry.nlink, 505 }).encode()) 506 } 507 508 super.write(this.header.block) 509 readEntry.pipe(this) 510 } 511 512 [PREFIX] (path) { 513 return prefixPath(path, this.prefix) 514 } 515 516 [MODE] (mode) { 517 return modeFix(mode, this.type === 'Directory', this.portable) 518 } 519 520 write (data) { 521 const writeLen = data.length 522 if (writeLen > this.blockRemain) { 523 throw new Error('writing more to entry than is appropriate') 524 } 525 this.blockRemain -= writeLen 526 return super.write(data) 527 } 528 529 end () { 530 if (this.blockRemain) { 531 super.write(Buffer.alloc(this.blockRemain)) 532 } 533 return super.end() 534 } 535}) 536 537WriteEntry.Sync = WriteEntrySync 538WriteEntry.Tar = WriteEntryTar 539 540const getType = stat => 541 stat.isFile() ? 'File' 542 : stat.isDirectory() ? 'Directory' 543 : stat.isSymbolicLink() ? 'SymbolicLink' 544 : 'Unsupported' 545 546module.exports = WriteEntry 547