Passed
Push — main ( 10dbfc...61a674 )
by LCS
05:39 queued 03:09
created

node_modules/tar/lib/write-entry.js   F

Complexity

Total Complexity 135
Complexity/F 3

Size

Lines of Code 518
Function Count 45

Duplication

Duplicated Lines 35
Ratio 6.76 %

Importance

Changes 0
Metric Value
eloc 366
dl 35
loc 518
rs 2
c 0
b 0
f 0
wmc 135
mnd 90
bc 90
fnc 45
bpm 2
cpm 3
noi 99

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like node_modules/tar/lib/write-entry.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
'use strict'
2
const Buffer = require('./buffer.js')
3
const MiniPass = require('minipass')
4
const Pax = require('./pax.js')
5
const Header = require('./header.js')
6
const ReadEntry = require('./read-entry.js')
7
const fs = require('fs')
8
const path = require('path')
9
const normPath = require('./normalize-windows-path.js')
10
const stripSlash = require('./strip-trailing-slashes.js')
11
12
const prefixPath = (path, prefix) => {
13
  if (!prefix)
14
    return path
15
  path = normPath(path).replace(/^\.(\/|$)/, '')
16
  return stripSlash(prefix) + '/' + path
17
}
18
19
const maxReadSize = 16 * 1024 * 1024
20
const PROCESS = Symbol('process')
21
const FILE = Symbol('file')
22
const DIRECTORY = Symbol('directory')
23
const SYMLINK = Symbol('symlink')
24
const HARDLINK = Symbol('hardlink')
25
const HEADER = Symbol('header')
26
const READ = Symbol('read')
27
const LSTAT = Symbol('lstat')
28
const ONLSTAT = Symbol('onlstat')
29
const ONREAD = Symbol('onread')
30
const ONREADLINK = Symbol('onreadlink')
31
const OPENFILE = Symbol('openfile')
32
const ONOPENFILE = Symbol('onopenfile')
33
const CLOSE = Symbol('close')
34
const MODE = Symbol('mode')
35
const AWAITDRAIN = Symbol('awaitDrain')
36
const ONDRAIN = Symbol('ondrain')
37
const PREFIX = Symbol('prefix')
38
const HAD_ERROR = Symbol('hadError')
39
const warner = require('./warn-mixin.js')
40
const winchars = require('./winchars.js')
41
const stripAbsolutePath = require('./strip-absolute-path.js')
42
43
const modeFix = require('./mode-fix.js')
44
45
const 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
357
class 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
402
const 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
509
WriteEntry.Sync = WriteEntrySync
510
WriteEntry.Tar = WriteEntryTar
511
512
const getType = stat =>
513
  stat.isFile() ? 'File'
514
  : stat.isDirectory() ? 'Directory'
515
  : stat.isSymbolicLink() ? 'SymbolicLink'
516
  : 'Unsupported'
517
518
module.exports = WriteEntry
519