New workspace crate clawhdf5-tools with one binary, h5rs, built only on the clawhdf5 facade and clawhdf5-format (no libhdf5, no C): - ls [-r] [-v] FILE[/path]: h5ls's listing (same text in its first two columns) plus the datatype; -v adds address, link count, layout and chunk index, chunk size, storage, filters, datatype and attributes. - dump [--json] [-A] [-p] [-d PATH] FILE: h5dump DDL (byte-identical to h5dump 1.14.6 on the test files) or hdf5-json. - stat FILE: h5stat's object/link/rank/layout/filter/attribute counts, raw data and total size. - diff [-r] [-q] [-d D] [-p R] A B [OBJ1 [OBJ2]]: structural and value differences, exit 0/1/2 like h5diff. - check [--data] FILE: walks every object, parses every message, verifies the checksums of every v2+ structure (including the fractal heap blocks the library never checks), checks chunk indexes against their datasets and raw data for out-of-file or overlapping extents; every problem with its address. Values over --max-bytes are reported, not read; dense-storage heaps are verified before objects are read from them; panics are caught (exit 3). Tests compare with h5ls, h5stat, h5dump and h5diff and with h5py's values, and flip the checksum of every checksummed structure in a v1.14-format file. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
306 lines
10 KiB
Rust
306 lines
10 KiB
Rust
//! `h5rs stat`: object, layout, filter and storage statistics, laid out like
|
|
//! h5stat's report.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use clawhdf5_format::data_layout::DataLayout;
|
|
use clawhdf5_format::dataspace::DataspaceType;
|
|
|
|
use crate::cli::{Args, Out};
|
|
use crate::h5::{H5, Kind, LinkKind};
|
|
use crate::info::{self, DsInfo};
|
|
|
|
pub const USAGE: &str = "\
|
|
usage: h5rs stat FILE
|
|
|
|
Print statistics for FILE, in the layout of h5stat's report: object counts,
|
|
links, dataset ranks, layouts, filters, attribute counts, raw-data size and
|
|
a file-space summary. Metadata space is not broken down by structure (h5stat
|
|
does); it is reported as one figure together with free space.
|
|
|
|
Exit status: 0 printed, 1 some object could not be read (counted as
|
|
\"unreadable\"), 2 error.";
|
|
|
|
#[derive(Default)]
|
|
struct Stats {
|
|
groups: u64,
|
|
datasets: u64,
|
|
datatypes: u64,
|
|
other: u64,
|
|
unreadable: u64,
|
|
links: u64,
|
|
max_links_to_object: u64,
|
|
max_objects_in_group: u64,
|
|
group_sizes: BTreeMap<usize, u64>,
|
|
ranks: BTreeMap<usize, u64>,
|
|
max_1d: u64,
|
|
layout: [u64; 4],
|
|
external: u64,
|
|
no_filter: u64,
|
|
filter_counts: BTreeMap<&'static str, u64>,
|
|
raw_data: u64,
|
|
raw_errors: u64,
|
|
attr_objects: u64,
|
|
max_attrs: u64,
|
|
attr_counts: BTreeMap<usize, u64>,
|
|
}
|
|
|
|
const FILTER_KINDS: [&str; 8] = [
|
|
"GZIP",
|
|
"SHUFFLE",
|
|
"FLETCHER32",
|
|
"SZIP",
|
|
"NBIT",
|
|
"SCALEOFFSET",
|
|
"USER-DEFINED",
|
|
"",
|
|
];
|
|
|
|
fn filter_kind(id: u16) -> &'static str {
|
|
match id {
|
|
1 => "GZIP",
|
|
2 => "SHUFFLE",
|
|
3 => "FLETCHER32",
|
|
4 => "SZIP",
|
|
5 => "NBIT",
|
|
6 => "SCALEOFFSET",
|
|
_ => "USER-DEFINED",
|
|
}
|
|
}
|
|
|
|
pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|
let mut file = None;
|
|
while let Some(a) = args.next() {
|
|
match a.as_str() {
|
|
"-h" | "--help" => {
|
|
writeln!(out.o, "{USAGE}")?;
|
|
return Ok(0);
|
|
}
|
|
s if s.starts_with('-') && s.len() > 1 => {
|
|
return args.usage_error(out, &format!("unknown option {a}"), USAGE);
|
|
}
|
|
_ if file.is_none() => file = Some(a),
|
|
_ => return args.usage_error(out, &format!("unexpected argument {a}"), USAGE),
|
|
}
|
|
}
|
|
let Some(file) = file else {
|
|
return args.usage_error(out, "missing FILE", USAGE);
|
|
};
|
|
let h5 = match H5::open(std::path::Path::new(&file)) {
|
|
Ok(h) => h,
|
|
Err(e) => {
|
|
writeln!(out.e, "h5rs stat: {e}")?;
|
|
return Ok(2);
|
|
}
|
|
};
|
|
let mut s = Stats::default();
|
|
let mut errors: Vec<String> = Vec::new();
|
|
let walk = h5.walk(|it| {
|
|
if let Some(l) = it.link
|
|
&& !matches!(l.kind, LinkKind::Hard(_))
|
|
{
|
|
s.links += 1;
|
|
return;
|
|
}
|
|
if it.first_path.is_some() {
|
|
return;
|
|
}
|
|
let Some(Ok(h)) = it.header else {
|
|
if let Some(Err(e)) = it.header {
|
|
s.unreadable += 1;
|
|
errors.push(format!("{}: {e}", it.path));
|
|
}
|
|
return;
|
|
};
|
|
s.max_links_to_object = s.max_links_to_object.max(info::link_count(h));
|
|
match h5.attributes(h) {
|
|
Ok((a, errs)) => {
|
|
let n = a.len() + errs.len();
|
|
if n > 0 {
|
|
s.attr_objects += 1;
|
|
s.max_attrs = s.max_attrs.max(n as u64);
|
|
*s.attr_counts.entry(n).or_default() += 1;
|
|
}
|
|
for e in errs {
|
|
errors.push(format!("{}: attribute: {e}", it.path));
|
|
}
|
|
}
|
|
Err(e) => errors.push(format!("{}: {e}", it.path)),
|
|
}
|
|
match Kind::of(h) {
|
|
Kind::Group => {
|
|
s.groups += 1;
|
|
match h5.links(h) {
|
|
Ok(l) => {
|
|
s.max_objects_in_group = s.max_objects_in_group.max(l.len() as u64);
|
|
*s.group_sizes.entry(l.len()).or_default() += 1;
|
|
}
|
|
Err(e) => {
|
|
s.unreadable += 1;
|
|
errors.push(format!("{}: {e}", it.path));
|
|
}
|
|
}
|
|
}
|
|
Kind::Datatype => s.datatypes += 1,
|
|
Kind::Unknown => {
|
|
// The root group of a file with no links at all has no
|
|
// group messages; count it as a group.
|
|
if it.link.is_none() {
|
|
s.groups += 1;
|
|
*s.group_sizes.entry(0).or_default() += 1;
|
|
} else {
|
|
s.other += 1;
|
|
}
|
|
}
|
|
Kind::Dataset => {
|
|
s.datasets += 1;
|
|
let info = DsInfo::read(&h5, it.path, h);
|
|
if let Ok(ds) = &info.ds {
|
|
let rank = match ds.space_type {
|
|
DataspaceType::Simple => ds.dimensions.len(),
|
|
_ => 0,
|
|
};
|
|
*s.ranks.entry(rank).or_default() += 1;
|
|
if rank == 1 {
|
|
s.max_1d = s.max_1d.max(ds.dimensions[0]);
|
|
}
|
|
}
|
|
match &info.layout {
|
|
Ok(l) => {
|
|
let i = match l {
|
|
DataLayout::Compact { .. } => 0,
|
|
DataLayout::Contiguous { .. } => 1,
|
|
DataLayout::Chunked { .. } => 2,
|
|
DataLayout::Virtual { .. } => 3,
|
|
};
|
|
s.layout[i] += 1;
|
|
}
|
|
Err(e) => errors.push(format!("{}: {e}", it.path)),
|
|
}
|
|
if info.external {
|
|
s.external += 1;
|
|
}
|
|
match &info.filters {
|
|
Ok(Some(p)) if !p.filters.is_empty() => {
|
|
let mut kinds: Vec<&'static str> =
|
|
p.filters.iter().map(|f| filter_kind(f.filter_id)).collect();
|
|
kinds.sort_unstable();
|
|
kinds.dedup();
|
|
for k in kinds {
|
|
*s.filter_counts.entry(k).or_default() += 1;
|
|
}
|
|
}
|
|
Ok(_) => s.no_filter += 1,
|
|
Err(e) => errors.push(format!("{}: {e}", it.path)),
|
|
}
|
|
match info::allocated_bytes(&h5, &info) {
|
|
Ok(b) => s.raw_data = s.raw_data.saturating_add(b),
|
|
Err(e) => {
|
|
s.raw_errors += 1;
|
|
errors.push(format!("{}: storage size: {e}", it.path));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if let Err(e) = walk {
|
|
errors.push(e.to_string());
|
|
}
|
|
report(&h5, &file, &s, out)?;
|
|
for e in &errors {
|
|
writeln!(out.e, "h5rs stat: {e}")?;
|
|
}
|
|
Ok(if errors.is_empty() { 0 } else { 1 })
|
|
}
|
|
|
|
fn report(h5: &H5, file: &str, s: &Stats, out: &mut Out) -> std::io::Result<()> {
|
|
let o = &mut out.o;
|
|
writeln!(o, "Filename: {file}")?;
|
|
writeln!(o, "File information")?;
|
|
writeln!(o, "\t# of unique groups: {}", s.groups)?;
|
|
writeln!(o, "\t# of unique datasets: {}", s.datasets)?;
|
|
writeln!(o, "\t# of unique named datatypes: {}", s.datatypes)?;
|
|
writeln!(o, "\t# of unique links: {}", s.links)?;
|
|
writeln!(o, "\t# of unique other: {}", s.other)?;
|
|
if s.unreadable > 0 {
|
|
writeln!(o, "\t# of unreadable objects: {}", s.unreadable)?;
|
|
}
|
|
writeln!(o, "\tMax. # of links to object: {}", s.max_links_to_object)?;
|
|
writeln!(
|
|
o,
|
|
"\tMax. # of objects in group: {}",
|
|
s.max_objects_in_group
|
|
)?;
|
|
let sb = h5.sb();
|
|
writeln!(o, "Superblock:")?;
|
|
writeln!(o, "\tVersion: {}", sb.version)?;
|
|
writeln!(o, "\tSize of offsets: {} bytes", sb.offset_size)?;
|
|
writeln!(o, "\tSize of lengths: {} bytes", sb.length_size)?;
|
|
writeln!(o, "\tUser block: {} bytes", h5.file.user_block_size())?;
|
|
writeln!(o, "Group bins:")?;
|
|
for (n, c) in &s.group_sizes {
|
|
writeln!(o, "\t# of groups with {n} link(s): {c}")?;
|
|
}
|
|
writeln!(o, "\tTotal # of groups: {}", s.groups)?;
|
|
writeln!(o, "Dataset dimension information:")?;
|
|
writeln!(
|
|
o,
|
|
"\tMax. rank of datasets: {}",
|
|
s.ranks.keys().next_back().copied().unwrap_or(0)
|
|
)?;
|
|
writeln!(o, "\tDataset ranks:")?;
|
|
for (r, c) in &s.ranks {
|
|
writeln!(o, "\t\t# of dataset with rank {r}: {c}")?;
|
|
}
|
|
writeln!(o, "1-D Dataset information:")?;
|
|
writeln!(o, "\tMax. dimension size of 1-D datasets: {}", s.max_1d)?;
|
|
writeln!(o, "Dataset storage information:")?;
|
|
writeln!(o, "\tTotal raw data size: {}", s.raw_data)?;
|
|
if s.raw_errors > 0 {
|
|
writeln!(
|
|
o,
|
|
"\tDatasets whose storage size could not be read: {}",
|
|
s.raw_errors
|
|
)?;
|
|
}
|
|
writeln!(o, "Dataset layout information:")?;
|
|
for (i, n) in ["COMPACT", "CONTIG", "CHUNKED", "VIRTUAL"]
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
writeln!(o, "\tDataset layout counts[{n}]: {}", s.layout[i])?;
|
|
}
|
|
writeln!(o, "\tDatasets with external raw data: {}", s.external)?;
|
|
writeln!(o, "Dataset filters information:")?;
|
|
writeln!(o, "\tNumber of datasets with:")?;
|
|
writeln!(o, "\t\tNO filter: {}", s.no_filter)?;
|
|
for k in FILTER_KINDS.iter().filter(|k| !k.is_empty()) {
|
|
writeln!(
|
|
o,
|
|
"\t\t{k} filter: {}",
|
|
s.filter_counts.get(k).copied().unwrap_or(0)
|
|
)?;
|
|
}
|
|
writeln!(o, "Attribute information:")?;
|
|
for (n, c) in &s.attr_counts {
|
|
writeln!(o, "\t# of objects with {n} attribute(s): {c}")?;
|
|
}
|
|
writeln!(
|
|
o,
|
|
"\tTotal # of objects with attributes: {}",
|
|
s.attr_objects
|
|
)?;
|
|
writeln!(o, "\tMax. # of attributes to objects: {}", s.max_attrs)?;
|
|
let total = std::fs::metadata(&h5.path).map(|m| m.len()).unwrap_or(0);
|
|
let ub = h5.file.user_block_size();
|
|
writeln!(o, "Summary of file space information:")?;
|
|
writeln!(o, " User block: {ub} bytes")?;
|
|
writeln!(o, " Raw data: {} bytes", s.raw_data)?;
|
|
writeln!(
|
|
o,
|
|
" Metadata and free space: {} bytes",
|
|
total.saturating_sub(s.raw_data).saturating_sub(ub)
|
|
)?;
|
|
writeln!(o, "Total space: {total} bytes")
|
|
}
|