use crate::{
cilassembly::{Operation, TableModifications},
metadata::{
tables::{TableDataOwned, TableId},
validation::{
context::{RawValidationContext, ValidationContext},
traits::RawValidator,
},
},
Result,
};
use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::HashMap;
pub struct RawOperationValidator;
impl RawOperationValidator {
#[must_use]
pub fn new() -> Self {
Self
}
fn validate_insert_operations(
table_changes: &HashMap<TableId, TableModifications>,
) -> Result<()> {
for (table_id, modifications) in table_changes {
if let TableModifications::Sparse {
operations,
next_rid,
original_row_count,
..
} = modifications
{
let mut insert_rids = FxHashSet::default();
for operation in operations {
if let Operation::Insert(rid, table_data) = &operation.operation {
if *rid == 0 {
return Err(malformed_error!(
"Insert operation for table {:?} has invalid RID 0 - RID 0 is reserved",
table_id
));
}
if *rid > 0xFF_FFFF {
return Err(malformed_error!(
"Insert operation for table {:?} has RID {} exceeding maximum metadata token limit",
table_id,
rid
));
}
if *rid <= *original_row_count {
return Err(malformed_error!(
"Insert operation for table {:?} targets RID {} which conflicts with existing row (original count: {})",
table_id,
rid,
original_row_count
));
}
if *rid >= *next_rid + 1000 {
return Err(malformed_error!(
"Insert operation for table {:?} has RID {} too far ahead of next available RID {} - potential RID exhaustion",
table_id,
rid,
next_rid
));
}
if !insert_rids.insert(*rid) {
return Err(malformed_error!(
"Multiple insert operations for table {:?} target the same RID {}",
table_id,
rid
));
}
if !Self::validate_table_data_compatibility(*table_id, table_data) {
return Err(malformed_error!(
"Insert operation for table {:?} has incompatible table data type",
table_id
));
}
}
}
}
}
Ok(())
}
fn validate_update_operations(
table_changes: &HashMap<TableId, TableModifications>,
) -> Result<()> {
for (table_id, modifications) in table_changes {
if let TableModifications::Sparse {
operations,
original_row_count,
deleted_rows,
..
} = modifications
{
let mut update_rids = FxHashMap::default();
for operation in operations {
if let Operation::Update(rid, table_data) = &operation.operation {
if *rid == 0 {
return Err(malformed_error!(
"Update operation for table {:?} has invalid RID 0 - RID 0 is reserved",
table_id
));
}
let targets_original = *rid <= *original_row_count;
let targets_inserted = operations.iter().any(|op| {
matches!(&op.operation, Operation::Insert(insert_rid, _) if insert_rid == rid)
});
if !targets_original && !targets_inserted {
return Err(malformed_error!(
"Update operation for table {:?} targets non-existent RID {}",
table_id,
rid
));
}
if deleted_rows.contains(rid) {
return Err(malformed_error!(
"Update operation for table {:?} targets deleted RID {}",
table_id,
rid
));
}
let update_count = update_rids.entry(*rid).or_insert(0);
*update_count += 1;
if *update_count > 10 {
return Err(malformed_error!(
"Excessive update operations ({}) for table {:?} RID {} - potential update loop",
update_count,
table_id,
rid
));
}
if !Self::validate_table_data_compatibility(*table_id, table_data) {
return Err(malformed_error!(
"Update operation for table {:?} has incompatible table data type",
table_id
));
}
}
}
}
}
Ok(())
}
fn validate_delete_operations(
table_changes: &HashMap<TableId, TableModifications>,
) -> Result<()> {
for (table_id, modifications) in table_changes {
if let TableModifications::Sparse {
operations,
original_row_count,
..
} = modifications
{
let mut delete_rids = FxHashSet::default();
for operation in operations {
if let Operation::Delete(rid) = &operation.operation {
if *rid == 0 {
return Err(malformed_error!(
"Delete operation for table {:?} has invalid RID 0 - RID 0 is reserved",
table_id
));
}
let targets_original = *rid <= *original_row_count;
let targets_inserted = operations.iter().any(|op| {
matches!(&op.operation, Operation::Insert(insert_rid, _) if insert_rid == rid)
});
if !targets_original && !targets_inserted {
return Err(malformed_error!(
"Delete operation for table {:?} targets non-existent RID {}",
table_id,
rid
));
}
if !delete_rids.insert(*rid) {
return Err(malformed_error!(
"Multiple delete operations for table {:?} target the same RID {}",
table_id,
rid
));
}
if matches!(table_id, TableId::Module | TableId::Assembly) && *rid == 1 {
return Err(malformed_error!(
"Delete operation for critical table {:?} targets RID 1 - cannot delete primary metadata entry",
table_id
));
}
}
}
}
}
Ok(())
}
fn validate_operation_sequences(
table_changes: &HashMap<TableId, TableModifications>,
) -> Result<()> {
for (table_id, modifications) in table_changes {
if let TableModifications::Sparse { operations, .. } = modifications {
for window in operations.windows(2) {
if window[0].timestamp > window[1].timestamp {
return Err(malformed_error!(
"Operations for table {:?} are not chronologically ordered - timestamp {} > {}",
table_id,
window[0].timestamp,
window[1].timestamp
));
}
}
let mut rid_operations: FxHashMap<u32, Vec<&Operation>> = FxHashMap::default();
for operation in operations {
let rid = operation.operation.get_rid();
rid_operations
.entry(rid)
.or_default()
.push(&operation.operation);
}
for (rid, ops) in rid_operations {
let mut has_insert = false;
let mut has_delete = false;
for op in &ops {
match op {
Operation::Insert(_, _) => {
if has_insert {
return Err(malformed_error!(
"Multiple insert operations for table {:?} RID {} - invalid sequence",
table_id,
rid
));
}
if has_delete {
return Err(malformed_error!(
"Insert operation after delete for table {:?} RID {} - invalid sequence",
table_id,
rid
));
}
has_insert = true;
}
Operation::Update(_, _) => {
if has_delete {
return Err(malformed_error!(
"Update operation after delete for table {:?} RID {} - invalid sequence",
table_id,
rid
));
}
}
Operation::Delete(_) => {
if has_delete {
return Err(malformed_error!(
"Multiple delete operations for table {:?} RID {} - invalid sequence",
table_id,
rid
));
}
has_delete = true;
}
}
}
}
}
}
Ok(())
}
fn validate_table_data_compatibility(table_id: TableId, table_data: &TableDataOwned) -> bool {
let data_table_id = table_data.table_id();
data_table_id == table_id
}
}
impl RawValidator for RawOperationValidator {
fn validate_raw(&self, context: &RawValidationContext) -> Result<()> {
if let Some(changes) = context.changes() {
let table_changes = &changes.table_changes;
Self::validate_insert_operations(table_changes)?;
Self::validate_update_operations(table_changes)?;
Self::validate_delete_operations(table_changes)?;
Self::validate_operation_sequences(table_changes)?;
}
Ok(())
}
fn name(&self) -> &'static str {
"RawOperationValidator"
}
fn priority(&self) -> u32 {
110
}
fn should_run(&self, context: &RawValidationContext) -> bool {
context.config().enable_structural_validation && context.is_modification_validation()
}
}
impl Default for RawOperationValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cilassembly::AssemblyChanges,
metadata::{
cilassemblyview::CilAssemblyView,
validation::{
context::RawValidationContext, scanner::ReferenceScanner, ValidationConfig,
},
},
test::{
factories::validation::raw_modification_operation::*, get_testfile_wb, validator_test,
},
Error,
};
use rayon::ThreadPoolBuilder;
#[test]
fn test_raw_operation_validator() -> Result<()> {
let validator = RawOperationValidator::new();
let config = ValidationConfig {
enable_structural_validation: true,
..Default::default()
};
validator_test(
raw_operation_validator_file_factory,
"RawOperationValidator",
"Malformed",
config,
|context| validator.validate_raw(context),
)
}
#[test]
fn test_raw_operation_validator_direct_corruption() -> Result<()> {
let validator = RawOperationValidator::new();
{
let corrupted_changes = create_corrupted_changes_with_invalid_rid_zero();
let result = test_validator_with_corrupted_changes(&validator, corrupted_changes);
assert!(result.is_err(), "Validator should reject RID 0 operation");
let error_msg = format!("{:?}", result.unwrap_err());
assert!(
error_msg.contains("RID 0 is reserved"),
"Error should mention RID 0 is reserved. Got: {error_msg}"
);
}
{
let corrupted_changes = create_corrupted_changes_with_excessive_rid();
let result = test_validator_with_corrupted_changes(&validator, corrupted_changes);
assert!(result.is_err(), "Validator should reject excessive RID");
let error_msg = format!("{:?}", result.unwrap_err());
assert!(
error_msg.contains("exceeding maximum metadata token limit"),
"Error should mention token limit. Got: {error_msg}"
);
}
{
let corrupted_changes = create_corrupted_changes_with_nonexistent_target();
let result = test_validator_with_corrupted_changes(&validator, corrupted_changes);
assert!(
result.is_err(),
"Validator should reject update to non-existent row"
);
let error_msg = format!("{:?}", result.unwrap_err());
assert!(
error_msg.contains("targets non-existent RID"),
"Error should mention non-existent RID. Got: {error_msg}"
);
}
{
let corrupted_changes = create_corrupted_changes_with_update_after_delete();
let result = test_validator_with_corrupted_changes(&validator, corrupted_changes);
assert!(
result.is_err(),
"Validator should reject update after delete"
);
let error_msg = format!("{:?}", result.unwrap_err());
assert!(
error_msg.contains("Update operation") && error_msg.contains("deleted RID"),
"Error should mention update operation on deleted RID. Got: {error_msg}"
);
}
{
let corrupted_changes = create_corrupted_changes_with_excessive_updates();
let result = test_validator_with_corrupted_changes(&validator, corrupted_changes);
assert!(result.is_err(), "Validator should reject excessive updates");
let error_msg = format!("{:?}", result.unwrap_err());
assert!(
error_msg.contains("Excessive update operations"),
"Error should mention excessive updates. Got: {error_msg}"
);
}
{
let corrupted_changes = create_corrupted_changes_with_unordered_operations();
let result = test_validator_with_corrupted_changes(&validator, corrupted_changes);
assert!(
result.is_err(),
"Validator should reject unordered operations"
);
let error_msg = format!("{:?}", result.unwrap_err());
assert!(
error_msg.contains("not chronologically ordered"),
"Error should mention chronological order. Got: {error_msg}"
);
}
{
let corrupted_changes = create_corrupted_changes_with_conflicting_inserts();
let result = test_validator_with_corrupted_changes(&validator, corrupted_changes);
assert!(
result.is_err(),
"Validator should reject conflicting inserts"
);
let error_msg = format!("{:?}", result.unwrap_err());
assert!(
error_msg.contains("Multiple insert operations"),
"Error should mention multiple inserts. Got: {error_msg}"
);
}
Ok(())
}
fn test_validator_with_corrupted_changes(
validator: &RawOperationValidator,
corrupted_changes: AssemblyChanges,
) -> Result<()> {
let Some(clean_testfile) = get_testfile_wb() else {
return Err(Error::Other("WindowsBase.dll not available".to_string()));
};
let view = CilAssemblyView::from_path(&clean_testfile)?;
let config = ValidationConfig {
enable_structural_validation: true,
..Default::default()
};
let scanner = ReferenceScanner::from_view(&view)?;
let thread_pool = ThreadPoolBuilder::new().num_threads(4).build().unwrap();
let context = RawValidationContext::new_for_modification(
&view,
&corrupted_changes,
&scanner,
&config,
&thread_pool,
);
validator.validate_raw(&context)
}
}