read: of two links with one name, the first wins everywhere
A valid group has one link per name, but a damaged or hand-made one can have two. resolve_child followed the first soft link of the name, the listing skipped a dangling one and listed the name via a later link, and path resolution followed the last symbolic link: three answers. All now take the first link of the name (header message order in a compact group, name index order in a dense one) and ignore the rest, even if the first dangles. That is libhdf5's rule for compact groups (H5G__compact_lookup stops at the first Link message); h5py opens nothing for a dangling first link although a later one resolves. For a dense group libhdf5 binary-searches the index and may land on another of several exact duplicates; documented on first_link_named. find_symbolic_link's v2 branch was dead (only v1 groups reach it) and is now v1-only. Test: an h5py compact group with soft links dup_A (dangling, or to /d) and dup_B (the other), dup_B renamed to dup_A in the header and re-checksummed. Lookup, path and listing through all three readers match h5py for both orders. With the old group_v2.rs the path lookup returned 42 where h5py opens nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -495,3 +495,118 @@ fn a_corrupt_internal_index_node_is_an_error_not_a_missing_name() {
|
||||
));
|
||||
assert_eq!(out, "checksum checksum");
|
||||
}
|
||||
|
||||
/// Rename the one link called `from` to `to` (same length) in `bytes`, and
|
||||
/// re-checksum the object header chunk holding it: two links of one name,
|
||||
/// which libhdf5 cannot write.
|
||||
fn rename_link_in_header(bytes: &mut [u8], from: &[u8], to: &[u8]) {
|
||||
assert_eq!(from.len(), to.len());
|
||||
let find = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).position(|w| w == needle);
|
||||
let at = find(bytes, from).expect("link name");
|
||||
assert!(find(&bytes[at + 1..], from).is_none(), "name not unique");
|
||||
bytes[at..at + to.len()].copy_from_slice(to);
|
||||
// The v2 object header (chunk 0) holding it.
|
||||
let ohdr = bytes[..at]
|
||||
.windows(4)
|
||||
.rposition(|w| w == b"OHDR")
|
||||
.expect("OHDR");
|
||||
let flags = bytes[ohdr + 5];
|
||||
let mut pos = ohdr + 6;
|
||||
if flags & 0x20 != 0 {
|
||||
pos += 16; // times
|
||||
}
|
||||
if flags & 0x10 != 0 {
|
||||
pos += 4; // attribute phase change
|
||||
}
|
||||
let width = 1usize << (flags & 3);
|
||||
let mut size = [0u8; 8];
|
||||
size[..width].copy_from_slice(&bytes[pos..pos + width]);
|
||||
let end = pos + width + usize::try_from(u64::from_le_bytes(size)).unwrap();
|
||||
assert!(at < end, "name outside chunk 0");
|
||||
let sum = jenkins_lookup3(&bytes[ohdr..end]);
|
||||
bytes[end..end + 4].copy_from_slice(&sum.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Two soft links of one name (a damaged or hand-made group; libhdf5
|
||||
/// cannot create one), one dangling: only the first counts, as in libhdf5,
|
||||
/// which opens the first Link message of a name and fails if it dangles.
|
||||
/// Lookup, path and listing agree — before, the listing skipped a dangling
|
||||
/// first link and listed the name via the second, which lookup did not
|
||||
/// follow, and path resolution followed the last.
|
||||
#[test]
|
||||
fn of_two_links_with_one_name_the_first_wins_everywhere() {
|
||||
skip_if_no_python!();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for dangling_first in [true, false] {
|
||||
let path = dir
|
||||
.path()
|
||||
.join(format!("dup_{dangling_first}.h5"))
|
||||
.display()
|
||||
.to_string();
|
||||
let (first, second) = if dangling_first {
|
||||
("/nowhere_xyz", "/d")
|
||||
} else {
|
||||
("/d", "/nowhere_xyz")
|
||||
};
|
||||
run_python(&format!(
|
||||
"import h5py, numpy as np\n\
|
||||
with h5py.File(r'{path}', 'w', libver='latest') as f:\n\
|
||||
\x20 f.create_dataset('d', data=np.int64(42))\n\
|
||||
\x20 s = f.create_group('s')\n\
|
||||
\x20 s['dup_A'] = h5py.SoftLink('{first}')\n\
|
||||
\x20 s['dup_B'] = h5py.SoftLink('{second}')",
|
||||
));
|
||||
let mut bytes = std::fs::read(&path).unwrap();
|
||||
rename_link_in_header(&mut bytes, b"dup_B", b"dup_A");
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
|
||||
// What libhdf5 opens under that name: both names listed, first link
|
||||
// followed.
|
||||
let out = run_python(&format!(
|
||||
"import h5py\n\
|
||||
with h5py.File(r'{path}', 'r') as f:\n\
|
||||
\x20 s = f['s']\n\
|
||||
\x20 assert list(s) == ['dup_A', 'dup_A'], list(s)\n\
|
||||
\x20 try:\n\
|
||||
\x20 print(int(s['dup_A'][()]))\n\
|
||||
\x20 except KeyError:\n\
|
||||
\x20 print('none')",
|
||||
));
|
||||
let want = if dangling_first { "none" } else { "42" };
|
||||
assert_eq!(out, want, "h5py, dangling first: {dangling_first}");
|
||||
let want = (!dangling_first).then_some(42i64);
|
||||
|
||||
let f = File::open(&path).unwrap();
|
||||
let s = f.group("s").unwrap();
|
||||
let got = |r: Result<clawhdf5::Dataset<'_>, clawhdf5::Error>| match r {
|
||||
Ok(ds) => Some(ds.read_i64().unwrap()[0]),
|
||||
Err(e) => {
|
||||
assert!(is_not_found(&e), "{e:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
assert_eq!(got(s.dataset("dup_A")), want, "lookup, {dangling_first}");
|
||||
assert_eq!(got(f.dataset("/s/dup_A")), want, "path, {dangling_first}");
|
||||
let listed = s.datasets().unwrap();
|
||||
let listed_n = listed.iter().filter(|n| *n == "dup_A").count();
|
||||
assert_eq!(listed_n, usize::from(want.is_some()), "{listed:?}");
|
||||
let entries = s.entries().unwrap();
|
||||
assert_eq!(entries.len(), listed_n, "{entries:?}");
|
||||
|
||||
let m = MmapFile::open(&path).unwrap();
|
||||
let l = LazyFile::open_mmap(&path).unwrap();
|
||||
let (mg, lg) = (m.group("s").unwrap(), l.group("s").unwrap());
|
||||
match want {
|
||||
Some(v) => {
|
||||
assert_eq!(mg.dataset("dup_A").unwrap().read_i64().unwrap(), vec![v]);
|
||||
assert_eq!(lg.dataset("dup_A").unwrap().read_i64().unwrap(), vec![v]);
|
||||
}
|
||||
None => {
|
||||
assert!(mg.dataset("dup_A").is_err_and(|e| is_not_found(&e)));
|
||||
assert!(lg.dataset("dup_A").is_err_and(|e| is_not_found(&e)));
|
||||
}
|
||||
}
|
||||
assert_eq!(mg.datasets().unwrap(), listed);
|
||||
assert_eq!(lg.datasets().unwrap(), listed);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user