use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use color_eyre::eyre::{Report, Result, eyre};
use crate::metadata::structs::Probe;
const BMP_PRODUCT_STRING: &str = "Black Magic Probe";
const BMP_BOOT_STRING: &str = "Black Magic Probe DFU";
const DRAGON_BOOT_STRING: &str = "dragonBoot DFU bootloader";
const BMP_NATIVE: &str = "native";
#[derive(PartialEq, Eq)]
pub struct ProbeIdentity
{
kind: DeviceKind,
pub version: VersionNumber,
}
#[derive(PartialEq, Eq)]
pub enum DeviceKind
{
Probe(Probe),
Bootloader(String),
}
enum ParseNameError
{
OpeningParenthesisAfterClosingParenthesis,
FoundNotMatchedParenthesis,
}
#[derive(Debug)]
enum ParseVersionError
{
FormattingPatternError,
EmptyOrWhitespaceVersion,
}
#[derive(Debug, PartialEq, Eq)]
pub enum VersionNumber
{
Unknown,
Invalid,
GitHash(String),
FullVersion(VersionParts),
}
#[derive(Debug, PartialEq, Eq)]
pub struct VersionParts
{
major: usize,
minor: usize,
revision: usize,
kind: VersionKind,
dirty: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum VersionKind
{
Release,
ReleaseCandidate(usize),
Development(GitVersion),
}
#[derive(Debug, PartialEq, Eq)]
pub struct GitVersion
{
release_candidate: Option<usize>,
commits: usize,
hash: String,
}
impl Display for ParseNameError
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result
{
match self {
ParseNameError::OpeningParenthesisAfterClosingParenthesis => {
write!(f, "A '(' parenthesis is found after a ')'.")
},
ParseNameError::FoundNotMatchedParenthesis => write!(f, "Not a matching pair of parenthesis found."),
}
}
}
impl Display for ParseVersionError
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result
{
match self {
ParseVersionError::FormattingPatternError => write!(
f,
"The version failed to match the pattern of '{} (<version number>)'.",
BMP_PRODUCT_STRING
),
ParseVersionError::EmptyOrWhitespaceVersion => write!(f, "The extracted version is empty or whitespace"),
}
}
}
fn parse_name_from_identity_string(input: &str) -> Result<&str, ParseNameError>
{
let opening_paren = input.find('(');
let closing_paren = input.find(')');
match (opening_paren, closing_paren) {
(None, None) => Ok(BMP_NATIVE),
(Some(opening_paren), Some(closing_paren)) => {
if opening_paren > closing_paren {
Err(ParseNameError::OpeningParenthesisAfterClosingParenthesis)
} else {
Ok(&input[opening_paren + 1..closing_paren])
}
},
(Some(_), None) => Err(ParseNameError::FoundNotMatchedParenthesis),
(None, Some(_)) => Err(ParseNameError::FoundNotMatchedParenthesis),
}
}
fn parse_version_from_identity_string(input: &str) -> Result<&str, ParseVersionError>
{
let start_index = input.rfind(' ').ok_or(ParseVersionError::FormattingPatternError)?;
let version = &input[start_index + 1..];
if version.trim().is_empty() {
return Err(ParseVersionError::EmptyOrWhitespaceVersion);
}
Ok(version)
}
impl TryFrom<&str> for ProbeIdentity
{
type Error = Report;
fn try_from(identity: &str) -> Result<Self>
{
if identity.starts_with(BMP_BOOT_STRING) || identity.starts_with(DRAGON_BOOT_STRING) {
return Ok(ProbeIdentity {
kind: DeviceKind::Bootloader(identity.to_string()),
version: VersionNumber::Unknown,
});
}
if !identity.starts_with(BMP_PRODUCT_STRING) {
return Err(eyre!("Product string doesn't start with '{}'", BMP_PRODUCT_STRING));
}
if identity == BMP_PRODUCT_STRING {
return Ok(ProbeIdentity {
kind: DeviceKind::Probe(Probe::Native),
version: VersionNumber::Unknown,
});
}
let parse_slice = &identity[BMP_PRODUCT_STRING.len()..];
let probe = parse_name_from_identity_string(parse_slice)
.map_err(|error| eyre!("Error while parsing probe string: {}", error))?
.to_lowercase();
let version = parse_version_from_identity_string(parse_slice)
.map_err(|error| eyre!("Error while parsing version string: {}", error))?;
Ok(ProbeIdentity {
kind: DeviceKind::Probe(probe.try_into()?),
version: version.into(),
})
}
}
impl TryFrom<String> for ProbeIdentity
{
type Error = Report;
fn try_from(identity: String) -> Result<Self>
{
identity.as_str().try_into()
}
}
impl Display for ProbeIdentity
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result
{
match &self.kind {
DeviceKind::Probe(probe) => {
write!(f, "{}", BMP_PRODUCT_STRING)?;
if probe != &Probe::Native {
write!(f, " ({})", probe.to_string())?;
}
match &self.version {
VersionNumber::Unknown => Ok(()),
VersionNumber::Invalid => write!(f, " <invalid version>"),
VersionNumber::GitHash(hash) => write!(f, " {}", hash),
VersionNumber::FullVersion(version_parts) => write!(f, " {}", version_parts.to_string()),
}
},
DeviceKind::Bootloader(ident) => write!(f, "{ident}"),
}
}
}
impl ProbeIdentity
{
pub fn variant(&self) -> Option<Probe>
{
match self.kind {
DeviceKind::Probe(probe) => Some(probe),
_ => None,
}
}
pub fn is_bootloader(&self) -> bool
{
matches!(self.kind, DeviceKind::Bootloader(_))
}
}
impl TryFrom<String> for Probe
{
type Error = Report;
fn try_from(value: String) -> Result<Self>
{
match value.as_str() {
"96b carbon" => Ok(Probe::_96bCarbon),
"blackpill-f401cc" => Ok(Probe::BlackpillF401CC),
"blackpill-f401ce" => Ok(Probe::BlackpillF401CE),
"blackpill-f411ce" => Ok(Probe::BlackpillF411CE),
"ctxlink" => Ok(Probe::CtxLink),
"f072-if" => Ok(Probe::F072),
"f3-if" => Ok(Probe::F3),
"f4discovery" => Ok(Probe::F4Discovery),
"hydrabus" => Ok(Probe::HydraBus),
"launchpad icdi" => Ok(Probe::LaunchpadICDI),
BMP_NATIVE => Ok(Probe::Native),
"st-link/v2" => Ok(Probe::Stlink),
"st-link v3" => Ok(Probe::Stlinkv3),
"swlink" => Ok(Probe::Swlink),
_ => Err(eyre!("Probe with unknown product string encountered")),
}
}
}
impl From<&str> for VersionNumber
{
fn from<'a>(value: &str) -> Self
{
if let Some(value) = value.strip_prefix("g") {
VersionNumber::GitHash(value.to_string())
} else if let Some(value) = value.strip_prefix("v") {
let version_parts = VersionParts::try_from(value);
match version_parts {
Ok(version_parts) => VersionNumber::FullVersion(version_parts),
Err(_) => VersionNumber::Invalid,
}
} else {
VersionNumber::Invalid
}
}
}
impl From<&String> for VersionNumber
{
fn from(value: &String) -> Self
{
value.as_str().into()
}
}
impl From<String> for VersionNumber
{
fn from(value: String) -> Self
{
value.as_str().into()
}
}
impl Display for VersionNumber
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result
{
match &self {
VersionNumber::Unknown => write!(f, "<unknown>"),
VersionNumber::Invalid => write!(f, "<invalid version>"),
VersionNumber::GitHash(hash) => write!(f, "{}", hash),
VersionNumber::FullVersion(version_parts) => write!(f, "{}", version_parts.to_string()),
}
}
}
impl PartialOrd for VersionNumber
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering>
{
match self {
Self::Unknown => None,
Self::Invalid => None,
Self::GitHash(lhs) => {
match other {
Self::GitHash(rhs) => {
if lhs == rhs {
Some(Ordering::Equal)
} else {
None
}
},
_ => None,
}
},
Self::FullVersion(lhs) => {
match other {
Self::Unknown | Self::Invalid | Self::GitHash(_) => None,
Self::FullVersion(rhs) => lhs.partial_cmp(rhs),
}
},
}
}
}
impl VersionParts
{
pub fn from_parts(major: usize, minor: usize, revision: usize, kind: VersionKind, dirty: bool) -> Self
{
Self {
major,
minor,
revision,
kind,
dirty,
}
}
}
impl TryFrom<&str> for VersionParts
{
type Error = Report;
fn try_from(value: &str) -> Result<Self>
{
let major_end = value.find('.').unwrap_or(value.len());
let major = value[..major_end].parse::<usize>()?;
let mut value = if major_end == value.len() {
&value[major_end..]
} else {
&value[major_end + 1..]
};
let minor_end = value.find('.').unwrap_or(value.len());
let minor = value[..minor_end].parse::<usize>()?;
value = if minor_end == value.len() {
&value[minor_end..]
} else {
&value[minor_end + 1..]
};
let revision_end = value.find('-').unwrap_or(value.len());
let revision = value[..revision_end].parse::<usize>()?;
value = if revision_end == value.len() {
&value[revision_end..]
} else {
&value[revision_end + 1..]
};
let dirty_begin = value.rfind('-').map(|value| value + 1).unwrap_or(0);
let dirty = &value[dirty_begin..] == "dirty";
if dirty {
value = &value[..dirty_begin];
if value.ends_with('-') {
value = &value[..value.len() - 1];
}
}
let kind = if value.is_empty() {
VersionKind::Release
} else {
let candidate = if value.starts_with("rc") {
let rc_end = value.find('-').unwrap_or(value.len());
let rc_number = value[2..rc_end].parse::<usize>()?;
value = if rc_end == value.len() {
&value[rc_end..]
} else {
&value[rc_end + 1..]
};
Some(rc_number)
} else {
None
};
if !value.is_empty() {
let commits_end = value
.find('-')
.ok_or_else(|| eyre!("Version string has invalid form of Git version tag {:?}", value))?;
let commits = value[..commits_end].parse::<usize>()?;
let hash = value[commits_end + 1..].to_string();
VersionKind::Development(GitVersion {
commits,
hash,
release_candidate: candidate,
})
} else {
candidate.map(VersionKind::ReleaseCandidate).unwrap()
}
};
Ok(Self {
major,
minor,
revision,
kind,
dirty,
})
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for VersionParts
{
fn to_string(&self) -> String
{
let mut version = format!("{}.{}.{}", self.major, self.minor, self.revision);
version += &self.kind.to_string();
if self.dirty {
version += "-dirty";
}
version
}
}
impl PartialOrd for VersionParts
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering>
{
Some(self.cmp(other))
}
}
impl Ord for VersionParts
{
fn cmp(&self, other: &Self) -> Ordering
{
if self.major < other.major {
return Ordering::Less;
} else if self.major > other.major {
return Ordering::Greater;
}
if self.minor < other.minor {
return Ordering::Less;
} else if self.minor > other.minor {
return Ordering::Greater;
}
if self.revision < other.revision {
return Ordering::Less;
} else if self.revision > other.revision {
return Ordering::Greater;
}
if self.kind < other.kind {
return Ordering::Less;
} else if self.kind > other.kind {
return Ordering::Greater;
}
if self.dirty && !other.dirty {
Ordering::Greater
} else if !self.dirty && other.dirty {
Ordering::Less
} else {
Ordering::Equal
}
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for VersionKind
{
fn to_string(&self) -> String
{
match self {
Self::Release => "".into(),
Self::ReleaseCandidate(rc_number) => format!("-rc{}", rc_number),
Self::Development(git_version) => git_version.to_string(),
}
}
}
impl PartialOrd for VersionKind
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering>
{
Some(self.cmp(other))
}
}
impl Ord for VersionKind
{
fn cmp(&self, other: &Self) -> Ordering
{
match self {
Self::Release => {
match other {
Self::Release => Ordering::Equal,
Self::ReleaseCandidate(_) => Ordering::Greater,
Self::Development(_) => Ordering::Less,
}
},
Self::ReleaseCandidate(lhs) => {
match other {
Self::Release => Ordering::Less,
Self::ReleaseCandidate(rhs) => lhs.cmp(rhs),
Self::Development(_) => Ordering::Less,
}
},
Self::Development(lhs) => {
match other {
Self::Development(rhs) => lhs.partial_cmp(rhs).unwrap(),
_ => Ordering::Greater,
}
},
}
}
}
impl GitVersion
{
pub fn from_parts(release_candidate: Option<usize>, commits: usize, hash: String) -> Self
{
Self {
release_candidate,
commits,
hash,
}
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for GitVersion
{
fn to_string(&self) -> String
{
let base_version = match self.release_candidate {
None => "".into(),
Some(rc_number) => format!("-rc{}", rc_number),
};
let git_version = format!("-{}-{}", self.commits, self.hash);
base_version + &git_version
}
}
impl PartialOrd for GitVersion
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering>
{
match self.release_candidate {
Some(lhs_rc_number) => {
match other.release_candidate {
Some(rhs_rc_number) => {
if lhs_rc_number != rhs_rc_number {
return lhs_rc_number.partial_cmp(&rhs_rc_number);
}
},
None => return Some(Ordering::Less),
}
},
None => {
if other.release_candidate.is_some() {
return Some(Ordering::Greater);
}
},
}
if self.commits < other.commits {
Some(Ordering::Less)
} else if self.commits > other.commits {
Some(Ordering::Greater)
} else if self.hash == other.hash {
Some(Ordering::Equal)
} else {
None
}
}
}