//! Input validation utilities for the Intelligence Layer //! //! This module provides validation functions for common input types //! to prevent injection attacks, path traversal, and memory exhaustion. //! //! NOTE: Some functions are defined for future use and external API exposure. #![allow(dead_code)] // Validation functions reserved for future API endpoints use std::fmt; /// Maximum length for identifier strings (agent_id, pipeline_id, skill_id, etc.) pub const MAX_IDENTIFIER_LENGTH: usize = 128; /// Minimum length for identifier strings pub const MIN_IDENTIFIER_LENGTH: usize = 1; /// Allowed characters in identifiers: alphanumeric, hyphen, underscore const IDENTIFIER_ALLOWED_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; /// Validation error types #[derive(Debug, Clone)] pub enum ValidationError { /// Identifier is too long IdentifierTooLong { field: String, max: usize, actual: usize }, /// Identifier is too short or empty IdentifierTooShort { field: String, min: usize, actual: usize }, /// Identifier contains invalid characters InvalidCharacters { field: String, invalid_chars: String }, /// String exceeds maximum length StringTooLong { field: String, max: usize, actual: usize }, /// Required field is missing or empty RequiredFieldEmpty { field: String }, } impl fmt::Display for ValidationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::IdentifierTooLong { field, max, actual } => { write!(f, "Field '{}' is too long: {} characters (max: {})", field, actual, max) } Self::IdentifierTooShort { field, min, actual } => { write!(f, "Field '{}' is too short: {} characters (min: {})", field, actual, min) } Self::InvalidCharacters { field, invalid_chars } => { write!(f, "Field '{}' contains invalid characters: '{}'. Allowed: alphanumeric, '-', '_'", field, invalid_chars) } Self::StringTooLong { field, max, actual } => { write!(f, "Field '{}' is too long: {} characters (max: {})", field, actual, max) } Self::RequiredFieldEmpty { field } => { write!(f, "Required field '{}' is empty", field) } } } } impl std::error::Error for ValidationError {} /// Validate an identifier (agent_id, pipeline_id, skill_id, etc.) /// /// # Rules /// - Length: 1-128 characters /// - Characters: alphanumeric, hyphen (-), underscore (_) /// - Cannot start with hyphen or underscore /// /// # Examples /// ```ignore /// use desktop_lib::intelligence::validation::validate_identifier; /// /// assert!(validate_identifier("agent-123", "agent_id").is_ok()); /// assert!(validate_identifier("my_skill", "skill_id").is_ok()); /// assert!(validate_identifier("", "agent_id").is_err()); /// assert!(validate_identifier("invalid@id", "agent_id").is_err()); /// ``` pub fn validate_identifier(value: &str, field_name: &str) -> Result<(), ValidationError> { let len = value.len(); // Check minimum length if len < MIN_IDENTIFIER_LENGTH { return Err(ValidationError::IdentifierTooShort { field: field_name.to_string(), min: MIN_IDENTIFIER_LENGTH, actual: len, }); } // Check maximum length if len > MAX_IDENTIFIER_LENGTH { return Err(ValidationError::IdentifierTooLong { field: field_name.to_string(), max: MAX_IDENTIFIER_LENGTH, actual: len, }); } // Check for invalid characters let invalid_chars: String = value .chars() .filter(|c| !IDENTIFIER_ALLOWED_CHARS.contains(*c)) .collect(); if !invalid_chars.is_empty() { return Err(ValidationError::InvalidCharacters { field: field_name.to_string(), invalid_chars, }); } // Cannot start with hyphen or underscore (reserved for system use) if value.starts_with('-') || value.starts_with('_') { return Err(ValidationError::InvalidCharacters { field: field_name.to_string(), invalid_chars: value.chars().next().unwrap().to_string(), }); } Ok(()) } /// Validate a string field with a maximum length /// /// # Arguments /// * `value` - The string to validate /// * `field_name` - Name of the field for error messages /// * `max_length` - Maximum allowed length /// /// # Examples /// ```ignore /// use desktop_lib::intelligence::validation::validate_string_length; /// /// assert!(validate_string_length("hello", "message", 100).is_ok()); /// assert!(validate_string_length("", "message", 100).is_err()); /// ``` pub fn validate_string_length(value: &str, field_name: &str, max_length: usize) -> Result<(), ValidationError> { let len = value.len(); if len == 0 { return Err(ValidationError::RequiredFieldEmpty { field: field_name.to_string(), }); } if len > max_length { return Err(ValidationError::StringTooLong { field: field_name.to_string(), max: max_length, actual: len, }); } Ok(()) } /// Validate an optional identifier field /// /// Returns Ok if the value is None or if it contains a valid identifier. pub fn validate_optional_identifier(value: Option<&str>, field_name: &str) -> Result<(), ValidationError> { match value { None => Ok(()), Some(v) if v.is_empty() => Ok(()), // Empty string treated as None Some(v) => validate_identifier(v, field_name), } } /// Validate a list of identifiers pub fn validate_identifiers<'a, I>(values: I, field_name: &str) -> Result<(), ValidationError> where I: IntoIterator, { for value in values { validate_identifier(value, field_name)?; } Ok(()) } /// Sanitize a string for safe logging (remove control characters, limit length) pub fn sanitize_for_logging(value: &str, max_len: usize) -> String { let sanitized: String = value .chars() .filter(|c| !c.is_control() || *c == '\n' || *c == '\t') .take(max_len) .collect(); if value.len() > max_len { format!("{}...", sanitized) } else { sanitized } } #[cfg(test)] mod tests { use super::*; #[test] fn test_valid_identifiers() { assert!(validate_identifier("agent-123", "agent_id").is_ok()); assert!(validate_identifier("my_skill", "skill_id").is_ok()); assert!(validate_identifier("Pipeline42", "pipeline_id").is_ok()); assert!(validate_identifier("a", "test").is_ok()); } #[test] fn test_invalid_identifiers() { // Too short assert!(matches!( validate_identifier("", "agent_id"), Err(ValidationError::IdentifierTooShort { .. }) )); // Too long let long_id = "a".repeat(200); assert!(matches!( validate_identifier(&long_id, "agent_id"), Err(ValidationError::IdentifierTooLong { .. }) )); // Invalid characters assert!(matches!( validate_identifier("invalid@id", "agent_id"), Err(ValidationError::InvalidCharacters { .. }) )); assert!(matches!( validate_identifier("invalid id", "agent_id"), Err(ValidationError::InvalidCharacters { .. }) )); // Starts with reserved characters assert!(matches!( validate_identifier("-invalid", "agent_id"), Err(ValidationError::InvalidCharacters { .. }) )); assert!(matches!( validate_identifier("_invalid", "agent_id"), Err(ValidationError::InvalidCharacters { .. }) )); } #[test] fn test_string_length_validation() { assert!(validate_string_length("hello", "message", 100).is_ok()); assert!(matches!( validate_string_length("", "message", 100), Err(ValidationError::RequiredFieldEmpty { .. }) )); let long_string = "a".repeat(200); assert!(matches!( validate_string_length(&long_string, "message", 100), Err(ValidationError::StringTooLong { .. }) )); } #[test] fn test_optional_identifier() { assert!(validate_optional_identifier(None, "agent_id").is_ok()); assert!(validate_optional_identifier(Some(""), "agent_id").is_ok()); assert!(validate_optional_identifier(Some("valid-id"), "agent_id").is_ok()); assert!(validate_optional_identifier(Some("invalid@id"), "agent_id").is_err()); } #[test] fn test_sanitize_for_logging() { assert_eq!(sanitize_for_logging("hello", 100), "hello"); assert_eq!(sanitize_for_logging("hello\x00world", 100), "helloworld"); assert_eq!(sanitize_for_logging("hello\nworld", 100), "hello\nworld"); assert_eq!(sanitize_for_logging("hello world", 5), "hello..."); } }