1pub fn validate_semver(version: &str) -> bool {
5 let s = version.strip_prefix('v').unwrap_or(version);
6 if s.is_empty() {
7 return false;
8 }
9
10 let (s, _build) = match s.split_once('+') {
12 Some((core, b)) => {
13 if b.is_empty()
14 || !b
15 .chars()
16 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
17 {
18 return false;
19 }
20 (core, Some(b))
21 }
22 None => (s, None),
23 };
24
25 let (core, _pre) = match s.split_once('-') {
27 Some((c, p)) => {
28 if p.is_empty()
29 || !p
30 .chars()
31 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
32 {
33 return false;
34 }
35 (c, Some(p))
36 }
37 None => (s, None),
38 };
39
40 let parts: Vec<&str> = core.split('.').collect();
42 if parts.len() != 3 {
43 return false;
44 }
45
46 for part in parts {
47 if part.is_empty() || !part.chars().all(|c| c.is_ascii_digit()) {
48 return false;
49 }
50 if part.len() > 1 && part.starts_with('0') {
52 return false;
53 }
54 }
55
56 true
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn test_valid_semver() {
65 assert!(validate_semver("1.0.0"));
66 assert!(validate_semver("v1.0.0"));
67 assert!(validate_semver("v0.1.0"));
68 assert!(validate_semver("v1.2.3-alpha.1"));
69 assert!(validate_semver("v1.2.3+build.42"));
70 assert!(validate_semver("v1.2.3-rc.1+build.1"));
71 }
72
73 #[test]
74 fn test_invalid_semver() {
75 assert!(!validate_semver(""));
76 assert!(!validate_semver("v"));
77 assert!(!validate_semver("1.0"));
78 assert!(!validate_semver("v1.0"));
79 assert!(!validate_semver("1.0.0.0"));
80 assert!(!validate_semver("v01.0.0"));
81 assert!(!validate_semver("latest"));
82 assert!(!validate_semver("v1.0.0-"));
83 }
84}