// SPDX-License-Identifier: GPL-3.0-or-later package main import ( "fmt" "io" "strings" "unicode" "unicode/utf8" "git.labunix.xyz/krino/internal/cond" ) // display makes s safe to print to a terminal (spec ยง15.1). File names, // paths, reasons and tool messages come from outside krino: a control // character in one could move the cursor, recolour or erase what krino // prints, and a bidirectional control could reorder it, so a name could // hide a step or fake one. Every C0 control, DEL, C1 control, Unicode // bidirectional embedding, override or isolate, and every byte that is not // valid UTF-8 is shown as an escape instead - \x1b for a byte, \u202e for // a rune. Everything else, backslash included, is printed as it is. Text // that is plain printable ASCII is returned without copying. func display(s string) string { plain := true for i := 0; i < len(s); i++ { if c := s[i]; c < 0x20 || c >= 0x7f { plain = false break } } if plain { return s } var b strings.Builder for i := 0; i < len(s); { r, size := utf8.DecodeRuneInString(s[i:]) switch { case r == utf8.RuneError && size == 1: fmt.Fprintf(&b, `\x%02x`, s[i]) case r < 0x20 || r == 0x7f: fmt.Fprintf(&b, `\x%02x`, r) case controlRune(r): fmt.Fprintf(&b, `\u%04x`, r) default: b.WriteString(s[i : i+size]) } i += size } return b.String() } // controlRune reports whether r is a C1 control, a Unicode bidirectional // control (embeddings, overrides, isolates, and the marks U+061C, U+200E and // U+200F), or a line or paragraph separator: code points a terminal acts on, // or that reorder or break the lines krino prints. func controlRune(r rune) bool { return (r >= 0x80 && r <= 0x9f) || unicode.Is(unicode.Bidi_Control, r) || unicode.In(r, unicode.Zl, unicode.Zp) } // safeWriter writes through display: every error and warning krino writes // to stderr may quote a file name or a tool's message, and nothing krino // itself writes there is styled (review M5). Each Write is one message line: // only its final newline is kept, and a newline inside it - from quoted text // - is escaped, so it cannot start a line that reads as krino's own // (re-review term F1). A message of several lines is written with one Write // per line. type safeWriter struct{ w io.Writer } func (s safeWriter) Write(p []byte) (int, error) { text := string(p) end := "" if strings.HasSuffix(text, "\n") { text, end = text[:len(text)-1], "\n" } if _, err := io.WriteString(s.w, display(text)+end); err != nil { return 0, err } return len(p), nil } // jsonSafe escapes, in an encoded JSON document, the code points // encoding/json leaves raw that a terminal acts on - DEL, C1 controls, // bidirectional controls - as \uXXXX. They can only occur inside strings, // where the escape is the same value (review terminal F4). func jsonSafe(b []byte) []byte { s := string(b) var out strings.Builder out.Grow(len(s)) for _, r := range s { if r == 0x7f || controlRune(r) { fmt.Fprintf(&out, `\u%04x`, r) continue } out.WriteRune(r) } return []byte(out.String()) } // displayTrace returns a copy of t with every label and error passed through // display, so a newline or escape in a file name or a tool's message cannot // forge or disturb an explain trace line (review M5). func displayTrace(t *cond.Trace) *cond.Trace { c := *t c.Label, c.Err = display(t.Label), display(t.Err) c.Children = make([]*cond.Trace, len(t.Children)) for i, ch := range t.Children { c.Children[i] = displayTrace(ch) } return &c }