Skip to content

Commit b7fc10a

Browse files
authored
fix: filter C0 and C1 control characters from logs and cli output (#7113)
1 parent 5e005ec commit b7fc10a

File tree

8 files changed

+278
-109
lines changed

8 files changed

+278
-109
lines changed

lib/commands/view.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -392,20 +392,20 @@ class View extends BaseCommand {
392392

393393
if (info.keywords.length) {
394394
this.npm.output('')
395-
this.npm.output('keywords:', chalk.yellow(info.keywords.join(', ')))
395+
this.npm.output(`keywords: ${chalk.yellow(info.keywords.join(', '))}`)
396396
}
397397

398398
if (info.bins.length) {
399399
this.npm.output('')
400-
this.npm.output('bin:', chalk.yellow(info.bins.join(', ')))
400+
this.npm.output(`bin: ${chalk.yellow(info.bins.join(', '))}`)
401401
}
402402

403403
this.npm.output('')
404404
this.npm.output('dist')
405-
this.npm.output('.tarball:', info.tarball)
406-
this.npm.output('.shasum:', info.shasum)
407-
info.integrity && this.npm.output('.integrity:', info.integrity)
408-
info.unpackedSize && this.npm.output('.unpackedSize:', info.unpackedSize)
405+
this.npm.output(`.tarball: ${info.tarball}`)
406+
this.npm.output(`.shasum: ${info.shasum}`)
407+
info.integrity && this.npm.output(`.integrity: ${info.integrity}`)
408+
info.unpackedSize && this.npm.output(`.unpackedSize: ${info.unpackedSize}`)
409409

410410
const maxDeps = 24
411411
if (info.deps.length) {
@@ -420,7 +420,7 @@ class View extends BaseCommand {
420420
if (info.maintainers && info.maintainers.length) {
421421
this.npm.output('')
422422
this.npm.output('maintainers:')
423-
info.maintainers.forEach((u) => this.npm.output('-', u))
423+
info.maintainers.forEach((u) => this.npm.output(`- ${u}`))
424424
}
425425

426426
this.npm.output('')

lib/npm.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ class Npm {
440440
output (...msg) {
441441
log.clearProgress()
442442
// eslint-disable-next-line no-console
443-
console.log(...msg)
443+
console.log(...msg.map(Display.clean))
444444
log.showProgress()
445445
}
446446

@@ -478,7 +478,7 @@ class Npm {
478478
outputError (...msg) {
479479
log.clearProgress()
480480
// eslint-disable-next-line no-console
481-
console.error(...msg)
481+
console.error(...msg.map(Display.clean))
482482
log.showProgress()
483483
}
484484
}

lib/utils/display.js

+92-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,44 @@ const npmlog = require('npmlog')
33
const log = require('./log-shim.js')
44
const { explain } = require('./explain-eresolve.js')
55

6+
const originalCustomInspect = Symbol('npm.display.original.util.inspect.custom')
7+
8+
// These are most assuredly not a mistake
9+
// https://eslint.org/docs/latest/rules/no-control-regex
10+
/* eslint-disable no-control-regex */
11+
// \x00 through \x1f, \x7f through \x9f, not including \x09 \x0a \x0b \x0d
12+
const hasC01 = /[\x00-\x08\x0c\x0e-\x1f\x7f-\x9f]/
13+
// Allows everything up to '[38;5;255m' in 8 bit notation
14+
const allowedSGR = /^\[[0-9;]{0,8}m/
15+
// '[38;5;255m'.length
16+
const sgrMaxLen = 10
17+
18+
// Strips all ANSI C0 and C1 control characters (except for SGR up to 8 bit)
19+
function stripC01 (str) {
20+
if (!hasC01.test(str)) {
21+
return str
22+
}
23+
let result = ''
24+
for (let i = 0; i < str.length; i++) {
25+
const char = str[i]
26+
const code = char.charCodeAt(0)
27+
if (!hasC01.test(char)) {
28+
// Most characters are in this set so continue early if we can
29+
result = `${result}${char}`
30+
} else if (code === 27 && allowedSGR.test(str.slice(i + 1, i + sgrMaxLen + 1))) {
31+
// \x1b with allowed SGR
32+
result = `${result}\x1b`
33+
} else if (code <= 31) {
34+
// escape all other C0 control characters besides \x7f
35+
result = `${result}^${String.fromCharCode(code + 64)}`
36+
} else {
37+
// hasC01 ensures this is now a C1 control character or \x7f
38+
result = `${result}^${String.fromCharCode(code - 64)}`
39+
}
40+
}
41+
return result
42+
}
43+
644
class Display {
745
#chalk = null
846

@@ -12,6 +50,57 @@ class Display {
1250
log.pause()
1351
}
1452

53+
static clean (output) {
54+
if (typeof output === 'string') {
55+
// Strings are cleaned inline
56+
return stripC01(output)
57+
}
58+
if (!output || typeof output !== 'object') {
59+
// Numbers, booleans, null all end up here and don't need cleaning
60+
return output
61+
}
62+
// output && typeof output === 'object'
63+
// We can't use hasOwn et al for detecting the original but we can use it
64+
// for detecting the properties we set via defineProperty
65+
if (
66+
output[inspect.custom] &&
67+
(!Object.hasOwn(output, originalCustomInspect))
68+
) {
69+
// Save the old one if we didn't already do it.
70+
Object.defineProperty(output, originalCustomInspect, {
71+
value: output[inspect.custom],
72+
writable: true,
73+
})
74+
}
75+
if (!Object.hasOwn(output, originalCustomInspect)) {
76+
// Put a dummy one in for when we run multiple times on the same object
77+
Object.defineProperty(output, originalCustomInspect, {
78+
value: function () {
79+
return this
80+
},
81+
writable: true,
82+
})
83+
}
84+
// Set the custom inspect to our own function
85+
Object.defineProperty(output, inspect.custom, {
86+
value: function () {
87+
const toClean = this[originalCustomInspect]()
88+
// Custom inspect can return things other than objects, check type again
89+
if (typeof toClean === 'string') {
90+
// Strings are cleaned inline
91+
return stripC01(toClean)
92+
}
93+
if (!toClean || typeof toClean !== 'object') {
94+
// Numbers, booleans, null all end up here and don't need cleaning
95+
return toClean
96+
}
97+
return stripC01(inspect(toClean, { customInspect: false }))
98+
},
99+
writable: true,
100+
})
101+
return output
102+
}
103+
15104
on () {
16105
process.on('log', this.#logHandler)
17106
}
@@ -103,7 +192,7 @@ class Display {
103192
// Explicitly call these on npmlog and not log shim
104193
// This is the final place we should call npmlog before removing it.
105194
#npmlog (level, ...args) {
106-
npmlog[level](...args)
195+
npmlog[level](...args.map(Display.clean))
107196
}
108197

109198
// Also (and this is a really inexcusable kludge), we patch the
@@ -112,8 +201,8 @@ class Display {
112201
// highly abbreviated explanation of what's being overridden.
113202
#eresolveWarn (level, heading, message, expl) {
114203
if (level === 'warn' &&
115-
heading === 'ERESOLVE' &&
116-
expl && typeof expl === 'object'
204+
heading === 'ERESOLVE' &&
205+
expl && typeof expl === 'object'
117206
) {
118207
this.#npmlog(level, heading, message)
119208
this.#npmlog(level, '', explain(expl, this.#chalk, 2))

lib/utils/log-file.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { Minipass } = require('minipass')
66
const fsMiniPass = require('fs-minipass')
77
const fs = require('fs/promises')
88
const log = require('./log-shim')
9+
const Display = require('./display')
910

1011
const padZero = (n, length) => n.toString().padStart(length.toString().length, '0')
1112
const globify = pattern => pattern.split('\\').join('/')
@@ -49,6 +50,7 @@ class LogFiles {
4950

5051
return format(...args)
5152
.split(/\r?\n/)
53+
.map(Display.clean)
5254
.reduce((lines, line) =>
5355
lines += prefix + (line ? ' ' : '') + line + os.EOL,
5456
''

lib/utils/reify-output.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const reifyOutput = (npm, arb) => {
7676
summary.audit = npm.command === 'audit' ? auditReport
7777
: auditReport.toJSON().metadata
7878
}
79-
npm.output(JSON.stringify(summary, 0, 2))
79+
npm.output(JSON.stringify(summary, null, 2))
8080
} else {
8181
packagesChangedMessage(npm, summary)
8282
packagesFundingMessage(npm, summary)

0 commit comments

Comments
 (0)