http_authentication/schemes/basic/
credentials.rs

1use alloc::{boxed::Box, format, string::String};
2use core::str::{self, FromStr};
3
4use base64::{engine::general_purpose, Engine as _};
5
6use crate::{schemes::NAME_BASIC as NAME, SP};
7
8//
9const COLON: char = ':';
10
11//
12#[derive(Debug, Clone)]
13pub struct Credentials {
14    pub user_id: Box<str>,
15    pub password: Box<str>,
16}
17
18impl Credentials {
19    pub fn new(user_id: impl AsRef<str>, password: impl AsRef<str>) -> Self {
20        Self {
21            user_id: user_id.as_ref().into(),
22            password: password.as_ref().into(),
23        }
24    }
25
26    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, CredentialsParseError> {
27        let bytes = bytes.as_ref();
28
29        if bytes.len() < NAME.len() + 1 {
30            return Err(CredentialsParseError::Other("too short"));
31        }
32
33        if !&bytes[..NAME.len()].eq_ignore_ascii_case(NAME.as_bytes()) {
34            return Err(CredentialsParseError::SchemeMismatch);
35        }
36
37        if bytes[NAME.len()..NAME.len() + 1] != [SP as u8] {
38            return Err(CredentialsParseError::OneSPMismatch);
39        }
40
41        let token68_bytes = &bytes[NAME.len() + 1..];
42
43        let token68_b64_decoded_bytes = general_purpose::STANDARD
44            .decode(token68_bytes)
45            .map_err(CredentialsParseError::Token68DecodeFailed)?;
46
47        let mut token68_split = token68_b64_decoded_bytes.split(|x| *x == COLON as u8);
48        let user_id = token68_split
49            .next()
50            .ok_or(CredentialsParseError::UserIdMissing)?;
51        let user_id = str::from_utf8(user_id).map_err(CredentialsParseError::UserIdToStrFailed)?;
52        let password = token68_split
53            .next()
54            .ok_or(CredentialsParseError::PasswordMissing)?;
55        let password =
56            str::from_utf8(password).map_err(CredentialsParseError::PasswordToStrFailed)?;
57        if token68_split.next().is_some() {
58            return Err(CredentialsParseError::Token68PairsMismatch);
59        }
60
61        Ok(Self::new(user_id, password))
62    }
63
64    fn internal_to_string(&self) -> String {
65        format!(
66            "{NAME}{SP}{}",
67            general_purpose::STANDARD.encode(format!("{}{COLON}{}", self.user_id, self.password))
68        )
69    }
70}
71
72//
73#[derive(Debug)]
74pub enum CredentialsParseError {
75    SchemeMismatch,
76    OneSPMismatch,
77    Token68DecodeFailed(base64::DecodeError),
78    UserIdMissing,
79    UserIdToStrFailed(str::Utf8Error),
80    PasswordMissing,
81    PasswordToStrFailed(str::Utf8Error),
82    Token68PairsMismatch,
83    Other(&'static str),
84}
85
86impl core::fmt::Display for CredentialsParseError {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        write!(f, "{self:?}")
89    }
90}
91
92#[cfg(feature = "std")]
93impl std::error::Error for CredentialsParseError {}
94
95//
96impl core::fmt::Display for Credentials {
97    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98        write!(f, "{}", self.internal_to_string())
99    }
100}
101
102//
103impl FromStr for Credentials {
104    type Err = CredentialsParseError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        Self::from_bytes(s.as_bytes())
108    }
109}
110
111//
112//
113//
114#[cfg(test)]
115pub(crate) const DEMO_CREDENTIALS_STR: &str = "Basic YWxhZGRpbjpvcGVuc2VzYW1l";
116#[cfg(test)]
117pub(crate) const DEMO_CREDENTIALS_USER_ID_STR: &str = "aladdin";
118#[cfg(test)]
119pub(crate) const DEMO_CREDENTIALS_PASSWORD_STR: &str = "opensesame";
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    use alloc::string::ToString as _;
126
127    #[test]
128    fn test_parse_and_render() {
129        let c = DEMO_CREDENTIALS_STR.parse::<Credentials>().unwrap();
130        assert_eq!(c.user_id, DEMO_CREDENTIALS_USER_ID_STR.into());
131        assert_eq!(c.password, DEMO_CREDENTIALS_PASSWORD_STR.into());
132        assert_eq!(c.to_string(), DEMO_CREDENTIALS_STR);
133
134        //
135        match Credentials::from_str("Basic") {
136            Err(CredentialsParseError::Other(err)) => {
137                assert_eq!(err, "too short")
138            }
139            x => panic!("{x:?}"),
140        }
141
142        match Credentials::from_str("MyScheme ") {
143            Err(CredentialsParseError::SchemeMismatch) => {}
144            x => panic!("{x:?}"),
145        }
146
147        match Credentials::from_str("Basic-") {
148            Err(CredentialsParseError::OneSPMismatch) => {}
149            x => panic!("{x:?}"),
150        }
151
152        match Credentials::from_str("Basic dGVzdDoxMjM6Zm9v") {
153            Err(CredentialsParseError::Token68PairsMismatch) => {}
154            x => panic!("{x:?}"),
155        }
156    }
157}