http-authentication 0.2.0

HTTP Authentication
Documentation
use alloc::{
    format,
    string::{String, ToString as _},
    vec,
    vec::Vec,
};
use core::{
    ops::Deref,
    str::{self, FromStr},
};

use http_auth::ChallengeParser;

use crate::{
    challenge::Challenge,
    schemes::{NAME_BASIC, NAME_BEARER, NAME_DIGEST},
    COMMA, SP,
};

//
#[derive(Debug, Clone)]
pub struct Challenges(pub Vec<Challenge>);

impl Deref for Challenges {
    type Target = Vec<Challenge>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Challenges {
    pub fn new(inner: Vec<Challenge>) -> Self {
        Self(inner)
    }

    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, ChallengesParseError> {
        let bytes = bytes.as_ref();
        let s = str::from_utf8(bytes).map_err(ChallengesParseError::ChallengesToStrFailed)?;
        Self::internal_from_str(s)
    }

    fn internal_from_str(s: impl AsRef<str>) -> Result<Self, ChallengesParseError> {
        let s = s.as_ref();

        if s.is_empty() {
            return Err(ChallengesParseError::Other("empty"));
        }

        let challenge_parser = ChallengeParser::new(s);

        #[allow(unused_mut)]
        let mut inner = vec![];
        for c in challenge_parser {
            let c = c.map_err(|err| ChallengesParseError::ChallengeParserError(err.to_string()))?;
            match c.scheme {
                x if x.eq_ignore_ascii_case(NAME_BASIC) => {
                    #[cfg(feature = "scheme-basic")]
                    {
                        let c = crate::schemes::basic::Challenge::try_from(&c)
                            .map(Challenge::Basic)
                            .map_err(ChallengesParseError::Basic)?;

                        inner.push(c)
                    }
                    #[cfg(not(feature = "scheme-basic"))]
                    {
                        return Err(ChallengesParseError::SchemeUnsupported(
                            "Require feature scheme-basic",
                        ));
                    }
                }
                x if x.eq_ignore_ascii_case(NAME_BEARER) => {
                    #[cfg(feature = "scheme-bearer")]
                    {
                        let c = crate::schemes::bearer::Challenge::try_from(&c)
                            .map(Challenge::Bearer)
                            .map_err(ChallengesParseError::Bearer)?;

                        inner.push(c)
                    }
                    #[cfg(not(feature = "scheme-bearer"))]
                    {
                        return Err(ChallengesParseError::SchemeUnsupported(
                            "Require feature scheme-bearer",
                        ));
                    }
                }
                x if x.eq_ignore_ascii_case(NAME_DIGEST) => {
                    return Err(ChallengesParseError::SchemeUnsupported("Unimplemented"))
                }
                _ => return Err(ChallengesParseError::SchemeUnknown),
            }
        }
        Ok(Self::new(inner))
    }
}

//
#[derive(Debug)]
pub enum ChallengesParseError {
    ChallengesToStrFailed(str::Utf8Error),
    ChallengeParserError(String),
    #[cfg(feature = "scheme-basic")]
    Basic(crate::schemes::basic::ChallengeParseError),
    #[cfg(feature = "scheme-bearer")]
    Bearer(crate::schemes::bearer::ChallengeParseError),
    SchemeUnknown,
    SchemeUnsupported(&'static str),
    Other(&'static str),
}

impl core::fmt::Display for ChallengesParseError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{self:?}")
    }
}

#[cfg(feature = "std")]
impl std::error::Error for ChallengesParseError {}

impl core::fmt::Display for Challenges {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}", ChallengesWithSlice(self.0.as_ref()))
    }
}

//
impl FromStr for Challenges {
    type Err = ChallengesParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::internal_from_str(s)
    }
}

//
//
#[derive(Debug, Clone)]
pub(crate) struct ChallengesWithSlice<'a>(&'a [Challenge]);

impl<'a> ChallengesWithSlice<'a> {
    #[allow(dead_code)]
    pub(crate) fn new(inner: &'a [Challenge]) -> Self {
        Self(inner)
    }
}

