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