#[derive(Debug, Clone)]
pub struct StreamKeyPolicy {
pub min_length: usize,
pub max_length: usize,
pub allowed_chars: Option<String>,
pub reject_numeric_only: bool,
pub reject_empty: bool,
}
impl Default for StreamKeyPolicy {
fn default() -> Self {
Self {
min_length: 1,
max_length: 256,
allowed_chars: None,
reject_numeric_only: false,
reject_empty: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StreamKeyValidator {
policy: StreamKeyPolicy,
allowlist: Vec<String>,
denylist_prefixes: Vec<String>,
}
impl StreamKeyValidator {
#[must_use]
pub fn new(policy: StreamKeyPolicy) -> Self {
Self {
policy,
allowlist: Vec::new(),
denylist_prefixes: Vec::new(),
}
}
pub fn add_allowed_key(&mut self, key: impl Into<String>) {
self.allowlist.push(key.into());
}
pub fn add_denied_prefix(&mut self, prefix: impl Into<String>) {
self.denylist_prefixes.push(prefix.into());
}
pub fn validate(&self, key: &str) -> Result<(), String> {
if self.policy.reject_empty && key.is_empty() {
return Err("stream key must not be empty".to_string());
}
if key.len() < self.policy.min_length {
return Err(format!(
"stream key too short: {} < {}",
key.len(),
self.policy.min_length
));
}
if key.len() > self.policy.max_length {
return Err(format!(
"stream key too long: {} > {}",
key.len(),
self.policy.max_length
));
}
if let Some(allowed) = &self.policy.allowed_chars {
for ch in key.chars() {
if !allowed.contains(ch) {
return Err(format!("stream key contains disallowed character: '{ch}'"));
}
}
} else {
for ch in key.chars() {
if !ch.is_ascii() || ch.is_ascii_control() {
return Err(format!("stream key must be printable ASCII; found '{ch}'"));
}
}
}
if self.policy.reject_numeric_only && key.chars().all(|c| c.is_ascii_digit()) {
return Err("stream key must not be numeric-only".to_string());
}
for prefix in &self.denylist_prefixes {
if key.starts_with(prefix.as_str()) {
return Err(format!("stream key starts with denied prefix '{prefix}'"));
}
}
if !self.allowlist.is_empty() && !self.allowlist.iter().any(|k| k == key) {
return Err("stream key not in allowlist".to_string());
}
Ok(())
}
#[must_use]
pub fn is_valid(&self, key: &str) -> bool {
self.validate(key).is_ok()
}
}