1"""
2Custom configparser classes
3"""
4
5import re
6from configparser import *  # pylint: disable=no-name-in-module,wildcard-import,unused-wildcard-import
7
8import salt.utils.stringutils
9
10try:
11    from collections import OrderedDict as _default_dict
12except ImportError:
13    # fallback for setup.py which hasn't yet built _collections
14    _default_dict = dict
15
16
17# pylint: disable=string-substitution-usage-error
18class GitConfigParser(RawConfigParser):
19    """
20    Custom ConfigParser which reads and writes git config files.
21
22    READ A GIT CONFIG FILE INTO THE PARSER OBJECT
23
24    >>> import salt.utils.configparser
25    >>> conf = salt.utils.configparser.GitConfigParser()
26    >>> conf.read('/home/user/.git/config')
27
28    MAKE SOME CHANGES
29
30    >>> # Change user.email
31    >>> conf.set('user', 'email', 'myaddress@mydomain.tld')
32    >>> # Add another refspec to the "origin" remote's "fetch" multivar
33    >>> conf.set_multivar('remote "origin"', 'fetch', '+refs/tags/*:refs/tags/*')
34
35    WRITE THE CONFIG TO A FILEHANDLE
36
37    >>> import salt.utils.files
38    >>> with salt.utils.files.fopen('/home/user/.git/config', 'w') as fh:
39    ...     conf.write(fh)
40    >>>
41    """
42
43    DEFAULTSECT = "DEFAULT"
44    SPACEINDENT = " " * 8
45
46    # pylint: disable=useless-super-delegation
47    def __init__(
48        self,
49        defaults=None,
50        dict_type=_default_dict,
51        allow_no_value=True,
52    ):
53        """
54        Changes default value for allow_no_value from False to True
55        """
56        super().__init__(defaults, dict_type, allow_no_value)
57
58    # pylint: enable=useless-super-delegation
59
60    def _read(self, fp, fpname):
61        """
62        Makes the following changes from the RawConfigParser:
63
64        1. Strip leading tabs from non-section-header lines.
65        2. Treat 8 spaces at the beginning of a line as a tab.
66        3. Treat lines beginning with a tab as options.
67        4. Drops support for continuation lines.
68        5. Multiple values for a given option are stored as a list.
69        6. Keys and values are decoded to the system encoding.
70        """
71        cursect = None  # None, or a dictionary
72        optname = None
73        lineno = 0
74        e = None  # None, or an exception
75        while True:
76            line = salt.utils.stringutils.to_unicode(fp.readline())
77            if not line:
78                break
79            lineno = lineno + 1
80            # comment or blank line?
81            if line.strip() == "" or line[0] in "#;":
82                continue
83            if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR":
84                # no leading whitespace
85                continue
86            # Replace space indentation with a tab. Allows parser to work
87            # properly in cases where someone has edited the git config by hand
88            # and indented using spaces instead of tabs.
89            if line.startswith(self.SPACEINDENT):
90                line = "\t" + line[len(self.SPACEINDENT) :]
91            # is it a section header?
92            mo = self.SECTCRE.match(line)
93            if mo:
94                sectname = mo.group("header")
95                if sectname in self._sections:
96                    cursect = self._sections[sectname]
97                elif sectname == self.DEFAULTSECT:
98                    cursect = self._defaults
99                else:
100                    cursect = self._dict()
101                    self._sections[sectname] = cursect
102                # So sections can't start with a continuation line
103                optname = None
104            # no section header in the file?
105            elif cursect is None:
106                raise MissingSectionHeaderError(  # pylint: disable=undefined-variable
107                    salt.utils.stringutils.to_str(fpname),
108                    lineno,
109                    salt.utils.stringutils.to_str(line),
110                )
111            # an option line?
112            else:
113                mo = self._optcre.match(line.lstrip())
114                if mo:
115                    optname, vi, optval = mo.group("option", "vi", "value")
116                    optname = self.optionxform(optname.rstrip())
117                    if optval is None:
118                        optval = ""
119                    if optval:
120                        if vi in ("=", ":") and ";" in optval:
121                            # ';' is a comment delimiter only if it follows
122                            # a spacing character
123                            pos = optval.find(";")
124                            if pos != -1 and optval[pos - 1].isspace():
125                                optval = optval[:pos]
126                        optval = optval.strip()
127                        # Empty strings should be considered as blank strings
128                        if optval in ('""', "''"):
129                            optval = ""
130                    self._add_option(cursect, optname, optval)
131                else:
132                    # a non-fatal parsing error occurred.  set up the
133                    # exception but keep going. the exception will be
134                    # raised at the end of the file and will contain a
135                    # list of all bogus lines
136                    if not e:
137                        e = ParsingError(fpname)  # pylint: disable=undefined-variable
138                    e.append(lineno, repr(line))
139        # if any parsing errors occurred, raise an exception
140        if e:
141            raise e  # pylint: disable=raising-bad-type
142
143    def _string_check(self, value, allow_list=False):
144        """
145        Based on the string-checking code from the SafeConfigParser's set()
146        function, this enforces string values for config options.
147        """
148        if self._optcre is self.OPTCRE or value:
149            is_list = isinstance(value, list)
150            if is_list and not allow_list:
151                raise TypeError(
152                    "option value cannot be a list unless allow_list is True"
153                )
154            elif not is_list:
155                value = [value]
156            if not all(isinstance(x, str) for x in value):
157                raise TypeError("option values must be strings")
158
159    def get(self, section, option, as_list=False):  # pylint: disable=arguments-differ
160        """
161        Adds an optional "as_list" argument to ensure a list is returned. This
162        is helpful when iterating over an option which may or may not be a
163        multivar.
164        """
165        ret = super().get(section, option)
166        if as_list and not isinstance(ret, list):
167            ret = [ret]
168        return ret
169
170    def set(self, section, option, value=""):
171        """
172        This is overridden from the RawConfigParser merely to change the
173        default value for the 'value' argument.
174        """
175        self._string_check(value)
176        super().set(section, option, value)
177
178    def _add_option(self, sectdict, key, value):
179        if isinstance(value, list):
180            sectdict[key] = value
181        elif isinstance(value, str):
182            try:
183                sectdict[key].append(value)
184            except KeyError:
185                # Key not present, set it
186                sectdict[key] = value
187            except AttributeError:
188                # Key is present but the value is not a list. Make it into a list
189                # and then append to it.
190                sectdict[key] = [sectdict[key]]
191                sectdict[key].append(value)
192        else:
193            raise TypeError(
194                "Expected str or list for option value, got %s" % type(value).__name__
195            )
196
197    def set_multivar(self, section, option, value=""):
198        """
199        This function is unique to the GitConfigParser. It will add another
200        value for the option if it already exists, converting the option's
201        value to a list if applicable.
202
203        If "value" is a list, then any existing values for the specified
204        section and option will be replaced with the list being passed.
205        """
206        self._string_check(value, allow_list=True)
207        if not section or section == self.DEFAULTSECT:
208            sectdict = self._defaults
209        else:
210            try:
211                sectdict = self._sections[section]
212            except KeyError:
213                raise NoSectionError(  # pylint: disable=undefined-variable
214                    salt.utils.stringutils.to_str(section)
215                )
216        key = self.optionxform(option)
217        self._add_option(sectdict, key, value)
218
219    def remove_option_regexp(self, section, option, expr):
220        """
221        Remove an option with a value matching the expression. Works on single
222        values and multivars.
223        """
224        if not section or section == self.DEFAULTSECT:
225            sectdict = self._defaults
226        else:
227            try:
228                sectdict = self._sections[section]
229            except KeyError:
230                raise NoSectionError(  # pylint: disable=undefined-variable
231                    salt.utils.stringutils.to_str(section)
232                )
233        option = self.optionxform(option)
234        if option not in sectdict:
235            return False
236        regexp = re.compile(expr)
237        if isinstance(sectdict[option], list):
238            new_list = [x for x in sectdict[option] if not regexp.search(x)]
239            # Revert back to a list if we removed all but one item
240            if len(new_list) == 1:
241                new_list = new_list[0]
242            existed = new_list != sectdict[option]
243            if existed:
244                del sectdict[option]
245                sectdict[option] = new_list
246            del new_list
247        else:
248            existed = bool(regexp.search(sectdict[option]))
249            if existed:
250                del sectdict[option]
251        return existed
252
253    def write(self, fp_):  # pylint: disable=arguments-differ
254        """
255        Makes the following changes from the RawConfigParser:
256
257        1. Prepends options with a tab character.
258        2. Does not write a blank line between sections.
259        3. When an option's value is a list, a line for each option is written.
260           This allows us to support multivars like a remote's "fetch" option.
261        4. Drops support for continuation lines.
262        """
263        convert = (
264            salt.utils.stringutils.to_bytes
265            if "b" in fp_.mode
266            else salt.utils.stringutils.to_str
267        )
268        if self._defaults:
269            fp_.write(convert("[%s]\n" % self.DEFAULTSECT))
270            for (key, value) in self._defaults.items():
271                value = salt.utils.stringutils.to_unicode(value).replace("\n", "\n\t")
272                fp_.write(convert("{} = {}\n".format(key, value)))
273        for section in self._sections:
274            fp_.write(convert("[%s]\n" % section))
275            for (key, value) in self._sections[section].items():
276                if (value is not None) or (self._optcre == self.OPTCRE):
277                    if not isinstance(value, list):
278                        value = [value]
279                    for item in value:
280                        fp_.write(convert("\t%s\n" % " = ".join((key, item)).rstrip()))
281