use std::fmt;
use crate::REDACTED;
use crate::config::ConfigError;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AdapterSet(String);
impl AdapterSet {
#[must_use = "a checked adapter-set name does nothing unless it is used"]
pub fn try_new(name: impl Into<String>) -> Result<Self, ConfigError> {
let name = name.into();
if name.trim().is_empty() {
return Err(ConfigError::EmptyAdapterSet);
}
if name.chars().any(char::is_control) {
return Err(ConfigError::AdapterSetControlCharacter);
}
Ok(Self(name))
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AdapterSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct Credentials {
user: Option<String>,
password: Option<String>,
}
impl Credentials {
#[must_use]
pub fn new(user: impl Into<String>, password: impl Into<String>) -> Self {
Self {
user: Some(user.into()),
password: Some(password.into()),
}
}
#[must_use]
#[inline]
pub const fn anonymous() -> Self {
Self {
user: None,
password: None,
}
}
#[must_use]
pub fn user_only(user: impl Into<String>) -> Self {
Self {
user: Some(user.into()),
password: None,
}
}
#[must_use]
#[inline]
pub fn user(&self) -> Option<&str> {
self.user.as_deref()
}
#[must_use]
#[inline]
pub const fn has_password(&self) -> bool {
self.password.is_some()
}
pub(crate) fn into_parts(self) -> (Option<String>, Option<String>) {
(self.user, self.password)
}
}
impl fmt::Debug for Credentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Credentials")
.field("user", &self.user.as_ref().map(|_| REDACTED))
.field("password", &self.password.as_ref().map(|_| REDACTED))
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adapter_set_accepts_a_plain_name() {
assert!(matches!(
AdapterSet::try_new("DEMO"),
Ok(set) if set.as_str() == "DEMO"
));
}
#[test]
fn test_adapter_set_rejects_an_empty_name() {
assert!(matches!(
AdapterSet::try_new(""),
Err(ConfigError::EmptyAdapterSet)
));
assert!(matches!(
AdapterSet::try_new(" \t "),
Err(ConfigError::EmptyAdapterSet)
));
}
#[test]
fn test_adapter_set_rejects_a_control_character() {
assert!(matches!(
AdapterSet::try_new("DE\r\nMO"),
Err(ConfigError::AdapterSetControlCharacter)
));
assert!(matches!(
AdapterSet::try_new("DE\u{0}MO"),
Err(ConfigError::AdapterSetControlCharacter)
));
}
#[test]
fn test_credentials_debug_redacts_both_halves() {
let credentials = Credentials::new("alice", "hunter2");
let rendered = format!("{credentials:?}");
assert!(!rendered.contains("hunter2"), "{rendered}");
assert!(!rendered.contains("alice"), "{rendered}");
assert!(rendered.contains(REDACTED), "{rendered}");
}
#[test]
fn test_credentials_debug_still_says_which_halves_are_set() {
let anonymous = format!("{:?}", Credentials::anonymous());
assert!(anonymous.contains("None"), "{anonymous}");
let user_only = format!("{:?}", Credentials::user_only("alice"));
assert!(user_only.contains(REDACTED), "{user_only}");
assert!(user_only.contains("None"), "{user_only}");
}
#[test]
fn test_a_config_holding_credentials_leaks_nothing_through_debug() {
let Ok(address) = crate::ServerAddress::try_new("wss://push.example.com") else {
unreachable!("the fixture address is valid");
};
let built = crate::ClientConfig::builder(address)
.with_credentials(Credentials::new("alice", "hunter2"))
.build();
match built {
Ok(config) => {
let rendered = format!("{config:?}");
assert!(!rendered.contains("hunter2"), "{rendered}");
assert!(!rendered.contains("alice"), "{rendered}");
}
Err(error) => panic!("the fixture configuration is valid: {error}"),
}
}
#[test]
fn test_credentials_anonymous_carries_nothing() {
let credentials = Credentials::anonymous();
assert_eq!(credentials.user(), None);
assert!(!credentials.has_password());
}
#[test]
fn test_credentials_user_only_has_no_password() {
let credentials = Credentials::user_only("alice");
assert_eq!(credentials.user(), Some("alice"));
assert!(!credentials.has_password());
}
}