//
impl core::fmt::Display for ChallengesWithSlice<'_> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "{}",
            self.0
                .iter()
                .map(|x| x.to_string())
                .collect::<Vec<_>>()
                .join(format!("{COMMA}{SP}").as_str())
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_and_render() {
        //
        #[cfg(feature = "scheme-basic")]
        {
            use crate::schemes::basic::{
                DEMO_CHALLENGE_CHARSET_STR, DEMO_CHALLENGE_REALM_STR, DEMO_CHALLENGE_STR,
                DEMO_CHALLENGE_STR_SIMPLE,
            };

            match DEMO_CHALLENGE_STR.parse::<Challenges>() {
                Ok(c) => {
                    assert_eq!(c.0.len(), 1);
                    let c = c.0.first().unwrap();
                    let c = c.as_basic().unwrap();
                    assert_eq!(c.realm, DEMO_CHALLENGE_REALM_STR.into());
                    assert_eq!(c.charset, Some(DEMO_CHALLENGE_CHARSET_STR.into()));
                    assert_eq!(c.to_string(), DEMO_CHALLENGE_STR);
                }
                x => panic!("{x:?}"),
            }

            match DEMO_CHALLENGE_STR_SIMPLE.parse::<Challenges>() {
                Ok(c) => {
                    assert_eq!(c.0.len(), 1);
                    let c = c.0.first().unwrap();
                    let c = c.as_basic().unwrap();
                    assert_eq!(c.realm, DEMO_CHALLENGE_REALM_STR.into());
                    assert_eq!(c.charset, None);
                    assert_eq!(c.to_string(), DEMO_CHALLENGE_STR_SIMPLE);
                }
                x => panic!("{x:?}"),
            }
        }
        #[cfg(not(feature = "scheme-basic"))]
        {
            match "Basic".parse::<Challenges>() {
                Err(ChallengesParseError::SchemeUnsupported(_)) => {}
                x => panic!("{x:?}"),
            }
        }

        //
        #[cfg(feature = "scheme-bearer")]
        {
            use crate::schemes::bearer::{
                DEMO_CHALLENGE_ERROR_DESCRIPTION_STR, DEMO_CHALLENGE_ERROR_STR,
                DEMO_CHALLENGE_REALM_STR, DEMO_CHALLENGE_STR, DEMO_CHALLENGE_STR_SIMPLE,
            };

            match DEMO_CHALLENGE_STR.parse::<Challenges>() {
                Ok(c) => {
                    assert_eq!(c.0.len(), 1);
                    let c = c.0.first().unwrap();
                    let c = c.as_bearer().unwrap();
                    assert_eq!(c.realm, DEMO_CHALLENGE_REALM_STR.into());
                    assert_eq!(c.scope, None);
                    assert_eq!(c.error, Some(DEMO_CHALLENGE_ERROR_STR.into()));
                    assert_eq!(
                        c.error_description,
                        Some(DEMO_CHALLENGE_ERROR_DESCRIPTION_STR.into())
                    );
                    assert_eq!(c.error_uri, None);
                    assert_eq!(c.to_string(), DEMO_CHALLENGE_STR);
                }
                x => panic!("{x:?}"),
            }

            match DEMO_CHALLENGE_STR_SIMPLE.parse::<Challenges>() {
                Ok(c) => {
                    assert_eq!(c.0.len(), 1);
                    let c = c.0.first().unwrap();
                    let c = c.as_bearer().unwrap();
                    assert_eq!(c.realm, DEMO_CHALLENGE_REALM_STR.into());
                    assert_eq!(c.scope, None);
                    assert_eq!(c.error, None);
                    assert_eq!(c.error_description, None);
                    assert_eq!(c.error_uri, None);
                    assert_eq!(c.to_string(), DEMO_CHALLENGE_STR_SIMPLE);
                }
                x => panic!("{x:?}"),
            }
        }
        #[cfg(not(feature = "scheme-bearer"))]
        {
            match "Bearer".parse::<Challenges>() {
                Err(ChallengesParseError::SchemeUnsupported(_)) => {}
                x => panic!("{x:?}"),
            }
        }

        #[cfg(all(feature = "scheme-basic", feature = "scheme-bearer"))]
        {
            use crate::schemes::{basic, bearer};

            let s = format!(
                "{}, {}",
                basic::DEMO_CHALLENGE_STR_SIMPLE,
                bearer::DEMO_CHALLENGE_STR_SIMPLE
            );

            match s.parse::<Challenges>() {
                Ok(c) => {
                    for (i, c) in c.iter().enumerate() {
                        match i {
                            0 => {
                                let c = c.as_basic().unwrap();
                                assert_eq!(c.realm, basic::DEMO_CHALLENGE_REALM_STR.into());
                            }
                            1 => {
                                let c = c.as_bearer().unwrap();
                                assert_eq!(c.realm, bearer::DEMO_CHALLENGE_REALM_STR.into());
                            }
                            i => panic!("{i} {c:?}"),
                        }
                    }

                    assert_eq!(c.to_string(), s);
                }
                x => panic!("{x:?}"),
            }
        }

        //
        match Challenges::from_str("") {
            Err(ChallengesParseError::Other(_)) => {}
            x => panic!("{x:?}"),
        }

        match Challenges::from_str("Foo") {
            Err(ChallengesParseError::SchemeUnknown) => {}
            x => panic!("{x:?}"),
        }
    }
}