Files
clawhdf5/crates/clawhdf5/tests/provenance_tests.rs
T
ClawHDF5 Coding Agent 5db1008eb7 security(format): wire provenance verify_dataset into the clawhdf5 read path
verify_dataset existed and was tested, but was only ever called from
clawhdf5-format's own test files — no reader path in clawhdf5-io or the
clawhdf5 facade called it, so a corrupted dataset was silently readable
even though the write-side SHA-256 hash machinery (gated on the
provenance feature) had already written what it needed to detect that.

Add Dataset::verify_provenance() to the clawhdf5 facade, gated behind a
new `provenance` feature (on by default, forwarding to
clawhdf5-format/provenance which is already default-on). It surfaces a
typed VerifyResult (Ok/Mismatch/NoHash) via the existing Error type
rather than panicking. Deliberately NOT called automatically on
open()/dataset() — it decodes and hashes the entire dataset, which would
regress every read path (including the zero-copy/mmap ones) if run
unconditionally; callers opt in per dataset where the cost is
acceptable (e.g. a periodic integrity sweep).

Also re-export clawhdf5_format::provenance from the facade crate so
VerifyResult is reachable without depending on clawhdf5-format directly.

INT-08
2026-08-17 00:54:44 +00:00

62 lines
2.3 KiB
Rust

//! Tests for `Dataset::verify_provenance` — the facade-crate wiring of
//! `clawhdf5_format::provenance::verify_dataset` into the read path (INT-08:
//! the write-side hash existed and was tested, but nothing in `clawhdf5-io`
//! or the `clawhdf5` facade ever called `verify_dataset`).
#![cfg(feature = "provenance")]
use clawhdf5::provenance::VerifyResult;
use clawhdf5::{File, FileBuilder};
#[test]
fn verify_provenance_ok_on_intact_dataset() {
let mut b = FileBuilder::new();
b.create_dataset("sensor")
.with_f64_data(&[1.0, 2.0, 3.0, 4.0])
.with_provenance("test-suite", "2026-08-17T00:00:00Z", None);
let bytes = b.finish().unwrap();
let file = File::from_bytes(bytes).unwrap();
let ds = file.dataset("sensor").unwrap();
assert_eq!(ds.verify_provenance().unwrap(), VerifyResult::Ok);
}
#[test]
fn verify_provenance_no_hash_when_not_written_with_provenance() {
let mut b = FileBuilder::new();
b.create_dataset("plain").with_f64_data(&[1.0, 2.0]);
let bytes = b.finish().unwrap();
let file = File::from_bytes(bytes).unwrap();
let ds = file.dataset("plain").unwrap();
assert_eq!(ds.verify_provenance().unwrap(), VerifyResult::NoHash);
}
/// A corrupted dataset (raw bytes flipped after write, stored hash left
/// stale) must surface as a typed `Mismatch`, not be silently readable.
#[test]
fn verify_provenance_detects_corruption() {
let mut b = FileBuilder::new();
b.create_dataset("sensor")
.with_f64_data(&[1.0, 2.0, 3.0, 4.0])
.with_provenance("test-suite", "2026-08-17T00:00:00Z", None);
let mut bytes = b.finish().unwrap();
// Flip a byte inside the dataset's raw f64 payload (well past the
// superblock/header region) without touching the stored hash attribute,
// simulating corruption that occurred after the hash was written.
let needle = 2.0f64.to_le_bytes();
let pos = bytes
.windows(needle.len())
.position(|w| w == needle)
.expect("expected to find the f64 payload for 2.0 in the file bytes");
bytes[pos] ^= 0xFF;
let file = File::from_bytes(bytes).unwrap();
let ds = file.dataset("sensor").unwrap();
match ds.verify_provenance().unwrap() {
VerifyResult::Mismatch { .. } => {}
other => panic!("expected Mismatch for corrupted data, got {other:?}"),
}
}