use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, Clone)]
pub struct FirmwareVersion {
raw: String,
components: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseFirmwareError {
input: String,
}
impl fmt::Display for ParseFirmwareError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid firmware version: {:?}", self.input)
}
}
impl std::error::Error for ParseFirmwareError {}
impl FirmwareVersion {
pub fn parse(s: &str) -> Result<Self, ParseFirmwareError> {
let raw = s.trim();
let err = || ParseFirmwareError {
input: s.to_string(),
};
let body = raw.strip_prefix(['v', 'V']).unwrap_or(raw);
if body.is_empty() {
return Err(err());
}
let mut components = body
.split('.')
.map(|part| part.parse::<u32>().map_err(|_| err()))
.collect::<Result<Vec<u32>, _>>()?;
while components.last() == Some(&0) {
components.pop();
}
Ok(Self {
raw: raw.to_string(),
components,
})
}
pub fn components(&self) -> &[u32] {
&self.components
}
}
impl PartialEq for FirmwareVersion {
fn eq(&self, other: &Self) -> bool {
self.components == other.components
}
}
impl Eq for FirmwareVersion {}
impl PartialOrd for FirmwareVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FirmwareVersion {
fn cmp(&self, other: &Self) -> Ordering {
self.components.cmp(&other.components)
}
}
impl std::hash::Hash for FirmwareVersion {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.components.hash(state);
}
}
impl serde::Serialize for FirmwareVersion {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.raw)
}
}
impl fmt::Display for FirmwareVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw)
}
}
impl std::str::FromStr for FirmwareVersion {
type Err = ParseFirmwareError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fw(s: &str) -> FirmwareVersion {
FirmwareVersion::parse(s).expect("should parse")
}
#[test]
fn parses_four_component_version() {
let v = fw("01.05.00.00");
assert_eq!(v.components(), &[1, 5]); }
#[test]
fn accepts_optional_v_prefix() {
assert_eq!(fw("v01.05.00"), fw("01.05.00"));
assert_eq!(fw("V01.05.00"), fw("01.05.00"));
}
#[test]
fn trailing_zero_components_are_equal() {
assert_eq!(fw("01.05"), fw("01.05.00.00"));
assert_eq!(fw("01.05.00"), fw("01.05.00.00"));
}
#[test]
fn orders_by_numeric_component() {
assert!(fw("01.04.99.00") < fw("01.05.00.00"));
assert!(fw("01.05.00.00") < fw("01.05.01.00"));
assert!(fw("01.05.00.00") < fw("01.05.00.02"));
assert!(fw("01.05.01") > fw("01.05.00.02"));
}
#[test]
fn developer_mode_threshold_comparison() {
let threshold = fw("01.05.00");
assert!(fw("01.04.00.00") < threshold);
assert!(fw("01.05.00.00") >= threshold);
assert!(fw("01.06.02.00") >= threshold);
}
#[test]
fn display_round_trips_original_string() {
assert_eq!(fw("01.05.00.00").to_string(), "01.05.00.00");
}
#[test]
fn rejects_non_numeric() {
assert!(FirmwareVersion::parse("not-a-version").is_err());
assert!(FirmwareVersion::parse("01.x.00").is_err());
assert!(FirmwareVersion::parse("").is_err());
}
}