1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
#![cfg(feature = "version")]
use std::io;
/// Parsed `"rustup 1.22.1 (b01adbbc3 2020-07-08)"`, decomposed into:<br>`{ tool_name: "rustup", version: "1.22.1", hash: "b01adbbc3", date: "2020-07-08" }`
#[derive(Clone, Debug)]
#[cfg_attr(doc_cfg, doc(cfg(feature = "version")))]
pub struct Version {
/// The tool name (e.g. `"rustup"`)
pub tool_name: String,
/// The semver (e.g. `"1.22.1"`)
pub version: semver::Version,
/// The hash (typically a git commit, e.g. `"b01adbbc3"`)
pub hash: String,
/// The date (typically in yyyy-mm-dd format, e.g. `"2020-07-08"`)
pub date: String
}
impl Version {
/// Parse input in the format `"{tool_name} {version}"` or `"{tool_name} {version} ({hash} {date})"`
///
/// # Examples
///
/// ```rust
/// # use mmrbi::Version;
/// Version::parse_rusty_version("rustup 1.22.1 (b01adbbc3 2020-07-08)").unwrap();
/// Version::parse_rusty_version("cargo 1.47.0 (f3c7e066a 2020-08-28)").unwrap();
/// Version::parse_rusty_version("rustc 1.47.0 (18bf6b4f0 2020-10-07)").unwrap();
///
/// Version::parse_rusty_version("rustup 1.22.1").unwrap();
/// Version::parse_rusty_version("cargo 1.47.0").unwrap();
/// Version::parse_rusty_version("rustc 1.47.0").unwrap();
/// ```
pub fn parse_rusty_version(line: &str) -> io::Result<Self> {
let mut line = line.trim_end_matches(|ch| "\r\n".contains(ch)).splitn(4, " ");
let tool_name = line.next().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing tool name"))?;
let semver = line.next().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing semver"))?;
let hash = line.next().map_or(Ok(""), parse_hash)?;
let date = line.next().map_or(Ok(""), parse_date)?;
let semver = semver::Version::parse(semver).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, format!("invalid version: {}", err)))?;
Ok(Self{
tool_name: tool_name.into(),
version: semver,
hash: hash.into(),
date: date.into(),
})
}
/// Check if the version is at least the given version
///
/// ```rust
/// # use mmrbi::Version;
/// let cargo_1_47_0_stable = Version::parse_rusty_version("cargo 1.47.0").unwrap();
/// assert_eq!(false, cargo_1_47_0_stable .is_at_least(1, 48, 0));
/// assert_eq!(true, cargo_1_47_0_stable .is_at_least(1, 47, 0));
/// assert_eq!(true, cargo_1_47_0_stable .is_at_least(1, 46, 0));
///
/// let cargo_1_47_0_beta = Version::parse_rusty_version("cargo 1.47.0-beta").unwrap();
/// assert_eq!(false, cargo_1_47_0_beta .is_at_least(1, 48, 0));
/// assert_eq!(false, cargo_1_47_0_beta .is_at_least(1, 47, 0));
/// assert_eq!(true, cargo_1_47_0_beta .is_at_least(1, 46, 0));
///
/// let cargo_1_47_0_nightly = Version::parse_rusty_version("cargo 1.47.0-nightly").unwrap();
/// assert_eq!(false, cargo_1_47_0_nightly .is_at_least(1, 48, 0));
/// assert_eq!(false, cargo_1_47_0_nightly .is_at_least(1, 47, 0));
/// assert_eq!(true, cargo_1_47_0_nightly .is_at_least(1, 46, 0));
/// ```
pub fn is_at_least(&self, major: u64, minor: u64, patch: u64) -> bool {
let self_ver = (self.version.major, self.version.minor, self.version.patch);
let check_ver = (major, minor, patch);
if self.version.is_prerelease() {
self_ver > check_ver
} else {
self_ver >= check_ver
}
}
/// Check if the version is after the given version
///
/// ```rust
/// # use mmrbi::Version;
/// let cargo_1_47_0_stable = Version::parse_rusty_version("cargo 1.47.0").unwrap();
/// assert_eq!(false, cargo_1_47_0_stable .is_after(1, 48, 0));
/// assert_eq!(false, cargo_1_47_0_stable .is_after(1, 47, 0));
/// assert_eq!(true, cargo_1_47_0_stable .is_after(1, 46, 0));
///
/// let cargo_1_47_0_beta = Version::parse_rusty_version("cargo 1.47.0-beta").unwrap();
/// assert_eq!(false, cargo_1_47_0_beta .is_after(1, 48, 0));
/// assert_eq!(false, cargo_1_47_0_beta .is_after(1, 47, 0));
/// assert_eq!(true, cargo_1_47_0_beta .is_after(1, 46, 0));
///
/// let cargo_1_47_0_nightly = Version::parse_rusty_version("cargo 1.47.0-nightly").unwrap();
/// assert_eq!(false, cargo_1_47_0_nightly .is_after(1, 48, 0));
/// assert_eq!(false, cargo_1_47_0_nightly .is_after(1, 47, 0));
/// assert_eq!(true, cargo_1_47_0_nightly .is_after(1, 46, 0));
/// ```
pub fn is_after(&self, major: u64, minor: u64, patch: u64) -> bool {
let self_ver = (self.version.major, self.version.minor, self.version.patch);
let check_ver = (major, minor, patch);
self_ver > check_ver
}
}
impl std::str::FromStr for Version {
type Err = io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { Self::parse_rusty_version(s) }
}
fn parse_hash(hash: &str) -> io::Result<&str> {
if !hash.starts_with("(") {
Err(io::Error::new(io::ErrorKind::InvalidData, format!("expected hash {:?} to be enclosed in a parenthesis", hash)))
} else if hash[1..].chars().any(|ch| !ch.is_ascii_hexdigit()) {
Err(io::Error::new(io::ErrorKind::InvalidData, format!("expected hash {:?} to be exclusively ASCII hexidecimal digits", &hash[1..])))
} else {
Ok(&hash[1..])
}
}
fn parse_date(date: &str) -> io::Result<&str> {
if !date.ends_with(")") {
return Err(io::Error::new(io::ErrorKind::InvalidData, format!("expected date {:?} to be enclosed in a parenthesis", date)));
}
let date = &date[..(date.len()-1)];
if date.chars().any(|ch| !(ch.is_ascii_digit() || ch == '-')) {
Err(io::Error::new(io::ErrorKind::InvalidData, format!("expected date {:?} to be exclusively ASCII digits or '-' separators", date)))
} else {
Ok(date)
}
}