use semver::{Op, Version, VersionReq};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Match {
Affected,
Unaffected,
Undetermined,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Precision {
Major,
Minor,
Exact,
}
struct Locked {
version: Version,
precision: Precision,
}
fn parse_locked(label: &str) -> Option<Locked> {
let bare = label
.strip_prefix('v')
.or_else(|| label.strip_prefix('V'))
.unwrap_or(label);
let (core, suffix) = bare.split_once('-').map_or((bare, ""), |(c, s)| (c, s));
let numeric = |part: &&str| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit());
let parts: Vec<&str> = core.split('.').collect();
if !parts.iter().all(numeric) {
return None;
}
let (padded, precision) = match parts.as_slice() {
[major] => (format!("{major}.0.0"), Precision::Major),
[major, minor] => (format!("{major}.{minor}.0"), Precision::Minor),
[major, minor, patch] => (format!("{major}.{minor}.{patch}"), Precision::Exact),
_ => return None,
};
let full = if suffix.is_empty() {
padded
} else {
format!("{padded}-{suffix}")
};
Version::parse(&full)
.ok()
.map(|version| Locked { version, precision })
}
fn partial_overlap(locked: &Locked, range: &VersionReq) -> bool {
if locked.precision == Precision::Exact {
return false;
}
range.comparators.iter().all(|bound| {
let excludes_line = bound.op == Op::Greater
&& match locked.precision {
Precision::Major => bound.minor.is_none(),
Precision::Minor => bound.patch.is_none(),
Precision::Exact => false,
};
bound.matches(&locked.version)
|| (!excludes_line
&& !matches!(bound.op, Op::Less | Op::LessEq)
&& bound.major == locked.version.major
&& (locked.precision == Precision::Major
|| bound.minor.is_none()
|| bound.minor == Some(locked.version.minor)))
})
}
pub(super) fn matches(label: &str, range: &str) -> Match {
let (Some(locked), Ok(requirement)) = (parse_locked(label), VersionReq::parse(range)) else {
return Match::Undetermined;
};
if requirement.matches(&locked.version) {
return Match::Affected;
}
if !locked.version.pre.is_empty() || partial_overlap(&locked, &requirement) {
return Match::Undetermined;
}
Match::Unaffected
}