use crate::security::patterns::{SENSITIVE_DETECTION_PATTERNS, SENSITIVE_KEYWORDS};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
static DEFAULT_DANGEROUS_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
vec![
Regex::new(r"[;<>|`$()]").unwrap(),
Regex::new(r"\$\{.*\}").unwrap(),
Regex::new(r"`[^`]+`").unwrap(),
Regex::new(r"\|").unwrap(),
Regex::new(r"&&").unwrap(),
Regex::new(r"\|\|").unwrap(),
Regex::new(r">>").unwrap(),
Regex::new(r"2>").unwrap(),
Regex::new(r"\.\.[/\\]").unwrap(),
Regex::new(r"[/\\]\.\.[/\\]").unwrap(),
Regex::new(r"(?i)(;?\s*(drop|delete|update|insert|alter|create)\b)").unwrap(),
Regex::new(r"(?i)(union\s+select\b)").unwrap(),
Regex::new(r"(?i)'+\s*(or|and)\b").unwrap(),
Regex::new(r"(?i)--\s*$").unwrap(),
]
});
#[derive(Debug, Clone)]
pub struct SensitiveDataDetector {
sensitive_patterns: Vec<Regex>,
high_sensitivity_keywords: HashSet<&'static str>,
custom_sensitive_fields: HashSet<String>,
}
impl Default for SensitiveDataDetector {
fn default() -> Self {
Self::new()
}
}
impl SensitiveDataDetector {
pub fn new() -> Self {
Self {
sensitive_patterns: Self::default_sensitive_patterns(),
high_sensitivity_keywords: Self::default_high_sensitivity_keywords(),
custom_sensitive_fields: HashSet::new(),
}
}
fn default_sensitive_patterns() -> Vec<Regex> {
SENSITIVE_DETECTION_PATTERNS.clone()
}
fn default_high_sensitivity_keywords() -> HashSet<&'static str> {
SENSITIVE_KEYWORDS.clone()
}
pub fn add_custom_sensitive_field(&mut self, field: impl Into<String>) {
self.custom_sensitive_fields
.insert(field.into().to_lowercase());
}
pub fn is_sensitive(&self, field_name: &str, field_value: &str) -> SensitivityResult {
let field_lower = field_name.to_lowercase();
let value_lower = field_value.to_lowercase();
if self
.high_sensitivity_keywords
.contains(field_lower.as_str())
{
return SensitivityResult::High {
field: field_name.to_string(),
reason: "high sensitivity keyword in field name".to_string(),
};
}
if self.custom_sensitive_fields.contains(&field_lower) {
return SensitivityResult::Medium {
field: field_name.to_string(),
reason: "custom sensitive field".to_string(),
};
}
for pattern in &self.sensitive_patterns {
if pattern.is_match(&field_lower) || pattern.is_match(&value_lower) {
return SensitivityResult::Medium {
field: field_name.to_string(),
reason: format!("sensitive pattern detected: {}", pattern.as_str()),
};
}
}
SensitivityResult::Low
}
pub fn detect_all<'a>(
&self,
data: &'a HashMap<String, String>,
) -> Vec<(&'a str, SensitivityResult)> {
data.iter()
.map(|(k, v)| (k.as_str(), self.is_sensitive(k, v)))
.filter(|(_, result)| !result.is_low())
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SensitivityResult {
Low,
Medium { field: String, reason: String },
High { field: String, reason: String },
}
impl SensitivityResult {
pub fn is_low(&self) -> bool {
matches!(self, SensitivityResult::Low)
}
pub fn needs_protection(&self) -> bool {
!self.is_low()
}
pub fn description(&self) -> String {
match self {
SensitivityResult::Low => "low sensitivity".to_string(),
SensitivityResult::Medium { field, reason } => {
format!("medium sensitivity: {} - {}", field, reason)
}
SensitivityResult::High { field, reason } => {
format!("high sensitivity: {} - {}", field, reason)
}
}
}
}
#[derive(Debug, Clone)]
pub struct InputValidator {
max_string_length: usize,
max_array_length: usize,
max_depth: usize,
allowed_chars_pattern: Option<Regex>,
dangerous_patterns: Vec<Regex>,
whitelist_patterns: Vec<Regex>,
}
impl Default for InputValidator {
fn default() -> Self {
Self::new()
}
}
impl InputValidator {
pub fn new() -> Self {
Self {
max_string_length: 1024,
max_array_length: 100,
max_depth: 10,
allowed_chars_pattern: None,
dangerous_patterns: Self::default_dangerous_patterns(),
whitelist_patterns: Vec::new(),
}
}
fn default_dangerous_patterns() -> Vec<Regex> {
DEFAULT_DANGEROUS_PATTERNS.clone()
}
pub fn with_max_string_length(mut self, length: usize) -> Self {
self.max_string_length = length;
self
}
pub fn with_max_array_length(mut self, length: usize) -> Self {
self.max_array_length = length;
self
}
pub fn with_max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
pub fn with_allowed_chars_pattern(mut self, pattern: &str) -> Self {
self.allowed_chars_pattern = Regex::new(pattern).ok();
self
}
pub fn add_whitelist_pattern(mut self, pattern: &str) -> Self {
if let Ok(regex) = Regex::new(pattern) {
self.whitelist_patterns.push(regex);
}
self
}
pub fn validate_string(&self, value: &str) -> Result<(), InputValidationError> {
if value.len() > self.max_string_length {
return Err(InputValidationError::TooLong {
max: self.max_string_length,
actual: value.len(),
});
}
if let Some(ref pattern) = self.allowed_chars_pattern {
if !pattern.is_match(value) {
return Err(InputValidationError::InvalidCharacters);
}
}
for pattern in &self.dangerous_patterns {
if pattern.is_match(value) {
return Err(InputValidationError::DangerousPattern {
pattern: pattern.as_str().to_string(),
});
}
}
Ok(())
}
pub fn sanitize_string(&self, value: &str) -> Result<String, InputValidationError> {
let mut result = String::new();
for c in value.chars() {
if !self.is_dangerous_char(c) {
result.push(c);
}
}
self.validate_string(&result)?;
Ok(result)
}
fn is_dangerous_char(&self, c: char) -> bool {
matches!(
c,
';' | '<' | '>' | '&' | '|' | '`' | '$' | '(' | ')' | '\0' | '{' | '}'
)
}
pub fn validate_field_name(&self, name: &str) -> Result<(), InputValidationError> {
if name.is_empty() {
return Err(InputValidationError::EmptyFieldName);
}
if name.len() > self.max_string_length {
return Err(InputValidationError::TooLong {
max: self.max_string_length,
actual: name.len(),
});
}
let valid_pattern = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]*$").unwrap();
if !valid_pattern.is_match(name) {
return Err(InputValidationError::InvalidFieldNameFormat);
}
Ok(())
}
pub fn validate_url(&self, url: &str) -> Result<(), InputValidationError> {
if url.len() > self.max_string_length {
return Err(InputValidationError::TooLong {
max: self.max_string_length,
actual: url.len(),
});
}
let parsed = url::Url::parse(url).map_err(|_| InputValidationError::InvalidUrl)?;
if !matches!(parsed.scheme(), "http" | "https") {
return Err(InputValidationError::InvalidUrlScheme);
}
for pattern in &self.dangerous_patterns {
if pattern.is_match(url) {
return Err(InputValidationError::DangerousPattern {
pattern: pattern.as_str().to_string(),
});
}
}
Ok(())
}
pub fn validate_email(&self, email: &str) -> Result<(), InputValidationError> {
if email.len() > self.max_string_length {
return Err(InputValidationError::TooLong {
max: self.max_string_length,
actual: email.len(),
});
}
let email_pattern =
Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
if !email_pattern.is_match(email) {
return Err(InputValidationError::InvalidEmail);
}
Ok(())
}
pub fn validate_whitelist(&self, value: &str) -> Result<(), InputValidationError> {
if self.whitelist_patterns.is_empty() {
return Ok(());
}
for pattern in &self.whitelist_patterns {
if pattern.is_match(value) {
return Ok(());
}
}
Err(InputValidationError::NotInWhitelist)
}
pub fn validate_all<'a>(
&'a self,
data: &'a HashMap<String, String>,
) -> Vec<(&'a String, InputValidationError)> {
let mut errors = Vec::new();
for (name, value) in data {
if let Err(e) = self.validate_field_name(name) {
errors.push((name, e));
continue;
}
if let Err(e) = self.validate_string(value) {
errors.push((name, e));
}
}
errors
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum InputValidationError {
TooLong { max: usize, actual: usize },
InvalidCharacters,
DangerousPattern { pattern: String },
EmptyFieldName,
InvalidFieldNameFormat,
InvalidUrl,
InvalidUrlScheme,
InvalidEmail,
NotInWhitelist,
DepthExceeded { max: usize, actual: usize },
ArrayTooLong { max: usize, actual: usize },
}
impl std::fmt::Display for InputValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InputValidationError::TooLong { max, actual } => {
write!(f, "Input too long: max={}, actual={}", max, actual)
}
InputValidationError::InvalidCharacters => {
write!(f, "Input contains invalid characters")
}
InputValidationError::DangerousPattern { pattern } => {
write!(f, "Input contains dangerous pattern: {}", pattern)
}
InputValidationError::EmptyFieldName => {
write!(f, "Field name is empty")
}
InputValidationError::InvalidFieldNameFormat => {
write!(f, "Field name format is invalid")
}
InputValidationError::InvalidUrl => {
write!(f, "URL is invalid")
}
InputValidationError::InvalidUrlScheme => {
write!(f, "URL scheme is not allowed")
}
InputValidationError::InvalidEmail => {
write!(f, "Email format is invalid")
}
InputValidationError::NotInWhitelist => {
write!(f, "Input does not match any whitelist pattern")
}
InputValidationError::DepthExceeded { max, actual } => {
write!(f, "Nesting depth exceeded: max={}, actual={}", max, actual)
}
InputValidationError::ArrayTooLong { max, actual } => {
write!(f, "Array too long: max={}, actual={}", max, actual)
}
}
}
}
impl std::error::Error for InputValidationError {}
#[derive(Default)]
pub struct ConfigValidatorBuilder {
validator: InputValidator,
sensitive_detector: SensitiveDataDetector,
}
impl ConfigValidatorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn build(self) -> ConfigValidator {
ConfigValidator {
input_validator: self.validator,
sensitive_detector: self.sensitive_detector,
}
}
pub fn max_string_length(mut self, length: usize) -> Self {
self.validator = self.validator.with_max_string_length(length);
self
}
pub fn add_sensitive_field(mut self, field: &str) -> Self {
self.sensitive_detector.add_custom_sensitive_field(field);
self
}
pub fn strict_mode(self) -> Self {
self.max_string_length(256)
.add_sensitive_field("token")
.add_sensitive_field("password")
}
}
#[derive(Debug, Clone)]
pub struct ConfigValidator {
input_validator: InputValidator,
sensitive_detector: SensitiveDataDetector,
}
impl ConfigValidator {
pub fn new() -> Self {
Self {
input_validator: InputValidator::new(),
sensitive_detector: SensitiveDataDetector::new(),
}
}
pub fn builder() -> ConfigValidatorBuilder {
ConfigValidatorBuilder::new()
}
#[cfg(test)]
pub fn sensitive_detector(&self) -> &SensitiveDataDetector {
&self.sensitive_detector
}
pub fn validate(&self, data: &HashMap<String, String>) -> ConfigValidationResult {
let mut errors = Vec::new();
let mut sensitive_fields = Vec::new();
for (name, value) in data {
if let Err(e) = self.input_validator.validate_field_name(name) {
errors.push(ConfigValidationError::FieldError {
field: name.clone(),
error: e,
});
}
if let Err(e) = self.input_validator.validate_string(value) {
errors.push(ConfigValidationError::FieldError {
field: name.clone(),
error: e,
});
}
let sensitivity = self.sensitive_detector.is_sensitive(name, value);
if sensitivity.needs_protection() {
sensitive_fields.push((name.clone(), sensitivity));
}
}
ConfigValidationResult {
errors,
sensitive_fields,
}
}
pub fn validate_safe(&self, data: &HashMap<String, String>) -> bool {
data.iter().all(|(name, value)| {
self.input_validator.validate_field_name(name).is_ok()
&& self.input_validator.validate_string(value).is_ok()
})
}
}
impl Default for ConfigValidator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ConfigValidationResult {
pub errors: Vec<ConfigValidationError>,
pub sensitive_fields: Vec<(String, SensitivityResult)>,
}
impl ConfigValidationResult {
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn has_sensitive_data(&self) -> bool {
!self.sensitive_fields.is_empty()
}
pub fn error_report(&self) -> String {
self.errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Debug, Clone)]
pub enum ConfigValidationError {
FieldError {
field: String,
error: InputValidationError,
},
SensitiveDataWarning {
field: String,
sensitivity: SensitivityResult,
},
}
impl std::fmt::Display for ConfigValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigValidationError::FieldError { field, error } => {
write!(f, "Field '{}': {}", field, error)
}
ConfigValidationError::SensitiveDataWarning { field, sensitivity } => {
write!(f, "Field '{}': {}", field, sensitivity.description())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sensitive_data_detection() {
let detector = SensitiveDataDetector::new();
assert!(detector
.is_sensitive("password", "value")
.needs_protection());
assert!(detector
.is_sensitive("secret_key", "value")
.needs_protection());
assert!(detector
.is_sensitive("api_token", "value")
.needs_protection());
assert!(detector
.is_sensitive("user_token", "value")
.needs_protection());
assert!(!detector
.is_sensitive("username", "value")
.needs_protection());
assert!(!detector.is_sensitive("port", "8080").needs_protection());
}
#[test]
fn test_input_validation() {
let validator = InputValidator::new();
assert!(validator.validate_string("hello world").is_ok());
assert!(validator.validate_field_name("app_port").is_ok());
assert!(validator.validate_string("hello;world").is_err());
assert!(validator.validate_string("hello${world}").is_err());
assert!(validator.validate_field_name("").is_err());
assert!(validator.validate_field_name("123port").is_err());
}
#[test]
fn test_url_validation() {
let validator = InputValidator::new();
assert!(validator.validate_url("https://example.com").is_ok());
assert!(validator.validate_url("http://localhost:8080").is_ok());
assert!(validator.validate_url("ftp://example.com").is_err());
assert!(validator.validate_url("javascript:alert(1)").is_err());
}
#[test]
fn test_email_validation() {
let validator = InputValidator::new();
assert!(validator.validate_email("user@example.com").is_ok());
assert!(validator.validate_email("invalid-email").is_err());
assert!(validator.validate_email("@example.com").is_err());
}
#[test]
fn test_sanitization() {
let validator = InputValidator::new();
let input = "hello; world ${test}";
let sanitized = validator.sanitize_string(input).unwrap();
assert!(!sanitized.contains(';'));
assert!(!sanitized.contains('$'));
assert_eq!(sanitized, "hello world test");
}
#[test]
fn test_config_validation() {
let validator = ConfigValidator::new();
let mut config = HashMap::new();
config.insert("app_name".to_string(), "my-app".to_string());
config.insert("app_port".to_string(), "8080".to_string());
config.insert("database_password".to_string(), "secret".to_string());
let result = validator.validate(&config);
assert!(result.is_valid());
assert!(result.has_sensitive_data());
assert_eq!(result.sensitive_fields.len(), 1);
}
#[test]
fn test_custom_sensitive_field() {
let detector = SensitiveDataDetector::new();
let mut custom_detector = detector.clone();
custom_detector.add_custom_sensitive_field("custom_field");
assert!(custom_detector
.is_sensitive("custom_field", "value")
.needs_protection());
}
#[test]
fn test_detector_default_and_clone() {
let detector = SensitiveDataDetector::default();
let high = detector.is_sensitive("password", "v");
assert!(matches!(high, SensitivityResult::High { .. }));
let cloned = detector.clone();
assert!(cloned.is_sensitive("token", "v").needs_protection());
assert!(detector.is_sensitive("name", "value").is_low());
}
#[test]
fn test_sensitivity_result_variants_and_description() {
let low = SensitivityResult::Low;
assert!(low.is_low());
assert!(!low.needs_protection());
assert_eq!(low.description(), "low sensitivity");
let medium = SensitivityResult::Medium {
field: "api_token".to_string(),
reason: "custom".to_string(),
};
assert!(!medium.is_low());
assert!(medium.needs_protection());
assert!(medium.description().contains("medium sensitivity"));
assert!(medium.description().contains("api_token"));
let high = SensitivityResult::High {
field: "password".to_string(),
reason: "kw".to_string(),
};
assert!(!high.is_low());
assert!(high.needs_protection());
assert!(high.description().contains("high sensitivity"));
}
#[test]
fn test_detect_all_filters_low() {
let detector = SensitiveDataDetector::new();
let mut data = HashMap::new();
data.insert("password".to_string(), "secret".to_string());
data.insert("username".to_string(), "alice".to_string());
data.insert("api_key".to_string(), "val".to_string());
let results = detector.detect_all(&data);
assert_eq!(results.len(), 2);
for (_, r) in &results {
assert!(r.needs_protection());
}
}
#[test]
fn test_custom_sensitive_field_case_insensitive() {
let mut detector = SensitiveDataDetector::new();
detector.add_custom_sensitive_field("MyCustomField");
assert!(detector
.is_sensitive("mycustomfield", "v")
.needs_protection());
assert!(detector
.is_sensitive("MYCUSTOMFIELD", "v")
.needs_protection());
let r = detector.is_sensitive("mycustomfield", "v");
assert!(matches!(r, SensitivityResult::Medium { .. }));
}
#[test]
fn test_xss_input_rejected() {
let validator = InputValidator::new();
assert!(validator
.validate_string("<script>alert(1)</script>")
.is_err());
assert!(validator.validate_string("<img onerror=alert(1)>").is_err());
assert!(validator.validate_string("<svg onload=alert(1)>").is_err());
assert!(validator
.validate_string("javascript:alert(document.cookie)")
.is_err());
let sanitized = validator
.sanitize_string("<script>alert(1)</script>")
.unwrap();
assert!(!sanitized.contains('<'));
assert!(!sanitized.contains('>'));
assert!(!sanitized.contains('('));
}
#[test]
fn test_sql_injection_rejected() {
let validator = InputValidator::new();
assert!(validator.validate_string("1; DROP TABLE users").is_err());
assert!(validator
.validate_string("1 UNION SELECT password FROM users")
.is_err());
assert!(validator.validate_string("' OR '1'='1").is_err());
assert!(validator.validate_string("admin'--").is_err());
assert!(validator.validate_string("; DELETE FROM accounts").is_err());
}
#[test]
fn test_path_traversal_rejected() {
let validator = InputValidator::new();
assert!(validator.validate_string("../../etc/passwd").is_err());
assert!(validator
.validate_string("..\\..\\windows\\system32")
.is_err());
assert!(validator.validate_string("/etc/../passwd").is_err());
assert!(validator
.validate_string("var/log/../../etc/shadow")
.is_err());
}
#[test]
fn test_command_injection_rejected() {
let validator = InputValidator::new();
assert!(validator.validate_string("; rm -rf /").is_err());
assert!(validator.validate_string("| cat /etc/passwd").is_err());
assert!(validator.validate_string("true && whoami").is_err());
assert!(validator.validate_string("false || malicious").is_err());
assert!(validator.validate_string("$(whoami)").is_err());
assert!(validator.validate_string("`whoami`").is_err());
assert!(validator.validate_string("${HOME}").is_err());
assert!(validator.validate_string(">> /tmp/file").is_err());
assert!(validator.validate_string("2> /dev/null").is_err());
}
#[test]
fn test_validate_string_too_long() {
let validator = InputValidator::new().with_max_string_length(5);
assert!(validator.validate_string("abcdef").is_err());
let err = validator.validate_string("abcdef").unwrap_err();
assert!(matches!(
err,
InputValidationError::TooLong { max: 5, actual: 6 }
));
assert!(validator.validate_string("abcde").is_ok());
}
#[test]
fn test_validate_string_invalid_characters() {
let validator = InputValidator::new().with_allowed_chars_pattern(r"^[a-z]+$");
assert!(validator.validate_string("hello").is_ok());
let err = validator.validate_string("Hello").unwrap_err();
assert_eq!(err, InputValidationError::InvalidCharacters);
let err2 = validator.validate_string("hello123").unwrap_err();
assert_eq!(err2, InputValidationError::InvalidCharacters);
}
#[test]
fn test_dangerous_pattern_error_carries_pattern() {
let validator = InputValidator::new();
let err = validator.validate_string("a;b").unwrap_err();
match err {
InputValidationError::DangerousPattern { pattern } => {
assert!(!pattern.is_empty());
}
_ => panic!("expected DangerousPattern"),
}
}
#[test]
fn test_sanitize_string_strips_all_dangerous_chars() {
let validator = InputValidator::new();
let sanitized = validator.sanitize_string(";<>|`$(){}").unwrap();
assert_eq!(sanitized, "");
let sanitized = validator.sanitize_string("he;ll{o}").unwrap();
assert_eq!(sanitized, "hello");
let sanitized = validator.sanitize_string("a\0b").unwrap();
assert_eq!(sanitized, "ab");
}
#[test]
fn test_validate_field_name_variants() {
let validator = InputValidator::new();
assert_eq!(
validator.validate_field_name("").unwrap_err(),
InputValidationError::EmptyFieldName
);
assert_eq!(
validator.validate_field_name("1abc").unwrap_err(),
InputValidationError::InvalidFieldNameFormat
);
assert_eq!(
validator.validate_field_name("a b").unwrap_err(),
InputValidationError::InvalidFieldNameFormat
);
assert!(validator.validate_field_name("a").is_ok());
assert!(validator.validate_field_name("app_name").is_ok());
assert!(validator.validate_field_name("app-name").is_ok());
assert!(validator.validate_field_name("APP").is_ok());
let v = InputValidator::new().with_max_string_length(3);
assert!(matches!(
v.validate_field_name("abcd").unwrap_err(),
InputValidationError::TooLong { max: 3, actual: 4 }
));
}
#[test]
fn test_validate_url_variants() {
let validator = InputValidator::new();
assert_eq!(
validator.validate_url("not a url").unwrap_err(),
InputValidationError::InvalidUrl
);
assert_eq!(
validator.validate_url("ftp://example.com").unwrap_err(),
InputValidationError::InvalidUrlScheme
);
assert_eq!(
validator.validate_url("file:///etc/passwd").unwrap_err(),
InputValidationError::InvalidUrlScheme
);
assert!(matches!(
validator.validate_url("https://example.com;a").unwrap_err(),
InputValidationError::DangerousPattern { .. }
));
assert!(matches!(
validator
.validate_url("https://example.com/${x}")
.unwrap_err(),
InputValidationError::DangerousPattern { .. }
));
let v = InputValidator::new().with_max_string_length(5);
assert!(matches!(
v.validate_url("https://a").unwrap_err(),
InputValidationError::TooLong { .. }
));
assert!(validator.validate_url("https://example.com/path").is_ok());
}
#[test]
fn test_url_with_ampersand_in_query_string() {
let validator = InputValidator::new();
assert!(validator
.validate_url("https://example.com/config?key=val&key2=val2")
.is_ok());
assert!(validator
.validate_url("https://example.com/path;cmd")
.is_err());
assert!(validator
.validate_url("https://example.com/path|cmd")
.is_err());
assert!(validator
.validate_url("https://example.com/path`cmd`")
.is_err());
assert!(validator
.validate_url("https://example.com/path$(cmd)")
.is_err());
}
#[test]
fn test_validate_email_variants() {
let validator = InputValidator::new();
assert_eq!(
validator.validate_email("invalid-email").unwrap_err(),
InputValidationError::InvalidEmail
);
assert_eq!(
validator.validate_email("@example.com").unwrap_err(),
InputValidationError::InvalidEmail
);
assert_eq!(
validator.validate_email("user@").unwrap_err(),
InputValidationError::InvalidEmail
);
assert_eq!(
validator.validate_email("user@example").unwrap_err(),
InputValidationError::InvalidEmail
);
let v = InputValidator::new().with_max_string_length(5);
assert!(matches!(
v.validate_email("a@b.co").unwrap_err(),
InputValidationError::TooLong { .. }
));
assert!(validator
.validate_email("user.name+tag@example.com")
.is_ok());
}
#[test]
fn test_validate_whitelist() {
let validator = InputValidator::new();
assert!(validator.validate_whitelist("anything").is_ok());
let validator = InputValidator::new()
.add_whitelist_pattern(r"^abc")
.add_whitelist_pattern(r"^xyz");
assert!(validator.validate_whitelist("abc123").is_ok());
assert!(validator.validate_whitelist("xyz789").is_ok());
assert_eq!(
validator.validate_whitelist("def456").unwrap_err(),
InputValidationError::NotInWhitelist
);
}
#[test]
fn test_validate_all_collects_errors() {
let validator = InputValidator::new();
let mut data = HashMap::new();
data.insert("valid_name".to_string(), "valid_value".to_string());
data.insert("1bad".to_string(), "ok".to_string());
data.insert("good_name".to_string(), "a;b".to_string());
let errors = validator.validate_all(&data);
assert_eq!(errors.len(), 2);
let names: Vec<&str> = errors.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"1bad"));
assert!(names.contains(&"good_name"));
}
#[test]
fn test_input_validation_error_display_all_variants() {
let s = format!("{}", InputValidationError::TooLong { max: 1, actual: 2 });
assert!(s.contains("max=1"));
assert!(s.contains("actual=2"));
assert!(format!("{}", InputValidationError::InvalidCharacters).contains("invalid"));
assert!(format!(
"{}",
InputValidationError::DangerousPattern {
pattern: "x".into()
}
)
.contains("x"));
assert!(format!("{}", InputValidationError::EmptyFieldName).contains("empty"));
assert!(format!("{}", InputValidationError::InvalidFieldNameFormat).contains("invalid"));
assert!(format!("{}", InputValidationError::InvalidUrl).contains("URL"));
assert!(format!("{}", InputValidationError::InvalidUrlScheme).contains("scheme"));
assert!(format!("{}", InputValidationError::InvalidEmail).contains("Email"));
assert!(format!("{}", InputValidationError::NotInWhitelist).contains("whitelist"));
assert!(format!(
"{}",
InputValidationError::DepthExceeded { max: 1, actual: 2 }
)
.contains("depth"));
assert!(format!(
"{}",
InputValidationError::ArrayTooLong { max: 1, actual: 2 }
)
.contains("Array"));
}
#[test]
fn test_config_validator_builder_and_strict() {
let validator = ConfigValidator::builder()
.max_string_length(10)
.add_sensitive_field("custom_token")
.build();
let mut data = HashMap::new();
data.insert("custom_token".to_string(), "short".to_string());
let result = validator.validate(&data);
assert!(result.has_sensitive_data());
let strict = ConfigValidatorBuilder::new().strict_mode().build();
let mut data = HashMap::new();
data.insert("token".to_string(), "abc".to_string());
data.insert("password".to_string(), "xyz".to_string());
let result = strict.validate(&data);
assert_eq!(result.sensitive_fields.len(), 2);
}
#[test]
fn test_config_validator_default_and_safe() {
let validator = ConfigValidator::default();
let mut data = HashMap::new();
data.insert("app_name".to_string(), "myapp".to_string());
assert!(validator.validate_safe(&data));
let mut bad = HashMap::new();
bad.insert("app_name".to_string(), "a;b".to_string());
assert!(!validator.validate_safe(&bad));
let mut bad_name = HashMap::new();
bad_name.insert("1bad".to_string(), "ok".to_string());
assert!(!validator.validate_safe(&bad_name));
}
#[test]
fn test_config_validation_result_report_and_sensitive() {
let validator = ConfigValidator::new();
let mut ok = HashMap::new();
ok.insert("app_name".to_string(), "myapp".to_string());
let result = validator.validate(&ok);
assert!(result.is_valid());
assert!(!result.has_sensitive_data());
assert_eq!(result.error_report(), "");
let mut bad = HashMap::new();
bad.insert("1bad".to_string(), "a;b".to_string());
let result = validator.validate(&bad);
assert!(!result.is_valid());
assert!(!result.error_report().is_empty());
assert!(result.error_report().contains("1bad"));
}
#[test]
fn test_config_validation_error_display() {
let field_err = ConfigValidationError::FieldError {
field: "f".to_string(),
error: InputValidationError::EmptyFieldName,
};
assert!(format!("{}", field_err).contains("f"));
let warn = ConfigValidationError::SensitiveDataWarning {
field: "pw".to_string(),
sensitivity: SensitivityResult::High {
field: "pw".to_string(),
reason: "r".to_string(),
},
};
let s = format!("{}", warn);
assert!(s.contains("pw"));
assert!(s.contains("high sensitivity"));
}
}