]> git.ipfire.org Git - thirdparty/systemd.git/blame - hwdb/parse_hwdb.py
hwdb: add a hwdb file to override ID_INPUT assignments
[thirdparty/systemd.git] / hwdb / parse_hwdb.py
CommitLineData
3e67e5c9 1#!/usr/bin/env python3
acb98601 2# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
0c9836c0 3#
acb98601
ZJS
4# This file is part of systemd. It is distrubuted under the MIT license, see
5# below.
0c9836c0 6#
acb98601 7# Copyright 2016 Zbigniew Jędrzejewski-Szmek
0c9836c0 8#
acb98601 9# The MIT License (MIT)
0c9836c0 10#
acb98601
ZJS
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:
0c9836c0 17#
acb98601
ZJS
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.
0c9836c0 28
c3f6a561
ZJS
29import glob
30import string
31import sys
32import os
0c9836c0
ZJS
33
34try:
35 from pyparsing import (Word, White, Literal, ParserElement, Regex,
36 LineStart, LineEnd,
8d9d1e3a 37 OneOrMore, Combine, Or, Optional, Suppress, Group,
0c9836c0 38 nums, alphanums, printables,
086c001e 39 stringEnd, pythonStyleComment, QuotedString,
0c9836c0
ZJS
40 ParseBaseException)
41except ImportError:
c3f6a561
ZJS
42 print('pyparsing is not available')
43 sys.exit(77)
0c9836c0
ZJS
44
45try:
46 from evdev.ecodes import ecodes
47except ImportError:
48 ecodes = None
49 print('WARNING: evdev is not available')
50
1258f088
ZJS
51try:
52 from functools import lru_cache
53except ImportError:
54 # don't do caching on old python
55 lru_cache = lambda: (lambda f: f)
56
0c9836c0 57EOL = LineEnd().suppress()
f644a6da 58EMPTYLINE = LineEnd()
0c9836c0
ZJS
59COMMENTLINE = pythonStyleComment + EOL
60INTEGER = Word(nums)
086c001e 61STRING = QuotedString('"')
0c9836c0 62REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
7fdc73af 63SIGNED_REAL = Combine(Optional(Word('-+')) + REAL)
0c9836c0
ZJS
64UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
65
66TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
67 'evdev': ('name', 'atkbd', 'input'),
ffac3034 68 'id-input': ('modalias'),
0c9836c0 69 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
c8ec393b 70 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
0c9836c0 71 'keyboard': ('name', ),
7fdc73af 72 'sensor': ('modalias', ),
8d9d1e3a 73 }
0c9836c0 74
1258f088 75@lru_cache()
0c9836c0
ZJS
76def 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() +
7fdc73af 83 Combine(UDEV_TAG - '=' - Word(alphanums + '_=:@*.!-;, "') - Optional(pythonStyleComment)) +
0c9836c0
ZJS
84 EOL)
85 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
86
87 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
88 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
8d9d1e3a 89 (EMPTYLINE ^ stringEnd()).suppress())
0c9836c0
ZJS
90 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
91
92 grammar = OneOrMore(group('GROUPS*') ^ commentgroup) + stringEnd()
93
94 return grammar
95
1258f088 96@lru_cache()
0c9836c0
ZJS
97def property_grammar():
98 ParserElement.setDefaultWhitespaceChars(' ')
99
7fdc73af
ZJS
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))),
0c9836c0 105 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
a6a8e60b 106 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
9107a337
PH
107 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
108 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
ffac3034
PH
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')),
0c9836c0 121 ('ID_INPUT_TRACKBALL', Literal('1')),
1a4132ae
PH
122 ('MOUSE_WHEEL_TILT_HORIZONTAL', Literal('1')),
123 ('MOUSE_WHEEL_TILT_VERTICAL', Literal('1')),
0c9836c0
ZJS
124 ('POINTINGSTICK_SENSITIVITY', INTEGER),
125 ('POINTINGSTICK_CONST_ACCEL', REAL),
c8ec393b 126 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
0c9836c0 127 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
086c001e 128 ('XKB_FIXED_LAYOUT', STRING),
8d9d1e3a 129 ('XKB_FIXED_VARIANT', STRING),
b698b5cf
BN
130 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
131 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
7fdc73af 132 ('ACCEL_MOUNT_MATRIX', mount_matrix),
8d9d1e3a 133 )
0c9836c0
ZJS
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')
8d9d1e3a 139 ]
0c9836c0
ZJS
140 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
141 - Suppress('=') -
142 Word(nums + ':')('VALUE')
8d9d1e3a 143 ]
0c9836c0 144
fe2a2a4f 145 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
0c9836c0
ZJS
146
147 return grammar
148
149ERROR = False
150def error(fmt, *args, **kwargs):
151 global ERROR
152 ERROR = True
153 print(fmt.format(*args, **kwargs))
154
155def 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
160def parse(fname):
161 grammar = hwdb_grammar()
162 try:
aeceb390
MP
163 with open(fname, 'r', encoding='UTF-8') as f:
164 parsed = grammar.parseFile(f)
0c9836c0
ZJS
165 except ParseBaseException as e:
166 error('Cannot parse {}: {}', fname, e)
167 return []
168 return [convert_properties(g) for g in parsed.GROUPS]
169
170def 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
179def 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
494d16aa
ZJS
184def 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
0c9836c0
ZJS
198def check_one_keycode(prop, value):
199 if value != '!' and ecodes is not None:
200 key = 'KEY_' + value.upper()
201 if key not in ecodes:
c6dce245
MS
202 key = value.upper()
203 if key not in ecodes:
204 error('Keycode {} unknown', key)
0c9836c0
ZJS
205
206def 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)
494d16aa
ZJS
224 elif parsed.NAME == 'ACCEL_MOUNT_MATRIX':
225 check_one_mount_matrix(prop, parsed.VALUE)
0c9836c0
ZJS
226 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
227 check_one_keycode(prop, parsed.VALUE)
228
229def 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),
8d9d1e3a 234 sum(len(props) for matches, props in groups)))
0c9836c0
ZJS
235
236if __name__ == '__main__':
c3f6a561
ZJS
237 args = sys.argv[1:] or glob.glob(os.path.dirname(sys.argv[0]) + '/[67]0-*.hwdb')
238
239 for fname in args:
0c9836c0
ZJS
240 groups = parse(fname)
241 print_summary(fname, groups)
242 check_match_uniqueness(groups)
243 check_properties(groups)
244
245 sys.exit(ERROR)