]> git.ipfire.org Git - thirdparty/systemd.git/blob - hwdb/parse_hwdb.py
hwdb/parse_hwdb.py: open files with UTF-8 mode
[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 functools
30 import glob
31 import string
32 import sys
33 import os
34
35 try:
36 from pyparsing import (Word, White, Literal, ParserElement, Regex,
37 LineStart, LineEnd,
38 ZeroOrMore, OneOrMore, Combine, Or, Optional, Suppress, Group,
39 nums, alphanums, printables,
40 stringEnd, pythonStyleComment,
41 ParseBaseException)
42 except ImportError:
43 print('pyparsing is not available')
44 sys.exit(77)
45
46 try:
47 from evdev.ecodes import ecodes
48 except ImportError:
49 ecodes = None
50 print('WARNING: evdev is not available')
51
52 try:
53 from functools import lru_cache
54 except ImportError:
55 # don't do caching on old python
56 lru_cache = lambda: (lambda f: f)
57
58 EOL = LineEnd().suppress()
59 EMPTYLINE = LineEnd()
60 COMMENTLINE = pythonStyleComment + EOL
61 INTEGER = Word(nums)
62 REAL = Combine((INTEGER + Optional('.' + Optional(INTEGER))) ^ ('.' + INTEGER))
63 UDEV_TAG = Word(string.ascii_uppercase, alphanums + '_')
64
65 TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'),
66 'evdev': ('name', 'atkbd', 'input'),
67 'touchpad': ('i8042', 'rmi', 'bluetooth', 'usb'),
68 'keyboard': ('name', ),
69 }
70
71 @lru_cache()
72 def hwdb_grammar():
73 ParserElement.setDefaultWhitespaceChars('')
74
75 prefix = Or(category + ':' + Or(conn) + ':'
76 for category, conn in TYPES.items())
77 matchline = Combine(prefix + Word(printables + ' ' + '®')) + EOL
78 propertyline = (White(' ', exact=1).suppress() +
79 Combine(UDEV_TAG - '=' - Word(alphanums + '_=:@*.! ') - Optional(pythonStyleComment)) +
80 EOL)
81 propertycomment = White(' ', exact=1) + pythonStyleComment + EOL
82
83 group = (OneOrMore(matchline('MATCHES*') ^ COMMENTLINE.suppress()) -
84 OneOrMore(propertyline('PROPERTIES*') ^ propertycomment.suppress()) -
85 (EMPTYLINE ^ stringEnd()).suppress() )
86 commentgroup = OneOrMore(COMMENTLINE).suppress() - EMPTYLINE.suppress()
87
88 grammar = OneOrMore(group('GROUPS*') ^ commentgroup) + stringEnd()
89
90 return grammar
91
92 @lru_cache()
93 def property_grammar():
94 ParserElement.setDefaultWhitespaceChars(' ')
95
96 setting = Optional('*')('DEFAULT') + INTEGER('DPI') + Suppress('@') + INTEGER('HZ')
97 props = (('MOUSE_DPI', Group(OneOrMore(setting('SETTINGS*')))),
98 ('MOUSE_WHEEL_CLICK_ANGLE', INTEGER),
99 ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER),
100 ('MOUSE_WHEEL_CLICK_COUNT', INTEGER),
101 ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER),
102 ('ID_INPUT_TRACKBALL', Literal('1')),
103 ('POINTINGSTICK_SENSITIVITY', INTEGER),
104 ('POINTINGSTICK_CONST_ACCEL', REAL),
105 ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))),
106 )
107 fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE')
108 for name, val in props]
109 kbd_props = [Regex(r'KEYBOARD_KEY_[0-9a-f]+')('NAME')
110 - Suppress('=') -
111 ('!' ^ (Optional('!') - Word(alphanums + '_')))('VALUE')
112 ]
113 abs_props = [Regex(r'EVDEV_ABS_[0-9a-f]{2}')('NAME')
114 - Suppress('=') -
115 Word(nums + ':')('VALUE')
116 ]
117
118 grammar = Or(fixed_props + kbd_props + abs_props)
119
120 return grammar
121
122 ERROR = False
123 def error(fmt, *args, **kwargs):
124 global ERROR
125 ERROR = True
126 print(fmt.format(*args, **kwargs))
127
128 def convert_properties(group):
129 matches = [m[0] for m in group.MATCHES]
130 props = [p[0] for p in group.PROPERTIES]
131 return matches, props
132
133 def parse(fname):
134 grammar = hwdb_grammar()
135 try:
136 with open(fname, 'r', encoding='UTF-8') as f:
137 parsed = grammar.parseFile(f)
138 except ParseBaseException as e:
139 error('Cannot parse {}: {}', fname, e)
140 return []
141 return [convert_properties(g) for g in parsed.GROUPS]
142
143 def check_match_uniqueness(groups):
144 matches = sum((group[0] for group in groups), [])
145 matches.sort()
146 prev = None
147 for match in matches:
148 if match == prev:
149 error('Match {!r} is duplicated', match)
150 prev = match
151
152 def check_one_default(prop, settings):
153 defaults = [s for s in settings if s.DEFAULT]
154 if len(defaults) > 1:
155 error('More than one star entry: {!r}', prop)
156
157 def check_one_keycode(prop, value):
158 if value != '!' and ecodes is not None:
159 key = 'KEY_' + value.upper()
160 if key not in ecodes:
161 error('Keycode {} unknown', key)
162
163 def check_properties(groups):
164 grammar = property_grammar()
165 for matches, props in groups:
166 prop_names = set()
167 for prop in props:
168 # print('--', prop)
169 prop = prop.partition('#')[0].rstrip()
170 try:
171 parsed = grammar.parseString(prop)
172 except ParseBaseException as e:
173 error('Failed to parse: {!r}', prop)
174 continue
175 # print('{!r}'.format(parsed))
176 if parsed.NAME in prop_names:
177 error('Property {} is duplicated', parsed.NAME)
178 prop_names.add(parsed.NAME)
179 if parsed.NAME == 'MOUSE_DPI':
180 check_one_default(prop, parsed.VALUE.SETTINGS)
181 elif parsed.NAME.startswith('KEYBOARD_KEY_'):
182 check_one_keycode(prop, parsed.VALUE)
183
184 def print_summary(fname, groups):
185 print('{}: {} match groups, {} matches, {} properties'
186 .format(fname,
187 len(groups),
188 sum(len(matches) for matches, props in groups),
189 sum(len(props) for matches, props in groups),
190 ))
191
192 if __name__ == '__main__':
193 args = sys.argv[1:] or glob.glob(os.path.dirname(sys.argv[0]) + '/[67]0-*.hwdb')
194
195 for fname in args:
196 groups = parse(fname)
197 print_summary(fname, groups)
198 check_match_uniqueness(groups)
199 check_properties(groups)
200
201 sys.exit(ERROR)