• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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')
9
10const types = require('./types.js')
11const maxReadSize = 16 * 1024 * 1024
12const PROCESS = Symbol('process')
13const FILE = Symbol('file')
14const DIRECTORY = Symbol('directory')
15const SYMLINK = Symbol('symlink')
16const HARDLINK = Symbol('hardlink')
17const HEADER = Symbol('header')
18const READ = Symbol('read')
19const LSTAT = Symbol('lstat')
20const ONLSTAT = Symbol('onlstat')
21const ONREAD = Symbol('onread')
22const ONREADLINK = Symbol('onreadlink')
23const OPENFILE = Symbol('openfile')
24const ONOPENFILE = Symbol('onopenfile')
25const CLOSE = Symbol('close')
26const MODE = Symbol('mode')
27const warner = require('./warn-mixin.js')
28const winchars = require('./winchars.js')
29
30const modeFix = require('./mode-fix.js')
31
32const WriteEntry = warner(class WriteEntry extends MiniPass {
33  constructor (p, opt) {
34    opt = opt || {}
35    super(opt)
36    if (typeof p !== 'string')
37      throw new TypeError('path is required')
38    this.path = p
39    // suppress atime, ctime, uid, gid, uname, gname
40    this.portable = !!opt.portable
41    // until node has builtin pwnam functions, this'll have to do
42    this.myuid = process.getuid && process.getuid()
43    this.myuser = process.env.USER || ''
44    this.maxReadSize = opt.maxReadSize || maxReadSize
45    this.linkCache = opt.linkCache || new Map()
46    this.statCache = opt.statCache || new Map()
47    this.preservePaths = !!opt.preservePaths
48    this.cwd = opt.cwd || process.cwd()
49    this.strict = !!opt.strict
50    this.noPax = !!opt.noPax
51    this.noMtime = !!opt.noMtime
52    this.mtime = opt.mtime || null
53
54    if (typeof opt.onwarn === 'function')
55      this.on('warn', opt.onwarn)
56
57    if (!this.preservePaths && path.win32.isAbsolute(p)) {
58      // absolutes on posix are also absolutes on win32
59      // so we only need to test this one to get both
60      const parsed = path.win32.parse(p)
61      this.warn('stripping ' + parsed.root + ' from absolute path', p)
62      this.path = p.substr(parsed.root.length)
63    }
64
65    this.win32 = !!opt.win32 || process.platform === 'win32'
66    if (this.win32) {
67      this.path = winchars.decode(this.path.replace(/\\/g, '/'))
68      p = p.replace(/\\/g, '/')
69    }
70
71    this.absolute = opt.absolute || path.resolve(this.cwd, p)
72
73    if (this.path === '')
74      this.path = './'
75
76    if (this.statCache.has(this.absolute))
77      this[ONLSTAT](this.statCache.get(this.absolute))
78    else
79      this[LSTAT]()
80  }
81
82  [LSTAT] () {
83    fs.lstat(this.absolute, (er, stat) => {
84      if (er)
85        return this.emit('error', er)
86      this[ONLSTAT](stat)
87    })
88  }
89
90  [ONLSTAT] (stat) {
91    this.statCache.set(this.absolute, stat)
92    this.stat = stat
93    if (!stat.isFile())
94      stat.size = 0
95    this.type = getType(stat)
96    this.emit('stat', stat)
97    this[PROCESS]()
98  }
99
100  [PROCESS] () {
101    switch (this.type) {
102      case 'File': return this[FILE]()
103      case 'Directory': return this[DIRECTORY]()
104      case 'SymbolicLink': return this[SYMLINK]()
105      // unsupported types are ignored.
106      default: return this.end()
107    }
108  }
109
110  [MODE] (mode) {
111    return modeFix(mode, this.type === 'Directory')
112  }
113
114  [HEADER] () {
115    if (this.type === 'Directory' && this.portable)
116      this.noMtime = true
117
118    this.header = new Header({
119      path: this.path,
120      linkpath: this.linkpath,
121      // only the permissions and setuid/setgid/sticky bitflags
122      // not the higher-order bits that specify file type
123      mode: this[MODE](this.stat.mode),
124      uid: this.portable ? null : this.stat.uid,
125      gid: this.portable ? null : this.stat.gid,
126      size: this.stat.size,
127      mtime: this.noMtime ? null : this.mtime || this.stat.mtime,
128      type: this.type,
129      uname: this.portable ? null :
130        this.stat.uid === this.myuid ? this.myuser : '',
131      atime: this.portable ? null : this.stat.atime,
132      ctime: this.portable ? null : this.stat.ctime
133    })
134
135    if (this.header.encode() && !this.noPax)
136      this.write(new Pax({
137        atime: this.portable ? null : this.header.atime,
138        ctime: this.portable ? null : this.header.ctime,
139        gid: this.portable ? null : this.header.gid,
140        mtime: this.noMtime ? null : this.mtime || this.header.mtime,
141        path: this.path,
142        linkpath: this.linkpath,
143        size: this.header.size,
144        uid: this.portable ? null : this.header.uid,
145        uname: this.portable ? null : this.header.uname,
146        dev: this.portable ? null : this.stat.dev,
147        ino: this.portable ? null : this.stat.ino,
148        nlink: this.portable ? null : this.stat.nlink
149      }).encode())
150    this.write(this.header.block)
151  }
152
153  [DIRECTORY] () {
154    if (this.path.substr(-1) !== '/')
155      this.path += '/'
156    this.stat.size = 0
157    this[HEADER]()
158    this.end()
159  }
160
161  [SYMLINK] () {
162    fs.readlink(this.absolute, (er, linkpath) => {
163      if (er)
164        return this.emit('error', er)
165      this[ONREADLINK](linkpath)
166    })
167  }
168
169  [ONREADLINK] (linkpath) {
170    this.linkpath = linkpath
171    this[HEADER]()
172    this.end()
173  }
174
175  [HARDLINK] (linkpath) {
176    this.type = 'Link'
177    this.linkpath = path.relative(this.cwd, linkpath)
178    this.stat.size = 0
179    this[HEADER]()
180    this.end()
181  }
182
183  [FILE] () {
184    if (this.stat.nlink > 1) {
185      const linkKey = this.stat.dev + ':' + this.stat.ino
186      if (this.linkCache.has(linkKey)) {
187        const linkpath = this.linkCache.get(linkKey)
188        if (linkpath.indexOf(this.cwd) === 0)
189          return this[HARDLINK](linkpath)
190      }
191      this.linkCache.set(linkKey, this.absolute)
192    }
193
194    this[HEADER]()
195    if (this.stat.size === 0)
196      return this.end()
197
198    this[OPENFILE]()
199  }
200
201  [OPENFILE] () {
202    fs.open(this.absolute, 'r', (er, fd) => {
203      if (er)
204        return this.emit('error', er)
205      this[ONOPENFILE](fd)
206    })
207  }
208
209  [ONOPENFILE] (fd) {
210    const blockLen = 512 * Math.ceil(this.stat.size / 512)
211    const bufLen = Math.min(blockLen, this.maxReadSize)
212    const buf = Buffer.allocUnsafe(bufLen)
213    this[READ](fd, buf, 0, buf.length, 0, this.stat.size, blockLen)
214  }
215
216  [READ] (fd, buf, offset, length, pos, remain, blockRemain) {
217    fs.read(fd, buf, offset, length, pos, (er, bytesRead) => {
218      if (er)
219        return this[CLOSE](fd, _ => this.emit('error', er))
220      this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead)
221    })
222  }
223
224  [CLOSE] (fd, cb) {
225    fs.close(fd, cb)
226  }
227
228  [ONREAD] (fd, buf, offset, length, pos, remain, blockRemain, bytesRead) {
229    if (bytesRead <= 0 && remain > 0) {
230      const er = new Error('encountered unexpected EOF')
231      er.path = this.absolute
232      er.syscall = 'read'
233      er.code = 'EOF'
234      this[CLOSE](fd, _ => _)
235      return this.emit('error', er)
236    }
237
238    if (bytesRead > remain) {
239      const er = new Error('did not encounter expected EOF')
240      er.path = this.absolute
241      er.syscall = 'read'
242      er.code = 'EOF'
243      this[CLOSE](fd, _ => _)
244      return this.emit('error', er)
245    }
246
247    // null out the rest of the buffer, if we could fit the block padding
248    if (bytesRead === remain) {
249      for (let i = bytesRead; i < length && bytesRead < blockRemain; i++) {
250        buf[i + offset] = 0
251        bytesRead ++
252        remain ++
253      }
254    }
255
256    const writeBuf = offset === 0 && bytesRead === buf.length ?
257      buf : buf.slice(offset, offset + bytesRead)
258    remain -= bytesRead
259    blockRemain -= bytesRead
260    pos += bytesRead
261    offset += bytesRead
262
263    this.write(writeBuf)
264
265    if (!remain) {
266      if (blockRemain)
267        this.write(Buffer.alloc(blockRemain))
268      this.end()
269      this[CLOSE](fd, _ => _)
270      return
271    }
272
273    if (offset >= length) {
274      buf = Buffer.allocUnsafe(length)
275      offset = 0
276    }
277    length = buf.length - offset
278    this[READ](fd, buf, offset, length, pos, remain, blockRemain)
279  }
280})
281
282class WriteEntrySync extends WriteEntry {
283  constructor (path, opt) {
284    super(path, opt)
285  }
286
287  [LSTAT] () {
288    this[ONLSTAT](fs.lstatSync(this.absolute))
289  }
290
291  [SYMLINK] () {
292    this[ONREADLINK](fs.readlinkSync(this.absolute))
293  }
294
295  [OPENFILE] () {
296    this[ONOPENFILE](fs.openSync(this.absolute, 'r'))
297  }
298
299  [READ] (fd, buf, offset, length, pos, remain, blockRemain) {
300    let threw = true
301    try {
302      const bytesRead = fs.readSync(fd, buf, offset, length, pos)
303      this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead)
304      threw = false
305    } finally {
306      if (threw)
307        try { this[CLOSE](fd) } catch (er) {}
308    }
309  }
310
311  [CLOSE] (fd) {
312    fs.closeSync(fd)
313  }
314}
315
316const WriteEntryTar = warner(class WriteEntryTar extends MiniPass {
317  constructor (readEntry, opt) {
318    opt = opt || {}
319    super(opt)
320    this.preservePaths = !!opt.preservePaths
321    this.portable = !!opt.portable
322    this.strict = !!opt.strict
323    this.noPax = !!opt.noPax
324    this.noMtime = !!opt.noMtime
325
326    this.readEntry = readEntry
327    this.type = readEntry.type
328    if (this.type === 'Directory' && this.portable)
329      this.noMtime = true
330
331    this.path = readEntry.path
332    this.mode = this[MODE](readEntry.mode)
333    this.uid = this.portable ? null : readEntry.uid
334    this.gid = this.portable ? null : readEntry.gid
335    this.uname = this.portable ? null : readEntry.uname
336    this.gname = this.portable ? null : readEntry.gname
337    this.size = readEntry.size
338    this.mtime = this.noMtime ? null : opt.mtime || readEntry.mtime
339    this.atime = this.portable ? null : readEntry.atime
340    this.ctime = this.portable ? null : readEntry.ctime
341    this.linkpath = readEntry.linkpath
342
343    if (typeof opt.onwarn === 'function')
344      this.on('warn', opt.onwarn)
345
346    if (path.isAbsolute(this.path) && !this.preservePaths) {
347      const parsed = path.parse(this.path)
348      this.warn(
349        'stripping ' + parsed.root + ' from absolute path',
350        this.path
351      )
352      this.path = this.path.substr(parsed.root.length)
353    }
354
355    this.remain = readEntry.size
356    this.blockRemain = readEntry.startBlockSize
357
358    this.header = new Header({
359      path: this.path,
360      linkpath: this.linkpath,
361      // only the permissions and setuid/setgid/sticky bitflags
362      // not the higher-order bits that specify file type
363      mode: this.mode,
364      uid: this.portable ? null : this.uid,
365      gid: this.portable ? null : this.gid,
366      size: this.size,
367      mtime: this.noMtime ? null : this.mtime,
368      type: this.type,
369      uname: this.portable ? null : this.uname,
370      atime: this.portable ? null : this.atime,
371      ctime: this.portable ? null : this.ctime
372    })
373
374    if (this.header.encode() && !this.noPax)
375      super.write(new Pax({
376        atime: this.portable ? null : this.atime,
377        ctime: this.portable ? null : this.ctime,
378        gid: this.portable ? null : this.gid,
379        mtime: this.noMtime ? null : this.mtime,
380        path: this.path,
381        linkpath: this.linkpath,
382        size: this.size,
383        uid: this.portable ? null : this.uid,
384        uname: this.portable ? null : this.uname,
385        dev: this.portable ? null : this.readEntry.dev,
386        ino: this.portable ? null : this.readEntry.ino,
387        nlink: this.portable ? null : this.readEntry.nlink
388      }).encode())
389
390    super.write(this.header.block)
391    readEntry.pipe(this)
392  }
393
394  [MODE] (mode) {
395    return modeFix(mode, this.type === 'Directory')
396  }
397
398  write (data) {
399    const writeLen = data.length
400    if (writeLen > this.blockRemain)
401      throw new Error('writing more to entry than is appropriate')
402    this.blockRemain -= writeLen
403    return super.write(data)
404  }
405
406  end () {
407    if (this.blockRemain)
408      this.write(Buffer.alloc(this.blockRemain))
409    return super.end()
410  }
411})
412
413WriteEntry.Sync = WriteEntrySync
414WriteEntry.Tar = WriteEntryTar
415
416const getType = stat =>
417  stat.isFile() ? 'File'
418  : stat.isDirectory() ? 'Directory'
419  : stat.isSymbolicLink() ? 'SymbolicLink'
420  : 'Unsupported'
421
422module.exports = WriteEntry
423