1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15
16import re
17
18from buildbot.util import ComparableMixin
19from buildbot.util import NotABranch
20
21
22def is_re_pattern(obj):
23    # re.Pattern only exists in Python 3.7
24    return hasattr(obj, 'search') and hasattr(obj, 'match')
25
26
27def extract_filter_values(values, filter_name):
28    if not isinstance(values, (list, str)):
29        raise ValueError("Values of filter {} must be list of strings or a string".format(
30            filter_name))
31    if isinstance(values, str):
32        values = [values]
33    else:
34        for value in values:
35            if not isinstance(value, str):
36                raise ValueError("Value of filter {} must be string".format(filter_name))
37    return values
38
39
40def extract_filter_values_branch(values, filter_name):
41    if not isinstance(values, (list, str, type(None))):
42        raise ValueError("Values of filter {} must be list of strings, a string or None".format(
43            filter_name))
44    if isinstance(values, (str, type(None))):
45        values = [values]
46    else:
47        for value in values:
48            if not isinstance(value, (str, type(None))):
49                raise ValueError("Value of filter {} must be string or None".format(filter_name))
50    return values
51
52
53def extract_filter_values_regex(values, filter_name):
54    if not isinstance(values, (list, str)) and not is_re_pattern(values):
55        raise ValueError("Values of filter {} must be list of strings, a string or regex".format(
56            filter_name))
57    if isinstance(values, str) or is_re_pattern(values):
58        values = [values]
59    else:
60        for value in values:
61            if not isinstance(value, str) and not is_re_pattern(value):
62                raise ValueError("Value of filter {} must be string or regex".format(filter_name))
63    return values
64
65
66class _FilterExactMatch:
67    def __init__(self, values):
68        self.values = values
69
70    def is_matched(self, value):
71        return value in self.values
72
73    def describe(self, prop):
74        return '{} in {}'.format(prop, self.values)
75
76
77class _FilterExactMatchInverse:
78    def __init__(self, values):
79        self.values = values
80
81    def is_matched(self, value):
82        return value not in self.values
83
84    def describe(self, prop):
85        return '{} not in {}'.format(prop, self.values)
86
87
88class _FilterRegex:
89    def __init__(self, regexes):
90        self.regexes = [self._compile(regex) for regex in regexes]
91
92    def _compile(self, regex):
93        if is_re_pattern(regex):
94            return regex
95        return re.compile(regex)
96
97    def is_matched(self, value):
98        if value is None:
99            return False
100        for regex in self.regexes:
101            if regex.match(value) is not None:
102                return True
103        return False
104
105    def describe(self, prop):
106        return '{} matches {}'.format(prop, self.regexes)
107
108
109class _FilterRegexInverse:
110    def __init__(self, regexes):
111        self.regexes = [self._compile(regex) for regex in regexes]
112
113    def _compile(self, regex):
114        if is_re_pattern(regex):
115            return regex
116        return re.compile(regex)
117
118    def is_matched(self, value):
119        if value is None:
120            return True
121        for regex in self.regexes:
122            if regex.match(value) is not None:
123                return False
124        return True
125
126    def describe(self, prop):
127        return '{} does not match {}'.format(prop, self.regexes)
128
129
130class SourceStampFilter(ComparableMixin):
131
132    compare_attrs = (
133        'filter_fn',
134        'project_filters',
135        'codebase_filters',
136        'repository_filters',
137        'branch_filters'
138    )
139
140    def __init__(self,
141                 # gets a SourceStamp dictionary, returns boolean
142                 filter_fn=None,
143
144                 project_eq=None, project_not_eq=None, project_re=None, project_not_re=None,
145                 repository_eq=None, repository_not_eq=None,
146                 repository_re=None, repository_not_re=None,
147                 branch_eq=NotABranch, branch_not_eq=NotABranch, branch_re=None, branch_not_re=None,
148                 codebase_eq=None, codebase_not_eq=None, codebase_re=None, codebase_not_re=None):
149
150        self.filter_fn = filter_fn
151        self.project_filters = self.create_filters(project_eq, project_not_eq,
152                                                   project_re, project_not_re, 'project')
153        self.codebase_filters = self.create_filters(codebase_eq, codebase_not_eq,
154                                                    codebase_re, codebase_not_re, 'codebase')
155        self.repository_filters = self.create_filters(repository_eq, repository_not_eq,
156                                                      repository_re, repository_not_re,
157                                                      'repository')
158        self.branch_filters = self.create_branch_filters(branch_eq, branch_not_eq,
159                                                         branch_re, branch_not_re, 'branch')
160
161    def create_branch_filters(self, eq, not_eq, regex, not_regex, filter_name):
162        filters = []
163        if eq is not NotABranch:
164            values = extract_filter_values_branch(eq, filter_name + '_eq')
165            filters.append(_FilterExactMatch(values))
166
167        if not_eq is not NotABranch:
168            values = extract_filter_values_branch(not_eq, filter_name + '_not_eq')
169            filters.append(_FilterExactMatchInverse(values))
170
171        if regex is not None:
172            values = extract_filter_values_regex(regex, filter_name + '_re')
173            filters.append(_FilterRegex(values))
174
175        if not_regex is not None:
176            values = extract_filter_values_regex(not_regex, filter_name + '_re')
177            filters.append(_FilterRegexInverse(values))
178
179        return filters
180
181    def create_filters(self, eq, not_eq, regex, not_regex, filter_name):
182        filters = []
183        if eq is not None:
184            values = extract_filter_values(eq, filter_name + '_eq')
185            filters.append(_FilterExactMatch(values))
186
187        if not_eq is not None:
188            values = extract_filter_values(not_eq, filter_name + '_not_eq')
189            filters.append(_FilterExactMatchInverse(values))
190
191        if regex is not None:
192            values = extract_filter_values_regex(regex, filter_name + '_re')
193            filters.append(_FilterRegex(values))
194
195        if not_regex is not None:
196            values = extract_filter_values_regex(not_regex, filter_name + '_re')
197            filters.append(_FilterRegexInverse(values))
198
199        return filters
200
201    def do_prop_match(self, ss, prop, filters):
202        value = ss.get(prop, '')
203        for filter in filters:
204            if not filter.is_matched(value):
205                return False
206        return True
207
208    def is_matched(self, ss):
209        if self.filter_fn is not None and not self.filter_fn(ss):
210            return False
211        if self.project_filters and not self.do_prop_match(ss, 'project', self.project_filters):
212            return False
213        if self.codebase_filters and not self.do_prop_match(ss, 'codebase', self.codebase_filters):
214            return False
215        if self.repository_filters and \
216                not self.do_prop_match(ss, 'repository', self.repository_filters):
217            return False
218        if self.branch_filters and not self.do_prop_match(ss, 'branch', self.branch_filters):
219            return False
220        return True
221
222    def is_matched_codebase(self, codebase):
223        for filter in self.codebase_filters:
224            if not filter.is_matched(codebase):
225                return False
226        return True
227
228    def _repr_filters(self, filters, prop):
229        return [filter.describe(prop) for filter in filters]
230
231    def __repr__(self):
232        filters = []
233        if self.filter_fn is not None:
234            filters.append('{}()'.format(self.filter_fn.__name__))
235        filters += self._repr_filters(self.project_filters, 'project')
236        filters += self._repr_filters(self.codebase_filters, 'codebase')
237        filters += self._repr_filters(self.repository_filters, 'repository')
238        filters += self._repr_filters(self.branch_filters, 'branch')
239
240        return "<{} on {}>".format(self.__class__.__name__, ' and '.join(filters))
241