1# This file helps to parse CSV eFuse tables
2#
3# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
4#
5# SPDX-License-Identifier: GPL-2.0-or-later
6
7import os
8import re
9import sys
10
11
12class CSVFuseTable(list):
13    @classmethod
14    def from_csv(cls, csv_contents):
15        res = CSVFuseTable()
16        lines = csv_contents.splitlines()
17
18        def expand_vars(f):
19            f = os.path.expandvars(f)
20            m = re.match(r"(?<!\\)\$([A-Za-z_]\w*)", f)
21            if m:
22                raise InputError(f"unknown variable '{m.group(1)}'")
23            return f
24
25        for line_no, line in enumerate(lines):
26            line = expand_vars(line).strip()
27            if line.startswith("#") or len(line) == 0:
28                continue
29            try:
30                res.append(FuseDefinition.from_csv(line))
31            except InputError as err:
32                raise InputError(f"Error at line {line_no + 1}: {err}")
33            except Exception:
34                sys.stderr.write(f"Unexpected error parsing line {line_no + 1}: {line}")
35                raise
36
37        # fix up missing bit_start
38        last_efuse_block = None
39        for i in res:
40            if last_efuse_block != i.efuse_block:
41                last_end = 0
42            if i.bit_start is None:
43                i.bit_start = last_end
44            last_end = i.bit_start + i.bit_count
45            last_efuse_block = i.efuse_block
46
47        res.verify_duplicate_name()
48
49        # fix up missing field_name
50        last_field = None
51        for i in res:
52            if i.field_name == "":
53                if last_field is None:
54                    raise InputError(
55                        f"Error at line {line_no + 1}: {i} missing field name"
56                    )
57                elif last_field is not None:
58                    i.field_name = last_field.field_name
59            last_field = i
60
61        # fill group
62        names = [p.field_name for p in res]
63        duplicates = set(n for n in names if names.count(n) > 1)
64        for dname in duplicates:
65            i_count = 0
66            for p in res:
67                if p.field_name != dname:
68                    continue
69                if len(duplicates.intersection([p.field_name])) != 0:
70                    p.field_name = f"{p.field_name}_{i_count}"
71                    if p.alt_names:
72                        p.alt_names = f"{p.alt_names}_{i_count}"
73                    i_count += 1
74                else:
75                    i_count = 0
76
77        for p in res:
78            p.field_name = p.field_name.replace(".", "_")
79            if p.alt_names:
80                p.alt_names = p.alt_names.replace(".", "_")
81        res.verify_duplicate_name()
82        return res
83
84    def verify_duplicate_name(self):
85        # check on duplicate name
86        names = [p.field_name for p in self]
87        names += [name.replace(".", "_") for name in names if "." in name]
88        duplicates = set(n for n in names if names.count(n) > 1)
89
90        # print sorted duplicate partitions by name
91        if len(duplicates) != 0:
92            fl_error = False
93            for p in self:
94                field_name = p.field_name + p.group
95                if field_name != "" and len(duplicates.intersection([field_name])) != 0:
96                    fl_error = True
97                    print(
98                        f"Field at {p.field_name}, {p.efuse_block}, "
99                        f"{p.bit_start}, {p.bit_count} have duplicate field_name"
100                    )
101            if fl_error is True:
102                raise InputError("Field names must be unique")
103
104    def check_struct_field_name(self):
105        # check that structured fields have a root field
106        for p in self:
107            if "." in p.field_name:
108                name = ""
109                for sub in p.field_name.split(".")[:-1]:
110                    name = sub if name == "" else name + "." + sub
111                    missed_name = True
112                    for d in self:
113                        if (
114                            p is not d
115                            and p.efuse_block == d.efuse_block
116                            and name == d.field_name
117                        ):
118                            missed_name = False
119                    if missed_name:
120                        raise InputError(f"{name} is not found")
121
122    def verify(self, type_table=None):
123        def check(p, n):
124            left = n.bit_start
125            right = n.bit_start + n.bit_count - 1
126            start = p.bit_start
127            end = p.bit_start + p.bit_count - 1
128            if left <= start <= right:
129                if left <= end <= right:
130                    return "included in"  # [n  [p...p]  n]
131                return "intersected with"  # [n  [p..n]..p]
132            if left <= end <= right:
133                return "intersected with"  # [p..[n..p] n]
134            if start <= left and right <= end:
135                return "wraps"  # [p  [n...n]  p]
136            return "ok"  # [p] [n]  or  [n] [p]
137
138        def print_error(p, n, state):
139            raise InputError(
140                f"Field at {p.field_name}, {p.efuse_block}, {p.bit_start}, {p.bit_count}  {state}  {n.field_name}, {n.efuse_block}, {n.bit_start}, {n.bit_count}"
141            )
142
143        for p in self:
144            p.verify(type_table)
145
146        self.verify_duplicate_name()
147        if type_table != "custom_table":
148            # check will be done for common and custom tables together
149            self.check_struct_field_name()
150
151        # check for overlaps
152        for p in self:
153            for n in self:
154                if p is not n and p.efuse_block == n.efuse_block:
155                    state = check(p, n)
156                    if state != "ok":
157                        if "." in p.field_name:
158                            name = ""
159                            for sub in p.field_name.split("."):
160                                name = sub if name == "" else name + "." + sub
161                                for d in self:
162                                    if (
163                                        p is not d
164                                        and p.efuse_block == d.efuse_block
165                                        and name == d.field_name
166                                    ):
167                                        state = check(p, d)
168                                        if state == "included in":
169                                            break
170                                        elif state != "intersected with":
171                                            state = "out of range"
172                                        print_error(p, d, state)
173                            continue
174                        elif "." in n.field_name:
175                            continue
176                        print_error(p, n, state)
177
178
179class FuseDefinition(object):
180    def __init__(self):
181        self.field_name = ""
182        self.group = ""
183        self.efuse_block = ""
184        self.bit_start = None
185        self.bit_count = None
186        self.define = None
187        self.comment = ""
188        self.alt_names = ""
189        self.MAX_BITS_OF_BLOCK = 256
190
191    @classmethod
192    def from_csv(cls, line):
193        """Parse a line from the CSV"""
194        line_w_defaults = line + ",,,,"
195        fields = [f.strip() for f in line_w_defaults.split(",")]
196
197        res = FuseDefinition()
198        res.field_name = fields[0]
199        res.efuse_block = res.parse_block(fields[1])
200        res.bit_start = res.parse_num(fields[2])
201        res.bit_count = res.parse_bit_count(fields[3])
202        if res.bit_count is None or res.bit_count == 0:
203            raise InputError("Field bit_count can't be empty")
204        res.comment = fields[4].rstrip("\\").rstrip()
205        res.comment += f" ({res.bit_start}-{res.bit_start + res.bit_count - 1})"
206        res.alt_names = res.get_alt_names(res.comment)
207        return res
208
209    def parse_num(self, strval):
210        if strval == "":
211            return None
212        return self.parse_int(strval)
213
214    def parse_bit_count(self, strval):
215        if strval == "MAX_BLK_LEN":
216            self.define = strval
217            return self.MAX_BITS_OF_BLOCK
218        else:
219            return self.parse_num(strval)
220
221    def parse_int(self, v):
222        try:
223            return int(v, 0)
224        except ValueError:
225            raise InputError(f"Invalid field value {v}")
226
227    def parse_block(self, strval):
228        if strval == "":
229            raise InputError("Field 'efuse_block' can't be left empty.")
230        return self.parse_int(strval.lstrip("EFUSE_BLK"))
231
232    def verify(self, type_table):
233        if self.efuse_block is None:
234            raise ValidationError(self, "efuse_block field is not set")
235        if self.bit_count is None:
236            raise ValidationError(self, "bit_count field is not set")
237        max_bits = self.MAX_BITS_OF_BLOCK
238        if self.bit_start + self.bit_count > max_bits:
239            raise ValidationError(
240                self,
241                f"The field is outside the boundaries(max_bits = {max_bits}) of the {self.efuse_block} block",
242            )
243
244    def get_bit_count(self, check_define=True):
245        if check_define is True and self.define is not None:
246            return self.define
247        else:
248            return self.bit_count
249
250    def get_alt_names(self, comment):
251        result = re.search(r"^\[(.*?)\]", comment)
252        if result:
253            return result.group(1)
254        return ""
255
256
257class InputError(RuntimeError):
258    def __init__(self, e):
259        super(InputError, self).__init__(e)
260
261
262class ValidationError(InputError):
263    def __init__(self, p, message):
264        super(ValidationError, self).__init__(
265            f"Entry {p.field_name} invalid: {message}"
266        )
267