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