|  | 
| 2 | 2 | Common functions for working with RPM packages | 
| 3 | 3 | """ | 
| 4 | 4 | 
 | 
|  | 5 | +from __future__ import annotations | 
|  | 6 | + | 
| 5 | 7 | import collections | 
| 6 | 8 | import datetime | 
| 7 | 9 | import logging | 
| @@ -157,6 +159,156 @@ def combine_comments(comments): | 
| 157 | 159 |     return "".join(ret) | 
| 158 | 160 | 
 | 
| 159 | 161 | 
 | 
|  | 162 | +def evr_compare( | 
|  | 163 | +    evr1: tuple[str | None, str | None, str | None], | 
|  | 164 | +    evr2: tuple[str | None, str | None, str | None], | 
|  | 165 | +) -> int: | 
|  | 166 | +    """ | 
|  | 167 | +    Compare two RPM package identifiers using full epoch–version–release semantics. | 
|  | 168 | +
 | 
|  | 169 | +    This is a pure‑Python equivalent of ``rpm.labelCompare()``, returning the same | 
|  | 170 | +    ordering as the system RPM library without requiring the ``python3-rpm`` bindings. | 
|  | 171 | +
 | 
|  | 172 | +    The comparison is performed in three stages: | 
|  | 173 | +
 | 
|  | 174 | +    1. **Epoch** — compared numerically; missing or empty values are treated as 0. | 
|  | 175 | +    2. **Version** — compared using RPM's ``rpmvercmp`` rules: | 
|  | 176 | +       - Split into digit, alpha, and tilde (``~``) segments. | 
|  | 177 | +       - Tilde sorts before all other characters (e.g. ``1.0~beta`` < ``1.0``). | 
|  | 178 | +       - Numeric segments are compared as integers, ignoring leading zeros. | 
|  | 179 | +       - Numeric segments sort before alpha segments. | 
|  | 180 | +    3. **Release** — compared with the same rules as version. | 
|  | 181 | +
 | 
|  | 182 | +    :param evr1: The first ``(epoch, version, release)`` triple to compare. | 
|  | 183 | +                 Each element may be a string or ``None``. | 
|  | 184 | +    :param evr2: The second ``(epoch, version, release)`` triple to compare. | 
|  | 185 | +                 Each element may be a string or ``None``. | 
|  | 186 | +    :return: ``-1`` if ``evr1`` is considered older than ``evr2``, | 
|  | 187 | +             ``0`` if they are considered equal, | 
|  | 188 | +             ``1`` if ``evr1`` is considered newer than ``evr2``. | 
|  | 189 | +
 | 
|  | 190 | +    .. note:: | 
|  | 191 | +       This comparison is **not** the same as PEP 440, ``LooseVersion``, or | 
|  | 192 | +       ``StrictVersion``. It is intended for RPM package metadata and will match | 
|  | 193 | +       the ordering used by tools like ``rpm``, ``dnf``, and ``yum``. | 
|  | 194 | +
 | 
|  | 195 | +    .. code-block:: python | 
|  | 196 | +
 | 
