]> git.ipfire.org Git - thirdparty/systemd.git/blob - hwdb/parse_hwdb.py
Merge pull request #6214 from keszybz/resolved-packet-size
[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 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
69 'joystick': ('i8042', 'rmi', 'bluetooth', 'usb'),
70 'keyboard': ('name', ),
71 'sensor': ('modalias', ),
72 }
73
74 @lru_cache()
75 def hwdb_grammar():
76 ParserElement.setDefaultWhitespaceChars('')
77
78 prefix = Or(category + ':' + Or(conn) + ':'
79 for category, conn in TYPES.items())
80 matchline = Combine(prefix + Word(printables + ' ' + '®')) + EOL
81 propertyline = (White(' ', exact=1).suppress() +
82 Combine(UDEV_TAG - '=' - Word(alphanums + '_=:@*.!-;, "') - Optional(pythonStyleComment)) +
83 EOL)
84 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
85
86 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
87 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
88 (EMPTYLINE ^ stringEnd()).suppress())
89 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
90
91 grammar = OneOrMore(group('GROUPS*') ^ commentgroup) + stringEnd()
92
93 return grammar
94
95 @lru_cache()
96 def property_grammar():
97 ParserElement.setDefaultWhitespaceChars(' ')
98
99 dpi_setting = (Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ'))('SETTINGS*')
100 mount_matrix_row = SIGNED_REAL + ',' + SIGNED_REAL + ',' + SIGNED_REAL
101 mount_matrix = (mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX')
102
103 props = (('MOUSE_DPI', Group(OneOrMore(dpi_setting))),
104 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
105 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
106 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
107 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
108 ('ID_INPUT_TRACKBALL', Literal('1')),
109 ('MOUSE_WHEEL_TILT_HORIZONTAL', Literal('1')),
110 ('MOUSE_WHEEL_TILT_VERTICAL', Literal('1')),
111 ('POINTINGSTICK_SENSITIVITY', INTEGER),
112 ('POINTINGSTICK_CONST_ACCEL', REAL),
113 ('ID_INPUT_JOYSTICK_INTEGRATION', Or(('internal', 'external'))),
114 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
115 ('XKB_FIXED_LAYOUT', STRING),
116 ('XKB_FIXED_VARIANT', STRING),
117 ('KEYBOARD_LED_NUMLOCK', Literal('0')),
118 ('KEYBOARD_LED_CAPSLOCK', Literal('0')),
119 ('ACCEL_MOUNT_MATRIX', mount_matrix),
120 )
121 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
122 for name, val in props]
123 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
124 - Suppress('=') -
125 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
126 ]
127 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
128 - Suppress('=') -
129 Word(nums + ':')('VALUE')
130 ]
131
132 grammar = Or(fixed_props + kbd_props + abs_props) + EOL
133
134 return grammar
135
136 ERROR = False
137 def error(fmt, *args, **kwargs):
138 global ERROR
139 ERROR = True
140 print(fmt.format(*args, **kwargs))
141
142 def convert_properties(group):
143 matches = [m[0] for m in group.MATCHES]
144 props = [p[0] for p in group.PROPERTIES]
145 return matches, props
146
147 def parse(fname):
148 grammar = hwdb_grammar()
149 try:
150 with open(fname, 'r', encoding='UTF-8') as f:
151 parsed = grammar.parseFile(f)
152 except ParseBaseException as e:
153 error('Cannot parse {}: {}', fname, e)
154 return []
155 return [convert_properties(g) for g in parsed.GROUPS]
156
157 def check_match_uniqueness(groups):
158 matches = sum((group[0] for group in groups), [])
159 matches.sort()
160 prev = None
161 for match in matches:
162 if match == prev:
163 error('Match {!r} is duplicated', match)
164 prev = match
165
166 def check_one_default(prop, settings):
167 defaults = [s for s in settings if s.DEFAULT]
168 if len(defaults) > 1:
169 error('More than one star entry: {!r}', prop)
170
171 def check_one_keycode(prop, value):
172 if value != '!' and ecodes is not None:
173 key = 'KEY_' + value.upper()
174 if key not in ecodes:
175 key = value.upper()
176 if key not in ecodes:
177 error('Keycode {} unknown', key)
178
179 def check_properties(groups):
180 grammar = property_grammar()
181 for matches, props in groups:
182 prop_names = set()
183 for prop in props:
184 # print('--', prop)
185 prop = prop.partition('#')[0].rstrip()
186 try:
187 parsed = grammar.parseString(prop)
188 except ParseBaseException as e:
189 error('Failed to parse: {!r}', prop)
190 continue
191 # print('{!r}'.format(parsed))
192 if parsed.NAME in prop_names:
193 error('Property {} is duplicated', parsed.NAME)
194 prop_names.add(parsed.NAME)
195 if parsed.NAME == 'MOUSE_DPI':
196 check_one_default(prop, parsed.VALUE.SETTINGS)
197 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
198 check_one_keycode(prop, parsed.VALUE)
199
200 def print_summary(fname, groups):
201 print('{}: {} match groups, {} matches, {} properties'
202 .format(fname,
203 len(groups),
204 sum(len(matches) for matches, props in groups),
205 sum(len(props) for matches, props in groups)))
206
207 if __name__ == '__main__':
208 args = sys.argv[1:] or glob.glob(os.path.dirname(sys.argv[0]) + '/[67]0-*.hwdb')
209
210 for fname in args:
211 groups = parse(fname)
212 print_summary(fname, groups)
213 check_match_uniqueness(groups)
214 check_properties(groups)
215
216 sys.exit(ERROR)