use super::super::{Error, SpecValidator, ValidationIssue};
use super::helpers::build_test_vhdx;
use crate::constants::{
HEADER2_OFFSET, METADATA_TABLE_SIZE, REGION_TABLE_SIZE, REGION_TABLE1_OFFSET,
REGION_TABLE2_OFFSET,
};
use bitvec::prelude::*;
use crc32c::crc32c;
#[test]
fn test_valid_vhdx_no_issues() {
let buf = build_test_vhdx();
let validator = SpecValidator::new(&buf, true);
let issues = validator.validate_file().unwrap();
assert!(issues.is_empty(), "valid VHDX should produce no issues");
}
#[test]
fn test_optional_unknown_region_pushes_issue() {
let mut buf = build_test_vhdx();
let rt_offset = REGION_TABLE1_OFFSET as usize;
buf[rt_offset + 8..rt_offset + 12].copy_from_slice(&3u32.to_le_bytes());
let entry_start = rt_offset + 16 + 2 * 32;
let unknown_guid: [u8; 16] = [
0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
0xBB,
];
buf[entry_start..entry_start + 16].copy_from_slice(&unknown_guid);
let offset: u64 = 4 * 1024 * 1024;
buf[entry_start + 16..entry_start + 24].copy_from_slice(&offset.to_le_bytes());
let length: u32 = 1024 * 1024;
buf[entry_start + 24..entry_start + 28].copy_from_slice(&length.to_le_bytes());
buf[entry_start + 28..entry_start + 32].copy_from_slice(&0u32.to_le_bytes());
buf[rt_offset + 4..rt_offset + 8].copy_from_slice(&0u32.to_le_bytes());
let checksum = crc32c(&buf[rt_offset..][..REGION_TABLE_SIZE as usize]);
buf[rt_offset + 4..rt_offset + 8].copy_from_slice(&checksum.to_le_bytes());
let rt2_offset = REGION_TABLE2_OFFSET as usize;
buf[rt2_offset + 8..rt2_offset + 12].copy_from_slice(&3u32.to_le_bytes());
let rt2_entry_start = rt2_offset + 16 + 2 * 32;
buf[rt2_entry_start..rt2_entry_start + 16].copy_from_slice(&unknown_guid);
buf[rt2_entry_start + 16..rt2_entry_start + 24].copy_from_slice(&offset.to_le_bytes());
buf[rt2_entry_start + 24..rt2_entry_start + 28].copy_from_slice(&length.to_le_bytes());
buf[rt2_entry_start + 28..rt2_entry_start + 32].copy_from_slice(&0u32.to_le_bytes());
buf[rt2_offset + 4..rt2_offset + 8].copy_from_slice(&0u32.to_le_bytes());
let checksum2 = crc32c(&buf[rt2_offset..][..REGION_TABLE_SIZE as usize]);
buf[rt2_offset + 4..rt2_offset + 8].copy_from_slice(&checksum2.to_le_bytes());
let needed = usize::try_from(offset + u64::from(length)).expect("needed size fits usize");
if buf.len() < needed {
buf.resize(needed, 0);
}
let validator = SpecValidator::new(&buf, false);
let issues = validator.validate_region_table().unwrap();
assert!(
!issues.is_empty(),
"expected at least one issue for optional unknown region"
);
let found = issues.iter().any(|i| i.code() == "REGION_OPTIONAL_UNKNOWN");
assert!(
found,
"expected REGION_OPTIONAL_UNKNOWN issue, got: {:?}",
issues.iter().map(ValidationIssue::code).collect::<Vec<_>>()
);
let issue = issues
.iter()
.find(|i| i.code() == "REGION_OPTIONAL_UNKNOWN")
.unwrap();
assert_eq!(issue.section(), "region_table");
assert_eq!(issue.spec_ref(), "RELAX");
assert!(issue.message().contains("tolerated"));
}
#[test]
fn test_optional_unknown_metadata_pushes_issue() {
let mut buf = build_test_vhdx();
let rt_offset = REGION_TABLE1_OFFSET as usize;
let metadata_offset =
u64::from_le_bytes(buf[rt_offset + 64..rt_offset + 72].try_into().unwrap());
let mo = usize::try_from(metadata_offset).expect("metadata offset fits usize");
buf[mo + 10..mo + 12].copy_from_slice(&7u16.to_le_bytes());
let entry_off = mo + 32 + 6 * 32;
let unknown_guid: [u8; 16] = [
0xDE, 0xAD, 0xBE, 0xEF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
0xBB,
];
buf[entry_off..entry_off + 16].copy_from_slice(&unknown_guid);
buf[entry_off + 16..entry_off + 20].copy_from_slice(&0u32.to_le_bytes());
buf[entry_off + 20..entry_off + 24].copy_from_slice(&0u32.to_le_bytes());
buf[entry_off + 24..entry_off + 28].copy_from_slice(&0u32.to_le_bytes());
buf[entry_off + 28..entry_off + 32].copy_from_slice(&0u32.to_le_bytes());
let validator = SpecValidator::new(&buf, false);
let issues = validator.validate_metadata().unwrap();
let found = issues
.iter()
.any(|i| i.code() == "METADATA_OPTIONAL_UNKNOWN");
assert!(
found,
"expected METADATA_OPTIONAL_UNKNOWN issue, got: {:?}",
issues.iter().map(ValidationIssue::code).collect::<Vec<_>>()
);
let issue = issues
.iter()
.find(|i| i.code() == "METADATA_OPTIONAL_UNKNOWN")
.unwrap();
assert_eq!(issue.section(), "metadata");
assert_eq!(issue.spec_ref(), "RELAX");
}
#[test]
fn test_strict_true_no_issue_for_optional_unknown_region() {
let mut buf = build_test_vhdx();
let rt_offset = REGION_TABLE1_OFFSET as usize;
buf[rt_offset + 8..rt_offset + 12].copy_from_slice(&3u32.to_le_bytes());
let entry_start = rt_offset + 16 + 2 * 32;
let unknown_guid: [u8; 16] = [
0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
0xBB,
];
buf[entry_start..entry_start + 16].copy_from_slice(&unknown_guid);
let offset: u64 = 4 * 1024 * 1024;
buf[entry_start + 16..entry_start + 24].copy_from_slice(&offset.to_le_bytes());
let length: u32 = 1024 * 1024;
buf[entry_start + 24..entry_start + 28].copy_from_slice(&length.to_le_bytes());
buf[entry_start + 28..entry_start + 32].copy_from_slice(&0u32.to_le_bytes());
buf[rt_offset + 4..rt_offset + 8].copy_from_slice(&0u32.to_le_bytes());
let checksum = crc32c(&buf[rt_offset..][..REGION_TABLE_SIZE as usize]);
buf[rt_offset + 4..rt_offset + 8].copy_from_slice(&checksum.to_le_bytes());
let rt2_offset = REGION_TABLE2_OFFSET as usize;
buf[rt2_offset + 8..rt2_offset + 12].copy_from_slice(&3u32.to_le_bytes());
let rt2_entry_start = rt2_offset + 16 + 2 * 32;
buf[rt2_entry_start..rt2_entry_start + 16].copy_from_slice(&unknown_guid);
buf[rt2_entry_start + 16..rt2_entry_start + 24].copy_from_slice(&offset.to_le_bytes());
buf[rt2_entry_start + 24..rt2_entry_start + 28].copy_from_slice(&length.to_le_bytes());
buf[rt2_entry_start + 28..rt2_entry_start + 32].copy_from_slice(&0u32.to_le_bytes());
buf[rt2_offset + 4..rt2_offset + 8].copy_from_slice(&0u32.to_le_bytes());
let checksum2 = crc32c(&buf[rt2_offset..][..REGION_TABLE_SIZE as usize]);
buf[rt2_offset + 4..rt2_offset + 8].copy_from_slice(&checksum2.to_le_bytes());
let needed = usize::try_from(offset + u64::from(length)).expect("needed size fits usize");
if buf.len() < needed {
buf.resize(needed, 0);
}
let validator = SpecValidator::new(&buf, true);
assert!(validator.validate_region_table().is_err());
}
#[test]
fn test_metadata_entry_reserved_nonzero() {
let mut buf = build_test_vhdx();
let rt_offset = REGION_TABLE1_OFFSET as usize;
let metadata_offset =
u64::from_le_bytes(buf[rt_offset + 64..rt_offset + 72].try_into().unwrap());
let mo = usize::try_from(metadata_offset).expect("metadata offset fits usize");
let reserved_off = mo + 32 + 28;
buf[reserved_off..reserved_off + 4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
let validator = SpecValidator::new(&buf, true);
let result = validator.validate_metadata();
assert!(result.is_err());
let err = result.unwrap_err();
match &err {
Error::MetadataEntryReservedNonzero { reserved } => {
assert_eq!(*reserved, 0xDEAD_BEEF);
}
other => panic!("expected MetadataEntryReservedNonzero error, got: {other:?}"),
}
}
#[test]
fn test_metadata_file_parameters_reserved_flags() {
let mut buf = build_test_vhdx();
let rt_offset = REGION_TABLE1_OFFSET as usize;
let metadata_offset =
u64::from_le_bytes(buf[rt_offset + 64..rt_offset + 72].try_into().unwrap());
let mo = usize::try_from(metadata_offset).expect("metadata offset fits usize");
let fp_data_off = mo + METADATA_TABLE_SIZE as usize;
buf[fp_data_off..fp_data_off + 8]
.view_bits_mut::<Lsb0>()
.set(34, true);
let validator = SpecValidator::new(&buf, true);
let result = validator.validate_required_metadata_items();
assert!(
result.is_err(),
"expected Err for reserved flags, got: {result:?}"
);
let err = result.unwrap_err();
match &err {
Error::FileParametersReservedFlags { flags } => {
assert_ne!(*flags, 0, "flags should be non-zero");
}
other => panic!("expected FileParametersReservedFlags error, got: {other:?}"),
}
}
#[test]
fn validate_header_accepts_single_valid_header() {
let mut buf = build_test_vhdx();
buf[HEADER2_OFFSET as usize] = 0xFF;
let validator = SpecValidator::new(&buf, true);
assert!(
validator.validate_header().is_ok(),
"validate_header should accept single valid header"
);
}