]> git.ipfire.org Git - thirdparty/systemd.git/blame - hwdb.d/parse_hwdb.py
license: LGPL-2.1+ -> LGPL-2.1-or-later
[thirdparty/systemd.git] / hwdb.d / parse_hwdb.py
CommitLineData
3e67e5c9 1#!/usr/bin/env python3
d8a0bcfd 2# SPDX-License-Identifier: MIT
0c9836c0 3#
818bf546 4# This file is distributed under the MIT license, see below.
0c9836c0 5#
acb98601 6# The MIT License (MIT)
0c9836c0 7#
acb98601
ZJS
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
0c9836c0 14#
acb98601
ZJS
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24# SOFTWARE.
0c9836c0 25
c3f6a561
ZJS
26import glob
27import string
28import sys
29import os
0c9836c0
ZJS
30
31try:
af182e78 32 from pyparsing import (Word, White, Literal, ParserElement, Regex, LineEnd,
8d9d1e3a 33 OneOrMore, Combine, Or, Optional, Suppress, Group,
0c9836c0 34 nums, alphanums, printables,
a136c2cd 35 stringEnd, pythonStyleComment,
0c9836c0
ZJS
36 ParseBaseException)
37except ImportError:
c3f6a561
ZJS
38 print('pyparsing is not available')
39 sys.exit(77)
0c9836c0
ZJS
40
41try:
42 from evdev.ecodes import ecodes
43except ImportError:
44 ecodes = None
45 print('WARNING: evdev is not available')
46
1258f088
ZJS
47try:
48 from functools import lru_cache
49except ImportError:
50 # don't do caching on old python
51 lru_cache = lambda: (lambda f: f)
52
0c9836c0 53EOL = LineEnd().suppress()
f644a6da 54EMPTYLINE = LineEnd()
0c9836c0
ZJS
55COMMENTLINE = pythonStyleComment + EOL
56INTEGER = Word(nums)
57REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
7fdc73af 58SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
0c9836c0
ZJS
59UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
60
aa549ff3 61# Those patterns are used in type-specific matches
0c9836c0
ZJS
62TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
63 'evdev': ('name', 'atkbd', 'input'),
ffac3034 64 'id-input': ('modalias'),
0c9836c0 65 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
c8ec393b 66 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
0c9836c0 67 'keyboard': ('name', ),
7fdc73af 68 'sensor': ('modalias', ),
8d9d1e3a 69 }
0c9836c0 70
aa549ff3
ZJS
71# Patterns that are used to set general properties on a device
72GENERAL_MATCHES = {'acpi',
73 'bluetooth',
74 'usb',
75 'pci',
76 'sdio',
77 'vmbus',
78 'OUI',
79 }
80
77547d53
ZJS
81def upperhex_word(length):
82 return Word(nums + 'ABCDEF', exact=length)
83
1258f088 84@lru_cache()
0c9836c0
ZJS
85def hwdb_grammar():
86 ParserElement.setDefaultWhitespaceChars('')
87
88 prefix = Or(category + ':' + Or(conn) + ':'
89 for category, conn in TYPES.items())
aa549ff3
ZJS
90
91 matchline_typed = Combine(prefix + Word(printables + ' ' + '®'))
457763aa 92 matchline_general = Combine(Or(GENERAL_MATCHES) + ':' + Word(printables + ' ' + '®'))
aa549ff3
ZJS
93 matchline = (matchline_typed | matchline_general) + EOL
94
0c9836c0 95 propertyline = (White(' ', exact=1).suppress() +
a136c2cd
ZJS
96 Combine(UDEV_TAG - '=' - Optional(Word(alphanums + '_=:@*.!-;, "'))
97 - Optional(pythonStyleComment)) +
0c9836c0
ZJS
98 EOL)
99 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
100
101 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
102 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
8d9d1e3a 103 (EMPTYLINE ^ stringEnd()).suppress())
0c9836c0
ZJS
104 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
105
2382a2e3 106 grammar = OneOrMore(Group(group)('GROUPS*') ^ commentgroup) + stringEnd()
0c9836c0
ZJS
107
108 return grammar
109
1258f088 110@lru_cache()
0c9836c0
ZJS
111def property_grammar():
112 ParserElement.setDefaultWhitespaceChars(' ')
113
7fdc73af
ZJS
114 dpi_setting = (Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ'))('SETTINGS*')
115 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
116 mount_matrix = (mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
a136c2cd 117 xkb_setting = Optional(Word(alphanums + '+-/@._'))
7fdc73af
ZJS
118
119 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
0c9836c0 120 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
a6a8e60b 121 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
9107a337
PH
122 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
123 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
aa549ff3 124 ('ID_AUTOSUSPEND', Literal('1')),
ffac3034
PH
125 ('ID_INPUT', Literal('1')),
126 ('ID_INPUT_ACCELEROMETER', Literal('1')),
127 ('ID_INPUT_JOYSTICK', Literal('1')),
128 ('ID_INPUT_KEY', Literal('1')),
129 ('ID_INPUT_KEYBOARD', Literal('1')),
130 ('ID_INPUT_MOUSE', Literal('1')),
131 ('ID_INPUT_POINTINGSTICK', Literal('1')),
132 ('ID_INPUT_SWITCH', Literal('1')),
133 ('ID_INPUT_TABLET', Literal('1')),
134 ('ID_INPUT_TABLET_PAD', Literal('1')),
135 ('ID_INPUT_TOUCHPAD', Literal('1')),
136 ('ID_INPUT_TOUCHSCREEN', Literal('1')),
0c9836c0
ZJS
137 ('ID_INPUT_TRACKBALL', Literal('1')),
138 ('POINTINGSTICK_SENSITIVITY', INTEGER),
139 ('POINTINGSTICK_CONST_ACCEL', REAL),
c8ec393b 140 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
0c9836c0 141 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
a136c2cd
ZJS
142 ('XKB_FIXED_LAYOUT', xkb_setting),
143 ('XKB_FIXED_VARIANT', xkb_setting),
144 ('XKB_FIXED_MODEL', xkb_setting),
b698b5cf
BN
145 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
146 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
7fdc73af 147 ('ACCEL_MOUNT_MATRIX', mount_matrix),
331e34fe 148 ('ACCEL_LOCATION', Or(('display', 'base'))),
1c5b427f 149 ('PROXIMITY_NEAR_LEVEL', INTEGER),
8d9d1e3a 150 )
0c9836c0
ZJS
151 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
152 for name, val in props]
153 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
154 - Suppress('=') -
155 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
8d9d1e3a 156 ]
0c9836c0
ZJS
157 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
158 - Suppress('=') -
159 Word(nums + ':')('VALUE')
8d9d1e3a 160 ]
0c9836c0 161
fe2a2a4f 162 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
0c9836c0
ZJS
163
164 return grammar
165
166ERROR = False
167def error(fmt, *args, **kwargs):
168 global ERROR
169 ERROR = True
170 print(fmt.format(*args, **kwargs))
171
172def convert_properties(group):
173 matches = [m[0] for m in group.MATCHES]
174 props = [p[0] for p in group.PROPERTIES]
175 return matches, props
176
177def parse(fname):
178 grammar = hwdb_grammar()
179 try:
aeceb390
MP
180 with open(fname, 'r', encoding='UTF-8') as f:
181 parsed = grammar.parseFile(f)
0c9836c0
ZJS
182 except ParseBaseException as e:
183 error('Cannot parse {}: {}', fname, e)
184 return []
185 return [convert_properties(g) for g in parsed.GROUPS]
186
77547d53 187def check_matches(groups):
0c9836c0 188 matches = sum((group[0] for group in groups), [])
77547d53
ZJS
189
190 # This is a partial check. The other cases could be also done, but those
191 # two are most commonly wrong.
192 grammars = { 'usb' : 'v' + upperhex_word(4) + Optional('p' + upperhex_word(4)),
193 'pci' : 'v' + upperhex_word(8) + Optional('d' + upperhex_word(8)),
194 }
195
196 for match in matches:
197 prefix, rest = match.split(':', maxsplit=1)
198 gr = grammars.get(prefix)
199 if gr:
200 try:
201 gr.parseString(rest)
202 except ParseBaseException as e:
203 error('Pattern {!r} is invalid: {}', rest, e)
204 continue
a21ac934
ZJS
205 if rest[-1] not in '*:':
206 error('pattern {} does not end with "*" or ":"', match)
77547d53 207
0c9836c0
ZJS
208 matches.sort()
209 prev = None
210 for match in matches:
211 if match == prev:
212 error('Match {!r} is duplicated', match)
213 prev = match
214
215def check_one_default(prop, settings):
216 defaults = [s for s in settings if s.DEFAULT]
217 if len(defaults) > 1:
218 error('More than one star entry: {!r}', prop)
219
494d16aa
ZJS
220def check_one_mount_matrix(prop, value):
221 numbers = [s for s in value if s not in {';', ','}]
222 if len(numbers) != 9:
223 error('Wrong accel matrix: {!r}', prop)
224 try:
225 numbers = [abs(float(number)) for number in numbers]
226 except ValueError:
227 error('Wrong accel matrix: {!r}', prop)
228 bad_x, bad_y, bad_z = max(numbers[0:3]) == 0, max(numbers[3:6]) == 0, max(numbers[6:9]) == 0
229 if bad_x or bad_y or bad_z:
230 error('Mount matrix is all zero in {} row: {!r}',
231 'x' if bad_x else ('y' if bad_y else 'z'),
232 prop)
233
0c9836c0
ZJS
234def check_one_keycode(prop, value):
235 if value != '!' and ecodes is not None:
236 key = 'KEY_' + value.upper()
12c7d4d6
ZJS
237 if not (key in ecodes or
238 value.upper() in ecodes or
239 # new keys added in kernel 5.5
240 'KBD_LCD_MENU' in key):
241 error('Keycode {} unknown', key)
0c9836c0
ZJS
242
243def check_properties(groups):
244 grammar = property_grammar()
245 for matches, props in groups:
246 prop_names = set()
247 for prop in props:
248 # print('--', prop)
249 prop = prop.partition('#')[0].rstrip()
250 try:
251 parsed = grammar.parseString(prop)
252 except ParseBaseException as e:
253 error('Failed to parse: {!r}', prop)
254 continue
255 # print('{!r}'.format(parsed))
256 if parsed.NAME in prop_names:
257 error('Property {} is duplicated', parsed.NAME)
258 prop_names.add(parsed.NAME)
259 if parsed.NAME == 'MOUSE_DPI':
260 check_one_default(prop, parsed.VALUE.SETTINGS)
494d16aa
ZJS
261 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
262 check_one_mount_matrix(prop, parsed.VALUE)
0c9836c0 263 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
2382a2e3
ZJS
264 val = parsed.VALUE if isinstance(parsed.VALUE, str) else parsed.VALUE[0]
265 check_one_keycode(prop, val)
0c9836c0
ZJS
266
267def print_summary(fname, groups):
f3c80bc0
ZJS
268 n_matches = sum(len(matches) for matches, props in groups)
269 n_props = sum(len(props) for matches, props in groups)
0c9836c0 270 print('{}: {} match groups, {} matches, {} properties'
f3c80bc0
ZJS
271 .format(fname, len(groups), n_matches, n_props))
272
273 if n_matches == 0 or n_props == 0:
274 error('{}: no matches or props'.format(fname))
0c9836c0
ZJS
275
276if __name__ == '__main__':
b32ae3aa 277 args = sys.argv[1:] or sorted(glob.glob(os.path.dirname(sys.argv[0]) + '/[67][0-9]-*.hwdb'))
c3f6a561
ZJS
278
279 for fname in args:
0c9836c0
ZJS
280 groups = parse(fname)
281 print_summary(fname, groups)
77547d53 282 check_matches(groups)
0c9836c0
ZJS
283 check_properties(groups)
284
285 sys.exit(ERROR)