Skip to content

Implement Validation Framework for Struct and Field Validation #61

@thep2p

Description

@thep2p

Overview

The Go implementation uses the validator/v10 library for comprehensive struct validation. This provides declarative validation rules, custom validators, and automatic validation at critical points. The Rust implementation needs a similar validation framework for data integrity and security.

Background

Reference: Go implementation uses github.com/go-playground/validator/v10

Validation is critical for:

  • Input sanitization
  • Data integrity
  • Security (preventing malformed data attacks)
  • Early error detection

Requirements

1. Define Validation Traits

use thiserror::Error;

/// Trait for validatable types
pub trait Validate {
    /// Validate the struct according to its rules
    fn validate(&self) -> Result<(), ValidationError>;
}

/// Validation error types
#[derive(Error, Debug)]
pub enum ValidationError {
    #[error("Field {field} failed validation: {message}")]
    FieldError { field: String, message: String },
    
    #[error("Multiple validation errors: {0:?}")]
    MultipleErrors(Vec<ValidationError>),
    
    #[error("Custom validation failed: {0}")]
    Custom(String),
}

/// Builder pattern for validation errors
pub struct ValidationErrors {
    errors: Vec<ValidationError>,
}

impl ValidationErrors {
    pub fn new() -> Self {
        Self { errors: Vec::new() }
    }
    
    pub fn add_field_error(&mut self, field: &str, message: &str) {
        self.errors.push(ValidationError::FieldError {
            field: field.to_string(),
            message: message.to_string(),
        });
    }
    
    pub fn is_empty(&self) -> bool {
        self.errors.is_empty()
    }
    
    pub fn into_result(self) -> Result<(), ValidationError> {
        if self.errors.is_empty() {
            Ok(())
        } else if self.errors.len() == 1 {
            Err(self.errors.into_iter().next().unwrap())
        } else {
            Err(ValidationError::MultipleErrors(self.errors))
        }
    }
}

2. Implement Derive Macro

Using validator crate or custom proc macro:

use validator::{Validate, ValidationError};

#[derive(Debug, Clone, Validate)]
pub struct Identifier {
    #[validate(length(equal = 32))]
    bytes: Vec<u8>,
}

#[derive(Debug, Clone, Validate)]
pub struct NetworkConfig {
    #[validate(range(min = 1024, max = 65535))]
    pub port: u16,
    
    #[validate(length(min = 1, max = 255))]
    pub node_name: String,
    
    #[validate(url)]
    pub bootstrap_url: Option<String>,
    
    #[validate(custom = "validate_address")]
    pub bind_address: String,
    
    #[validate(range(min = 1, max = 1000))]
    pub max_connections: usize,
}

fn validate_address(address: &str) -> Result<(), ValidationError> {
    use std::net::SocketAddr;
    address.parse::<SocketAddr>()
        .map(|_| ())
        .map_err(|_| ValidationError::new("invalid socket address"))
}

3. Custom Validators

pub mod validators {
    use super::*;
    
    /// Validate that bytes are exactly 32 bytes (for identifiers)
    pub fn validate_identifier_bytes(bytes: &[u8]) -> Result<(), ValidationError> {
        if bytes.len() != 32 {
            return Err(ValidationError::Custom(
                format!("Identifier must be exactly 32 bytes, got {}", bytes.len())
            ));
        }
        Ok(())
    }
    
    /// Validate membership vector format
    pub fn validate_membership_vector(vec: &[u8]) -> Result<(), ValidationError> {
        if vec.is_empty() {
            return Err(ValidationError::Custom(
                "Membership vector cannot be empty".to_string()
            ));
        }
        
        for (i, &byte) in vec.iter().enumerate() {
            if byte != 0 && byte != 1 {
                return Err(ValidationError::Custom(
                    format!("Membership vector byte {} must be 0 or 1, got {}", i, byte)
                ));
            }
        }
        
        Ok(())
    }
    
    /// Validate skip graph level
    pub fn validate_level(level: u32, max_level: u32) -> Result<(), ValidationError> {
        if level > max_level {
            return Err(ValidationError::Custom(
                format!("Level {} exceeds maximum {}", level, max_level)
            ));
        }
        Ok(())
    }
}

4. Manual Validation Implementation

For types that need complex validation:

impl Validate for SearchRequest {
    fn validate(&self) -> Result<(), ValidationError> {
        let mut errors = ValidationErrors::new();
        
        // Validate target ID
        if let Err(e) = validators::validate_identifier_bytes(&self.target_id) {
            errors.add_field_error("target_id", &e.to_string());
        }
        
        // Validate level
        if let Err(e) = validators::validate_level(self.level, MAX_LEVEL) {
            errors.add_field_error("level", &e.to_string());
        }
        
        // Validate request ID format
        if self.request_id.is_empty() {
            errors.add_field_error("request_id", "Request ID cannot be empty");
        }
        
        errors.into_result()
    }
}

5. Integration Points

/// Network layer integration
impl NetworkImpl {
    pub async fn send_message(&self, msg: impl Validate + ProtoMessage) -> Result<(), NetworkError> {
        // Validate before sending
        msg.validate()
            .map_err(|e| NetworkError::ValidationFailed(e))?;
        
        // Continue with sending...
        Ok(())
    }
}

/// API layer integration
pub async fn handle_join_request(req: JoinRequest) -> Result<JoinResponse, ApiError> {
    // Validate request
    req.validate()?;
    
    // Process valid request
    Ok(process_join(req).await?)
}

/// Deserialization integration
impl ProtoMessage for JoinRequest {
    fn from_proto(proto: proto::JoinRequest) -> Result<Self, DecodeError> {
        let req = JoinRequest {
            node_id: Identifier::from_bytes(proto.node_id)?,
            membership_vector: proto.membership_vector,
            address: proto.address,
        };
        
        // Validate after deserialization
        req.validate()
            .map_err(|e| DecodeError::ValidationFailed(e))?;
        
        Ok(req)
    }
}

6. Validation Middleware

/// Middleware for automatic validation
pub struct ValidationMiddleware<T> {
    inner: T,
}

impl<T: MessageProcessor> MessageProcessor for ValidationMiddleware<T> {
    async fn process_incoming_message(
        &self,
        channel: Channel,
        origin_id: Identifier,
        message: Box<dyn Message>,
    ) {
        // Validate origin ID
        if let Err(e) = origin_id.validate() {
            log::warn!("Invalid origin ID from {}: {}", origin_id, e);
            return;
        }
        
        // Forward to inner processor
        self.inner.process_incoming_message(channel, origin_id, message).await;
    }
}

Validation Rules to Implement

  1. Identifiers: Exactly 32 bytes
  2. Membership Vectors: Binary string (0s and 1s only)
  3. Network Addresses: Valid socket addresses
  4. Port Numbers: Valid range (1024-65535 for user ports)
  5. Message Sizes: Within acceptable limits
  6. Timestamps: Not too far in future/past
  7. Channel Names: From allowed set
  8. Node Names: Length and character restrictions

Benefits

  • Security: Prevent malformed data attacks
  • Early Detection: Catch errors at boundaries
  • Declarative: Validation rules in one place
  • Reusable: Common validators across codebase
  • Type Safety: Compile-time validation where possible

Testing Requirements

  • Test all validators with valid/invalid inputs
  • Test validation error messages
  • Test composite validations
  • Test custom validators
  • Performance benchmarks for validation

Dependencies

  • validator (declarative validation)
  • validator_derive (proc macros)
  • thiserror (error handling)

Priority

Medium - Important for security and robustness

Related Issues

  • Enhances: All network and message handling components

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions