use hdf5_pure::{AttrValue, EditSession, File, FileBuilder, Object};
const UB: usize = 512;
const MARKER: &[u8] = b"USERBLOCK-MARKER-0104";
fn build_userblock_file(path: &std::path::Path) -> Vec<u8> {
let mut b = FileBuilder::new();
b.with_userblock(UB as u64);
b.create_dataset("alpha")
.with_f64_data(&[1.0, 2.0, 3.0, 4.0]);
b.create_dataset("beta").with_i32_data(&[10, 20, 30]);
let mut g = b.create_group("grp");
g.create_dataset("inner").with_f64_data(&[7.5, 8.5]);
b.add_group(g.finish());
let mut bytes = b.finish().unwrap();
bytes[..MARKER.len()].copy_from_slice(MARKER);
bytes[UB - 1] = 0xAB;
let userblock = bytes[..UB].to_vec();
std::fs::write(path, &bytes).unwrap();
userblock
}
#[test]
fn synthetic_userblock_file_roundtrip() {
let path = std::env::temp_dir().join("hdf5_pure_ub_roundtrip.h5");
let userblock = build_userblock_file(&path);
{
let mut s = EditSession::open(&path).unwrap();
s.write_dataset("alpha")
.with_f64_data(&[9.0, 8.0, 7.0, 6.0]);
s.create_dataset("added").with_i32_data(&[100, 200]);
s.create_dataset("grp/added_inner").with_f64_data(&[3.25]);
s.create_group("newgrp");
s.set_group_attr("newgrp", "tag", AttrValue::AsciiString("v104".into()));
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
assert_eq!(
file.dataset("alpha").unwrap().read_f64().unwrap(),
vec![9.0, 8.0, 7.0, 6.0]
);
assert_eq!(
file.dataset("beta").unwrap().read_i32().unwrap(),
vec![10, 20, 30]
);
assert_eq!(
file.dataset("grp/inner").unwrap().read_f64().unwrap(),
vec![7.5, 8.5]
);
assert_eq!(
file.dataset("added").unwrap().read_i32().unwrap(),
vec![100, 200]
);
assert_eq!(
file.dataset("grp/added_inner").unwrap().read_f64().unwrap(),
vec![3.25]
);
let mut groups = file.root().groups().unwrap();
groups.sort();
assert!(groups.contains(&"newgrp".to_string()));
let attrs = file.group("newgrp").unwrap().attrs().unwrap();
assert_eq!(attrs.get("tag"), Some(&AttrValue::String("v104".into())));
let after = std::fs::read(&path).unwrap();
assert_eq!(
&after[..UB],
&userblock[..],
"userblock bytes changed across the edit"
);
assert_eq!(&after[..MARKER.len()], MARKER);
assert_eq!(after[UB - 1], 0xAB);
std::fs::remove_file(&path).ok();
}
#[test]
fn userblock_inplace_overwrite_only_takes_fast_path() {
let path = std::env::temp_dir().join("hdf5_pure_ub_inplace_only.h5");
let userblock = build_userblock_file(&path);
{
let mut s = EditSession::open(&path).unwrap();
s.write_dataset("beta").with_i32_data(&[-1, -2, -3]);
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
assert_eq!(
file.dataset("beta").unwrap().read_i32().unwrap(),
vec![-1, -2, -3]
);
assert_eq!(
file.dataset("alpha").unwrap().read_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0]
);
assert_eq!(&std::fs::read(&path).unwrap()[..UB], &userblock[..]);
std::fs::remove_file(&path).ok();
}
#[test]
fn userblock_cross_file_copy_from_userblock_source_is_refused() {
let src_path = std::env::temp_dir().join("hdf5_pure_ub_xcopy_src_refuse.h5");
let dst_path = std::env::temp_dir().join("hdf5_pure_ub_xcopy_dst_refuse.h5");
build_userblock_file(&src_path);
build_userblock_file(&dst_path);
let dst_before = std::fs::read(&dst_path).unwrap();
let source = File::open(&src_path).unwrap();
let mut s = EditSession::open(&dst_path).unwrap();
let err = s.copy_from(&source, "alpha", "imported");
assert!(
err.is_err(),
"cross-file copy from a userblock source should be refused"
);
drop(s);
assert_eq!(
std::fs::read(&dst_path).unwrap(),
dst_before,
"a refused cross-file copy must not modify the destination"
);
std::fs::remove_file(&src_path).ok();
std::fs::remove_file(&dst_path).ok();
}
#[test]
fn userblock_add_empty_dataset_roundtrip() {
let path = std::env::temp_dir().join("hdf5_pure_ub_add_empty.h5");
let userblock = build_userblock_file(&path);
{
let mut s = EditSession::open(&path).unwrap();
s.create_dataset("empty")
.with_f64_data(&[])
.with_shape(&[0, 3]);
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
let empty = file.dataset("empty").unwrap();
assert_eq!(empty.shape().unwrap(), vec![0, 3]);
assert_eq!(empty.read_f64().unwrap(), Vec::<f64>::new());
assert_eq!(
file.dataset("alpha").unwrap().read_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0]
);
assert_eq!(&std::fs::read(&path).unwrap()[..UB], &userblock[..]);
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "provenance")]
#[test]
fn userblock_add_provenance_dataset_roundtrip() {
use hdf5_pure::VerifyResult;
let path = std::env::temp_dir().join("hdf5_pure_ub_add_provenance.h5");
let userblock = build_userblock_file(&path);
{
let mut s = EditSession::open(&path).unwrap();
s.create_dataset("sensor")
.with_f64_data(&[1.0, 2.0, 3.0])
.with_provenance("test-suite", "2026-02-19T12:00:00Z", Some("bench"));
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
let ds = file.dataset("sensor").unwrap();
assert_eq!(ds.read_f64().unwrap(), vec![1.0, 2.0, 3.0]);
assert_eq!(ds.verify_provenance().unwrap(), VerifyResult::Ok);
assert_eq!(
ds.attrs().unwrap().get("_provenance_creator"),
Some(&AttrValue::String("test-suite".into()))
);
assert_eq!(&std::fs::read(&path).unwrap()[..UB], &userblock[..]);
std::fs::remove_file(&path).ok();
}
#[test]
fn userblock_add_dataset_with_vlen_attribute_roundtrip() {
let path = std::env::temp_dir().join("hdf5_pure_ub_add_vlen_attr.h5");
let userblock = build_userblock_file(&path);
{
let mut s = EditSession::open(&path).unwrap();
s.create_dataset("labeled")
.with_i32_data(&[1, 2, 3])
.set_attr(
"tags",
AttrValue::VarLenAsciiArray(vec!["one".into(), "two".into()]),
);
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
let ds = file.dataset("labeled").unwrap();
assert_eq!(ds.read_i32().unwrap(), vec![1, 2, 3]);
assert_eq!(
ds.attrs().unwrap().get("tags"),
Some(&AttrValue::StringArray(vec!["one".into(), "two".into()]))
);
assert_eq!(&std::fs::read(&path).unwrap()[..UB], &userblock[..]);
std::fs::remove_file(&path).ok();
}
#[test]
fn userblock_add_vlen_string_dataset_roundtrip() {
let path = std::env::temp_dir().join("hdf5_pure_ub_add_vlen_string_ds.h5");
let userblock = build_userblock_file(&path);
{
let mut s = EditSession::open(&path).unwrap();
s.create_dataset("labels")
.with_vlen_strings(&["alpha", "", "gamma"]);
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
let ds = file.dataset("labels").unwrap();
assert_eq!(
ds.read_string().unwrap(),
vec!["alpha".to_string(), String::new(), "gamma".to_string()]
);
assert_eq!(&std::fs::read(&path).unwrap()[..UB], &userblock[..]);
std::fs::remove_file(&path).ok();
}
#[test]
fn userblock_add_reference_dataset_roundtrip() {
let path = std::env::temp_dir().join("hdf5_pure_ub_add_ref.h5");
let userblock = build_userblock_file(&path);
{
let mut s = EditSession::open(&path).unwrap();
s.create_dataset("refs").with_path_references(&["alpha"]);
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
let targets = file.dataset("refs").unwrap().dereference().unwrap();
assert_eq!(targets.len(), 1);
match &targets[0] {
Object::Dataset(ds) => assert_eq!(
ds.read_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0],
"reference should resolve to `alpha`'s original contents"
),
Object::Group(_) => panic!("expected a dataset reference"),
}
assert_eq!(&std::fs::read(&path).unwrap()[..UB], &userblock[..]);
std::fs::remove_file(&path).ok();
}
#[test]
fn real_mat_add_dataset_preserves_userblock_and_data() {
let src = std::path::Path::new("tests/fixtures/mat_real/test_string_v73.mat");
let path = std::env::temp_dir().join("hdf5_pure_ub_real_mat.mat");
std::fs::copy(src, &path).unwrap();
let original_userblock = std::fs::read(&path).unwrap()[..UB].to_vec();
assert_eq!(
&original_userblock[..10],
b"MATLAB 7.3",
"fixture is not a MATLAB v7.3 userblock file"
);
let before = File::open(&path)
.unwrap()
.dataset("string_scalar")
.unwrap()
.read_u32()
.unwrap();
{
let mut s = EditSession::open(&path).unwrap();
s.create_dataset("hdf5_pure_probe")
.with_f64_data(&[42.0, -1.0, 3.5]);
s.commit().unwrap();
}
let file = File::open(&path).unwrap();
assert_eq!(
file.dataset("hdf5_pure_probe").unwrap().read_f64().unwrap(),
vec![42.0, -1.0, 3.5]
);
assert_eq!(
file.dataset("string_scalar").unwrap().read_u32().unwrap(),
before
);
let datasets = file.root().datasets().unwrap();
for name in [
"string_array",
"string_empty",
"string_scalar",
"hdf5_pure_probe",
] {
assert!(
datasets.contains(&name.to_string()),
"missing dataset {name}"
);
}
let groups = file.root().groups().unwrap();
assert!(groups.contains(&"#refs#".to_string()));
assert!(groups.contains(&"#subsystem#".to_string()));
let after_userblock = std::fs::read(&path).unwrap()[..UB].to_vec();
assert_eq!(
after_userblock, original_userblock,
"MATLAB userblock changed across the edit"
);
std::fs::remove_file(&path).ok();
}