1use std::cmp::Ordering;
9
10fn is_special(c: u8) -> bool {
11 matches!(c, b'-' | b'_' | b'+')
12}
13
14pub fn canonicalize(version: &str) -> String {
18 let mut out = Vec::new();
19 canonicalize_into(version, &mut out);
20 String::from_utf8_lossy(&out).into_owned()
21}
22
23fn canonicalize_into(version: &str, out: &mut Vec<u8>) {
25 out.clear();
26 let bytes = version.as_bytes();
27 let Some((&first, rest)) = bytes.split_first() else {
28 return;
29 };
30 let is_dig = |c: u8| c.is_ascii_digit();
31 let is_ndig = |c: u8| !c.is_ascii_digit() && c != b'.';
32 out.reserve(bytes.len() * 2);
33 out.push(first);
34 let mut lp = first;
35 for &c in rest {
36 if is_special(c) {
37 if out.last() != Some(&b'.') {
38 out.push(b'.');
39 }
40 } else if (is_ndig(lp) && is_dig(c)) || (is_dig(lp) && is_ndig(c)) {
41 if out.last() != Some(&b'.') {
42 out.push(b'.');
43 }
44 out.push(c);
45 } else if !c.is_ascii_alphanumeric() {
46 if out.last() != Some(&b'.') {
47 out.push(b'.');
48 }
49 } else {
50 out.push(c);
51 }
52 lp = c;
53 }
54}
55
56fn special_rank(form: &str) -> i32 {
58 const FORMS: &[(&str, i32)] = &[
59 ("dev", 0),
60 ("alpha", 1),
61 ("a", 1),
62 ("beta", 2),
63 ("b", 2),
64 ("RC", 3),
65 ("rc", 3),
66 ("#", 4),
67 ("pl", 5),
68 ("p", 5),
69 ];
70 for (name, order) in FORMS {
71 if form.starts_with(name) {
72 return *order;
73 }
74 }
75 -1
76}
77
78fn compare_special(a: &str, b: &str) -> Ordering {
79 special_rank(a).cmp(&special_rank(b))
80}
81
82fn parse_num(s: &str) -> i64 {
83 let mut n: i64 = 0;
85 for c in s.bytes().take_while(u8::is_ascii_digit) {
86 n = n.saturating_mul(10).saturating_add(i64::from(c - b'0'));
87 }
88 n
89}
90
91fn is_plain_dotted(s: &str) -> bool {
94 let b = s.as_bytes();
95 !b.is_empty()
96 && b[0].is_ascii_digit()
97 && b[b.len() - 1].is_ascii_digit()
98 && b.iter().all(|c| c.is_ascii_digit() || *c == b'.')
99 && !b.windows(2).any(|w| w == b"..")
100}
101
102fn starts_digit(s: &str) -> bool {
103 s.bytes().next().is_some_and(|b| b.is_ascii_digit())
104}
105
106fn compare_canonical(a: &str, b: &str) -> Ordering {
108 if a.is_empty() || b.is_empty() {
109 return match (a.is_empty(), b.is_empty()) {
110 (true, true) => Ordering::Equal,
111 (true, false) => Ordering::Less,
112 (false, true) => Ordering::Greater,
113 _ => Ordering::Equal,
114 };
115 }
116 let mut pa = a.split('.').peekable();
117 let mut pb = b.split('.').peekable();
118 let mut result = Ordering::Equal;
119 loop {
120 match (pa.next(), pb.next()) {
121 (Some(x), Some(y)) => {
122 result = match (starts_digit(x), starts_digit(y)) {
123 (true, true) => parse_num(x).cmp(&parse_num(y)),
124 (false, false) => compare_special(x, y),
125 (true, false) => compare_special("#N#", y),
126 (false, true) => compare_special(x, "#N#"),
127 };
128 if result != Ordering::Equal {
129 return result;
130 }
131 }
132 (Some(x), None) => {
133 return if starts_digit(x) {
134 Ordering::Greater
135 } else {
136 compare_canonical(x, "#N#")
137 };
138 }
139 (None, Some(y)) => {
140 return if starts_digit(y) {
141 Ordering::Less
142 } else {
143 compare_canonical("#N#", y)
144 };
145 }
146 (None, None) => return result,
147 }
148 }
149}
150
151pub fn version_compare(a: &str, b: &str) -> Ordering {
153 if is_plain_dotted(a) && is_plain_dotted(b) {
154 let mut pa = a.split('.');
157 let mut pb = b.split('.');
158 loop {
159 match (pa.next(), pb.next()) {
160 (Some(x), Some(y)) => {
161 let c = parse_num(x).cmp(&parse_num(y));
162 if c != Ordering::Equal {
163 return c;
164 }
165 }
166 (Some(_), None) => return Ordering::Greater,
167 (None, Some(_)) => return Ordering::Less,
168 (None, None) => return Ordering::Equal,
169 }
170 }
171 }
172 thread_local! {
173 static BUFS: std::cell::RefCell<(Vec<u8>, Vec<u8>)> = const { std::cell::RefCell::new((Vec::new(), Vec::new())) };
174 }
175 BUFS.with(|bufs| {
176 let mut bufs = bufs.borrow_mut();
177 let (ca, cb) = &mut *bufs;
178 canonicalize_into(a, ca);
179 canonicalize_into(b, cb);
180 compare_canonical(&String::from_utf8_lossy(ca), &String::from_utf8_lossy(cb))
182 })
183}
184
185pub fn version_compare_op(a: &str, b: &str, op: &str) -> bool {
187 let c = version_compare(a, b);
188 match op {
189 "<" | "lt" => c == Ordering::Less,
190 "<=" | "le" => c != Ordering::Greater,
191 ">" | "gt" => c == Ordering::Greater,
192 ">=" | "ge" => c != Ordering::Less,
193 "==" | "=" | "eq" => c == Ordering::Equal,
194 "!=" | "<>" | "ne" => c != Ordering::Equal,
195 _ => false,
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn canonical_forms() {
205 assert_eq!(canonicalize("1.0.0-dev"), "1.0.0.dev");
206 assert_eq!(canonicalize("1.0RC1"), "1.0.RC.1");
207 assert_eq!(canonicalize("1.0.0.0-beta2"), "1.0.0.0.beta.2");
208 assert_eq!(canonicalize("9999999-dev"), "9999999.dev");
209 assert_eq!(canonicalize("dev-main"), "dev.main");
210 }
211
212 #[test]
213 fn known_orderings() {
214 assert_eq!(version_compare("1.0.0.0-dev", "1.0.0.0"), Ordering::Less);
215 assert_eq!(
216 version_compare("1.0.0.0-alpha1", "1.0.0.0-beta1"),
217 Ordering::Less
218 );
219 assert_eq!(version_compare("1.0.0.0-RC1", "1.0.0.0"), Ordering::Less);
220 assert_eq!(version_compare("1.0.0.0", "1.0.0.0-patch1"), Ordering::Less);
221 assert_eq!(version_compare("1.0.0.0", "1.0.0"), Ordering::Greater);
222 assert_eq!(version_compare("1.0.0", "1.0.0.0"), Ordering::Less);
223 assert_eq!(version_compare("2.0.0.0", "10.0.0.0"), Ordering::Less);
224 assert_eq!(
225 version_compare("1.9999999.9999999.9999999-dev", "2.0.0.0-dev"),
226 Ordering::Less
227 );
228 assert_eq!(
229 version_compare("1.0.0.0-dev", "1.0.0.0-alpha"),
230 Ordering::Less
231 );
232 assert_eq!(
233 version_compare("1.0.0.0-b", "1.0.0.0-beta1"),
234 Ordering::Less
235 );
236 }
237}