// SPDX-License-Identifier: MIT // Copyright (c) 2026 Lain contributors // SemVer parser/precedence slice informed by dtolnay/semver 1.0.27. // See NOTICE and LICENSE-MIT for upstream attribution and permission. namespace lain_semver_core; // A borrowed view. The caller owns the source bytes and must keep them alive. // Only values returned with ok=true are valid inputs to compare_precedence. struct Version { major: u64 = 0, minor: u64 = 0, patch: u64 = 0, pre: const u8[:] = "", build: const u8[:] = "" } fn digit(c: u8) -> bool { c >= '0' && c <= '9' } // Both views always borrow text, even when no separator is present. // The bool distinguishes an absent field from a present-but-empty field. fn split_once(text: const u8[:], separator: u8) -> const u8[:], const u8[:], bool { var pos = 0; while pos < text.len && text[pos] != separator { pos++; } if pos == text.len { return text, text[pos..], false; } return text[..pos], text[pos + 1..], true; } // Check before arithmetic: release wrapping and debug overflow both stay safe. fn number(text: const u8[:], pos: i64&) -> u64, bool { let start = pos; var n: u64 = 0; while pos < text.len && digit(text[pos]) { let d = (text[pos] - '0') as u64; if n > (18446744073709551615 - d) / 10 { return 0, false; } n = n * 10 + d; pos++; } if pos == start { return 0, false; } if pos - start > 1 && text[start] == '0' { return 0, false; } return n, true; } fn identifiers_ok(text: const u8[:], pre: bool) -> bool { if text.len == 0 { return false; } var start = 0; var pos = 0; var numeric = true; while pos <= text.len { if pos == text.len || text[pos] == '.' { if pos == start { return false; } if pre && numeric && pos - start > 1 && text[start] == '0' { return false; } start = pos + 1; numeric = true; } else { let c = text[pos]; if !digit(c) && !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && c != '-' { return false; } if !digit(c) { numeric = false; } } pos++; } return true; } // Strict three-component ASCII SemVer. On failure the result is a zero/empty // sentinel; inspect ok before using it. No allocation, input mutation or globals. fn parse(text: const u8[:]) -> Version, bool { let version_text, build, has_build = split_once(text, '+'); let core, pre, has_pre = split_once(version_text, '-'); var pos = 0; let major, major_ok = number(core, pos); if !major_ok || pos >= core.len || core[pos] != '.' { return Version {}, false; } pos++; let minor, minor_ok = number(core, pos); if !minor_ok || pos >= core.len || core[pos] != '.' { return Version {}, false; } pos++; let patch, patch_ok = number(core, pos); if !patch_ok || pos != core.len { return Version {}, false; } if has_pre && !identifiers_ok(pre, true) { return Version {}, false; } if has_build && !identifiers_ok(build, false) { return Version {}, false; } return Version { major, minor, patch, pre, build }, true; } fn order(a: u64, b: u64) -> i64 { if a < b { return -1; } if a > b { return 1; } return 0; } fn numeric(text: const u8[:]) -> bool { for c in text { if !digit(c) { return false; } } return true; } fn identifier_order(a: const u8[:], b: const u8[:]) -> i64 { let an = numeric(a); let bn = numeric(b); if an && !bn { return -1; } if !an && bn { return 1; } // Arbitrarily long numeric prereleases compare by length, then bytes. // No numeric conversion and therefore no u64 overflow here. if an && bn && a.len != b.len { return order(a.len as u64, b.len as u64); } var i = 0; while i < a.len && i < b.len { if a[i] != b[i] { return order(a[i], b[i]); } i++; } return order(a.len as u64, b.len as u64); } // SemVer precedence, equivalent to Rust Version::cmp_precedence, NOT its // total Ord/Eq over build metadata. Returns exactly -1, 0, or 1. fn compare_precedence(a: Version, b: Version) -> i64 { let major = order(a.major, b.major); if major != 0 { return major; } let minor = order(a.minor, b.minor); if minor != 0 { return minor; } let patch = order(a.patch, b.patch); if patch != 0 { return patch; } if a.pre.len == 0 && b.pre.len == 0 { return 0; } if a.pre.len == 0 { return 1; } if b.pre.len == 0 { return -1; } var a_remaining = a.pre; var b_remaining = b.pre; while a_remaining.len > 0 && b_remaining.len > 0 { let a_id, a_tail, _ = split_once(a_remaining, '.'); let b_id, b_tail, _ = split_once(b_remaining, '.'); let result = identifier_order(a_id, b_id); if result != 0 { return result; } a_remaining = a_tail; b_remaining = b_tail; } if a_remaining.len > 0 { return 1; } if b_remaining.len > 0 { return -1; } return 0; }