Skip to main content

hdf5_pure/
provenance.rs

1//! SHINES provenance: SHA-256 content hashing, provenance attributes, and
2//! data-integrity verification.
3//!
4//! Enable with the `provenance` Cargo feature (opt-in; not in the default set).
5//! Writing is done via
6//! [`DatasetBuilder::with_provenance`](crate::DatasetBuilder::with_provenance);
7//! the stored hash is checked back with
8//! [`Dataset::verify_provenance`](crate::Dataset::verify_provenance).
9
10#[cfg(not(feature = "std"))]
11use alloc::{format, string::String, vec::Vec};
12
13use sha2::{Digest, Sha256};
14
15use crate::attribute::AttributeMessage;
16use crate::type_builders::{AttrValue, build_attr_message};
17
18// ---- Attribute name constants ----
19
20/// SHA-256 hex digest of the raw dataset bytes.
21pub const ATTR_SHA256: &str = "_provenance_sha256";
22/// Creator identifier (tool/user).
23pub const ATTR_CREATOR: &str = "_provenance_creator";
24/// ISO-8601 timestamp when the dataset was written.
25pub const ATTR_TIMESTAMP: &str = "_provenance_timestamp";
26/// Optional free-form description of the data source.
27pub const ATTR_SOURCE: &str = "_provenance_source";
28
29// ---- SHA-256 hashing ----
30
31/// Compute the SHA-256 digest of `data` and return the lowercase hex string.
32pub fn sha256_hex(data: &[u8]) -> String {
33    let hash = Sha256::digest(data);
34    let mut hex = String::with_capacity(64);
35    for byte in hash.iter() {
36        hex.push_str(&format!("{byte:02x}"));
37    }
38    hex
39}
40
41// ---- Provenance metadata builder ----
42
43/// Collects provenance information to be stored as HDF5 attributes.
44pub struct Provenance {
45    pub creator: String,
46    pub timestamp: String,
47    pub source: Option<String>,
48}
49
50impl Provenance {
51    /// Build provenance attribute messages for the given raw dataset bytes.
52    ///
53    /// Returns a `Vec<AttributeMessage>` containing:
54    /// - `_provenance_sha256`   — hex digest of `raw_data`
55    /// - `_provenance_creator`  — the creator string
56    /// - `_provenance_timestamp` — the timestamp string
57    /// - `_provenance_source`   — (optional) source description
58    pub fn build_attrs(&self, raw_data: &[u8]) -> Vec<AttributeMessage> {
59        let hash = sha256_hex(raw_data);
60        let mut attrs = Vec::with_capacity(4);
61        let hash_val = AttrValue::String(hash);
62        attrs.push(build_attr_message(ATTR_SHA256, &hash_val));
63        let creator_val = AttrValue::String(self.creator.clone());
64        attrs.push(build_attr_message(ATTR_CREATOR, &creator_val));
65        let ts_val = AttrValue::String(self.timestamp.clone());
66        attrs.push(build_attr_message(ATTR_TIMESTAMP, &ts_val));
67        if let Some(ref src) = self.source {
68            let src_val = AttrValue::String(src.clone());
69            attrs.push(build_attr_message(ATTR_SOURCE, &src_val));
70        }
71        attrs
72    }
73}
74
75// ---- Verification ----
76
77/// Outcome of checking a dataset against its stored provenance hash.
78///
79/// Returned by [`Dataset::verify_provenance`](crate::Dataset::verify_provenance).
80/// `NoHash` is kept distinct from `Mismatch` so a dataset that was simply never
81/// written with provenance is not reported as corrupt.
82/// Non-exhaustive: a future check can report an outcome that is neither a match,
83/// a mismatch, nor a missing hash, so match with a `_` arm.
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum VerifyResult {
87    /// The recomputed hash matches the stored `_provenance_sha256` attribute.
88    Ok,
89    /// The recomputed hash differs from the stored one. Carries both digests
90    /// (lowercase hex) for diagnostics.
91    Mismatch {
92        /// The hash recorded in the `_provenance_sha256` attribute.
93        stored: String,
94        /// The hash recomputed from the dataset's current raw bytes.
95        computed: String,
96    },
97    /// The dataset carries no `_provenance_sha256` attribute, so there is
98    /// nothing to verify against.
99    NoHash,
100}
101
102// ---- Tests ----
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn sha256_empty() {
110        // Well-known: SHA-256 of empty input
111        let h = sha256_hex(b"");
112        assert_eq!(
113            h,
114            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
115        );
116    }
117
118    #[test]
119    fn sha256_hello() {
120        let h = sha256_hex(b"hello");
121        assert_eq!(
122            h,
123            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
124        );
125    }
126
127    #[test]
128    fn provenance_builds_attrs_without_source() {
129        let prov = Provenance {
130            creator: "rustyhdf5".into(),
131            timestamp: "2026-02-19T00:00:00Z".into(),
132            source: None,
133        };
134        let attrs = prov.build_attrs(b"hello");
135        assert_eq!(attrs.len(), 3);
136        assert_eq!(attrs[0].name, ATTR_SHA256);
137        assert_eq!(attrs[1].name, ATTR_CREATOR);
138        assert_eq!(attrs[2].name, ATTR_TIMESTAMP);
139    }
140
141    #[test]
142    fn provenance_builds_attrs_with_source() {
143        let prov = Provenance {
144            creator: "test".into(),
145            timestamp: "2026-01-01T00:00:00Z".into(),
146            source: Some("sensor_42".into()),
147        };
148        let attrs = prov.build_attrs(b"data");
149        assert_eq!(attrs.len(), 4);
150        assert_eq!(attrs[3].name, ATTR_SOURCE);
151    }
152}