1'use strict' 2 3const assert = require('assert') 4const EE = require('events').EventEmitter 5const Parser = require('./parse.js') 6const fs = require('fs') 7const fsm = require('fs-minipass') 8const path = require('path') 9const mkdir = require('./mkdir.js') 10const mkdirSync = mkdir.sync 11const wc = require('./winchars.js') 12 13const ONENTRY = Symbol('onEntry') 14const CHECKFS = Symbol('checkFs') 15const ISREUSABLE = Symbol('isReusable') 16const MAKEFS = Symbol('makeFs') 17const FILE = Symbol('file') 18const DIRECTORY = Symbol('directory') 19const LINK = Symbol('link') 20const SYMLINK = Symbol('symlink') 21const HARDLINK = Symbol('hardlink') 22const UNSUPPORTED = Symbol('unsupported') 23const UNKNOWN = Symbol('unknown') 24const CHECKPATH = Symbol('checkPath') 25const MKDIR = Symbol('mkdir') 26const ONERROR = Symbol('onError') 27const PENDING = Symbol('pending') 28const PEND = Symbol('pend') 29const UNPEND = Symbol('unpend') 30const ENDED = Symbol('ended') 31const MAYBECLOSE = Symbol('maybeClose') 32const SKIP = Symbol('skip') 33const DOCHOWN = Symbol('doChown') 34const UID = Symbol('uid') 35const GID = Symbol('gid') 36const crypto = require('crypto') 37 38// Unlinks on Windows are not atomic. 39// 40// This means that if you have a file entry, followed by another 41// file entry with an identical name, and you cannot re-use the file 42// (because it's a hardlink, or because unlink:true is set, or it's 43// Windows, which does not have useful nlink values), then the unlink 44// will be committed to the disk AFTER the new file has been written 45// over the old one, deleting the new file. 46// 47// To work around this, on Windows systems, we rename the file and then 48// delete the renamed file. It's a sloppy kludge, but frankly, I do not 49// know of a better way to do this, given windows' non-atomic unlink 50// semantics. 51// 52// See: https://github.com/npm/node-tar/issues/183 53/* istanbul ignore next */ 54const unlinkFile = (path, cb) => { 55 if (process.platform !== 'win32') 56 return fs.unlink(path, cb) 57 58 const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex') 59 fs.rename(path, name, er => { 60 if (er) 61 return cb(er) 62 fs.unlink(name, cb) 63 }) 64} 65 66/* istanbul ignore next */ 67const unlinkFileSync = path => { 68 if (process.platform !== 'win32') 69 return fs.unlinkSync(path) 70 71 const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex') 72 fs.renameSync(path, name) 73 fs.unlinkSync(name) 74} 75 76// this.gid, entry.gid, this.processUid 77const uint32 = (a, b, c) => 78 a === a >>> 0 ? a 79 : b === b >>> 0 ? b 80 : c 81 82class Unpack extends Parser { 83 constructor (opt) { 84 if (!opt) 85 opt = {} 86 87 opt.ondone = _ => { 88 this[ENDED] = true 89 this[MAYBECLOSE]() 90 } 91 92 super(opt) 93 94 this.transform = typeof opt.transform === 'function' ? opt.transform : null 95 96 this.writable = true 97 this.readable = false 98 99 this[PENDING] = 0 100 this[ENDED] = false 101 102 this.dirCache = opt.dirCache || new Map() 103 104 if (typeof opt.uid === 'number' || typeof opt.gid === 'number') { 105 // need both or neither 106 if (typeof opt.uid !== 'number' || typeof opt.gid !== 'number') 107 throw new TypeError('cannot set owner without number uid and gid') 108 if (opt.preserveOwner) 109 throw new TypeError( 110 'cannot preserve owner in archive and also set owner explicitly') 111 this.uid = opt.uid 112 this.gid = opt.gid 113 this.setOwner = true 114 } else { 115 this.uid = null 116 this.gid = null 117 this.setOwner = false 118 } 119 120 // default true for root 121 if (opt.preserveOwner === undefined && typeof opt.uid !== 'number') 122 this.preserveOwner = process.getuid && process.getuid() === 0 123 else 124 this.preserveOwner = !!opt.preserveOwner 125 126 this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ? 127 process.getuid() : null 128 this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ? 129 process.getgid() : null 130 131 // mostly just for testing, but useful in some cases. 132 // Forcibly trigger a chown on every entry, no matter what 133 this.forceChown = opt.forceChown === true 134 135 // turn ><?| in filenames into 0xf000-higher encoded forms 136 this.win32 = !!opt.win32 || process.platform === 'win32' 137 138 // do not unpack over files that are newer than what's in the archive 139 this.newer = !!opt.newer 140 141 // do not unpack over ANY files 142 this.keep = !!opt.keep 143 144 // do not set mtime/atime of extracted entries 145 this.noMtime = !!opt.noMtime 146 147 // allow .., absolute path entries, and unpacking through symlinks 148 // without this, warn and skip .., relativize absolutes, and error 149 // on symlinks in extraction path 150 this.preservePaths = !!opt.preservePaths 151 152 // unlink files and links before writing. This breaks existing hard 153 // links, and removes symlink directories rather than erroring 154 this.unlink = !!opt.unlink 155 156 this.cwd = path.resolve(opt.cwd || process.cwd()) 157 this.strip = +opt.strip || 0 158 this.processUmask = process.umask() 159 this.umask = typeof opt.umask === 'number' ? opt.umask : this.processUmask 160 // default mode for dirs created as parents 161 this.dmode = opt.dmode || (0o0777 & (~this.umask)) 162 this.fmode = opt.fmode || (0o0666 & (~this.umask)) 163 this.on('entry', entry => this[ONENTRY](entry)) 164 } 165 166 [MAYBECLOSE] () { 167 if (this[ENDED] && this[PENDING] === 0) { 168 this.emit('prefinish') 169 this.emit('finish') 170 this.emit('end') 171 this.emit('close') 172 } 173 } 174 175 [CHECKPATH] (entry) { 176 if (this.strip) { 177 const parts = entry.path.split(/\/|\\/) 178 if (parts.length < this.strip) 179 return false 180 entry.path = parts.slice(this.strip).join('/') 181 182 if (entry.type === 'Link') { 183 const linkparts = entry.linkpath.split(/\/|\\/) 184 if (linkparts.length >= this.strip) 185 entry.linkpath = linkparts.slice(this.strip).join('/') 186 } 187 } 188 189 if (!this.preservePaths) { 190 const p = entry.path 191 if (p.match(/(^|\/|\\)\.\.(\\|\/|$)/)) { 192 this.warn('path contains \'..\'', p) 193 return false 194 } 195 196 // absolutes on posix are also absolutes on win32 197 // so we only need to test this one to get both 198 if (path.win32.isAbsolute(p)) { 199 const parsed = path.win32.parse(p) 200 this.warn('stripping ' + parsed.root + ' from absolute path', p) 201 entry.path = p.substr(parsed.root.length) 202 } 203 } 204 205 // only encode : chars that aren't drive letter indicators 206 if (this.win32) { 207 const parsed = path.win32.parse(entry.path) 208 entry.path = parsed.root === '' ? wc.encode(entry.path) 209 : parsed.root + wc.encode(entry.path.substr(parsed.root.length)) 210 } 211 212 if (path.isAbsolute(entry.path)) 213 entry.absolute = entry.path 214 else 215 entry.absolute = path.resolve(this.cwd, entry.path) 216 217 return true 218 } 219 220 [ONENTRY] (entry) { 221 if (!this[CHECKPATH](entry)) 222 return entry.resume() 223 224 assert.equal(typeof entry.absolute, 'string') 225 226 switch (entry.type) { 227 case 'Directory': 228 case 'GNUDumpDir': 229 if (entry.mode) 230 entry.mode = entry.mode | 0o700 231 232 case 'File': 233 case 'OldFile': 234 case 'ContiguousFile': 235 case 'Link': 236 case 'SymbolicLink': 237 return this[CHECKFS](entry) 238 239 case 'CharacterDevice': 240 case 'BlockDevice': 241 case 'FIFO': 242 return this[UNSUPPORTED](entry) 243 } 244 } 245 246 [ONERROR] (er, entry) { 247 // Cwd has to exist, or else nothing works. That's serious. 248 // Other errors are warnings, which raise the error in strict 249 // mode, but otherwise continue on. 250 if (er.name === 'CwdError') 251 this.emit('error', er) 252 else { 253 this.warn(er.message, er) 254 this[UNPEND]() 255 entry.resume() 256 } 257 } 258 259 [MKDIR] (dir, mode, cb) { 260 mkdir(dir, { 261 uid: this.uid, 262 gid: this.gid, 263 processUid: this.processUid, 264 processGid: this.processGid, 265 umask: this.processUmask, 266 preserve: this.preservePaths, 267 unlink: this.unlink, 268 cache: this.dirCache, 269 cwd: this.cwd, 270 mode: mode 271 }, cb) 272 } 273 274 [DOCHOWN] (entry) { 275 // in preserve owner mode, chown if the entry doesn't match process 276 // in set owner mode, chown if setting doesn't match process 277 return this.forceChown || 278 this.preserveOwner && 279 ( typeof entry.uid === 'number' && entry.uid !== this.processUid || 280 typeof entry.gid === 'number' && entry.gid !== this.processGid ) 281 || 282 ( typeof this.uid === 'number' && this.uid !== this.processUid || 283 typeof this.gid === 'number' && this.gid !== this.processGid ) 284 } 285 286 [UID] (entry) { 287 return uint32(this.uid, entry.uid, this.processUid) 288 } 289 290 [GID] (entry) { 291 return uint32(this.gid, entry.gid, this.processGid) 292 } 293 294 [FILE] (entry) { 295 const mode = entry.mode & 0o7777 || this.fmode 296 const stream = new fsm.WriteStream(entry.absolute, { 297 mode: mode, 298 autoClose: false 299 }) 300 stream.on('error', er => this[ONERROR](er, entry)) 301 302 let actions = 1 303 const done = er => { 304 if (er) 305 return this[ONERROR](er, entry) 306 307 if (--actions === 0) 308 fs.close(stream.fd, _ => this[UNPEND]()) 309 } 310 311 stream.on('finish', _ => { 312 // if futimes fails, try utimes 313 // if utimes fails, fail with the original error 314 // same for fchown/chown 315 const abs = entry.absolute 316 const fd = stream.fd 317 318 if (entry.mtime && !this.noMtime) { 319 actions++ 320 const atime = entry.atime || new Date() 321 const mtime = entry.mtime 322 fs.futimes(fd, atime, mtime, er => 323 er ? fs.utimes(abs, atime, mtime, er2 => done(er2 && er)) 324 : done()) 325 } 326 327 if (this[DOCHOWN](entry)) { 328 actions++ 329 const uid = this[UID](entry) 330 const gid = this[GID](entry) 331 fs.fchown(fd, uid, gid, er => 332 er ? fs.chown(abs, uid, gid, er2 => done(er2 && er)) 333 : done()) 334 } 335 336 done() 337 }) 338 339 const tx = this.transform ? this.transform(entry) || entry : entry 340 if (tx !== entry) { 341 tx.on('error', er => this[ONERROR](er, entry)) 342 entry.pipe(tx) 343 } 344 tx.pipe(stream) 345 } 346 347 [DIRECTORY] (entry) { 348 const mode = entry.mode & 0o7777 || this.dmode 349 this[MKDIR](entry.absolute, mode, er => { 350 if (er) 351 return this[ONERROR](er, entry) 352 353 let actions = 1 354 const done = _ => { 355 if (--actions === 0) { 356 this[UNPEND]() 357 entry.resume() 358 } 359 } 360 361 if (entry.mtime && !this.noMtime) { 362 actions++ 363 fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, done) 364 } 365 366 if (this[DOCHOWN](entry)) { 367 actions++ 368 fs.chown(entry.absolute, this[UID](entry), this[GID](entry), done) 369 } 370 371 done() 372 }) 373 } 374 375 [UNSUPPORTED] (entry) { 376 this.warn('unsupported entry type: ' + entry.type, entry) 377 entry.resume() 378 } 379 380 [SYMLINK] (entry) { 381 this[LINK](entry, entry.linkpath, 'symlink') 382 } 383 384 [HARDLINK] (entry) { 385 this[LINK](entry, path.resolve(this.cwd, entry.linkpath), 'link') 386 } 387 388 [PEND] () { 389 this[PENDING]++ 390 } 391 392 [UNPEND] () { 393 this[PENDING]-- 394 this[MAYBECLOSE]() 395 } 396 397 [SKIP] (entry) { 398 this[UNPEND]() 399 entry.resume() 400 } 401 402 // Check if we can reuse an existing filesystem entry safely and 403 // overwrite it, rather than unlinking and recreating 404 // Windows doesn't report a useful nlink, so we just never reuse entries 405 [ISREUSABLE] (entry, st) { 406 return entry.type === 'File' && 407 !this.unlink && 408 st.isFile() && 409 st.nlink <= 1 && 410 process.platform !== 'win32' 411 } 412 413 // check if a thing is there, and if so, try to clobber it 414 [CHECKFS] (entry) { 415 this[PEND]() 416 this[MKDIR](path.dirname(entry.absolute), this.dmode, er => { 417 if (er) 418 return this[ONERROR](er, entry) 419 fs.lstat(entry.absolute, (er, st) => { 420 if (st && (this.keep || this.newer && st.mtime > entry.mtime)) 421 this[SKIP](entry) 422 else if (er || this[ISREUSABLE](entry, st)) 423 this[MAKEFS](null, entry) 424 else if (st.isDirectory()) { 425 if (entry.type === 'Directory') { 426 if (!entry.mode || (st.mode & 0o7777) === entry.mode) 427 this[MAKEFS](null, entry) 428 else 429 fs.chmod(entry.absolute, entry.mode, er => this[MAKEFS](er, entry)) 430 } else 431 fs.rmdir(entry.absolute, er => this[MAKEFS](er, entry)) 432 } else 433 unlinkFile(entry.absolute, er => this[MAKEFS](er, entry)) 434 }) 435 }) 436 } 437 438 [MAKEFS] (er, entry) { 439 if (er) 440 return this[ONERROR](er, entry) 441 442 switch (entry.type) { 443 case 'File': 444 case 'OldFile': 445 case 'ContiguousFile': 446 return this[FILE](entry) 447 448 case 'Link': 449 return this[HARDLINK](entry) 450 451 case 'SymbolicLink': 452 return this[SYMLINK](entry) 453 454 case 'Directory': 455 case 'GNUDumpDir': 456 return this[DIRECTORY](entry) 457 } 458 } 459 460 [LINK] (entry, linkpath, link) { 461 // XXX: get the type ('file' or 'dir') for windows 462 fs[link](linkpath, entry.absolute, er => { 463 if (er) 464 return this[ONERROR](er, entry) 465 this[UNPEND]() 466 entry.resume() 467 }) 468 } 469} 470 471class UnpackSync extends Unpack { 472 constructor (opt) { 473 super(opt) 474 } 475 476 [CHECKFS] (entry) { 477 const er = this[MKDIR](path.dirname(entry.absolute), this.dmode) 478 if (er) 479 return this[ONERROR](er, entry) 480 try { 481 const st = fs.lstatSync(entry.absolute) 482 if (this.keep || this.newer && st.mtime > entry.mtime) 483 return this[SKIP](entry) 484 else if (this[ISREUSABLE](entry, st)) 485 return this[MAKEFS](null, entry) 486 else { 487 try { 488 if (st.isDirectory()) { 489 if (entry.type === 'Directory') { 490 if (entry.mode && (st.mode & 0o7777) !== entry.mode) 491 fs.chmodSync(entry.absolute, entry.mode) 492 } else 493 fs.rmdirSync(entry.absolute) 494 } else 495 unlinkFileSync(entry.absolute) 496 return this[MAKEFS](null, entry) 497 } catch (er) { 498 return this[ONERROR](er, entry) 499 } 500 } 501 } catch (er) { 502 return this[MAKEFS](null, entry) 503 } 504 } 505 506 [FILE] (entry) { 507 const mode = entry.mode & 0o7777 || this.fmode 508 509 const oner = er => { 510 try { fs.closeSync(fd) } catch (_) {} 511 if (er) 512 this[ONERROR](er, entry) 513 } 514 515 let stream 516 let fd 517 try { 518 fd = fs.openSync(entry.absolute, 'w', mode) 519 } catch (er) { 520 return oner(er) 521 } 522 const tx = this.transform ? this.transform(entry) || entry : entry 523 if (tx !== entry) { 524 tx.on('error', er => this[ONERROR](er, entry)) 525 entry.pipe(tx) 526 } 527 528 tx.on('data', chunk => { 529 try { 530 fs.writeSync(fd, chunk, 0, chunk.length) 531 } catch (er) { 532 oner(er) 533 } 534 }) 535 536 tx.on('end', _ => { 537 let er = null 538 // try both, falling futimes back to utimes 539 // if either fails, handle the first error 540 if (entry.mtime && !this.noMtime) { 541 const atime = entry.atime || new Date() 542 const mtime = entry.mtime 543 try { 544 fs.futimesSync(fd, atime, mtime) 545 } catch (futimeser) { 546 try { 547 fs.utimesSync(entry.absolute, atime, mtime) 548 } catch (utimeser) { 549 er = futimeser 550 } 551 } 552 } 553 554 if (this[DOCHOWN](entry)) { 555 const uid = this[UID](entry) 556 const gid = this[GID](entry) 557 558 try { 559 fs.fchownSync(fd, uid, gid) 560 } catch (fchowner) { 561 try { 562 fs.chownSync(entry.absolute, uid, gid) 563 } catch (chowner) { 564 er = er || fchowner 565 } 566 } 567 } 568 569 oner(er) 570 }) 571 } 572 573 [DIRECTORY] (entry) { 574 const mode = entry.mode & 0o7777 || this.dmode 575 const er = this[MKDIR](entry.absolute, mode) 576 if (er) 577 return this[ONERROR](er, entry) 578 if (entry.mtime && !this.noMtime) { 579 try { 580 fs.utimesSync(entry.absolute, entry.atime || new Date(), entry.mtime) 581 } catch (er) {} 582 } 583 if (this[DOCHOWN](entry)) { 584 try { 585 fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry)) 586 } catch (er) {} 587 } 588 entry.resume() 589 } 590 591 [MKDIR] (dir, mode) { 592 try { 593 return mkdir.sync(dir, { 594 uid: this.uid, 595 gid: this.gid, 596 processUid: this.processUid, 597 processGid: this.processGid, 598 umask: this.processUmask, 599 preserve: this.preservePaths, 600 unlink: this.unlink, 601 cache: this.dirCache, 602 cwd: this.cwd, 603 mode: mode 604 }) 605 } catch (er) { 606 return er 607 } 608 } 609 610 [LINK] (entry, linkpath, link) { 611 try { 612 fs[link + 'Sync'](linkpath, entry.absolute) 613 entry.resume() 614 } catch (er) { 615 return this[ONERROR](er, entry) 616 } 617 } 618} 619 620Unpack.Sync = UnpackSync 621module.exports = Unpack 622