1package client
2
3import (
4 "errors"
5 "strings"
6 "unicode/utf8"
7)
8
9const uiMaxInputBytes = 16 << 10
10
11var errUIInvalidInput = errors.New("invalid UI input")
12
13// encodeUIText accepts literal printable UTF-8, never terminal controls.
14func encodeUIText(text string) ([]byte, error) {
15 if text == "" || len(text) > uiMaxInputBytes || !utf8.ValidString(text) {
16 return nil, errUIInvalidInput
17 }
18 for _, r := range text {
19 if r < 0x20 || r >= 0x7f && r <= 0x9f {
20 return nil, errUIInvalidInput
21 }
22 }
23 return []byte(text), nil
24}
25
26// encodeUIKeys validates the complete batch as well as each token: individually
27// legal Escape and ASCII tokens must not assemble a terminal reply or paste.
28func encodeUIKeys(keys []string, applicationCursor bool) ([]byte, error) {
29 if len(keys) == 0 || len(keys) > 256 {
30 return nil, errUIInvalidInput
31 }
32 var out []byte
33 for _, key := range keys {
34 encoded, ok := encodeUIKey(key, applicationCursor)
35 if !ok || len(out)+len(encoded) > uiMaxInputBytes {
36 return nil, errUIInvalidInput
37 }
38 out = append(out, encoded...)
39 }
40 if !validUIKeyBatch(out) {
41 return nil, errUIInvalidInput
42 }
43 return out, nil
44}
45
46func encodeUIKey(key string, applicationCursor bool) (string, bool) {
47 if strings.HasPrefix(key, "Alt+") {
48 plain := strings.TrimPrefix(key, "Alt+")
49 switch plain {
50 case "Enter", "Escape", "Tab", "Backspace", "Space":
51 encoded, _ := encodeUIKey(plain, applicationCursor)
52 return "\x1b" + encoded, true
53 }
54 if len(plain) == 1 && plain[0] >= 0x20 && plain[0] <= 0x7e {
55 return "\x1b" + plain, true
56 }
57 return "", false
58 }
59 if strings.HasPrefix(key, "Ctrl+") {
60 plain := strings.TrimPrefix(key, "Ctrl+")
61 if len(plain) == 1 && (plain[0] >= 'A' && plain[0] <= 'Z' || strings.ContainsRune("@[\\]^_", rune(plain[0]))) {
62 return string([]byte{plain[0] & 31}), true
63 }
64 return "", false
65 }
66 if len(key) == 1 && key[0] >= 0x20 && key[0] <= 0x7e {
67 return key, true
68 }
69 switch key {
70 case "Enter":
71 return "\r", true
72 case "Escape":
73 return "\x1b", true
74 case "Tab":
75 return "\t", true
76 case "Backspace":
77 return "\x7f", true
78 case "Space":
79 return " ", true
80 case "PageUp":
81 return "\x1b[5~", true
82 case "PageDown":
83 return "\x1b[6~", true
84 }
85 var final string
86 switch key {
87 case "Up":
88 final = "A"
89 case "Down":
90 final = "B"
91 case "Right":
92 final = "C"
93 case "Left":
94 final = "D"
95 case "Home":
96 final = "H"
97 case "End":
98 final = "F"
99 default:
100 return "", false
101 }
102 prefix := "\x1b["
103 if applicationCursor {
104 prefix = "\x1bO"
105 }
106 return prefix + final, true
107}
108
109func validUIKeyBatch(data []byte) bool {
110 for i := 0; i < len(data); i++ {
111 if data[i] != 0x1b {
112 continue
113 }
114 // A bare Escape is the only intentionally incomplete escape sequence.
115 if i+1 == len(data) {
116 continue
117 }
118 switch data[i+1] {
119 case ']', 'P', '_', '^', 'X':
120 // A lone Alt character is valid, but adding a suffix would turn
121 // it into an OSC/DCS/APC/string prefix.
122 if i+2 < len(data) {
123 return false
124 }
125 continue
126 case '[':
127 // Alt+[ is a valid one-character key. Once it has a suffix, only
128 // one of the finite CSI keyboard sequences is accepted.
129 if i+2 >= len(data) {
130 continue
131 }
132 if strings.ContainsRune("ABCDHF", rune(data[i+2])) {
133 i += 2
134 continue
135 }
136 if i+3 < len(data) && (data[i+2] == '5' || data[i+2] == '6') && data[i+3] == '~' {
137 i += 3
138 continue
139 }
140 return false
141 case 'O':
142 // Alt+O is valid on its own; otherwise accept only application
143 // cursor sequences.
144 if i+2 >= len(data) {
145 continue
146 }
147 if !strings.ContainsRune("ABCDHF", rune(data[i+2])) {
148 return false
149 }
150 i += 2
151 default:
152 // The remaining form is Alt+one character. Its second byte may be
153 // one of the explicit control-key aliases, but no other control
154 // sequence or C1 byte is admitted. A second Escape must be checked
155 // again as a possible sequence introducer; otherwise ESC ESC ]...
156 // could hide an OSC response behind a valid Alt+Escape token.
157 next := data[i+1]
158 if next == 0x1b {
159 continue
160 }
161 if next < 0x20 && next != '\r' && next != '\t' || next >= 0x80 && next <= 0x9f {
162 return false
163 }
164 i++
165 }
166 }
167 return true
168}