Read HDF5 1.6-era files, user blocks, VDS, dense attributes and large groups #13

Merged
osobh merged 28 commits from fix/p1-read-gaps into main 2026-09-26 09:42:10 +00:00
8 changed files with 277 additions and 133 deletions
Showing only changes of commit aadfd18d4c - Show all commits
+10
View File
@@ -327,6 +327,16 @@
cannot be followed without the application that registered the class, so cannot be followed without the application that registered the class, so
they are now left out of `datasets()`/`groups()` and path lookup, as h5py they are now left out of `datasets()`/`groups()` and path lookup, as h5py
leaves out links it cannot open; reserved link types are still an error. leaves out links it cannot open; reserved link types are still an error.
- `clawhdf5` — soft links are listed, as h5py lists them: `datasets()` and
`groups()` on `Group`/`MmapGroup`/`LazyGroup` include each soft link under
its own name as the kind of object it resolves to, and `dataset(name)` /
`group(name)` open through it. Relative targets resolve from the group
holding the link. Dangling or cyclic soft links, external links and
user-defined links are left out (h5py lists their names but cannot open
them). Previously soft links were missing from the listings, and in
old-style (symbol table) groups a soft link made the listing fail. New
`group_v2::resolve_group_children` / `resolve_path_from` and
`group_v1::v1_soft_links` in `clawhdf5-format`.
- `clawhdf5-format` writer — **files libhdf5 rejects or reads wrong:** - `clawhdf5-format` writer — **files libhdf5 rejects or reads wrong:**
- Extensible Array (one unlimited dimension): chunks from index 244 on were - Extensible Array (one unlimited dimension): chunks from index 244 on were
written but never indexed and read as 0, by libhdf5 and by us. written but never indexed and read as 0, by libhdf5 and by us.
+60 -5
View File
@@ -73,6 +73,53 @@ pub fn find_v1_soft_link(
offset_size: u8, offset_size: u8,
length_size: u8, length_size: u8,
) -> Result<Option<String>, FormatError> { ) -> Result<Option<String>, FormatError> {
let mut found = None;
for_each_v1_soft_link(
file_data,
sym_table_msg,
offset_size,
length_size,
|link_name| link_name == name,
|_, target| {
found = Some(target);
false
},
)?;
Ok(found)
}
/// Every soft link in a v1 group, as `(name, target path)`.
pub fn v1_soft_links(
file_data: &[u8],
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
) -> Result<Vec<(String, String)>, FormatError> {
let mut links = Vec::new();
for_each_v1_soft_link(
file_data,
sym_table_msg,
offset_size,
length_size,
|_| true,
|name, target| {
links.push((String::from(name), target));
true
},
)?;
Ok(links)
}
/// Visit the soft links of a v1 group whose name passes `wanted`, with their
/// target paths, until `visit` returns false.
fn for_each_v1_soft_link(
file_data: &[u8],
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
wanted: impl Fn(&str) -> bool,
mut visit: impl FnMut(&str, String) -> bool,
) -> Result<(), FormatError> {
let heap = LocalHeap::parse( let heap = LocalHeap::parse(
file_data, file_data,
sym_table_msg.local_heap_address as usize, sym_table_msg.local_heap_address as usize,
@@ -91,7 +138,8 @@ pub fn find_v1_soft_link(
if entry.cache_type != CACHE_TYPE_SOFT_LINK { if entry.cache_type != CACHE_TYPE_SOFT_LINK {
continue; continue;
} }
if heap.read_string(file_data, entry.link_name_offset)? != name { let name = heap.read_string(file_data, entry.link_name_offset)?;
if !wanted(&name) {
continue; continue;
} }
let value_offset = u32::from_le_bytes([ let value_offset = u32::from_le_bytes([
@@ -100,12 +148,19 @@ pub fn find_v1_soft_link(
entry.scratch_pad[2], entry.scratch_pad[2],
entry.scratch_pad[3], entry.scratch_pad[3],
]); ]);
return heap let target = heap.read_string(file_data, u64::from(value_offset))?;
.read_string(file_data, u64::from(value_offset)) if !visit(&name, target) {
.map(Some); return Ok(());
} }
} }
Ok(None) }
Ok(())
}
/// Whether a v1 symbol-table entry is a soft link (no object header of its
/// own; its target path is in the local heap).
pub fn is_v1_soft_link(entry: &GroupEntry) -> bool {
entry.cache_type == CACHE_TYPE_SOFT_LINK
} }
/// Extract the SymbolTableMessage from an object header's messages. /// Extract the SymbolTableMessage from an object header's messages.
+118 -18
View File
@@ -255,32 +255,135 @@ pub fn resolve_path_any(
superblock: &Superblock, superblock: &Superblock,
path: &str, path: &str,
) -> Result<u64, FormatError> { ) -> Result<u64, FormatError> {
resolve_path_following_links(file_data, superblock, path, 0) resolve_path_following_links(
file_data,
superblock,
superblock.root_group_address,
path,
0,
)
}
/// Resolve `path` relative to the group at `group_address` (an absolute path
/// starts at the root group instead), following soft links. This is how a
/// relative soft link's target is resolved: from the group holding the link.
pub fn resolve_path_from(
file_data: &[u8],
superblock: &Superblock,
group_address: u64,
path: &str,
) -> Result<u64, FormatError> {
let start = if path.starts_with('/') {
superblock.root_group_address
} else {
group_address
};
resolve_path_following_links(file_data, superblock, start, path, 0)
}
/// The children of the group at `group_address` that can be opened, as h5py
/// lists them: hard links, and soft links resolved to the object they point
/// at (under the soft link's own name). Links that cannot be followed are
/// left out rather than failing the listing — a dangling or cyclic soft link
/// (h5py lists its name but cannot open it), an external link (another
/// file), and a user-defined link. An object header that is not a group has
/// no children.
///
/// Any other error, such as a corrupt structure met while resolving a soft
/// link, is returned.
pub fn resolve_group_children(
file_data: &[u8],
superblock: &Superblock,
group_address: u64,
) -> Result<Vec<GroupEntry>, FormatError> {
let os = superblock.offset_size;
let ls = superblock.length_size;
let header = ObjectHeader::parse(file_data, group_address as usize, os, ls)?;
let mut entries = Vec::new();
let mut soft = Vec::new();
if is_v1_group(&header) {
let sym_msg = header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, os)?;
let all = group_v1::resolve_v1_group_entries(file_data, &stm, os, ls)?;
if all.iter().any(group_v1::is_v1_soft_link) {
soft = group_v1::v1_soft_links(file_data, &stm, os, ls)?;
}
entries.extend(all.into_iter().filter(|e| !group_v1::is_v1_soft_link(e)));
} else if is_v2_group(&header) {
let mut visit = |link: LinkMessage| match link.link_target {
LinkTarget::Hard {
object_header_address,
} => entries.push(GroupEntry {
name: link.name,
object_header_address,
cache_type: 0,
}),
LinkTarget::Soft { target_path } => soft.push((link.name, target_path)),
LinkTarget::External { .. } => {}
};
let link_info = find_link_info(&header, os)?;
if let Some(fh_addr) = link_info.fractal_heap_address {
for_each_dense_link(file_data, &link_info, fh_addr, os, ls, visit)?;
} else {
for msg in &header.messages {
if msg.msg_type == MessageType::Link
&& let Some(link) = parse_link(&msg.data, os)?
{
visit(link);
}
}
}
}
for (name, target) in soft {
match resolve_path_from(file_data, superblock, group_address, &target) {
Ok(object_header_address) => entries.push(GroupEntry {
name,
object_header_address,
cache_type: 0,
}),
// Dangling, cyclic, or ending in another file: not openable here.
Err(
FormatError::PathNotFound(_)
| FormatError::NestingDepthExceeded
| FormatError::ExternalLinkUnsupported { .. },
) => {}
Err(e) => return Err(e),
}
}
Ok(entries)
} }
/// Soft links followed while resolving one path. Guards against link cycles /// Soft links followed while resolving one path. Guards against link cycles
/// (`a -> b -> a`), which are legal to create. /// (`a -> b -> a`), which are legal to create.
const MAX_SOFT_LINK_DEPTH: u8 = 16; const MAX_SOFT_LINK_DEPTH: u8 = 16;
/// Walk `path` from the group at `start`, following soft links.
fn resolve_path_following_links( fn resolve_path_following_links(
file_data: &[u8], file_data: &[u8],
superblock: &Superblock, superblock: &Superblock,
start: u64,
path: &str, path: &str,
depth: u8, depth: u8,
) -> Result<u64, FormatError> { ) -> Result<u64, FormatError> {
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); let components: Vec<&str> = path
.split('/')
.filter(|s| !s.is_empty() && *s != ".")
.collect();
if components.is_empty() { if components.is_empty() {
return Ok(superblock.root_group_address); return Ok(start);
} }
let os = superblock.offset_size; let os = superblock.offset_size;
let ls = superblock.length_size; let ls = superblock.length_size;
let root_header = let mut current_addr = start;
ObjectHeader::parse(file_data, superblock.root_group_address as usize, os, ls)?; let mut current_header = ObjectHeader::parse(file_data, start as usize, os, ls)?;
let mut current_addr = superblock.root_group_address;
let mut current_header = root_header;
for (i, component) in components.iter().enumerate() { for (i, component) in components.iter().enumerate() {
let entries = resolve_group_entries(file_data, &current_header, os, ls)?; let entries = resolve_group_entries(file_data, &current_header, os, ls)?;
@@ -304,20 +407,17 @@ fn resolve_path_following_links(
} }
// A relative target is relative to the group holding // A relative target is relative to the group holding
// the link; then the rest of the original path. // the link; then the rest of the original path.
let mut full = String::new(); let from = if target_path.starts_with('/') {
if !target_path.starts_with('/') { superblock.root_group_address
for parent in &components[..i] { } else {
full.push('/'); current_addr
full.push_str(parent); };
} let mut full = target_path;
}
full.push('/');
full.push_str(&target_path);
for rest in &components[i + 1..] { for rest in &components[i + 1..] {
full.push('/'); full.push('/');
full.push_str(rest); full.push_str(rest);
} }
resolve_path_following_links(file_data, superblock, &full, depth + 1) resolve_path_following_links(file_data, superblock, from, &full, depth + 1)
} }
Some(LinkTarget::External { Some(LinkTarget::External {
filename, filename,
+7 -36
View File
@@ -19,13 +19,12 @@ use clawhdf5_format::dataspace::Dataspace;
use clawhdf5_format::datatype::Datatype; use clawhdf5_format::datatype::Datatype;
use clawhdf5_format::error::FormatError; use clawhdf5_format::error::FormatError;
use clawhdf5_format::filter_pipeline::FilterPipeline; use clawhdf5_format::filter_pipeline::FilterPipeline;
use clawhdf5_format::group_v1::{self, GroupEntry}; use clawhdf5_format::group_v1::GroupEntry;
use clawhdf5_format::group_v2; use clawhdf5_format::group_v2;
use clawhdf5_format::message_type::MessageType; use clawhdf5_format::message_type::MessageType;
use clawhdf5_format::object_header::ObjectHeader; use clawhdf5_format::object_header::ObjectHeader;
use clawhdf5_format::signature; use clawhdf5_format::signature;
use clawhdf5_format::superblock::Superblock; use clawhdf5_format::superblock::Superblock;
use clawhdf5_format::symbol_table::SymbolTableMessage;
use clawhdf5_io::HDF5Read; use clawhdf5_io::HDF5Read;
@@ -275,12 +274,14 @@ impl<'f, R: HDF5Read> LazyGroup<'f, R> {
}) })
} }
/// This group's links that can be opened: hard links, and soft links
/// resolved to their targets (see
/// [`group_v2::resolve_group_children`]); dangling, external and
/// user-defined links are left out.
fn children(&self) -> Result<Vec<GroupEntry>, Error> { fn children(&self) -> Result<Vec<GroupEntry>, Error> {
let hdr = self.file.get_or_parse_header(self.address)?;
let data = self.file.reader.as_bytes(); let data = self.file.reader.as_bytes();
let os = self.file.offset_size(); group_v2::resolve_group_children(data, &self.file.superblock, self.address)
let ls = self.file.length_size(); .map_err(Error::Format)
resolve_group_entries(data, &hdr, os, ls).map_err(Error::Format)
} }
} }
@@ -530,33 +531,3 @@ fn is_group(header: &ObjectHeader) -> bool {
|| m.msg_type == MessageType::SymbolTable || m.msg_type == MessageType::SymbolTable
}) })
} }
fn resolve_group_entries(
file_data: &[u8],
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let is_v1 = object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::SymbolTable);
let is_v2 = object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::LinkInfo || m.msg_type == MessageType::Link);
if is_v1 {
let sym_msg = object_header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound("no symbol table message".into()))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
group_v1::resolve_v1_group_entries(file_data, &stm, offset_size, length_size)
} else if is_v2 {
group_v2::resolve_v2_group_entries(file_data, object_header, offset_size, length_size)
} else {
Ok(Vec::new())
}
}
+7 -36
View File
@@ -14,13 +14,12 @@ use clawhdf5_format::dataspace::Dataspace;
use clawhdf5_format::datatype::Datatype; use clawhdf5_format::datatype::Datatype;
use clawhdf5_format::error::FormatError; use clawhdf5_format::error::FormatError;
use clawhdf5_format::filter_pipeline::FilterPipeline; use clawhdf5_format::filter_pipeline::FilterPipeline;
use clawhdf5_format::group_v1::{self, GroupEntry}; use clawhdf5_format::group_v1::GroupEntry;
use clawhdf5_format::group_v2; use clawhdf5_format::group_v2;
use clawhdf5_format::message_type::MessageType; use clawhdf5_format::message_type::MessageType;
use clawhdf5_format::object_header::ObjectHeader; use clawhdf5_format::object_header::ObjectHeader;
use clawhdf5_format::signature; use clawhdf5_format::signature;
use clawhdf5_format::superblock::Superblock; use clawhdf5_format::superblock::Superblock;
use clawhdf5_format::symbol_table::SymbolTableMessage;
use clawhdf5_io::MmapReader; use clawhdf5_io::MmapReader;
@@ -197,12 +196,14 @@ impl<'f> MmapGroup<'f> {
}) })
} }
/// This group's links that can be opened: hard links, and soft links
/// resolved to their targets (see
/// [`group_v2::resolve_group_children`]); dangling, external and
/// user-defined links are left out.
fn children(&self) -> Result<Vec<GroupEntry>, Error> { fn children(&self) -> Result<Vec<GroupEntry>, Error> {
let data = self.file.reader.as_bytes(); let data = self.file.reader.as_bytes();
let hdr = self.file.parse_header(self.address)?; group_v2::resolve_group_children(data, &self.file.superblock, self.address)
let os = self.file.offset_size(); .map_err(Error::Format)
let ls = self.file.length_size();
resolve_group_entries(data, &hdr, os, ls).map_err(Error::Format)
} }
} }
@@ -470,33 +471,3 @@ fn is_group(header: &ObjectHeader) -> bool {
|| m.msg_type == MessageType::SymbolTable || m.msg_type == MessageType::SymbolTable
}) })
} }
fn resolve_group_entries(
file_data: &[u8],
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let is_v1 = object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::SymbolTable);
let is_v2 = object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::LinkInfo || m.msg_type == MessageType::Link);
if is_v1 {
let sym_msg = object_header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound("no symbol table message".into()))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
group_v1::resolve_v1_group_entries(file_data, &stm, offset_size, length_size)
} else if is_v2 {
group_v2::resolve_v2_group_entries(file_data, object_header, offset_size, length_size)
} else {
Ok(Vec::new())
}
}
+7 -37
View File
@@ -15,13 +15,12 @@ use clawhdf5_format::dataspace::Dataspace;
use clawhdf5_format::datatype::{Datatype, DatatypeByteOrder}; use clawhdf5_format::datatype::{Datatype, DatatypeByteOrder};
use clawhdf5_format::error::FormatError; use clawhdf5_format::error::FormatError;
use clawhdf5_format::filter_pipeline::FilterPipeline; use clawhdf5_format::filter_pipeline::FilterPipeline;
use clawhdf5_format::group_v1::{self, GroupEntry}; use clawhdf5_format::group_v1::GroupEntry;
use clawhdf5_format::group_v2; use clawhdf5_format::group_v2;
use clawhdf5_format::message_type::MessageType; use clawhdf5_format::message_type::MessageType;
use clawhdf5_format::object_header::ObjectHeader; use clawhdf5_format::object_header::ObjectHeader;
use clawhdf5_format::signature; use clawhdf5_format::signature;
use clawhdf5_format::superblock::Superblock; use clawhdf5_format::superblock::Superblock;
use clawhdf5_format::symbol_table::SymbolTableMessage;
use crate::error::Error; use crate::error::Error;
use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype}; use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype};
@@ -337,12 +336,14 @@ impl<'f> Group<'f> {
}) })
} }
/// This group's links that can be opened: hard links, and soft links
/// resolved to their targets (see
/// [`group_v2::resolve_group_children`]); dangling, external and
/// user-defined links are left out.
fn children(&self) -> Result<Vec<GroupEntry>, Error> { fn children(&self) -> Result<Vec<GroupEntry>, Error> {
let data = self.file.data.as_bytes(); let data = self.file.data.as_bytes();
let hdr = self.file.parse_header(self.address)?; group_v2::resolve_group_children(data, &self.file.superblock, self.address)
let os = self.file.offset_size(); .map_err(Error::Format)
let ls = self.file.length_size();
resolve_group_entries(data, &hdr, os, ls).map_err(Error::Format)
} }
} }
@@ -959,37 +960,6 @@ fn is_group(header: &ObjectHeader) -> bool {
}) })
} }
fn resolve_group_entries(
file_data: &[u8],
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let is_v1 = object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::SymbolTable);
let is_v2 = object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::LinkInfo || m.msg_type == MessageType::Link);
if is_v1 {
let sym_msg = object_header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound("no symbol table message".into()))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
group_v1::resolve_v1_group_entries(file_data, &stm, offset_size, length_size)
} else if is_v2 {
group_v2::resolve_v2_group_entries(file_data, object_header, offset_size, length_size)
} else {
// Empty group or unrecognized — return empty
Ok(Vec::new())
}
}
#[cfg(test)] #[cfg(test)]
mod sibling_file_name_tests { mod sibling_file_name_tests {
use super::sibling_file_name; use super::sibling_file_name;
@@ -298,3 +298,69 @@ fn user_defined_links_do_not_break_the_listing() {
assert!(f.root().datasets().unwrap().is_empty()); assert!(f.root().datasets().unwrap().is_empty());
assert!(f.root().groups().unwrap().is_empty()); assert!(f.root().groups().unwrap().is_empty());
} }
/// Soft links (absolute, relative, to a dataset and to a group), a dangling
/// one, a cycle and an external link, in an old-style (symbol table) or
/// new-style (link message) group.
fn soft_link_file(libver: &str) -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("soft.h5").display().to_string();
run_python(&format!(
"import h5py, numpy as np\n\
with h5py.File(r'{path}', 'w', libver='{libver}') as f:\n\
\x20 f.create_dataset('a/b/deep', data=np.arange(3.0))\n\
\x20 f.create_dataset('plain', data=[1.0])\n\
\x20 f['soft_ds'] = h5py.SoftLink('/a/b/deep')\n\
\x20 f['soft_grp'] = h5py.SoftLink('/a')\n\
\x20 f['a/rel'] = h5py.SoftLink('b/deep')\n\
\x20 f['a/rel_grp'] = h5py.SoftLink('b')\n\
\x20 f['dangling'] = h5py.SoftLink('/nope')\n\
\x20 f['loop1'] = h5py.SoftLink('/loop2')\n\
\x20 f['loop2'] = h5py.SoftLink('/loop1')\n\
\x20 f['ext'] = h5py.ExternalLink('elsewhere.h5', '/x')\n"
));
(dir, path)
}
fn check_soft_links(libver: &str) {
let (_dir, path) = soft_link_file(libver);
assert_same_listing(&path, "/");
assert_same_listing(&path, "a");
let f = File::open(&path).unwrap();
let root = f.root();
let deep = vec![0.0, 1.0, 2.0];
assert_eq!(root.dataset("soft_ds").unwrap().read_f64().unwrap(), deep);
let a = root.group("soft_grp").unwrap();
assert_eq!(a.dataset("rel").unwrap().read_f64().unwrap(), deep);
assert_eq!(a.group("rel_grp").unwrap().datasets().unwrap(), ["deep"]);
// A dangling link is not listed and cannot be opened.
assert!(root.dataset("dangling").is_err());
assert!(root.dataset("loop1").is_err());
// The memory-mapped and lazy handles list the same way.
let (ds, gs) = h5py_listing(&path, "/");
let m = clawhdf5::MmapFile::open(&path).unwrap();
let mut mds = m.root().datasets().unwrap();
let mut mgs = m.root().groups().unwrap();
mds.sort();
mgs.sort();
assert_eq!((mds, mgs), (ds.clone(), gs.clone()));
let l = clawhdf5::LazyFile::from_bytes(std::fs::read(&path).unwrap()).unwrap();
let mut lds = l.root().datasets().unwrap();
let mut lgs = l.root().groups().unwrap();
lds.sort();
lgs.sort();
assert_eq!((lds, lgs), (ds, gs));
}
/// Soft links were left out of `datasets()`/`groups()` (and could not be
/// opened by name from a group handle); in old-style groups, where a soft
/// link has no object header address, they made the listing fail. h5py
/// lists a soft link under its own name as whatever it points at.
#[test]
fn soft_links_are_listed_as_their_targets() {
skip_if_no_python!();
check_soft_links("latest");
check_soft_links("earliest");
}
+2 -1
View File
@@ -82,7 +82,8 @@ the VDS item, which is marked.
**Fixed 2026-09-25:** two bugs — fractal-heap child indirect blocks had **Fixed 2026-09-25:** two bugs — fractal-heap child indirect blocks had
the wrong row count, and v2 B-tree internal nodes at depth 3+ were read the wrong row count, and v2 B-tree internal nodes at depth 3+ were read
with the wrong pointer widths. with the wrong pointer widths.
- Soft links are left out of `datasets()`. - Soft links are left out of `datasets()`. **Fixed 2026-09-25:** soft
links are listed as their targets; dangling ones are left out.
- **Dense attributes:** a large attribute stored as a fractal-heap "huge" - **Dense attributes:** a large attribute stored as a fractal-heap "huge"
object makes every attribute on the object fail. This affects real NetCDF object makes every attribute on the object fail. This affects real NetCDF
files (`issue671.nc`). **Fixed 2026-09-25:** huge and tiny heap objects, files (`issue671.nc`). **Fixed 2026-09-25:** huge and tiny heap objects,