///|
priv enum VersionComparator {
Less(Array[Int32])
LessOrEqual(Array[Int32])
Greater(Array[Int32])
GreaterOrEqual(Array[Int32])
Equal(Array[Int32])
}
///|
/// Match a stable three-part numeric version against an affected range.
///
/// Supported comparators are `<`, `<=`, `>`, `>=`, `=`, and bare exact
/// versions. Space-separated terms form an intersection and `||` forms a
/// union. Pre-release versions, build metadata, and malformed ranges return
/// `Uncomparable` rather than being treated as vulnerable.
pub fn match_version(version : String, affected : String) -> FindingStatus {
match (parse_version(version), parse_ranges(affected)) {
(Some(current), Some(ranges)) => {
for range in ranges {
if range.all(fn(comparator) { matches_comparator(current, comparator) }) {
return Affected
}
}
NotAffected
}
_ => Uncomparable
}
}
///|
fn is_supported_affected_range(affected : String) -> Bool {
parse_ranges(affected) is Some(_)
}
///|
fn is_stable_version(version : String) -> Bool {
parse_version(version) is Some(_)
}
///|
fn parse_ranges(text : String) -> Array[Array[VersionComparator]]? {
let ranges : Array[Array[VersionComparator]] = []
for alternative in text.split("||") {
let expression = alternative.trim().to_owned()
if expression == "" {
return None
}
if expression == "*" {
ranges.push([])
continue
}
let comparators : Array[VersionComparator] = []
for part in expression.split(" ") {
let term = part.trim().to_owned()
if term != "" {
match parse_comparator(term) {
Some(comparator) => comparators.push(comparator)
None => return None
}
}
}
if comparators.length() == 0 {
return None
}
ranges.push(comparators)
}
Some(ranges)
}
///|
fn parse_comparator(text : String) -> VersionComparator? {
if text.has_prefix(">=") {
parse_version(text[2:].to_owned()).map(fn(version) {
GreaterOrEqual(version)
})
} else if text.has_prefix("<=") {
parse_version(text[2:].to_owned()).map(fn(version) { LessOrEqual(version) })
} else if text.has_prefix(">") {
parse_version(text[1:].to_owned()).map(fn(version) { Greater(version) })
} else if text.has_prefix("<") {
parse_version(text[1:].to_owned()).map(fn(version) { Less(version) })
} else if text.has_prefix("=") {
parse_version(text[1:].to_owned()).map(fn(version) { Equal(version) })
} else {
parse_version(text).map(fn(version) { Equal(version) })
}
}
///|
fn parse_version(version : String) -> Array[Int32]? {
let raw = version.trim().to_owned()
if raw.contains("-") || raw.contains("+") {
return None
}
let pieces = raw.split(".").to_array()
if pieces.length() != 3 {
return None
}
let values : Array[Int32] = []
for piece in pieces {
match parse_numeric_component(piece.to_owned()) {
Some(value) => values.push(value)
None => return None
}
}
Some(values)
}
///|
fn parse_numeric_component(component : String) -> Int32? {
if component == "" ||
component.length() > 10 ||
(component.length() > 1 && component[0] == '0') {
return None
}
let mut value = 0
for char in component {
match digit_value(char) {
Some(digit) => {
if value > 214748364 || (value == 214748364 && digit > 7) {
return None
}
value = value * 10 + digit
}
None => return None
}
}
Some(value)
}
///|
fn digit_value(char : Char) -> Int32? {
match char {
'0' => Some(0)
'1' => Some(1)
'2' => Some(2)
'3' => Some(3)
'4' => Some(4)
'5' => Some(5)
'6' => Some(6)
'7' => Some(7)
'8' => Some(8)
'9' => Some(9)
_ => None
}
}
///|
fn matches_comparator(
version : Array[Int32],
comparator : VersionComparator,
) -> Bool {
match comparator {
Less(bound) => compare_versions(version, bound) < 0
LessOrEqual(bound) => compare_versions(version, bound) <= 0
Greater(bound) => compare_versions(version, bound) > 0
GreaterOrEqual(bound) => compare_versions(version, bound) >= 0
Equal(bound) => compare_versions(version, bound) == 0
}
}
///|
fn compare_versions(left : Array[Int32], right : Array[Int32]) -> Int32 {
for index in 0..<3 {
if left[index] < right[index] {
return -1
}
if left[index] > right[index] {
return 1
}
}
0
}