|  | 197 | +       >>> label_compare(("0", "1.2.3", "1"), ("0", "1.2.3", "2")) | 
|  | 198 | +       -1 | 
|  | 199 | +       >>> label_compare(("1", "1.0", "1"), ("0", "9.9", "9")) | 
|  | 200 | +       1 | 
|  | 201 | +       >>> label_compare(("0", "1.0~beta", "1"), ("0", "1.0", "1")) | 
|  | 202 | +       -1 | 
|  | 203 | +    """ | 
|  | 204 | +    epoch1, version1, release1 = evr1 | 
|  | 205 | +    epoch2, version2, release2 = evr2 | 
|  | 206 | +    epoch1 = int(epoch1 or 0) | 
|  | 207 | +    epoch2 = int(epoch2 or 0) | 
|  | 208 | +    if epoch1 != epoch2: | 
|  | 209 | +        return 1 if epoch1 > epoch2 else -1 | 
|  | 210 | +    cmp_versions = _rpmvercmp(version1 or "", version2 or "") | 
|  | 211 | +    if cmp_versions != 0: | 
|  | 212 | +        return cmp_versions | 
|  | 213 | +    return _rpmvercmp(release1 or "", release2 or "") | 
|  | 214 | + | 
|  | 215 | + | 
|  | 216 | +def _rpmvercmp(a: str, b: str) -> int: | 
|  | 217 | +    """ | 
|  | 218 | +    Pure-Python comparator matching RPM's rpmvercmp(). | 
|  | 219 | +    Handles separators, tilde (~), caret (^), numeric/alpha segments. | 
|  | 220 | +    """ | 
|  | 221 | +    # Fast path: identical strings | 
|  | 222 | +    if a == b: | 
|  | 223 | +        return 0 | 
|  | 224 | + | 
|  | 225 | +    # Work with mutable indices instead of C char* pointers | 
|  | 226 | +    i = j = 0 | 
|  | 227 | +    la, lb = len(a), len(b) | 
|  | 228 | + | 
|  | 229 | +    def isalnum_(c: str) -> bool: | 
|  | 230 | +        return c.isalnum() | 
|  | 231 | + | 
|  | 232 | +    while i < la or j < lb: | 
|  | 233 | +        # Skip separators: anything not alnum, not ~, not ^ | 
|  | 234 | +        while i < la and not (isalnum_(a[i]) or a[i] in "~^"): | 
|  | 235 | +            i += 1 | 
|  | 236 | +        while j < lb and not (isalnum_(b[j]) or b[j] in "~^"): | 
|  | 237 | +            j += 1 | 
|  | 238 | + | 
|  | 239 | +        # Tilde: sorts before everything else | 
|  | 240 | +        if i < la and a[i] == "~" or j < lb and b[j] == "~": | 
|  | 241 | +            if not (i < la and a[i] == "~"): | 
|  | 242 | +                return 1 | 
|  | 243 | +            if not (j < lb and b[j] == "~"): | 
|  | 244 | +                return -1 | 
|  | 245 | +            i += 1 | 
|  | 246 | +            j += 1 | 
|  | 247 | +            continue | 
|  | 248 | + | 
|  | 249 | +        # Caret: like tilde except base (end) loses to caret | 
|  | 250 | +        if i < la and a[i] == "^" or j < lb and b[j] == "^": | 
|  | 251 | +            if i >= la: | 
|  | 252 | +                return -1 | 
|  | 253 | +            if j >= lb: | 
|  | 254 | +                return 1 | 
|  | 255 | +            if not (i < la and a[i] == "^"): | 
|  | 256 | +                return 1 | 
|  | 257 | +            if not (j < lb and b[j] == "^"): | 
|  | 258 | +                return -1 | 
|  | 259 | +            i += 1 | 
|  | 260 | +            j += 1 | 
|  | 261 | +            continue | 
|  | 262 | + | 
|  | 263 | +        # If either ran out now, stop | 
|  | 264 | +        if not (i < la and j < lb): | 
|  | 265 | +            break | 
|  | 266 | + | 
|  | 267 | +        # Segment start positions | 
|  | 268 | +        si, sj = i, j | 
|  | 269 | + | 
|  | 270 | +        # Decide type from left side | 
|  | 271 | +        isnum = a[i].isdigit() | 
|  | 272 | +        if isnum: | 
|  | 273 | +            while i < la and a[i].isdigit(): | 
|  | 274 | +                i += 1 | 
|  | 275 | +            while j < lb and b[j].isdigit(): | 
|  | 276 | +                j += 1 | 
|  | 277 | +        else: | 
|  | 278 | +            while i < la and a[i].isalpha(): | 
|  | 279 | +                i += 1 | 
|  | 280 | +            while j < lb and b[j].isalpha(): | 
|  | 281 | +                j += 1 | 
|  | 282 | + | 
|  | 283 | +        # If right side had no same‑type run, types differ | 
|  | 284 | +        if sj == j: | 
|  | 285 | +            return 1 if isnum else -1 | 
|  | 286 | + | 
|  | 287 | +        seg_a = a[si:i] | 
|  | 288 | +        seg_b = b[sj:j] | 
|  | 289 | + | 
|  | 290 | +        if isnum: | 
|  | 291 | +            # Strip leading zeros | 
|  | 292 | +            seg_a_nz = seg_a.lstrip("0") | 
|  | 293 | +            seg_b_nz = seg_b.lstrip("0") | 
|  | 294 | +            # Compare by length | 
|  | 295 | +            if len(seg_a_nz) != len(seg_b_nz): | 
|  | 296 | +                return 1 if len(seg_a_nz) > len(seg_b_nz) else -1 | 
|  | 297 | +            # Same length: lexicographic | 
|  | 298 | +            if seg_a_nz != seg_b_nz: | 
|  | 299 | +                return 1 if seg_a_nz > seg_b_nz else -1 | 
|  | 300 | +        else: | 
|  | 301 | +            # Alpha vs alpha | 
|  | 302 | +            if seg_a != seg_b: | 
|  | 303 | +                return 1 if seg_a > seg_b else -1 | 
|  | 304 | +        # else equal segment → loop continues | 
|  | 305 | + | 
|  | 306 | +    # Tail handling | 
|  | 307 | +    if i >= la and j >= lb: | 
|  | 308 | +        return 0 | 
|  | 309 | +    return -1 if i >= la else 1 | 
|  | 310 | + | 
|  | 311 | + | 
| 160 | 312 | def version_to_evr(verstring): | 
| 161 | 313 |     """ | 
| 162 | 314 |     Split the package version string into epoch, version and release. | 
|  | 
0 commit comments