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"
"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
}
|