fix(tools): h5rs diff compares every name of a hard-linked object
The path walk skipped the second hard link to an object, so a file that shares one dataset between /x and /y differed from a file holding two identical copies: "</y> exists only in <B>", exit 1, where h5diff exits 0. For a hard-linked group every member was reported the same way. diff now enumerates every path below the start object (a hard link back to an ancestor is recorded but not descended into), so each name is compared. A group whose links cannot be read is now an error instead of an empty group. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::cell::OnceCell;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::rc::Rc;
|
||||
|
||||
use clawhdf5_format::attribute::AttributeMessage;
|
||||
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
||||
@@ -9,7 +10,7 @@ use clawhdf5_format::datatype::Datatype;
|
||||
use clawhdf5_format::object_header::ObjectHeader;
|
||||
|
||||
use crate::cli::{Args, Out};
|
||||
use crate::h5::{H5, Kind, LinkKind};
|
||||
use crate::h5::{H5, Kind, Link, LinkKind};
|
||||
use crate::value::{self, Decoder, Value};
|
||||
|
||||
pub const USAGE: &str = "\
|
||||
@@ -177,25 +178,13 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
||||
} else {
|
||||
format!("/{base}")
|
||||
};
|
||||
let mut entries = BTreeMap::new();
|
||||
let walk = h5.walk_from(addr, "", &mut |it| {
|
||||
let e = match (it.link.map(|l| &l.kind), it.header) {
|
||||
(Some(LinkKind::Soft(t)), _) => Entry::Soft(t.clone()),
|
||||
(Some(LinkKind::External { file, path }), _) => {
|
||||
Entry::External(file.clone(), path.clone())
|
||||
}
|
||||
(Some(LinkKind::UserDefined(t)), _) => Entry::UserDefined(*t),
|
||||
(_, Some(Err(e))) => Entry::Broken(e.to_string()),
|
||||
(_, Some(Ok(h))) => Entry::Obj(it.addr.unwrap_or(0), Kind::of(h)),
|
||||
// A second hard link to an object already compared.
|
||||
(_, None) => return,
|
||||
};
|
||||
entries.insert(it.path.to_string(), e);
|
||||
});
|
||||
if let Err(e) = walk {
|
||||
writeln!(out.e, "h5rs diff: {f}: {e}")?;
|
||||
return Ok(2);
|
||||
}
|
||||
let entries = match collect(h5, addr) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
writeln!(out.e, "h5rs diff: {f}: {e}")?;
|
||||
return Ok(2);
|
||||
}
|
||||
};
|
||||
sides.push(Side {
|
||||
h5,
|
||||
label: f.clone(),
|
||||
@@ -248,6 +237,88 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
||||
})
|
||||
}
|
||||
|
||||
/// One object as the path walk sees it: what its paths compare as, and
|
||||
/// (for a group) its links.
|
||||
struct Node {
|
||||
entry: Entry,
|
||||
links: Vec<Link>,
|
||||
}
|
||||
|
||||
/// Every path below the object at `start`, relative to it (`""` is `start`
|
||||
/// itself). Each hard link is its own path, so an object linked under two
|
||||
/// names is compared under both, with everything below it: two files that
|
||||
/// hold the same values are equal whether one shares an object between
|
||||
/// names and the other stores copies. A hard link back to an ancestor (a
|
||||
/// cycle) is recorded, not descended into.
|
||||
fn collect(h5: &H5, start: u64) -> crate::h5::Result<BTreeMap<String, Entry>> {
|
||||
let mut cache: HashMap<u64, Rc<Node>> = HashMap::new();
|
||||
let mut node = |addr: u64| -> Rc<Node> {
|
||||
cache
|
||||
.entry(addr)
|
||||
.or_insert_with(|| {
|
||||
Rc::new(match h5.header(addr) {
|
||||
Err(e) => Node {
|
||||
entry: Entry::Broken(e.to_string()),
|
||||
links: Vec::new(),
|
||||
},
|
||||
Ok(h) => match Kind::of(&h) {
|
||||
Kind::Group => match h5.links(&h) {
|
||||
Ok(links) => Node {
|
||||
entry: Entry::Obj(addr, Kind::Group),
|
||||
links,
|
||||
},
|
||||
Err(e) => Node {
|
||||
entry: Entry::Broken(format!("links: {e}")),
|
||||
links: Vec::new(),
|
||||
},
|
||||
},
|
||||
k => Node {
|
||||
entry: Entry::Obj(addr, k),
|
||||
links: Vec::new(),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
.clone()
|
||||
};
|
||||
let mut entries = BTreeMap::new();
|
||||
// (address, relative path, addresses of the groups above it)
|
||||
let mut stack: Vec<(u64, String, Rc<Vec<u64>>)> =
|
||||
vec![(start, String::new(), Rc::new(Vec::new()))];
|
||||
while let Some((addr, path, above)) = stack.pop() {
|
||||
if entries.len() >= crate::h5::MAX_OBJECTS {
|
||||
return Err(crate::h5::Error::new(format!(
|
||||
"more than {} paths; stopped walking",
|
||||
crate::h5::MAX_OBJECTS
|
||||
)));
|
||||
}
|
||||
let n = node(addr);
|
||||
entries.insert(path.clone(), n.entry.clone());
|
||||
if n.links.is_empty() || above.contains(&addr) {
|
||||
continue;
|
||||
}
|
||||
let mut chain = (*above).clone();
|
||||
chain.push(addr);
|
||||
let chain = Rc::new(chain);
|
||||
for l in &n.links {
|
||||
let child = format!("{path}/{}", l.name);
|
||||
match &l.kind {
|
||||
LinkKind::Hard(a) => stack.push((*a, child, chain.clone())),
|
||||
LinkKind::Soft(t) => {
|
||||
entries.insert(child, Entry::Soft(t.clone()));
|
||||
}
|
||||
LinkKind::External { file, path } => {
|
||||
entries.insert(child, Entry::External(file.clone(), path.clone()));
|
||||
}
|
||||
LinkKind::UserDefined(t) => {
|
||||
entries.insert(child, Entry::UserDefined(*t));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn kind_word(k: Kind) -> &'static str {
|
||||
match k {
|
||||
Kind::Group => "group",
|
||||
|
||||
Reference in New Issue
Block a user