fix(tools): h5rs dump shows NUL padding in nested strings, like h5dump

A null-padded fixed string inside a compound or an array member printed
trimmed ("" for three NULs, "a" for "a\0b"), where h5dump prints every
byte ("\000\000\000", "a\000b"); only top-level strings were shown in
full. DATA blocks now render elements through one function that keeps
the padding at any depth.

The README now lists the remaining known differences from h5dump:
nested compounds print inline, and long double values are printed as
errors (exit 1) with the datatype as an H5T_FLOAT block.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 01:23:32 -05:00
co-authored by Claude Opus 5.5
parent b8492bd28d
commit c4d96c1390
5 changed files with 124 additions and 21 deletions
+73 -15
View File
@@ -518,7 +518,9 @@ impl Dump<'_> {
errors += 1;
}
let comma = if i + 1 < n { "," } else { "" };
if let Value::Compound(members) = &v {
let esize = dt.type_size() as usize;
let eb = raw.get(i * esize..(i + 1) * esize).unwrap_or_default();
if let (Value::Compound(_), Datatype::Compound { members, .. }) = (&v, dt) {
// h5dump prints each compound element as a block, one
// member per line.
if !line.is_empty() {
@@ -526,25 +528,22 @@ impl Dump<'_> {
line.clear();
}
writeln!(out.o, "{pad}({}): {{", index_text(i as u64, &dims))?;
for (k, (_, m)) in members.iter().enumerate() {
for (k, m) in members.iter().enumerate() {
let sep = if k + 1 < members.len() { "," } else { "" };
writeln!(out.o, "{pad} {}{sep}", value::text(m, &paths))?;
let mb = usize::try_from(m.byte_offset)
.ok()
.and_then(|o| eb.get(o..))
.unwrap_or_default();
let t = ddl_text(&dec, &m.datatype, mb, &paths, 1);
writeln!(out.o, "{pad} {t}{sep}")?;
}
writeln!(out.o, "{pad} }}{comma}")?;
continue;
}
let t = match dt {
// h5dump shows a null-padded string's padding.
Datatype::String {
padding: StringPadding::NullPad,
size,
..
} if !matches!(v, Value::Error(_)) => {
let sz = *size as usize;
let b = raw.get(i * sz..(i + 1) * sz).unwrap_or_default();
format!("{}{comma}", value::quote_bytes(b))
}
_ => format!("{}{comma}", value::text(&v, &paths)),
let t = if matches!(v, Value::Error(_)) {
format!("{}{comma}", value::text(&v, &paths))
} else {
format!("{}{comma}", ddl_text(&dec, dt, eb, &paths, 0))
};
let row_start = i % last == 0;
if row_start || line.len() + 1 + t.len() > 77 {
@@ -863,6 +862,65 @@ const MAX_DDL_DEPTH: usize = 256;
const JSON_BYTES_PER_ELEMENT: u64 = 64;
/// `(i,j,k)` index of flat element `i` of an array with `dims`.
/// One element as h5dump prints it in a DATA block: [`value::text`], except
/// that a null-padded fixed string shows its padding (every byte, NULs as
/// `\000`) at any depth, in compound members and array elements too.
fn ddl_text(
dec: &Decoder,
dt: &Datatype,
b: &[u8],
paths: &dyn Fn(u64) -> Option<String>,
depth: u32,
) -> String {
let err = || value::text(&Value::Error("short element".into()), paths);
if depth > 32 {
return value::text(&dec.decode(dt, b, depth), paths);
}
match dt {
Datatype::String {
padding: StringPadding::NullPad,
size,
..
} => match b.get(..*size as usize) {
Some(s) => value::quote_bytes(s),
None => err(),
},
Datatype::Compound { members, .. } => format!(
"{{ {} }}",
members
.iter()
.map(
|m| match usize::try_from(m.byte_offset).ok().and_then(|o| b.get(o..)) {
Some(mb) => ddl_text(dec, &m.datatype, mb, paths, depth + 1),
None => err(),
}
)
.collect::<Vec<_>>()
.join(", ")
),
Datatype::Array {
base_type,
dimensions,
} => {
let bs = base_type.type_size() as usize;
let n = dimensions
.iter()
.try_fold(1usize, |a, &d| a.checked_mul(d as usize));
match n.filter(|n| n.checked_mul(bs).is_some_and(|t| t <= b.len())) {
Some(n) => format!(
"[ {} ]",
(0..n)
.map(|k| ddl_text(dec, base_type, &b[k * bs..], paths, depth + 1))
.collect::<Vec<_>>()
.join(", ")
),
None => value::text(&dec.decode(dt, b, depth), paths),
}
}
_ => value::text(&dec.decode(dt, b, depth), paths),
}
}
fn index_text(mut i: u64, dims: &[u64]) -> String {
let mut idx = vec![0u64; dims.len()];
for (k, &d) in dims.iter().enumerate().rev() {