fix(wayland): widen virtual keymap keycode range to avoid an Xwayland heap overflow
- commit
- cc0df8fc664565f3d90330ec048395ceb6192b64
- author
- brice · · 2026-09-26 14:52 UTC
- parent
- 3338d18
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +290 -0
A internal/adapters/wayland/keycodes.go +212 -0
| @@ -0,0 +1,212 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strconv" | |
| 5 | + | "strings" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // minKeycodeMax is the smallest keycode maximum sent to clients. | |
| 9 | + | // Xwayland (xkb/xkbUtils.c _XkbCopyClientMap, still in 24.1) clears | |
| 10 | + | // MAP_LENGTH-(max+1) entries starting at that same index of its 256-entry | |
| 11 | + | // key maps: with a maximum below 127 it writes past the end of the heap | |
| 12 | + | // block and aborts on the next realloc. Dictation tools (wtype-like) | |
| 13 | + | // send keymaps holding only the keys they type (e.g. 8..26). Keycodes up | |
| 14 | + | // to 255 are always valid, so the range is widened without changing any | |
| 15 | + | // key. | |
| 16 | + | const minKeycodeMax = 255 | |
| 17 | + | ||
| 18 | + | // widenKeycodes raises the keycode maximum of a keymap's xkb_keycodes | |
| 19 | + | // section to minKeycodeMax. An explicit maximum below it is rewritten; a | |
| 20 | + | // section without one (its highest keycode is the maximum) gets one, | |
| 21 | + | // unless it includes other files or already has a keycode above it. It | |
| 22 | + | // returns the keymap and the former maximum (the highest keycode when | |
| 23 | + | // none was set), 0 when unchanged. Keywords are case-insensitive; | |
| 24 | + | // comments, strings and key names are skipped. | |
| 25 | + | func widenKeycodes(text string) (string, int) { | |
| 26 | + | open, end := keycodesSection(text) | |
| 27 | + | if open < 0 { | |
| 28 | + | return text, 0 | |
| 29 | + | } | |
| 30 | + | type span struct{ from, to int } | |
| 31 | + | var low []span | |
| 32 | + | maxSet, include := false, false | |
| 33 | + | from, highest := 0, 0 | |
| 34 | + | lastName := false // the previous token was a <name>: "= N" is a keycode | |
| 35 | + | for i := open + 1; i < end; { | |
| 36 | + | if j := skipNoise(text, i); j != i { | |
| 37 | + | lastName = text[i] == '<' | |
| 38 | + | i = j | |
| 39 | + | continue | |
| 40 | + | } | |
| 41 | + | c := text[i] | |
| 42 | + | switch { | |
| 43 | + | case isIdentStart(c): | |
| 44 | + | j := i | |
| 45 | + | for j < end && isIdent(text[j]) { | |
| 46 | + | j++ | |
| 47 | + | } | |
| 48 | + | word := text[i:j] | |
| 49 | + | lastName = false | |
| 50 | + | i = j | |
| 51 | + | switch { | |
| 52 | + | case strings.EqualFold(word, "include"), strings.EqualFold(word, "augment"), strings.EqualFold(word, "override"), strings.EqualFold(word, "replace"): | |
| 53 | + | include = true | |
| 54 | + | case strings.EqualFold(word, "maximum"): | |
| 55 | + | k := skipBlank(text, j, end) | |
| 56 | + | if k >= end || text[k] != '=' { | |
| 57 | + | continue | |
| 58 | + | } | |
| 59 | + | k = skipBlank(text, k+1, end) | |
| 60 | + | d := k | |
| 61 | + | for d < end && text[d] >= '0' && text[d] <= '9' { | |
| 62 | + | d++ | |
| 63 | + | } | |
| 64 | + | n, err := strconv.Atoi(text[k:d]) | |
| 65 | + | if err != nil { | |
| 66 | + | continue | |
| 67 | + | } | |
| 68 | + | maxSet = true | |
| 69 | + | if n < minKeycodeMax { | |
| 70 | + | low = append(low, span{k, d}) | |
| 71 | + | from = n | |
| 72 | + | } | |
| 73 | + | i = d | |
| 74 | + | } | |
| 75 | + | case c == '=' && lastName: | |
| 76 | + | k := skipBlank(text, i+1, end) | |
| 77 | + | d := k | |
| 78 | + | for d < end && text[d] >= '0' && text[d] <= '9' { | |
| 79 | + | d++ | |
| 80 | + | } | |
| 81 | + | if n, err := strconv.Atoi(text[k:d]); err == nil { | |
| 82 | + | highest = max(highest, n) | |
| 83 | + | } | |
| 84 | + | lastName = false | |
| 85 | + | i = max(d, i+1) | |
| 86 | + | default: | |
| 87 | + | if c != ' ' && c != '\t' && c != '\n' && c != '\r' { | |
| 88 | + | lastName = false | |
| 89 | + | } | |
| 90 | + | i++ | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | if !maxSet { | |
| 94 | + | if include || highest >= minKeycodeMax || highest == 0 { | |
| 95 | + | return text, 0 | |
| 96 | + | } | |
| 97 | + | return text[:open+1] + " maximum = " + strconv.Itoa(minKeycodeMax) + ";" + text[open+1:], highest | |
| 98 | + | } | |
| 99 | + | if len(low) == 0 { | |
| 100 | + | return text, 0 | |
| 101 | + | } | |
| 102 | + | var b strings.Builder | |
| 103 | + | last := 0 | |
| 104 | + | for _, sp := range low { | |
| 105 | + | b.WriteString(text[last:sp.from]) | |
| 106 | + | b.WriteString(strconv.Itoa(minKeycodeMax)) | |
| 107 | + | last = sp.to | |
| 108 | + | } | |
| 109 | + | b.WriteString(text[last:]) | |
| 110 | + | return b.String(), from | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // keycodesSection returns the indexes of the braces of the first | |
| 114 | + | // xkb_keycodes section, -1 without one. | |
| 115 | + | func keycodesSection(text string) (open, end int) { | |
| 116 | + | for i := 0; i < len(text); { | |
| 117 | + | if j := skipNoise(text, i); j != i { | |
| 118 | + | i = j | |
| 119 | + | continue | |
| 120 | + | } | |
| 121 | + | if !isIdentStart(text[i]) { | |
| 122 | + | i++ | |
| 123 | + | continue | |
| 124 | + | } | |
| 125 | + | j := i | |
| 126 | + | for j < len(text) && isIdent(text[j]) { | |
| 127 | + | j++ | |
| 128 | + | } | |
| 129 | + | if !strings.EqualFold(text[i:j], "xkb_keycodes") { | |
| 130 | + | i = j | |
| 131 | + | continue | |
| 132 | + | } | |
| 133 | + | // The section's name may be a string; its body is the next brace. | |
| 134 | + | for i = j; i < len(text) && text[i] != '{'; { | |
| 135 | + | if k := skipNoise(text, i); k != i { | |
| 136 | + | i = k | |
| 137 | + | continue | |
| 138 | + | } | |
| 139 | + | if text[i] == ';' { | |
| 140 | + | return -1, -1 // a declaration without a body | |
| 141 | + | } | |
| 142 | + | i++ | |
| 143 | + | } | |
| 144 | + | if i >= len(text) { | |
| 145 | + | return -1, -1 | |
| 146 | + | } | |
| 147 | + | open, depth := i, 0 | |
| 148 | + | for i < len(text) { | |
| 149 | + | if k := skipNoise(text, i); k != i { | |
| 150 | + | i = k | |
| 151 | + | continue | |
| 152 | + | } | |
| 153 | + | switch text[i] { | |
| 154 | + | case '{': | |
| 155 | + | depth++ | |
| 156 | + | case '}': | |
| 157 | + | if depth--; depth == 0 { | |
| 158 | + | return open, i | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | i++ | |
| 162 | + | } | |
| 163 | + | return -1, -1 | |
| 164 | + | } | |
| 165 | + | return -1, -1 | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // skipNoise returns the index after a comment (// or # to the end of the | |
| 169 | + | // line), a string or a <key name> starting at i, else i. | |
| 170 | + | func skipNoise(text string, i int) int { | |
| 171 | + | switch { | |
| 172 | + | case text[i] == '#', strings.HasPrefix(text[i:], "//"): | |
| 173 | + | if n := strings.IndexByte(text[i:], '\n'); n >= 0 { | |
| 174 | + | return i + n + 1 | |
| 175 | + | } | |
| 176 | + | return len(text) | |
| 177 | + | case text[i] == '"': | |
| 178 | + | for j := i + 1; j < len(text); j++ { | |
| 179 | + | switch text[j] { | |
| 180 | + | case '\\': | |
| 181 | + | j++ | |
| 182 | + | case '"': | |
| 183 | + | return j + 1 | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | return len(text) | |
| 187 | + | case text[i] == '<': | |
| 188 | + | if n := strings.IndexByte(text[i:], '>'); n >= 0 { | |
| 189 | + | return i + n + 1 | |
| 190 | + | } | |
| 191 | + | return len(text) | |
| 192 | + | } | |
| 193 | + | return i | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | // skipBlank skips spaces and comments from i, up to end. | |
| 197 | + | func skipBlank(text string, i, end int) int { | |
| 198 | + | for i < end { | |
| 199 | + | switch { | |
| 200 | + | case text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r': | |
| 201 | + | i++ | |
| 202 | + | case text[i] == '#' || strings.HasPrefix(text[i:], "//"): | |
| 203 | + | i = skipNoise(text, i) | |
| 204 | + | default: | |
| 205 | + | return i | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | return i | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | func isIdentStart(c byte) bool { return c == '_' || c|0x20 >= 'a' && c|0x20 <= 'z' } | |
| 212 | + | func isIdent(c byte) bool { return isIdentStart(c) || c >= '0' && c <= '9' } | |
| @@ -0,0 +1,212 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strconv" | |||
| 5 | + | "strings" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // minKeycodeMax is the smallest keycode maximum sent to clients. | |||
| 9 | + | // Xwayland (xkb/xkbUtils.c _XkbCopyClientMap, still in 24.1) clears | |||
| 10 | + | // MAP_LENGTH-(max+1) entries starting at that same index of its 256-entry | |||
| 11 | + | // key maps: with a maximum below 127 it writes past the end of the heap | |||
| 12 | + | // block and aborts on the next realloc. Dictation tools (wtype-like) | |||
| 13 | + | // send keymaps holding only the keys they type (e.g. 8..26). Keycodes up | |||
| 14 | + | // to 255 are always valid, so the range is widened without changing any | |||
| 15 | + | // key. | |||
| 16 | + | const minKeycodeMax = 255 | |||
| 17 | + | ||||
| 18 | + | // widenKeycodes raises the keycode maximum of a keymap's xkb_keycodes | |||
| 19 | + | // section to minKeycodeMax. An explicit maximum below it is rewritten; a | |||
| 20 | + | // section without one (its highest keycode is the maximum) gets one, | |||
| 21 | + | // unless it includes other files or already has a keycode above it. It | |||
| 22 | + | // returns the keymap and the former maximum (the highest keycode when | |||
| 23 | + | // none was set), 0 when unchanged. Keywords are case-insensitive; | |||
| 24 | + | // comments, strings and key names are skipped. | |||
| 25 | + | func widenKeycodes(text string) (string, int) { | |||
| 26 | + | open, end := keycodesSection(text) | |||
| 27 | + | if open < 0 { | |||
| 28 | + | return text, 0 | |||
| 29 | + | } | |||
| 30 | + | type span struct{ from, to int } | |||
| 31 | + | var low []span | |||
| 32 | + | maxSet, include := false, false | |||
| 33 | + | from, highest := 0, 0 | |||
| 34 | + | lastName := false // the previous token was a <name>: "= N" is a keycode | |||
| 35 | + | for i := open + 1; i < end; { | |||
| 36 | + | if j := skipNoise(text, i); j != i { | |||
| 37 | + | lastName = text[i] == '<' | |||
| 38 | + | i = j | |||
| 39 | + | continue | |||
| 40 | + | } | |||
| 41 | + | c := text[i] | |||
| 42 | + | switch { | |||
| 43 | + | case isIdentStart(c): | |||
| 44 | + | j := i | |||
| 45 | + | for j < end && isIdent(text[j]) { | |||
| 46 | + | j++ | |||
| 47 | + | } | |||
| 48 | + | word := text[i:j] | |||
| 49 | + | lastName = false | |||
| 50 | + | i = j | |||
| 51 | + | switch { | |||
| 52 | + | case strings.EqualFold(word, "include"), strings.EqualFold(word, "augment"), strings.EqualFold(word, "override"), strings.EqualFold(word, "replace"): | |||
| 53 | + | include = true | |||
| 54 | + | case strings.EqualFold(word, "maximum"): | |||
| 55 | + | k := skipBlank(text, j, end) | |||
| 56 | + | if k >= end || text[k] != '=' { | |||
| 57 | + | continue | |||
| 58 | + | } | |||
| 59 | + | k = skipBlank(text, k+1, end) | |||
| 60 | + | d := k | |||
| 61 | + | for d < end && text[d] >= '0' && text[d] <= '9' { | |||
| 62 | + | d++ | |||
| 63 | + | } | |||
| 64 | + | n, err := strconv.Atoi(text[k:d]) | |||
| 65 | + | if err != nil { | |||
| 66 | + | continue | |||
| 67 | + | } | |||
| 68 | + | maxSet = true | |||
| 69 | + | if n < minKeycodeMax { | |||
| 70 | + | low = append(low, span{k, d}) | |||
| 71 | + | from = n | |||
| 72 | + | } | |||
| 73 | + | i = d | |||
| 74 | + | } | |||
| 75 | + | case c == '=' && lastName: | |||
| 76 | + | k := skipBlank(text, i+1, end) | |||
| 77 | + | d := k | |||
| 78 | + | for d < end && text[d] >= '0' && text[d] <= '9' { | |||
| 79 | + | d++ | |||
| 80 | + | } | |||
| 81 | + | if n, err := strconv.Atoi(text[k:d]); err == nil { | |||
| 82 | + | highest = max(highest, n) | |||
| 83 | + | } | |||
| 84 | + | lastName = false | |||
| 85 | + | i = max(d, i+1) | |||
| 86 | + | default: | |||
| 87 | + | if c != ' ' && c != '\t' && c != '\n' && c != '\r' { | |||
| 88 | + | lastName = false | |||
| 89 | + | } | |||
| 90 | + | i++ | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | if !maxSet { | |||
| 94 | + | if include || highest >= minKeycodeMax || highest == 0 { | |||
| 95 | + | return text, 0 | |||
| 96 | + | } | |||
| 97 | + | return text[:open+1] + " maximum = " + strconv.Itoa(minKeycodeMax) + ";" + text[open+1:], highest | |||
| 98 | + | } | |||
| 99 | + | if len(low) == 0 { | |||
| 100 | + | return text, 0 | |||
| 101 | + | } | |||
| 102 | + | var b strings.Builder | |||
| 103 | + | last := 0 | |||
| 104 | + | for _, sp := range low { | |||
| 105 | + | b.WriteString(text[last:sp.from]) | |||
| 106 | + | b.WriteString(strconv.Itoa(minKeycodeMax)) | |||
| 107 | + | last = sp.to | |||
| 108 | + | } | |||
| 109 | + | b.WriteString(text[last:]) | |||
| 110 | + | return b.String(), from | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // keycodesSection returns the indexes of the braces of the first | |||
| 114 | + | // xkb_keycodes section, -1 without one. | |||
| 115 | + | func keycodesSection(text string) (open, end int) { | |||
| 116 | + | for i := 0; i < len(text); { | |||
| 117 | + | if j := skipNoise(text, i); j != i { | |||
| 118 | + | i = j | |||
| 119 | + | continue | |||
| 120 | + | } | |||
| 121 | + | if !isIdentStart(text[i]) { | |||
| 122 | + | i++ | |||
| 123 | + | continue | |||
| 124 | + | } | |||
| 125 | + | j := i | |||
| 126 | + | for j < len(text) && isIdent(text[j]) { | |||
| 127 | + | j++ | |||
| 128 | + | } | |||
| 129 | + | if !strings.EqualFold(text[i:j], "xkb_keycodes") { | |||
| 130 | + | i = j | |||
| 131 | + | continue | |||
| 132 | + | } | |||
| 133 | + | // The section's name may be a string; its body is the next brace. | |||
| 134 | + | for i = j; i < len(text) && text[i] != '{'; { | |||
| 135 | + | if k := skipNoise(text, i); k != i { | |||
| 136 | + | i = k | |||
| 137 | + | continue | |||
| 138 | + | } | |||
| 139 | + | if text[i] == ';' { | |||
| 140 | + | return -1, -1 // a declaration without a body | |||
| 141 | + | } | |||
| 142 | + | i++ | |||
| 143 | + | } | |||
| 144 | + | if i >= len(text) { | |||
| 145 | + | return -1, -1 | |||
| 146 | + | } | |||
| 147 | + | open, depth := i, 0 | |||
| 148 | + | for i < len(text) { | |||
| 149 | + | if k := skipNoise(text, i); k != i { | |||
| 150 | + | i = k | |||
| 151 | + | continue | |||
| 152 | + | } | |||
| 153 | + | switch text[i] { | |||
| 154 | + | case '{': | |||
| 155 | + | depth++ | |||
| 156 | + | case '}': | |||
| 157 | + | if depth--; depth == 0 { | |||
| 158 | + | return open, i | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | i++ | |||
| 162 | + | } | |||
| 163 | + | return -1, -1 | |||
| 164 | + | } | |||
| 165 | + | return -1, -1 | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // skipNoise returns the index after a comment (// or # to the end of the | |||
| 169 | + | // line), a string or a <key name> starting at i, else i. | |||
| 170 | + | func skipNoise(text string, i int) int { | |||
| 171 | + | switch { | |||
| 172 | + | case text[i] == '#', strings.HasPrefix(text[i:], "//"): | |||
| 173 | + | if n := strings.IndexByte(text[i:], '\n'); n >= 0 { | |||
| 174 | + | return i + n + 1 | |||
| 175 | + | } | |||
| 176 | + | return len(text) | |||
| 177 | + | case text[i] == '"': | |||
| 178 | + | for j := i + 1; j < len(text); j++ { | |||
| 179 | + | switch text[j] { | |||
| 180 | + | case '\\': | |||
| 181 | + | j++ | |||
| 182 | + | case '"': | |||
| 183 | + | return j + 1 | |||
| 184 | + | } | |||
| 185 | + | } | |||
| 186 | + | return len(text) | |||
| 187 | + | case text[i] == '<': | |||
| 188 | + | if n := strings.IndexByte(text[i:], '>'); n >= 0 { | |||
| 189 | + | return i + n + 1 | |||
| 190 | + | } | |||
| 191 | + | return len(text) | |||
| 192 | + | } | |||
| 193 | + | return i | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | // skipBlank skips spaces and comments from i, up to end. | |||
| 197 | + | func skipBlank(text string, i, end int) int { | |||
| 198 | + | for i < end { | |||
| 199 | + | switch { | |||
| 200 | + | case text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r': | |||
| 201 | + | i++ | |||
| 202 | + | case text[i] == '#' || strings.HasPrefix(text[i:], "//"): | |||
| 203 | + | i = skipNoise(text, i) | |||
| 204 | + | default: | |||
| 205 | + | return i | |||
| 206 | + | } | |||
| 207 | + | } | |||
| 208 | + | return i | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | func isIdentStart(c byte) bool { return c == '_' || c|0x20 >= 'a' && c|0x20 <= 'z' } | |||
| 212 | + | func isIdent(c byte) bool { return isIdentStart(c) || c >= '0' && c <= '9' } | |||
M internal/adapters/wayland/virtualkeyboard.go +4 -0
| @@ -58,6 +58,10 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format | |||
| 58 | 58 | k.server.log.Debug().Msg("virtual keymap rejected") | |
| 59 | 59 | return | |
| 60 | 60 | } | |
| 61 | + | if wide, from := widenKeycodes(text); from != 0 { | |
| 62 | + | k.server.log.Debug().Int("maximum", from).Int("widened", minKeycodeMax).Msg("virtual keymap keycode range widened") | |
| 63 | + | text = wide | |
| 64 | + | } | |
| 61 | 65 | copyFD, copySize, err := keymapFile(text) | |
| 62 | 66 | if err != nil { | |
| 63 | 67 | k.server.log.Warn().Err(err).Msg("virtual keymap copy failed") | |
| @@ -58,6 +58,10 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format | |||||
| 58 | k.server.log.Debug().Msg("virtual keymap rejected") | 58 | k.server.log.Debug().Msg("virtual keymap rejected") | ||
| 59 | return | 59 | return | ||
| 60 | } | 60 | } | ||
| 61 | + | if wide, from := widenKeycodes(text); from != 0 { | |||
| 62 | + | k.server.log.Debug().Int("maximum", from).Int("widened", minKeycodeMax).Msg("virtual keymap keycode range widened") | |||
| 63 | + | text = wide | |||
| 64 | + | } | |||
| 61 | copyFD, copySize, err := keymapFile(text) | 65 | copyFD, copySize, err := keymapFile(text) | ||
| 62 | if err != nil { | 66 | if err != nil { | ||
| 63 | k.server.log.Warn().Err(err).Msg("virtual keymap copy failed") | 67 | k.server.log.Warn().Err(err).Msg("virtual keymap copy failed") | ||
M internal/adapters/wayland/virtualkeyboard_test.go +74 -0
| @@ -258,3 +258,77 @@ func TestVirtualKeyboardOtherKeymapSent(t *testing.T) { | |||
| 258 | 258 | t.Fatalf("keymaps %d, want virtual, updated virtual, seat", len(proxy.keymaps)) | |
| 259 | 259 | } | |
| 260 | 260 | } | |
| 261 | + | ||
| 262 | + | // smallKeymap is a dictation tool keymap: only the keys it types. | |
| 263 | + | const smallKeymap = `xkb_keymap { | |
| 264 | + | xkb_keycodes "(unnamed)" { | |
| 265 | + | minimum = 8; | |
| 266 | + | maximum = 26; | |
| 267 | + | <K1> = 9; | |
| 268 | + | }; | |
| 269 | + | xkb_types "(unnamed)" { include "complete" }; | |
| 270 | + | xkb_compatibility "(unnamed)" { include "complete" }; | |
| 271 | + | xkb_symbols "(unnamed)" { | |
| 272 | + | key <K1> {[D]}; | |
| 273 | + | }; | |
| 274 | + | }; | |
| 275 | + | ` | |
| 276 | + | ||
| 277 | + | func TestWidenKeycodes(t *testing.T) { | |
| 278 | + | const tail = "\nxkb_symbols { key <K1> {[D]}; };\n};\n" | |
| 279 | + | cases := []struct { | |
| 280 | + | name, in, want string | |
| 281 | + | from int | |
| 282 | + | }{ | |
| 283 | + | {"small maximum", smallKeymap, strings.Replace(smallKeymap, "maximum = 26;", "maximum = 255;", 1), 26}, | |
| 284 | + | {"maximum 127..254", "xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 200; <K1> = 9; };" + tail, | |
| 285 | + | "xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 255; <K1> = 9; };" + tail, 200}, | |
| 286 | + | {"no maximum", "xkb_keymap {\nxkb_keycodes \"x\" { minimum = 8; <K1> = 9; <K2> = 25; };" + tail, | |
| 287 | + | "xkb_keymap {\nxkb_keycodes \"x\" { maximum = 255; minimum = 8; <K1> = 9; <K2> = 25; };" + tail, 25}, | |
| 288 | + | {"mixed case", "XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 26; <K1> = 9; };" + tail, | |
| 289 | + | "XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 255; <K1> = 9; };" + tail, 26}, | |
| 290 | + | {"brace in comment", "xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 26;\n<K1> = 9;\n};" + tail, | |
| 291 | + | "xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 255;\n<K1> = 9;\n};" + tail, 26}, | |
| 292 | + | {"commented maximum", "xkb_keymap {\nxkb_keycodes {\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail, | |
| 293 | + | "xkb_keymap {\nxkb_keycodes { maximum = 255;\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail, 9}, | |
| 294 | + | {"maximum after comment", "xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 26;\n<K1> = 9;\n};" + tail, | |
| 295 | + | "xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 255;\n<K1> = 9;\n};" + tail, 26}, | |
| 296 | + | } | |
| 297 | + | for _, c := range cases { | |
| 298 | + | got, from := widenKeycodes(c.in) | |
| 299 | + | if got != c.want || from != c.from { | |
| 300 | + | t.Errorf("%s: from %d want %d\n%s", c.name, from, c.from, got) | |
| 301 | + | } | |
| 302 | + | } | |
| 303 | + | for name, keep := range map[string]string{ | |
| 304 | + | "seat keymap": keymapText(t), | |
| 305 | + | "maximum 255": "xkb_keymap { xkb_keycodes { maximum = 255; <K1> = 9; }; };", | |
| 306 | + | "maximum above 255": "xkb_keymap { xkb_keycodes { minimum = 8; maximum = 708; <K1> = 9; }; };", | |
| 307 | + | "no keycodes": "xkb_keymap { xkb_symbols { }; };", | |
| 308 | + | "includes": "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; };", | |
| 309 | + | "maximum in string": "xkb_keymap { xkb_keycodes \"maximum = 26;\" { minimum = 8; <K1> = 300; }; };", | |
| 310 | + | } { | |
| 311 | + | if got, from := widenKeycodes(keep); from != 0 || got != keep { | |
| 312 | + | t.Errorf("%s changed:\n%s", name, got) | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | // Xwayland overflows its key maps on a keymap whose maximum keycode is | |
| 318 | + | // small: clients get the virtual keymap with the full keycode range. | |
| 319 | + | func TestVirtualKeymapWidenedForClients(t *testing.T) { | |
| 320 | + | s, events, commands, dir := keyboardServer(t) | |
| 321 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |
| 322 | + | typer, kb := virtualTyper(t, s, dir) | |
| 323 | + | sendVirtualKeymap(t, typer, kb, smallKeymap) | |
| 324 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(1), uint32(1)) | |
| 325 | + | roundtrip(t, typer, target) | |
| 326 | + | deadline := time.Now().Add(2 * time.Second) | |
| 327 | + | for len(proxy.keymaps) < 1 && time.Now().Before(deadline) { | |
| 328 | + | roundtrip(t, target) | |
| 329 | + | time.Sleep(5 * time.Millisecond) | |
| 330 | + | } | |
| 331 | + | if n := len(proxy.keymaps); n == 0 || !strings.Contains(proxy.keymaps[n-1], "maximum = 255;") || strings.Contains(proxy.keymaps[n-1], "maximum = 26;") { | |
| 332 | + | t.Fatalf("keymaps %q", proxy.keymaps) | |
| 333 | + | } | |
| 334 | + | } | |
| @@ -258,3 +258,77 @@ func TestVirtualKeyboardOtherKeymapSent(t *testing.T) { | |||||
| 258 | t.Fatalf("keymaps %d, want virtual, updated virtual, seat", len(proxy.keymaps)) | 258 | t.Fatalf("keymaps %d, want virtual, updated virtual, seat", len(proxy.keymaps)) | ||
| 259 | } | 259 | } | ||
| 260 | } | 260 | } | ||
| 261 | + | ||||
| 262 | + | // smallKeymap is a dictation tool keymap: only the keys it types. | |||
| 263 | + | const smallKeymap = `xkb_keymap { | |||
| 264 | + | xkb_keycodes "(unnamed)" { | |||
| 265 | + | minimum = 8; | |||
| 266 | + | maximum = 26; | |||
| 267 | + | <K1> = 9; | |||
| 268 | + | }; | |||
| 269 | + | xkb_types "(unnamed)" { include "complete" }; | |||
| 270 | + | xkb_compatibility "(unnamed)" { include "complete" }; | |||
| 271 | + | xkb_symbols "(unnamed)" { | |||
| 272 | + | key <K1> {[D]}; | |||
| 273 | + | }; | |||
| 274 | + | }; | |||
| 275 | + | ` | |||
| 276 | + | ||||
| 277 | + | func TestWidenKeycodes(t *testing.T) { | |||
| 278 | + | const tail = "\nxkb_symbols { key <K1> {[D]}; };\n};\n" | |||
| 279 | + | cases := []struct { | |||
| 280 | + | name, in, want string | |||
| 281 | + | from int | |||
| 282 | + | }{ | |||
| 283 | + | {"small maximum", smallKeymap, strings.Replace(smallKeymap, "maximum = 26;", "maximum = 255;", 1), 26}, | |||
| 284 | + | {"maximum 127..254", "xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 200; <K1> = 9; };" + tail, | |||
| 285 | + | "xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 255; <K1> = 9; };" + tail, 200}, | |||
| 286 | + | {"no maximum", "xkb_keymap {\nxkb_keycodes \"x\" { minimum = 8; <K1> = 9; <K2> = 25; };" + tail, | |||
| 287 | + | "xkb_keymap {\nxkb_keycodes \"x\" { maximum = 255; minimum = 8; <K1> = 9; <K2> = 25; };" + tail, 25}, | |||
| 288 | + | {"mixed case", "XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 26; <K1> = 9; };" + tail, | |||
| 289 | + | "XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 255; <K1> = 9; };" + tail, 26}, | |||
| 290 | + | {"brace in comment", "xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 26;\n<K1> = 9;\n};" + tail, | |||
| 291 | + | "xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 255;\n<K1> = 9;\n};" + tail, 26}, | |||
| 292 | + | {"commented maximum", "xkb_keymap {\nxkb_keycodes {\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail, | |||
| 293 | + | "xkb_keymap {\nxkb_keycodes { maximum = 255;\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail, 9}, | |||
| 294 | + | {"maximum after comment", "xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 26;\n<K1> = 9;\n};" + tail, | |||
| 295 | + | "xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 255;\n<K1> = 9;\n};" + tail, 26}, | |||
| 296 | + | } | |||
| 297 | + | for _, c := range cases { | |||
| 298 | + | got, from := widenKeycodes(c.in) | |||
| 299 | + | if got != c.want || from != c.from { | |||
| 300 | + | t.Errorf("%s: from %d want %d\n%s", c.name, from, c.from, got) | |||
| 301 | + | } | |||
| 302 | + | } | |||
| 303 | + | for name, keep := range map[string]string{ | |||
| 304 | + | "seat keymap": keymapText(t), | |||
| 305 | + | "maximum 255": "xkb_keymap { xkb_keycodes { maximum = 255; <K1> = 9; }; };", | |||
| 306 | + | "maximum above 255": "xkb_keymap { xkb_keycodes { minimum = 8; maximum = 708; <K1> = 9; }; };", | |||
| 307 | + | "no keycodes": "xkb_keymap { xkb_symbols { }; };", | |||
| 308 | + | "includes": "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; };", | |||
| 309 | + | "maximum in string": "xkb_keymap { xkb_keycodes \"maximum = 26;\" { minimum = 8; <K1> = 300; }; };", | |||
| 310 | + | } { | |||
| 311 | + | if got, from := widenKeycodes(keep); from != 0 || got != keep { | |||
| 312 | + | t.Errorf("%s changed:\n%s", name, got) | |||
| 313 | + | } | |||
| 314 | + | } | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | // Xwayland overflows its key maps on a keymap whose maximum keycode is | |||
| 318 | + | // small: clients get the virtual keymap with the full keycode range. | |||
| 319 | + | func TestVirtualKeymapWidenedForClients(t *testing.T) { | |||
| 320 | + | s, events, commands, dir := keyboardServer(t) | |||
| 321 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |||
| 322 | + | typer, kb := virtualTyper(t, s, dir) | |||
| 323 | + | sendVirtualKeymap(t, typer, kb, smallKeymap) | |||
| 324 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(1), uint32(1)) | |||
| 325 | + | roundtrip(t, typer, target) | |||
| 326 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 327 | + | for len(proxy.keymaps) < 1 && time.Now().Before(deadline) { | |||
| 328 | + | roundtrip(t, target) | |||
| 329 | + | time.Sleep(5 * time.Millisecond) | |||
| 330 | + | } | |||
| 331 | + | if n := len(proxy.keymaps); n == 0 || !strings.Contains(proxy.keymaps[n-1], "maximum = 255;") || strings.Contains(proxy.keymaps[n-1], "maximum = 26;") { | |||
| 332 | + | t.Fatalf("keymaps %q", proxy.keymaps) | |||
| 333 | + | } | |||
| 334 | + | } | |||