-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvis.go
More file actions
184 lines (176 loc) · 5.68 KB
/
Copy pathvis.go
File metadata and controls
184 lines (176 loc) · 5.68 KB
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package gotmucks
import "strings"
// tmux escapes a string with vis(3) in its C style in two places: on the way
// out of show-options, and on the way *in* to a window or session name, which
// is stored escaped and read back that way through "#{window_name}". It is
// one encoding, so both halves of it live here.
//
// The set was measured rather than read. scripts/probe-roundtrip.sh sets every
// printable byte as an option value and as a window name on 3.2a and prints
// both back: of the printable bytes only '\' and '$' are touched, and '$'
// because tmux means the value to be feedable back through set-option, where a
// bare '$' is a variable. Below space it is vis(3)'s C style — "\a" "\b" "\t"
// "\n" "\v" "\f" "\r" — and three octal digits for the rest.
//
// An option value gets one thing more, which is not vis and does not belong
// here: tmux's args_escape puts a bare backslash in front of two shapes before
// this encoding runs. It is undone in [unprefixOptionValue], because a name
// never carries it and widening the decoder would change what a name decodes
// to.
//
// The probe's "must be empty" diff is the assertion that keeps this honest: an
// alteration the package undoes neither way is a value it hands back wrong,
// and the probe exits non-zero when it finds one. The sweep behind that diff
// asks each byte in four positions rather than one, since round ten found that
// tmux's answer depends on where in the value the byte sits and every table in
// this tree had put it in the middle.
// visDecode undoes that escaping.
//
// This is one left-to-right pass rather than a sequence of replacements. Two
// passes over the whole string — undoing "\\" and then "\t" or the other way
// round — is the shape that turns a literal backslash followed by a t into a
// tab, and it happens to be safe here only because of the order the escapes
// are written in.
func visDecode(v string) string {
if !strings.Contains(v, `\`) {
return v
}
var b strings.Builder
b.Grow(len(v))
for i := 0; i < len(v); i++ {
if v[i] != '\\' || i+1 >= len(v) {
b.WriteByte(v[i])
continue
}
i++
switch c := v[i]; c {
case 'a':
b.WriteByte('\a')
case 'b':
b.WriteByte('\b')
case 'f':
b.WriteByte('\f')
case 'n':
b.WriteByte('\n')
case 'r':
b.WriteByte('\r')
case 't':
b.WriteByte('\t')
case 'v':
b.WriteByte('\v')
case 's':
b.WriteByte(' ')
case '\\', '"', '\'', '$':
// '$' is the one that took eight review rounds to notice, because
// it is the only escape here that a value picks up silently and
// keeps: a caller that reads an option, edits it and writes it
// back gained a backslash every cycle for as long as the case was
// missing.
b.WriteByte(c)
default:
if o, ok := octalByte(v, i); ok {
b.WriteByte(o)
i += 2
continue
}
// Not an escape this package knows. Keep both bytes: the value is
// worth more than the objection.
b.WriteByte('\\')
b.WriteByte(c)
}
}
return b.String()
}
// octalByte decodes exactly three octal digits starting at i.
func octalByte(s string, i int) (byte, bool) {
if i+3 > len(s) {
return 0, false
}
n := 0
for j := i; j < i+3; j++ {
if s[j] < '0' || s[j] > '7' {
return 0, false
}
n = n*8 + int(s[j]-'0')
}
if n > 0xFF {
return 0, false
}
return byte(n), true
}
// visEncode applies the escaping, for the one call that would otherwise skip
// it.
//
// rename-window, rename-session and new-session -s all run a name through
// tmux's own vis before storing it. new-session -n does not — verified on
// 3.2a, where "-n" stores a raw tab and a raw backslash exactly as given while
// every other path stores "\t" and "\\". Without this, the same name would
// read back differently according to which call set it, and a name containing
// a backslash would come back as something else entirely once [visDecode] ran
// over it.
//
// It matches what 3.2a stores for every byte a name can carry, with one
// deliberate difference: a byte above 0x7f is left alone, where tmux writes
// three octal digits for one that is not part of a valid UTF-8 sequence. Both
// forms decode back to the same bytes, which is the only property that has to
// hold — nothing can observe the stored form except through the decoder.
//
// A NUL is encoded rather than passed through, which lets new-session -n carry
// one — verified through the package on 3.2a, where a window named "a\x00b"
// reads back as "a\x00b". No other call can: Go refuses to build an argument
// vector containing a NUL, so rename-window never starts tmux at all and fails
// with "invalid argument" from fork/exec rather than truncating the name. That
// error arrives wrapped in an [ExitError] whose Code is -1 and whose Stderr is
// empty, since there was no process to write one.
func visEncode(s string) string {
i := 0
for ; i < len(s); i++ {
if visEscape(s[i]) != "" {
break
}
}
if i == len(s) {
return s
}
var b strings.Builder
b.Grow(len(s) + 8)
b.WriteString(s[:i])
for ; i < len(s); i++ {
if e := visEscape(s[i]); e != "" {
b.WriteString(e)
continue
}
b.WriteByte(s[i])
}
return b.String()
}
// visEscape returns the escape tmux writes for a byte, or "" for a byte it
// writes as it stands.
func visEscape(c byte) string {
switch c {
case '\\':
return `\\`
case '$':
return `\$`
case '\a':
return `\a`
case '\b':
return `\b`
case '\t':
return `\t`
case '\n':
return `\n`
case '\v':
return `\v`
case '\f':
return `\f`
case '\r':
return `\r`
}
if c >= 0x20 && c != 0x7f {
return ""
}
// Three octal digits, which for a byte in this range never needs more
// than the two bits the first digit can hold.
return string([]byte{'\\', '0' + c>>6, '0' + (c>>3)&7, '0' + c&7})
}