1pub const INFINITE: &str = "//INFINITE//";
7const PLATFORM_SPECIFIERS: &[&str] = &["macos", "darwin", "linux", "win", "mingw"];
8
9fn simplify_pre_release(pre: &str) -> Option<String> {
12 if PLATFORM_SPECIFIERS.iter().any(|p| pre.contains(p)) {
13 None
14 } else {
15 Some(pre.to_owned())
16 }
17}
18
19pub fn normalize_version_operators(s: &str) -> String {
22 let mut out = String::with_capacity(s.len());
23 let is_op = |c: char| "<>=!~^".contains(c);
24 let mut in_op_seq = false;
25 for c in s.chars() {
26 if is_op(c) {
27 in_op_seq = true;
28 out.push(c);
29 } else if c == ' ' && in_op_seq {
30 } else {
32 in_op_seq = false;
33 out.push(c);
34 }
35 }
36 out
37}
38
39pub fn normalize_ver(version: &str) -> (Vec<String>, Option<String>) {
42 let (ver_part, pre_release) = version
43 .split_once('-')
44 .map_or((version, None), |(a, b)| (a, Some(b)));
45
46 let ver_owned;
48 let ver_part = if ver_part.contains(':') && !ver_part.contains(['<', '>', '=']) {
49 if let Some((epoch, rest)) = ver_part.split_once(':') {
50 ver_owned = format!("{epoch}.{rest}");
51 ver_owned.as_str()
52 } else {
53 ver_part
54 }
55 } else {
56 ver_part
57 };
58
59 let parts: Vec<String> = ver_part.split(['.', '-']).map(String::from).collect();
60 let pre = pre_release
61 .map(str::to_lowercase)
62 .and_then(|p| simplify_pre_release(&p));
63
64 (parts, pre)
65}
66
67fn is_numeric(s: &str) -> bool {
70 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
71}
72
73fn cmp_numeric_strs(a: &str, b: &str) -> std::cmp::Ordering {
74 let a = a.trim_start_matches('0');
75 let b = b.trim_start_matches('0');
76 match a.len().cmp(&b.len()) {
77 std::cmp::Ordering::Equal => a.cmp(b),
78 other => other,
79 }
80}
81
82fn aux_compare_tokens(t1: &str, t2: &str) -> Option<bool> {
83 if is_numeric(t1) && is_numeric(t2) {
84 return Some(cmp_numeric_strs(t1, t2) == std::cmp::Ordering::Greater);
85 }
86 if is_numeric(t1) || t1 == INFINITE {
87 return Some(true); }
89 if is_numeric(t2) || t2 == INFINITE {
90 return Some(false); }
92 if t1 != t2 {
93 return Some(t1 > t2);
94 }
95 None }
97
98fn aux_compare_suffix(s1: &str, s2: &str) -> Option<bool> {
99 match (s1.is_empty(), s2.is_empty()) {
100 (false, true) => Some(true),
101 (true, false) => Some(false),
102 _ if s1 != s2 => Some(s1 > s2),
103 _ => None,
104 }
105}
106
107fn compare_tokens(t1: &str, t2: &str) -> Option<bool> {
108 if t1.contains('+') || t2.contains('+') {
109 let (num1, suf1) = t1.split_once('+').unwrap_or((t1, ""));
110 let (num2, suf2) = t2.split_once('+').unwrap_or((t2, ""));
111 if is_numeric(num1) && is_numeric(num2) {
112 let ord = cmp_numeric_strs(num1, num2);
113 if ord != std::cmp::Ordering::Equal {
114 return Some(ord == std::cmp::Ordering::Greater);
115 }
116 } else if num1 != num2 {
117 return Some(num1 > num2);
118 }
119 if let Some(r) = aux_compare_suffix(suf1, suf2) {
120 return Some(r);
121 }
122 }
123 aux_compare_tokens(t1, t2)
124}
125
126fn compare_pre_releases(p1: Option<&str>, p2: Option<&str>) -> bool {
127 match (p1, p2) {
128 (None, Some(_)) => true, (Some(a), Some(b)) => a > b,
131 _ => false,
132 }
133}
134
135fn resolve_equal_parts(
136 v1_pre: Option<&str>,
137 v2_pre: Option<&str>,
138 v1: &[String],
139 v2: &[String],
140 include_same: bool,
141) -> bool {
142 if v2_pre.is_none() && v2.iter().all(|s| s == "0") && v1.iter().all(|s| s == "0") {
143 return true;
144 }
145 if let (Some(a), Some(b)) = (v1_pre, v2_pre) {
146 if a != b {
147 return compare_pre_releases(Some(a), Some(b));
148 }
149 }
150 if include_same {
151 return v1_pre.is_none() && v2_pre.is_none() || compare_pre_releases(v1_pre, v2_pre);
152 }
153 false
154}
155
156pub fn compare_versions(version1: &str, version2: &str, include_same: bool) -> bool {
159 let (v1, v1_pre) = normalize_ver(version1);
160 let (v2, v2_pre) = normalize_ver(version2);
161
162 if include_same && v1 == v2 && v1_pre.as_deref() == v2_pre.as_deref() {
163 return true;
164 }
165
166 let max_len = v1.len().max(v2.len());
167 for i in 0..max_len {
168 let p1 = v1.get(i).map_or("0", String::as_str);
169 let p2 = v2.get(i).map_or("0", String::as_str);
170 if p1 != p2 {
171 if let Some(result) = compare_tokens(p1, p2) {
172 return result;
173 }
174 }
175 }
176
177 resolve_equal_parts(v1_pre.as_deref(), v2_pre.as_deref(), &v1, &v2, include_same)
178}
179
180fn parse_range_pairs(s: &str) -> Vec<(String, String)> {
185 let mut pairs = Vec::new();
186 let bytes = s.as_bytes();
187 let mut i = 0;
188 while let Some(&b) = bytes.get(i) {
189 if b != b'<' && b != b'>' {
190 i = i.saturating_add(1);
191 continue;
192 }
193 let op_start = i;
194 i = i.saturating_add(1);
195 if bytes.get(i) == Some(&b'=') {
196 i = i.saturating_add(1);
197 }
198 let op = s[op_start..i].to_string();
199 while bytes.get(i) == Some(&b' ') {
200 i = i.saturating_add(1);
201 }
202 let val_start = i;
203 while matches!(bytes.get(i), Some(&c) if c != b'<' && c != b'>' && c != b'=' && c != b' ') {
204 i = i.saturating_add(1);
205 }
206 if val_start < i {
207 pairs.push((op, s[val_start..i].to_string()));
208 }
209 }
210 pairs
211}
212
213pub fn parse_version_range(version_range: &str) -> Option<(String, String, String, String)> {
216 let mut range = version_range.to_owned();
217 if range.starts_with('<') {
218 range = format!(">0 {range}");
219 }
220 if range.starts_with('>') && !range.contains('<') {
221 range = format!("{range} <{INFINITE}");
222 }
223 let pairs = parse_range_pairs(&range);
224 if let [(op1, val1), (op2, val2)] = pairs.as_slice() {
225 Some((op1.clone(), val1.clone(), op2.clone(), val2.clone()))
226 } else {
227 tracing::error!("Invalid version range: {version_range}. Values cannot be parsed.");
228 None
229 }
230}
231
232pub fn is_single_version(s: &str) -> bool {
233 !s.contains('<') && !s.contains('>')
234}
235
236pub fn is_single_version_in_range(version: &str, range: &str) -> bool {
237 let v = version.trim_start_matches('=');
238 let Some((op1, min2, op2, max2)) = parse_version_range(range) else {
239 return false;
240 };
241 compare_versions(v, &min2, op1.contains('=')) && compare_versions(&max2, v, op2.contains('='))
242}
243
244pub fn do_ranges_intersect(r1: &str, r2: &str) -> bool {
245 let sv1 = is_single_version(r1);
246 let sv2 = is_single_version(r2);
247 if sv1 && sv2 {
248 return r1 == r2;
249 }
250 if sv1 {
251 return is_single_version_in_range(r1, r2);
252 }
253 if sv2 {
254 return is_single_version_in_range(r2, r1);
255 }
256 let Some((op1_r1, lower1, op2_r1, upper1)) = parse_version_range(r1) else {
257 return false;
258 };
259 let Some((op1_r2, lower2, op2_r2, upper2)) = parse_version_range(r2) else {
260 return false;
261 };
262 let inc1 = op1_r1.contains('=') && op2_r2.contains('=');
263 let inc2 = op1_r2.contains('=') && op2_r1.contains('=');
264 compare_versions(&upper2, &lower1, inc1) && compare_versions(&upper1, &lower2, inc2)
265}
266
267pub fn increment_version(version: &str, position: usize) -> String {
270 let mut parts: Vec<String> = version.split('.').map(String::from).collect();
271 let position = if parts.len() < 2 {
272 while parts.len() < 2 {
273 parts.push("0".to_owned());
274 }
275 if position > 1 {
276 position.saturating_sub(1)
277 } else {
278 position
279 }
280 } else {
281 position
282 };
283 if let Some(p) = parts.get_mut(position) {
284 INFINITE.clone_into(p);
285 for part in parts.iter_mut().skip(position.saturating_add(1)) {
286 "0".clone_into(part);
287 }
288 }
289 parts.join(".")
290}
291
292fn simplify_final_version(version: &str) -> String {
293 let v = if let Some((base, _)) = version.rsplit_once('+') {
295 base
296 } else {
297 version
298 };
299 let lower = v.to_lowercase();
300 let parts: Vec<&str> = lower.split('.').collect();
301 if parts.last().is_some_and(|p| p.contains("final")) {
302 parts
304 .split_last()
305 .map_or_else(String::new, |(_, init)| init.join("."))
306 } else {
307 v.to_owned()
308 }
309}
310
311fn convert_asterisk_to_range(version: &str) -> String {
312 let parts: Vec<&str> = version.split('.').collect();
313 match parts.as_slice() {
314 [major, rest @ ..] if rest.last() == Some(&"*") => match rest.len() {
315 1 => format!(">={major}.0.0 <{major}.{INFINITE}.0"),
316 2.. => {
317 if let [minor, ..] = rest {
318 format!(">={major}.{minor}.0 <{major}.{minor}.{INFINITE}")
319 } else {
320 version.to_owned()
321 }
322 }
323 _ => version.to_owned(),
324 },
325 _ => version.to_owned(),
326 }
327}
328
329pub fn convert_semver_to_range(version: &str) -> String {
333 let version = version.replace("==", "=").replace(' ', "");
334
335 if let Some(rest) = version.strip_prefix('~') {
336 let (inner, position) = rest.strip_prefix(['=', '>']).map_or_else(
338 || {
339 let inner = if rest.split('.').count() == 2 {
340 format!("{rest}.0")
341 } else {
342 rest.to_owned()
343 };
344 (inner, 2usize)
345 },
346 |inner| {
347 let dot_count = inner.split('.').count();
348 let pos = if dot_count == 2 { 1 } else { 2 };
349 (inner.to_owned(), pos)
350 },
351 );
352 return format!(">={inner} <{}", increment_version(&inner, position));
353 }
354
355 if let Some(rest) = version.strip_prefix('^') {
356 return format!(">={rest} <{}", increment_version(rest, 1));
357 }
358
359 if version.contains(".*") {
360 return convert_asterisk_to_range(&version);
361 }
362
363 if is_single_version(&version) && !version.starts_with('=') {
364 return format!("={}", simplify_final_version(&version));
365 }
366
367 simplify_final_version(&version)
368}
369
370pub fn convert_to_range(version: &str) -> Vec<String> {
371 let normalized = normalize_version_operators(version);
372 normalized
373 .split(|c: char| c == ',' || c.is_whitespace())
374 .filter(|s| !s.is_empty())
375 .map(|s| convert_semver_to_range(s.trim()))
376 .collect()
377}
378
379fn match_version_ranges(dep_version: &str, vulnerable_version: &str) -> bool {
380 let and_ranges = convert_to_range(dep_version);
381 if and_ranges.is_empty() {
382 return false;
383 }
384 vulnerable_version.split("||").any(|vuln| {
385 let normalized = vuln.trim().replace(',', " ");
386 let vuln_range = convert_semver_to_range(&normalized);
387 and_ranges
388 .iter()
389 .all(|ar| do_ranges_intersect(ar, &vuln_range))
390 })
391}
392
393pub fn match_vulnerable_versions(dep_version: &str, advisory_range: Option<&str>) -> bool {
398 let Some(advisory_range) = advisory_range else {
399 return false;
400 };
401 if dep_version.is_empty() {
402 return false;
403 }
404 let dep_version = normalize_version_operators(dep_version);
405 let normalized = dep_version.replace("||", "|");
406 normalized
407 .split('|')
408 .any(|dv| match_version_ranges(dv.trim(), advisory_range))
409}
410
411#[cfg(test)]
415mod tests {
416 use super::*;
417
418 macro_rules! mvv {
421 ($dep:expr, $adv:expr, $expected:expr) => {
422 assert_eq!(
423 match_vulnerable_versions($dep, $adv),
424 $expected,
425 "match_vulnerable_versions({:?}, {:?})",
426 $dep,
427 $adv,
428 );
429 };
430 }
431
432 #[test]
433 #[allow(clippy::too_many_lines)]
434 fn test_match_vulnerable_versions() {
435 mvv!("^1.0.0", Some("<0.0"), false);
436 mvv!("^7.0.0", Some("=6.12.2"), false);
437 mvv!("^7.0.0", Some("=6.12.2 || =6.9.1"), false);
438 mvv!("~2.2.3", Some(">=3.0.0 <=4.0.0"), false);
439 mvv!("=2.2", Some(">=2.3.0 <=2.4.0"), false);
440 mvv!("~0.8.0", Some(">=0 <=1.8.6"), true);
441 mvv!("1.8.0", Some(">=0 <=0.3.0 || >=1.0.1 <=1.8.6"), true);
442 mvv!("^2.1.0", Some(">=0 <11.0.5 || >=11.1.0 <11.1.0"), true);
443 mvv!("2.1.0", Some("~2"), true);
444 mvv!("=2.3.0-pre", Some(">=2.1.1 <2.3.0"), false);
445 mvv!("=2.3.0-pre", Some(">=2.3.0 <2.7.0"), false);
446 mvv!("=2.2.0-rc1", Some(">=2.1.1 <2.3.0"), true);
447 mvv!("=2.1.0-pre", Some("=2.1.0-pre"), true);
448 mvv!("=2.1.0-pre", Some("=2.1.0"), false);
449 mvv!(
450 ">2.1.1 <=2.3.0",
451 Some("<2.1.0||=2.3.0-pre||>=2.4.0 <2.5.0"),
452 true
453 );
454 mvv!(">2.1.1 <2.3.0", Some("<2.1.0||=2.3.1-pre"), false);
455 mvv!("1.0.0-beta.8", Some("<=1.0.0-beta.6"), false);
456 mvv!("1.0.0-beta.4", Some("<=1.0.0-beta.6"), true);
457 mvv!("^1.0.0-rc.10", Some(">2.0.0 <=4.0.0"), false);
458 mvv!("^1.0.0-rc.10", Some(">=1.0.0 <=2.0.0"), true);
459 mvv!(
460 "^7.23.2",
461 Some(">=0 <7.23.2 || >=8.0.0-alpha.0 <8.0.0-alpha.4"),
462 false
463 );
464 mvv!("7.23.2", Some(">=0 <=7.23.2"), true);
465 mvv!("7.23.2", Some(">=6.5.1"), true);
466 mvv!("=7.23.2", Some(">=6.5.1"), true);
467 mvv!(">=11.1", Some(">=0 <12.3.3"), true);
468 mvv!("^1.2.0", Some(">=0 <1.0.3"), false);
469 mvv!("2.0.0||^3.0.0", Some(">=3.0.0"), true);
470 mvv!("3.*", Some(">=3.2.0 <4.0.0"), true);
471 mvv!("4.0", Some("=3.5.1 || =4.0 || =5.0"), true);
472 mvv!("4.2.2.RELEASE", Some(">0 <4.2.16"), true);
473 mvv!("2.13.14", Some(">0 <2.13.14-1"), false);
474 mvv!("8.4", Some(">=0 <7.6.3 || >=8.0.0 <8.4.0"), false);
475 mvv!("6.1.5.Final", Some(">=6.1.2 <6.1.5"), false);
476 mvv!("6.1.5.Final", Some(">=6.1.2 <=6.1.5"), true);
477 mvv!("==3.0.0 || >=4.0.1 <4.0.2 || ==4.0.1", Some("=3.0.0"), true);
478 mvv!("1.16.5-x86_64-darwin", Some("<1.16.5"), false);
479 mvv!("1.16.5-x86_64-mingw-10", Some("<1.16.5"), false);
480 mvv!("1.16.5-aarch64-linux", Some("<=1.16.5"), true);
481 mvv!("0.0.0-20221012-56ae", Some(">=0.0.0 <0.17.0"), true);
482 mvv!("0.0.0-20221012-56ae", Some("<0.17.0"), true);
483 mvv!("=0.10.0-20221012-e7cb96979f69", Some("<0.10.0"), false);
484 mvv!("=0.10.0-20221012-e7cb96979f69", Some("<=0.10.0"), true);
485 mvv!("${lombokVersion}", Some(">0"), false);
486 mvv!("", Some(">0"), false);
487 mvv!("0.0.0", None::<&str>, false);
488 mvv!("2.*,<2.3", Some(">=2.0.1"), true);
489 mvv!("2.*,<2.3", Some(">=1.3.0 <2.0.0"), false);
490 mvv!("1.2.0", Some(">=1.0.0,<=2.0.0"), true);
491 mvv!("1.2.0", Some(">=1.0.0, <=2.0.0"), true);
492 mvv!("3.2.0+incompatible", Some(">1.0.0 <=3.2.0"), true);
493 mvv!(">= 3.1.44 , < 3.2.0", Some(">=3.1.0"), true);
494 mvv!(">= 3.1.44 < 3.2.0", Some(">=3.1.0"), true);
495 mvv!("1.2.3 <=2.0.0", Some(">=1.0.0"), true);
496 mvv!("4.0.0", Some(">=3,<5"), true);
497 mvv!("3.0.0", Some(">=3,<5"), true);
498 mvv!("2.9.9", Some(">=3,<5"), false);
499 mvv!("5.0.0", Some(">=3,<5"), false);
500 mvv!("3.0.0", Some(">3,<5"), false);
501 mvv!("4.0.0", Some(">=3,<=4"), true);
502 mvv!("4.0.0", Some(">=3,<4"), false);
503 }
504
505 #[test]
506 fn test_match_vulnerable_versions_rpm_epoch() {
507 mvv!("0:2.35.2-42.el9", Some("<=0:2.35.2-42.el9"), true);
508 mvv!("0:2.35.2-63.el9", Some("<=0:2.35.2-42.el9"), false);
509 mvv!("0:2.35.2-30.el9", Some("<=0:2.35.2-42.el9"), true);
510 mvv!("0:3.8.3-6.el9", Some("<0:3.8.3-6.el9_6.2"), true);
511 mvv!("0:1.2.3", Some("<=0:1.2.3"), true);
512 mvv!("0:1.2.3", Some("<0:1.2.3"), false);
513 mvv!("0:1.2.3", Some(">=0:1.2.3"), true);
514 mvv!("0:1.2.3", Some(">0:1.2.3"), false);
515 mvv!("1:2.3.4", Some("==0:2.3.4"), false);
516 mvv!("0:2.3.4", Some("==1:2.3.4"), false);
517 mvv!("0:2.35.2-42.el9", Some("==0:2.35.2-42.el9"), true);
518 mvv!("0:2.35.2-42.el9", Some("==0:2.35.2-63.el9"), false);
519 mvv!("0:2.35.2-63.el9", Some("==0:2.35.2-42.el9"), false);
520 }
521
522 #[test]
523 fn test_match_vulnerable_versions_rhel_major_minor() {
524 mvv!("0:3.8.3-6.el9", Some("<0:3.8.3-6.el9_6.2"), true);
525 mvv!("0:3.8.3-6.el9_3", Some("<0:3.8.3-6.el9_6"), true);
526 mvv!("0:3.8.3-6.el9_6", Some("<0:3.8.3-6.el9_6.2"), true);
527 mvv!("0:2.35.2-42.el8_5", Some("<0:2.35.2-42.el8_8"), true);
528 mvv!("0:1.2.3-4.el7_6", Some("<0:1.2.3-4.el7_9"), true);
529 mvv!("0:3.8.3-6.el9_6", Some("<0:3.8.3-6.el9"), false);
530 mvv!("0:3.8.3-6.el9_6.2", Some("<0:3.8.3-6.el9_6"), false);
531 mvv!("0:3.8.3-6.el9_6", Some("<=0:3.8.3-6.el9_6"), true);
532 }
533
534 macro_rules! csr {
537 ($input:expr, $expected:expr) => {
538 assert_eq!(
539 convert_semver_to_range($input),
540 $expected,
541 "convert_semver_to_range({:?})",
542 $input,
543 );
544 };
545 }
546
547 #[test]
548 fn test_convert_semver_to_range() {
549 csr!("~1.4.2", ">=1.4.2 <1.4.//INFINITE//");
551 csr!("~1.4", ">=1.4.0 <1.4.//INFINITE//");
552 csr!("~=1.4.2", ">=1.4.2 <1.4.//INFINITE//");
554 csr!("~=1.4", ">=1.4 <1.//INFINITE//");
555 csr!("~>1.4.2", ">=1.4.2 <1.4.//INFINITE//");
557 csr!("~>1.4", ">=1.4 <1.//INFINITE//");
558 csr!("~=1", ">=1 <1.//INFINITE//");
560 csr!("~>1", ">=1 <1.//INFINITE//");
561 csr!("~1", ">=1 <1.//INFINITE//");
562 csr!("^1", ">=1 <1.//INFINITE//");
564 csr!("^2", ">=2 <2.//INFINITE//");
565 }
566
567 #[test]
570 fn test_do_ranges_intersect_unparseable_range() {
571 assert!(!do_ranges_intersect(">=", ">= 3.4.0"));
573 }
574
575 #[test]
576 fn test_is_single_version_in_range_unparseable_range() {
577 assert!(!is_single_version_in_range("1.0.0", ">"));
579 }
580}