1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
|
# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
import collections
from enum import IntEnum
"""
In order to stay as compatible to Alpine's apk as possible, this code
is heavily based on:
https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/5d796b567819ce91740fcdea7cbafecbda65d8f3/src/version.c
"""
class Token(IntEnum):
"""C equivalent: enum PARTS"""
INVALID = -1
DIGIT_OR_ZERO = 0
DIGIT = 1
LETTER = 2
SUFFIX = 3
SUFFIX_NO = 4
REVISION_NO = 5
END = 6
def next_token(previous: Token, rest: str) -> tuple[Token, str]:
"""
Parse the next token in the rest of the version string, we're
currently looking at.
We do *not* get the value of the token, or advance the rest string
beyond the whole token that is what the get_token() function does
(see below).
:param previous: the token before
:param rest: of the version string
:returns: (next, rest) next is the upcoming token, rest is the
input "rest" string with one leading '.', '_' or '-'
character removed (if there was any).
C equivalent: next_token()
"""
next = Token.INVALID
char = rest[:1]
# Tokes, which do not change rest
if not len(rest):
next = Token.END
elif previous in [Token.DIGIT, Token.DIGIT_OR_ZERO] and char.islower():
next = Token.LETTER
elif previous == Token.LETTER and char.isdigit():
next = Token.DIGIT
elif previous == Token.SUFFIX and char.isdigit():
next = Token.SUFFIX_NO
# Tokens, which remove the first character of rest
else:
if char == ".":
next = Token.DIGIT_OR_ZERO
elif char == "_":
next = Token.SUFFIX
elif rest.startswith("-r"):
next = Token.REVISION_NO
rest = rest[1:]
elif char == "-":
next = Token.INVALID
rest = rest[1:]
# Validate current token
# Check if the transition from previous to current is valid
if next < previous and not (
(next == Token.DIGIT_OR_ZERO and previous == Token.DIGIT)
or (next == Token.SUFFIX and previous == Token.SUFFIX_NO)
or (next == Token.DIGIT and previous == Token.LETTER)
):
next = Token.INVALID
return (next, rest)
def parse_suffix(rest: str) -> tuple[str, int, bool]:
"""
Cut off the suffix of rest (which is now at the beginning of the
rest variable, but regarding the whole version string, it is a
suffix), and return a value integer (so it can be compared later,
"beta" > "alpha" etc).
:param rest: what is left of the version string that we are
currently parsing, starts with a "suffix" value
(see below for valid suffixes).
:returns: (rest, value, invalid_suffix)
- rest: is the input "rest" string without the suffix
- value: is a signed integer (negative for pre-,
positive for post-suffixes).
- invalid_suffix: is true, when rest does not start
with anything from the suffixes variable.
C equivalent: get_token(), case TOKEN_SUFFIX
"""
name_suffixes = collections.OrderedDict(
[
("pre", ["alpha", "beta", "pre", "rc"]),
("post", ["cvs", "svn", "git", "hg", "p"]),
]
)
for name, suffixes in name_suffixes.items():
for i, suffix in enumerate(suffixes):
if not rest.startswith(suffix):
continue
rest = rest[len(suffix) :]
value = i
if name == "pre":
value = value - len(suffixes)
return (rest, value, False)
return (rest, 0, True)
def get_token(previous: Token, rest: str) -> tuple[Token, int, str]:
"""
This function does three things:
* get the next token
* get the token value
* cut-off the whole token from rest
:param previous: the token before
:param rest: of the version string
:returns: (next, value, rest) next is the new token string,
value is an integer for comparing, rest is the rest of the
input string.
C equivalent: get_token()
"""
# Set defaults
value = 0
next = Token.INVALID
invalid_suffix = False
# Bail out if at the end
if not len(rest):
return (Token.END, 0, rest)
# Cut off leading zero digits
if previous == Token.DIGIT_OR_ZERO and rest.startswith("0"):
while rest.startswith("0"):
rest = rest[1:]
value -= 1
next = Token.DIGIT
# Add up numeric values
elif previous in [Token.DIGIT_OR_ZERO, Token.DIGIT, Token.SUFFIX_NO, Token.REVISION_NO]:
for i in range(len(rest)):
while len(rest) and rest[0].isdigit():
value *= 10
value += int(rest[i])
rest = rest[1:]
# Append chars or parse suffix
elif previous == Token.LETTER:
value = ord(rest[0])
rest = rest[1:]
elif previous == Token.SUFFIX:
(rest, value, invalid_suffix) = parse_suffix(rest)
# Invalid previous token
else:
value = -1
# Get the next token (for non-leading zeros)
if not len(rest):
next = Token.END
elif next == Token.INVALID and not invalid_suffix:
(next, rest) = next_token(previous, rest)
return (next, value, rest)
def validate(version: str) -> bool:
"""
Check whether one version string is valid.
:param version: full version string
:returns: True when the version string is valid
C equivalent: apk_version_validate()
"""
current = Token.DIGIT
rest = version
while current != Token.END:
(current, _, rest) = get_token(current, rest)
if current == Token.INVALID:
return False
return True
def compare(a_version: str, b_version: str, fuzzy: bool = False) -> int:
"""
Compare two versions A and B to find out which one is higher, or if
both are equal.
:param a_version: full version string A
:param b_version: full version string B
:param fuzzy: treat version strings, which end in different token
types as equal
:returns:
(a < b): -1
(a == b): 0
(a > b): 1
C equivalent: apk_version_compare_blob_fuzzy()
"""
# Defaults
a_token = Token.DIGIT
b_token = Token.DIGIT
a_value = 0
b_value = 0
a_rest = a_version
b_rest = b_version
# Parse A and B one token at a time, until one string ends, or the
# current token has a different type/value
while a_token == b_token and a_token not in [Token.END, Token.INVALID] and a_value == b_value:
(a_token, a_value, a_rest) = get_token(a_token, a_rest)
(b_token, b_value, b_rest) = get_token(b_token, b_rest)
# Compare the values inside the last tokens
if a_value < b_value:
return -1
if a_value > b_value:
return 1
# Equal: When tokens are the same strings, or when the value
# is the same and fuzzy compare is enabled
if a_token == b_token or fuzzy:
return 0
# Leading version components and their values are equal, now the
# non-terminating version is greater unless it's a suffix
# indicating pre-release
if a_token == Token.SUFFIX:
(a_token, a_value, a_rest) = get_token(a_token, a_rest)
if a_value < 0:
return -1
if b_token == Token.SUFFIX:
(b_token, b_value, b_rest) = get_token(b_token, b_rest)
if b_value < 0:
return 1
# Compare the token value (e.g. digit < letter)
if a_token > b_token:
return -1
if a_token < b_token:
return 1
# The tokens are not the same, but previous checks revealed that it
# is equal anyway (e.g. "1.0" == "1").
return 0
"""
Convenience functions below are not modeled after apk's version.c.
"""
def check_string(a_version: str, rule: str) -> bool:
"""
Compare a version against a check string. This is used in "pmbootstrap
kconfig check", to only require certain options if the pkgver is in a
specified range (#1795).
:param a_version: "3.4.1"
:param rule: ">=1.0.0"
:returns: True if a_version matches rule, false otherwise.
"""
# Operators and the expected returns of compare(a,b)
operator_results = {">=": [1, 0], "<": [-1]}
# Find the operator
b_version = None
expected_results = None
for operator in operator_results:
if rule.startswith(operator):
b_version = rule[len(operator) :]
expected_results = operator_results[operator]
break
# No operator found
if not b_version:
raise RuntimeError(
"Could not find operator in '" + rule + "'. You"
" probably need to adjust check_string() in"
" pmb/parse/version.py."
)
# Compare
result = compare(a_version, b_version)
return not expected_results or result in expected_results
|