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