xref: /linux/tools/lib/python/abi/abi_regex.py (revision 47646ce123fbd07d7c4d8052831c35f58ffcd074)
1#!/usr/bin/env python3
2# xxpylint: disable=R0903
3# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
4# SPDX-License-Identifier: GPL-2.0
5
6"""
7Convert ABI what into regular expressions
8"""
9
10import re
11import sys
12
13from pprint import pformat
14
15from abi.abi_parser import AbiParser
16from abi.helpers import AbiDebug
17
18class AbiRegex(AbiParser):
19    """
20    Extends AbiParser to search ABI nodes with regular expressions.
21
22    There some optimizations here to allow a quick symbol search:
23    instead of trying to place all symbols altogether an doing linear
24    search which is very time consuming, create a tree with one depth,
25    grouping similar symbols altogether.
26
27    Yet, sometimes a full search will be needed, so we have a special branch
28    on such group tree where other symbols are placed.
29    """
30
31    #: Escape only ASCII visible characters.
32    escape_symbols = r"([\x21-\x29\x2b-\x2d\x3a-\x40\x5c\x60\x7b-\x7e])"
33
34    #: Special group for other nodes.
35    leave_others = "others"
36
37    # Tuples with regular expressions to be compiled and replacement data
38    re_whats = [
39        # Drop escape characters that might exist
40        (re.compile("\\\\"), ""),
41
42        # Temporarily escape dot characters
43        (re.compile(r"\."),  "\xf6"),
44
45        # Temporarily change [0-9]+ type of patterns
46        (re.compile(r"\[0\-9\]\+"),  "\xff"),
47
48        # Temporarily change [\d+-\d+] type of patterns
49        (re.compile(r"\[0\-\d+\]"),  "\xff"),
50        (re.compile(r"\[0:\d+\]"),  "\xff"),
51        (re.compile(r"\[(\d+)\]"),  "\xf4\\\\d+\xf5"),
52
53        # Temporarily change [0-9] type of patterns
54        (re.compile(r"\[(\d)\-(\d)\]"),  "\xf4\1-\2\xf5"),
55
56        # Handle multiple option patterns
57        (re.compile(r"[\{\<\[]([\w_]+)(?:[,|]+([\w_]+)){1,}[\}\>\]]"), r"(\1|\2)"),
58
59        # Handle wildcards
60        (re.compile(r"([^\/])\*"), "\\1\\\\w\xf7"),
61        (re.compile(r"/\*/"), "/.*/"),
62        (re.compile(r"/\xf6\xf6\xf6"), "/.*"),
63        (re.compile(r"\<[^\>]+\>"), "\\\\w\xf7"),
64        (re.compile(r"\{[^\}]+\}"), "\\\\w\xf7"),
65        (re.compile(r"\[[^\]]+\]"), "\\\\w\xf7"),
66
67        (re.compile(r"XX+"), "\\\\w\xf7"),
68        (re.compile(r"(?<![A-Z])[XYZ](?![A-Z])"), "\\\\w\xf7"),
69        (re.compile(r"_[AB]_"), "_\\\\w\xf7_"),
70
71        # Recover [0-9] type of patterns
72        (re.compile(r"\xf4"), "["),
73        (re.compile(r"\xf5"),  "]"),
74
75        # Remove duplicated spaces
76        (re.compile(r"\s+"), r" "),
77
78        # Special case: drop comparison as in:
79        # What: foo = <something>
80        # (this happens on a few IIO definitions)
81        (re.compile(r"\s*\=.*$"), ""),
82
83        # Escape all other symbols
84        (re.compile(escape_symbols), r"\\\1"),
85        (re.compile(r"\\\\"), r"\\"),
86        (re.compile(r"\\([\[\]\(\)\|])"), r"\1"),
87        (re.compile(r"(\d+)\\(-\d+)"), r"\1\2"),
88
89        (re.compile(r"\xff"), r"\\d+"),
90
91        # Special case: IIO ABI which a parenthesis.
92        (re.compile(r"sqrt(.*)"), r"sqrt(.*)"),
93
94        # Simplify regexes with multiple .*
95        (re.compile(r"(?:\.\*){2,}"),  ""),
96
97        # Recover dot characters
98        (re.compile(r"\xf6"), "\\."),
99        # Recover plus characters
100        (re.compile(r"\xf7"), "+"),
101    ]
102
103    #: Regex to check if the symbol name has a number on it.
104    re_has_num = re.compile(r"\\d")
105
106    #: Symbol name after escape_chars that are considered a devnode basename.
107    re_symbol_name =  re.compile(r"(\w|\\[\.\-\:])+$")
108
109    #: List of popular group names to be skipped to minimize regex group size
110    #: Use AbiDebug.SUBGROUP_SIZE to detect those.
111    skip_names = set(["devices", "hwmon"])
112
113    def regex_append(self, what, new):
114        """
115        Get a search group for a subset of regular expressions.
116
117        As ABI may have thousands of symbols, using a for to search all
118        regular expressions is at least O(n^2). When there are wildcards,
119        the complexity increases substantially, eventually becoming exponential.
120
121        To avoid spending too much time on them, use a logic to split
122        them into groups. The smaller the group, the better, as it would
123        mean that searches will be confined to a small number of regular
124        expressions.
125
126        The conversion to a regex subset is tricky, as we need something
127        that can be easily obtained from the sysfs symbol and from the
128        regular expression. So, we need to discard nodes that have
129        wildcards.
130
131        If it can't obtain a subgroup, place the regular expression inside
132        a special group (self.leave_others).
133        """
134
135        search_group = None
136
137        for search_group in reversed(new.split("/")):
138            if not search_group or search_group in self.skip_names:
139                continue
140            if self.re_symbol_name.match(search_group):
141                break
142
143        if not search_group:
144            search_group = self.leave_others
145
146        if self.debug & AbiDebug.SUBGROUP_MAP:
147            self.log.debug("%s: mapped as %s", what, search_group)
148
149        try:
150            if search_group not in self.regex_group:
151                self.regex_group[search_group] = []
152
153            self.regex_group[search_group].append(re.compile(new))
154            if self.search_string:
155                if what.find(self.search_string) >= 0:
156                    print(f"What: {what}")
157        except re.error:
158            self.log.warning("Ignoring '%s' as it produced an invalid regex:\n"
159                             "           '%s'", what, new)
160
161    def get_regexes(self, what):
162        """
163        Given an ABI devnode, return a list of all regular expressions that
164        may match it, based on the sub-groups created by regex_append().
165        """
166
167        re_list = []
168
169        patches = what.split("/")
170        patches.reverse()
171        patches.append(self.leave_others)
172
173        for search_group in patches:
174            if search_group in self.regex_group:
175                re_list += self.regex_group[search_group]
176
177        return re_list
178
179    def __init__(self, *args, **kwargs):
180        """
181        Override init method to get verbose argument
182        """
183
184        self.regex_group = None
185        self.search_string = None
186        self.re_string = None
187
188        if "search_string" in kwargs:
189            self.search_string = kwargs.get("search_string")
190            del kwargs["search_string"]
191
192            if self.search_string:
193
194                try:
195                    self.re_string = re.compile(self.search_string)
196                except re.error as e:
197                    msg = f"{self.search_string} is not a valid regular expression"
198                    raise ValueError(msg) from e
199
200        super().__init__(*args, **kwargs)
201
202    def parse_abi(self, *args, **kwargs):
203
204        super().parse_abi(*args, **kwargs)
205
206        self.regex_group = {}
207
208        print("Converting ABI What fields into regexes...", file=sys.stderr)
209
210        for t in sorted(self.data.items(), key=lambda x: x[0]):
211            v = t[1]
212            if v.get("type") == "File":
213                continue
214
215            v["regex"] = []
216
217            for what in v.get("what", []):
218                if not what.startswith("/sys"):
219                    continue
220
221                new = what
222                for r, s in self.re_whats:
223                    try:
224                        new = r.sub(s, new)
225                    except re.error as e:
226                        # Help debugging troubles with new regexes
227                        raise re.error(f"{e}\nwhile re.sub('{r.pattern}', {s}, str)") from e
228
229                v["regex"].append(new)
230
231                if self.debug & AbiDebug.REGEX:
232                    self.log.debug("%-90s <== %s", new, what)
233
234                # Store regex into a subgroup to speedup searches
235                self.regex_append(what, new)
236
237        if self.debug & AbiDebug.SUBGROUP_DICT:
238            self.log.debug("%s", pformat(self.regex_group))
239
240        if self.debug & AbiDebug.SUBGROUP_SIZE:
241            biggestd_keys = sorted(self.regex_group.keys(),
242                                   key= lambda k: len(self.regex_group[k]),
243                                   reverse=True)
244
245            print("Top regex subgroups:", file=sys.stderr)
246            for k in biggestd_keys[:10]:
247                print(f"{k} has {len(self.regex_group[k])} elements", file=sys.stderr)
248