1package xkbcommon
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "os/exec"
8 "regexp"
9 "strings"
10 "testing"
11 "unsafe"
12
13 "github.com/bnema/purego-xkbcommon/raw"
14)
15
16func live(t *testing.T) *Context {
17 t.Helper()
18 if err := Available(); err != nil {
19 if strings.Contains(err.Error(), "load libxkbcommon.so.0") {
20 t.Skipf("live libxkbcommon.so.0 absent: %v", err)
21 }
22 t.Fatal(err)
23 }
24 c, e := NewContext()
25 if e != nil {
26 t.Fatal(e)
27 }
28 t.Cleanup(func() { c.Close() })
29 return c
30}
31func rules(t *testing.T, c *Context, layout string) *Keymap {
32 t.Helper()
33 k, e := c.NewKeymapRules("evdev", "pc105", layout, "", "")
34 if e != nil {
35 t.Fatalf("rules %s: %v", layout, e)
36 }
37 t.Cleanup(func() { k.Close() })
38 return k
39}
40func state(t *testing.T, k *Keymap) *State {
41 t.Helper()
42 s, e := k.NewState()
43 if e != nil {
44 t.Fatal(e)
45 }
46 t.Cleanup(func() { s.Close() })
47 return s
48}
49func TestABI(t *testing.T) {
50 if unsafe.Sizeof(raw.RuleNames{}) != 5*unsafe.Sizeof(uintptr(0)) {
51 t.Fatal("rule names ABI")
52 }
53 if unsafe.Sizeof(raw.Keycode(0)) != 4 || unsafe.Sizeof(raw.Keysym(0)) != 4 || unsafe.Sizeof(raw.ModMask(0)) != 4 {
54 t.Fatal("scalar ABI")
55 }
56}
57func TestSymbols(t *testing.T) {
58 live(t)
59 if len(raw.Symbols) < 25 {
60 t.Fatal("incomplete required symbol set")
61 }
62}
63func TestMalformedAndLifetime(t *testing.T) {
64 c := live(t)
65 if _, e := c.NewKeymapString("garbage"); e == nil {
66 t.Fatal("malformed keymap compiled")
67 }
68 if _, e := c.NewKeymapString("foo\x00bar"); e == nil {
69 t.Fatal("embedded NUL")
70 }
71 k := rules(t, c, "us")
72 s := state(t, k)
73 c.Close()
74 k.Close()
75 for i := 0; i < 3; i++ {
76 c.Close()
77 k.Close()
78 }
79 if _, e := k.NewState(); !errors.Is(e, ErrClosed) {
80 t.Fatal(e)
81 }
82 v, e := s.UTF8(38)
83 if e != nil || v != "a" {
84 t.Fatalf("state after parent close: %q %v", v, e)
85 }
86 s.Close()
87 s.Close()
88 if _, e := s.KeySym(38); !errors.Is(e, ErrClosed) {
89 t.Fatal(e)
90 }
91}
92func TestFD(t *testing.T) {
93 c := live(t)
94 short := keymapFile(t, []byte("short"))
95 if _, e := c.NewKeymapFD(int(short.Fd()), 4096); e == nil || !strings.Contains(e.Error(), "short read") {
96 t.Fatalf("truncated keymap: %v", e)
97 }
98 if _, e := short.Stat(); e != nil {
99 t.Fatalf("FD was closed on error: %v", e)
100 }
101 for _, size := range []int{0, 16<<20 + 1} {
102 if _, e := c.NewKeymapFD(int(short.Fd()), size); e == nil {
103 t.Fatalf("accepted size %d", size)
104 }
105 }
106 if _, e := c.NewKeymapFD(-1, 5); e == nil {
107 t.Fatal("negative FD")
108 }
109 if _, e := exec.LookPath("xkbcli"); e != nil {
110 t.Skipf("xkbcli compile-keymap reference unavailable: %v", e)
111 }
112 out, e := exec.Command("xkbcli", "compile-keymap", "--rules", "evdev", "--model", "pc105", "--layout", "fr", "--output-format", "1").Output()
113 if e != nil {
114 t.Fatalf("reference xkbcli: %v", e)
115 }
116 f := keymapFile(t, append(out, 0))
117 other, e := c.NewKeymapFD(int(f.Fd()), len(out)+1)
118 if e != nil {
119 t.Fatal(e)
120 }
121 other.Close()
122 if _, e = f.Stat(); e != nil {
123 t.Fatalf("FD was closed: %v", e)
124 }
125 if _, e = c.NewKeymapFD(int(f.Fd()), len(out)+2); e == nil || !strings.Contains(e.Error(), "short read") {
126 t.Fatalf("truncated keymap: %v", e)
127 }
128 if _, e = c.NewKeymapFD(int(f.Fd()), len(out)); e != nil {
129 t.Fatalf("non-NUL text: %v", e)
130 }
131}
132
133func keymapFile(t *testing.T, data []byte) *os.File {
134 t.Helper()
135 f, e := os.CreateTemp(t.TempDir(), "keymap")
136 if e != nil {
137 t.Fatal(e)
138 }
139 t.Cleanup(func() { f.Close() })
140 if _, e = f.Write(data); e != nil {
141 t.Fatal(e)
142 }
143 return f
144}
145
146func TestLoaderDiagnostics(t *testing.T) {
147 _, e := resolveLibrary("/no/such/libxkbcommon.so", raw.Symbols)
148 if e == nil || !strings.Contains(e.Error(), "load /no/such/libxkbcommon.so") {
149 t.Fatalf("missing library: %v", e)
150 }
151 if e := Available(); e != nil {
152 if strings.Contains(e.Error(), "load libxkbcommon.so.0") {
153 t.Skipf("missing symbol test needs libxkbcommon.so.0: %v", e)
154 }
155 t.Fatal(e)
156 }
157 _, e = resolveLibrary("libxkbcommon.so.0", []string{"xkb_nonexistent_test_symbol"})
158 if e == nil || !strings.Contains(e.Error(), "missing xkb_nonexistent_test_symbol") {
159 t.Fatalf("missing symbol: %v", e)
160 }
161}
162func TestLayoutsAndDifferential(t *testing.T) {
163 c := live(t)
164 for _, tc := range []struct {
165 layout string
166 code uint32
167 want string
168 alt bool
169 }{{"us", 38, "a", false}, {"fr", 24, "a", false}, {"de", 29, "z", false}, {"de", 26, "€", true}, {"fr", 26, "€", true}} {
170 t.Run(tc.layout+tc.want, func(t *testing.T) {
171 k := rules(t, c, tc.layout)
172 s := state(t, k)
173 if tc.alt {
174 idx, e := k.ModIndex("LevelThree")
175 if e != nil || idx == ^uint32(0) || idx >= 32 {
176 t.Fatalf("LevelThree: %d %v", idx, e)
177 }
178 s.UpdateMask(1<<idx, 0, 0, 0, 0, 0)
179 }
180 got, e := s.UTF8(tc.code)
181 if e != nil || got != tc.want {
182 t.Fatalf("utf8 %q want %q: %v", got, tc.want, e)
183 }
184 sym, e := s.KeySym(tc.code)
185 if e != nil {
186 t.Fatal(e)
187 }
188 name, e := KeysymName(sym)
189 if e != nil || name == "" {
190 t.Fatalf("name %q: %v", name, e)
191 }
192 back, e := KeysymFromName(name)
193 if e != nil || back != sym {
194 t.Fatalf("name roundtrip %d %d %v", sym, back, e)
195 }
196 // Compare safe calls to the independently resolved native entry points for this real rules keymap.
197 nativeSym := loader.f.Xkb_state_key_get_one_sym(s.h, tc.code)
198 if nativeSym != sym {
199 t.Fatalf("native sym %d != %d", nativeSym, sym)
200 }
201 nativeN := loader.f.Xkb_state_key_get_utf8(s.h, tc.code, nil, 0)
202 if nativeN != int32(len(got)) {
203 t.Fatalf("native bytes %d != %d", nativeN, len(got))
204 }
205 })
206 }
207 k := rules(t, c, "us,de")
208 s := state(t, k)
209 n, e := k.LayoutCount()
210 if e != nil || n != 2 {
211 t.Fatalf("layouts %d %v", n, e)
212 }
213 s.UpdateMask(0, 0, 0, 0, 0, 1)
214 group, e := s.Layout()
215 if e != nil || group != 1 {
216 t.Fatalf("group %d %v", group, e)
217 }
218 v, _ := s.UTF8(29)
219 if v != "z" {
220 t.Fatalf("group 1 %q", v)
221 }
222 shift, e := k.ModIndex("Shift")
223 if e != nil || shift >= 32 {
224 t.Fatalf("shift %d %v", shift, e)
225 }
226 s.UpdateMask(1<<shift, 0, 0, 0, 0, 1)
227 v, _ = s.UTF8(29)
228 if v != "Z" {
229 t.Fatalf("shift group 1 %q", v)
230 }
231 mods, _ := s.Mods()
232 if mods&(1<<shift) == 0 {
233 t.Fatalf("mods %#x", mods)
234 }
235}
236
237// These expectations are fixed evdev/XKB facts, not values computed by the
238// binding under test. Every case compiles through the Wayland FD path.
239func TestFDReferenceKeyEvents(t *testing.T) {
240 c := live(t)
241 for _, tc := range []struct {
242 layout, key, utf8, modifiers string
243 evdev, group, sym uint32
244 }{
245 {"us", "q", "q", "", 16, 0, raw.XKB_KEY_q},
246 {"us", "Q", "Q", "Shift", 16, 0, raw.XKB_KEY_Q},
247 {"us", "Q", "Q", "Lock", 16, 0, raw.XKB_KEY_Q},
248 {"fr", "a", "a", "", 16, 0, raw.XKB_KEY_a},
249 {"fr", "€", "€", "Mod5", 18, 0, raw.XKB_KEY_EuroSign},
250 {"de", "z", "z", "", 21, 0, raw.XKB_KEY_z},
251 {"de", "@", "@", "Mod5", 16, 0, raw.XKB_KEY_at},
252 {"de", "dead_circumflex", "", "", 41, 0, raw.XKB_KEY_dead_circumflex},
253 {"us,fr", "a", "a", "", 16, 1, raw.XKB_KEY_a},
254 } {
255 t.Run(tc.layout+"/"+tc.key+"/"+tc.modifiers, func(t *testing.T) {
256 // Wayland-format keymaps pinned in testdata/keymaps were produced by
257 // `xkbcli compile-keymap --output-format 1` (libxkbcommon 1.13.2), so
258 // the FD path is always exercised without optional tools.
259 text, e := os.ReadFile("testdata/keymaps/" + strings.ReplaceAll(tc.layout, ",", "_") + ".xkb")
260 if e != nil {
261 t.Fatal(e)
262 }
263 f := keymapFile(t, append(text, 0))
264 k, e := c.NewKeymapFD(int(f.Fd()), len(text)+1)
265 if e != nil {
266 t.Fatal(e)
267 }
268 t.Cleanup(func() { k.Close() })
269 s := state(t, k)
270 var mask uint32
271 if tc.modifiers != "" {
272 idx, e := k.ModIndex(tc.modifiers)
273 if e != nil || idx >= 32 {
274 t.Fatalf("modifier %s: %d %v", tc.modifiers, idx, e)
275 }
276 mask = 1 << idx
277 }
278 var depressed, locked uint32
279 if tc.modifiers == "Lock" {
280 locked = mask
281 } else {
282 depressed = mask
283 }
284 if _, e := s.UpdateMask(depressed, 0, locked, 0, 0, tc.group); e != nil {
285 t.Fatal(e)
286 }
287 if group, e := s.Layout(); e != nil || group != tc.group {
288 t.Fatalf("group %d: %v", group, e)
289 }
290 code := WaylandKeycode(tc.evdev)
291 if sym, e := s.KeySym(code); e != nil || sym != tc.sym {
292 t.Fatalf("keysym %#x want %#x: %v", sym, tc.sym, e)
293 }
294 if v, e := s.UTF8(code); e != nil || v != tc.utf8 {
295 t.Fatalf("utf8 %q want %q: %v", v, tc.utf8, e)
296 }
297 if tc.sym == raw.XKB_KEY_dead_circumflex {
298 if v, e := s.UTF8(WaylandKeycode(18)); e != nil || v != "e" {
299 t.Fatalf("compose input e: %q %v", v, e)
300 }
301 table, e := c.NewComposeTable("en_US.UTF-8")
302 if e != nil {
303 t.Fatal(e)
304 }
305 t.Cleanup(func() { table.Close() })
306 compose, e := table.NewState()
307 if e != nil {
308 t.Fatal(e)
309 }
310 t.Cleanup(func() { compose.Close() })
311 if _, e := compose.Feed(tc.sym); e != nil {
312 t.Fatal(e)
313 }
314 if _, e := compose.Feed(raw.XKB_KEY_e); e != nil {
315 t.Fatal(e)
316 }
317 if status, e := compose.Status(); e != nil || status != raw.XKB_COMPOSE_COMPOSED {
318 t.Fatalf("compose status %d: %v", status, e)
319 }
320 if sym, e := compose.KeySym(); e != nil || sym != raw.XKB_KEY_ecircumflex {
321 t.Fatalf("compose sym %#x: %v", sym, e)
322 }
323 if v, e := compose.UTF8(); e != nil || v != "ê" {
324 t.Fatalf("compose utf8 %q: %v", v, e)
325 }
326 }
327 if _, e := exec.LookPath("xkbcli"); e == nil && tc.group == 0 && tc.modifiers != "Lock" {
328 args := []string{"how-to-type", "--layout", tc.layout}
329 if tc.sym == raw.XKB_KEY_dead_circumflex {
330 args = append(args, "--keysym")
331 }
332 out, e := exec.Command("xkbcli", append(args, tc.key)...).Output()
333 if e != nil {
334 t.Fatal(e)
335 }
336 // The CLI reports XKB keycodes, not evdev codes.
337 pattern := regexp.MustCompile(`(?m)^\s*` + fmt.Sprint(code) + `\s+\S+\s+\d+\s+.*\[.*\]`)
338 if !pattern.Match(out) {
339 t.Fatalf("xkbcli how-to-type lacks keycode %d: %s", code, out)
340 }
341 }
342 })
343 }
344}
345
346func TestCompose(t *testing.T) {
347 c := live(t)
348 table, e := c.NewComposeTable("en_US.UTF-8")
349 if e != nil {
350 t.Fatal(e)
351 }
352 s, e := table.NewState()
353 if e != nil {
354 t.Fatal(e)
355 }
356 table.Close()
357 c.Close()
358 if _, e = s.Feed(raw.XKB_KEY_dead_acute); e != nil {
359 t.Fatal(e)
360 }
361 status, _ := s.Status()
362 if status != raw.XKB_COMPOSE_COMPOSING {
363 t.Fatalf("status %d", status)
364 }
365 s.Feed(raw.XKB_KEY_e)
366 status, _ = s.Status()
367 if status != raw.XKB_COMPOSE_COMPOSED {
368 t.Fatalf("status %d", status)
369 }
370 v, e := s.UTF8()
371 if e != nil || v != "é" {
372 t.Fatalf("compose %q %v", v, e)
373 }
374 s.Reset()
375 s.Feed(raw.XKB_KEY_Multi_key)
376 s.Feed(raw.XKB_KEY_apostrophe)
377 s.Feed(raw.XKB_KEY_e)
378 v, _ = s.UTF8()
379 if v != "é" {
380 t.Fatalf("multikey %q", v)
381 }
382 s.Close()
383 s.Close()
384 table.Close()
385 if _, e = s.UTF8(); !errors.Is(e, ErrClosed) {
386 t.Fatal(e)
387 }
388}
389func TestSizing(t *testing.T) {
390 v, e := sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return 0 })
391 if e != nil || v != "" {
392 t.Fatalf("%q %v", v, e)
393 }
394 _, e = sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return -1 })
395 if e == nil {
396 t.Fatal("negative result accepted")
397 }
398 v, e = KeysymName(raw.XKB_KEY_EuroSign)
399 if e != nil || v != "EuroSign" {
400 t.Fatalf("name %q %v", v, e)
401 }
402}