]> git.ipfire.org Git - thirdparty/systemd.git/blob - hwdb/parse_hwdb.py
hwdb: add XKB_FIXED_LAYOUT/VARIANT to the keyboard hwdb
[thirdparty/systemd.git] / hwdb / parse_hwdb.py
1 #!/usr/bin/python3
2 # -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
3 #
4 # This file is part of systemd. It is distrubuted under the MIT license, see
5 # below.
6 #
7 # Copyright 2016 Zbigniew Jędrzejewski-Szmek
8 #
9 # The MIT License (MIT)
10 #
11 # Permission is hereby granted, free of charge, to any person obtaining a copy
12 # of this software and associated documentation files (the "Software"), to deal
13 # in the Software without restriction, including without limitation the rights
14 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 # copies of the Software, and to permit persons to whom the Software is
16 # furnished to do so, subject to the following conditions:
17 #
18 # The above copyright notice and this permission notice shall be included in
19 # all copies or substantial portions of the Software.
20 #
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 # SOFTWARE.
28
29 import functools
30 import glob
31 import string
32 import sys
33 import os
34
35 try:
36 from pyparsing import (Word, White, Literal, ParserElement, Regex,
37 LineStart, LineEnd,
38 ZeroOrMore, OneOrMore, Combine, Or, Optional, Suppress, Group,
39 nums, alphanums, printables,
40 stringEnd, pythonStyleComment, QuotedString,
41 ParseBaseException)
42 except ImportError:
43 print('pyparsing is not available')
44 sys.exit(77)
45
46 try:
47 from evdev.ecodes import ecodes
48 except ImportError:
49 ecodes = None
50 print('WARNING: evdev is not available')
51
52 try:
53 from functools import lru_cache
54 except ImportError:
55 # don't do caching on old python
56 lru_cache = lambda: (lambda f: f)
57
58 EOL = LineEnd().suppress()
59 EMPTYLINE = LineEnd()
60 COMMENTLINE = pythonStyleComment + EOL
61 INTEGER = Word(nums)
62 STRING = QuotedString('"')
63 REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
64 UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
65
66 TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
67 'evdev': ('name', 'atkbd', 'input'),
68 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
69 'keyboard': ('name', ),
70 }
71
72 @lru_cache()
73 def hwdb_grammar():
74 ParserElement.setDefaultWhitespaceChars('')
75
76 prefix = Or(category + ':' + Or(conn) + ':'
77 for category, conn in TYPES.items())
78 matchline = Combine(prefix + Word(printables + ' ' + '®')) + EOL
79 propertyline = (White(' ', exact=1).suppress() +
80 Combine(UDEV_TAG - '=' - Word(alphanums + '_=:@*.! "') - Optional(pythonStyleComment)) +
81 EOL)
82 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
83
84 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
85 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
86 (EMPTYLINE ^ stringEnd()).suppress() )
87 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
88
89 grammar = OneOrMore(group('GROUPS*') ^ commentgroup) + stringEnd()
90
91 return grammar
92
93 @lru_cache()
94 def property_grammar():
95 ParserElement.setDefaultWhitespaceChars(' ')
96
97 setting = Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ')
98 props = (('MOUSE_DPI', Group(OneOrMore(setting('SETTINGS*')))),
99 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
100 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
101 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
102 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
103 ('ID_INPUT_TRACKBALL', Literal('1')),
104 ('POINTINGSTICK_SENSITIVITY', INTEGER),
105 ('POINTINGSTICK_CONST_ACCEL', REAL),
106 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
107 ('XKB_FIXED_LAYOUT', STRING),
108 ('XKB_FIXED_VARIANT', STRING)
109 )
110 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
111 for name, val in props]
112 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
113 - Suppress('=') -
114 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
115 ]
116 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
117 - Suppress('=') -
118 Word(nums + ':')('VALUE')
119 ]
120
121 grammar = Or(fixed_props + kbd_props + abs_props)
122
123 return grammar
124
125 ERROR = False
126 def error(fmt, *args, **kwargs):
127 global ERROR
128 ERROR = True
129 print(fmt.format(*args, **kwargs))
130
131 def convert_properties(group):
132 matches = [m[0] for m in group.MATCHES]
133 props = [p[0] for p in group.PROPERTIES]
134 return matches, props
135
136 def parse(fname):
137 grammar = hwdb_grammar()
138 try:
139 with open(fname, 'r', encoding='UTF-8') as f:
140 parsed = grammar.parseFile(f)
141 except ParseBaseException as e:
142 error('Cannot parse {}: {}', fname, e)
143 return []
144 return [convert_properties(g) for g in parsed.GROUPS]
145
146 def check_match_uniqueness(groups):
147 matches = sum((group[0] for group in groups), [])
148 matches.sort()
149 prev = None
150 for match in matches:
151 if match == prev:
152 error('Match {!r} is duplicated', match)
153 prev = match
154
155 def check_one_default(prop, settings):
156 defaults = [s for s in settings if s.DEFAULT]
157 if len(defaults) > 1:
158 error('More than one star entry: {!r}', prop)
159
160 def check_one_keycode(prop, value):
161 if value != '!' and ecodes is not None:
162 key = 'KEY_' + value.upper()
163 if key not in ecodes:
164 error('Keycode {} unknown', key)
165
166 def check_properties(groups):
167 grammar = property_grammar()
168 for matches, props in groups:
169 prop_names = set()
170 for prop in props:
171 # print('--', prop)
172 prop = prop.partition('#')[0].rstrip()
173 try:
174 parsed = grammar.parseString(prop)
175 except ParseBaseException as e:
176 error('Failed to parse: {!r}', prop)
177 continue
178 # print('{!r}'.format(parsed))
179 if parsed.NAME in prop_names:
180 error('Property {} is duplicated', parsed.NAME)
181 prop_names.add(parsed.NAME)
182 if parsed.NAME == 'MOUSE_DPI':
183 check_one_default(prop, parsed.VALUE.SETTINGS)
184 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
185 check_one_keycode(prop, parsed.VALUE)
186
187 def print_summary(fname, groups):
188 print('{}: {} match groups, {} matches, {} properties'
189 .format(fname,
190 len(groups),
191 sum(len(matches) for matches, props in groups),
192 sum(len(props) for matches, props in groups),
193 ))
194
195 if __name__ == '__main__':
196 args = sys.argv[1:] or glob.glob(os.path.dirname(sys.argv[0]) + '/[67]0-*.hwdb')
197
198 for fname in args:
199 groups = parse(fname)
200 print_summary(fname, groups)
201 check_match_uniqueness(groups)
202 check_properties(groups)
203
204 sys.exit(ERROR)