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  }