use super::{config::SecurityConfig, error::ValidationError};
use std::{borrow::Cow, fmt::Debug};
use urlencoding::decode;
#[derive(Debug, Clone)]
pub struct SanitizedInput<T> {
pub original: String,
pub cleaned: T,
}
#[async_trait::async_trait]
pub trait Validator<T>: Send + Sync {
fn validate(&self, input: &str) -> Result<T, ValidationError>;
async fn validate_async(&self, input: &str) -> Result<T, ValidationError> {
Ok(self.validate(input)?)
}
fn target_type(&self) -> &'static str;
}
pub fn sanitize_and_validate<T>(
input: &str,
validator: &impl Validator<T>,
config: &SecurityConfig,
) -> Result<SanitizedInput<T>, ValidationError>
where
T: Debug + Send + Sync,
{
let decoded = decode(input).unwrap_or(Cow::Borrowed(input));
let (cleaned, bad_chars) = sanitize_input(&decoded, config);
if !bad_chars.is_empty() {
let symbols = bad_chars
.iter()
.map(|c| format!("'{}'", c))
.collect::<Vec<_>>()
.join(", ");
return Err(ValidationError::DangerousCharacters {
symbols,
count: bad_chars.len(),
});
}
if config.has_blocked_pattern(&cleaned) {
return Err(ValidationError::BlockedPattern {
pattern: "blocked pattern detected".to_string(),
});
}
validator.validate(&cleaned).map(|result| SanitizedInput {
original: input.to_string(),
cleaned: result,
})
}
pub async fn sanitize_and_validate_async<T>(
input: &str,
validator: &impl Validator<T>,
config: &SecurityConfig,
) -> Result<SanitizedInput<T>, ValidationError>
where
T: Debug + Send + Sync,
{
let decoded = decode(input).unwrap_or(Cow::Borrowed(input));
let (cleaned, bad_chars) = sanitize_input(&decoded, config);
if !bad_chars.is_empty() {
let symbols = bad_chars
.iter()
.map(|c| format!("'{}'", c))
.collect::<Vec<_>>()
.join(", ");
return Err(ValidationError::DangerousCharacters {
symbols,
count: bad_chars.len(),
});
}
if config.has_blocked_pattern(&cleaned) {
return Err(ValidationError::BlockedPattern {
pattern: "blocked pattern detected".to_string(),
});
}
validator
.validate_async(&cleaned)
.await
.map(|result| SanitizedInput {
original: input.to_string(),
cleaned: result,
})
}
pub fn sanitize_input(input: &str, config: &SecurityConfig) -> (String, Vec<char>) {
let mut cleaned = String::with_capacity(input.len());
let mut bad_chars = Vec::with_capacity(8);
input.chars().for_each(|c| {
if config.is_char_forbidden(&c) {
bad_chars.push(c);
} else {
cleaned.push(c);
}
});
(cleaned, bad_chars)
}