rido 0.5.4

Fetch valid URLs and checksums of Microsoft Operating Systems.
Documentation
use std::fmt;
use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter};
use thiserror::Error;

#[cfg(feature = "consumer")]
mod consumer;
#[cfg(feature = "consumer")]
pub use consumer::{ConsumerLanguage, ConsumerRelease};

#[cfg(feature = "enterprise")]
mod enterprise;
#[cfg(feature = "enterprise")]
pub use enterprise::{EnterpriseLanguage, EnterpriseRelease};

#[derive(Debug, Clone)]
pub struct WindowsData {
    pub info: WindowsEntry,
    pub url: String,
    pub hash: Option<String>,
}

impl WindowsData {
    pub fn new<R, L, A>(release: R, lang: L, arch: A) -> Result<Self, RidoError>
    where
        WindowsLanguage: TryFrom<(WindowsRelease, L), Error = RidoError>,
        R: TryInto<WindowsRelease, Error = RidoError>,
        A: TryInto<WindowsArchitecture, Error = RidoError>,
    {
        let release = release.try_into()?;
        let lang = (release, lang).try_into()?;
        let arch = arch.try_into()?;
        WindowsEntry { release, lang, arch }.try_into()
    }
}

#[derive(PartialEq, Debug, Clone)]
pub struct WindowsEntry {
    pub release: WindowsRelease,
    pub arch: WindowsArchitecture,
    pub lang: WindowsLanguage,
}

impl WindowsEntry {
    pub fn list_all() -> Vec<Self> {
        let mut entries = Vec::new();

        #[cfg(feature = "consumer")]
        entries.extend(ConsumerRelease::iter().flat_map(move |rel| {
            let release: WindowsRelease = rel.into();
            ConsumerLanguage::iter()
                .filter(move |&lang| lang.validate(release))
                .flat_map(move |lang| {
                    let lang = lang.into();
                    WindowsArchitecture::iter()
                        .filter(move |&arch| rel.validate(arch))
                        .map(move |arch| Self { release, lang, arch })
                })
        }));

        #[cfg(feature = "enterprise")]
        entries.extend(EnterpriseRelease::iter().flat_map(move |rel| {
            let release: WindowsRelease = rel.into();
            EnterpriseLanguage::iter()
                .filter(move |&lang| lang.validate(release))
                .flat_map(move |lang| {
                    let lang = lang.into();
                    WindowsArchitecture::iter()
                        .filter(move |&arch| rel.validate(arch))
                        .map(move |arch| Self { release, lang, arch })
                })
        }));

        entries
    }
}

impl TryFrom<WindowsEntry> for WindowsData {
    type Error = RidoError;
    fn try_from(entry: WindowsEntry) -> Result<Self, Self::Error> {
        let (url, hash) = match (entry.release, entry.lang) {
            #[cfg(feature = "consumer")]
            (WindowsRelease::Consumer(release), WindowsLanguage::Consumer(lang)) => consumer::get_consumer_info(release, lang, entry.arch)?,
            #[cfg(feature = "enterprise")]
            (WindowsRelease::Enterprise(release), WindowsLanguage::Enterprise(lang)) => enterprise::get_enterprise_info(release, lang, entry.arch)?,
            #[allow(unreachable_patterns)]
            _ => return Err(RidoError::InvalidSelection),
        };
        Ok(Self { info: entry, url, hash })
    }
}

#[derive(PartialEq, Debug, Copy, Clone)]
pub enum WindowsRelease {
    #[cfg(feature = "consumer")]
    Consumer(ConsumerRelease),
    #[cfg(feature = "enterprise")]
    Enterprise(EnterpriseRelease),
}
impl fmt::Display for WindowsRelease {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            #[cfg(feature = "consumer")]
            Self::Consumer(release) => write!(f, "{release}"),
            #[cfg(feature = "enterprise")]
            Self::Enterprise(release) => write!(f, "{release}"),
        }
    }
}
impl TryFrom<&str> for WindowsRelease {
    type Error = RidoError;
    fn try_from(value: &str) -> Result<Self, Self::Error> {
        #[cfg(feature = "consumer")]
        if let Ok(release) = value.try_into() {
            return Ok(Self::Consumer(release));
        }
        #[cfg(feature = "enterprise")]
        if let Ok(release) = value.try_into() {
            return Ok(Self::Enterprise(release));
        }
        Err(RidoError::InvalidReleaseStr)
    }
}

#[derive(PartialEq, Debug, Copy, Clone)]
pub enum WindowsLanguage {
    #[cfg(feature = "consumer")]
    Consumer(ConsumerLanguage),
    #[cfg(feature = "enterprise")]
    Enterprise(EnterpriseLanguage),
}
impl fmt::Display for WindowsLanguage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            #[cfg(feature = "consumer")]
            Self::Consumer(lang) => write!(f, "{lang}"),
            #[cfg(feature = "enterprise")]
            Self::Enterprise(lang) => write!(f, "{lang}"),
        }
    }
}
impl TryFrom<(WindowsRelease, &str)> for WindowsLanguage {
    type Error = RidoError;
    fn try_from(value: (WindowsRelease, &str)) -> Result<Self, Self::Error> {
        match value.0 {
            #[cfg(feature = "consumer")]
            WindowsRelease::Consumer(_) => ConsumerLanguage::try_from(value.1).map(Into::into),
            #[cfg(feature = "enterprise")]
            WindowsRelease::Enterprise(_) => EnterpriseLanguage::try_from(value.1).map(Into::into),
        }
    }
}
impl<L> TryFrom<(WindowsRelease, L)> for WindowsLanguage
where
    L: Into<WindowsLanguage>,
{
    type Error = RidoError;
    fn try_from(value: (WindowsRelease, L)) -> Result<Self, Self::Error> {
        Ok(value.1.into())
    }
}

#[allow(non_camel_case_types)]
#[derive(EnumIter, Debug, Display, Copy, Clone, PartialEq)]
pub enum WindowsArchitecture {
    x86_64,
    i686,
}
impl TryFrom<&str> for WindowsArchitecture {
    type Error = RidoError;
    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Ok(match value {
            "i386" | "i686" | "x86" | "x32" => Self::i686,
            "x86_64" | "amd64" | "x64" => Self::x86_64,
            _ => return Err(RidoError::InvalidArchitectureStr),
        })
    }
}

#[derive(Debug, Error)]
pub enum RidoError {
    #[error("Specified architecture {1} is not available for release {0}")]
    InvalidArchitecture(WindowsRelease, WindowsArchitecture),
    #[error("Specified language {1} is not available for release {0}")]
    InvalidLanguage(WindowsRelease, WindowsLanguage),
    #[error("Invalid release")]
    InvalidReleaseStr,
    #[error("Invalid language")]
    InvalidLanguageStr,
    #[error("Invalid architecture")]
    InvalidArchitectureStr,
    #[error("The language type must match the release type (Enterprise/Consumer)")]
    InvalidSelection,
    #[error("Microsoft servers gave us an empty response to our request for an automated download.")]
    EmptyResponse,
    #[error("Microsoft blocked the automated download request based on your IP address.")]
    BlockedRequest,
    #[error("Unable to parse download link from HTML")]
    HTMLParse,
    #[error("Could not find SKUID")]
    SKUID,
    #[error("Could not find Product ID")]
    ProductID,
    #[error("Reqwest error: {0}")]
    Reqwest(#[from] reqwest::Error),
}

pub(crate) trait ValidateLanguage {
    fn validate(&self, release: WindowsRelease) -> bool;
}
pub(crate) trait ValidateWithArch {
    fn validate(&self, arch: WindowsArchitecture) -> bool;
}