1# -*- coding: utf-8 -*-
2# This Source Code Form is subject to the terms of the Mozilla Public
3# License, v. 2.0. If a copy of the MPL was not distributed with this
4# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
6from __future__ import absolute_import, print_function
7
8import os
9import re
10from six import string_types
11
12from .chunking import getChunk
13
14
15class UpdateVerifyError(Exception):
16    pass
17
18
19class UpdateVerifyConfig(object):
20    comment_regex = re.compile("^#")
21    key_write_order = (
22        "release",
23        "product",
24        "platform",
25        "build_id",
26        "locales",
27        "channel",
28        "patch_types",
29        "from",
30        "aus_server",
31        "ftp_server_from",
32        "ftp_server_to",
33        "to",
34        "mar_channel_IDs",
35        "override_certs",
36        "to_build_id",
37        "to_display_version",
38        "to_app_version",
39        "updater_package",
40    )
41    global_keys = (
42        "product",
43        "channel",
44        "aus_server",
45        "to",
46        "to_build_id",
47        "to_display_version",
48        "to_app_version",
49        "override_certs",
50    )
51    release_keys = (
52        "release",
53        "build_id",
54        "locales",
55        "patch_types",
56        "from",
57        "ftp_server_from",
58        "ftp_server_to",
59        "mar_channel_IDs",
60        "platform",
61        "updater_package",
62    )
63    first_only_keys = (
64        "from",
65        "aus_server",
66        "to",
67        "to_build_id",
68        "to_display_version",
69        "to_app_version",
70        "override_certs",
71    )
72    compare_attrs = global_keys + ("releases",)
73
74    def __init__(
75        self,
76        product=None,
77        channel=None,
78        aus_server=None,
79        to=None,
80        to_build_id=None,
81        to_display_version=None,
82        to_app_version=None,
83        override_certs=None,
84    ):
85        self.product = product
86        self.channel = channel
87        self.aus_server = aus_server
88        self.to = to
89        self.to_build_id = to_build_id
90        self.to_display_version = to_display_version
91        self.to_app_version = to_app_version
92        self.override_certs = override_certs
93        self.releases = []
94
95    def __eq__(self, other):
96        self_list = [getattr(self, attr) for attr in self.compare_attrs]
97        other_list = [getattr(other, attr) for attr in self.compare_attrs]
98        return self_list == other_list
99
100    def __ne__(self, other):
101        return not self.__eq__(other)
102
103    def _parseLine(self, line):
104        entry = {}
105        items = re.findall(r"\w+=[\"'][^\"']*[\"']", line)
106        for i in items:
107            m = re.search(r"(?P<key>\w+)=[\"'](?P<value>.+)[\"']", i).groupdict()
108            if m["key"] not in self.global_keys and m["key"] not in self.release_keys:
109                raise UpdateVerifyError(
110                    "Unknown key '%s' found on line:\n%s" % (m["key"], line)
111                )
112            if m["key"] in entry:
113                raise UpdateVerifyError(
114                    "Multiple values found for key '%s' on line:\n%s" % (m["key"], line)
115                )
116            entry[m["key"]] = m["value"]
117        if not entry:
118            raise UpdateVerifyError("No parseable data in line '%s'" % line)
119        return entry
120
121    def _addEntry(self, entry, first):
122        releaseKeys = {}
123        for k, v in entry.items():
124            if k in self.global_keys:
125                setattr(self, k, entry[k])
126            elif k in self.release_keys:
127                # "from" is reserved in Python
128                if k == "from":
129                    releaseKeys["from_path"] = v
130                else:
131                    releaseKeys[k] = v
132        self.addRelease(**releaseKeys)
133
134    def read(self, config):
135        f = open(config)
136        # Only the first non-comment line of an update verify config should
137        # have a "from" and"ausServer". Ignore any subsequent lines with them.
138        first = True
139        for line in f.readlines():
140            # Skip comment lines
141            if self.comment_regex.search(line):
142                continue
143            self._addEntry(self._parseLine(line), first)
144            first = False
145
146    def write(self, fh):
147        first = True
148        for releaseInfo in self.releases:
149            for key in self.key_write_order:
150                if key in self.global_keys and (
151                    first or key not in self.first_only_keys
152                ):
153                    value = getattr(self, key)
154                elif key in self.release_keys:
155                    value = releaseInfo[key]
156                else:
157                    value = None
158                if value is not None:
159                    fh.write(key.encode("utf-8"))
160                    fh.write(b"=")
161                    if isinstance(value, (list, tuple)):
162                        fh.write(('"%s" ' % " ".join(value)).encode("utf-8"))
163                    else:
164                        fh.write(('"%s" ' % value).encode("utf-8"))
165            # Rewind one character to avoid having a trailing space
166            fh.seek(-1, os.SEEK_CUR)
167            fh.write(b"\n")
168            first = False
169
170    def addRelease(
171        self,
172        release=None,
173        build_id=None,
174        locales=[],
175        patch_types=["complete"],
176        from_path=None,
177        ftp_server_from=None,
178        ftp_server_to=None,
179        mar_channel_IDs=None,
180        platform=None,
181        updater_package=None,
182    ):
183        """Locales and patch_types can be passed as either a string or a list.
184        If a string is passed, they will be converted to a list for internal
185        storage"""
186        if self.getRelease(build_id, from_path):
187            raise UpdateVerifyError(
188                "Couldn't add release identified by build_id '%s' and from_path '%s': "
189                "already exists in config" % (build_id, from_path)
190            )
191        if isinstance(locales, string_types):
192            locales = sorted(list(locales.split()))
193        if isinstance(patch_types, string_types):
194            patch_types = list(patch_types.split())
195        self.releases.append(
196            {
197                "release": release,
198                "build_id": build_id,
199                "locales": locales,
200                "patch_types": patch_types,
201                "from": from_path,
202                "ftp_server_from": ftp_server_from,
203                "ftp_server_to": ftp_server_to,
204                "mar_channel_IDs": mar_channel_IDs,
205                "platform": platform,
206                "updater_package": updater_package,
207            }
208        )
209
210    def addLocaleToRelease(self, build_id, locale, from_path=None):
211        r = self.getRelease(build_id, from_path)
212        if not r:
213            raise UpdateVerifyError(
214                "Couldn't add '%s' to release identified by build_id '%s' and from_path '%s': "
215                "'%s' doesn't exist in this config."
216                % (locale, build_id, from_path, build_id)
217            )
218        r["locales"].append(locale)
219        r["locales"] = sorted(r["locales"])
220
221    def getRelease(self, build_id, from_path):
222        for r in self.releases:
223            if r["build_id"] == build_id and r["from"] == from_path:
224                return r
225        return {}
226
227    def getFullReleaseTests(self):
228        return [r for r in self.releases if r["from"] is not None]
229
230    def getQuickReleaseTests(self):
231        return [r for r in self.releases if r["from"] is None]
232
233    def getChunk(self, chunks, thisChunk):
234        fullTests = []
235        quickTests = []
236        for test in self.getFullReleaseTests():
237            for locale in test["locales"]:
238                fullTests.append([test["build_id"], locale, test["from"]])
239        for test in self.getQuickReleaseTests():
240            for locale in test["locales"]:
241                quickTests.append([test["build_id"], locale, test["from"]])
242        allTests = getChunk(fullTests, chunks, thisChunk)
243        allTests.extend(getChunk(quickTests, chunks, thisChunk))
244
245        newConfig = UpdateVerifyConfig(
246            self.product,
247            self.channel,
248            self.aus_server,
249            self.to,
250            self.to_build_id,
251            self.to_display_version,
252            self.to_app_version,
253            self.override_certs,
254        )
255        for t in allTests:
256            build_id, locale, from_path = t
257            if from_path == "None":
258                from_path = None
259            r = self.getRelease(build_id, from_path)
260            try:
261                newConfig.addRelease(
262                    r["release"],
263                    build_id,
264                    locales=[],
265                    ftp_server_from=r["ftp_server_from"],
266                    ftp_server_to=r["ftp_server_to"],
267                    patch_types=r["patch_types"],
268                    from_path=from_path,
269                    mar_channel_IDs=r["mar_channel_IDs"],
270                    platform=r["platform"],
271                    updater_package=r["updater_package"],
272                )
273            except UpdateVerifyError:
274                pass
275            newConfig.addLocaleToRelease(build_id, locale, from_path)
276        return newConfig
277