//! 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 = (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 = (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 = (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 = (0..256).map(|i| (i * 73 + 5) as u8).collect(); let _ = Datatype::parse(&garbage); } #[test] fn random_bytes_no_panic_dataspace() { let garbage: Vec = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (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" ); } // --------------------------------------------------------------------------- // Fuzzer finds, kept as regression tests // --------------------------------------------------------------------------- /// `fuzz_btree_v2` crash input from 2026-09-20 (82 bytes): a B-tree v2 header /// followed by internal nodes that point back into themselves. It predates the /// depth cap and record budget added to B-tree v2 traversal that day and no /// longer crashes; this replays the fuzz target's exact code path on it so a /// regression fails CI rather than waiting for a fuzz run. #[test] fn fuzz_btree_v2_crash_f98c19dc_is_a_clean_result() { use clawhdf5_format::btree_v2::{BTreeV2Header, collect_btree_v2_records}; let data: &[u8] = &[ 0x42, 0x54, 0x48, 0x44, 0x00, 0x06, 0x00, 0xed, 0xef, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x40, 0x14, 0x93, 0x42, 0x54, 0x49, 0x4e, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x54, 0x48, 0x44, 0x00, 0x00, 0x00, 0x13, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x14, 0x93, 0x42, 0x54, 0x00, 0x49, 0x00, 0x01, 0x4e, 0x42, 0x42, 0x54, 0xbe, ]; assert_eq!(data.len(), 82); for offset_size in [4u8, 8] { for length_size in [4u8, 8] { if let Ok(header) = BTreeV2Header::parse(data, 0, offset_size, length_size) { let _ = collect_btree_v2_records(data, &header, offset_size, length_size); } } } let (fields, file) = data.split_first_chunk::<20>().unwrap(); let header = BTreeV2Header { tree_type: fields[0], node_size: u32::from_le_bytes([fields[1], fields[2], fields[3], fields[4]]), record_size: u16::from_le_bytes([fields[5], fields[6]]), depth: u16::from_le_bytes([fields[7], fields[8]]), root_node_address: u64::from(u32::from_le_bytes([ fields[9], fields[10], fields[11], fields[12], ])), num_records_in_root: u16::from_le_bytes([fields[13], fields[14]]), total_records: u64::from(u32::from_le_bytes([ fields[15], fields[16], fields[17], fields[18], ])), }; let offset_size = if fields[19] & 1 == 0 { 4 } else { 8 }; let _ = collect_btree_v2_records(file, &header, offset_size, 8); }