]> git.proxmox.com Git - systemd.git/blame - hwdb.d/parse_hwdb.py
New upstream version 249~rc1
[systemd.git] / hwdb.d / parse_hwdb.py
CommitLineData
81c58355 1#!/usr/bin/env python3
6e866b33 2# SPDX-License-Identifier: MIT
8a584da2 3#
b012e921 4# This file is distributed under the MIT license, see below.
8a584da2
MP
5#
6# The MIT License (MIT)
7#
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:
14#
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.
25
8a584da2
MP
26import glob
27import string
28import sys
29import os
30
31try:
6e866b33 32 from pyparsing import (Word, White, Literal, ParserElement, Regex, LineEnd,
2897b343 33 OneOrMore, Combine, Or, Optional, Suppress, Group,
8a584da2 34 nums, alphanums, printables,
a032b68d 35 stringEnd, pythonStyleComment,
3a6ce677 36 ParseBaseException, __diag__)
8a584da2
MP
37except ImportError:
38 print('pyparsing is not available')
39 sys.exit(77)
40
41try:
42 from evdev.ecodes import ecodes
43except ImportError:
44 ecodes = None
45 print('WARNING: evdev is not available')
46
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
3a6ce677
BR
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
8a584da2 59EOL = LineEnd().suppress()
2897b343 60EMPTYLINE = LineEnd()
8a584da2
MP
61COMMENTLINE = pythonStyleComment + EOL
62INTEGER = Word(nums)
63REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
2897b343 64SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
8a584da2
MP
65UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
66
a10f5d05 67# Those patterns are used in type-specific matches
8a584da2
MP
68TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
69 'evdev': ('name', 'atkbd', 'input'),
52ad194e 70 'id-input': ('modalias'),
8a584da2 71 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
81c58355 72 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
8a584da2 73 'keyboard': ('name', ),
2897b343 74 'sensor': ('modalias', ),
8b3d4ff0 75 'ieee1394-unit-function' : ('node', ),
2897b343 76 }
8a584da2 77
a10f5d05
MB
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',
8b3d4ff0 86 'ieee1394',
a10f5d05
MB
87 }
88
89def upperhex_word(length):
90 return Word(nums + 'ABCDEF', exact=length)
91
8a584da2
MP
92@lru_cache()
93def hwdb_grammar():
94 ParserElement.setDefaultWhitespaceChars('')
95
96 prefix = Or(category + ':' + Or(conn) + ':'
97 for category, conn in TYPES.items())
a10f5d05
MB
98
99 matchline_typed = Combine(prefix + Word(printables + ' ' + '®'))
100 matchline_general = Combine(Or(GENERAL_MATCHES) + ':' + Word(printables + ' ' + '®'))
101 matchline = (matchline_typed | matchline_general) + EOL
102
8a584da2 103 propertyline = (White(' ', exact=1).suppress() +
8b3d4ff0 104 Combine(UDEV_TAG - '=' - Optional(Word(alphanums + '_=:@*.!-;, "/'))
a032b68d 105 - Optional(pythonStyleComment)) +
8a584da2
MP
106 EOL)
107 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
108
109 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
110 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
2897b343 111 (EMPTYLINE ^ stringEnd()).suppress())
8a584da2
MP
112 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
113
e1f67bc7 114 grammar = OneOrMore(Group(group)('GROUPS*') ^ commentgroup) + stringEnd()
8a584da2
MP
115
116 return grammar
117
118@lru_cache()
119def property_grammar():
120 ParserElement.setDefaultWhitespaceChars(' ')
121
3a6ce677 122 dpi_setting = Group(Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ'))('SETTINGS*')
2897b343 123 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
3a6ce677 124 mount_matrix = Group(mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
a032b68d 125 xkb_setting = Optional(Word(alphanums + '+-/@._'))
2897b343 126
8b3d4ff0
MB
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
2897b343 130 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
8a584da2
MP
131 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
132 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
133 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
134 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
3a6ce677
BR
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')))),
8a584da2
MP
149 ('POINTINGSTICK_SENSITIVITY', INTEGER),
150 ('POINTINGSTICK_CONST_ACCEL', REAL),
81c58355 151 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
8a584da2 152 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
a032b68d
MB
153 ('XKB_FIXED_LAYOUT', xkb_setting),
154 ('XKB_FIXED_VARIANT', xkb_setting),
155 ('XKB_FIXED_MODEL', xkb_setting),
81c58355
MB
156 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
157 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
2897b343 158 ('ACCEL_MOUNT_MATRIX', mount_matrix),
f2dec872 159 ('ACCEL_LOCATION', Or(('display', 'base'))),
46cdbd49 160 ('PROXIMITY_NEAR_LEVEL', INTEGER),
8b3d4ff0
MB
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),
2897b343 166 )
8a584da2
MP
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')
2897b343 172 ]
8a584da2
MP
173 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
174 - Suppress('=') -
175 Word(nums + ':')('VALUE')
2897b343 176 ]
8a584da2 177
2897b343 178 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
8a584da2
MP
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:
2897b343
MP
196 with open(fname, 'r', encoding='UTF-8') as f:
197 parsed = grammar.parseFile(f)
8a584da2
MP
198 except ParseBaseException as e:
199 error('Cannot parse {}: {}', fname, e)
200 return []
201 return [convert_properties(g) for g in parsed.GROUPS]
202
a10f5d05 203def check_matches(groups):
8a584da2 204 matches = sum((group[0] for group in groups), [])
a10f5d05
MB
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
221 if rest[-1] not in '*:':
222 error('pattern {} does not end with "*" or ":"', match)
223
8a584da2
MP
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
f5e65279
MB
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
8a584da2
MP
250def check_one_keycode(prop, value):
251 if value != '!' and ecodes is not None:
252 key = 'KEY_' + value.upper()
97e5042f
MB
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)
8a584da2 258
3a6ce677
BR
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
8a584da2
MP
268def check_properties(groups):
269 grammar = property_grammar()
270 for matches, props in groups:
3a6ce677 271 seen_props = {}
8a584da2
MP
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))
3a6ce677 281 if parsed.NAME in seen_props:
8a584da2 282 error('Property {} is duplicated', parsed.NAME)
3a6ce677 283 seen_props[parsed.NAME] = parsed.VALUE
8a584da2
MP
284 if parsed.NAME == 'MOUSE_DPI':
285 check_one_default(prop, parsed.VALUE.SETTINGS)
f5e65279
MB
286 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
287 check_one_mount_matrix(prop, parsed.VALUE)
8a584da2 288 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
e1f67bc7
MB
289 val = parsed.VALUE if isinstance(parsed.VALUE, str) else parsed.VALUE[0]
290 check_one_keycode(prop, val)
8a584da2 291
3a6ce677
BR
292 check_wheel_clicks(seen_props)
293
8a584da2 294def print_summary(fname, groups):
e1f67bc7
MB
295 n_matches = sum(len(matches) for matches, props in groups)
296 n_props = sum(len(props) for matches, props in groups)
8a584da2 297 print('{}: {} match groups, {} matches, {} properties'
e1f67bc7
MB
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))
8a584da2
MP
302
303if __name__ == '__main__':
8b3d4ff0 304 args = sys.argv[1:] or sorted(glob.glob(os.path.dirname(sys.argv[0]) + '/[678][0-9]-*.hwdb'))
8a584da2
MP
305
306 for fname in args:
307 groups = parse(fname)
308 print_summary(fname, groups)
a10f5d05 309 check_matches(groups)
8a584da2
MP
310 check_properties(groups)
311
312 sys.exit(ERROR)