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
style: 统一代码格式和注释风格 docs: 更新多个功能文档的完整度和状态 feat(runtime): 添加路径验证工具支持 fix(pipeline): 改进条件判断和变量解析逻辑 test(types): 为ID类型添加全面测试用例 chore: 更新依赖项和Cargo.lock文件 perf(mcp): 优化MCP协议传输和错误处理
273 lines
8.9 KiB
Rust
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...");
|
|
}
|
|
}
|