]> git.ipfire.org Git - thirdparty/systemd.git/blob - hwdb/parse_hwdb.py
Merge pull request #7096 from keszybz/logind-session-killing
[thirdparty/systemd.git] / hwdb / parse_hwdb.py
1 #!/usr/bin/env python3
2 # -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
3 #
4 # This file is part of systemd. It is distrubuted under the MIT license, see
5 # below.
6 #
7 # Copyright 2016 Zbigniew Jędrzejewski-Szmek
8 #
9 # The MIT License (MIT)
10 #
11 # Permission is hereby granted, free of charge, to any person obtaining a copy
12 # of this software and associated documentation files (the "Software"), to deal
13 # in the Software without restriction, including without limitation the rights
14 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 # copies of the Software, and to permit persons to whom the Software is
16 # furnished to do so, subject to the following conditions:
17 #
18 # The above copyright notice and this permission notice shall be included in
19 # all copies or substantial portions of the Software.
20 #
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 # SOFTWARE.
28
29 import glob
30 import string
31 import sys
32 import os
33
34 try:
35 from pyparsing import (Word, White, Literal, ParserElement, Regex,
36 LineStart, LineEnd,
37 OneOrMore, Combine, Or, Optional, Suppress, Group,
38 nums, alphanums, printables,
39 stringEnd, pythonStyleComment, QuotedString,
40 ParseBaseException)
41 except ImportError:
42 print('pyparsing is not available')
43 sys.exit(77)
44
45 try:
46 from evdev.ecodes import ecodes
47 except ImportError:
48 ecodes = None
49 print('WARNING: evdev is not available')
50
51 try:
52 from functools import lru_cache
53 except ImportError:
54 # don't do caching on old python
55 lru_cache = lambda: (lambda f: f)
56
57 EOL = LineEnd().suppress()
58 EMPTYLINE = LineEnd()
59 COMMENTLINE = pythonStyleComment + EOL
60 INTEGER = Word(nums)
61 STRING = QuotedString('"')
62 REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
63 SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
64 UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
65
66 TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
67 'evdev': ('name', 'atkbd', 'input'),
68 'id-input': ('modalias'),
69 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
70 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
71 'keyboard': ('name', ),
72 'sensor': ('modalias', ),
73 }
74
75 @lru_cache()
76 def hwdb_grammar():
77 ParserElement.setDefaultWhitespaceChars('')
78
79 prefix = Or(category + ':' + Or(conn) + ':'
80 for category, conn in TYPES.items())
81 matchline = Combine(prefix + Word(printables + ' ' + '®')) + EOL
82 propertyline = (White(' ', exact=1).suppress() +
83 Combine(UDEV_TAG - '=' - Word(alphanums + '_=:@*.!-;, "') - Optional(pythonStyleComment)) +
84 EOL)
85 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
86
87 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
88 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
89 (EMPTYLINE ^ stringEnd()).suppress())
90 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
91
92 grammar = OneOrMore(group('GROUPS*') ^ commentgroup) + stringEnd()
93
94 return grammar
95
96 @lru_cache()
97 def property_grammar():
98 ParserElement.setDefaultWhitespaceChars(' ')
99
100 dpi_setting = (Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ'))('SETTINGS*')
101 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
102 mount_matrix = (mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
103
104 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
105 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
106 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
107 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
108 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
109 ('ID_INPUT', Literal('1')),
110 ('ID_INPUT_ACCELEROMETER', Literal('1')),
111 ('ID_INPUT_JOYSTICK', Literal('1')),
112 ('ID_INPUT_KEY', Literal('1')),
113 ('ID_INPUT_KEYBOARD', Literal('1')),
114 ('ID_INPUT_MOUSE', Literal('1')),
115 ('ID_INPUT_POINTINGSTICK', Literal('1')),
116 ('ID_INPUT_SWITCH', Literal('1')),
117 ('ID_INPUT_TABLET', Literal('1')),
118 ('ID_INPUT_TABLET_PAD', Literal('1')),
119 ('ID_INPUT_TOUCHPAD', Literal('1')),
120 ('ID_INPUT_TOUCHSCREEN', Literal('1')),
121 ('ID_INPUT_TRACKBALL', Literal('1')),
122 ('MOUSE_WHEEL_TILT_HORIZONTAL', Literal('1')),
123 ('MOUSE_WHEEL_TILT_VERTICAL', Literal('1')),
124 ('POINTINGSTICK_SENSITIVITY', INTEGER),
125 ('POINTINGSTICK_CONST_ACCEL', REAL),
126 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
127 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
128 ('XKB_FIXED_LAYOUT', STRING),
129 ('XKB_FIXED_VARIANT', STRING),
130 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
131 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
132 ('ACCEL_MOUNT_MATRIX', mount_matrix),
133 )
134 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
135 for name, val in props]
136 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
137 - Suppress('=') -
138 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
139 ]
140 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
141 - Suppress('=') -
142 Word(nums + ':')('VALUE')
143 ]
144
145 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
146
147 return grammar
148
149 ERROR = False
150 def error(fmt, *args, **kwargs):
151 global ERROR
152 ERROR = True
153 print(fmt.format(*args, **kwargs))
154
155 def convert_properties(group):
156 matches = [m[0] for m in group.MATCHES]
157 props = [p[0] for p in group.PROPERTIES]
158 return matches, props
159
160 def parse(fname):
161 grammar = hwdb_grammar()
162 try:
163 with open(fname, 'r', encoding='UTF-8') as f:
164 parsed = grammar.parseFile(f)
165 except ParseBaseException as e:
166 error('Cannot parse {}: {}', fname, e)
167 return []
168 return [convert_properties(g) for g in parsed.GROUPS]
169
170 def check_match_uniqueness(groups):
171 matches = sum((group[0] for group in groups), [])
172 matches.sort()
173 prev = None
174 for match in matches:
175 if match == prev:
176 error('Match {!r} is duplicated', match)
177 prev = match
178
179 def check_one_default(prop, settings):
180 defaults = [s for s in settings if s.DEFAULT]
181 if len(defaults) > 1:
182 error('More than one star entry: {!r}', prop)
183
184 def check_one_mount_matrix(prop, value):
185 numbers = [s for s in value if s not in {';', ','}]
186 if len(numbers) != 9:
187 error('Wrong accel matrix: {!r}', prop)
188 try:
189 numbers = [abs(float(number)) for number in numbers]
190 except ValueError:
191 error('Wrong accel matrix: {!r}', prop)
192 bad_x, bad_y, bad_z = max(numbers[0:3]) == 0, max(numbers[3:6]) == 0, max(numbers[6:9]) == 0
193 if bad_x or bad_y or bad_z:
194 error('Mount matrix is all zero in {} row: {!r}',
195 'x' if bad_x else ('y' if bad_y else 'z'),
196 prop)
197
198 def check_one_keycode(prop, value):
199 if value != '!' and ecodes is not None:
200 key = 'KEY_' + value.upper()
201 if key not in ecodes:
202 key = value.upper()
203 if key not in ecodes:
204 error('Keycode {} unknown', key)
205
206 def check_properties(groups):
207 grammar = property_grammar()
208 for matches, props in groups:
209 prop_names = set()
210 for prop in props:
211 # print('--', prop)
212 prop = prop.partition('#')[0].rstrip()
213 try:
214 parsed = grammar.parseString(prop)
215 except ParseBaseException as e:
216 error('Failed to parse: {!r}', prop)
217 continue
218 # print('{!r}'.format(parsed))
219 if parsed.NAME in prop_names:
220 error('Property {} is duplicated', parsed.NAME)
221 prop_names.add(parsed.NAME)
222 if parsed.NAME == 'MOUSE_DPI':
223 check_one_default(prop, parsed.VALUE.SETTINGS)
224 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
225 check_one_mount_matrix(prop, parsed.VALUE)
226 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
227 check_one_keycode(prop, parsed.VALUE)
228
229 def print_summary(fname, groups):
230 print('{}: {} match groups, {} matches, {} properties'
231 .format(fname,
232 len(groups),
233 sum(len(matches) for matches, props in groups),
234 sum(len(props) for matches, props in groups)))
235
236 if __name__ == '__main__':
237 args = sys.argv[1:] or glob.glob(os.path.dirname(sys.argv[0]) + '/[67]0-*.hwdb')
238
239 for fname in args:
240 groups = parse(fname)
241 print_summary(fname, groups)
242 check_match_uniqueness(groups)
243 check_properties(groups)
244
245 sys.exit(ERROR)