use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenKind {
Account,
AgentId,
SessionId,
RoomId,
ModuleId,
RosterHostId,
CredentialPublic,
RootProvider,
EventName,
EventVersion,
}
impl TokenKind {
pub const fn label(self) -> &'static str {
match self {
Self::Account => "acct",
Self::AgentId => "agent_id",
Self::SessionId => "session_id",
Self::RoomId => "room_id",
Self::ModuleId => "module_id",
Self::RosterHostId => "roster_host_id",
Self::CredentialPublic => "credential_public",
Self::RootProvider => "root_provider",
Self::EventName => "event",
Self::EventVersion => "event_version",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamingError {
kind: TokenKind,
token: String,
reason: &'static str,
}
impl NamingError {
pub fn kind(&self) -> TokenKind {
self.kind
}
pub fn token(&self) -> &str {
&self.token
}
pub fn reason(&self) -> &'static str {
self.reason
}
fn new(kind: TokenKind, token: &str, reason: &'static str) -> Self {
Self {
kind,
token: token.to_owned(),
reason,
}
}
}
impl fmt::Display for NamingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid {} token {:?}: {}",
self.kind.label(),
self.token,
self.reason
)
}
}
impl Error for NamingError {}
pub fn validate_account_token(token: &str) -> Result<(), NamingError> {
validate_no_hyphen_token(
TokenKind::Account,
token,
"expected [a-z0-9][a-z0-9_]{0,62}; hyphens and normalization are forbidden",
)
}
fn validate_event_name(token: &str) -> Result<(), NamingError> {
validate_no_hyphen_token(
TokenKind::EventName,
token,
"expected [a-z0-9][a-z0-9_]{0,62}; an event name is one subject token, so dots, hyphens, wildcards and whitespace are forbidden",
)
}
pub fn validate_event_version(token: &str) -> Result<u32, NamingError> {
let refuse = || {
NamingError::new(
TokenKind::EventVersion,
token,
"expected v{n} with n a decimal integer of at least 1 and no leading zero",
)
};
let digits = token.strip_prefix('v').ok_or_else(refuse)?;
if digits.is_empty()
|| digits.starts_with('0')
|| !digits.bytes().all(|byte| byte.is_ascii_digit())
{
return Err(refuse());
}
digits.parse::<u32>().map_err(|_| refuse())
}
fn validate_no_hyphen_token(
kind: TokenKind,
token: &str,
reason: &'static str,
) -> Result<(), NamingError> {
validate_len_and_first(kind, token)?;
if token
.bytes()
.skip(1)
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
{
Ok(())
} else {
Err(NamingError::new(kind, token, reason))
}
}
pub fn validate_token(kind: TokenKind, token: &str) -> Result<(), NamingError> {
match kind {
TokenKind::Account => return validate_account_token(token),
TokenKind::CredentialPublic => return validate_credential_public(token),
TokenKind::EventName => return validate_event_name(token),
TokenKind::EventVersion => return validate_event_version(token).map(|_| ()),
_ => {}
}
validate_len_and_first(kind, token)?;
if token.bytes().skip(1).all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-'
}) {
Ok(())
} else {
Err(NamingError::new(
kind,
token,
"expected [a-z0-9][a-z0-9_-]{0,62}; dots, wildcards, whitespace, and normalization are forbidden",
))
}
}
fn validate_len_and_first(kind: TokenKind, token: &str) -> Result<(), NamingError> {
if token.is_empty() || token.len() > 63 {
return Err(NamingError::new(
kind,
token,
"token must contain between 1 and 63 ASCII bytes",
));
}
let first = token.as_bytes()[0];
if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
return Err(NamingError::new(
kind,
token,
"token must start with a lowercase ASCII letter or digit",
));
}
Ok(())
}
fn validate_credential_public(token: &str) -> Result<(), NamingError> {
if token.is_empty() || token.len() > 63 {
return Err(NamingError::new(
TokenKind::CredentialPublic,
token,
"inbox component must contain between 1 and 63 ASCII bytes",
));
}
if token
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
{
Ok(())
} else {
Err(NamingError::new(
TokenKind::CredentialPublic,
token,
"inbox component may contain only ASCII letters, digits, underscore, or hyphen",
))
}
}