1# Copyright (C) 2023 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14import dataclasses
15import re
16from pathlib import Path
17
18import util
19
20
21@dataclasses.dataclass(frozen=True)
22class Defaults:
23    allowed: bool
24    recurse: bool
25
26
27@dataclasses.dataclass(frozen=True)
28class GoAllowlistManipulator:
29    """
30    This is a bare-bones regex-based utility for manipulating `allowlists.go`
31    It expects that file to be propertly formatted.
32    """
33
34    lines: list[str]
35    """the source code lines of `allowlists.go`"""
36    _lists: dict[str, "GoList"] = dataclasses.field(default_factory=lambda: {})
37    """
38    All GoList instances retrieved via `locate()` indexed by their list names.
39    This dict is kept around such that any list when modified can adjust the
40    line numbers of all other lists appropriately
41    """
42    dir_defaults: dict[Path, Defaults] = dataclasses.field(default_factory=lambda: {})
43    """
44    the mappings from directories to whether they are bp2build allowed or not
45    """
46
47    def __post_init__(self):
48        #  reads the Bp2BuildConfig to materialize `dir_defaults`
49        start = re.compile(r"\w+\s*=\s*Bp2BuildConfig\{")
50        entry = re.compile(
51            r'"(?P<path>[^"]+)"\s*:\s*Bp2BuildDefault(?P<allowed>True|False)(?P<recurse>Recursively)?'
52        )
53        begun = False
54        left_pad: str = ""
55        for line in self.lines:
56            line = line.strip()
57            if not begun:
58                begun = bool(start.match(line))
59            elif line == "}":
60                break
61            else:
62                real_item = line.strip()
63                m = entry.search(real_item)
64                if m:
65                    key = Path(m.group("path"))
66                    value = Defaults(
67                        m.group("allowed") == "True", bool(m.group("recurse"))
68                    )
69                    self.dir_defaults[key] = value
70                    if left_pad == "":
71                        left_pad = line[: line.index(real_item)]
72
73        else:
74            raise RuntimeError("Bp2BuildConfig missing")
75
76    def locate(self, listname: str) -> "GoList":
77        if listname in self._lists:
78            return self._lists[listname]
79        start = re.compile(r"^\s*{l}\s=\s*\[]string\{{\s*$".format(l=listname))
80        begin: int = -1
81        left_pad: str = ""
82        for i, line in enumerate(self.lines):
83            if begin == -1:
84                if start.match(line):
85                    begin = i + 1
86            else:
87                if line.strip() == "}":
88                    go_list = GoList(self, begin, end=i, left_pad=left_pad)
89                    self._lists[listname] = go_list
90                    return go_list
91                elif left_pad == "":
92                    real_item = line.lstrip()
93                    left_pad = line[: line.index(real_item)]
94        raise RuntimeError(f"{listname} not found")
95
96    def is_dir_allowed(self, d: Path) -> bool:
97        if d.is_absolute():
98            d = d.relative_to(util.get_top_dir())
99        if d in self.dir_defaults:
100            return self.dir_defaults[d].allowed
101        while d.parent != d:
102            if d.parent in self.dir_defaults:
103                v = self.dir_defaults[d.parent]
104                return v.allowed and v.recurse
105            d = d.parent
106        return False
107
108    @property
109    def lists(self):
110        return self._lists
111
112
113@dataclasses.dataclass
114class GoList:
115    parent: GoAllowlistManipulator
116    begin: int
117    end: int
118    left_pad: str = ""
119
120    def __contains__(self, item: str) -> bool:
121        quoted = f'"{item}"'
122        for i in range(self.begin, self.end):
123            if quoted in self.parent.lines[i]:
124                return True
125        return False
126
127    def prepend(self, items: list[str]):
128        clones = [f'{self.left_pad}"{i}",\n' for i in items]
129        self.parent.lines[self.begin : self.begin] = clones
130        growth = len(items)
131        self.end += growth
132        for go_list in self.parent.lists.values():
133            #  adjust line numbers for all subsequent lists
134            #  in the source code
135            if go_list.begin > self.begin:
136                go_list.begin += growth
137                go_list.end += growth
138