Skip to main content

xianyun_update/
version.rs

1//! Semver comparison utilities for tool version checks.
2//!
3//! Ported from `internal/version/version.go`. Go's implementation defers
4//! parsing and comparison to `golang.org/x/mod/semver`. Rather than pull in
5//! a new `semver`-crate dependency for a check that only ever compares
6//! major.minor, this port hand-rolls the narrow slice of
7//! `golang.org/x/mod/semver`'s behavior the Go original actually uses:
8//! `IsValid`, `MajorMinor`, and `Compare` restricted to major/minor.
9
10use thiserror::Error;
11
12/// Ported from `ErrVersionTooOld` and `ErrInvalidVersion`.
13#[derive(Debug, Error, PartialEq, Eq)]
14pub enum VersionError {
15    /// Ported from `ErrVersionTooOld`.
16    #[error(
17        "version too old: installed {installed} ({installed_major_minor}), minimum required {required_major_minor}"
18    )]
19    TooOld {
20        installed: String,
21        installed_major_minor: String,
22        required_major_minor: String,
23    },
24
25    /// Ported from `ErrInvalidVersion`.
26    #[error("invalid semver version: {0:?}")]
27    Invalid(String),
28}
29
30/// Ported from `CheckMinimumVersion`. Compares the major.minor of
31/// `installed` against `required`. Version strings are normalized:
32/// tool-name prefixes (e.g. `"mirrord 3.187.0"`) and `"v"` prefixes are
33/// handled automatically.
34pub fn check_minimum_version(installed: &str, required: &str) -> Result<(), VersionError> {
35    let inst_norm = normalize(installed);
36    let (inst_major, inst_minor) =
37        major_minor(&inst_norm).ok_or_else(|| VersionError::Invalid(installed.to_string()))?;
38
39    let req_norm = normalize(required);
40    let (req_major, req_minor) =
41        major_minor(&req_norm).ok_or_else(|| VersionError::Invalid(required.to_string()))?;
42
43    if (inst_major, inst_minor) < (req_major, req_minor) {
44        return Err(VersionError::TooOld {
45            installed: installed.trim().to_string(),
46            installed_major_minor: format!("v{inst_major}.{inst_minor}"),
47            required_major_minor: format!("v{req_major}.{req_minor}"),
48        });
49    }
50
51    Ok(())
52}
53
54/// Ported from the package-level `normalize` helper.
55fn normalize(raw: &str) -> String {
56    let s = raw.trim();
57
58    let s = match s.rfind(' ') {
59        Some(idx) => &s[idx + 1..],
60        None => s,
61    };
62
63    if s.starts_with('v') {
64        s.to_string()
65    } else {
66        format!("v{s}")
67    }
68}
69
70/// Parses a `"v<major>.<minor>.<patch>..."`-shaped string into its
71/// major/minor components, mirroring the narrow slice of
72/// `golang.org/x/mod/semver.IsValid`/`MajorMinor` this module needs.
73/// Returns `None` if the string is not a valid semver-ish version.
74fn major_minor(v: &str) -> Option<(u64, u64)> {
75    let rest = v.strip_prefix('v')?;
76    if rest.is_empty() {
77        return None;
78    }
79
80    let mut parts = rest.split('.');
81    let major_str = parts.next()?;
82    let minor_str = parts.next()?;
83    // A third (patch) component must exist for the version to be
84    // considered valid, matching semver.IsValid's requirement of at
85    // least major.minor.patch, but its value is not otherwise used here.
86    parts.next()?;
87
88    if major_str.is_empty() || minor_str.is_empty() {
89        return None;
90    }
91
92    let major = major_str.parse::<u64>().ok()?;
93    let minor = minor_str.parse::<u64>().ok()?;
94
95    Some((major, minor))
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    struct Case {
103        name: &'static str,
104        installed: &'static str,
105        required: &'static str,
106        want_err: WantErr,
107    }
108
109    enum WantErr {
110        None,
111        TooOld,
112        Invalid,
113    }
114
115    // --- Ported from TestCheckMinimumVersion ---
116
117    #[test]
118    fn check_minimum_version_cases() {
119        let cases = [
120            Case {
121                name: "same version",
122                installed: "mirrord 3.187.0",
123                required: "3.187.0",
124                want_err: WantErr::None,
125            },
126            Case {
127                name: "newer installed",
128                installed: "mirrord 3.200.0",
129                required: "3.187.0",
130                want_err: WantErr::None,
131            },
132            Case {
133                name: "older major",
134                installed: "mirrord 2.100.0",
135                required: "3.187.0",
136                want_err: WantErr::TooOld,
137            },
138            Case {
139                name: "older minor",
140                installed: "mirrord 3.100.0",
141                required: "3.187.0",
142                want_err: WantErr::TooOld,
143            },
144            Case {
145                name: "skaffold v prefix",
146                installed: "v2.17.3",
147                required: "2.17.3",
148                want_err: WantErr::None,
149            },
150            Case {
151                name: "skaffold too old",
152                installed: "v1.39.0",
153                required: "2.17.3",
154                want_err: WantErr::TooOld,
155            },
156            Case {
157                name: "newer patch same major minor",
158                installed: "v2.17.5",
159                required: "2.17.3",
160                want_err: WantErr::None,
161            },
162            Case {
163                name: "invalid installed version",
164                installed: "garbage",
165                required: "2.17.3",
166                want_err: WantErr::Invalid,
167            },
168            Case {
169                name: "invalid required version",
170                installed: "v2.17.3",
171                required: "not-a-version",
172                want_err: WantErr::Invalid,
173            },
174            Case {
175                name: "older patch same major minor passes",
176                installed: "v2.17.1",
177                required: "2.17.3",
178                want_err: WantErr::None,
179            },
180        ];
181
182        for case in cases {
183            let result = check_minimum_version(case.installed, case.required);
184
185            match case.want_err {
186                WantErr::None => assert!(result.is_ok(), "{}", case.name),
187                WantErr::TooOld => assert!(
188                    matches!(result, Err(VersionError::TooOld { .. })),
189                    "{}",
190                    case.name
191                ),
192                WantErr::Invalid => assert!(
193                    matches!(result, Err(VersionError::Invalid(_))),
194                    "{}",
195                    case.name
196                ),
197            }
198        }
199    }
200}