]> git.proxmox.com Git - systemd.git/blame - hwdb.d/parse_hwdb.py
bump version to 252.11-pve1
[systemd.git] / hwdb.d / parse_hwdb.py
CommitLineData
81c58355 1#!/usr/bin/env python3
2345c4ad
LB
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
6e866b33 4# SPDX-License-Identifier: MIT
8a584da2 5#
b012e921 6# This file is distributed under the MIT license, see below.
8a584da2
MP
7#
8# The MIT License (MIT)
9#
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:
16#
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.
27
8a584da2
MP
28import glob
29import string
30import sys
31import os
32
33try:
6e866b33 34 from pyparsing import (Word, White, Literal, ParserElement, Regex, LineEnd,
2897b343 35 OneOrMore, Combine, Or, Optional, Suppress, Group,
8a584da2 36 nums, alphanums, printables,
a032b68d 37 stringEnd, pythonStyleComment,
2345c4ad 38 ParseBaseException)
8a584da2
MP
39except ImportError:
40 print('pyparsing is not available')
41 sys.exit(77)
42
2345c4ad
LB
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
8a584da2
MP
54try:
55 from evdev.ecodes import ecodes
56except ImportError:
57 ecodes = None
58 print('WARNING: evdev is not available')
59
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
66EOL = LineEnd().suppress()
2897b343 67EMPTYLINE = LineEnd()
8a584da2
MP
68COMMENTLINE = pythonStyleComment + EOL
69INTEGER = Word(nums)
70REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
2897b343 71SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
8a584da2
MP
72UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
73
a10f5d05 74# Those patterns are used in type-specific matches
8a584da2
MP
75TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
76 'evdev': ('name', 'atkbd', 'input'),
c3c690cd 77 'fb': ('pci'),
52ad194e 78 'id-input': ('modalias'),
8a584da2 79 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
81c58355 80 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
8a584da2 81 'keyboard': ('name', ),
086111aa
LB
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 ),
8b3d4ff0 96 'ieee1394-unit-function' : ('node', ),
ea0999c9 97 'camera': ('usb'),
2897b343 98 }
8a584da2 99
a10f5d05
MB
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',
8b3d4ff0 108 'ieee1394',
a10f5d05
MB
109 }
110
111def upperhex_word(length):
112 return Word(nums + 'ABCDEF', exact=length)
113
8a584da2
MP
114@lru_cache()
115def hwdb_grammar():
116 ParserElement.setDefaultWhitespaceChars('')
117
118 prefix = Or(category + ':' + Or(conn) + ':'
119 for category, conn in TYPES.items())
a10f5d05
MB
120
121 matchline_typed = Combine(prefix + Word(printables + ' ' + '®'))
122 matchline_general = Combine(Or(GENERAL_MATCHES) + ':' + Word(printables + ' ' + '®'))
123 matchline = (matchline_typed | matchline_general) + EOL
124
8a584da2 125 propertyline = (White(' ', exact=1).suppress() +
8b3d4ff0 126 Combine(UDEV_TAG - '=' - Optional(Word(alphanums + '_=:@*.!-;, "/'))
a032b68d 127 - Optional(pythonStyleComment)) +
8a584da2
MP
128 EOL)
129 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
130
131 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
132 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
2897b343 133 (EMPTYLINE ^ stringEnd()).suppress())
8a584da2
MP
134 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
135
e1f67bc7 136 grammar = OneOrMore(Group(group)('GROUPS*') ^ commentgroup) + stringEnd()
8a584da2
MP
137
138 return grammar
139
140@lru_cache()
141def property_grammar():
142 ParserElement.setDefaultWhitespaceChars(' ')
143
f5caa8fa 144 dpi_setting = Group(Optional('*')('DEFAULT') + INTEGER('DPI') + Optional(Suppress('@') + INTEGER('HZ')))('SETTINGS*')
2897b343 145 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
3a6ce677 146 mount_matrix = Group(mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
a032b68d 147 xkb_setting = Optional(Word(alphanums + '+-/@._'))
2897b343 148
8b3d4ff0
MB
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
2897b343 152 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
8a584da2
MP
153 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
154 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
155 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
156 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
3a6ce677 157 ('ID_AUTOSUSPEND', Or((Literal('0'), Literal('1')))),
b3e21333 158 ('ID_AUTOSUSPEND_DELAY_MS', INTEGER),
f5caa8fa 159 ('ID_AV_PRODUCTION_CONTROLLER', Or((Literal('0'), Literal('1')))),
ea0999c9 160 ('ID_PERSIST', Or((Literal('0'), Literal('1')))),
f5caa8fa 161 ('ID_PDA', Or((Literal('0'), Literal('1')))),
3a6ce677
BR
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')))),
ea0999c9 175 ('ID_SIGNAL_ANALYZER', Or((Literal('0'), Literal('1')))),
8a584da2
MP
176 ('POINTINGSTICK_SENSITIVITY', INTEGER),
177 ('POINTINGSTICK_CONST_ACCEL', REAL),
81c58355 178 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
8a584da2 179 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
a032b68d
MB
180 ('XKB_FIXED_LAYOUT', xkb_setting),
181 ('XKB_FIXED_VARIANT', xkb_setting),
182 ('XKB_FIXED_MODEL', xkb_setting),
81c58355
MB
183 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
184 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
2897b343 185 ('ACCEL_MOUNT_MATRIX', mount_matrix),
f2dec872 186 ('ACCEL_LOCATION', Or(('display', 'base'))),
46cdbd49 187 ('PROXIMITY_NEAR_LEVEL', INTEGER),
8b3d4ff0
MB
188 ('IEEE1394_UNIT_FUNCTION_MIDI', Or((Literal('0'), Literal('1')))),
189 ('IEEE1394_UNIT_FUNCTION_AUDIO', Or((Literal('0'), Literal('1')))),
190 ('IEEE1394_UNIT_FUNCTION_VIDEO', Or((Literal('0'), Literal('1')))),
191 ('ID_VENDOR_FROM_DATABASE', name_literal),
192 ('ID_MODEL_FROM_DATABASE', name_literal),
c3c690cd 193 ('ID_TAG_MASTER_OF_SEAT', Literal('1')),
ea0999c9
MB
194 ('ID_INFRARED_CAMERA', Or((Literal('0'), Literal('1')))),
195 ('ID_CAMERA_DIRECTION', Or(('front', 'rear'))),
2897b343 196 )
8a584da2
MP
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')
2897b343 202 ]
8a584da2
MP
203 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
204 - Suppress('=') -
b7785f44 205 Word('-' + nums + ':')('VALUE')
2897b343 206 ]
8a584da2 207
2897b343 208 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
8a584da2
MP
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:
2897b343
MP
226 with open(fname, 'r', encoding='UTF-8') as f:
227 parsed = grammar.parseFile(f)
8a584da2
MP
228 except ParseBaseException as e:
229 error('Cannot parse {}: {}', fname, e)
230 return []
231 return [convert_properties(g) for g in parsed.GROUPS]
232
a10f5d05 233def check_matches(groups):
8a584da2 234 matches = sum((group[0] for group in groups), [])
a10f5d05
MB
235
236 # This is a partial check. The other cases could be also done, but those
237 # two are most commonly wrong.
36ceca03
MB
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(':')) + '*',
a10f5d05
MB
240 }
241
242 for match in matches:
243 prefix, rest = match.split(':', maxsplit=1)
244 gr = grammars.get(prefix)
245 if gr:
36ceca03
MB
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
a10f5d05
MB
250 try:
251 gr.parseString(rest)
252 except ParseBaseException as e:
253 error('Pattern {!r} is invalid: {}', rest, e)
254 continue
a10f5d05 255
8a584da2
MP
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
f5e65279
MB
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
2345c4ad 282def check_one_keycode(value):
8a584da2
MP
283 if value != '!' and ecodes is not None:
284 key = 'KEY_' + value.upper()
97e5042f
MB
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)
8a584da2 290
3a6ce677
BR
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
8a584da2
MP
300def check_properties(groups):
301 grammar = property_grammar()
2345c4ad 302 for _, props in groups:
3a6ce677 303 seen_props = {}
8a584da2
MP
304 for prop in props:
305 # print('--', prop)
306 prop = prop.partition('#')[0].rstrip()
307 try:
308 parsed = grammar.parseString(prop)
2345c4ad 309 except ParseBaseException:
8a584da2
MP
310 error('Failed to parse: {!r}', prop)
311 continue
312 # print('{!r}'.format(parsed))
3a6ce677 313 if parsed.NAME in seen_props:
8a584da2 314 error('Property {} is duplicated', parsed.NAME)
3a6ce677 315 seen_props[parsed.NAME] = parsed.VALUE
8a584da2
MP
316 if parsed.NAME == 'MOUSE_DPI':
317 check_one_default(prop, parsed.VALUE.SETTINGS)
f5e65279
MB
318 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
319 check_one_mount_matrix(prop, parsed.VALUE)
8a584da2 320 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
e1f67bc7 321 val = parsed.VALUE if isinstance(parsed.VALUE, str) else parsed.VALUE[0]
2345c4ad 322 check_one_keycode(val)
8a584da2 323
3a6ce677
BR
324 check_wheel_clicks(seen_props)
325
8a584da2 326def print_summary(fname, groups):
e1f67bc7
MB
327 n_matches = sum(len(matches) for matches, props in groups)
328 n_props = sum(len(props) for matches, props in groups)
2345c4ad 329 print(f'{fname}: {len(groups)} match groups, {n_matches} matches, {n_props} properties')
e1f67bc7
MB
330
331 if n_matches == 0 or n_props == 0:
2345c4ad 332 error(f'{fname}: no matches or props')
8a584da2
MP
333
334if __name__ == '__main__':
8b3d4ff0 335 args = sys.argv[1:] or sorted(glob.glob(os.path.dirname(sys.argv[0]) + '/[678][0-9]-*.hwdb'))
8a584da2
MP
336
337 for fname in args:
338 groups = parse(fname)
339 print_summary(fname, groups)
a10f5d05 340 check_matches(groups)
8a584da2
MP
341 check_properties(groups)
342
343 sys.exit(ERROR)