fix(format): give empty string attributes a 1-byte type

An empty AttrValue::String (or a StringArray of empty strings) was
written with a size-0 fixed-length string type. libhdf5 rejects that
("invalid datatype size"), and the failure takes every attribute on the
object with it. Strings are now at least 1 byte, NUL-padded, which is
how h5py stores "" and reads back as "" in both h5py and our reader.
check_encodable also refuses a size-0 string type passed in directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-25 21:12:12 -05:00
co-authored by Claude Opus 5.5
parent 8c3ef996ea
commit 14876b8ae5
3 changed files with 71 additions and 3 deletions
@@ -464,3 +464,63 @@ fn h5py_sees_our_fill_time_and_fill_value() {
);
h5dump_ok(&path);
}
// ---- 5. empty string attributes ----
fn empty_string_file() -> Vec<u8> {
let mut fw = FileWriter::new();
fw.set_root_attr("empty", AttrValue::String(String::new()));
fw.set_root_attr("x", AttrValue::String("héllo".into()));
fw.set_root_attr(
"empties",
AttrValue::StringArray(vec![String::new(), String::new()]),
);
fw.set_root_attr("n", AttrValue::I64(3));
fw.finish().unwrap()
}
#[test]
fn empty_string_attribute_has_a_one_byte_type() {
// Measured: "" got a size-0 string type, and libhdf5 then refused every
// attribute on the object ("invalid datatype size").
let bytes = empty_string_file();
let (sb, root) = header_at(&bytes, "/");
let attrs = clawhdf5_format::attribute::extract_attributes(&root, sb.length_size).unwrap();
for name in ["empty", "empties"] {
let a = attrs.iter().find(|a| a.name == name).unwrap();
assert_eq!(a.datatype.type_size(), 1, "{name}");
let strings = a.read_as_strings().unwrap();
assert!(strings.iter().all(String::is_empty), "{name}: {strings:?}");
}
// A size-0 string type handed in directly is refused, not written.
let mut fw = FileWriter::new();
fw.set_root_attr(
"raw",
AttrValue::Raw {
datatype: Datatype::String {
size: 0,
padding: clawhdf5_format::datatype::StringPadding::NullPad,
charset: clawhdf5_format::datatype::CharacterSet::Ascii,
},
shape: vec![],
data: vec![],
},
);
assert!(fw.finish().is_err());
}
#[test]
#[ignore = "requires Python h5py module and h5dump"]
fn h5py_reads_all_attributes_next_to_an_empty_string() {
let path = write_tmp("empty_str", &empty_string_file());
let out = h5py(
&path,
"f = h5py.File(path, 'r')\n\
d = lambda v: v.decode() if isinstance(v, bytes) else v\n\
print(json.dumps([d(f.attrs['empty']), d(f.attrs['x']),\n\
\x20 [d(s) for s in f.attrs['empties']], int(f.attrs['n'])], ensure_ascii=False))",
);
assert_eq!(out, r#"["", "héllo", ["", ""], 3]"#);
h5dump_ok(&path);
}