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