h5rs tools, browser reader, libhdf5 header checks, plugin filters, concurrency benchmark #14
+4
-2
@@ -123,8 +123,10 @@
|
|||||||
- `h5rs dump [--json] [-A] [-p] [-d PATH] FILE` prints DDL text that is
|
- `h5rs dump [--json] [-A] [-p] [-d PATH] FILE` prints DDL text that is
|
||||||
byte-identical to h5dump 1.14.6's on the test files (all layouts and
|
byte-identical to h5dump 1.14.6's on the test files (all layouts and
|
||||||
chunk indexes, v1/v2 groups, compound, enum, strings, links, named
|
chunk indexes, v1/v2 groups, compound, enum, strings, links, named
|
||||||
types, attributes), or JSON in the HDF Group's hdf5-json layout (schema
|
types, attributes; null-padded strings show their NULs at any depth),
|
||||||
in the crate README).
|
or JSON in the HDF Group's hdf5-json layout (schema in the crate
|
||||||
|
README). Nested compounds print inline and `long double` values as
|
||||||
|
errors (exit 1); both are listed in the README.
|
||||||
- `h5rs stat FILE` reports h5stat's object, link, rank, layout, filter,
|
- `h5rs stat FILE` reports h5stat's object, link, rank, layout, filter,
|
||||||
attribute, raw-data and file-size figures (equal to h5stat's on the test
|
attribute, raw-data and file-size figures (equal to h5stat's on the test
|
||||||
files); metadata space is one figure, not broken down.
|
files); metadata space is one figure, not broken down.
|
||||||
|
|||||||
@@ -72,10 +72,22 @@ groups; integers of both byte orders, floats, compound with an array member,
|
|||||||
enum, fixed and variable-length strings; soft, external and hard links; a
|
enum, fixed and variable-length strings; soft, external and hard links; a
|
||||||
named datatype; compact and dense attributes) `h5rs dump` and `h5rs dump -A`
|
named datatype; compact and dense attributes) `h5rs dump` and `h5rs dump -A`
|
||||||
print the same bytes as h5dump 1.14.6 — `dump_matches_h5dump` in
|
print the same bytes as h5dump 1.14.6 — `dump_matches_h5dump` in
|
||||||
`tests/h5rs_interop.rs` checks this. Not covered by that test: references,
|
`tests/h5rs_interop.rs` checks this, and `dump_shows_nul_padding_in_nested_strings`
|
||||||
opaque, bitfield, variable-length sequences and virtual datasets. Floats print at their own precision (a `float32` 0.1 prints as
|
that null-padded strings show their NULs (`"a\000b"`) at any depth, as
|
||||||
`0.1`), which for some values is more digits than h5dump's `%g`. The `-p`
|
h5dump's do. Not covered by those tests: references, opaque, bitfield,
|
||||||
block is h5rs's own (it names the chunk index), not h5dump's.
|
variable-length sequences and virtual datasets. Known differences from
|
||||||
|
h5dump:
|
||||||
|
|
||||||
|
- Floats print at their own precision (a `float32` 0.1 prints as `0.1`),
|
||||||
|
which for some values is more digits than h5dump's `%g`.
|
||||||
|
- A compound nested in a compound prints inline (`{ 1, 2.5 }`) where
|
||||||
|
h5dump prints it as an indented block, one member per line; only the
|
||||||
|
outer compound is a block.
|
||||||
|
- `long double` (x87 80-bit) and other floats wider than 64 bits: the
|
||||||
|
datatype is printed as an `H5T_FLOAT { ... }` block instead of h5dump's
|
||||||
|
one-line description, and each value as `<error: ...>`; `dump` then
|
||||||
|
exits 1. The library cannot convert them (see `docs/known-issues.md`).
|
||||||
|
- The `-p` block is h5rs's own (it names the chunk index), not h5dump's.
|
||||||
|
|
||||||
### JSON schema
|
### JSON schema
|
||||||
|
|
||||||
|
|||||||
@@ -518,7 +518,9 @@ impl Dump<'_> {
|
|||||||
errors += 1;
|
errors += 1;
|
||||||
}
|
}
|
||||||
let comma = if i + 1 < n { "," } else { "" };
|
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
|
// h5dump prints each compound element as a block, one
|
||||||
// member per line.
|
// member per line.
|
||||||
if !line.is_empty() {
|
if !line.is_empty() {
|
||||||
@@ -526,25 +528,22 @@ impl Dump<'_> {
|
|||||||
line.clear();
|
line.clear();
|
||||||
}
|
}
|
||||||
writeln!(out.o, "{pad}({}): {{", index_text(i as u64, &dims))?;
|
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 { "" };
|
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}")?;
|
writeln!(out.o, "{pad} }}{comma}")?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let t = match dt {
|
let t = if matches!(v, Value::Error(_)) {
|
||||||
// h5dump shows a null-padded string's padding.
|
format!("{}{comma}", value::text(&v, &paths))
|
||||||
Datatype::String {
|
} else {
|
||||||
padding: StringPadding::NullPad,
|
format!("{}{comma}", ddl_text(&dec, dt, eb, &paths, 0))
|
||||||
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 row_start = i % last == 0;
|
let row_start = i % last == 0;
|
||||||
if row_start || line.len() + 1 + t.len() > 77 {
|
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;
|
const JSON_BYTES_PER_ELEMENT: u64 = 64;
|
||||||
|
|
||||||
/// `(i,j,k)` index of flat element `i` of an array with `dims`.
|
/// `(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 {
|
fn index_text(mut i: u64, dims: &[u64]) -> String {
|
||||||
let mut idx = vec![0u64; dims.len()];
|
let mut idx = vec![0u64; dims.len()];
|
||||||
for (k, &d) in dims.iter().enumerate().rev() {
|
for (k, &d) in dims.iter().enumerate().rev() {
|
||||||
|
|||||||
@@ -149,6 +149,15 @@ for name, t in (("target1.h5", "/a"), ("target2.h5", "/b")):
|
|||||||
f["s"] = h5py.SoftLink(t)
|
f["s"] = h5py.SoftLink(t)
|
||||||
f["rel"] = h5py.SoftLink("a")
|
f["rel"] = h5py.SoftLink("a")
|
||||||
|
|
||||||
|
# Null-padded fixed strings with NULs, at the top level and nested in a
|
||||||
|
# compound and in an array member.
|
||||||
|
with h5py.File(os.path.join(out, "nulstrings.h5"), "w") as f:
|
||||||
|
f["top"] = np.array([b"", b"ab", b"a\x00b"], dtype="S3")
|
||||||
|
f["cmp"] = np.array(
|
||||||
|
[(b"", 1), (b"ab", 2), (b"a\x00b", 3)], dtype=[("s", "S3"), ("i", "i4")]
|
||||||
|
)
|
||||||
|
f["arr"] = np.array([([b"", b"x"],)], dtype=[("v", "(2,)S2")])
|
||||||
|
|
||||||
with h5py.File(os.path.join(out, "userblock.h5"), "w", userblock_size=1024) as f:
|
with h5py.File(os.path.join(out, "userblock.h5"), "w", userblock_size=1024) as f:
|
||||||
f["d"] = np.arange(10)
|
f["d"] = np.arange(10)
|
||||||
|
|
||||||
|
|||||||
@@ -280,6 +280,28 @@ fn dump_matches_h5dump() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A null-padded fixed string prints every byte (NULs as `\000`), as
|
||||||
|
/// h5dump prints it, also inside a compound and an array member.
|
||||||
|
#[test]
|
||||||
|
fn dump_shows_nul_padding_in_nested_strings() {
|
||||||
|
let Some(f) = generate() else { return };
|
||||||
|
let p = f.p("nulstrings.h5");
|
||||||
|
let ours = stdout(&h5rs(&["dump", &p]));
|
||||||
|
for want in [
|
||||||
|
r#"(0): "\000\000\000", "ab\000", "a\000b""#,
|
||||||
|
r#""\000\000\000","#,
|
||||||
|
r#""a\000b","#,
|
||||||
|
r#"[ "\000\000", "x\000" ]"#,
|
||||||
|
] {
|
||||||
|
assert!(ours.contains(want), "no {want} in\n{ours}");
|
||||||
|
}
|
||||||
|
if missing(tool_available("h5dump"), "h5dump") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let reference = run("h5dump", &[&p]);
|
||||||
|
assert_eq!(ours, stdout(&reference).replacen(&p, "nulstrings.h5", 1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dump_json_values_match_h5py() {
|
fn dump_json_values_match_h5py() {
|
||||||
let Some(f) = generate() else { return };
|
let Some(f) = generate() else { return };
|
||||||
|
|||||||
Reference in New Issue
Block a user