1use crate::error::{GoError, Result};
4use regex::Regex;
5use std::cmp::Ordering;
6
7pub fn escape_module_path(path: &str) -> String {
24 let mut result = String::with_capacity(path.len() + 10);
25
26 for c in path.chars() {
27 if c.is_uppercase() {
28 result.push('!');
29 result.push(c.to_ascii_lowercase());
30 } else if c.is_ascii_alphanumeric()
31 || c == '/'
32 || c == '-'
33 || c == '.'
34 || c == '_'
35 || c == '~'
36 {
37 result.push(c);
38 } else {
39 let mut buf = [0u8; 4];
41 let encoded = c.encode_utf8(&mut buf);
42 for &byte in encoded.as_bytes() {
43 result.push_str(&format!("%{byte:02X}"));
44 }
45 }
46 }
47
48 result
49}
50
51pub fn is_pseudo_version(version: &str) -> bool {
64 static PSEUDO_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
65 Regex::new(r"^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+.*)?$").unwrap()
66 });
67
68 PSEUDO_REGEX.is_match(version)
69}
70
71pub fn base_version_from_pseudo(pseudo: &str) -> Option<String> {
84 if !is_pseudo_version(pseudo) {
85 return None;
86 }
87
88 let parts: Vec<&str> = pseudo.split('-').collect();
89 if parts.len() < 3 {
90 return None;
91 }
92
93 let version_part = parts[0];
94 let pre_release_part = parts[1];
95
96 if pre_release_part.starts_with('0') {
97 let semver = version_part.strip_prefix('v')?;
98 let mut components: Vec<u32> = semver.split('.').filter_map(|s| s.parse().ok()).collect();
99 if components.len() == 3 && components[2] > 0 {
100 components[2] -= 1;
101 return Some(format!(
102 "v{}.{}.{}",
103 components[0], components[1], components[2]
104 ));
105 }
106 }
107
108 Some(version_part.to_string())
109}
110
111pub fn compare_versions(v1: &str, v2: &str) -> Ordering {
139 let clean1 = v1.trim_start_matches('v').replace("+incompatible", "");
140 let clean2 = v2.trim_start_matches('v').replace("+incompatible", "");
141
142 let cmp1 = if is_pseudo_version(v1) {
143 base_version_from_pseudo(v1).unwrap_or(clean1)
144 } else {
145 clean1
146 };
147
148 let cmp2 = if is_pseudo_version(v2) {
149 base_version_from_pseudo(v2).unwrap_or(clean2)
150 } else {
151 clean2
152 };
153
154 match (parse_semver(&cmp1), parse_semver(&cmp2)) {
155 (Ok(ver1), Ok(ver2)) => ver1.cmp(&ver2),
156 _ => v1.cmp(v2),
157 }
158}
159
160fn parse_semver(version: &str) -> Result<semver::Version> {
161 let cleaned = version.trim_start_matches('v');
162
163 let split_at_prerelease = cleaned.split('-').next().unwrap_or(cleaned);
164
165 semver::Version::parse(split_at_prerelease).map_err(|e| GoError::InvalidVersionSpecifier {
166 specifier: version.to_string(),
167 message: e.to_string(),
168 })
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_escape_module_path() {
177 assert_eq!(
178 escape_module_path("github.com/User/Repo"),
179 "github.com/!user/!repo"
180 );
181 assert_eq!(
182 escape_module_path("github.com/gin-gonic/gin"),
183 "github.com/gin-gonic/gin"
184 );
185 assert_eq!(
186 escape_module_path("github.com/user/repo"),
187 "github.com/user/repo"
188 );
189 }
190
191 #[test]
192 fn test_escape_module_path_multiple_uppercase() {
193 assert_eq!(
194 escape_module_path("github.com/MyUser/MyRepo"),
195 "github.com/!my!user/!my!repo"
196 );
197 }
198
199 #[test]
200 fn test_is_pseudo_version() {
201 assert!(is_pseudo_version("v0.0.0-20191109021931-daa7c04131f5"));
202 assert!(is_pseudo_version("v1.2.4-0.20191109021931-daa7c04131f5"));
203 assert!(!is_pseudo_version("v1.2.3"));
204 assert!(!is_pseudo_version("v1.2.3-beta.1"));
205 }
206
207 #[test]
208 fn test_is_pseudo_version_with_incompatible() {
209 assert!(is_pseudo_version(
210 "v2.0.1-0.20191109021931-daa7c04131f5+incompatible"
211 ));
212 }
213
214 #[test]
215 fn test_base_version_from_pseudo() {
216 assert_eq!(
217 base_version_from_pseudo("v1.2.4-0.20191109021931-daa7c04131f5"),
218 Some("v1.2.3".to_string())
219 );
220 assert_eq!(
221 base_version_from_pseudo("v0.0.0-20191109021931-daa7c04131f5"),
222 Some("v0.0.0".to_string())
223 );
224 }
225
226 #[test]
227 fn test_base_version_from_pseudo_invalid() {
228 assert_eq!(base_version_from_pseudo("v1.2.3"), None);
229 }
230
231 #[test]
232 fn test_compare_versions() {
233 assert_eq!(compare_versions("v1.0.0", "v2.0.0"), Ordering::Less);
234 assert_eq!(compare_versions("v1.2.3", "v1.2.3"), Ordering::Equal);
235 assert_eq!(compare_versions("v2.0.0", "v1.0.0"), Ordering::Greater);
236 }
237
238 #[test]
239 fn test_compare_versions_patch() {
240 assert_eq!(compare_versions("v1.2.3", "v1.2.4"), Ordering::Less);
241 assert_eq!(compare_versions("v1.2.5", "v1.2.4"), Ordering::Greater);
242 }
243
244 #[test]
245 fn test_compare_versions_minor() {
246 assert_eq!(compare_versions("v1.2.0", "v1.3.0"), Ordering::Less);
247 assert_eq!(compare_versions("v1.5.0", "v1.3.0"), Ordering::Greater);
248 }
249
250 #[test]
251 fn test_compare_versions_incompatible() {
252 assert_eq!(
253 compare_versions("v2.0.0+incompatible", "v2.1.0+incompatible"),
254 Ordering::Less
255 );
256 }
257
258 #[test]
259 fn test_parse_semver_valid() {
260 assert!(parse_semver("1.2.3").is_ok());
261 assert!(parse_semver("v1.2.3").is_ok());
262 }
263
264 #[test]
265 fn test_parse_semver_invalid() {
266 assert!(parse_semver("invalid").is_err());
267 assert!(parse_semver("v1.2").is_err());
268 }
269
270 #[test]
271 fn test_pseudo_regex_compiles() {
272 let _ = is_pseudo_version("v0.0.0-20191109021931-daa7c04131f5");
273 }
274}