1# Copyright (c) 2020 Matt Martz <matt@sivel.net>
2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
4# Make coding more python3-ish
5from __future__ import (absolute_import, division, print_function)
6__metaclass__ = type
7
8import re
9
10from distutils.version import LooseVersion, Version
11
12from ansible.module_utils.six import text_type
13
14
15# Regular expression taken from
16# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
17SEMVER_RE = re.compile(
18    r'''
19    ^
20        (?P<major>0|[1-9]\d*)
21        \.
22        (?P<minor>0|[1-9]\d*)
23        \.
24        (?P<patch>0|[1-9]\d*)
25        (?:
26            -
27            (?P<prerelease>
28                (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
29                (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
30            )
31        )?
32        (?:
33            \+
34            (?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)
35        )?
36    $
37    ''',
38    flags=re.X
39)
40
41
42class _Alpha:
43    """Class to easily allow comparing strings
44
45    Largely this exists to make comparing an integer and a string on py3
46    so that it works like py2.
47    """
48    def __init__(self, specifier):
49        self.specifier = specifier
50
51    def __repr__(self):
52        return repr(self.specifier)
53
54    def __eq__(self, other):
55        if isinstance(other, _Alpha):
56            return self.specifier == other.specifier
57        elif isinstance(other, str):
58            return self.specifier == other
59
60        return False
61
62    def __ne__(self, other):
63        return not self.__eq__(other)
64
65    def __lt__(self, other):
66        if isinstance(other, _Alpha):
67            return self.specifier < other.specifier
68        elif isinstance(other, str):
69            return self.specifier < other
70        elif isinstance(other, _Numeric):
71            return False
72
73        raise ValueError
74
75    def __le__(self, other):
76        return self.__lt__(other) or self.__eq__(other)
77
78    def __gt__(self, other):
79        return not self.__le__(other)
80
81    def __ge__(self, other):
82        return not self.__lt__(other)
83
84
85class _Numeric:
86    """Class to easily allow comparing numbers
87
88    Largely this exists to make comparing an integer and a string on py3
89    so that it works like py2.
90    """
91    def __init__(self, specifier):
92        self.specifier = int(specifier)
93
94    def __repr__(self):
95        return repr(self.specifier)
96
97    def __eq__(self, other):
98        if isinstance(other, _Numeric):
99            return self.specifier == other.specifier
100        elif isinstance(other, int):
101            return self.specifier == other
102
103        return False
104
105    def __ne__(self, other):
106        return not self.__eq__(other)
107
108    def __lt__(self, other):
109        if isinstance(other, _Numeric):
110            return self.specifier < other.specifier
111        elif isinstance(other, int):
112            return self.specifier < other
113        elif isinstance(other, _Alpha):
114            return True
115
116        raise ValueError
117
118    def __le__(self, other):
119        return self.__lt__(other) or self.__eq__(other)
120
121    def __gt__(self, other):
122        return not self.__le__(other)
123
124    def __ge__(self, other):
125        return not self.__lt__(other)
126
127
128class SemanticVersion(Version):
129    """Version comparison class that implements Semantic Versioning 2.0.0
130
131    Based off of ``distutils.version.Version``
132    """
133
134    version_re = SEMVER_RE
135
136    def __init__(self, vstring=None):
137        self.vstring = vstring
138        self.major = None
139        self.minor = None
140        self.patch = None
141        self.prerelease = ()
142        self.buildmetadata = ()
143
144        if vstring:
145            self.parse(vstring)
146
147    def __repr__(self):
148        return 'SemanticVersion(%r)' % self.vstring
149
150    @staticmethod
151    def from_loose_version(loose_version):
152        """This method is designed to take a ``LooseVersion``
153        and attempt to construct a ``SemanticVersion`` from it
154
155        This is useful where you want to do simple version math
156        without requiring users to provide a compliant semver.
157        """
158        if not isinstance(loose_version, LooseVersion):
159            raise ValueError("%r is not a LooseVersion" % loose_version)
160
161        try:
162            version = loose_version.version[:]
163        except AttributeError:
164            raise ValueError("%r is not a LooseVersion" % loose_version)
165
166        extra_idx = 3
167        for marker in ('-', '+'):
168            try:
169                idx = version.index(marker)
170            except ValueError:
171                continue
172            else:
173                if idx < extra_idx:
174                    extra_idx = idx
175        version = version[:extra_idx]
176
177        if version and set(type(v) for v in version) != set((int,)):
178            raise ValueError("Non integer values in %r" % loose_version)
179
180        # Extra is everything to the right of the core version
181        extra = re.search('[+-].+$', loose_version.vstring)
182
183        version = version + [0] * (3 - len(version))
184        return SemanticVersion(
185            '%s%s' % (
186                '.'.join(str(v) for v in version),
187                extra.group(0) if extra else ''
188            )
189        )
190
191    def parse(self, vstring):
192        match = SEMVER_RE.match(vstring)
193        if not match:
194            raise ValueError("invalid semantic version '%s'" % vstring)
195
196        (major, minor, patch, prerelease, buildmetadata) = match.group(1, 2, 3, 4, 5)
197        self.major = int(major)
198        self.minor = int(minor)
199        self.patch = int(patch)
200
201        if prerelease:
202            self.prerelease = tuple(_Numeric(x) if x.isdigit() else _Alpha(x) for x in prerelease.split('.'))
203        if buildmetadata:
204            self.buildmetadata = tuple(_Numeric(x) if x.isdigit() else _Alpha(x) for x in buildmetadata.split('.'))
205
206    @property
207    def core(self):
208        return self.major, self.minor, self.patch
209
210    @property
211    def is_prerelease(self):
212        return bool(self.prerelease)
213
214    @property
215    def is_stable(self):
216        # Major version zero (0.y.z) is for initial development. Anything MAY change at any time.
217        # The public API SHOULD NOT be considered stable.
218        # https://semver.org/#spec-item-4
219        return not (self.major == 0 or self.is_prerelease)
220
221    def _cmp(self, other):
222        if isinstance(other, str):
223            other = SemanticVersion(other)
224
225        if self.core != other.core:
226            # if the core version doesn't match
227            # prerelease and buildmetadata doesn't matter
228            if self.core < other.core:
229                return -1
230            else:
231                return 1
232
233        if not any((self.prerelease, other.prerelease)):
234            return 0
235
236        if self.prerelease and not other.prerelease:
237            return -1
238        elif not self.prerelease and other.prerelease:
239            return 1
240        else:
241            if self.prerelease < other.prerelease:
242                return -1
243            elif self.prerelease > other.prerelease:
244                return 1
245
246        # Build metadata MUST be ignored when determining version precedence
247        # https://semver.org/#spec-item-10
248        # With the above in mind it is ignored here
249
250        # If we have made it here, things should be equal
251        return 0
252
253    # The Py2 and Py3 implementations of distutils.version.Version
254    # are quite different, this makes the Py2 and Py3 implementations
255    # the same
256    def __eq__(self, other):
257        return self._cmp(other) == 0
258
259    def __ne__(self, other):
260        return not self.__eq__(other)
261
262    def __lt__(self, other):
263        return self._cmp(other) < 0
264
265    def __le__(self, other):
266        return self._cmp(other) <= 0
267
268    def __gt__(self, other):
269        return self._cmp(other) > 0
270
271    def __ge__(self, other):
272        return self._cmp(other) >= 0
273