1#!/usr/bin/env python3
2
3import re
4import sys
5
6
7def usage():
8    sys.stderr.write("Usage: $0 allowlist-config-in allowlist-header-out\n")
9    sys.stderr.write("    Reads in allowlist config, sorting the contents\n")
10    sys.stderr.write("    and outputs a header file\n")
11    sys.exit(-1)
12
13
14class Error(Exception):
15    pass
16
17
18class DuplicateEntry(Error):
19    def __init__(self, e):
20        super(Error, self).__init__(
21            "Multiple entries with matching netfn/cmd found ({})".format(e)
22        )
23
24
25class ParseError(Error):
26    def __init__(self, d):
27        super(Error, self).__init__("Parse error at: '{}'".format(d))
28
29
30class entry:
31    linere = re.compile(
32        r"(0x[0-9a-f]{2}):(0x[0-9a-f]{2})((:(0x[0-9a-f]{4}))?)\s*((//\s*(.*))?)",  # noqa: E501
33        re.I,
34    )
35
36    def __init__(self, data):
37        # parse data line into values:
38        # type 1, two values: netfn, cmd
39        # type 2, three values: netfn, cmd, channels
40        try:
41            m = self.linere.fullmatch(data).groups()
42        except Exception:
43            raise ParseError(data)
44        self.netfn = int(m[0], 16)
45        self.cmd = int(m[1], 16)
46        if m[4] is not None:
47            self.channels = int(m[4], 16)
48        else:
49            # if no channel was provided, default to previous behavior, which
50            # is allow all interfaces, including the system interface (ch 15)
51            self.channels = 0xFFFF
52        if m[6] is not None:
53            self.comment = "// " + m[7]
54        else:
55            self.comment = "//"
56
57    def __str__(self):
58        return " ".join(
59            [
60                "{",
61                "0x{0.netfn:02x},".format(self),
62                "0x{0.cmd:02x},".format(self),
63                "0x{0.channels:04x}".format(self),
64                "},",
65                "{0.comment}".format(self),
66            ]
67        )
68
69    def __lt__(self, other):
70        if self.netfn == other.netfn:
71            return self.cmd < other.cmd
72        return self.netfn < other.netfn
73
74    def match(self, other):
75        return (self.netfn == other.netfn) and (self.cmd == other.cmd)
76
77
78def parse(config):
79    entries = []
80    with open(config) as f:
81        for line in f:
82            line = line.strip()
83            if len(line) == 0 or line[0] == "#":
84                continue
85            e = entry(line)
86            if any([e.match(item) for item in entries]):
87                d = DuplicateEntry(e)
88                sys.stderr.write("WARNING: {}\n".format(d))
89            else:
90                entries.append(e)
91    entries.sort()
92    return entries
93
94
95def output(entries, hppfile):
96    lines = [
97        "#pragma once",
98        "",
99        "// AUTOGENERATED FILE; DO NOT MODIFY",
100        "",
101        "#include <array>",
102        "#include <tuple>",
103        "",
104        (
105            "using netfncmd_tuple = std::tuple<unsigned char, unsigned char,"
106            " unsigned short>;"
107        ),
108        "",
109        "constexpr const std::array<netfncmd_tuple, {}> allowlist = ".format(
110            len(entries)
111        ),
112        "{{",
113    ]
114    lines.extend(["    {}".format(e) for e in entries])
115    lines.append("}};\n")
116
117    with open(hppfile, "w") as hpp:
118        hpp.write("\n".join(lines))
119
120
121if __name__ == "__main__":
122    if len(sys.argv) != 3:
123        usage()
124    config = sys.argv[1]
125    header = sys.argv[2]
126    entries = parse(config)
127    output(entries, header)
128