1#[cfg(not(feature = "std"))]
7use alloc::{format, string::String, vec::Vec};
8
9use sha2::{Digest, Sha256};
10
11use crate::attribute::AttributeMessage;
12use crate::data_layout::DataLayout;
13use crate::data_read::read_raw_data;
14use crate::dataspace::Dataspace;
15use crate::datatype::Datatype;
16use crate::error::FormatError;
17use crate::object_header::ObjectHeader;
18use crate::type_builders::{build_attr_message, AttrValue};
19
20pub const ATTR_SHA256: &str = "_provenance_sha256";
24pub const ATTR_CREATOR: &str = "_provenance_creator";
26pub const ATTR_TIMESTAMP: &str = "_provenance_timestamp";
28pub const ATTR_SOURCE: &str = "_provenance_source";
30
31pub fn sha256_hex(data: &[u8]) -> String {
35 let hash = Sha256::digest(data);
36 let mut hex = String::with_capacity(64);
37 for byte in hash.iter() {
38 hex.push_str(&format!("{byte:02x}"));
39 }
40 hex
41}
42
43pub struct Provenance {
47 pub creator: String,
48 pub timestamp: String,
49 pub source: Option<String>,
50}
51
52impl Provenance {
53 pub fn build_attrs(&self, raw_data: &[u8]) -> Vec<AttributeMessage> {
61 let hash = sha256_hex(raw_data);
62 let mut attrs = Vec::with_capacity(4);
63 let hash_val = AttrValue::String(hash);
64 attrs.push(build_attr_message(ATTR_SHA256, &hash_val));
65 let creator_val = AttrValue::String(self.creator.clone());
66 attrs.push(build_attr_message(ATTR_CREATOR, &creator_val));
67 let ts_val = AttrValue::String(self.timestamp.clone());
68 attrs.push(build_attr_message(ATTR_TIMESTAMP, &ts_val));
69 if let Some(ref src) = self.source {
70 let src_val = AttrValue::String(src.clone());
71 attrs.push(build_attr_message(ATTR_SOURCE, &src_val));
72 }
73 attrs
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum VerifyResult {
82 Ok,
84 Mismatch { stored: String, computed: String },
86 NoHash,
88}
89
90pub fn verify_dataset(
96 file_data: &[u8],
97 header: &ObjectHeader,
98 offset_size: u8,
99 length_size: u8,
100) -> Result<VerifyResult, FormatError> {
101 let attrs = crate::attribute::extract_attributes_full(
103 file_data,
104 header,
105 offset_size,
106 length_size,
107 )?;
108
109 let stored_hash = attrs
111 .iter()
112 .find(|a| a.name == ATTR_SHA256)
113 .and_then(|a| core::str::from_utf8(&a.raw_data).ok())
114 .map(|s| s.trim_end_matches('\0').to_string());
115
116 let stored_hash = match stored_hash {
117 Some(h) => h,
118 None => return Ok(VerifyResult::NoHash),
119 };
120
121 let dt_msg = header
123 .messages
124 .iter()
125 .find(|m| m.msg_type == crate::message_type::MessageType::Datatype)
126 .ok_or_else(|| FormatError::SerializationError("missing Datatype message".into()))?;
127 let ds_msg = header
128 .messages
129 .iter()
130 .find(|m| m.msg_type == crate::message_type::MessageType::Dataspace)
131 .ok_or_else(|| FormatError::SerializationError("missing Dataspace message".into()))?;
132 let dl_msg = header
133 .messages
134 .iter()
135 .find(|m| m.msg_type == crate::message_type::MessageType::DataLayout)
136 .ok_or_else(|| FormatError::SerializationError("missing DataLayout message".into()))?;
137
138 let (dt, _) = Datatype::parse(&dt_msg.data)?;
139 let ds = Dataspace::parse(&ds_msg.data, length_size)?;
140 let dl = DataLayout::parse(&dl_msg.data, offset_size, length_size)?;
141
142 let pipeline = header
143 .messages
144 .iter()
145 .find(|m| m.msg_type == crate::message_type::MessageType::FilterPipeline)
146 .map(|m| crate::filter_pipeline::FilterPipeline::parse(&m.data))
147 .transpose()?;
148
149 let raw = match &dl {
150 DataLayout::Chunked { .. } => crate::chunked_read::read_chunked_data(
151 file_data,
152 &dl,
153 &ds,
154 &dt,
155 pipeline.as_ref(),
156 offset_size,
157 length_size,
158 )?,
159 _ => read_raw_data(file_data, &dl, &ds, &dt)?,
160 };
161
162 let computed = sha256_hex(&raw);
164 if computed == stored_hash {
165 Ok(VerifyResult::Ok)
166 } else {
167 Ok(VerifyResult::Mismatch {
168 stored: stored_hash,
169 computed,
170 })
171 }
172}
173
174#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn sha256_empty() {
182 let h = sha256_hex(b"");
184 assert_eq!(
185 h,
186 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
187 );
188 }
189
190 #[test]
191 fn sha256_hello() {
192 let h = sha256_hex(b"hello");
193 assert_eq!(
194 h,
195 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
196 );
197 }
198
199 #[test]
200 fn provenance_builds_attrs_without_source() {
201 let prov = Provenance {
202 creator: "rustyhdf5".into(),
203 timestamp: "2026-02-19T00:00:00Z".into(),
204 source: None,
205 };
206 let attrs = prov.build_attrs(b"hello");
207 assert_eq!(attrs.len(), 3);
208 assert_eq!(attrs[0].name, ATTR_SHA256);
209 assert_eq!(attrs[1].name, ATTR_CREATOR);
210 assert_eq!(attrs[2].name, ATTR_TIMESTAMP);
211 }
212
213 #[test]
214 fn provenance_builds_attrs_with_source() {
215 let prov = Provenance {
216 creator: "test".into(),
217 timestamp: "2026-01-01T00:00:00Z".into(),
218 source: Some("sensor_42".into()),
219 };
220 let attrs = prov.build_attrs(b"data");
221 assert_eq!(attrs.len(), 4);
222 assert_eq!(attrs[3].name, ATTR_SOURCE);
223 }
224
225 #[test]
226 fn roundtrip_provenance_on_file() {
227 use crate::file_writer::FileWriter;
228 use crate::group_v2::resolve_path_any;
229 use crate::signature;
230 use crate::superblock::Superblock;
231
232 let raw_data: Vec<u8> = (0..24u64)
233 .flat_map(|v| (v as f64).to_le_bytes())
234 .collect();
235 let expected_hash = sha256_hex(&raw_data);
236
237 let mut fw = FileWriter::new();
238 let ds = fw.create_dataset("sensor");
239 ds.with_f64_data(
240 &(0..24).map(|v| v as f64).collect::<Vec<_>>(),
241 );
242 ds.set_attr(ATTR_SHA256, AttrValue::String(expected_hash.clone()));
243 ds.set_attr(
244 ATTR_CREATOR,
245 AttrValue::String("test-suite".into()),
246 );
247 ds.set_attr(
248 ATTR_TIMESTAMP,
249 AttrValue::String("2026-02-19T12:00:00Z".into()),
250 );
251 let bytes = fw.finish().unwrap();
252
253 let sig = signature::find_signature(&bytes).unwrap();
255 let sb = Superblock::parse(&bytes, sig).unwrap();
256 let addr = resolve_path_any(&bytes, &sb, "sensor").unwrap();
257 let hdr = crate::object_header::ObjectHeader::parse(
258 &bytes,
259 addr as usize,
260 sb.offset_size,
261 sb.length_size,
262 )
263 .unwrap();
264
265 let result = verify_dataset(&bytes, &hdr, sb.offset_size, sb.length_size).unwrap();
266 assert_eq!(result, VerifyResult::Ok);
267 }
268
269 #[test]
270 fn verify_no_hash_attribute() {
271 use crate::file_writer::FileWriter;
272 use crate::group_v2::resolve_path_any;
273 use crate::signature;
274 use crate::superblock::Superblock;
275
276 let mut fw = FileWriter::new();
277 fw.create_dataset("plain").with_f64_data(&[1.0, 2.0]);
278 let bytes = fw.finish().unwrap();
279
280 let sig = signature::find_signature(&bytes).unwrap();
281 let sb = Superblock::parse(&bytes, sig).unwrap();
282 let addr = resolve_path_any(&bytes, &sb, "plain").unwrap();
283 let hdr = crate::object_header::ObjectHeader::parse(
284 &bytes,
285 addr as usize,
286 sb.offset_size,
287 sb.length_size,
288 )
289 .unwrap();
290
291 let result = verify_dataset(&bytes, &hdr, sb.offset_size, sb.length_size).unwrap();
292 assert_eq!(result, VerifyResult::NoHash);
293 }
294}