]> git.ipfire.org Git - thirdparty/systemd.git/blame_incremental - hwdb.d/parse_hwdb.py
Merge pull request #22277 from yuwata/test-network-activation-policy
[thirdparty/systemd.git] / hwdb.d / parse_hwdb.py
... / ...
CommitLineData
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3#
4# This file is distributed under the MIT license, see below.
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
26import glob
27import string
28import sys
29import os
30
31try:
32 from pyparsing import (Word, White, Literal, ParserElement, Regex, LineEnd,
33 OneOrMore, Combine, Or, Optional, Suppress, Group,
34 nums, alphanums, printables,
35 stringEnd, pythonStyleComment,
36 ParseBaseException, __diag__)
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
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
59EOL = LineEnd().suppress()
60EMPTYLINE = LineEnd()
61COMMENTLINE = pythonStyleComment + EOL
62INTEGER = Word(nums)
63REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
64SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
65UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
66
67# Those patterns are used in type-specific matches
68TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
69 'evdev': ('name', 'atkbd', 'input'),
70 'fb': ('pci'),
71 'id-input': ('modalias'),
72 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
73 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
74 'keyboard': ('name', ),
75 'sensor': ('modalias', ),
76 'ieee1394-unit-function' : ('node', ),
77 'camera': ('usb'),
78 }
79
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',
88 'ieee1394',
89 }
90
91def upperhex_word(length):
92 return Word(nums + 'ABCDEF', exact=length)
93
94@lru_cache()
95def hwdb_grammar():
96 ParserElement.setDefaultWhitespaceChars('')
97
98 prefix = Or(category + ':' + Or(conn) + ':'
99 for category, conn in TYPES.items())
100
101 matchline_typed = Combine(prefix + Word(printables + ' ' + '®'))
102 matchline_general = Combine(Or(GENERAL_MATCHES) + ':' + Word(printables + ' ' + '®'))
103 matchline = (matchline_typed | matchline_general) + EOL
104
105 propertyline = (White(' ', exact=1).suppress() +
106 Combine(UDEV_TAG - '=' - Optional(Word(alphanums + '_=:@*.!-;, "/'))
107 - Optional(pythonStyleComment)) +
108 EOL)
109 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
110
111 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
112 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
113 (EMPTYLINE ^ stringEnd()).suppress())
114 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
115
116 grammar = OneOrMore(Group(group)('GROUPS*') ^ commentgroup) + stringEnd()
117
118 return grammar
119
120@lru_cache()
121def property_grammar():
122 ParserElement.setDefaultWhitespaceChars(' ')
123
124 dpi_setting = Group(Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ'))('SETTINGS*')
125 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
126 mount_matrix = Group(mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
127 xkb_setting = Optional(Word(alphanums + '+-/@._'))
128
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
132 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
133 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
134 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
135 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
136 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
137 ('ID_AUTOSUSPEND', Or((Literal('0'), Literal('1')))),
138 ('ID_PERSIST', Or((Literal('0'), Literal('1')))),
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')))),
152 ('ID_SIGNAL_ANALYZER', Or((Literal('0'), Literal('1')))),
153 ('POINTINGSTICK_SENSITIVITY', INTEGER),
154 ('POINTINGSTICK_CONST_ACCEL', REAL),
155 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
156 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
157 ('XKB_FIXED_LAYOUT', xkb_setting),
158 ('XKB_FIXED_VARIANT', xkb_setting),
159 ('XKB_FIXED_MODEL', xkb_setting),
160 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
161 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
162 ('ACCEL_MOUNT_MATRIX', mount_matrix),
163 ('ACCEL_LOCATION', Or(('display', 'base'))),
164 ('PROXIMITY_NEAR_LEVEL', INTEGER),
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),
170 ('ID_TAG_MASTER_OF_SEAT', Literal('1')),
171 ('ID_INFRARED_CAMERA', Or((Literal('0'), Literal('1')))),
172 ('ID_CAMERA_DIRECTION', Or(('front', 'rear'))),
173 )
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')
179 ]
180 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
181 - Suppress('=') -
182 Word(nums + ':')('VALUE')
183 ]
184
185 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
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:
203 with open(fname, 'r', encoding='UTF-8') as f:
204 parsed = grammar.parseFile(f)
205 except ParseBaseException as e:
206 error('Cannot parse {}: {}', fname, e)
207 return []
208 return [convert_properties(g) for g in parsed.GROUPS]
209
210def check_matches(groups):
211 matches = sum((group[0] for group in groups), [])
212
213 # This is a partial check. The other cases could be also done, but those
214 # two are most commonly wrong.
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(':')) + '*',
217 }
218
219 for match in matches:
220 prefix, rest = match.split(':', maxsplit=1)
221 gr = grammars.get(prefix)
222 if gr:
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
227 try:
228 gr.parseString(rest)
229 except ParseBaseException as e:
230 error('Pattern {!r} is invalid: {}', rest, e)
231 continue
232
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
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
259def check_one_keycode(prop, value):
260 if value != '!' and ecodes is not None:
261 key = 'KEY_' + value.upper()
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)
267
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
277def check_properties(groups):
278 grammar = property_grammar()
279 for matches, props in groups:
280 seen_props = {}
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))
290 if parsed.NAME in seen_props:
291 error('Property {} is duplicated', parsed.NAME)
292 seen_props[parsed.NAME] = parsed.VALUE
293 if parsed.NAME == 'MOUSE_DPI':
294 check_one_default(prop, parsed.VALUE.SETTINGS)
295 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
296 check_one_mount_matrix(prop, parsed.VALUE)
297 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
298 val = parsed.VALUE if isinstance(parsed.VALUE, str) else parsed.VALUE[0]
299 check_one_keycode(prop, val)
300
301 check_wheel_clicks(seen_props)
302
303def print_summary(fname, groups):
304 n_matches = sum(len(matches) for matches, props in groups)
305 n_props = sum(len(props) for matches, props in groups)
306 print('{}: {} match groups, {} matches, {} properties'
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))
311
312if __name__ == '__main__':
313 args = sys.argv[1:] or sorted(glob.glob(os.path.dirname(sys.argv[0]) + '/[678][0-9]-*.hwdb'))
314
315 for fname in args:
316 groups = parse(fname)
317 print_summary(fname, groups)
318 check_matches(groups)
319 check_properties(groups)
320
321 sys.exit(ERROR)