Source: semver_core.goose
SHA-256 0bce7ec04cccfe84900556ed651e7ce9f803a6123f8d62e8c8aa6aadf404a79a · Raw source
1// SPDX-License-Identifier: MIT2// Copyright (c) 2026 Lain contributors3// SemVer parser/precedence slice informed by dtolnay/semver 1.0.27.4// See NOTICE and LICENSE-MIT for upstream attribution and permission.5namespace lain_semver_core;67// A borrowed view. The caller owns the source bytes and must keep them alive.8// Only values returned with ok=true are valid inputs to compare_precedence.9struct Version {10major: u64 = 0, minor: u64 = 0, patch: u64 = 0,11pre: const u8[:] = "", build: const u8[:] = ""12}1314fn digit(c: u8) -> bool { c >= '0' && c <= '9' }1516// Both views always borrow text, even when no separator is present.17// The bool distinguishes an absent field from a present-but-empty field.18fn split_once(text: const u8[:], separator: u8) -> const u8[:], const u8[:], bool {19var pos = 0;20while pos < text.len && text[pos] != separator { pos++; }21if pos == text.len { return text, text[pos..], false; }22return text[..pos], text[pos + 1..], true;23}2425// Check before arithmetic: release wrapping and debug overflow both stay safe.26fn number(text: const u8[:], pos: i64&) -> u64, bool {27let start = pos;28var n: u64 = 0;29while pos < text.len && digit(text[pos]) {30let d = (text[pos] - '0') as u64;31if n > (18446744073709551615 - d) / 10 { return 0, false; }32n = n * 10 + d;33pos++;34}35if pos == start { return 0, false; }36if pos - start > 1 && text[start] == '0' { return 0, false; }37return n, true;38}3940fn identifiers_ok(text: const u8[:], pre: bool) -> bool {41if text.len == 0 { return false; }42var start = 0;43var pos = 0;44var numeric = true;45while pos <= text.len {46if pos == text.len || text[pos] == '.' {47if pos == start { return false; }48if pre && numeric && pos - start > 1 && text[start] == '0' { return false; }49start = pos + 1;50numeric = true;51} else {52let c = text[pos];53if !digit(c) && !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && c != '-' {54return false;55}56if !digit(c) { numeric = false; }57}58pos++;59}60return true;61}6263// Strict three-component ASCII SemVer. On failure the result is a zero/empty64// sentinel; inspect ok before using it. No allocation, input mutation or globals.65fn parse(text: const u8[:]) -> Version, bool {66let version_text, build, has_build = split_once(text, '+');67let core, pre, has_pre = split_once(version_text, '-');68var pos = 0;69let major, major_ok = number(core, pos);70if !major_ok || pos >= core.len || core[pos] != '.' { return Version {}, false; }71pos++;72let minor, minor_ok = number(core, pos);73if !minor_ok || pos >= core.len || core[pos] != '.' { return Version {}, false; }74pos++;75let patch, patch_ok = number(core, pos);76if !patch_ok || pos != core.len { return Version {}, false; }77if has_pre && !identifiers_ok(pre, true) { return Version {}, false; }78if has_build && !identifiers_ok(build, false) { return Version {}, false; }79return Version { major, minor, patch, pre, build }, true;80}8182fn order(a: u64, b: u64) -> i64 {83if a < b { return -1; }84if a > b { return 1; }85return 0;86}8788fn numeric(text: const u8[:]) -> bool {89for c in text { if !digit(c) { return false; } }90return true;91}9293fn identifier_order(a: const u8[:], b: const u8[:]) -> i64 {94let an = numeric(a);95let bn = numeric(b);96if an && !bn { return -1; }97if !an && bn { return 1; }98// Arbitrarily long numeric prereleases compare by length, then bytes.99// No numeric conversion and therefore no u64 overflow here.100if an && bn && a.len != b.len { return order(a.len as u64, b.len as u64); }101var i = 0;102while i < a.len && i < b.len {103if a[i] != b[i] { return order(a[i], b[i]); }104i++;105}106return order(a.len as u64, b.len as u64);107}108109// SemVer precedence, equivalent to Rust Version::cmp_precedence, NOT its110// total Ord/Eq over build metadata. Returns exactly -1, 0, or 1.111fn compare_precedence(a: Version, b: Version) -> i64 {112let major = order(a.major, b.major);113if major != 0 { return major; }114let minor = order(a.minor, b.minor);115if minor != 0 { return minor; }116let patch = order(a.patch, b.patch);117if patch != 0 { return patch; }118if a.pre.len == 0 && b.pre.len == 0 { return 0; }119if a.pre.len == 0 { return 1; }120if b.pre.len == 0 { return -1; }121var a_remaining = a.pre;122var b_remaining = b.pre;123while a_remaining.len > 0 && b_remaining.len > 0 {124let a_id, a_tail, _ = split_once(a_remaining, '.');125let b_id, b_tail, _ = split_once(b_remaining, '.');126let result = identifier_order(a_id, b_id);127if result != 0 { return result; }128a_remaining = a_tail;129b_remaining = b_tail;130}131if a_remaining.len > 0 { return 1; }132if b_remaining.len > 0 { return -1; }133return 0;134}