use crate::core::error::CoreError;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EntityIdRef<'a>(&'a str);
impl<'a> EntityIdRef<'a> {
pub fn new(value: &'a str) -> Result<Self, CoreError> {
if value.is_empty() {
return Err(CoreError::EntityIdEmpty);
}
if value.len() > 1024 {
return Err(CoreError::EntityIdTooLong(value.len()));
}
Ok(EntityIdRef(value))
}
pub fn as_str(&self) -> &'a str {
self.0
}
pub fn to_owned(&self) -> EntityId {
EntityId(self.0.to_string())
}
}
impl<'a> fmt::Display for EntityIdRef<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EntityId(String);
impl EntityId {
pub fn new(value: impl Into<String>) -> Result<Self, CoreError> {
let s = value.into();
if s.is_empty() {
return Err(CoreError::EntityIdEmpty);
}
if s.len() > 1024 {
return Err(CoreError::EntityIdTooLong(s.len()));
}
Ok(EntityId(s))
}
pub fn as_ref(&self) -> EntityIdRef<'_> {
EntityIdRef(&self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for EntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SamlId(String);
impl SamlId {
pub fn generate() -> Self {
let mut bytes = [0u8; 16];
rand::fill(&mut bytes);
let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
SamlId(format!("_{}", hex))
}
pub fn from_string(value: impl Into<String>) -> Result<Self, CoreError> {
let s = value.into();
if s.is_empty() {
return Err(CoreError::InvalidId(
"SAML ID must not be empty".to_string(),
));
}
let first = s.chars().next().unwrap();
if !first.is_ascii_alphabetic() && first != '_' {
return Err(CoreError::InvalidId(format!(
"SAML ID must start with a letter or underscore, got: {}",
first
)));
}
Ok(SamlId(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SamlId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SamlIdRef<'a>(&'a str);
impl<'a> SamlIdRef<'a> {
pub fn new(value: &'a str) -> Result<Self, CoreError> {
if value.is_empty() {
return Err(CoreError::InvalidId(
"SAML ID must not be empty".to_string(),
));
}
let first = value.chars().next().unwrap();
if !first.is_ascii_alphabetic() && first != '_' {
return Err(CoreError::InvalidId(format!(
"SAML ID must start with a letter or underscore, got: {}",
first
)));
}
Ok(SamlIdRef(value))
}
pub fn as_str(&self) -> &'a str {
self.0
}
pub fn to_owned(&self) -> SamlId {
SamlId(self.0.to_string())
}
}
impl<'a> fmt::Display for SamlIdRef<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SamlVersion {
pub major: u8,
pub minor: u8,
}
impl SamlVersion {
pub const V2_0: SamlVersion = SamlVersion { major: 2, minor: 0 };
pub fn parse(s: &str) -> Result<Self, CoreError> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 2 {
return Err(CoreError::InvalidVersion(s.to_string()));
}
let major = parts[0]
.parse::<u8>()
.map_err(|_| CoreError::InvalidVersion(s.to_string()))?;
let minor = parts[1]
.parse::<u8>()
.map_err(|_| CoreError::InvalidVersion(s.to_string()))?;
Ok(SamlVersion { major, minor })
}
pub fn is_v2_0(&self) -> bool {
self.major == 2 && self.minor == 0
}
pub fn as_str(&self) -> &'static str {
if self.major == 2 && self.minor == 0 {
"2.0"
} else if self.major == 1 && self.minor == 1 {
"1.1"
} else if self.major == 1 && self.minor == 0 {
"1.0"
} else {
"2.0"
}
}
pub fn try_from_str(s: &str) -> Option<Self> {
Self::parse(s).ok()
}
}
impl fmt::Display for SamlVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
impl Default for SamlVersion {
fn default() -> Self {
SamlVersion::V2_0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entity_id_valid() {
let id = EntityId::new("https://sp.example.com/saml").unwrap();
assert_eq!(id.as_str(), "https://sp.example.com/saml");
}
#[test]
fn test_entity_id_empty() {
assert!(matches!(EntityId::new(""), Err(CoreError::EntityIdEmpty)));
}
#[test]
fn test_entity_id_too_long() {
let long = "x".repeat(1025);
assert!(matches!(
EntityId::new(long),
Err(CoreError::EntityIdTooLong(1025))
));
}
#[test]
fn test_entity_id_max_length_ok() {
let max = "x".repeat(1024);
assert!(EntityId::new(max).is_ok());
}
#[test]
fn test_entity_id_ref_to_owned() {
let s = "https://idp.example.com";
let id_ref = EntityIdRef::new(s).unwrap();
let owned = id_ref.to_owned();
assert_eq!(owned.as_str(), s);
}
#[test]
fn test_entity_id_as_ref() {
let owned = EntityId::new("https://sp.example.com").unwrap();
let ref_ = owned.as_ref();
assert_eq!(ref_.as_str(), "https://sp.example.com");
}
#[test]
fn test_saml_id_generate() {
let id = SamlId::generate();
assert!(id.as_str().starts_with('_'));
assert_eq!(id.as_str().len(), 33); }
#[test]
fn test_saml_id_generate_unique() {
let id1 = SamlId::generate();
let id2 = SamlId::generate();
assert_ne!(id1, id2);
}
#[test]
fn test_saml_id_from_string() {
let id = SamlId::from_string("_abc123").unwrap();
assert_eq!(id.as_str(), "_abc123");
}
#[test]
fn test_saml_id_from_string_letter_start() {
let id = SamlId::from_string("abc123").unwrap();
assert_eq!(id.as_str(), "abc123");
}
#[test]
fn test_saml_id_from_string_invalid_start() {
assert!(SamlId::from_string("123abc").is_err());
}
#[test]
fn test_saml_id_from_string_empty() {
assert!(SamlId::from_string("").is_err());
}
#[test]
fn test_saml_id_ref() {
let id_ref = SamlIdRef::new("_abc123").unwrap();
assert_eq!(id_ref.as_str(), "_abc123");
let owned = id_ref.to_owned();
assert_eq!(owned.as_str(), "_abc123");
}
#[test]
fn test_saml_version_default() {
let v = SamlVersion::default();
assert!(v.is_v2_0());
assert_eq!(v.to_string(), "2.0");
}
#[test]
fn test_saml_version_parse() {
let v = SamlVersion::parse("2.0").unwrap();
assert!(v.is_v2_0());
}
#[test]
fn test_saml_version_parse_other() {
let v = SamlVersion::parse("1.1").unwrap();
assert!(!v.is_v2_0());
assert_eq!(v.major, 1);
assert_eq!(v.minor, 1);
}
#[test]
fn test_saml_version_parse_invalid() {
assert!(SamlVersion::parse("abc").is_err());
assert!(SamlVersion::parse("2").is_err());
assert!(SamlVersion::parse("2.0.1").is_err());
}
}