use crate::{ParaglobHeader, PatternDataMapping};
use matchy_data_format::DataValue;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use zerocopy::FromBytes;
pub trait EntryValidator: Send + Sync {
fn validate(
&self,
key: &str,
data: &HashMap<String, DataValue>,
) -> Result<(), Box<dyn Error + Send + Sync>>;
}
#[derive(Debug, Clone)]
pub struct FormatValidationResult {
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub stats: FormatStats,
}
impl FormatValidationResult {
#[must_use]
pub fn new() -> Self {
Self {
errors: Vec::new(),
warnings: Vec::new(),
stats: FormatStats::default(),
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn error(&mut self, msg: String) {
self.errors.push(msg);
}
pub fn warning(&mut self, msg: String) {
self.warnings.push(msg);
}
}
impl Default for FormatValidationResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct FormatStats {
pub mappings_validated: usize,
pub patterns_with_data: usize,
pub duplicate_mappings: usize,
}
#[must_use]
pub fn validate_data_mapping_consistency(
buffer: &[u8],
header: &ParaglobHeader,
) -> FormatValidationResult {
let mut result = FormatValidationResult::new();
let mapping_offset = header.mapping_table_offset as usize;
let mapping_count = header.mapping_count as usize;
let data_offset = header.data_section_offset as usize;
let data_size = header.data_section_size as usize;
if mapping_count == 0 {
return result;
}
if mapping_offset == 0 {
result.warning("Mapping table offset is 0 but mapping_count > 0".to_string());
return result;
}
let mut patterns_with_data = HashSet::new();
let mut duplicate_mappings = 0;
for i in 0..mapping_count {
let entry_offset = mapping_offset + i * std::mem::size_of::<PatternDataMapping>();
if entry_offset + std::mem::size_of::<PatternDataMapping>() > buffer.len() {
result.error(format!(
"Mapping entry {i} at offset {entry_offset} truncated"
));
continue;
}
let mapping = match PatternDataMapping::read_from_prefix(&buffer[entry_offset..]) {
Ok((m, _)) => m,
Err(_) => {
result.error(format!(
"Failed to read PatternDataMapping at offset {entry_offset}"
));
continue;
}
};
if !patterns_with_data.insert(mapping.pattern_id) {
duplicate_mappings += 1;
}
if mapping.pattern_id >= header.pattern_count {
result.error(format!(
"Mapping entry {} references invalid pattern ID {} (max: {})",
i,
mapping.pattern_id,
header.pattern_count - 1
));
continue;
}
if header.has_inline_data() {
let data_ref = mapping.data_offset as usize;
if data_ref >= data_offset && data_ref < data_offset + data_size {
let data_end = data_ref + mapping.data_size as usize;
if data_end > data_offset + data_size {
result.error(format!(
"Mapping entry {} data range [{}, {}) exceeds data section [{}, {})",
i,
data_ref,
data_end,
data_offset,
data_offset + data_size
));
}
}
}
result.stats.mappings_validated += 1;
}
result.stats.patterns_with_data = patterns_with_data.len();
result.stats.duplicate_mappings = duplicate_mappings;
if duplicate_mappings > 0 {
result.warning(format!(
"Found {duplicate_mappings} duplicate pattern IDs in data mapping table"
));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_header(pattern_count: u32, mapping_count: u32) -> ParaglobHeader {
let mut header = ParaglobHeader::new();
header.pattern_count = pattern_count;
header.mapping_count = mapping_count;
header.mapping_table_offset = 1000; header.data_section_offset = 5000;
header.data_section_size = 1000;
header.data_flags = 0x01; header
}
fn encode_mapping(pattern_id: u32, data_offset: u32, data_size: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&pattern_id.to_le_bytes());
buf.extend_from_slice(&data_offset.to_le_bytes());
buf.extend_from_slice(&data_size.to_le_bytes());
buf
}
#[test]
fn test_validate_no_mappings() {
let header = create_test_header(10, 0);
let buffer = vec![0u8; 6000];
let result = validate_data_mapping_consistency(&buffer, &header);
assert!(result.is_valid());
assert_eq!(result.stats.mappings_validated, 0);
}
#[test]
fn test_validate_valid_mappings() {
let header = create_test_header(10, 3);
let mut buffer = vec![0u8; 6000];
let mappings = vec![
encode_mapping(0, 5100, 50),
encode_mapping(1, 5200, 50),
encode_mapping(2, 5300, 50),
];
let mut offset = 1000;
for mapping_bytes in mappings {
buffer[offset..offset + mapping_bytes.len()].copy_from_slice(&mapping_bytes);
offset += mapping_bytes.len();
}
let result = validate_data_mapping_consistency(&buffer, &header);
assert!(result.is_valid());
assert_eq!(result.stats.mappings_validated, 3);
assert_eq!(result.stats.patterns_with_data, 3);
assert_eq!(result.stats.duplicate_mappings, 0);
}
#[test]
fn test_validate_duplicate_pattern_ids() {
let header = create_test_header(10, 3);
let mut buffer = vec![0u8; 6000];
let mappings = vec![
encode_mapping(0, 5100, 50),
encode_mapping(1, 5200, 50),
encode_mapping(0, 5300, 50), ];
let mut offset = 1000;
for mapping_bytes in mappings {
buffer[offset..offset + mapping_bytes.len()].copy_from_slice(&mapping_bytes);
offset += mapping_bytes.len();
}
let result = validate_data_mapping_consistency(&buffer, &header);
assert!(result.is_valid()); assert_eq!(result.warnings.len(), 1);
assert_eq!(result.stats.duplicate_mappings, 1);
assert_eq!(result.stats.patterns_with_data, 2); }
#[test]
fn test_validate_invalid_pattern_id() {
let header = create_test_header(10, 2);
let mut buffer = vec![0u8; 6000];
let mappings = vec![
encode_mapping(5, 5100, 50),
encode_mapping(99, 5200, 50), ];
let mut offset = 1000;
for mapping_bytes in mappings {
buffer[offset..offset + mapping_bytes.len()].copy_from_slice(&mapping_bytes);
offset += mapping_bytes.len();
}
let result = validate_data_mapping_consistency(&buffer, &header);
assert!(!result.is_valid());
assert_eq!(result.errors.len(), 1);
assert!(result.errors[0].contains("invalid pattern ID 99"));
}
#[test]
fn test_validate_data_bounds() {
let header = create_test_header(10, 2);
let mut buffer = vec![0u8; 6000];
let mappings = vec![
encode_mapping(0, 5100, 50), encode_mapping(1, 5900, 200), ];
let mut offset = 1000;
for mapping_bytes in mappings {
buffer[offset..offset + mapping_bytes.len()].copy_from_slice(&mapping_bytes);
offset += mapping_bytes.len();
}
let result = validate_data_mapping_consistency(&buffer, &header);
assert!(!result.is_valid());
assert_eq!(result.errors.len(), 1);
assert!(result.errors[0].contains("exceeds data section"));
}
#[test]
fn test_validate_truncated_mapping_table() {
let header = create_test_header(10, 3);
let buffer = vec![0u8; 1020];
let result = validate_data_mapping_consistency(&buffer, &header);
assert!(!result.is_valid());
assert!(result.errors.iter().any(|e| e.contains("truncated")));
}
}