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