Files
clawhdf5/crates/clawhdf5-tools/src/ls.rs
T
osobhandClaude Opus 5.5 c513f7e6d7 h5rs: URLs as FILE arguments (feature remote)
With the `remote` feature (`remote-https` for https://), ls, dump, stat
and diff take an http(s):// (or s3://, gs://, az:// with those
clawhdf5-remote features) URL wherever they take a file, and read it by
range requests through clawhdf5-remote's block cache. check validates
every byte, so it downloads a remote file whole and checks it as before.
Without the feature a URL is a clean error naming it.

The tools read the file through File::storage instead of as_bytes: object
headers, shared messages, attributes, v1 and v2 group links, dense
storage (fractal heaps and v2 B-trees), path resolution, chunk listings
and variable-length values go through the format crate's *_in functions,
and the fractal-heap block verifier reads each block through the storage
(a read failure of a remote file is reported as a problem, not as "past
the end of the file"). A local file's storage is its mapped bytes, so its
reads are still slices. stat's file size comes from the opened file, so
it is right for a URL.

Tests: tests/remote.rs serves fixtures (old and new formats, a paged
file, a metadata cache image, a multi-block fractal heap, compounds, v1
groups) with the clawhdf5-remote test server and requires every
subcommand's output and exit status for the URL to equal the local
file's, and diff of the two to be clean; 404s, non-HDF5 bodies and
https without its feature are clean errors. Local output is unchanged:
the old and new h5rs print the same for ls -r -v, dump, stat and check
--data on the 747 conformance and CVE corpus files (tank, 2026-09-26;
the dumps of h5diff_hyper1/2.h5 were too large for the comparison
script, their ls, stat and check agree), except cve-2025-2310.h5, whose
dump error messages differ between runs of the old binary too (which
failing chunk is reported first).

ci-test.sh lints h5rs with remote-https, runs the URL tests and checks
h5rs with remote for C.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 17:23:53 -05:00

386 lines
13 KiB
Rust

//! `h5rs ls`: list a file's objects like h5ls.
use std::collections::HashMap;
use clawhdf5_format::attribute::AttributeMessage;
use clawhdf5_format::dataspace::DataspaceType;
use clawhdf5_format::object_header::ObjectHeader;
use crate::cli::{Args, Out};
use crate::h5::{H5, Kind, Link, LinkKind, split_file_arg};
use crate::info::{self, DsInfo};
use crate::value::{self, Decoder};
pub const USAGE: &str = "\
usage: h5rs ls [-r] [-v] [--max-bytes N] FILE[/OBJECT]
List the objects in FILE (or in the group OBJECT), one per line, like h5ls:
name, kind, shape and, for datasets, the datatype.
-r, --recursive list every object below, with full paths
-v, --verbose also print address, link count, layout, chunking,
storage, filters, datatype and attributes
--max-bytes N largest attribute value decoded for -v (default 1 GiB)
Exit status: 0 listed, 1 some object could not be described, 2 error.";
struct Ls<'a> {
h5: &'a H5,
verbose: bool,
kinds: HashMap<u64, Kind>,
problems: usize,
}
pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
let mut recursive = false;
let mut verbose = false;
let mut max_bytes = None;
let mut target = None;
while let Some(a) = args.next() {
match a.as_str() {
"-r" | "--recursive" => recursive = true,
"-v" | "--verbose" => verbose = true,
"-rv" | "-vr" => {
recursive = true;
verbose = true;
}
"--max-bytes" => match args.number() {
Some(n) => max_bytes = Some(n),
None => return args.usage_error(out, "--max-bytes needs a number", USAGE),
},
"-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 target.is_none() => target = Some(a),
_ => return args.usage_error(out, &format!("unexpected argument {a}"), USAGE),
}
}
let Some(target) = target else {
return args.usage_error(out, "missing FILE", USAGE);
};
let (file, obj) = split_file_arg(&target);
let mut h5 = match H5::open_arg(&file) {
Ok(h) => h,
Err(e) => {
writeln!(out.e, "h5rs ls: {e}")?;
return Ok(2);
}
};
if let Some(m) = max_bytes {
h5.max_bytes = m;
}
let mut ls = Ls {
h5: &h5,
verbose,
kinds: HashMap::new(),
problems: 0,
};
let obj_path = obj.unwrap_or_else(|| "/".into());
let addr = match h5.resolve(&obj_path) {
Ok(a) => a,
Err(e) => {
writeln!(out.e, "h5rs ls: {obj_path}: not found: {e}")?;
return Ok(2);
}
};
let header = match h5.header(addr) {
Ok(h) => h,
Err(e) => {
writeln!(out.e, "h5rs ls: {obj_path}: {e}")?;
return Ok(2);
}
};
let is_root = obj_path.trim_matches('/').is_empty();
if Kind::of(&header) != Kind::Group && !is_root {
// h5ls names a single object by its base name, or with -r by its
// path as given (without the leading slash).
let name = if recursive {
obj_path.trim_start_matches('/').to_string()
} else {
obj_path.rsplit('/').next().unwrap_or(&obj_path).to_string()
};
ls.object_line(out, &name, &obj_path, addr, &Ok(header))?;
} else if recursive {
let start = if is_root { "/" } else { "" };
let mut io_err = None;
let res = h5.walk_from(addr, start, &mut |item| {
if io_err.is_some() || (item.link.is_none() && !is_root) {
return; // listing a group's contents, not the group
}
let path = if item.path.is_empty() { "/" } else { item.path };
let full = if is_root {
path.to_string()
} else {
format!("{}{path}", obj_path.trim_end_matches('/'))
};
if let Err(e) = ls.item(
out,
path,
&full,
item.link,
item.addr,
item.first_path,
item.header,
) {
io_err = Some(e);
}
});
if let Some(e) = io_err {
return Err(e);
}
if let Err(e) = res {
writeln!(out.e, "h5rs ls: {e}")?;
ls.problems += 1;
}
} else {
match h5.links(&header) {
Ok(links) => {
for l in &links {
let (a, hdr) = match l.kind {
LinkKind::Hard(a) => (Some(a), Some(h5.header(a))),
_ => (None, None),
};
let full = format!("{}/{}", obj_path.trim_end_matches('/'), l.name);
ls.item(out, &l.name, &full, Some(l), a, None, hdr.as_ref())?;
}
}
Err(e) => {
writeln!(out.e, "h5rs ls: {obj_path}: cannot list group: {e}")?;
ls.problems += 1;
}
}
}
Ok(if ls.problems > 0 { 1 } else { 0 })
}
impl Ls<'_> {
#[allow(clippy::too_many_arguments)]
fn item(
&mut self,
out: &mut Out,
name: &str,
full: &str,
link: Option<&Link>,
addr: Option<u64>,
first: Option<&str>,
header: Option<&crate::h5::Result<ObjectHeader>>,
) -> std::io::Result<()> {
if let Some(l) = link {
match &l.kind {
LinkKind::Soft(t) => return writeln!(out.o, "{name:<24} Soft Link {{{t}}}"),
LinkKind::External { file, path } => {
return writeln!(out.o, "{name:<24} External Link {{{file}/{path}}}");
}
LinkKind::UserDefined(t) => {
return writeln!(out.o, "{name:<24} User-defined link (type {t})");
}
LinkKind::Hard(_) => {}
}
}
let Some(addr) = addr else { return Ok(()) };
if let Some(first) = first {
let k = self.kinds.get(&addr).copied().unwrap_or(Kind::Unknown);
return writeln!(out.o, "{name:<24} {}, same as {first}", k.name());
}
match header {
Some(h) => self.object_line(out, name, full, addr, h),
None => Ok(()),
}
}
fn object_line(
&mut self,
out: &mut Out,
name: &str,
full: &str,
addr: u64,
header: &crate::h5::Result<ObjectHeader>,
) -> std::io::Result<()> {
let h = match header {
Ok(h) => h,
Err(e) => {
self.problems += 1;
writeln!(out.o, "{name:<24} ** error **")?;
return writeln!(out.e, "h5rs ls: {name}: {e}");
}
};
let kind = Kind::of(h);
self.kinds.insert(addr, kind);
match kind {
Kind::Dataset => {
let info = DsInfo::read(self.h5, full, h);
let shape = match &info.ds {
Ok(ds) => info::shape_text(ds, self.verbose),
Err(_) => "{?}".into(),
};
if self.verbose {
writeln!(out.o, "{name:<24} Dataset {shape}")?;
} else {
let t = match &info.dt {
Ok(dt) => crate::dtype::short(dt),
Err(_) => "?".into(),
};
writeln!(out.o, "{name:<24} Dataset {shape} {t}")?;
}
for e in [
info.dt.as_ref().err(),
info.ds.as_ref().err(),
info.layout.as_ref().err(),
info.filters.as_ref().err(),
]
.into_iter()
.flatten()
{
self.problems += 1;
writeln!(out.e, "h5rs ls: {name}: {e}")?;
}
if self.verbose {
self.dataset_details(out, addr, h, &info)?;
}
}
k => {
writeln!(out.o, "{name:<24} {}", k.name())?;
if self.verbose {
writeln!(out.o, " Address: {addr}")?;
writeln!(out.o, " Links: {}", info::link_count(h))?;
if k == Kind::Datatype
&& let Ok(dt) = self.h5.datatype(h)
{
writeln!(out.o, " Type: {}", crate::dtype::long(&dt))?;
}
self.attributes(out, name, h)?;
}
}
}
Ok(())
}
fn dataset_details(
&mut self,
out: &mut Out,
addr: u64,
h: &ObjectHeader,
info: &DsInfo,
) -> std::io::Result<()> {
writeln!(out.o, " Address: {addr}")?;
writeln!(out.o, " Links: {}", info::link_count(h))?;
if let Ok(l) = &info.layout {
let idx = info::chunk_index_name(l);
if idx.is_empty() {
writeln!(out.o, " Layout: {}", info::layout_name(l))?;
} else {
writeln!(
out.o,
" Layout: {} ({idx} index)",
info::layout_name(l)
)?;
}
if let clawhdf5_format::data_layout::DataLayout::Chunked {
chunk_dimensions, ..
} = l
{
let rank = chunk_dimensions.len().saturating_sub(1);
let dims = &chunk_dimensions[..rank];
let esize = info
.dt
.as_ref()
.map(|d| u64::from(d.type_size()))
.unwrap_or(0);
let bytes = dims
.iter()
.try_fold(esize, |a, &d| a.checked_mul(u64::from(d)))
.map(|b| b.to_string())
.unwrap_or_else(|| "?".into());
writeln!(
out.o,
" Chunks: {{{}}} {bytes} bytes",
dims.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ")
)?;
}
}
if info.external {
writeln!(out.o, " External: raw data in external files")?;
}
let logical = info.logical_bytes();
match (logical, info::allocated_bytes(self.h5, info)) {
(Some(l), Ok(a)) => {
if a > 0 {
writeln!(
out.o,
" Storage: {l} logical bytes, {a} allocated bytes, {:.2}% utilization",
l as f64 * 100.0 / a as f64
)?;
} else {
writeln!(out.o, " Storage: {l} logical bytes, 0 allocated bytes")?;
}
}
(_, Err(e)) => {
self.problems += 1;
writeln!(out.e, "h5rs ls: {e}")?;
}
_ => {}
}
if let Ok(Some(p)) = &info.filters {
for (i, f) in p.filters.iter().enumerate() {
writeln!(out.o, " Filter-{i}: {}", info::filter_text(f))?;
}
}
if let Ok(dt) = &info.dt {
writeln!(out.o, " Type: {}", crate::dtype::long(dt))?;
}
self.attributes(out, "", h)
}
fn attributes(&mut self, out: &mut Out, name: &str, h: &ObjectHeader) -> std::io::Result<()> {
let (attrs, errs) = match self.h5.attributes(h) {
Ok(x) => x,
Err(e) => {
self.problems += 1;
return writeln!(out.e, "h5rs ls: {name}: {e}");
}
};
for e in errs {
self.problems += 1;
writeln!(out.e, "h5rs ls: {name}: attribute: {e}")?;
}
for a in &attrs {
self.attribute(out, a)?;
}
Ok(())
}
fn attribute(&mut self, out: &mut Out, a: &AttributeMessage) -> std::io::Result<()> {
let shape = match a.dataspace.space_type {
DataspaceType::Scalar => "scalar".to_string(),
DataspaceType::Null => "null".to_string(),
DataspaceType::Simple => info::shape_text(&a.dataspace, false),
};
writeln!(out.o, " Attribute: {} {shape}", a.name)?;
writeln!(
out.o,
" Type: {}",
crate::dtype::long(&a.datatype)
)?;
let n = crate::h5::num_elements(&a.dataspace).unwrap_or(0);
if n == 0 {
return Ok(());
}
let dec = Decoder::new(self.h5);
let shown = n.min(8) as usize;
let mut vals = Vec::with_capacity(shown);
for i in 0..shown {
let v = dec.element(&a.datatype, &a.raw_data, i);
vals.push(value::text(&v, &|_| None));
}
let more = if n > 8 { ", ..." } else { "" };
writeln!(out.o, " Data: {}{more}", vals.join(", "))
}
}