1package keyboard
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "reflect"
8 "testing"
9 "time"
10
11 xkb "github.com/bnema/purego-xkbcommon"
12 "github.com/bnema/purego-xkbcommon/raw"
13)
14
15func TestSecretASCIIAndMultibyteWithoutRetainedText(t *testing.T) {
16 k := requiredKeyboard(t, "en_US.UTF-8")
17 defer k.Close()
18 for _, tc := range []struct {
19 layout string
20 code uint32
21 mod string
22 want []byte
23 }{
24 {"us", 16, "", []byte{'q'}}, {"de", 18, "Mod5", []byte{0xe2, 0x82, 0xac}},
25 } {
26 if err := k.Rules(tc.layout, ""); err != nil {
27 t.Fatal(err)
28 }
29 if tc.mod != "" {
30 idx, err := k.keymap.ModIndex(tc.mod)
31 if err != nil {
32 t.Fatal(err)
33 }
34 if err := k.Mask(1<<idx, 0, 0, 0, 0, 0); err != nil {
35 t.Fatal(err)
36 }
37 }
38 var scratch [512]byte
39 calls := 0
40 err := k.EventSecret(tc.code, true, time.Now(), scratch[:], func(key SecretKey, data []byte) {
41 calls++
42 if key.Kind != SecretBytes || !key.Pressed || key.Repeat || !bytes.Equal(data, tc.want) {
43 t.Fatal("wrong scoped bytes/metadata")
44 }
45 if cap(data) != len(data) {
46 t.Fatal("callback can access scratch beyond payload")
47 }
48 if len(data) > 0 && &data[0] != &scratch[0] {
49 t.Fatal("bytes copied instead of caller scratch")
50 }
51 })
52 if err != nil || calls != 1 {
53 t.Fatal(calls, err)
54 }
55 if !bytes.Equal(scratch[:], make([]byte, len(scratch))) || len(k.held) != 0 {
56 t.Fatal("plaintext retained")
57 }
58 if key := k.secretHeld[tc.code]; key.Kind != SecretBytes || key.Physical != tc.code {
59 t.Fatal("missing scalar held metadata")
60 }
61 }
62 for _, typ := range []reflect.Type{reflect.TypeFor[SecretKey]()} {
63 for i := 0; i < typ.NumField(); i++ {
64 if typ.Field(i).Type.Kind() == reflect.String || typ.Field(i).Type.Kind() == reflect.Slice {
65 t.Fatal("secret metadata retains payload")
66 }
67 }
68 }
69}
70
71func TestSecretComposeAndClear(t *testing.T) {
72 k := requiredKeyboard(t, "en_US.UTF-8")
73 defer k.Close()
74 if err := k.Rules("de", ""); err != nil {
75 t.Fatal(err)
76 }
77 var scratch [512]byte
78 now := time.Now()
79 calls := 0
80 consume := func(key SecretKey, data []byte) {
81 if !key.Pressed {
82 return
83 }
84 calls++
85 if key.Kind != SecretBytes || !bytes.Equal(data, []byte{0xc3, 0xaa}) {
86 t.Fatal("compose bytes incorrect")
87 }
88 }
89 for _, ev := range []struct {
90 code uint32
91 down bool
92 }{{41, true}, {41, false}, {18, true}} {
93 if err := k.EventSecret(ev.code, ev.down, now, scratch[:], consume); err != nil {
94 t.Fatal(err)
95 }
96 }
97 if calls != 1 || k.composePending {
98 t.Fatal("compose did not finish")
99 }
100 k.ClearSecret()
101 if len(k.secretHeld) != 0 || k.repeating != 0 {
102 t.Fatal("clear retained held/repeat")
103 }
104 if err := k.EventSecret(41, true, now, scratch[:], consume); err != nil {
105 t.Fatal(err)
106 }
107 k.Focus(false)
108 k.Focus(true)
109 if err := k.EventSecret(18, true, now, scratch[:], func(key SecretKey, data []byte) {
110 if !bytes.Equal(data, []byte{'e'}) {
111 t.Fatal("focus loss preserved compose")
112 }
113 }); err != nil {
114 t.Fatal(err)
115 }
116}
117
118func TestSecretControlsAndRepeat(t *testing.T) {
119 k := requiredKeyboard(t, "en_US.UTF-8")
120 defer k.Close()
121 if err := k.Rules("us", ""); err != nil {
122 t.Fatal(err)
123 }
124 k.RepeatInfo(10, 100*time.Millisecond)
125 now := time.Now()
126 var scratch [512]byte
127 for _, tc := range []struct {
128 code uint32
129 kind SecretKind
130 repeat bool
131 }{{14, SecretBackspace, true}, {28, SecretReturn, false}, {1, SecretEscape, false}} {
132 k.ClearSecret()
133 calls := 0
134 consume := func(key SecretKey, data []byte) {
135 if len(data) != 0 || key.Kind != tc.kind {
136 t.Fatal("control leaked text")
137 }
138 calls++
139 }
140 if err := k.EventSecret(tc.code, true, now, scratch[:], consume); err != nil {
141 t.Fatal(err)
142 }
143 if err := k.EventSecret(tc.code, true, now, scratch[:], consume); err != nil {
144 t.Fatal(err)
145 }
146 got, err := k.TickSecret(now.Add(100*time.Millisecond), scratch[:], consume)
147 if err != nil || got != tc.repeat {
148 t.Fatal(got, err)
149 }
150 want := 1
151 if tc.repeat {
152 want = 3
153 }
154 if calls != want {
155 t.Fatalf("control callbacks=%d want=%d", calls, want)
156 }
157 }
158 k.ClearSecret()
159 idx, err := k.keymap.ModIndex("Control")
160 if err != nil {
161 t.Fatal(err)
162 }
163 if err := k.Mask(1<<idx, 0, 0, 0, 0, 0); err != nil {
164 t.Fatal(err)
165 }
166 if err := k.EventSecret(22, true, now, scratch[:], func(key SecretKey, data []byte) {
167 if key.Kind != SecretClear || len(data) != 0 {
168 t.Fatal("CtrlU not named control")
169 }
170 }); err != nil {
171 t.Fatal(err)
172 }
173 k.ClearSecret()
174 _ = k.Mask(0, 0, 0, 0, 0, 0)
175 if err := k.EventSecret(16, true, now, scratch[:], func(SecretKey, []byte) {}); err != nil {
176 t.Fatal(err)
177 }
178 shift, _ := k.keymap.ModIndex("Shift")
179 _ = k.Mask(1<<shift, 0, 0, 0, 0, 0)
180 got, err := k.TickSecret(now.Add(100*time.Millisecond), scratch[:], func(key SecretKey, data []byte) {
181 if !key.Repeat || key.Logical != raw.XKB_KEY_Q || !bytes.Equal(data, []byte{'Q'}) {
182 t.Fatal("repeat cached press-time bytes")
183 }
184 })
185 if err != nil || !got {
186 t.Fatal(got, err)
187 }
188 if _, ok := k.Tick(now.Add(time.Second)); ok {
189 t.Fatal("ordinary repeat exposed secret")
190 }
191 k.Focus(false)
192 if got, err := k.TickSecret(now.Add(time.Second), scratch[:], func(SecretKey, []byte) { t.Fatal("repeat after focus loss") }); got || err != nil {
193 t.Fatal(got, err)
194 }
195}
196
197func TestSecretBoundsAndOrdinaryIsolation(t *testing.T) {
198 k := requiredKeyboard(t, "")
199 defer k.Close()
200 if err := k.Rules("us", ""); err != nil {
201 t.Fatal(err)
202 }
203 now := time.Now()
204 if _, err := k.Event(16, true, now); err != nil {
205 t.Fatal(err)
206 }
207 for _, size := range []int{0, 513} {
208 buf := make([]byte, size)
209 if err := k.EventSecret(30, true, now, buf, func(SecretKey, []byte) { t.Fatal("invalid bound callback") }); !errors.Is(err, xkb.ErrUTF8Buffer) {
210 t.Fatal(err)
211 }
212 }
213 short := []byte{0xff}
214 if err := k.EventSecret(30, true, now, short, func(SecretKey, []byte) { t.Fatal("truncated callback") }); !errors.Is(err, io.ErrShortBuffer) {
215 t.Fatal(err)
216 }
217 if short[0] != 0 || len(k.held) != 0 || len(k.secretHeld) != 0 {
218 t.Fatal("error retained bytes/state")
219 }
220 var scratch [512]byte
221 if err := k.EventSecret(30, true, now, scratch[:], func(SecretKey, []byte) {}); err != nil {
222 t.Fatal(err)
223 }
224 if key, err := k.Event(30, false, now); err != nil || key.Text != "" || key.Name != "" {
225 t.Fatal("ordinary release exposed secret")
226 }
227}
228
229func TestSecretRepeatBoundedAllocations(t *testing.T) {
230 k := requiredKeyboard(t, "")
231 defer k.Close()
232 if err := k.Rules("us", ""); err != nil {
233 t.Fatal(err)
234 }
235 k.RepeatInfo(20, 0)
236 var scratch [512]byte
237 now := time.Now()
238 consume := func(SecretKey, []byte) {}
239 if err := k.EventSecret(16, true, now, scratch[:], consume); err != nil {
240 t.Fatal(err)
241 }
242 allocs := testing.AllocsPerRun(100, func() {
243 now = now.Add(time.Second)
244 if _, err := k.TickSecret(now, scratch[:], consume); err != nil {
245 t.Fatal(err)
246 }
247 })
248 // Fixed native-dispatch/modifier-query overhead is allowed; no allocation
249 // should scale with payload length or retain a text-bearing Key.
250 t.Logf("secret repeat fixed allocations: %v", allocs)
251 if allocs > 24 {
252 t.Fatalf("secret repeat allocations=%v", allocs)
253 }
254}
255
256func TestSecretControlsCancelPriorPrintableRepeat(t *testing.T) {
257 for _, tc := range []struct {
258 code uint32
259 kind SecretKind
260 ctrl bool
261 }{{28, SecretReturn, false}, {1, SecretEscape, false}, {22, SecretClear, true}} {
262 k := requiredKeyboard(t, "en_US.UTF-8")
263 if err := k.Rules("us", ""); err != nil {
264 t.Fatal(err)
265 }
266 k.RepeatInfo(20, 0)
267 now := time.Now()
268 var scratch [512]byte
269 if err := k.EventSecret(16, true, now, scratch[:], func(SecretKey, []byte) {}); err != nil {
270 t.Fatal(err)
271 }
272 if tc.ctrl {
273 idx, err := k.keymap.ModIndex("Control")
274 if err != nil {
275 t.Fatal(err)
276 }
277 if err := k.Mask(1<<idx, 0, 0, 0, 0, 0); err != nil {
278 t.Fatal(err)
279 }
280 }
281 if err := k.EventSecret(tc.code, true, now, scratch[:], func(key SecretKey, data []byte) {
282 if key.Kind != tc.kind || len(data) != 0 || k.repeating != 0 || !k.next.IsZero() {
283 t.Fatal("control callback preceded repeat cancellation")
284 }
285 }); err != nil {
286 t.Fatal(err)
287 }
288 if err := k.Mask(0, 0, 0, 0, 0, 0); err != nil {
289 t.Fatal(err)
290 }
291 if got, err := k.TickSecret(now.Add(time.Second), scratch[:], func(SecretKey, []byte) { t.Fatal("bytes after control") }); got || err != nil {
292 t.Fatal(got, err)
293 }
294 k.Close()
295 }
296}
297
298func TestSecretValidationErrorsClearAllInputState(t *testing.T) {
299 for _, ordinary := range []bool{false, true} {
300 for _, tick := range []bool{false, true} {
301 for _, invalid := range []string{"empty", "oversize", "nil-callback"} {
302 k := requiredKeyboard(t, "en_US.UTF-8")
303 if err := k.Rules("de", ""); err != nil {
304 t.Fatal(err)
305 }
306 k.RepeatInfo(20, 0)
307 now := time.Now()
308 var scratch [512]byte
309 if ordinary {
310 if _, err := k.Event(16, true, now); err != nil {
311 t.Fatal(err)
312 }
313 } else {
314 if err := k.EventSecret(16, true, now, scratch[:], func(SecretKey, []byte) {}); err != nil {
315 t.Fatal(err)
316 }
317 }
318 // Real native composing state must also be reset on validation failure.
319 if _, err := k.compose.Feed(raw.XKB_KEY_dead_circumflex); err != nil {
320 t.Fatal(err)
321 }
322 k.composePending = true
323 buf := scratch[:]
324 consume := func(SecretKey, []byte) { t.Fatal("invalid call delivered") }
325 want := xkb.ErrUTF8Buffer
326 switch invalid {
327 case "empty":
328 buf = nil
329 case "oversize":
330 buf = make([]byte, 513)
331 case "nil-callback":
332 consume = nil
333 want = errSecretCallback
334 }
335 var err error
336 if tick {
337 _, err = k.TickSecret(now.Add(time.Second), buf, consume)
338 } else {
339 err = k.EventSecret(16, false, now, buf, consume)
340 } // release validation must not retain held state
341 if !errors.Is(err, want) {
342 t.Fatal(invalid, err)
343 }
344 status, err := k.compose.Status()
345 if err != nil || status != raw.XKB_COMPOSE_NOTHING {
346 t.Fatal("compose survived validation error")
347 }
348 if len(k.held) != 0 || len(k.secretHeld) != 0 || k.repeating != 0 || !k.next.IsZero() || k.composePending {
349 t.Fatal("validation error retained ordinary/secret state")
350 }
351 if _, ok := k.Tick(now.Add(time.Second)); ok {
352 t.Fatal("ordinary repeat after failed secret entry")
353 }
354 if got, err := k.TickSecret(now.Add(time.Second), scratch[:], func(SecretKey, []byte) { t.Fatal("secret repeat after failed validation") }); got || err != nil {
355 t.Fatal(got, err)
356 }
357 k.Close()
358 }
359 }
360 }
361}
362
363func TestSecretCallbackPanicWipesScratch(t *testing.T) {
364 k := requiredKeyboard(t, "")
365 defer k.Close()
366 if err := k.Rules("us", ""); err != nil {
367 t.Fatal(err)
368 }
369 var scratch [512]byte
370 func() {
371 defer func() {
372 if recover() == nil {
373 t.Fatal("callback did not panic")
374 }
375 }()
376 _ = k.EventSecret(16, true, time.Now(), scratch[:], func(_ SecretKey, data []byte) {
377 if !bytes.Equal(data, []byte{'q'}) {
378 t.Fatal("no bytes before panic")
379 }
380 panic("consumer failure")
381 })
382 }()
383 if !bytes.Equal(scratch[:], make([]byte, len(scratch))) {
384 t.Fatal("panic retained scratch payload")
385 }
386}
387
388func TestSecretRepeatTracksLayoutGroup(t *testing.T) {
389 k := requiredKeyboard(t, "")
390 defer k.Close()
391 if err := k.Rules("us,fr", ""); err != nil {
392 t.Fatal(err)
393 }
394 k.RepeatInfo(20, 0)
395 now := time.Now()
396 var scratch [512]byte
397 if err := k.EventSecret(16, true, now, scratch[:], func(_ SecretKey, data []byte) {
398 if !bytes.Equal(data, []byte{'q'}) {
399 t.Fatal("initial group")
400 }
401 }); err != nil {
402 t.Fatal(err)
403 }
404 if err := k.Mask(0, 0, 0, 0, 0, 1); err != nil {
405 t.Fatal(err)
406 }
407 if ok, err := k.TickSecret(now.Add(time.Second), scratch[:], func(key SecretKey, data []byte) {
408 if !key.Repeat || !bytes.Equal(data, []byte{'a'}) {
409 t.Fatal("repeat cached previous group")
410 }
411 }); err != nil || !ok {
412 t.Fatal(ok, err)
413 }
414}