github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/terminalescaper/escaper.go (about) 1 package terminalescaper 2 3 import ( 4 "io" 5 "unicode/utf8" 6 ) 7 8 // Taken from unexported data at golang.org/x/crypto/ssh/terminal 9 // and expanded with data at github.com/keybase/client/go/client:color.go 10 type EscapeCode []byte 11 12 var keyEscape byte = 27 13 var vt100EscapeCodes = []EscapeCode{ 14 // Foreground colors 15 {keyEscape, '[', '3', '0', 'm'}, 16 {keyEscape, '[', '3', '1', 'm'}, 17 {keyEscape, '[', '3', '2', 'm'}, 18 {keyEscape, '[', '3', '3', 'm'}, 19 {keyEscape, '[', '3', '4', 'm'}, 20 {keyEscape, '[', '3', '5', 'm'}, 21 {keyEscape, '[', '3', '6', 'm'}, 22 {keyEscape, '[', '3', '7', 'm'}, 23 {keyEscape, '[', '9', '0', 'm'}, 24 // Reset foreground color 25 {keyEscape, '[', '3', '9', 'm'}, 26 // Bold 27 {keyEscape, '[', '1', 'm'}, 28 // Italic 29 {keyEscape, '[', '3', 'm'}, 30 // Underline 31 {keyEscape, '[', '4', 'm'}, 32 // Reset bold (or doubly underline according to ECMA-48; fallback is code [22m) 33 // See https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters 34 {keyEscape, '[', '2', '1', 'm'}, 35 // Normal intensity 36 {keyEscape, '[', '2', '2', 'm'}, 37 // Reset italic 38 {keyEscape, '[', '2', '3', 'm'}, 39 // Reset underline 40 {keyEscape, '[', '2', '4', 'm'}, 41 // Reset all formatting 42 {keyEscape, '[', '0', 'm'}, 43 } 44 45 // Clean escapes the UTF8 encoded string provided as input so it is safe to print on a unix terminal. 46 // It removes non printing characters and substitutes the vt100 escape character 0x1b with '^['. 47 func Clean(s string) string { 48 return replace(func(r rune) rune { 49 switch { 50 case r >= 32 && r != 127: // Values below 32 (and 127) are special non printing characters (i.e. DEL, ESC, carriage return). 51 return r 52 case r == '\n' || r == '\t': // Allow newlines and tabs. 53 return r 54 case r == rune(keyEscape): 55 // Start of a vt100 escape sequence. If not a color, we will 56 // substitute it with '^[' (this is how it is usually shown, i.e. 57 // in vim). 58 return -1 59 } 60 return -2 61 }, s) 62 } 63 64 func isStartOfColorCode(s string, i int) bool { 65 outer: 66 for _, code := range vt100EscapeCodes { 67 if i+len(code) > len(s) { 68 continue 69 } 70 for j, c := range code { 71 if s[i+j] != c { 72 continue outer 73 } 74 } 75 return true 76 } 77 return false 78 } 79 80 // replace returns a copy of the string s with all its characters modified 81 // according to the mapping function. 82 // If mapping returns -1, the character is substituted with the two character string `^[` (unless it is the start of a color code). 83 // If mapping returns any other negative value, the character is dropped from the string with no replacement. 84 // This function is copied from strings.Map, and is identical except for how -1 is handled (differences are marked). 85 func replace(mapping func(rune) rune, s string) string { 86 // In the worst case, the string can grow when mapped, making 87 // things unpleasant. But it's so rare we barge in assuming it's 88 // fine. It could also shrink but that falls out naturally. 89 90 // The output buffer b is initialized on demand, the first 91 // time a character differs. 92 var b []byte 93 // nbytes is the number of bytes encoded in b. 94 var nbytes int 95 96 for i, c := range s { 97 r := mapping(c) 98 if r == c { 99 continue 100 } 101 102 b = make([]byte, len(s)+utf8.UTFMax) 103 nbytes = copy(b, s[:i]) 104 switch { 105 case r >= 0: 106 if r <= utf8.RuneSelf { 107 b[nbytes] = byte(r) 108 nbytes++ 109 } else { 110 nbytes += utf8.EncodeRune(b[nbytes:], r) 111 } 112 case r == -1 && isStartOfColorCode(s, i): 113 // This branch is NOT part of strings.Map 114 // Allow color codes. 115 b[nbytes] = byte(c) 116 nbytes++ 117 case r == -1: 118 // This else branch is NOT part of strings.Map 119 // Substitute escape code with ^[ to nullify it. 120 b[nbytes] = byte('^') 121 b[nbytes+1] = byte('[') 122 nbytes += 2 123 } 124 125 if c == utf8.RuneError { 126 // RuneError is the result of either decoding 127 // an invalid sequence or '\uFFFD'. Determine 128 // the correct number of bytes we need to advance. 129 _, w := utf8.DecodeRuneInString(s[i:]) 130 i += w 131 } else { 132 i += utf8.RuneLen(c) 133 } 134 135 s = s[i:] 136 break 137 } 138 139 if b == nil { 140 return s 141 } 142 143 for i, c := range s { 144 r := mapping(c) 145 146 // common case 147 if (0 <= r && r <= utf8.RuneSelf) && nbytes < len(b) { 148 b[nbytes] = byte(r) 149 nbytes++ 150 continue 151 } 152 153 // b is not big enough or r is not a ASCII rune. 154 // The isStartOfColorCode check is NOT part of strings.Map 155 switch { 156 case r >= 0: 157 if nbytes+utf8.UTFMax >= len(b) { 158 // Grow the buffer. 159 nb := make([]byte, 2*len(b)) 160 copy(nb, b[:nbytes]) 161 b = nb 162 } 163 nbytes += utf8.EncodeRune(b[nbytes:], r) 164 case r == -1 && isStartOfColorCode(s, i): 165 // This branch is NOT part of strings.Map 166 // Allow color codes. 167 b[nbytes] = byte(c) 168 nbytes++ 169 case r == -1: // This else branch is NOT part of strings.Map, but mirrors the preceding if branch 170 if nbytes+2 >= len(b) { 171 // Grow the buffer. 172 nb := make([]byte, 2*len(b)) 173 copy(nb, b[:nbytes]) 174 b = nb 175 } 176 b[nbytes] = byte('^') 177 b[nbytes+1] = byte('[') 178 nbytes += 2 179 } 180 } 181 182 return string(b[:nbytes]) 183 } 184 185 // CleanBytes is a wrapper around Clean to work on byte slices instead of strings. 186 func CleanBytes(p []byte) []byte { 187 return []byte(Clean(string(p))) 188 } 189 190 // Writer can be used to write data to the underlying io.Writer, while transparently sanitizing it. 191 // If an error occurs writing to a Writer, all subsequent writes will return the error. 192 // Note that the sanitization might alter the size of the actual data being written. 193 type Writer struct { 194 err error 195 io.Writer 196 } 197 198 // Write writes p to the underlying io.Writer, after sanitizing it. 199 // It returns n = len(p) on a successful write (regardless of how much data is written). 200 // This is because the escaping function might alter the actual dimension of the data, but the caller is interested 201 // in knowing how much of what they wanted to write was actually written. In case of errors it (conservatively) returns n=0 202 // and the error, and no other writes are possible. 203 func (w *Writer) Write(p []byte) (int, error) { 204 if w.err != nil { 205 return 0, w.err 206 } 207 _, err := w.Writer.Write(CleanBytes(p)) 208 if err == nil { 209 return len(p), nil 210 } 211 w.err = err 212 return 0, err 213 }