v0.3.0 6800c6a

secret_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close