The reader now refuses a compound with a repeated field name or no fields and an enum member with an empty name, as libhdf5 does, but the writer still wrote them: CompoundTypeBuilder and EnumTypeBuilder build them without complaint, so clawhdf5 wrote files it could not read back. They were never valid HDF5; h5py refuses them. Datatype::check_encodable, which FileWriter::finish runs on every dataset and attribute type, now parses the type's own encoding back and refuses one the reader refuses, with the reader's reason. That keeps the writer in step with every reader check, not only these three. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
142 lines
4.9 KiB
Rust
142 lines
4.9 KiB
Rust
//! Files written by older clawhdf5 releases must keep opening, even where
|
|
//! libhdf5 refuses them: stricter validation of corrupt files must not lock
|
|
//! users out of their own data.
|
|
//!
|
|
//! `fixtures/written_by_v2_7_0.h5` and `written_by_v2_7_0_paged.h5` were
|
|
//! written by clawhdf5 v2.7.0 (`FileBuilder` / `FileWriter` with every
|
|
//! datatype, layout and attribute kind it could write). v2.7.0 wrote the sign
|
|
//! bit of every float at position 63 and a size-0 string type for an empty
|
|
//! string attribute; libhdf5 refuses both, this reader must not.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use clawhdf5::{AttrValue, File};
|
|
|
|
fn fixture(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
#[test]
|
|
fn every_object_of_a_v2_7_0_file_reads() {
|
|
let file = File::open(fixture("written_by_v2_7_0.h5")).unwrap();
|
|
|
|
let (attrs, errors) = file.root().attrs_with_errors().unwrap();
|
|
assert!(errors.is_empty(), "{errors:?}");
|
|
// (It reads as no strings, as it did before.)
|
|
assert!(
|
|
matches!(&attrs["empty"], AttrValue::StringArray(v) if v.iter().all(String::is_empty)),
|
|
"{:?}",
|
|
attrs["empty"]
|
|
);
|
|
assert!(matches!(&attrs["title"], AttrValue::String(s) if s == "old"));
|
|
|
|
let f32s = |name: &str| file.dataset(name).unwrap().read_f32().unwrap();
|
|
assert_eq!(f32s("f32"), [1.0, 2.0, 3.0]);
|
|
assert_eq!(
|
|
f32s("f32_2d"),
|
|
(0..60).map(|x| x as f32).collect::<Vec<_>>()
|
|
);
|
|
assert_eq!(
|
|
f32s("chunked"),
|
|
(0..1000).map(|x| x as f32).collect::<Vec<_>>()
|
|
);
|
|
assert!(f32s("empty").is_empty());
|
|
assert_eq!(
|
|
file.dataset("f64").unwrap().read_f64().unwrap(),
|
|
[1.0, 2.0, 3.0]
|
|
);
|
|
assert_eq!(file.dataset("i32").unwrap().read_i32().unwrap(), [1, -2, 3]);
|
|
assert_eq!(file.dataset("i64").unwrap().read_i64().unwrap(), [1, -2, 3]);
|
|
assert_eq!(file.dataset("u64").unwrap().read_u64().unwrap(), [1, 2, 3]);
|
|
assert_eq!(
|
|
file.dataset("chunked_2d").unwrap().read_i32().unwrap(),
|
|
(0..600).collect::<Vec<_>>()
|
|
);
|
|
for name in ["unlimited", "maxshape"] {
|
|
assert_eq!(
|
|
file.dataset(name).unwrap().read_f64().unwrap(),
|
|
(0..100).map(|x| x as f64).collect::<Vec<_>>(),
|
|
"{name}"
|
|
);
|
|
}
|
|
assert_eq!(
|
|
file.dataset("compact").unwrap().read_i32().unwrap(),
|
|
[7, 8, 9]
|
|
);
|
|
for name in ["u8", "compound", "enum", "enum8"] {
|
|
file.dataset(name)
|
|
.unwrap()
|
|
.dtype()
|
|
.unwrap_or_else(|e| panic!("{name}: {e}"));
|
|
}
|
|
|
|
let grp = file.group("grp").unwrap();
|
|
let (attrs, errors) = grp.attrs_with_errors().unwrap();
|
|
assert!(errors.is_empty(), "{errors:?}");
|
|
assert_eq!(attrs.len(), 21);
|
|
assert_eq!(grp.dataset("d").unwrap().read_f32().unwrap(), [4.0, 5.0]);
|
|
|
|
let paged = File::open(fixture("written_by_v2_7_0_paged.h5")).unwrap();
|
|
assert_eq!(paged.dataset("d").unwrap().read_f32().unwrap(), [1.0, 2.0]);
|
|
}
|
|
|
|
/// Datatypes libhdf5 (and this reader) refuse — a compound with a repeated
|
|
/// field name or no fields, an enum member with an empty name — must not be
|
|
/// written: they made files that did not read back. Valid neighbours of each
|
|
/// still write and read back.
|
|
#[test]
|
|
fn datatypes_the_reader_refuses_are_not_written() {
|
|
use clawhdf5::{CompoundTypeBuilder, EnumTypeBuilder, FileBuilder};
|
|
|
|
let write_compound = |dt, raw: Vec<u8>| {
|
|
let mut b = FileBuilder::new();
|
|
b.create_dataset("d").with_compound_data(dt, raw, 1);
|
|
b.finish()
|
|
};
|
|
let write_enum = |dt| {
|
|
let mut b = FileBuilder::new();
|
|
b.create_dataset("d").with_enum_u8_data(dt, &[0, 1]);
|
|
b.finish()
|
|
};
|
|
let refused = |r: Result<Vec<u8>, clawhdf5::Error>, what: &str| {
|
|
let err = r.expect_err("written");
|
|
let msg = err.to_string();
|
|
assert!(msg.contains("datatype cannot be written"), "{msg}");
|
|
assert!(msg.contains(what), "{msg}");
|
|
};
|
|
|
|
let dup = CompoundTypeBuilder::new()
|
|
.f64_field("x")
|
|
.f64_field("x")
|
|
.build();
|
|
refused(
|
|
write_compound(dup, vec![0; 16]),
|
|
"duplicated compound field name 'x'",
|
|
);
|
|
refused(
|
|
write_compound(CompoundTypeBuilder::new().build(), vec![]),
|
|
"invalid",
|
|
);
|
|
let empty_name = EnumTypeBuilder::u8_based()
|
|
.u8_value("A", 0)
|
|
.u8_value("", 1)
|
|
.build();
|
|
refused(write_enum(empty_name), "0 length enum name");
|
|
|
|
let ok = CompoundTypeBuilder::new()
|
|
.f64_field("x")
|
|
.f64_field("y")
|
|
.build();
|
|
let bytes = write_compound(ok, vec![0; 16]).unwrap();
|
|
let file = File::from_bytes(bytes).unwrap();
|
|
file.dataset("d").unwrap().dtype().unwrap();
|
|
let ok = EnumTypeBuilder::u8_based()
|
|
.u8_value("A", 0)
|
|
.u8_value("B", 1)
|
|
.build();
|
|
let file = File::from_bytes(write_enum(ok).unwrap()).unwrap();
|
|
file.dataset("d").unwrap().dtype().unwrap();
|
|
}
|