aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/display.go
blob: 5bcfb5e4bc995650eec0458fc48aee4d52d8a81f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// 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
}