Source: semver_core.goose

SHA-256 0bce7ec04cccfe84900556ed651e7ce9f803a6123f8d62e8c8aa6aadf404a79a · Raw source

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Lain contributors
3// 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;
6
7// 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 {
10    major: u64 = 0, minor: u64 = 0, patch: u64 = 0,
11    pre: const u8[:] = "", build: const u8[:] = ""
12}
13
14fn digit(c: u8) -> bool { c >= '0' && c <= '9' }
15
16// 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 {
19    var pos = 0;
20    while pos < text.len && text[pos] != separator { pos++; }
21    if pos == text.len { return text, text[pos..], false; }
22    return text[..pos], text[pos + 1..], true;
23}
24
25// Check before arithmetic: release wrapping and debug overflow both stay safe.
26fn number(text: const u8[:], pos: i64&) -> u64, bool {
27    let start = pos;
28    var n: u64 = 0;
29    while pos < text.len && digit(text[pos]) {
30        let d = (text[pos] - '0') as u64;
31        if n > (18446744073709551615 - d) / 10 { return 0, false; }
32        n = n * 10 + d;
33        pos++;
34    }
35    if pos == start { return 0, false; }
36    if pos - start > 1 && text[start] == '0' { return 0, false; }
37    return n, true;
38}
39
40fn identifiers_ok(text: const u8[:], pre: bool) -> bool {
41    if text.len == 0 { return false; }
42    var start = 0;
43    var pos = 0;
44    var numeric = true;
45    while pos <= text.len {
46        if pos == text.len || text[pos] == '.' {
47            if pos == start { return false; }
48            if pre && numeric && pos - start > 1 && text[start] == '0' { return false; }
49            start = pos + 1;
50            numeric = true;
51        } else {
52            let c = text[pos];
53            if !digit(c) && !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && c != '-' {
54                return false;
55            }
56            if !digit(c) { numeric = false; }
57        }
58        pos++;
59    }
60    return true;
61}
62
63// Strict three-component ASCII SemVer. On failure the result is a zero/empty
64// sentinel; inspect ok before using it. No allocation, input mutation or globals.
65fn parse(text: const u8[:]) -> Version, bool {
66    let version_text, build, has_build = split_once(text, '+');
67    let core, pre, has_pre = split_once(version_text, '-');
68    var pos = 0;
69    let major, major_ok = number(core, pos);
70    if !major_ok || pos >= core.len || core[pos] != '.' { return Version {}, false; }
71    pos++;
72    let minor, minor_ok = number(core, pos);
73    if !minor_ok || pos >= core.len || core[pos] != '.' { return Version {}, false; }
74    pos++;
75    let patch, patch_ok = number(core, pos);
76    if !patch_ok || pos != core.len { return Version {}, false; }
77    if has_pre && !identifiers_ok(pre, true) { return Version {}, false; }
78    if has_build && !identifiers_ok(build, false) { return Version {}, false; }
79    return Version { major, minor, patch, pre, build }, true;
80}
81
82fn order(a: u64, b: u64) -> i64 {
83    if a < b { return -1; }
84    if a > b { return 1; }
85    return 0;
86}
87
88fn numeric(text: const u8[:]) -> bool {
89    for c in text { if !digit(c) { return false; } }
90    return true;
91}
92
93fn identifier_order(a: const u8[:], b: const u8[:]) -> i64 {
94    let an = numeric(a);
95    let bn = numeric(b);
96    if an && !bn { return -1; }
97    if !an && bn { return 1; }
98    // Arbitrarily long numeric prereleases compare by length, then bytes.
99    // No numeric conversion and therefore no u64 overflow here.
100    if an && bn && a.len != b.len { return order(a.len as u64, b.len as u64); }
101    var i = 0;
102    while i < a.len && i < b.len {
103        if a[i] != b[i] { return order(a[i], b[i]); }
104        i++;
105    }
106    return order(a.len as u64, b.len as u64);
107}
108
109// SemVer precedence, equivalent to Rust Version::cmp_precedence, NOT its
110// total Ord/Eq over build metadata. Returns exactly -1, 0, or 1.
111fn compare_precedence(a: Version, b: Version) -> i64 {
112    let major = order(a.major, b.major);
113    if major != 0 { return major; }
114    let minor = order(a.minor, b.minor);
115    if minor != 0 { return minor; }
116    let patch = order(a.patch, b.patch);
117    if patch != 0 { return patch; }
118    if a.pre.len == 0 && b.pre.len == 0 { return 0; }
119    if a.pre.len == 0 { return 1; }
120    if b.pre.len == 0 { return -1; }
121    var a_remaining = a.pre;
122    var b_remaining = b.pre;
123    while a_remaining.len > 0 && b_remaining.len > 0 {
124        let a_id, a_tail, _ = split_once(a_remaining, '.');
125        let b_id, b_tail, _ = split_once(b_remaining, '.');
126        let result = identifier_order(a_id, b_id);
127        if result != 0 { return result; }
128        a_remaining = a_tail;
129        b_remaining = b_tail;
130    }
131    if a_remaining.len > 0 { return 1; }
132    if b_remaining.len > 0 { return -1; }
133    return 0;
134}