Files
clawhdf5/crates/clawhdf5-format/tests/robustness_tests.rs
T

315 lines
9.1 KiB
Rust

//! Robustness tests: verify parsers return errors (not panics) on malformed input.
use clawhdf5_format::btree_v2::BTreeV2Header;
use clawhdf5_format::dataspace::Dataspace;
use clawhdf5_format::datatype::Datatype;
use clawhdf5_format::error::FormatError;
use clawhdf5_format::fractal_heap::FractalHeapHeader;
use clawhdf5_format::object_header::ObjectHeader;
use clawhdf5_format::signature;
use clawhdf5_format::superblock::Superblock;
// ---- Truncated / empty inputs ----
#[test]
fn empty_file_no_signature() {
let result = signature::find_signature(&[]);
assert!(result.is_err());
}
#[test]
fn short_file_no_signature() {
let result = signature::find_signature(&[0x89, 0x48, 0x44]);
assert!(result.is_err());
}
#[test]
fn truncated_superblock() {
// Valid signature but truncated superblock
let mut data = vec![0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a];
data.push(3); // version 3
// Truncated — not enough bytes for a full superblock
let sig = signature::find_signature(&data).unwrap();
let result = Superblock::parse(&data, sig);
assert!(result.is_err());
}
#[test]
fn truncated_object_header_v2() {
// "OHDR" + version 2 but truncated
let data = b"OHDR\x02\x00";
let result = ObjectHeader::parse(data, 0, 8, 8);
assert!(result.is_err());
}
#[test]
fn truncated_datatype() {
let result = Datatype::parse(&[0x03]); // class 0, version 1, but truncated
assert!(result.is_err());
}
#[test]
fn truncated_dataspace() {
let result = Dataspace::parse(&[0x02], 8); // version but truncated
assert!(result.is_err());
}
#[test]
fn truncated_fractal_heap() {
let data = b"FRHP\x00";
let result = FractalHeapHeader::parse(data, 0, 8, 8);
assert!(result.is_err());
}
#[test]
fn truncated_btree_v2() {
let data = b"BTHD\x00";
let result = BTreeV2Header::parse(data, 0, 8, 8);
assert!(result.is_err());
}
// ---- Invalid signatures ----
#[test]
fn bad_object_header_signature() {
let data = b"XHDR\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let result = ObjectHeader::parse(data, 0, 8, 8);
assert!(result.is_err());
}
#[test]
fn bad_fractal_heap_signature() {
let data = b"XRHP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let result = FractalHeapHeader::parse(data, 0, 8, 8);
assert!(result.is_err());
}
#[test]
fn bad_btree_v2_signature() {
let data = b"XTHD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let result = BTreeV2Header::parse(data, 0, 8, 8);
assert!(result.is_err());
}
// ---- Invalid versions ----
#[test]
fn bad_superblock_version() {
let mut data = vec![0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a];
data.push(99); // unsupported version
data.extend_from_slice(&[0; 100]); // padding
let sig = signature::find_signature(&data).unwrap();
let result = Superblock::parse(&data, sig);
assert!(result.is_err());
}
#[test]
fn bad_fractal_heap_version() {
let mut data = vec![0; 256];
data[0..4].copy_from_slice(b"FRHP");
data[4] = 99; // bad version
let result = FractalHeapHeader::parse(&data, 0, 8, 8);
assert!(matches!(
result,
Err(FormatError::InvalidFractalHeapVersion(99))
));
}
#[test]
fn bad_btree_v2_version() {
let mut data = vec![0; 64];
data[0..4].copy_from_slice(b"BTHD");
data[4] = 99; // bad version
let result = BTreeV2Header::parse(&data, 0, 8, 8);
assert!(matches!(
result,
Err(FormatError::InvalidBTreeV2Version(99))
));
}
// ---- Random / garbage data ----
#[test]
fn random_bytes_no_signature() {
let garbage: Vec<u8> = (0..1024).map(|i| (i * 37 + 13) as u8).collect();
let result = signature::find_signature(&garbage);
assert!(result.is_err());
}
#[test]
fn random_bytes_no_panic_superblock() {
let garbage: Vec<u8> = (0..256).map(|i| (i * 73 + 5) as u8).collect();
// Should not panic even if it fails
let _ = Superblock::parse(&garbage, 0);
}
#[test]
fn random_bytes_no_panic_object_header() {
let garbage: Vec<u8> = (0..256).map(|i| (i * 73 + 5) as u8).collect();
let _ = ObjectHeader::parse(&garbage, 0, 8, 8);
}
#[test]
fn random_bytes_no_panic_datatype() {
let garbage: Vec<u8> = (0..256).map(|i| (i * 73 + 5) as u8).collect();
let _ = Datatype::parse(&garbage);
}
#[test]
fn random_bytes_no_panic_dataspace() {
let garbage: Vec<u8> = (0..256).map(|i| (i * 73 + 5) as u8).collect();
let _ = Dataspace::parse(&garbage, 8);
}
#[test]
fn random_bytes_no_panic_fractal_heap() {
let garbage: Vec<u8> = (0..256).map(|i| (i * 73 + 5) as u8).collect();
let _ = FractalHeapHeader::parse(&garbage, 0, 8, 8);
}
#[test]
fn random_bytes_no_panic_btree_v2() {
let garbage: Vec<u8> = (0..256).map(|i| (i * 73 + 5) as u8).collect();
let _ = BTreeV2Header::parse(&garbage, 0, 8, 8);
}
// ---- Multiple garbage patterns (pseudo-fuzz) ----
#[test]
fn pseudo_fuzz_superblock() {
for seed in 0u16..100 {
let data: Vec<u8> = (0..128)
.map(|i| ((i as u16 * seed.wrapping_add(7)) & 0xFF) as u8)
.collect();
let _ = signature::find_signature(&data);
let _ = Superblock::parse(&data, 0);
}
}
#[test]
fn pseudo_fuzz_object_header() {
for seed in 0u16..100 {
let data: Vec<u8> = (0..512)
.map(|i| ((i as u16 * seed.wrapping_add(13)) & 0xFF) as u8)
.collect();
let _ = ObjectHeader::parse(&data, 0, 8, 8);
let _ = ObjectHeader::parse(&data, 0, 4, 4);
}
}
#[test]
fn pseudo_fuzz_datatype() {
for seed in 0u16..100 {
let data: Vec<u8> = (0..128)
.map(|i| ((i as u16 * seed.wrapping_add(3)) & 0xFF) as u8)
.collect();
let _ = Datatype::parse(&data);
}
}
#[test]
fn pseudo_fuzz_fractal_heap() {
for seed in 0u16..100 {
let data: Vec<u8> = (0..512)
.map(|i| ((i as u16 * seed.wrapping_add(17)) & 0xFF) as u8)
.collect();
let _ = FractalHeapHeader::parse(&data, 0, 8, 8);
}
}
#[test]
fn pseudo_fuzz_btree_v2() {
for seed in 0u16..100 {
let data: Vec<u8> = (0..256)
.map(|i| ((i as u16 * seed.wrapping_add(11)) & 0xFF) as u8)
.collect();
let _ = BTreeV2Header::parse(&data, 0, 8, 8);
}
}
// ---- Checksum corruption detection ----
#[test]
fn corrupted_file_detected() {
// Write a valid file then corrupt it
let mut fw = clawhdf5_format::file_writer::FileWriter::new();
fw.create_dataset("data").with_f64_data(&[1.0, 2.0, 3.0]);
let mut bytes = fw.finish().unwrap();
// Verify it works uncorrupted
let sig = signature::find_signature(&bytes).unwrap();
let sb = Superblock::parse(&bytes, sig).unwrap();
let _ = ObjectHeader::parse(
&bytes,
sb.root_group_address as usize,
sb.offset_size,
sb.length_size,
)
.unwrap();
// Corrupt a byte in the middle of the object header
let oh_start = sb.root_group_address as usize;
if oh_start + 20 < bytes.len() {
bytes[oh_start + 10] ^= 0xFF;
// Should detect corruption via checksum
let result = ObjectHeader::parse(&bytes, oh_start, sb.offset_size, sb.length_size);
assert!(
result.is_err(),
"corrupted object header should fail checksum"
);
}
}
// ---- Attribute reading robustness ----
#[test]
fn extract_attrs_from_dataset_no_panic() {
// Build a valid file, then try extracting attrs from the dataset
let mut fw = clawhdf5_format::file_writer::FileWriter::new();
fw.create_dataset("data").with_f64_data(&[1.0]);
let bytes = fw.finish().unwrap();
let sig = signature::find_signature(&bytes).unwrap();
let sb = Superblock::parse(&bytes, sig).unwrap();
let addr = clawhdf5_format::group_v2::resolve_path_any(&bytes, &sb, "data").unwrap();
let hdr = ObjectHeader::parse(&bytes, addr as usize, sb.offset_size, sb.length_size).unwrap();
// Should return empty vec, not panic
let attrs = clawhdf5_format::attribute::extract_attributes_full(
&bytes,
&hdr,
sb.offset_size,
sb.length_size,
)
.unwrap();
assert!(attrs.is_empty());
}
// ---- Provenance verification on corrupted data ----
#[test]
fn provenance_mismatch_on_corruption() {
let mut fw = clawhdf5_format::file_writer::FileWriter::new();
let ds = fw.create_dataset("sensor");
ds.with_f64_data(&[1.0, 2.0, 3.0])
.with_provenance("test", "2026-01-01T00:00:00Z", None);
let mut bytes = fw.finish().unwrap();
// Corrupt the raw data region (last bytes in file)
let len = bytes.len();
bytes[len - 5] ^= 0xFF;
let sig = signature::find_signature(&bytes).unwrap();
let sb = Superblock::parse(&bytes, sig).unwrap();
let addr = clawhdf5_format::group_v2::resolve_path_any(&bytes, &sb, "sensor").unwrap();
let hdr = ObjectHeader::parse(&bytes, addr as usize, sb.offset_size, sb.length_size).unwrap();
let result =
clawhdf5_format::provenance::verify_dataset(&bytes, &hdr, sb.offset_size, sb.length_size)
.unwrap();
assert!(
matches!(
result,
clawhdf5_format::provenance::VerifyResult::Mismatch { .. }
),
"corrupted data should produce hash mismatch"
);
}