http_authentication/schemes/basic/
credentials.rs1use 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
8const COLON: char = ':';
10
11#[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#[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
95impl 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
102impl 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#[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 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}