"""Minimal Cargo-style semver parsing/matching (enough for RustSec version ranges).""" import re _V = re.compile(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$") def parse(v): m = _V.match(v.strip()) if not m: raise ValueError(v) a, b, c, pre = m.groups() return (int(a), int(b or 0), int(c or 0)), pre def key(v): t, pre = parse(v) # prerelease sorts before release return (t, 0 if pre else 1, pre or "") def is_pre(v): return parse(v)[1] is not None def _parts(s): m = _V.match(s.strip()) a, b, c, pre = m.groups() return int(a), (None if b is None else int(b)), (None if c is None else int(c)), pre def _cmp_one(v, op, rhs): kv = key(v) kr = key(rhs) if op == "=": a, b, c, _ = _parts(rhs) t = kv[0] if b is None: return t[0] == a if c is None: return t[0] == a and t[1] == b return kv == kr if op == "<": return kv < kr if op == "<=": a, b, c, _ = _parts(rhs) if c is None: # "<= 1.2" means < 1.3 style; be permissive hi = (a, (b if b is not None else 0), 10**9) return kv[0] <= hi return kv <= kr if op == ">": return kv > kr if op == ">=": return kv >= kr if op == "^" or op == "": a, b, c, _ = _parts(rhs) lo = kr if a > 0 or b is None: hi = (a + 1, 0, 0) elif b > 0 or c is None: hi = (0, b + 1, 0) else: hi = (0, 0, c + 1) return kv >= lo and kv[0] < hi if op == "~": a, b, c, _ = _parts(rhs) hi = (a + 1, 0, 0) if b is None else (a, b + 1, 0) return kv >= kr and kv[0] < hi raise ValueError(op) _C = re.compile(r"^\s*(<=|>=|=|<|>|\^|~)?\s*(\S+)\s*$") def matches_req(v, req): """req: comma-separated comparator list; all must hold.""" for part in req.split(","): m = _C.match(part) if not m: raise ValueError(req) op, rhs = m.groups() if rhs.endswith(".*"): rhs, op = rhs[:-2], "=" if not _cmp_one(v, op or "", rhs): return False return True def matches_any(v, reqs): return any(matches_req(v, r) for r in reqs)