1# Copyright (C) 2011-2013 Kai Willadsen <kai.willadsen@gmail.com>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 2 of the License, or (at
6# your option) any later version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11# General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16import logging
17import re
18
19log = logging.getLogger(__name__)
20
21
22def try_compile(regex, flags=0):
23    try:
24        compiled = re.compile(regex, flags)
25    except re.error:
26        log.warning(
27            'Error compiling regex {!r} with flags {!r}'.format(regex, flags))
28        compiled = None
29    return compiled
30
31
32class FilterEntry:
33
34    __slots__ = ("label", "active", "filter", "byte_filter", "filter_string")
35
36    REGEX, SHELL = 0, 1
37
38    def __init__(self, label, active, filter, byte_filter, filter_string):
39        self.label = label
40        self.active = active
41        self.filter = filter
42        self.byte_filter = byte_filter
43        self.filter_string = filter_string
44
45    @classmethod
46    def compile_regex(cls, regex, byte_regex=False):
47        if byte_regex and not isinstance(regex, bytes):
48            # TODO: Register a custom error handling function to replace
49            # encoding errors with '.'?
50            regex = regex.encode('utf8', 'replace')
51        return try_compile(regex, re.M)
52
53    @classmethod
54    def compile_shell_pattern(cls, pattern):
55        bits = pattern.split()
56        if not bits:
57            # An empty pattern would match everything, so skip it
58            return None
59        elif len(bits) > 1:
60            regexes = [shell_to_regex(b)[:-1] for b in bits]
61            regex = "(%s)$" % "|".join(regexes)
62        else:
63            regex = shell_to_regex(bits[0])
64        return try_compile(regex)
65
66    @classmethod
67    def new_from_gsetting(cls, elements, filter_type):
68        name, active, filter_string = elements
69        if filter_type == cls.REGEX:
70            str_re = cls.compile_regex(filter_string)
71            bytes_re = cls.compile_regex(filter_string, byte_regex=True)
72        elif filter_type == cls.SHELL:
73            str_re = cls.compile_shell_pattern(filter_string)
74            bytes_re = None
75        else:
76            raise ValueError("Unknown filter type")
77
78        active = active and bool(str_re)
79        return cls(name, active, str_re, bytes_re, filter_string)
80
81    @classmethod
82    def check_filter(cls, filter_string, filter_type):
83        if filter_type == cls.REGEX:
84            compiled = cls.compile_regex(filter_string)
85        elif filter_type == cls.SHELL:
86            compiled = cls.compile_shell_pattern(filter_string)
87        return compiled is not None
88
89    def __copy__(self):
90        new = type(self)(
91            self.label, self.active, None, None, self.filter_string)
92        if self.filter is not None:
93            new.filter = re.compile(self.filter.pattern, self.filter.flags)
94        if self.byte_filter is not None:
95            new.byte_filter = re.compile(
96                self.byte_filter.pattern, self.byte_filter.flags)
97        return new
98
99
100def shell_to_regex(pat):
101    """Translate a shell PATTERN to a regular expression.
102
103    Based on fnmatch.translate().
104    We also handle {a,b,c} where fnmatch does not.
105    """
106
107    i, n = 0, len(pat)
108    res = ''
109    while i < n:
110        c = pat[i]
111        i += 1
112        if c == '\\':
113            try:
114                c = pat[i]
115            except IndexError:
116                pass
117            else:
118                i += 1
119                res += re.escape(c)
120        elif c == '*':
121            res += '.*'
122        elif c == '?':
123            res += '.'
124        elif c == '[':
125            try:
126                j = pat.index(']', i)
127            except ValueError:
128                res += r'\['
129            else:
130                stuff = pat[i:j]
131                i = j + 1
132                if stuff[0] == '!':
133                    stuff = '^%s' % stuff[1:]
134                elif stuff[0] == '^':
135                    stuff = r'\^%s' % stuff[1:]
136                res += '[%s]' % stuff
137        elif c == '{':
138            try:
139                j = pat.index('}', i)
140            except ValueError:
141                res += '\\{'
142            else:
143                stuff = pat[i:j]
144                i = j + 1
145                res += '(%s)' % "|".join(
146                    [shell_to_regex(p)[:-1] for p in stuff.split(",")]
147                )
148        else:
149            res += re.escape(c)
150    return res + "$"
151