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