]> git.ipfire.org Git - thirdparty/systemd.git/blob - hwdb/parse_hwdb.py
hwdb: add MOUSE_WHEEL_TILT_VERTICAL/HORIZONTAL to the mouse db (#5023)
[thirdparty/systemd.git] / hwdb / parse_hwdb.py
1 #!/usr/bin/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 'touchpad': ('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_TRACKBALL', Literal('1')),
108 ('MOUSE_WHEEL_TILT_HORIZONTAL', Literal('1')),
109 ('MOUSE_WHEEL_TILT_VERTICAL', Literal('1')),
110 ('POINTINGSTICK_SENSITIVITY', INTEGER),
111 ('POINTINGSTICK_CONST_ACCEL', REAL),
112 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
113 ('XKB_FIXED_LAYOUT', STRING),
114 ('XKB_FIXED_VARIANT', STRING),
115 ('ACCEL_MOUNT_MATRIX', mount_matrix),
116 )
117 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
118 for name, val in props]
119 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
120 - Suppress('=') -
121 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
122 ]
123 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
124 - Suppress('=') -
125 Word(nums + ':')('VALUE')
126 ]
127
128 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
129
130 return grammar
131
132 ERROR = False
133 def error(fmt, *args, **kwargs):
134 global ERROR
135 ERROR = True
136 print(fmt.format(*args, **kwargs))
137
138 def convert_properties(group):
139 matches = [m[0] for m in group.MATCHES]
140 props = [p[0] for p in group.PROPERTIES]
141 return matches, props
142
143 def parse(fname):
144 grammar = hwdb_grammar()
145 try:
146 with open(fname, 'r', encoding='UTF-8') as f:
147 parsed = grammar.parseFile(f)
148 except ParseBaseException as e:
149 error('Cannot parse {}: {}', fname, e)
150 return []
151 return [convert_properties(g) for g in parsed.GROUPS]
152
153 def check_match_uniqueness(groups):
154 matches = sum((group[0] for group in groups), [])
155 matches.sort()
156 prev = None
157 for match in matches:
158 if match == prev:
159 error('Match {!r} is duplicated', match)
160 prev = match
161
162 def check_one_default(prop, settings):
163 defaults = [s for s in settings if s.DEFAULT]
164 if len(defaults) > 1:
165 error('More than one star entry: {!r}', prop)
166
167 def check_one_keycode(prop, value):
168 if value != '!' and ecodes is not None:
169 key = 'KEY_' + value.upper()
170 if key not in ecodes:
171 error('Keycode {} unknown', key)
172
173 def check_properties(groups):
174 grammar = property_grammar()
175 for matches, props in groups:
176 prop_names = set()
177 for prop in props:
178 # print('--', prop)
179 prop = prop.partition('#')[0].rstrip()
180 try:
181 parsed = grammar.parseString(prop)
182 except ParseBaseException as e:
183 error('Failed to parse: {!r}', prop)
184 continue
185 # print('{!r}'.format(parsed))
186 if parsed.NAME in prop_names:
187 error('Property {} is duplicated', parsed.NAME)
188 prop_names.add(parsed.NAME)
189 if parsed.NAME == 'MOUSE_DPI':
190 check_one_default(prop, parsed.VALUE.SETTINGS)
191 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
192 check_one_keycode(prop, parsed.VALUE)
193
194 def print_summary(fname, groups):
195 print('{}: {} match groups, {} matches, {} properties'
196 .format(fname,
197 len(groups),
198 sum(len(matches) for matches, props in groups),
199 sum(len(props) for matches, props in groups)))
200
201 if __name__ == '__main__':
202 args = sys.argv[1:] or glob.glob(os.path.dirname(sys.argv[0]) + '/[67]0-*.hwdb')
203
204 for fname in args:
205 groups = parse(fname)
206 print_summary(fname, groups)
207 check_match_uniqueness(groups)
208 check_properties(groups)
209
210 sys.exit(ERROR)