]> git.ipfire.org Git - thirdparty/systemd.git/blame - hwdb.d/parse_hwdb.py
hwdb: allow parser to expect usage of slash sign in value of property
[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,
e77fed20 36 ParseBaseException, __diag__)
0c9836c0 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
e77fed20
YW
53__diag__.warn_multiple_tokens_in_named_alternation = True
54__diag__.warn_ungrouped_named_tokens_in_collection = True
55__diag__.warn_name_set_on_empty_Forward = True
56__diag__.warn_on_multiple_string_args_to_oneof = True
57__diag__.enable_debug_on_named_expressions = True
58
0c9836c0 59EOL = LineEnd().suppress()
f644a6da 60EMPTYLINE = LineEnd()
0c9836c0
ZJS
61COMMENTLINE = pythonStyleComment + EOL
62INTEGER = Word(nums)
63REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
7fdc73af 64SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
0c9836c0
ZJS
65UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
66
aa549ff3 67# Those patterns are used in type-specific matches
0c9836c0
ZJS
68TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
69 'evdev': ('name', 'atkbd', 'input'),
ffac3034 70 'id-input': ('modalias'),
0c9836c0 71 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
c8ec393b 72 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
0c9836c0 73 'keyboard': ('name', ),
7fdc73af 74 'sensor': ('modalias', ),
7713f3fc 75 'ieee1394-unit-function' : ('node', ),
8d9d1e3a 76 }
0c9836c0 77
aa549ff3
ZJS
78# Patterns that are used to set general properties on a device
79GENERAL_MATCHES = {'acpi',
80 'bluetooth',
81 'usb',
82 'pci',
83 'sdio',
84 'vmbus',
85 'OUI',
7713f3fc 86 'ieee1394',
aa549ff3
ZJS
87 }
88
77547d53
ZJS
89def upperhex_word(length):
90 return Word(nums + 'ABCDEF', exact=length)
91
1258f088 92@lru_cache()
0c9836c0
ZJS
93def hwdb_grammar():
94 ParserElement.setDefaultWhitespaceChars('')
95
96 prefix = Or(category + ':' + Or(conn) + ':'
97 for category, conn in TYPES.items())
aa549ff3
ZJS
98
99 matchline_typed = Combine(prefix + Word(printables + ' ' + '®'))
457763aa 100 matchline_general = Combine(Or(GENERAL_MATCHES) + ':' + Word(printables + ' ' + '®'))
aa549ff3
ZJS
101 matchline = (matchline_typed | matchline_general) + EOL
102
0c9836c0 103 propertyline = (White(' ', exact=1).suppress() +
5e939304 104 Combine(UDEV_TAG - '=' - Optional(Word(alphanums + '_=:@*.!-;, "/'))
a136c2cd 105 - Optional(pythonStyleComment)) +
0c9836c0
ZJS
106 EOL)
107 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
108
109 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
110 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
8d9d1e3a 111 (EMPTYLINE ^ stringEnd()).suppress())
0c9836c0
ZJS
112 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
113
2382a2e3 114 grammar = OneOrMore(Group(group)('GROUPS*') ^ commentgroup) + stringEnd()
0c9836c0
ZJS
115
116 return grammar
117
1258f088 118@lru_cache()
0c9836c0
ZJS
119def property_grammar():
120 ParserElement.setDefaultWhitespaceChars(' ')
121
315a3c9f 122 dpi_setting = Group(Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ'))('SETTINGS*')
7fdc73af 123 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
315a3c9f 124 mount_matrix = Group(mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
a136c2cd 125 xkb_setting = Optional(Word(alphanums + '+-/@._'))
7fdc73af 126
7713f3fc
TS
127 # Although this set doesn't cover all of characters in database entries, it's enough for test targets.
128 name_literal = Word(printables + ' ')
129
7fdc73af 130 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
0c9836c0 131 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
a6a8e60b 132 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
9107a337
PH
133 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
134 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
ad0d9c01
BD
135 ('ID_AUTOSUSPEND', Or((Literal('0'), Literal('1')))),
136 ('ID_INPUT', Or((Literal('0'), Literal('1')))),
137 ('ID_INPUT_ACCELEROMETER', Or((Literal('0'), Literal('1')))),
138 ('ID_INPUT_JOYSTICK', Or((Literal('0'), Literal('1')))),
139 ('ID_INPUT_KEY', Or((Literal('0'), Literal('1')))),
140 ('ID_INPUT_KEYBOARD', Or((Literal('0'), Literal('1')))),
141 ('ID_INPUT_MOUSE', Or((Literal('0'), Literal('1')))),
142 ('ID_INPUT_POINTINGSTICK', Or((Literal('0'), Literal('1')))),
143 ('ID_INPUT_SWITCH', Or((Literal('0'), Literal('1')))),
144 ('ID_INPUT_TABLET', Or((Literal('0'), Literal('1')))),
145 ('ID_INPUT_TABLET_PAD', Or((Literal('0'), Literal('1')))),
146 ('ID_INPUT_TOUCHPAD', Or((Literal('0'), Literal('1')))),
147 ('ID_INPUT_TOUCHSCREEN', Or((Literal('0'), Literal('1')))),
148 ('ID_INPUT_TRACKBALL', Or((Literal('0'), Literal('1')))),
0c9836c0
ZJS
149 ('POINTINGSTICK_SENSITIVITY', INTEGER),
150 ('POINTINGSTICK_CONST_ACCEL', REAL),
c8ec393b 151 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
0c9836c0 152 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
a136c2cd
ZJS
153 ('XKB_FIXED_LAYOUT', xkb_setting),
154 ('XKB_FIXED_VARIANT', xkb_setting),
155 ('XKB_FIXED_MODEL', xkb_setting),
b698b5cf
BN
156 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
157 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
7fdc73af 158 ('ACCEL_MOUNT_MATRIX', mount_matrix),
331e34fe 159 ('ACCEL_LOCATION', Or(('display', 'base'))),
1c5b427f 160 ('PROXIMITY_NEAR_LEVEL', INTEGER),
7713f3fc
TS
161 ('IEEE1394_UNIT_FUNCTION_MIDI', Or((Literal('0'), Literal('1')))),
162 ('IEEE1394_UNIT_FUNCTION_AUDIO', Or((Literal('0'), Literal('1')))),
163 ('IEEE1394_UNIT_FUNCTION_VIDEO', Or((Literal('0'), Literal('1')))),
164 ('ID_VENDOR_FROM_DATABASE', name_literal),
165 ('ID_MODEL_FROM_DATABASE', name_literal),
8d9d1e3a 166 )
0c9836c0
ZJS
167 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
168 for name, val in props]
169 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
170 - Suppress('=') -
171 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
8d9d1e3a 172 ]
0c9836c0
ZJS
173 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
174 - Suppress('=') -
175 Word(nums + ':')('VALUE')
8d9d1e3a 176 ]
0c9836c0 177
fe2a2a4f 178 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
0c9836c0
ZJS
179
180 return grammar
181
182ERROR = False
183def error(fmt, *args, **kwargs):
184 global ERROR
185 ERROR = True
186 print(fmt.format(*args, **kwargs))
187
188def convert_properties(group):
189 matches = [m[0] for m in group.MATCHES]
190 props = [p[0] for p in group.PROPERTIES]
191 return matches, props
192
193def parse(fname):
194 grammar = hwdb_grammar()
195 try:
aeceb390
MP
196 with open(fname, 'r', encoding='UTF-8') as f:
197 parsed = grammar.parseFile(f)
0c9836c0
ZJS
198 except ParseBaseException as e:
199 error('Cannot parse {}: {}', fname, e)
200 return []
201 return [convert_properties(g) for g in parsed.GROUPS]
202
77547d53 203def check_matches(groups):
0c9836c0 204 matches = sum((group[0] for group in groups), [])
77547d53
ZJS
205
206 # This is a partial check. The other cases could be also done, but those
207 # two are most commonly wrong.
208 grammars = { 'usb' : 'v' + upperhex_word(4) + Optional('p' + upperhex_word(4)),
209 'pci' : 'v' + upperhex_word(8) + Optional('d' + upperhex_word(8)),
210 }
211
212 for match in matches:
213 prefix, rest = match.split(':', maxsplit=1)
214 gr = grammars.get(prefix)
215 if gr:
216 try:
217 gr.parseString(rest)
218 except ParseBaseException as e:
219 error('Pattern {!r} is invalid: {}', rest, e)
220 continue
a21ac934
ZJS
221 if rest[-1] not in '*:':
222 error('pattern {} does not end with "*" or ":"', match)
77547d53 223
0c9836c0
ZJS
224 matches.sort()
225 prev = None
226 for match in matches:
227 if match == prev:
228 error('Match {!r} is duplicated', match)
229 prev = match
230
231def check_one_default(prop, settings):
232 defaults = [s for s in settings if s.DEFAULT]
233 if len(defaults) > 1:
234 error('More than one star entry: {!r}', prop)
235
494d16aa
ZJS
236def check_one_mount_matrix(prop, value):
237 numbers = [s for s in value if s not in {';', ','}]
238 if len(numbers) != 9:
239 error('Wrong accel matrix: {!r}', prop)
240 try:
241 numbers = [abs(float(number)) for number in numbers]
242 except ValueError:
243 error('Wrong accel matrix: {!r}', prop)
244 bad_x, bad_y, bad_z = max(numbers[0:3]) == 0, max(numbers[3:6]) == 0, max(numbers[6:9]) == 0
245 if bad_x or bad_y or bad_z:
246 error('Mount matrix is all zero in {} row: {!r}',
247 'x' if bad_x else ('y' if bad_y else 'z'),
248 prop)
249
0c9836c0
ZJS
250def check_one_keycode(prop, value):
251 if value != '!' and ecodes is not None:
252 key = 'KEY_' + value.upper()
12c7d4d6
ZJS
253 if not (key in ecodes or
254 value.upper() in ecodes or
255 # new keys added in kernel 5.5
256 'KBD_LCD_MENU' in key):
257 error('Keycode {} unknown', key)
0c9836c0 258
9fc168cd
PH
259def check_wheel_clicks(properties):
260 pairs = (('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', 'MOUSE_WHEEL_CLICK_COUNT'),
261 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', 'MOUSE_WHEEL_CLICK_ANGLE'),
262 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', 'MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL'),
263 ('MOUSE_WHEEL_CLICK_COUNT', 'MOUSE_WHEEL_CLICK_ANGLE'))
264 for pair in pairs:
265 if pair[0] in properties and pair[1] not in properties:
266 error('{} requires {} to be specified', *pair)
267
0c9836c0
ZJS
268def check_properties(groups):
269 grammar = property_grammar()
270 for matches, props in groups:
9fc168cd 271 seen_props = {}
0c9836c0
ZJS
272 for prop in props:
273 # print('--', prop)
274 prop = prop.partition('#')[0].rstrip()
275 try:
276 parsed = grammar.parseString(prop)
277 except ParseBaseException as e:
278 error('Failed to parse: {!r}', prop)
279 continue
280 # print('{!r}'.format(parsed))
9fc168cd 281 if parsed.NAME in seen_props:
0c9836c0 282 error('Property {} is duplicated', parsed.NAME)
9fc168cd 283 seen_props[parsed.NAME] = parsed.VALUE
0c9836c0
ZJS
284 if parsed.NAME == 'MOUSE_DPI':
285 check_one_default(prop, parsed.VALUE.SETTINGS)
494d16aa
ZJS
286 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
287 check_one_mount_matrix(prop, parsed.VALUE)
0c9836c0 288 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
2382a2e3
ZJS
289 val = parsed.VALUE if isinstance(parsed.VALUE, str) else parsed.VALUE[0]
290 check_one_keycode(prop, val)
0c9836c0 291
9fc168cd
PH
292 check_wheel_clicks(seen_props)
293
0c9836c0 294def print_summary(fname, groups):
f3c80bc0
ZJS
295 n_matches = sum(len(matches) for matches, props in groups)
296 n_props = sum(len(props) for matches, props in groups)
0c9836c0 297 print('{}: {} match groups, {} matches, {} properties'
f3c80bc0
ZJS
298 .format(fname, len(groups), n_matches, n_props))
299
300 if n_matches == 0 or n_props == 0:
301 error('{}: no matches or props'.format(fname))
0c9836c0
ZJS
302
303if __name__ == '__main__':
b32ae3aa 304 args = sys.argv[1:] or sorted(glob.glob(os.path.dirname(sys.argv[0]) + '/[67][0-9]-*.hwdb'))
c3f6a561
ZJS
305
306 for fname in args:
0c9836c0
ZJS
307 groups = parse(fname)
308 print_summary(fname, groups)
77547d53 309 check_matches(groups)
0c9836c0
ZJS
310 check_properties(groups)
311
312 sys.exit(ERROR)