1use deps_core::{DepsError, 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 escape_version(version: &str) -> String {
73 let mut result = String::with_capacity(version.len());
74
75 for c in version.chars() {
76 if c.is_uppercase() {
77 result.push('!');
78 result.push(c.to_ascii_lowercase());
79 } else if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '+' {
80 result.push(c);
81 } else {
82 let mut buf = [0u8; 4];
83 let encoded = c.encode_utf8(&mut buf);
84 for &byte in encoded.as_bytes() {
85 result.push_str(&format!("%{byte:02X}"));
86 }
87 }
88 }
89
90 result
91}
92
93pub fn is_pseudo_version(version: &str) -> bool {
106 static PSEUDO_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
107 Regex::new(r"^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+.*)?$").unwrap()
108 });
109
110 PSEUDO_REGEX.is_match(version)
111}
112
113pub fn base_version_from_pseudo(pseudo: &str) -> Option<String> {
126 if !is_pseudo_version(pseudo) {
127 return None;
128 }
129
130 let parts: Vec<&str> = pseudo.split('-').collect();
131 if parts.len() < 3 {
132 return None;
133 }
134
135 let version_part = parts[0];
136 let pre_release_part = parts[1];
137
138 if pre_release_part.starts_with('0') {
139 let semver = version_part.strip_prefix('v')?;
140 let mut components: Vec<u32> = semver.split('.').filter_map(|s| s.parse().ok()).collect();
141 if components.len() == 3 && components[2] > 0 {
142 components[2] -= 1;
143 return Some(format!(
144 "v{}.{}.{}",
145 components[0], components[1], components[2]
146 ));
147 }
148 }
149
150 Some(version_part.to_string())
151}
152
153pub fn compare_versions(v1: &str, v2: &str) -> Ordering {
181 let clean1 = v1.trim_start_matches('v').replace("+incompatible", "");
182 let clean2 = v2.trim_start_matches('v').replace("+incompatible", "");
183
184 let cmp1 = if is_pseudo_version(v1) {
185 base_version_from_pseudo(v1).unwrap_or(clean1)
186 } else {
187 clean1
188 };
189
190 let cmp2 = if is_pseudo_version(v2) {
191 base_version_from_pseudo(v2).unwrap_or(clean2)
192 } else {
193 clean2
194 };
195
196 match (parse_semver(&cmp1), parse_semver(&cmp2)) {
197 (Ok(ver1), Ok(ver2)) => ver1.cmp(&ver2),
198 _ => v1.cmp(v2),
199 }
200}
201
202fn parse_semver(version: &str) -> Result<semver::Version> {
203 let cleaned = version.trim_start_matches('v');
204
205 let split_at_prerelease = cleaned.split('-').next().unwrap_or(cleaned);
206
207 semver::Version::parse(split_at_prerelease)
208 .map_err(|e| DepsError::InvalidVersionReq(e.to_string()))
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_escape_module_path() {
217 assert_eq!(
218 escape_module_path("github.com/User/Repo"),
219 "github.com/!user/!repo"
220 );
221 assert_eq!(
222 escape_module_path("github.com/gin-gonic/gin"),
223 "github.com/gin-gonic/gin"
224 );
225 assert_eq!(
226 escape_module_path("github.com/user/repo"),
227 "github.com/user/repo"
228 );
229 }
230
231 #[test]
232 fn test_escape_module_path_multiple_uppercase() {
233 assert_eq!(
234 escape_module_path("github.com/MyUser/MyRepo"),
235 "github.com/!my!user/!my!repo"
236 );
237 }
238
239 #[test]
240 fn test_escape_version_preserves_legitimate_versions() {
241 assert_eq!(escape_version("v1.9.1"), "v1.9.1");
242 assert_eq!(
243 escape_version("v0.0.0-20191109021931-daa7c04131f5"),
244 "v0.0.0-20191109021931-daa7c04131f5"
245 );
246 assert_eq!(escape_version("v2.0.0+incompatible"), "v2.0.0+incompatible");
247 }
248
249 #[test]
250 fn test_escape_version_encodes_query_and_fragment_chars() {
251 let escaped = escape_version("v1?a=b");
252 assert!(!escaped.contains('?'));
253 assert!(!escaped.contains('='));
254
255 let escaped = escape_version("v1#frag");
256 assert!(!escaped.contains('#'));
257
258 let escaped = escape_version("v1 space");
259 assert!(!escaped.contains(' '));
260 }
261
262 #[test]
263 fn test_escape_version_case_folds_uppercase() {
264 assert_eq!(escape_version("v1.7.0-RC"), "v1.7.0-!r!c");
265 assert_eq!(escape_version("V1.0.0"), "!v1.0.0");
266 }
267
268 #[test]
269 fn test_is_pseudo_version() {
270 assert!(is_pseudo_version("v0.0.0-20191109021931-daa7c04131f5"));
271 assert!(is_pseudo_version("v1.2.4-0.20191109021931-daa7c04131f5"));
272 assert!(!is_pseudo_version("v1.2.3"));
273 assert!(!is_pseudo_version("v1.2.3-beta.1"));
274 }
275
276 #[test]
277 fn test_is_pseudo_version_with_incompatible() {
278 assert!(is_pseudo_version(
279 "v2.0.1-0.20191109021931-daa7c04131f5+incompatible"
280 ));
281 }
282
283 #[test]
284 fn test_base_version_from_pseudo() {
285 assert_eq!(
286 base_version_from_pseudo("v1.2.4-0.20191109021931-daa7c04131f5"),
287 Some("v1.2.3".to_string())
288 );
289 assert_eq!(
290 base_version_from_pseudo("v0.0.0-20191109021931-daa7c04131f5"),
291 Some("v0.0.0".to_string())
292 );
293 }
294
295 #[test]
296 fn test_base_version_from_pseudo_invalid() {
297 assert_eq!(base_version_from_pseudo("v1.2.3"), None);
298 }
299
300 #[test]
301 fn test_compare_versions() {
302 assert_eq!(compare_versions("v1.0.0", "v2.0.0"), Ordering::Less);
303 assert_eq!(compare_versions("v1.2.3", "v1.2.3"), Ordering::Equal);
304 assert_eq!(compare_versions("v2.0.0", "v1.0.0"), Ordering::Greater);
305 }
306
307 #[test]
308 fn test_compare_versions_patch() {
309 assert_eq!(compare_versions("v1.2.3", "v1.2.4"), Ordering::Less);
310 assert_eq!(compare_versions("v1.2.5", "v1.2.4"), Ordering::Greater);
311 }
312
313 #[test]
314 fn test_compare_versions_minor() {
315 assert_eq!(compare_versions("v1.2.0", "v1.3.0"), Ordering::Less);
316 assert_eq!(compare_versions("v1.5.0", "v1.3.0"), Ordering::Greater);
317 }
318
319 #[test]
320 fn test_compare_versions_incompatible() {
321 assert_eq!(
322 compare_versions("v2.0.0+incompatible", "v2.1.0+incompatible"),
323 Ordering::Less
324 );
325 }
326
327 #[test]
328 fn test_parse_semver_valid() {
329 assert!(parse_semver("1.2.3").is_ok());
330 assert!(parse_semver("v1.2.3").is_ok());
331 }
332
333 #[test]
334 fn test_parse_semver_invalid() {
335 assert!(parse_semver("invalid").is_err());
336 assert!(parse_semver("v1.2").is_err());
337 }
338
339 #[test]
340 fn test_pseudo_regex_compiles() {
341 let _ = is_pseudo_version("v0.0.0-20191109021931-daa7c04131f5");
342 }
343}