use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
const AOSCX_PREFIXES: &[&str] = &["FL.", "GL.", "LL.", "ML.", "DL.", "PL.", "QL."];
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FirmwareVersion {
pub family_prefix: Option<String>,
pub major: u32,
pub minor: u32,
pub patch: u32,
pub build: u32,
pub suffix: Option<String>,
}
impl FirmwareVersion {
pub fn parse(s: &str) -> Result<Self> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(Error::BadFirmwareVersion(s.to_owned()));
}
let (family_prefix, body) = AOSCX_PREFIXES
.iter()
.find_map(|p| {
trimmed
.strip_prefix(p)
.map(|rest| (Some(p.trim_end_matches('.').to_owned()), rest))
})
.unwrap_or((None, trimmed));
let normalized = body.replace('(', ".").replace(')', "");
let (numeric, suffix) = split_suffix(&normalized);
let parts: Vec<&str> = numeric.split('.').collect();
if parts.is_empty() || parts[0].is_empty() {
return Err(Error::BadFirmwareVersion(s.to_owned()));
}
let parse_u32 = |idx: usize| -> Result<u32> {
parts
.get(idx)
.copied()
.unwrap_or("0")
.parse::<u32>()
.map_err(|_| Error::BadFirmwareVersion(s.to_owned()))
};
Ok(Self {
family_prefix,
major: parse_u32(0)?,
minor: parse_u32(1)?,
patch: parse_u32(2)?,
build: parse_u32(3)?,
suffix,
})
}
}
fn split_suffix(s: &str) -> (String, Option<String>) {
for (i, ch) in s.char_indices() {
if !ch.is_ascii_digit() && ch != '.' {
let (head, tail) = s.split_at(i);
return (head.trim_end_matches('.').to_owned(), Some(tail.to_owned()));
}
}
(s.to_owned(), None)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionRange {
clauses: Vec<Vec<Bound>>,
wildcard: bool,
raw: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Bound {
Ge(FirmwareVersion),
Gt(FirmwareVersion),
Le(FirmwareVersion),
Lt(FirmwareVersion),
Eq(FirmwareVersion),
}
impl VersionRange {
pub fn parse(expr: &str) -> Result<Self> {
let trimmed = expr.trim();
if trimmed == "*" {
return Ok(Self {
clauses: vec![],
wildcard: true,
raw: expr.to_owned(),
});
}
let mut clauses = Vec::new();
for disj in trimmed.split("||") {
let mut conj = Vec::new();
for piece in disj.split(',') {
conj.push(
parse_bound(piece.trim()).map_err(|reason| Error::BadVersionRange {
expr: expr.to_owned(),
reason,
})?,
);
}
if !conj.is_empty() {
clauses.push(conj);
}
}
if clauses.is_empty() {
return Err(Error::BadVersionRange {
expr: expr.to_owned(),
reason: "empty range".to_owned(),
});
}
Ok(Self {
clauses,
wildcard: false,
raw: expr.to_owned(),
})
}
pub fn matches(&self, v: &FirmwareVersion) -> bool {
if self.wildcard {
return true;
}
self.clauses
.iter()
.any(|conj| conj.iter().all(|b| b.matches(v)))
}
pub fn specificity(&self) -> usize {
if self.wildcard {
return 0;
}
self.clauses.iter().map(|c| c.len()).sum()
}
pub fn as_str(&self) -> &str {
&self.raw
}
}
fn parse_bound(s: &str) -> std::result::Result<Bound, String> {
let (op, rest) = if let Some(r) = s.strip_prefix(">=") {
(">=", r)
} else if let Some(r) = s.strip_prefix("<=") {
("<=", r)
} else if let Some(r) = s.strip_prefix(">") {
(">", r)
} else if let Some(r) = s.strip_prefix("<") {
("<", r)
} else if let Some(r) = s.strip_prefix("=") {
("=", r)
} else {
("=", s)
};
let v = FirmwareVersion::parse(rest.trim())
.map_err(|e| format!("can't parse '{}': {}", rest.trim(), e))?;
Ok(match op {
">=" => Bound::Ge(v),
">" => Bound::Gt(v),
"<=" => Bound::Le(v),
"<" => Bound::Lt(v),
_ => Bound::Eq(v),
})
}
impl Bound {
fn matches(&self, v: &FirmwareVersion) -> bool {
let other = match self {
Bound::Ge(b) | Bound::Gt(b) | Bound::Le(b) | Bound::Lt(b) | Bound::Eq(b) => b,
};
if let (Some(a), Some(b)) = (&v.family_prefix, &other.family_prefix) {
if a != b {
return false;
}
}
let cmp = (v.major, v.minor, v.patch, v.build).cmp(&(
other.major,
other.minor,
other.patch,
other.build,
));
match self {
Bound::Ge(_) => cmp.is_ge(),
Bound::Gt(_) => cmp.is_gt(),
Bound::Le(_) => cmp.is_le(),
Bound::Lt(_) => cmp.is_lt(),
Bound::Eq(_) => cmp.is_eq(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_plain_semver() {
let v = FirmwareVersion::parse("17.6.4").unwrap();
assert_eq!(v.major, 17);
assert_eq!(v.minor, 6);
assert_eq!(v.patch, 4);
assert_eq!(v.family_prefix, None);
}
#[test]
fn parses_aoscx_family_prefix() {
let v = FirmwareVersion::parse("FL.10.13.1000").unwrap();
assert_eq!(v.family_prefix.as_deref(), Some("FL"));
assert_eq!(v.major, 10);
assert_eq!(v.minor, 13);
assert_eq!(v.patch, 1000);
}
#[test]
fn parses_cisco_parens() {
let v = FirmwareVersion::parse("9.3(5)").unwrap();
assert_eq!((v.major, v.minor, v.patch), (9, 3, 5));
}
#[test]
fn wildcard_matches_anything() {
let r = VersionRange::parse("*").unwrap();
assert!(r.matches(&FirmwareVersion::parse("1.0.0").unwrap()));
assert!(r.matches(&FirmwareVersion::parse("FL.10.13.1000").unwrap()));
}
#[test]
fn ge_lt_range_matches_inside() {
let r = VersionRange::parse(">=15.0,<17.0").unwrap();
assert!(r.matches(&FirmwareVersion::parse("15.0.0").unwrap()));
assert!(r.matches(&FirmwareVersion::parse("16.6.4").unwrap()));
assert!(!r.matches(&FirmwareVersion::parse("17.0.0").unwrap()));
assert!(!r.matches(&FirmwareVersion::parse("14.9.99").unwrap()));
}
}