summaryrefslogtreecommitdiff
path: root/scripts/lib/abi/abi_regex.py
blob: 8a57846cbc69c33bb04798be1d779b16e2c0205e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#!/usr/bin/env python3
# xxpylint: disable=R0903
# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
# SPDX-License-Identifier: GPL-2.0

"""
Convert ABI what into regular expressions
"""

import re
import sys

from pprint import pformat

from abi_parser import AbiParser
from helpers import AbiDebug

class AbiRegex(AbiParser):
    """Extends AbiParser to search ABI nodes with regular expressions"""

    # Escape only ASCII visible characters
    escape_symbols = r"([\x21-\x29\x2b-\x2d\x3a-\x40\x5c\x60\x7b-\x7e])"
    leave_others = "others"

    # Tuples with regular expressions to be compiled and replacement data
    re_whats = [
        # Drop escape characters that might exist
        (re.compile("\\\\"), ""),

        # Temporarily escape dot characters
        (re.compile(r"\."),  "\xf6"),

        # Temporarily change [0-9]+ type of patterns
        (re.compile(r"\[0\-9\]\+"),  "\xff"),

        # Temporarily change [\d+-\d+] type of patterns
        (re.compile(r"\[0\-\d+\]"),  "\xff"),
        (re.compile(r"\[0:\d+\]"),  "\xff"),
        (re.compile(r"\[(\d+)\]"),  "\xf4\\\\d+\xf5"),

        # Temporarily change [0-9] type of patterns
        (re.compile(r"\[(\d)\-(\d)\]"),  "\xf4\1-\2\xf5"),

        # Handle multiple option patterns
        (re.compile(r"[\{\<\[]([\w_]+)(?:[,|]+([\w_]+)){1,}[\}\>\]]"), r"(\1|\2)"),

        # Handle wildcards
        (re.compile(r"([^\/])\*"), "\\1\\\\w\xf7"),
        (re.compile(r"/\*/"), "/.*/"),
        (re.compile(r"/\xf6\xf6\xf6"), "/.*"),
        (re.compile(r"\<[^\>]+\>"), "\\\\w\xf7"),
        (re.compile(r"\{[^\}]+\}"), "\\\\w\xf7"),
        (re.compile(r"\[[^\]]+\]"), "\\\\w\xf7"),

        (re.compile(r"XX+"), "\\\\w\xf7"),
        (re.compile(r"([^A-Z])[XYZ]([^A-Z])"), "\\1\\\\w\xf7\\2"),
        (re.compile(r"([^A-Z])[XYZ]$"), "\\1\\\\w\xf7"),
        (re.compile(r"_[AB]_"), "_\\\\w\xf7_"),

        # Recover [0-9] type of patterns
        (re.compile(r"\xf4"), "["),
        (re.compile(r"\xf5"),  "]"),

        # Remove duplicated spaces
        (re.compile(r"\s+"), r" "),

        # Special case: drop comparison as in:
        # What: foo = <something>
        # (this happens on a few IIO definitions)
        (re.compile(r"\s*\=.*$"), ""),

        # Escape all other symbols
        (re.compile(escape_symbols), r"\\\1"),
        (re.compile(r"\\\\"), r"\\"),
        (re.compile(r"\\([\[\]\(\)\|])"), r"\1"),
        (re.compile(r"(\d+)\\(-\d+)"), r"\1\2"),

        (re.compile(r"\xff"), r"\\d+"),

        # Special case: IIO ABI which a parenthesis.
        (re.compile(r"sqrt(.*)"), r"sqrt(.*)"),

        # Simplify regexes with multiple .*
        (re.compile(r"(?:\.\*){2,}"),  ""),

        # Recover dot characters
        (re.compile(r"\xf6"), "\\."),
        # Recover plus characters
        (re.compile(r"\xf7"), "+"),
    ]
    re_has_num = re.compile(r"\\d")

    # Symbol name after escape_chars that are considered a devnode basename
    re_symbol_name =  re.compile(r"(\w|\\[\.\-\:])+$")

    # List of popular group names to be skipped to minimize regex group size
    # Use AbiDebug.SUBGROUP_SIZE to detect those
    skip_names = set(["devices", "hwmon"])

    def regex_append(self, what, new):
        """
        Get a search group for a subset of regular expressions.

        As ABI may have thousands of symbols, using a for to search all
        regular expressions is at least O(n^2). When there are wildcards,
        the complexity increases substantially, eventually becoming exponential.

        To avoid spending too much time on them, use a logic to split
        them into groups. The smaller the group, the better, as it would
        mean that searches will be confined to a small number of regular
        expressions.

        The conversion to a regex subset is tricky, as we need something
        that can be easily obtained from the sysfs symbol and from the
        regular expression. So, we need to discard nodes that have
        wildcards.

        If it can't obtain a subgroup, place the regular expression inside
        a special group (self.leave_others).
        """

        search_group = None

        for search_group in reversed(new.split("/")):
            if not search_group or search_group in self.skip_names:
                continue
            if self.re_symbol_name.match(search_group):
                break

        if not search_group:
            search_group = self.leave_others

        if self.debug & AbiDebug.SUBGROUP_MAP:
            self.log.debug("%s: mapped as %s", what, search_group)

        try:
            if search_group not in self.regex_group:
                self.regex_group[search_group] = []

            self.regex_group[search_group].append(re.compile(new))
            if self.search_string:
                if what.find(self.search_string) >= 0:
                    print(f"What: {what}")
        except re.PatternError:
            self.log.warning("Ignoring '%s' as it produced an invalid regex:\n"
                             "           '%s'", what, new)

    def get_regexes(self, what):
        """
        Given an ABI devnode, return a list of all regular expressions that
        may match it, based on the sub-groups created by regex_append()
        """

        re_list = []

        patches = what.split("/")
        patches.reverse()
        patches.append(self.leave_others)

        for search_group in patches:
            if search_group in self.regex_group:
                re_list += self.regex_group[search_group]

        return re_list

    def __init__(self, *args, **kwargs):
        """
        Override init method to get verbose argument
        """

        self.regex_group = None
        self.search_string = None
        self.re_string = None

        if "search_string" in kwargs:
            self.search_string = kwargs.get("search_string")
            del kwargs["search_string"]

            if self.search_string:

                try:
                    self.re_string = re.compile(self.search_string)
                except re.PatternError as e:
                    msg = f"{self.search_string} is not a valid regular expression"
                    raise ValueError(msg) from e

        super().__init__(*args, **kwargs)

    def parse_abi(self, *args, **kwargs):

        super().parse_abi(*args, **kwargs)

        self.regex_group = {}

        print("Converting ABI What fields into regexes...", file=sys.stderr)

        for t in sorted(self.data.items(), key=lambda x: x[0]):
            v = t[1]
            if v.get("type") == "File":
                continue

            v["regex"] = []

            for what in v.get("what", []):
                if not what.startswith("/sys"):
                    continue

                new = what
                for r, s in self.re_whats:
                    try:
                        new = r.sub(s, new)
                    except re.PatternError as e:
                        # Help debugging troubles with new regexes
                        raise re.PatternError(f"{e}\nwhile re.sub('{r.pattern}', {s}, str)") from e

                v["regex"].append(new)

                if self.debug & AbiDebug.REGEX:
                    self.log.debug("%-90s <== %s", new, what)

                # Store regex into a subgroup to speedup searches
                self.regex_append(what, new)

        if self.debug & AbiDebug.SUBGROUP_DICT:
            self.log.debug("%s", pformat(self.regex_group))

        if self.debug & AbiDebug.SUBGROUP_SIZE:
            biggestd_keys = sorted(self.regex_group.keys(),
                                   key= lambda k: len(self.regex_group[k]),
                                   reverse=True)

            print("Top regex subgroups:", file=sys.stderr)
            for k in biggestd_keys[:10]:
                print(f"{k} has {len(self.regex_group[k])} elements", file=sys.stderr)