fix(keymap): copy bounded FD keymaps and verify independent references
- commit
- 4e91a3635a1e53c4872b8c356e881b41d34dc717
- author
- brice · · 2026-09-28 07:08 UTC
- parent
- 9e67979
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +216 -40
M loader.go +21 -12
| @@ -14,25 +14,34 @@ var loader struct { | |||
| 14 | 14 | err error | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | + | // resolveLibrary keeps path and required symbols injectable for diagnostic tests. | |
| 18 | + | // On success it leaves the library open for the lifetime of registered functions. | |
| 19 | + | func resolveLibrary(path string, symbols []string) (map[string]uintptr, error) { | |
| 20 | + | lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL) | |
| 21 | + | if err != nil { | |
| 22 | + | return nil, fmt.Errorf("xkbcommon: load %s: %w", path, err) | |
| 23 | + | } | |
| 24 | + | addresses := make(map[string]uintptr, len(symbols)) | |
| 25 | + | for _, name := range symbols { | |
| 26 | + | address, e := purego.Dlsym(lib, name) | |
| 27 | + | if e != nil || address == 0 { | |
| 28 | + | _ = purego.Dlclose(lib) | |
| 29 | + | return nil, fmt.Errorf("xkbcommon: incompatible %s: missing %s: %v", path, name, e) | |
| 30 | + | } | |
| 31 | + | addresses[name] = address | |
| 32 | + | } | |
| 33 | + | return addresses, nil | |
| 34 | + | } | |
| 35 | + | ||
| 17 | 36 | // Available resolves the SONAME and every required symbol, reporting the first | |
| 18 | 37 | // incompatible symbol rather than failing later in a keyboard event callback. | |
| 19 | 38 | func Available() error { | |
| 20 | 39 | loader.Do(func() { | |
| 21 | - | lib, err := purego.Dlopen("libxkbcommon.so.0", purego.RTLD_NOW|purego.RTLD_LOCAL) | |
| 40 | + | addresses, err := resolveLibrary("libxkbcommon.so.0", raw.Symbols) | |
| 22 | 41 | if err != nil { | |
| 23 | - | loader.err = fmt.Errorf("xkbcommon: load libxkbcommon.so.0: %w", err) | |
| 42 | + | loader.err = err | |
| 24 | 43 | return | |
| 25 | 44 | } | |
| 26 | - | addresses := make(map[string]uintptr, len(raw.Symbols)) | |
| 27 | - | for _, name := range raw.Symbols { | |
| 28 | - | address, e := purego.Dlsym(lib, name) | |
| 29 | - | if e != nil || address == 0 { | |
| 30 | - | loader.err = fmt.Errorf("xkbcommon: incompatible libxkbcommon.so.0: missing %s: %v", name, e) | |
| 31 | - | _ = purego.Dlclose(lib) | |
| 32 | - | return | |
| 33 | - | } | |
| 34 | - | addresses[name] = address | |
| 35 | - | } | |
| 36 | 45 | raw.Register(&loader.f, addresses, purego.RegisterFunc) | |
| 37 | 46 | // Intentionally keep the library open: registered function pointers and live | |
| 38 | 47 | // native objects remain valid for the process lifetime. | |
| @@ -14,25 +14,34 @@ var loader struct { | |||||
| 14 | err error | 14 | err error | ||
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
| 17 | + | // resolveLibrary keeps path and required symbols injectable for diagnostic tests. | |||
| 18 | + | // On success it leaves the library open for the lifetime of registered functions. | |||
| 19 | + | func resolveLibrary(path string, symbols []string) (map[string]uintptr, error) { | |||
| 20 | + | lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL) | |||
| 21 | + | if err != nil { | |||
| 22 | + | return nil, fmt.Errorf("xkbcommon: load %s: %w", path, err) | |||
| 23 | + | } | |||
| 24 | + | addresses := make(map[string]uintptr, len(symbols)) | |||
| 25 | + | for _, name := range symbols { | |||
| 26 | + | address, e := purego.Dlsym(lib, name) | |||
| 27 | + | if e != nil || address == 0 { | |||
| 28 | + | _ = purego.Dlclose(lib) | |||
| 29 | + | return nil, fmt.Errorf("xkbcommon: incompatible %s: missing %s: %v", path, name, e) | |||
| 30 | + | } | |||
| 31 | + | addresses[name] = address | |||
| 32 | + | } | |||
| 33 | + | return addresses, nil | |||
| 34 | + | } | |||
| 35 | + | ||||
| 17 | // Available resolves the SONAME and every required symbol, reporting the first | 36 | // Available resolves the SONAME and every required symbol, reporting the first | ||
| 18 | // incompatible symbol rather than failing later in a keyboard event callback. | 37 | // incompatible symbol rather than failing later in a keyboard event callback. | ||
| 19 | func Available() error { | 38 | func Available() error { | ||
| 20 | loader.Do(func() { | 39 | loader.Do(func() { | ||
| 21 | - | lib, err := purego.Dlopen("libxkbcommon.so.0", purego.RTLD_NOW|purego.RTLD_LOCAL) | 40 | + | addresses, err := resolveLibrary("libxkbcommon.so.0", raw.Symbols) |
| 22 | if err != nil { | 41 | if err != nil { | ||
| 23 | - | loader.err = fmt.Errorf("xkbcommon: load libxkbcommon.so.0: %w", err) | 42 | + | loader.err = err |
| 24 | return | 43 | return | ||
| 25 | } | 44 | } | ||
| 26 | - | addresses := make(map[string]uintptr, len(raw.Symbols)) | |||
| 27 | - | for _, name := range raw.Symbols { | |||
| 28 | - | address, e := purego.Dlsym(lib, name) | |||
| 29 | - | if e != nil || address == 0 { | |||
| 30 | - | loader.err = fmt.Errorf("xkbcommon: incompatible libxkbcommon.so.0: missing %s: %v", name, e) | |||
| 31 | - | _ = purego.Dlclose(lib) | |||
| 32 | - | return | |||
| 33 | - | } | |||
| 34 | - | addresses[name] = address | |||
| 35 | - | } | |||
| 36 | raw.Register(&loader.f, addresses, purego.RegisterFunc) | 45 | raw.Register(&loader.f, addresses, purego.RegisterFunc) | ||
| 37 | // Intentionally keep the library open: registered function pointers and live | 46 | // Intentionally keep the library open: registered function pointers and live | ||
| 38 | // native objects remain valid for the process lifetime. | 47 | // native objects remain valid for the process lifetime. | ||
M xkbcommon.go +23 -15
| @@ -129,31 +129,39 @@ func (c *Context) NewKeymapRules(rules, model, layout, variant, options string) | |||
| 129 | 129 | }) | |
| 130 | 130 | } | |
| 131 | 131 | ||
| 132 | - | // NewKeymapFD maps exactly size bytes read-only and privately, then compiles the | |
| 133 | - | // Wayland xkb_v1 format. The caller retains ownership of fd, including on error; | |
| 134 | - | // this function never closes it. The mapping is removed before return. size must | |
| 135 | - | // include the terminating NUL required by the Wayland protocol. | |
| 132 | + | // NewKeymapFD copies exactly size bytes from offset zero of a Wayland xkb_v1 | |
| 133 | + | // keymap FD (at most 16 MiB). A trailing Wayland NUL, if present, is excluded | |
| 134 | + | // from the compiled text. The caller retains ownership of fd, even on error. | |
| 136 | 135 | func (c *Context) NewKeymapFD(fd int, size int) (*Keymap, error) { | |
| 137 | - | if fd < 0 || size < 1 || size > 64<<20 { | |
| 138 | - | return nil, errors.New("xkbcommon: invalid keymap FD or size (limit 64 MiB)") | |
| 136 | + | if fd < 0 || size < 1 || size > 16<<20 { | |
| 137 | + | return nil, errors.New("xkbcommon: invalid keymap FD or size (limit 16 MiB)") | |
| 139 | 138 | } | |
| 140 | 139 | var stat syscall.Stat_t | |
| 141 | 140 | if e := syscall.Fstat(fd, &stat); e != nil { | |
| 142 | 141 | return nil, fmt.Errorf("xkbcommon: stat keymap FD: %w", e) | |
| 143 | 142 | } | |
| 144 | - | if stat.Mode&syscall.S_IFMT != syscall.S_IFREG || stat.Size < int64(size) { | |
| 145 | - | return nil, errors.New("xkbcommon: keymap FD is not a regular file of the supplied size") | |
| 143 | + | if stat.Mode&syscall.S_IFMT != syscall.S_IFREG { | |
| 144 | + | return nil, errors.New("xkbcommon: keymap FD is not a regular file") | |
| 146 | 145 | } | |
| 147 | - | b, e := syscall.Mmap(fd, 0, size, syscall.PROT_READ, syscall.MAP_PRIVATE) | |
| 148 | - | if e != nil { | |
| 149 | - | return nil, fmt.Errorf("xkbcommon: mmap keymap: %w", e) | |
| 146 | + | b := make([]byte, size) | |
| 147 | + | for offset := 0; offset < size; { | |
| 148 | + | n, e := syscall.Pread(fd, b[offset:], int64(offset)) | |
| 149 | + | offset += n | |
| 150 | + | if e == syscall.EINTR { | |
| 151 | + | continue | |
| 152 | + | } | |
| 153 | + | if e != nil { | |
| 154 | + | return nil, fmt.Errorf("xkbcommon: read keymap FD: %w", e) | |
| 155 | + | } | |
| 156 | + | if n == 0 { | |
| 157 | + | return nil, fmt.Errorf("xkbcommon: read keymap FD: %w", errors.New("short read")) | |
| 158 | + | } | |
| 150 | 159 | } | |
| 151 | - | defer syscall.Munmap(b) | |
| 152 | - | if b[len(b)-1] != 0 { | |
| 153 | - | return nil, errors.New("xkbcommon: Wayland keymap not NUL terminated") | |
| 160 | + | if b[len(b)-1] == 0 { | |
| 161 | + | b = b[:len(b)-1] | |
| 154 | 162 | } | |
| 155 | 163 | return newKeymap(c, func(h uintptr) uintptr { | |
| 156 | - | v := loader.f.Xkb_keymap_new_from_buffer(h, ptr(b), uintptr(size-1), raw.XKB_KEYMAP_FORMAT_TEXT_V1, 0) | |
| 164 | + | v := loader.f.Xkb_keymap_new_from_buffer(h, ptr(b), uintptr(len(b)), raw.XKB_KEYMAP_FORMAT_TEXT_V1, 0) | |
| 157 | 165 | runtime.KeepAlive(b) | |
| 158 | 166 | return v | |
| 159 | 167 | }) | |
| @@ -129,31 +129,39 @@ func (c *Context) NewKeymapRules(rules, model, layout, variant, options string) | |||||
| 129 | }) | 129 | }) | ||
| 130 | } | 130 | } | ||
| 131 | 131 | ||||
| 132 | - | // NewKeymapFD maps exactly size bytes read-only and privately, then compiles the | 132 | + | // NewKeymapFD copies exactly size bytes from offset zero of a Wayland xkb_v1 |
| 133 | - | // Wayland xkb_v1 format. The caller retains ownership of fd, including on error; | 133 | + | // keymap FD (at most 16 MiB). A trailing Wayland NUL, if present, is excluded |
| 134 | - | // this function never closes it. The mapping is removed before return. size must | 134 | + | // from the compiled text. The caller retains ownership of fd, even on error. |
| 135 | - | // include the terminating NUL required by the Wayland protocol. | |||
| 136 | func (c *Context) NewKeymapFD(fd int, size int) (*Keymap, error) { | 135 | func (c *Context) NewKeymapFD(fd int, size int) (*Keymap, error) { | ||
| 137 | - | if fd < 0 || size < 1 || size > 64<<20 { | 136 | + | if fd < 0 || size < 1 || size > 16<<20 { |
| 138 | - | return nil, errors.New("xkbcommon: invalid keymap FD or size (limit 64 MiB)") | 137 | + | return nil, errors.New("xkbcommon: invalid keymap FD or size (limit 16 MiB)") |
| 139 | } | 138 | } | ||
| 140 | var stat syscall.Stat_t | 139 | var stat syscall.Stat_t | ||
| 141 | if e := syscall.Fstat(fd, &stat); e != nil { | 140 | if e := syscall.Fstat(fd, &stat); e != nil { | ||
| 142 | return nil, fmt.Errorf("xkbcommon: stat keymap FD: %w", e) | 141 | return nil, fmt.Errorf("xkbcommon: stat keymap FD: %w", e) | ||
| 143 | } | 142 | } | ||
| 144 | - | if stat.Mode&syscall.S_IFMT != syscall.S_IFREG || stat.Size < int64(size) { | 143 | + | if stat.Mode&syscall.S_IFMT != syscall.S_IFREG { |
| 145 | - | return nil, errors.New("xkbcommon: keymap FD is not a regular file of the supplied size") | 144 | + | return nil, errors.New("xkbcommon: keymap FD is not a regular file") |
| 146 | } | 145 | } | ||
| 147 | - | b, e := syscall.Mmap(fd, 0, size, syscall.PROT_READ, syscall.MAP_PRIVATE) | 146 | + | b := make([]byte, size) |
| 148 | - | if e != nil { | 147 | + | for offset := 0; offset < size; { |
| 149 | - | return nil, fmt.Errorf("xkbcommon: mmap keymap: %w", e) | 148 | + | n, e := syscall.Pread(fd, b[offset:], int64(offset)) |
| 149 | + | offset += n | |||
| 150 | + | if e == syscall.EINTR { | |||
| 151 | + | continue | |||
| 152 | + | } | |||
| 153 | + | if e != nil { | |||
| 154 | + | return nil, fmt.Errorf("xkbcommon: read keymap FD: %w", e) | |||
| 155 | + | } | |||
| 156 | + | if n == 0 { | |||
| 157 | + | return nil, fmt.Errorf("xkbcommon: read keymap FD: %w", errors.New("short read")) | |||
| 158 | + | } | |||
| 150 | } | 159 | } | ||
| 151 | - | defer syscall.Munmap(b) | 160 | + | if b[len(b)-1] == 0 { |
| 152 | - | if b[len(b)-1] != 0 { | 161 | + | b = b[:len(b)-1] |
| 153 | - | return nil, errors.New("xkbcommon: Wayland keymap not NUL terminated") | |||
| 154 | } | 162 | } | ||
| 155 | return newKeymap(c, func(h uintptr) uintptr { | 163 | return newKeymap(c, func(h uintptr) uintptr { | ||
| 156 | - | v := loader.f.Xkb_keymap_new_from_buffer(h, ptr(b), uintptr(size-1), raw.XKB_KEYMAP_FORMAT_TEXT_V1, 0) | 164 | + | v := loader.f.Xkb_keymap_new_from_buffer(h, ptr(b), uintptr(len(b)), raw.XKB_KEYMAP_FORMAT_TEXT_V1, 0) |
| 157 | runtime.KeepAlive(b) | 165 | runtime.KeepAlive(b) | ||
| 158 | return v | 166 | return v | ||
| 159 | }) | 167 | }) | ||
M xkbcommon_test.go +172 -13
| @@ -2,8 +2,10 @@ package xkbcommon | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "fmt" | |
| 5 | 6 | "os" | |
| 6 | 7 | "os/exec" | |
| 8 | + | "regexp" | |
| 7 | 9 | "strings" | |
| 8 | 10 | "testing" | |
| 9 | 11 | "unsafe" | |
| @@ -89,32 +91,72 @@ func TestMalformedAndLifetime(t *testing.T) { | |||
| 89 | 91 | } | |
| 90 | 92 | func TestFD(t *testing.T) { | |
| 91 | 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 | + | } | |
| 92 | 112 | out, e := exec.Command("xkbcli", "compile-keymap", "--rules", "evdev", "--model", "pc105", "--layout", "fr", "--output-format", "1").Output() | |
| 93 | 113 | if e != nil { | |
| 94 | 114 | t.Fatalf("reference xkbcli: %v", e) | |
| 95 | 115 | } | |
| 96 | - | f, e := os.CreateTemp(t.TempDir(), "keymap") | |
| 116 | + | f := keymapFile(t, append(out, 0)) | |
| 117 | + | other, e := c.NewKeymapFD(int(f.Fd()), len(out)+1) | |
| 97 | 118 | if e != nil { | |
| 98 | 119 | t.Fatal(e) | |
| 99 | 120 | } | |
| 100 | - | defer f.Close() | |
| 101 | - | data := append(out, 0) | |
| 102 | - | if _, e = f.Write(data); e != nil { | |
| 103 | - | t.Fatal(e) | |
| 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) | |
| 104 | 127 | } | |
| 105 | - | other, e := c.NewKeymapFD(int(f.Fd()), len(data)) | |
| 128 | + | if _, e = c.NewKeymapFD(int(f.Fd()), len(out)); e != nil { | |
| 129 | + | t.Fatalf("non-NUL text: %v", e) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | func keymapFile(t *testing.T, data []byte) *os.File { | |
| 134 | + | t.Helper() | |
| 135 | + | f, e := os.CreateTemp(t.TempDir(), "keymap") | |
| 106 | 136 | if e != nil { | |
| 107 | 137 | t.Fatal(e) | |
| 108 | 138 | } | |
| 109 | - | other.Close() | |
| 110 | - | if _, e = f.Stat(); e != nil { | |
| 111 | - | t.Fatalf("FD was closed: %v", e) | |
| 139 | + | t.Cleanup(func() { f.Close() }) | |
| 140 | + | if _, e = f.Write(data); e != nil { | |
| 141 | + | t.Fatal(e) | |
| 112 | 142 | } | |
| 113 | - | if _, e = c.NewKeymapFD(int(f.Fd()), len(data)-1); e == nil { | |
| 114 | - | t.Fatal("missing NUL accepted") | |
| 143 | + | return f | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | func 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) | |
| 115 | 156 | } | |
| 116 | - | if _, e = c.NewKeymapFD(-1, len(data)); e == nil { | |
| 117 | - | t.Fatal("negative FD") | |
| 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) | |
| 118 | 160 | } | |
| 119 | 161 | } | |
| 120 | 162 | func TestLayoutsAndDifferential(t *testing.T) { | |
| @@ -191,6 +233,123 @@ func TestLayoutsAndDifferential(t *testing.T) { | |||
| 191 | 233 | t.Fatalf("mods %#x", mods) | |
| 192 | 234 | } | |
| 193 | 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. | |
| 239 | + | func TestFDReferenceKeyEvents(t *testing.T) { | |
| 240 | + | c := live(t) | |
| 241 | + | if _, e := exec.LookPath("xkbcli"); e != nil { | |
| 242 | + | t.Logf("xkbcli compile-keymap/how-to-type comparison skipped: %v", e) | |
| 243 | + | } | |
| 244 | + | for _, tc := range []struct { | |
| 245 | + | layout, key, utf8, modifiers string | |
| 246 | + | evdev, group, sym uint32 | |
| 247 | + | }{ | |
| 248 | + | {"us", "q", "q", "", 16, 0, raw.XKB_KEY_q}, | |
| 249 | + | {"us", "Q", "Q", "Shift", 16, 0, raw.XKB_KEY_Q}, | |
| 250 | + | {"us", "Q", "Q", "Lock", 16, 0, raw.XKB_KEY_Q}, | |
| 251 | + | {"fr", "a", "a", "", 16, 0, raw.XKB_KEY_a}, | |
| 252 | + | {"fr", "€", "€", "Mod5", 18, 0, raw.XKB_KEY_EuroSign}, | |
| 253 | + | {"de", "z", "z", "", 21, 0, raw.XKB_KEY_z}, | |
| 254 | + | {"de", "@", "@", "Mod5", 16, 0, raw.XKB_KEY_at}, | |
| 255 | + | {"de", "dead_circumflex", "", "", 41, 0, raw.XKB_KEY_dead_circumflex}, | |
| 256 | + | {"us,fr", "a", "a", "", 16, 1, raw.XKB_KEY_a}, | |
| 257 | + | } { | |
| 258 | + | t.Run(tc.layout+"/"+tc.key+"/"+tc.modifiers, func(t *testing.T) { | |
| 259 | + | // xkbcli independently compiles the text used by the Wayland FD path. | |
| 260 | + | // Without it, still check the fixed reference table against rules. | |
| 261 | + | var k *Keymap | |
| 262 | + | if _, e := exec.LookPath("xkbcli"); e == nil { | |
| 263 | + | text, e := exec.Command("xkbcli", "compile-keymap", "--layout", tc.layout, "--output-format", "1").Output() | |
| 264 | + | if e != nil { | |
| 265 | + | t.Fatal(e) | |
| 266 | + | } | |
| 267 | + | f := keymapFile(t, append(text, 0)) | |
| 268 | + | k, e = c.NewKeymapFD(int(f.Fd()), len(text)+1) | |
| 269 | + | if e != nil { | |
| 270 | + | t.Fatal(e) | |
| 271 | + | } | |
| 272 | + | t.Cleanup(func() { k.Close() }) | |
| 273 | + | } else { | |
| 274 | + | k = rules(t, c, tc.layout) | |
| 275 | + | } | |
| 276 | + | s := state(t, k) | |
| 277 | + | var mask uint32 | |
| 278 | + | if tc.modifiers != "" { | |
| 279 | + | idx, e := k.ModIndex(tc.modifiers) | |
| 280 | + | if e != nil || idx >= 32 { | |
| 281 | + | t.Fatalf("modifier %s: %d %v", tc.modifiers, idx, e) | |
| 282 | + | } | |
| 283 | + | mask = 1 << idx | |
| 284 | + | } | |
| 285 | + | var depressed, locked uint32 | |
| 286 | + | if tc.modifiers == "Lock" { | |
| 287 | + | locked = mask | |
| 288 | + | } else { | |
| 289 | + | depressed = mask | |
| 290 | + | } | |
| 291 | + | if _, e := s.UpdateMask(depressed, 0, locked, 0, 0, tc.group); e != nil { | |
| 292 | + | t.Fatal(e) | |
| 293 | + | } | |
| 294 | + | if group, e := s.Layout(); e != nil || group != tc.group { | |
| 295 | + | t.Fatalf("group %d: %v", group, e) | |
| 296 | + | } | |
| 297 | + | code := WaylandKeycode(tc.evdev) | |
| 298 | + | if sym, e := s.KeySym(code); e != nil || sym != tc.sym { | |
| 299 | + | t.Fatalf("keysym %#x want %#x: %v", sym, tc.sym, e) | |
| 300 | + | } | |
| 301 | + | if v, e := s.UTF8(code); e != nil || v != tc.utf8 { | |
| 302 | + | t.Fatalf("utf8 %q want %q: %v", v, tc.utf8, e) | |
| 303 | + | } | |
| 304 | + | if tc.sym == raw.XKB_KEY_dead_circumflex { | |
| 305 | + | if v, e := s.UTF8(WaylandKeycode(18)); e != nil || v != "e" { | |
| 306 | + | t.Fatalf("compose input e: %q %v", v, e) | |
| 307 | + | } | |
| 308 | + | table, e := c.NewComposeTable("en_US.UTF-8") | |
| 309 | + | if e != nil { | |
| 310 | + | t.Fatal(e) | |
| 311 | + | } | |
| 312 | + | t.Cleanup(func() { table.Close() }) | |
| 313 | + | compose, e := table.NewState() | |
| 314 | + | if e != nil { | |
| 315 | + | t.Fatal(e) | |
| 316 | + | } | |
| 317 | + | t.Cleanup(func() { compose.Close() }) | |
| 318 | + | if _, e := compose.Feed(tc.sym); e != nil { | |
| 319 | + | t.Fatal(e) | |
| 320 | + | } | |
| 321 | + | if _, e := compose.Feed(raw.XKB_KEY_e); e != nil { | |
| 322 | + | t.Fatal(e) | |
| 323 | + | } | |
| 324 | + | if status, e := compose.Status(); e != nil || status != raw.XKB_COMPOSE_COMPOSED { | |
| 325 | + | t.Fatalf("compose status %d: %v", status, e) | |
| 326 | + | } | |
| 327 | + | if sym, e := compose.KeySym(); e != nil || sym != raw.XKB_KEY_ecircumflex { | |
| 328 | + | t.Fatalf("compose sym %#x: %v", sym, e) | |
| 329 | + | } | |
| 330 | + | if v, e := compose.UTF8(); e != nil || v != "ê" { | |
| 331 | + | t.Fatalf("compose utf8 %q: %v", v, e) | |
| 332 | + | } | |
| 333 | + | } | |
| 334 | + | if _, e := exec.LookPath("xkbcli"); e == nil && tc.group == 0 && tc.modifiers != "Lock" { | |
| 335 | + | args := []string{"how-to-type", "--layout", tc.layout} | |
| 336 | + | if tc.sym == raw.XKB_KEY_dead_circumflex { | |
| 337 | + | args = append(args, "--keysym") | |
| 338 | + | } | |
| 339 | + | out, e := exec.Command("xkbcli", append(args, tc.key)...).Output() | |
| 340 | + | if e != nil { | |
| 341 | + | t.Fatal(e) | |
| 342 | + | } | |
| 343 | + | // The CLI reports XKB keycodes, not evdev codes. | |
| 344 | + | pattern := regexp.MustCompile(`(?m)^\s*` + fmt.Sprint(code) + `\s+\S+\s+\d+\s+.*\[.*\]`) | |
| 345 | + | if !pattern.Match(out) { | |
| 346 | + | t.Fatalf("xkbcli how-to-type lacks keycode %d: %s", code, out) | |
| 347 | + | } | |
| 348 | + | } | |
| 349 | + | }) | |
| 350 | + | } | |
| 351 | + | } | |
| 352 | + | ||
| 194 | 353 | func TestCompose(t *testing.T) { | |
| 195 | 354 | c := live(t) | |
| 196 | 355 | table, e := c.NewComposeTable("en_US.UTF-8") | |
| @@ -2,8 +2,10 @@ package xkbcommon | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "fmt" | |||
| 5 | "os" | 6 | "os" | ||
| 6 | "os/exec" | 7 | "os/exec" | ||
| 8 | + | "regexp" | |||
| 7 | "strings" | 9 | "strings" | ||
| 8 | "testing" | 10 | "testing" | ||
| 9 | "unsafe" | 11 | "unsafe" | ||
| @@ -89,32 +91,72 @@ func TestMalformedAndLifetime(t *testing.T) { | |||||
| 89 | } | 91 | } | ||
| 90 | func TestFD(t *testing.T) { | 92 | func TestFD(t *testing.T) { | ||
| 91 | c := live(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 | + | } | |||
| 92 | out, e := exec.Command("xkbcli", "compile-keymap", "--rules", "evdev", "--model", "pc105", "--layout", "fr", "--output-format", "1").Output() | 112 | out, e := exec.Command("xkbcli", "compile-keymap", "--rules", "evdev", "--model", "pc105", "--layout", "fr", "--output-format", "1").Output() | ||
| 93 | if e != nil { | 113 | if e != nil { | ||
| 94 | t.Fatalf("reference xkbcli: %v", e) | 114 | t.Fatalf("reference xkbcli: %v", e) | ||
| 95 | } | 115 | } | ||
| 96 | - | f, e := os.CreateTemp(t.TempDir(), "keymap") | 116 | + | f := keymapFile(t, append(out, 0)) |
| 117 | + | other, e := c.NewKeymapFD(int(f.Fd()), len(out)+1) | |||
| 97 | if e != nil { | 118 | if e != nil { | ||
| 98 | t.Fatal(e) | 119 | t.Fatal(e) | ||
| 99 | } | 120 | } | ||
| 100 | - | defer f.Close() | 121 | + | other.Close() |
| 101 | - | data := append(out, 0) | 122 | + | if _, e = f.Stat(); e != nil { |
| 102 | - | if _, e = f.Write(data); e != nil { | 123 | + | t.Fatalf("FD was closed: %v", e) |
| 103 | - | t.Fatal(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) | |||
| 104 | } | 127 | } | ||
| 105 | - | other, e := c.NewKeymapFD(int(f.Fd()), len(data)) | 128 | + | if _, e = c.NewKeymapFD(int(f.Fd()), len(out)); e != nil { |
| 129 | + | t.Fatalf("non-NUL text: %v", e) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | func keymapFile(t *testing.T, data []byte) *os.File { | |||
| 134 | + | t.Helper() | |||
| 135 | + | f, e := os.CreateTemp(t.TempDir(), "keymap") | |||
| 106 | if e != nil { | 136 | if e != nil { | ||
| 107 | t.Fatal(e) | 137 | t.Fatal(e) | ||
| 108 | } | 138 | } | ||
| 109 | - | other.Close() | 139 | + | t.Cleanup(func() { f.Close() }) |
| 110 | - | if _, e = f.Stat(); e != nil { | 140 | + | if _, e = f.Write(data); e != nil { |
| 111 | - | t.Fatalf("FD was closed: %v", e) | 141 | + | t.Fatal(e) |
| 112 | } | 142 | } | ||
| 113 | - | if _, e = c.NewKeymapFD(int(f.Fd()), len(data)-1); e == nil { | 143 | + | return f |
| 114 | - | t.Fatal("missing NUL accepted") | 144 | + | } |
| 145 | + | ||||
| 146 | + | func 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) | |||
| 115 | } | 156 | } | ||
| 116 | - | if _, e = c.NewKeymapFD(-1, len(data)); e == nil { | 157 | + | _, e = resolveLibrary("libxkbcommon.so.0", []string{"xkb_nonexistent_test_symbol"}) |
| 117 | - | t.Fatal("negative FD") | 158 | + | if e == nil || !strings.Contains(e.Error(), "missing xkb_nonexistent_test_symbol") { |
| 159 | + | t.Fatalf("missing symbol: %v", e) | |||
| 118 | } | 160 | } | ||
| 119 | } | 161 | } | ||
| 120 | func TestLayoutsAndDifferential(t *testing.T) { | 162 | func TestLayoutsAndDifferential(t *testing.T) { | ||
| @@ -191,6 +233,123 @@ func TestLayoutsAndDifferential(t *testing.T) { | |||||
| 191 | t.Fatalf("mods %#x", mods) | 233 | t.Fatalf("mods %#x", mods) | ||
| 192 | } | 234 | } | ||
| 193 | } | 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. | |||
| 239 | + | func TestFDReferenceKeyEvents(t *testing.T) { | |||
| 240 | + | c := live(t) | |||
| 241 | + | if _, e := exec.LookPath("xkbcli"); e != nil { | |||
| 242 | + | t.Logf("xkbcli compile-keymap/how-to-type comparison skipped: %v", e) | |||
| 243 | + | } | |||
| 244 | + | for _, tc := range []struct { | |||
| 245 | + | layout, key, utf8, modifiers string | |||
| 246 | + | evdev, group, sym uint32 | |||
| 247 | + | }{ | |||
| 248 | + | {"us", "q", "q", "", 16, 0, raw.XKB_KEY_q}, | |||
| 249 | + | {"us", "Q", "Q", "Shift", 16, 0, raw.XKB_KEY_Q}, | |||
| 250 | + | {"us", "Q", "Q", "Lock", 16, 0, raw.XKB_KEY_Q}, | |||
| 251 | + | {"fr", "a", "a", "", 16, 0, raw.XKB_KEY_a}, | |||
| 252 | + | {"fr", "€", "€", "Mod5", 18, 0, raw.XKB_KEY_EuroSign}, | |||
| 253 | + | {"de", "z", "z", "", 21, 0, raw.XKB_KEY_z}, | |||
| 254 | + | {"de", "@", "@", "Mod5", 16, 0, raw.XKB_KEY_at}, | |||
| 255 | + | {"de", "dead_circumflex", "", "", 41, 0, raw.XKB_KEY_dead_circumflex}, | |||
| 256 | + | {"us,fr", "a", "a", "", 16, 1, raw.XKB_KEY_a}, | |||
| 257 | + | } { | |||
| 258 | + | t.Run(tc.layout+"/"+tc.key+"/"+tc.modifiers, func(t *testing.T) { | |||
| 259 | + | // xkbcli independently compiles the text used by the Wayland FD path. | |||
| 260 | + | // Without it, still check the fixed reference table against rules. | |||
| 261 | + | var k *Keymap | |||
| 262 | + | if _, e := exec.LookPath("xkbcli"); e == nil { | |||
| 263 | + | text, e := exec.Command("xkbcli", "compile-keymap", "--layout", tc.layout, "--output-format", "1").Output() | |||
| 264 | + | if e != nil { | |||
| 265 | + | t.Fatal(e) | |||
| 266 | + | } | |||
| 267 | + | f := keymapFile(t, append(text, 0)) | |||
| 268 | + | k, e = c.NewKeymapFD(int(f.Fd()), len(text)+1) | |||
| 269 | + | if e != nil { | |||
| 270 | + | t.Fatal(e) | |||
| 271 | + | } | |||
| 272 | + | t.Cleanup(func() { k.Close() }) | |||
| 273 | + | } else { | |||
| 274 | + | k = rules(t, c, tc.layout) | |||
| 275 | + | } | |||
| 276 | + | s := state(t, k) | |||
| 277 | + | var mask uint32 | |||
| 278 | + | if tc.modifiers != "" { | |||
| 279 | + | idx, e := k.ModIndex(tc.modifiers) | |||
| 280 | + | if e != nil || idx >= 32 { | |||
| 281 | + | t.Fatalf("modifier %s: %d %v", tc.modifiers, idx, e) | |||
| 282 | + | } | |||
| 283 | + | mask = 1 << idx | |||
| 284 | + | } | |||
| 285 | + | var depressed, locked uint32 | |||
| 286 | + | if tc.modifiers == "Lock" { | |||
| 287 | + | locked = mask | |||
| 288 | + | } else { | |||
| 289 | + | depressed = mask | |||
| 290 | + | } | |||
| 291 | + | if _, e := s.UpdateMask(depressed, 0, locked, 0, 0, tc.group); e != nil { | |||
| 292 | + | t.Fatal(e) | |||
| 293 | + | } | |||
| 294 | + | if group, e := s.Layout(); e != nil || group != tc.group { | |||
| 295 | + | t.Fatalf("group %d: %v", group, e) | |||
| 296 | + | } | |||
| 297 | + | code := WaylandKeycode(tc.evdev) | |||
| 298 | + | if sym, e := s.KeySym(code); e != nil || sym != tc.sym { | |||
| 299 | + | t.Fatalf("keysym %#x want %#x: %v", sym, tc.sym, e) | |||
| 300 | + | } | |||
| 301 | + | if v, e := s.UTF8(code); e != nil || v != tc.utf8 { | |||
| 302 | + | t.Fatalf("utf8 %q want %q: %v", v, tc.utf8, e) | |||
| 303 | + | } | |||
| 304 | + | if tc.sym == raw.XKB_KEY_dead_circumflex { | |||
| 305 | + | if v, e := s.UTF8(WaylandKeycode(18)); e != nil || v != "e" { | |||
| 306 | + | t.Fatalf("compose input e: %q %v", v, e) | |||
| 307 | + | } | |||
| 308 | + | table, e := c.NewComposeTable("en_US.UTF-8") | |||
| 309 | + | if e != nil { | |||
| 310 | + | t.Fatal(e) | |||
| 311 | + | } | |||
| 312 | + | t.Cleanup(func() { table.Close() }) | |||
| 313 | + | compose, e := table.NewState() | |||
| 314 | + | if e != nil { | |||
| 315 | + | t.Fatal(e) | |||
| 316 | + | } | |||
| 317 | + | t.Cleanup(func() { compose.Close() }) | |||
| 318 | + | if _, e := compose.Feed(tc.sym); e != nil { | |||
| 319 | + | t.Fatal(e) | |||
| 320 | + | } | |||
| 321 | + | if _, e := compose.Feed(raw.XKB_KEY_e); e != nil { | |||
| 322 | + | t.Fatal(e) | |||
| 323 | + | } | |||
| 324 | + | if status, e := compose.Status(); e != nil || status != raw.XKB_COMPOSE_COMPOSED { | |||
| 325 | + | t.Fatalf("compose status %d: %v", status, e) | |||
| 326 | + | } | |||
| 327 | + | if sym, e := compose.KeySym(); e != nil || sym != raw.XKB_KEY_ecircumflex { | |||
| 328 | + | t.Fatalf("compose sym %#x: %v", sym, e) | |||
| 329 | + | } | |||
| 330 | + | if v, e := compose.UTF8(); e != nil || v != "ê" { | |||
| 331 | + | t.Fatalf("compose utf8 %q: %v", v, e) | |||
| 332 | + | } | |||
| 333 | + | } | |||
| 334 | + | if _, e := exec.LookPath("xkbcli"); e == nil && tc.group == 0 && tc.modifiers != "Lock" { | |||
| 335 | + | args := []string{"how-to-type", "--layout", tc.layout} | |||
| 336 | + | if tc.sym == raw.XKB_KEY_dead_circumflex { | |||
| 337 | + | args = append(args, "--keysym") | |||
| 338 | + | } | |||
| 339 | + | out, e := exec.Command("xkbcli", append(args, tc.key)...).Output() | |||
| 340 | + | if e != nil { | |||
| 341 | + | t.Fatal(e) | |||
| 342 | + | } | |||
| 343 | + | // The CLI reports XKB keycodes, not evdev codes. | |||
| 344 | + | pattern := regexp.MustCompile(`(?m)^\s*` + fmt.Sprint(code) + `\s+\S+\s+\d+\s+.*\[.*\]`) | |||
| 345 | + | if !pattern.Match(out) { | |||
| 346 | + | t.Fatalf("xkbcli how-to-type lacks keycode %d: %s", code, out) | |||
| 347 | + | } | |||
| 348 | + | } | |||
| 349 | + | }) | |||
| 350 | + | } | |||
| 351 | + | } | |||
| 352 | + | ||||
| 194 | func TestCompose(t *testing.T) { | 353 | func TestCompose(t *testing.T) { | ||
| 195 | c := live(t) | 354 | c := live(t) | ||
| 196 | table, e := c.NewComposeTable("en_US.UTF-8") | 355 | table, e := c.NewComposeTable("en_US.UTF-8") | ||