]> git.ipfire.org Git - thirdparty/systemd.git/blame - hwdb.d/parse_hwdb.py
hwdb: allow spaces in usb: matches and similar patterns
[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,
086c001e 35 stringEnd, pythonStyleComment, QuotedString,
0c9836c0
ZJS
36 ParseBaseException)
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
0c9836c0 53EOL = LineEnd().suppress()
f644a6da 54EMPTYLINE = LineEnd()
0c9836c0
ZJS
55COMMENTLINE = pythonStyleComment + EOL
56INTEGER = Word(nums)
086c001e 57STRING = QuotedString('"')
0c9836c0 58REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
7fdc73af 59SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
0c9836c0
ZJS
60UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
61
aa549ff3 62# Those patterns are used in type-specific matches
0c9836c0
ZJS
63TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
64 'evdev': ('name', 'atkbd', 'input'),
ffac3034 65 'id-input': ('modalias'),
0c9836c0 66 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
c8ec393b 67 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
0c9836c0 68 'keyboard': ('name', ),
7fdc73af 69 'sensor': ('modalias', ),
8d9d1e3a 70 }
0c9836c0 71
aa549ff3
ZJS
72# Patterns that are used to set general properties on a device
73GENERAL_MATCHES = {'acpi',
74 'bluetooth',
75 'usb',
76 'pci',
77 'sdio',
78 'vmbus',
79 'OUI',
80 }
81
1258f088 82@lru_cache()
0c9836c0
ZJS
83def hwdb_grammar():
84 ParserElement.setDefaultWhitespaceChars('')
85
86 prefix = Or(category + ':' + Or(conn) + ':'
87 for category, conn in TYPES.items())
aa549ff3
ZJS
88
89 matchline_typed = Combine(prefix + Word(printables + ' ' + '®'))
457763aa 90 matchline_general = Combine(Or(GENERAL_MATCHES) + ':' + Word(printables + ' ' + '®'))
aa549ff3
ZJS
91 matchline = (matchline_typed | matchline_general) + EOL
92
0c9836c0 93 propertyline = (White(' ', exact=1).suppress() +
7fdc73af 94 Combine(UDEV_TAG - '=' - Word(alphanums + '_=:@*.!-;, "') - Optional(pythonStyleComment)) +
0c9836c0
ZJS
95 EOL)
96 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
97
98 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
99 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
8d9d1e3a 100 (EMPTYLINE ^ stringEnd()).suppress())
0c9836c0
ZJS
101 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
102
2382a2e3 103 grammar = OneOrMore(Group(group)('GROUPS*') ^ commentgroup) + stringEnd()
0c9836c0
ZJS
104
105 return grammar
106
1258f088 107@lru_cache()
0c9836c0
ZJS
108def property_grammar():
109 ParserElement.setDefaultWhitespaceChars(' ')
110
7fdc73af
ZJS
111 dpi_setting = (Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ'))('SETTINGS*')
112 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
113 mount_matrix = (mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
114
115 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
0c9836c0 116 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
a6a8e60b 117 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
9107a337
PH
118 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
119 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
aa549ff3 120 ('ID_AUTOSUSPEND', Literal('1')),
ffac3034
PH
121 ('ID_INPUT', Literal('1')),
122 ('ID_INPUT_ACCELEROMETER', Literal('1')),
123 ('ID_INPUT_JOYSTICK', Literal('1')),
124 ('ID_INPUT_KEY', Literal('1')),
125 ('ID_INPUT_KEYBOARD', Literal('1')),
126 ('ID_INPUT_MOUSE', Literal('1')),
127 ('ID_INPUT_POINTINGSTICK', Literal('1')),
128 ('ID_INPUT_SWITCH', Literal('1')),
129 ('ID_INPUT_TABLET', Literal('1')),
130 ('ID_INPUT_TABLET_PAD', Literal('1')),
131 ('ID_INPUT_TOUCHPAD', Literal('1')),
132 ('ID_INPUT_TOUCHSCREEN', Literal('1')),
0c9836c0
ZJS
133 ('ID_INPUT_TRACKBALL', Literal('1')),
134 ('POINTINGSTICK_SENSITIVITY', INTEGER),
135 ('POINTINGSTICK_CONST_ACCEL', REAL),
c8ec393b 136 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
0c9836c0 137 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
086c001e 138 ('XKB_FIXED_LAYOUT', STRING),
8d9d1e3a 139 ('XKB_FIXED_VARIANT', STRING),
d7d31692 140 ('XKB_FIXED_MODEL', STRING),
b698b5cf
BN
141 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
142 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
7fdc73af 143 ('ACCEL_MOUNT_MATRIX', mount_matrix),
331e34fe 144 ('ACCEL_LOCATION', Or(('display', 'base'))),
1c5b427f 145 ('PROXIMITY_NEAR_LEVEL', INTEGER),
8d9d1e3a 146 )
0c9836c0
ZJS
147 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
148 for name, val in props]
149 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
150 - Suppress('=') -
151 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
8d9d1e3a 152 ]
0c9836c0
ZJS
153 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
154 - Suppress('=') -
155 Word(nums + ':')('VALUE')
8d9d1e3a 156 ]
0c9836c0 157
fe2a2a4f 158 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
0c9836c0
ZJS
159
160 return grammar
161
162ERROR = False
163def error(fmt, *args, **kwargs):
164 global ERROR
165 ERROR = True
166 print(fmt.format(*args, **kwargs))
167
168def convert_properties(group):
169 matches = [m[0] for m in group.MATCHES]
170 props = [p[0] for p in group.PROPERTIES]
171 return matches, props
172
173def parse(fname):
174 grammar = hwdb_grammar()
175 try:
aeceb390
MP
176 with open(fname, 'r', encoding='UTF-8') as f:
177 parsed = grammar.parseFile(f)
0c9836c0
ZJS
178 except ParseBaseException as e:
179 error('Cannot parse {}: {}', fname, e)
180 return []
181 return [convert_properties(g) for g in parsed.GROUPS]
182
183def check_match_uniqueness(groups):
184 matches = sum((group[0] for group in groups), [])
185 matches.sort()
186 prev = None
187 for match in matches:
188 if match == prev:
189 error('Match {!r} is duplicated', match)
190 prev = match
191
192def check_one_default(prop, settings):
193 defaults = [s for s in settings if s.DEFAULT]
194 if len(defaults) > 1:
195 error('More than one star entry: {!r}', prop)
196
494d16aa
ZJS
197def check_one_mount_matrix(prop, value):
198 numbers = [s for s in value if s not in {';', ','}]
199 if len(numbers) != 9:
200 error('Wrong accel matrix: {!r}', prop)
201 try:
202 numbers = [abs(float(number)) for number in numbers]
203 except ValueError:
204 error('Wrong accel matrix: {!r}', prop)
205 bad_x, bad_y, bad_z = max(numbers[0:3]) == 0, max(numbers[3:6]) == 0, max(numbers[6:9]) == 0
206 if bad_x or bad_y or bad_z:
207 error('Mount matrix is all zero in {} row: {!r}',
208 'x' if bad_x else ('y' if bad_y else 'z'),
209 prop)
210
0c9836c0
ZJS
211def check_one_keycode(prop, value):
212 if value != '!' and ecodes is not None:
213 key = 'KEY_' + value.upper()
12c7d4d6
ZJS
214 if not (key in ecodes or
215 value.upper() in ecodes or
216 # new keys added in kernel 5.5
217 'KBD_LCD_MENU' in key):
218 error('Keycode {} unknown', key)
0c9836c0
ZJS
219
220def check_properties(groups):
221 grammar = property_grammar()
222 for matches, props in groups:
223 prop_names = set()
224 for prop in props:
225 # print('--', prop)
226 prop = prop.partition('#')[0].rstrip()
227 try:
228 parsed = grammar.parseString(prop)
229 except ParseBaseException as e:
230 error('Failed to parse: {!r}', prop)
231 continue
232 # print('{!r}'.format(parsed))
233 if parsed.NAME in prop_names:
234 error('Property {} is duplicated', parsed.NAME)
235 prop_names.add(parsed.NAME)
236 if parsed.NAME == 'MOUSE_DPI':
237 check_one_default(prop, parsed.VALUE.SETTINGS)
494d16aa
ZJS
238 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
239 check_one_mount_matrix(prop, parsed.VALUE)
0c9836c0 240 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
2382a2e3
ZJS
241 val = parsed.VALUE if isinstance(parsed.VALUE, str) else parsed.VALUE[0]
242 check_one_keycode(prop, val)
0c9836c0
ZJS
243
244def print_summary(fname, groups):
f3c80bc0
ZJS
245 n_matches = sum(len(matches) for matches, props in groups)
246 n_props = sum(len(props) for matches, props in groups)
0c9836c0 247 print('{}: {} match groups, {} matches, {} properties'
f3c80bc0
ZJS
248 .format(fname, len(groups), n_matches, n_props))
249
250 if n_matches == 0 or n_props == 0:
251 error('{}: no matches or props'.format(fname))
0c9836c0
ZJS
252
253if __name__ == '__main__':
b32ae3aa 254 args = sys.argv[1:] or sorted(glob.glob(os.path.dirname(sys.argv[0]) + '/[67][0-9]-*.hwdb'))
c3f6a561
ZJS
255
256 for fname in args:
0c9836c0
ZJS
257 groups = parse(fname)
258 print_summary(fname, groups)
259 check_match_uniqueness(groups)
260 check_properties(groups)
261
262 sys.exit(ERROR)