Files
zclaw_openfang/desktop/src-tauri/src/intelligence/validation.rs
iven bf6d81f9c6
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor: 清理未使用代码并添加未来功能标记
style: 统一代码格式和注释风格

docs: 更新多个功能文档的完整度和状态

feat(runtime): 添加路径验证工具支持

fix(pipeline): 改进条件判断和变量解析逻辑

test(types): 为ID类型添加全面测试用例

chore: 更新依赖项和Cargo.lock文件

perf(mcp): 优化MCP协议传输和错误处理
2026-03-25 21:55:12 +08:00

273 lines
8.9 KiB
Rust

//! 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<Item = &'a str>,
{
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...");
}
}