Skip to content

Implement Validation Framework for Struct and Field Validation #61

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

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions