From 8b026e52f64369c953d528357f3ce14ba8d57b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 20:56:42 +0200 Subject: [PATCH 1/5] Share the link iteration types and callback for attributes H5Aiterate2 has the same shape as H5Literate: an index type, an iteration order, an in/out position and a callback that continues, stops or fails. IndexType, IterationOrder and the cursor move to hl::iteration together with the callback driver, so attribute iteration can reuse them instead of a second copy of the unsafe code. LinkCursor is renamed IterationCursor since it no longer counts only links. --- CHANGELOG.md | 2 +- hdf5/examples/link_order.rs | 4 +- hdf5/src/hl.rs | 4 +- hdf5/src/hl/group.rs | 211 ++++-------------------------------- hdf5/src/hl/iteration.rs | 203 ++++++++++++++++++++++++++++++++++ hdf5/src/lib.rs | 2 +- 6 files changed, 234 insertions(+), 192 deletions(-) create mode 100644 hdf5/src/hl/iteration.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 05870896..b76cd304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Added more variants to `LibraryVersion`. If you specified `Latest` before you may start generating files which are no longer compatible with earlier versions of `hdf5` - Exported `IterationOrder` and `IndexType`, the arguments of `Group::iter_visit` - Changed `Group::iter_visit` and `Group::iter_visit_default` to take an `FnMut(&str, LinkInfo) -> Result<()>` closure instead of an accumulator and a `bool` closure, and `LinkInfo::is_utf8` to `LinkInfo::char_encoding` (breaking change). An error returned by the closure is propagated, and `Group::groups`, `Group::datasets`, `Group::committed_datatypes` and `Group::member_names` now fail instead of returning a truncated list when a link cannot be resolved -- Added `Group::member_names_by` and `Group::links` to list the links along an index type in an order, `Group::find_link` to stop a link iteration with a value, and `Group::iter_visit_from` with `LinkCursor` to skip links and resume a stopped iteration +- Added `Group::member_names_by` and `Group::links` to list the links along an index type in an order, `Group::find_link` to stop a link iteration with a value, and `Group::iter_visit_from` with `IterationCursor` to skip links and resume a stopped iteration - Added `FileCreateBuilder::link_creation_order`, `GroupCreateBuilder::link_creation_order`, the matching getters and `LinkCreationOrder`, so a group can track and index link creation order - Changed `AttrCreationOrder` from bitflags to an enum with `Untracked`, `Tracked` and `Indexed`, matching `LinkCreationOrder` (breaking change) - Added `GroupCreateBuilder::attr_creation_order` and `GroupCreateBuilder::attr_phase_change` with the matching `GroupCreate` getters diff --git a/hdf5/examples/link_order.rs b/hdf5/examples/link_order.rs index bcc268a1..5735043b 100644 --- a/hdf5/examples/link_order.rs +++ b/hdf5/examples/link_order.rs @@ -6,7 +6,7 @@ //! be requested when the group is created and cannot be turned on later. use hdf5::plist::group_create::LinkCreationOrder; -use hdf5::{File, IndexType, IterationOrder, LinkCursor, LinkType, MajorErrorCode, Result}; +use hdf5::{File, IndexType, IterationCursor, IterationOrder, LinkType, MajorErrorCode, Result}; use hdf5_metno as hdf5; const FILE_NAME: &str = "link_order.h5"; @@ -59,7 +59,7 @@ fn read() -> Result<()> { assert_eq!(first_soft, Some("temp".to_owned())); // A cursor resumes a stopped iteration, here to read the links in pages. - let mut cursor = LinkCursor::start(IndexType::CreationOrder, IterationOrder::Increasing); + let mut cursor = IterationCursor::start(IndexType::CreationOrder, IterationOrder::Increasing); let mut page = vec![]; while let Some(((), next)) = tracked.iter_visit_from(cursor, |name, _| { page.push(name.to_owned()); diff --git a/hdf5/src/hl.rs b/hdf5/src/hl.rs index c2e30817..7c476df6 100644 --- a/hdf5/src/hl.rs +++ b/hdf5/src/hl.rs @@ -9,6 +9,7 @@ pub mod extents; pub mod file; pub mod filters; pub mod group; +pub mod iteration; pub mod location; pub mod object; pub mod plist; @@ -29,7 +30,8 @@ pub use self::{ dataspace::Dataspace, datatype::{Conversion, Datatype}, file::{File, FileBuilder, OpenMode}, - group::{Group, GroupBuilder, IndexType, IterationOrder, LinkCursor, LinkInfo, LinkType}, + group::{Group, GroupBuilder, LinkInfo, LinkType}, + iteration::{IndexType, IterationCursor, IterationOrder}, location::{Location, LocationInfo, LocationToken, LocationType}, object::Object, plist::PropertyList, diff --git a/hdf5/src/hl/group.rs b/hdf5/src/hl/group.rs index 0f48707f..a4e842fa 100644 --- a/hdf5/src/hl/group.rs +++ b/hdf5/src/hl/group.rs @@ -1,11 +1,7 @@ -use std::any::Any; use std::fmt::{self, Debug}; use std::ops::Deref; -use std::panic::{self, AssertUnwindSafe}; -use std::ptr::addr_of_mut; use hdf5_sys::{ - h5::{H5_index_t, H5_iter_order_t, hsize_t}, h5d::H5Dopen2, h5g::{H5G_info_t, H5Gcreate_anon, H5Gcreate2, H5Gget_create_plist, H5Gget_info, H5Gopen2}, h5l::{ @@ -18,6 +14,7 @@ use hdf5_sys::{ use crate::globals::H5P_LINK_CREATE; use crate::hl::dataset::Maybe; +use crate::hl::iteration::visit; use crate::hl::plist::group_create::{GroupCreate, GroupCreateBuilder}; use crate::hl::plist::link_create::{CharEncoding, LinkCreate, LinkCreateBuilder}; use crate::internal_prelude::*; @@ -420,104 +417,6 @@ impl GroupBuilder { } } -/// The index the links of a group are traversed along. -/// -/// Corresponds to `H5_index_t`. Traversing by [`CreationOrder`](Self::CreationOrder) -/// requires the group to track link creation order, see -/// [`LinkCreationOrder`](crate::plist::group_create::LinkCreationOrder). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum IndexType { - /// Index on link names. - Name, - /// Index on link creation order. - CreationOrder, -} - -impl Default for IndexType { - fn default() -> Self { - Self::Name - } -} - -impl From for H5_index_t { - fn from(v: IndexType) -> Self { - match v { - IndexType::Name => Self::H5_INDEX_NAME, - IndexType::CreationOrder => Self::H5_INDEX_CRT_ORDER, - } - } -} - -/// The order the links of a group are visited in along an [`IndexType`]. -/// -/// Corresponds to `H5_iter_order_t`. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum IterationOrder { - /// Increasing order. - Increasing, - /// Decreasing order. - Decreasing, - /// No particular order, whatever is fastest. - Native, -} - -impl Default for IterationOrder { - fn default() -> Self { - Self::Native - } -} - -impl From for H5_iter_order_t { - fn from(v: IterationOrder) -> Self { - match v { - IterationOrder::Increasing => Self::H5_ITER_INC, - IterationOrder::Decreasing => Self::H5_ITER_DEC, - IterationOrder::Native => Self::H5_ITER_NATIVE, - } - } -} - -/// A position in a link iteration. -/// -/// The cursor pairs the position with the [`IndexType`] and [`IterationOrder`] it -/// counts along, so an iteration can only be resumed the way it was started. -/// [`Group::iter_visit_from`] returns the cursor of a stopped iteration and accepts -/// it back to continue. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct LinkCursor { - index_type: IndexType, - iteration_order: IterationOrder, - position: u64, -} - -impl LinkCursor { - /// Creates a cursor at the first link along `index_type` in `iteration_order`. - pub const fn start(index_type: IndexType, iteration_order: IterationOrder) -> Self { - Self { index_type, iteration_order, position: 0 } - } - - /// Moves the cursor past the next `links` links. - #[must_use] - pub const fn skip(self, links: u64) -> Self { - Self { position: self.position + links, ..self } - } - - /// Returns the index type the cursor counts along. - pub const fn index_type(self) -> IndexType { - self.index_type - } - - /// Returns the iteration order the cursor counts along. - pub const fn iteration_order(self) -> IterationOrder { - self.iteration_order - } - - /// Returns the number of links before the cursor. - pub const fn position(self) -> u64 { - self.position - } -} - /// The type of an object link. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinkType { @@ -599,7 +498,7 @@ impl Group { where F: FnMut(&str, LinkInfo) -> Result<()>, { - self.iter_visit_from(LinkCursor::start(index_type, iteration_order), |name, info| { + self.iter_visit_from(IterationCursor::start(index_type, iteration_order), |name, info| { op(name, info)?; Ok(None::<()>) })?; @@ -648,7 +547,7 @@ impl Group { where F: FnMut(&str, LinkInfo) -> Result>, { - match self.iter_visit_from(LinkCursor::start(index_type, iteration_order), op)? { + match self.iter_visit_from(IterationCursor::start(index_type, iteration_order), op)? { Some((value, _)) => Ok(Some(value)), None => Ok(None), } @@ -668,14 +567,14 @@ impl Group { /// # Examples /// /// ``` - /// use hdf5_metno::{File, IndexType, IterationOrder, LinkCursor}; + /// use hdf5_metno::{File, IndexType, IterationOrder, IterationCursor}; /// /// let file = File::with_options().with_fapl(|p| p.core_filebacked(false)).create("iter_visit_from.h5")?; /// for name in ["a", "b", "c"] { /// file.create_group(name)?; /// } /// - /// let mut cursor = LinkCursor::start(IndexType::Name, IterationOrder::Increasing).skip(1); + /// let mut cursor = IterationCursor::start(IndexType::Name, IterationOrder::Increasing).skip(1); /// let mut names = vec![]; /// while let Some((name, next)) = /// file.iter_visit_from(cursor, |name, _| Ok(Some(name.to_owned())))? @@ -688,88 +587,26 @@ impl Group { /// # Ok::<(), hdf5_metno::Error>(()) /// ``` pub fn iter_visit_from( - &self, cursor: LinkCursor, op: F, - ) -> Result> + &self, cursor: IterationCursor, op: F, + ) -> Result> where F: FnMut(&str, LinkInfo) -> Result>, { - enum Stop { - Found(B), - Error(Error), - Panic(Box), - } - - struct OpData { - op: F, - stop: Option>, - } - - // Called by H5Literate once per link, never concurrently - unsafe extern "C" fn callback( - _id: hid_t, name: *const c_char, info: *const H5L_info_t, op_data: *mut c_void, - ) -> herr_t - where - F: FnMut(&str, LinkInfo) -> Result>, - { - // SAFETY: op_data is the pointer to the OpData passed to H5Literate below, which - // outlives the H5Literate call, and H5Literate does not run the callback concurrently - let Some(data) = (unsafe { op_data.cast::>().as_mut() }) else { - return -1; - }; - let visited = panic::catch_unwind(AssertUnwindSafe(|| { - assert!(!name.is_null(), "iter_visit: null name ptr"); - // SAFETY: HDF5 passes a nul-terminated link name that is valid for the duration - // of the callback - let name = unsafe { std::ffi::CStr::from_ptr(name) }; - // SAFETY: HDF5 passes a pointer to the link info that is valid for the duration - // of the callback - let info = unsafe { info.as_ref() }.expect("iter_visit: null info ptr"); - (data.op)(name.to_string_lossy().as_ref(), info.into()) - })); - match visited { - Ok(Ok(None)) => 0, - Ok(Ok(Some(value))) => { - data.stop = Some(Stop::Found(value)); - 1 - } - Ok(Err(err)) => { - data.stop = Some(Stop::Error(err)); - -1 - } - Err(payload) => { - data.stop = Some(Stop::Panic(payload)); - -1 - } - } - } - - // H5Literate rejects a start position at or past the last link - if cursor.position > 0 && cursor.position >= group_info(self.id())?.nlinks { - return Ok(None); - } - - let mut data = OpData { op, stop: None }; - let mut position: hsize_t = cursor.position; - let ret = h5call!(H5Literate( - self.id(), - cursor.index_type.into(), - cursor.iteration_order.into(), - &mut position, - Some(callback::), - addr_of_mut!(data).cast::() - )); - match data.stop { - Some(Stop::Panic(payload)) => panic::resume_unwind(payload), - Some(Stop::Error(err)) => Err(err), - Some(Stop::Found(value)) => { - ret?; - Ok(Some((value, LinkCursor { position, ..cursor }))) - } - None => { - ret?; - Ok(None) - } - } + visit( + cursor, + || Ok(group_info(self.id())?.nlinks), + op, + |index_type, iteration_order, position, callback, op_data| unsafe { + H5Literate( + self.id(), + index_type, + iteration_order, + position, + Some(callback), + op_data, + ) + }, + ) } fn get_all_of_type(&self, loc_type: LocationType) -> Result> { @@ -870,7 +707,7 @@ pub mod tests { use crate::hl::plist::file_access::LibraryVersion; use crate::hl::plist::link_create::CharEncoding; use crate::internal_prelude::*; - use crate::{IndexType, IterationOrder, LinkCursor, LinkType}; + use crate::{IndexType, IterationCursor, IterationOrder, LinkType}; use hdf5_types::{IntSize, TypeDescriptor, VarLenUnicode}; use std::panic::{self, AssertUnwindSafe}; @@ -1505,7 +1342,7 @@ pub mod tests { for name in ["a", "b", "c"] { file.create_group(name).unwrap(); } - let start = LinkCursor::start(IndexType::Name, IterationOrder::Increasing); + let start = IterationCursor::start(IndexType::Name, IterationOrder::Increasing); let stop_at = |cursor, wanted: &str| { let mut visited = vec![]; diff --git a/hdf5/src/hl/iteration.rs b/hdf5/src/hl/iteration.rs new file mode 100644 index 00000000..49b4ceec --- /dev/null +++ b/hdf5/src/hl/iteration.rs @@ -0,0 +1,203 @@ +//! Traversal of the links of a group and the attributes of an object. + +use std::any::Any; +use std::panic::{self, AssertUnwindSafe}; +use std::ptr::addr_of_mut; + +use hdf5_sys::h5::{H5_index_t, H5_iter_order_t, hsize_t}; + +use crate::internal_prelude::*; + +/// The index the links of a group or the attributes of an object are traversed along. +/// +/// Corresponds to `H5_index_t`. Traversing by [`CreationOrder`](Self::CreationOrder) +/// requires creation order to be tracked, see +/// [`LinkCreationOrder`](crate::plist::group_create::LinkCreationOrder) and +/// [`AttrCreationOrder`](crate::plist::group_create::AttrCreationOrder). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IndexType { + /// Index on link names. + Name, + /// Index on link creation order. + CreationOrder, +} + +impl Default for IndexType { + fn default() -> Self { + Self::Name + } +} + +impl From for H5_index_t { + fn from(v: IndexType) -> Self { + match v { + IndexType::Name => Self::H5_INDEX_NAME, + IndexType::CreationOrder => Self::H5_INDEX_CRT_ORDER, + } + } +} + +/// The order links or attributes are visited in along an [`IndexType`]. +/// +/// Corresponds to `H5_iter_order_t`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IterationOrder { + /// Increasing order. + Increasing, + /// Decreasing order. + Decreasing, + /// No particular order, whatever is fastest. + Native, +} + +impl Default for IterationOrder { + fn default() -> Self { + Self::Native + } +} + +impl From for H5_iter_order_t { + fn from(v: IterationOrder) -> Self { + match v { + IterationOrder::Increasing => Self::H5_ITER_INC, + IterationOrder::Decreasing => Self::H5_ITER_DEC, + IterationOrder::Native => Self::H5_ITER_NATIVE, + } + } +} + +/// A position in a link or attribute iteration. +/// +/// The cursor pairs the position with the [`IndexType`] and [`IterationOrder`] it +/// counts along, so an iteration can only be resumed the way it was started. +/// [`Group::iter_visit_from`](crate::Group::iter_visit_from) returns the cursor of a +/// stopped iteration and accepts it back to continue. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct IterationCursor { + index_type: IndexType, + iteration_order: IterationOrder, + position: u64, +} + +impl IterationCursor { + /// Creates a cursor at the first link along `index_type` in `iteration_order`. + pub const fn start(index_type: IndexType, iteration_order: IterationOrder) -> Self { + Self { index_type, iteration_order, position: 0 } + } + + /// Moves the cursor past the next `items` links or attributes. + #[must_use] + pub const fn skip(self, items: u64) -> Self { + Self { position: self.position + items, ..self } + } + + /// Returns the index type the cursor counts along. + pub const fn index_type(self) -> IndexType { + self.index_type + } + + /// Returns the iteration order the cursor counts along. + pub const fn iteration_order(self) -> IterationOrder { + self.iteration_order + } + + /// Returns the number of links or attributes before the cursor. + pub const fn position(self) -> u64 { + self.position + } +} + +pub(crate) type Callback = + unsafe extern "C" fn(hid_t, *const c_char, *const I, *mut c_void) -> herr_t; + +/// Drives an HDF5 iteration from `cursor` until `op` returns a value. +/// +/// `iterate` makes the C call with the index type, the order, the position, the +/// callback and its data. `count` is consulted only for a cursor past the start, +/// since HDF5 rejects a start position at or past the last item. +pub(crate) fn visit( + cursor: IterationCursor, count: N, op: F, iterate: C, +) -> Result> +where + T: for<'a> From<&'a I>, + F: FnMut(&str, T) -> Result>, + N: FnOnce() -> Result, + C: FnOnce(H5_index_t, H5_iter_order_t, *mut hsize_t, Callback, *mut c_void) -> herr_t, +{ + enum Stop { + Found(B), + Error(Error), + Panic(Box), + } + + struct OpData { + op: F, + stop: Option>, + } + + // Called by HDF5 once per item, never concurrently + unsafe extern "C" fn callback( + _id: hid_t, name: *const c_char, info: *const I, op_data: *mut c_void, + ) -> herr_t + where + T: for<'a> From<&'a I>, + F: FnMut(&str, T) -> Result>, + { + // SAFETY: op_data is the pointer to the OpData passed to the C call below, which + // outlives that call, and HDF5 does not run the callback concurrently + let Some(data) = (unsafe { op_data.cast::>().as_mut() }) else { + return -1; + }; + let visited = panic::catch_unwind(AssertUnwindSafe(|| { + assert!(!name.is_null(), "iteration: null name ptr"); + // SAFETY: HDF5 passes a nul-terminated name that is valid for the duration of + // the callback + let name = unsafe { std::ffi::CStr::from_ptr(name) }; + // SAFETY: HDF5 passes a pointer to the item info that is valid for the duration + // of the callback + let info = unsafe { info.as_ref() }.expect("iteration: null info ptr"); + (data.op)(name.to_string_lossy().as_ref(), T::from(info)) + })); + match visited { + Ok(Ok(None)) => 0, + Ok(Ok(Some(value))) => { + data.stop = Some(Stop::Found(value)); + 1 + } + Ok(Err(err)) => { + data.stop = Some(Stop::Error(err)); + -1 + } + Err(payload) => { + data.stop = Some(Stop::Panic(payload)); + -1 + } + } + } + + if cursor.position > 0 && cursor.position >= count()? { + return Ok(None); + } + + let mut data = OpData { op, stop: None }; + let mut position: hsize_t = cursor.position; + let ret = h5call!(iterate( + cursor.index_type.into(), + cursor.iteration_order.into(), + &mut position, + callback::, + addr_of_mut!(data).cast::() + )); + match data.stop { + Some(Stop::Panic(payload)) => panic::resume_unwind(payload), + Some(Stop::Error(err)) => Err(err), + Some(Stop::Found(value)) => { + ret?; + Ok(Some((value, IterationCursor { position, ..cursor }))) + } + None => { + ret?; + Ok(None) + } + } +} diff --git a/hdf5/src/lib.rs b/hdf5/src/lib.rs index 8be9d785..18cd36f3 100644 --- a/hdf5/src/lib.rs +++ b/hdf5/src/lib.rs @@ -64,7 +64,7 @@ mod export { AttributeBuilderEmptyShape, ByteReader, CommittedDatatype, Container, Conversion, Dataset, DatasetBuilder, DatasetBuilderData, DatasetBuilderEmpty, DatasetBuilderEmptyShape, DatasetType, Dataspace, Datatype, File, FileBuilder, Group, - GroupBuilder, IndexType, IterationOrder, LinkCursor, LinkInfo, LinkType, Location, + GroupBuilder, IndexType, IterationCursor, IterationOrder, LinkInfo, LinkType, Location, LocationInfo, LocationToken, LocationType, Object, OpenMode, PropertyList, Reader, Writer, references::{ObjectReference, ObjectReference1, ReferencedObject}, From ecc14bc339eb2ea31fab162e6e932a45f672c508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 20:59:04 +0200 Subject: [PATCH 2/5] Add attribute iteration by index type and order Location::iter_attrs, find_attr and iter_attrs_from mirror the link iteration on Group, over H5Aiterate2 with the shared callback driver. The callback receives an AttrInfo built from H5A_info_t. attr_names is now the name order case of iter_attrs. --- CHANGELOG.md | 1 + hdf5/src/hl.rs | 2 +- hdf5/src/hl/attribute.rs | 58 +++---- hdf5/src/hl/iteration.rs | 7 +- hdf5/src/hl/location.rs | 362 ++++++++++++++++++++++++++++++++++++++- hdf5/src/lib.rs | 2 +- 6 files changed, 383 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b76cd304..81eeafc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Added `FileCreateBuilder::link_creation_order`, `GroupCreateBuilder::link_creation_order`, the matching getters and `LinkCreationOrder`, so a group can track and index link creation order - Changed `AttrCreationOrder` from bitflags to an enum with `Untracked`, `Tracked` and `Indexed`, matching `LinkCreationOrder` (breaking change) - Added `GroupCreateBuilder::attr_creation_order` and `GroupCreateBuilder::attr_phase_change` with the matching `GroupCreate` getters +- Added `Location::iter_attrs`, `Location::find_attr` and `Location::iter_attrs_from` with `AttrInfo`, and `Location::attr_names_by` and `Location::attrs`, to iterate attributes by name or creation order in either direction, sharing `IndexType`, `IterationOrder` and `IterationCursor` with link iteration ## hdf5-derive unreleased ## hdf5-types unreleased ## hdf5-sys unreleased diff --git a/hdf5/src/hl.rs b/hdf5/src/hl.rs index 7c476df6..7711ad44 100644 --- a/hdf5/src/hl.rs +++ b/hdf5/src/hl.rs @@ -18,7 +18,7 @@ pub mod selection; pub use self::{ attribute::{ - Attribute, AttributeBuilder, AttributeBuilderData, AttributeBuilderEmpty, + AttrInfo, Attribute, AttributeBuilder, AttributeBuilderData, AttributeBuilderEmpty, AttributeBuilderEmptyShape, }, committed_datatype::CommittedDatatype, diff --git a/hdf5/src/hl/attribute.rs b/hdf5/src/hl/attribute.rs index 33acb73b..18207152 100644 --- a/hdf5/src/hl/attribute.rs +++ b/hdf5/src/hl/attribute.rs @@ -1,18 +1,32 @@ use std::fmt::{self, Debug}; use std::ops::Deref; -use std::ptr::addr_of_mut; -use hdf5_sys::h5a::{H5Aget_create_plist, H5Aget_name}; -use hdf5_sys::{ - h5::{H5_index_t, H5_iter_order_t}, - h5a::{H5A_info_t, H5A_operator2_t, H5Acreate2, H5Adelete, H5Aiterate2}, -}; +use hdf5_sys::h5a::{H5A_info_t, H5Acreate2, H5Adelete, H5Aget_create_plist, H5Aget_name}; use hdf5_types::TypeDescriptor; use ndarray::ArrayView; use crate::hl::plist::attribute_create::{AttributeCreate, AttributeCreateBuilder, CharEncoding}; use crate::internal_prelude::*; +/// Information about an attribute of an object. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AttrInfo { + /// Position in the creation order of the object's attributes, if known. + pub creation_order: Option, + /// Encoding of the attribute name. HDF5 values other than UTF-8 are reported as ASCII. + pub char_encoding: CharEncoding, + /// Size of the attribute data in bytes. + pub data_size: u64, +} + +impl From<&H5A_info_t> for AttrInfo { + fn from(info: &H5A_info_t) -> Self { + let creation_order = if info.corder_valid == 1 { Some(info.corder) } else { None }; + let char_encoding = CharEncoding::try_from(info.cset).unwrap_or(CharEncoding::Ascii); + Self { creation_order, char_encoding, data_size: info.data_size } + } +} + /// Represents the HDF5 attribute object. #[repr(transparent)] #[derive(Clone)] @@ -65,38 +79,6 @@ impl Attribute { // attribute is attached to, not the attribute's own name. h5lock!(get_h5_str(|m, s| H5Aget_name(self.id(), s, m)).unwrap_or_else(|_| String::new())) } - - /// Returns names of all the members in the group, non-recursively. - pub fn attr_names(obj: &Location) -> Result> { - unsafe extern "C" fn attributes_callback( - _id: hid_t, attr_name: *const c_char, _info: *const H5A_info_t, op_data: *mut c_void, - ) -> herr_t { - std::panic::catch_unwind(|| { - let other_data: &mut Vec = - unsafe { &mut *(op_data.cast::>()) }; - // SAFETY: caller guarantees attr_name points to valid UTF-8 C string - other_data.push(unsafe { string_from_cstr(attr_name) }); - 0 // Continue iteration - }) - .unwrap_or(-1) - } - - let callback_fn: H5A_operator2_t = Some(attributes_callback); - let iteration_position: *mut hsize_t = &mut { 0_u64 }; - let mut result: Vec = Vec::new(); - let other_data: *mut c_void = addr_of_mut!(result).cast(); - - h5call!(H5Aiterate2( - obj.handle().id(), - H5_index_t::H5_INDEX_NAME, - H5_iter_order_t::H5_ITER_INC, - iteration_position, - callback_fn, - other_data - ))?; - - Ok(result) - } } #[derive(Clone)] diff --git a/hdf5/src/hl/iteration.rs b/hdf5/src/hl/iteration.rs index 49b4ceec..57df3cf9 100644 --- a/hdf5/src/hl/iteration.rs +++ b/hdf5/src/hl/iteration.rs @@ -10,9 +10,10 @@ use crate::internal_prelude::*; /// The index the links of a group or the attributes of an object are traversed along. /// -/// Corresponds to `H5_index_t`. Traversing by [`CreationOrder`](Self::CreationOrder) -/// requires creation order to be tracked, see -/// [`LinkCreationOrder`](crate::plist::group_create::LinkCreationOrder) and +/// Corresponds to `H5_index_t`. Links can only be traversed by +/// [`CreationOrder`](Self::CreationOrder) in a group that tracks it, see +/// [`LinkCreationOrder`](crate::plist::group_create::LinkCreationOrder). Attribute +/// creation order is tracked with /// [`AttrCreationOrder`](crate::plist::group_create::AttrCreationOrder). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum IndexType { diff --git a/hdf5/src/hl/location.rs b/hdf5/src/hl/location.rs index fcc54b8e..6841640e 100644 --- a/hdf5/src/hl/location.rs +++ b/hdf5/src/hl/location.rs @@ -19,7 +19,7 @@ use hdf5_sys::h5o::{H5Oget_info_by_name2, H5Oget_info2}; #[cfg(not(feature = "1.12.0"))] use hdf5_sys::{h5::haddr_t, h5o::H5O_info1_t, h5o::H5Oopen_by_addr}; use hdf5_sys::{ - h5a::{H5Adelete, H5Aopen}, + h5a::{H5Adelete, H5Aiterate2, H5Aopen}, h5f::H5Fget_name, h5i::{H5Iget_file_id, H5Iget_name}, h5o::{H5O_type_t, H5Oget_comment}, @@ -27,7 +27,8 @@ use hdf5_sys::{ use crate::internal_prelude::*; -use super::attribute::AttributeBuilderEmpty; +use super::attribute::{AttrInfo, AttributeBuilderEmpty}; +use super::iteration::visit; /// Named location (file, group, dataset, named datatype). #[repr(transparent)] @@ -125,13 +126,166 @@ impl Location { Attribute::from_id(h5try!(H5Aopen(self.id(), name.as_ptr(), H5P_DEFAULT))) } - /// Return the names of all attributes on the object. + /// Returns the names of all attributes of the object, in increasing name order. + pub fn attr_names(&self) -> Result> { + self.attr_names_by(IndexType::Name, IterationOrder::Increasing) + } + + /// Returns the names of all attributes of the object along `index_type` in + /// `iteration_order`. + pub fn attr_names_by( + &self, index_type: IndexType, iteration_order: IterationOrder, + ) -> Result> { + let mut names = vec![]; + self.iter_attrs(index_type, iteration_order, |name, _| { + names.push(name.to_owned()); + Ok(()) + })?; + Ok(names) + } + + /// Returns the name and [`AttrInfo`] of all attributes of the object along `index_type` + /// in `iteration_order`. + pub fn attrs( + &self, index_type: IndexType, iteration_order: IterationOrder, + ) -> Result> { + let mut attrs = vec![]; + self.iter_attrs(index_type, iteration_order, |name, info| { + attrs.push((name.to_owned(), info)); + Ok(()) + })?; + Ok(attrs) + } + + /// Visits every attribute of the object. + /// + /// The attributes are traversed along `index_type` in `iteration_order`, and `op` + /// is called with the name and the [`AttrInfo`] of each attribute. Use + /// [`find_attr`](Self::find_attr) to stop early. + /// + /// An object that does not track attribute creation order is still traversed by + /// [`IndexType::CreationOrder`]. Attributes stored compactly in the object header + /// come in storage sequence with a position each. Attributes stored densely come + /// from the name index in native order with no positions. Track the order with + /// [`AttrCreationOrder`](crate::plist::group_create::AttrCreationOrder) for a + /// reliable result. /// /// # Errors /// - /// Returns an error if an underlying library call fails. - pub fn attr_names(&self) -> Result> { - Attribute::attr_names(self) + /// Returns the first error returned by `op`, or the HDF5 error if the iteration + /// itself fails. + /// + /// # Panics + /// + /// A panic in `op` is caught while HDF5 frames are on the stack and resumed once + /// the iteration has returned. + /// + /// # Examples + /// + /// ``` + /// use hdf5_metno::plist::group_create::AttrCreationOrder; + /// use hdf5_metno::{File, IndexType, IterationOrder}; + /// + /// let file = File::with_options().with_fapl(|p| p.core_filebacked(false)).create("iter_attrs.h5")?; + /// let group = file + /// .create_group_builder() + /// .with_gcpl(|gcpl| gcpl.attr_creation_order(AttrCreationOrder::Tracked)) + /// .create("g")?; + /// group.new_attr::().create("b")?; + /// group.new_attr::().create("a")?; + /// + /// let mut names = vec![]; + /// group.iter_attrs(IndexType::CreationOrder, IterationOrder::Increasing, |name, _| { + /// names.push(name.to_owned()); + /// Ok(()) + /// })?; + /// assert_eq!(names, ["b", "a"]); + /// assert_eq!(group.attr_names()?, ["a", "b"]); + /// # Ok::<(), hdf5_metno::Error>(()) + /// ``` + pub fn iter_attrs( + &self, index_type: IndexType, iteration_order: IterationOrder, mut op: F, + ) -> Result<()> + where + F: FnMut(&str, AttrInfo) -> Result<()>, + { + self.iter_attrs_from(IterationCursor::start(index_type, iteration_order), |name, info| { + op(name, info)?; + Ok(None::<()>) + })?; + Ok(()) + } + + /// Visits the attributes of the object until `op` returns a value. + /// + /// The attributes are traversed along `index_type` in `iteration_order`, and `op` + /// is called with the name and the [`AttrInfo`] of each attribute until it returns + /// `Some`. That value is returned, or `None` once every attribute was visited. + /// + /// # Errors + /// + /// As for [`iter_attrs`](Self::iter_attrs). + /// + /// # Examples + /// + /// ``` + /// use hdf5_metno::{File, IndexType, IterationOrder}; + /// + /// let file = File::with_options().with_fapl(|p| p.core_filebacked(false)).create("find_attr.h5")?; + /// file.new_attr::().create("small")?; + /// file.new_attr::().create("large")?; + /// + /// let wide = file.find_attr(IndexType::Name, IterationOrder::Increasing, |name, info| { + /// if info.data_size > 4 { Ok(Some(name.to_owned())) } else { Ok(None) } + /// })?; + /// assert_eq!(wide, Some("large".to_owned())); + /// # Ok::<(), hdf5_metno::Error>(()) + /// ``` + pub fn find_attr( + &self, index_type: IndexType, iteration_order: IterationOrder, op: F, + ) -> Result> + where + F: FnMut(&str, AttrInfo) -> Result>, + { + match self.iter_attrs_from(IterationCursor::start(index_type, iteration_order), op)? { + Some((value, _)) => Ok(Some(value)), + None => Ok(None), + } + } + + /// Visits the attributes of the object from `cursor` onwards until `op` returns a + /// value. + /// + /// Behaves like [`find_attr`](Self::find_attr). The value is returned together + /// with the cursor of the next attribute, so the iteration can be resumed by + /// passing that cursor back. Returns `None` once every attribute was visited, + /// including when `cursor` is already at or past the last attribute. See + /// [`Group::iter_visit_from`](crate::Group::iter_visit_from) for a paging loop. + /// + /// # Errors + /// + /// As for [`iter_attrs`](Self::iter_attrs). + pub fn iter_attrs_from( + &self, cursor: IterationCursor, op: F, + ) -> Result> + where + F: FnMut(&str, AttrInfo) -> Result>, + { + visit( + cursor, + || Ok(self.loc_info()?.num_attrs as u64), + op, + |index_type, iteration_order, position, callback, op_data| unsafe { + H5Aiterate2( + self.id(), + index_type, + iteration_order, + position, + Some(callback), + op_data, + ) + }, + ) } pub fn delete_attr(&self, name: &str) -> Result<()> { @@ -383,6 +537,10 @@ fn H5O_open_by_token(loc_id: hid_t, token: LocationToken) -> Result { #[cfg(test)] pub mod tests { + use crate::hl::plist::common::AttrCreationOrder; + #[cfg(feature = "1.10.2")] + use crate::hl::plist::file_access::LibraryVersion; + use crate::hl::plist::link_create::CharEncoding; use crate::{hl::plist::object_copy::ObjectCopy, internal_prelude::*, plist::LinkCreate}; #[test] @@ -639,4 +797,196 @@ pub mod tests { }) }) } + + #[test] + pub fn test_iter_attrs_order() { + with_tmp_file(|file| { + let obj = file.create_group("o").unwrap(); + for name in ["foo", "123", "bar"] { + obj.new_attr::().create(name).unwrap(); + } + let names = |order| obj.attr_names_by(IndexType::Name, order).unwrap(); + assert_eq!(names(IterationOrder::Increasing), ["123", "bar", "foo"]); + assert_eq!(names(IterationOrder::Decreasing), ["foo", "bar", "123"]); + assert_eq!(obj.attr_names().unwrap(), ["123", "bar", "foo"]); + + let empty = file.create_group("empty").unwrap(); + assert!( + empty.attr_names_by(IndexType::Name, IterationOrder::Native).unwrap().is_empty() + ); + }) + } + + #[test] + pub fn test_iter_attrs_creation_order() { + with_tmp_file(|file| { + let obj = file + .create_group_builder() + .with_gcpl(|gcpl| gcpl.attr_creation_order(AttrCreationOrder::Tracked)) + .create("o") + .unwrap(); + obj.new_attr::().create("foo").unwrap(); + obj.new_attr::().char_encoding(CharEncoding::Ascii).create("123").unwrap(); + obj.new_attr::().create("bar").unwrap(); + + let attr = |name: &str, order, char_encoding, data_size| { + ( + name.to_owned(), + AttrInfo { creation_order: Some(order), char_encoding, data_size }, + ) + }; + let foo = attr("foo", 0, CharEncoding::Utf8, 4); + let num = attr("123", 1, CharEncoding::Ascii, 8); + let bar = attr("bar", 2, CharEncoding::Utf8, 4); + let attrs = |order| obj.attrs(IndexType::CreationOrder, order).unwrap(); + assert_eq!(attrs(IterationOrder::Increasing), [foo.clone(), num.clone(), bar.clone()]); + assert_eq!(attrs(IterationOrder::Decreasing), [bar, num, foo]); + }) + } + + #[test] + pub fn test_find_attr() { + with_tmp_file(|file| { + for name in ["a", "b", "c"] { + file.new_attr::().create(name).unwrap(); + } + + let find = |wanted: &str| { + let mut visited = vec![]; + let found = file + .find_attr(IndexType::Name, IterationOrder::Increasing, |name, info| { + visited.push(name.to_owned()); + if name == wanted { Ok(Some(info.data_size)) } else { Ok(None) } + }) + .unwrap(); + (found, visited) + }; + + let (found, visited) = find("b"); + assert_eq!(found, Some(4)); + assert_eq!(visited, ["a", "b"]); + + let (found, visited) = find("z"); + assert_eq!(found, None); + assert_eq!(visited, ["a", "b", "c"]); + }) + } + + #[test] + pub fn test_iter_attrs_from() { + with_tmp_file(|file| { + for name in ["a", "b", "c"] { + file.new_attr::().create(name).unwrap(); + } + let start = IterationCursor::start(IndexType::Name, IterationOrder::Increasing); + + let stop_at = |cursor, wanted: &str| { + let mut visited = vec![]; + let stopped = file + .iter_attrs_from(cursor, |name, _| { + visited.push(name.to_owned()); + if name == wanted { Ok(Some(())) } else { Ok(None) } + }) + .unwrap(); + (stopped, visited) + }; + + let (stopped, visited) = stop_at(start, "b"); + assert_eq!(visited, ["a", "b"]); + assert_eq!(stopped, Some(((), start.skip(2)))); + + let (stopped, visited) = stop_at(start.skip(2), "z"); + assert_eq!(visited, ["c"]); + assert_eq!(stopped, None); + + let (stopped, visited) = stop_at(start.skip(3), "a"); + assert!(visited.is_empty()); + assert_eq!(stopped, None); + }) + } + fn attrs_by_creation_order( + file: &File, name: &str, creation_order: AttrCreationOrder, dense: bool, + ) -> Vec<(String, Option)> { + let obj = file + .create_group_builder() + .with_gcpl(|gcpl| { + gcpl.attr_creation_order(creation_order); + if dense { + gcpl.attr_phase_change(0, 0); + } + gcpl + }) + .create(name) + .unwrap(); + for name in ["c", "a", "b"] { + obj.new_attr::().create(name).unwrap(); + } + let attrs = obj.attrs(IndexType::CreationOrder, IterationOrder::Increasing).unwrap(); + attrs.into_iter().map(|(name, info)| (name, info.creation_order)).collect() + } + + fn assert_storage_sequence(attrs: &[(String, Option)]) { + let expected = + [("c".to_owned(), Some(0)), ("a".to_owned(), Some(1)), ("b".to_owned(), Some(2))]; + assert_eq!(attrs, expected); + } + + fn assert_name_index_fallback(attrs: &[(String, Option)]) { + let mut names: Vec<&str> = attrs.iter().map(|(name, _)| name.as_str()).collect(); + names.sort_unstable(); + assert_eq!(names, ["a", "b", "c"]); + assert!(attrs.iter().all(|(_, order)| order.is_none()), "{attrs:?}"); + } + + // Only a tracked object has an attribute creation order index. Compact storage in + // the object header still yields the storage sequence with positions, dense storage + // in a heap falls back to the name index. Dense storage needs a version-2 object + // header, which the default file format only produces from libhdf5 2.0 on. + #[test] + pub fn test_iter_attrs_untracked_default_format() { + with_tmp_file(|file| { + let compact = + attrs_by_creation_order(&file, "compact", AttrCreationOrder::Untracked, false); + assert_storage_sequence(&compact); + + let dense = attrs_by_creation_order(&file, "dense", AttrCreationOrder::Untracked, true); + if cfg!(feature = "2.0.0") { + assert_name_index_fallback(&dense); + } else { + assert_storage_sequence(&dense); + } + }) + } + + #[cfg(feature = "1.10.2")] + #[test] + pub fn test_iter_attrs_v2_object_header() { + for low in [LibraryVersion::V18, LibraryVersion::latest()] { + with_tmp_path(|path| { + let file = File::with_options() + .with_fapl(|fapl| fapl.libver_bounds(low, LibraryVersion::latest())) + .create(&path) + .unwrap(); + let attrs = |name, creation_order, dense| { + attrs_by_creation_order(&file, name, creation_order, dense) + }; + assert_storage_sequence(&attrs( + "tracked_compact", + AttrCreationOrder::Tracked, + false, + )); + assert_storage_sequence(&attrs("tracked_dense", AttrCreationOrder::Tracked, true)); + assert_storage_sequence(&attrs( + "untracked_compact", + AttrCreationOrder::Untracked, + false, + )); + assert_name_index_fallback(&attrs( + "untracked_dense", + AttrCreationOrder::Untracked, + true, + )); + }) + } + } } diff --git a/hdf5/src/lib.rs b/hdf5/src/lib.rs index 18cd36f3..1d214d49 100644 --- a/hdf5/src/lib.rs +++ b/hdf5/src/lib.rs @@ -60,7 +60,7 @@ mod export { hl::extents::{Extent, Extents, SimpleExtents}, hl::selection::{Hyperslab, Selection, SliceOrIndex}, hl::{ - Attribute, AttributeBuilder, AttributeBuilderData, AttributeBuilderEmpty, + AttrInfo, Attribute, AttributeBuilder, AttributeBuilderData, AttributeBuilderEmpty, AttributeBuilderEmptyShape, ByteReader, CommittedDatatype, Container, Conversion, Dataset, DatasetBuilder, DatasetBuilderData, DatasetBuilderEmpty, DatasetBuilderEmptyShape, DatasetType, Dataspace, Datatype, File, FileBuilder, Group, From 3735aa28afdf169f83bc43c9c534d5ee84dcb950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 21:00:32 +0200 Subject: [PATCH 3/5] Add Location::attr_by_index and Location::attr_info attr_by_index opens an attribute by its position along an index type and order over H5Aopen_by_idx. attr_info returns the AttrInfo of a named attribute over H5Aget_info_by_name. --- CHANGELOG.md | 1 + hdf5/src/hl/location.rs | 107 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81eeafc4..a58cb669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Changed `AttrCreationOrder` from bitflags to an enum with `Untracked`, `Tracked` and `Indexed`, matching `LinkCreationOrder` (breaking change) - Added `GroupCreateBuilder::attr_creation_order` and `GroupCreateBuilder::attr_phase_change` with the matching `GroupCreate` getters - Added `Location::iter_attrs`, `Location::find_attr` and `Location::iter_attrs_from` with `AttrInfo`, and `Location::attr_names_by` and `Location::attrs`, to iterate attributes by name or creation order in either direction, sharing `IndexType`, `IterationOrder` and `IterationCursor` with link iteration +- Added `Location::attr_by_index` and `Location::attr_info` ## hdf5-derive unreleased ## hdf5-types unreleased ## hdf5-sys unreleased diff --git a/hdf5/src/hl/location.rs b/hdf5/src/hl/location.rs index 6841640e..a3a474f5 100644 --- a/hdf5/src/hl/location.rs +++ b/hdf5/src/hl/location.rs @@ -19,7 +19,7 @@ use hdf5_sys::h5o::{H5Oget_info_by_name2, H5Oget_info2}; #[cfg(not(feature = "1.12.0"))] use hdf5_sys::{h5::haddr_t, h5o::H5O_info1_t, h5o::H5Oopen_by_addr}; use hdf5_sys::{ - h5a::{H5Adelete, H5Aiterate2, H5Aopen}, + h5a::{H5A_info_t, H5Adelete, H5Aget_info_by_name, H5Aiterate2, H5Aopen, H5Aopen_by_idx}, h5f::H5Fget_name, h5i::{H5Iget_file_id, H5Iget_name}, h5o::{H5O_type_t, H5Oget_comment}, @@ -126,6 +126,59 @@ impl Location { Attribute::from_id(h5try!(H5Aopen(self.id(), name.as_ptr(), H5P_DEFAULT))) } + /// Opens the attribute at `index` along `index_type` in `iteration_order`. + /// + /// # Errors + /// + /// Fails if `index` is not below the number of attributes. + /// + /// # Examples + /// + /// ``` + /// use hdf5_metno::{File, IndexType, IterationOrder}; + /// + /// let file = File::with_options().with_fapl(|p| p.core_filebacked(false)).create("attr_by_index.h5")?; + /// file.new_attr::().create("b")?; + /// file.new_attr::().create("a")?; + /// + /// let last = file.attr_by_index(IndexType::Name, IterationOrder::Decreasing, 0)?; + /// assert_eq!(last.name(), "b"); + /// # Ok::<(), hdf5_metno::Error>(()) + /// ``` + pub fn attr_by_index( + &self, index_type: IndexType, iteration_order: IterationOrder, index: u64, + ) -> Result { + Attribute::from_id(h5try!(H5Aopen_by_idx( + self.id(), + b".\0".as_ptr().cast::(), + index_type.into(), + iteration_order.into(), + index, + H5P_DEFAULT, + H5P_DEFAULT + ))) + } + + /// Returns information about the attribute called `name`. + /// + /// # Errors + /// + /// Fails if the object has no attribute called `name`. + pub fn attr_info(&self, name: &str) -> Result { + let name = to_cstring(name)?; + let mut info = MaybeUninit::::uninit(); + h5call!(H5Aget_info_by_name( + self.id(), + b".\0".as_ptr().cast::(), + name.as_ptr(), + info.as_mut_ptr(), + H5P_DEFAULT + ))?; + // SAFETY: H5Aget_info_by_name fills the info on success, and the error was checked + let info = unsafe { info.assume_init() }; + Ok(AttrInfo::from(&info)) + } + /// Returns the names of all attributes of the object, in increasing name order. pub fn attr_names(&self) -> Result> { self.attr_names_by(IndexType::Name, IterationOrder::Increasing) @@ -904,6 +957,58 @@ pub mod tests { assert_eq!(stopped, None); }) } + + #[test] + pub fn test_attr_by_index() { + with_tmp_file(|file| { + let obj = file + .create_group_builder() + .with_gcpl(|gcpl| gcpl.attr_creation_order(AttrCreationOrder::Tracked)) + .create("o") + .unwrap(); + for name in ["c", "a", "b"] { + obj.new_attr::().create(name).unwrap(); + } + + let name = |index_type, iteration_order, index| { + obj.attr_by_index(index_type, iteration_order, index).unwrap().name() + }; + assert_eq!(name(IndexType::CreationOrder, IterationOrder::Increasing, 1), "a"); + assert_eq!(name(IndexType::CreationOrder, IterationOrder::Decreasing, 0), "b"); + assert_eq!(name(IndexType::Name, IterationOrder::Increasing, 2), "c"); + + let err = + obj.attr_by_index(IndexType::Name, IterationOrder::Increasing, 3).unwrap_err(); + assert!(err.contains_major(MajorErrorCode::Args), "{err:?}"); + assert!(err.contains_minor(MinorErrorCode::BadValue), "{err:?}"); + }) + } + + #[test] + pub fn test_attr_info() { + with_tmp_file(|file| { + file.new_attr::().create("a").unwrap(); + file.new_attr::().char_encoding(CharEncoding::Ascii).create("b").unwrap(); + + let expected = AttrInfo { + creation_order: Some(1), + char_encoding: CharEncoding::Ascii, + data_size: 8, + }; + assert_eq!(file.attr_info("b").unwrap(), expected); + let reported = file + .find_attr(IndexType::Name, IterationOrder::Increasing, |name, info| { + if name == "b" { Ok(Some(info)) } else { Ok(None) } + }) + .unwrap(); + assert_eq!(reported, Some(expected)); + + let err = file.attr_info("missing").unwrap_err(); + assert!(err.contains_major(MajorErrorCode::Attr), "{err:?}"); + assert!(err.contains_minor(MinorErrorCode::NotFound), "{err:?}"); + }) + } + fn attrs_by_creation_order( file: &File, name: &str, creation_order: AttrCreationOrder, dense: bool, ) -> Vec<(String, Option)> { From 5ce7f879bb4d1c5d9f7224b8c79020c166ea0139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 21:08:54 +0200 Subject: [PATCH 4/5] Extend the link order example to attributes The tracked group also tracks attribute creation order, and the example lists its attributes in that order, opens one by position and looks up the info of another by name. The example is renamed creation_order since it no longer covers only links. --- .github/workflows/ci.yml | 2 +- .../{link_order.rs => creation_order.rs} | 42 +++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) rename hdf5/examples/{link_order.rs => creation_order.rs} (61%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c322eb1f..096e7974 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,7 +175,7 @@ jobs: cargo r --example continuously_rw_1d --features ${{ env.FEATURES }} cargo r --example committed_datatype --features ${{ env.FEATURES }} cargo r --example swmr --features ${{ env.FEATURES }} - cargo r --example link_order --features ${{ env.FEATURES }} + cargo r --example creation_order --features ${{ env.FEATURES }} if: matrix.rust != 'stable-gnu' env: FEATURES: hdf5-sys/static,hdf5-sys/zlib,lzf,blosc-all diff --git a/hdf5/examples/link_order.rs b/hdf5/examples/creation_order.rs similarity index 61% rename from hdf5/examples/link_order.rs rename to hdf5/examples/creation_order.rs index 5735043b..b026542b 100644 --- a/hdf5/examples/link_order.rs +++ b/hdf5/examples/creation_order.rs @@ -1,16 +1,18 @@ -//! List the links of a group in the order they were created +//! List the links and attributes of a group in the order they were created //! -//! HDF5 keeps the links of a group in a name index. A group can also record the order in which its -//! links were created, which is what h5py does with `track_order=True` and what netCDF-4 does for -//! every group, so a reader sees the variables in the order the writer added them. Tracking has to -//! be requested when the group is created and cannot be turned on later. +//! HDF5 keeps the links of a group and the attributes of an object in a name index. Both can also +//! record the order in which they were created, which is what h5py does with `track_order=True` +//! and what netCDF-4 does for every group, so a reader sees variables and attributes in the order +//! the writer added them. Tracking has to be requested when the object is created and cannot be +//! turned on later. -use hdf5::plist::group_create::LinkCreationOrder; +use hdf5::plist::group_create::{AttrCreationOrder, LinkCreationOrder}; use hdf5::{File, IndexType, IterationCursor, IterationOrder, LinkType, MajorErrorCode, Result}; use hdf5_metno as hdf5; -const FILE_NAME: &str = "link_order.h5"; +const FILE_NAME: &str = "creation_order.h5"; const VARIABLES: [&str; 5] = ["time", "latitude", "longitude", "temperature", "pressure"]; +const ATTRIBUTES: [&str; 3] = ["title", "history", "Conventions"]; const PAGE_SIZE: usize = 3; fn write() -> Result<()> { @@ -18,12 +20,18 @@ fn write() -> Result<()> { let tracked = file .create_group_builder() - .with_gcpl(|gcpl| gcpl.link_creation_order(LinkCreationOrder::Tracked)) + .with_gcpl(|gcpl| { + gcpl.link_creation_order(LinkCreationOrder::Tracked) + .attr_creation_order(AttrCreationOrder::Tracked) + }) .create("tracked")?; for name in VARIABLES { tracked.new_dataset::().shape(24).create(name)?; } tracked.link_soft("temperature", "temp")?; + for name in ATTRIBUTES { + tracked.new_attr::().create(name)?; + } // A group created with the defaults has only the name index. let untracked = file.create_group("untracked")?; @@ -70,6 +78,24 @@ fn read() -> Result<()> { cursor = next; } + // Attributes follow the same index types. The tracked group lists them in creation order. + println!("attributes by name: {:?}", tracked.attr_names()?); + let mut attrs_by_creation = vec![]; + tracked.iter_attrs(IndexType::CreationOrder, IterationOrder::Increasing, |name, info| { + let order = info.creation_order.expect("the group tracks attribute creation order"); + println!("created {order}: @{name}"); + attrs_by_creation.push(name.to_owned()); + Ok(()) + })?; + assert_eq!(attrs_by_creation, ATTRIBUTES); + + // An attribute is opened by its position in either index, and its info looked up by name. + let first = tracked.attr_by_index(IndexType::CreationOrder, IterationOrder::Increasing, 0)?; + assert_eq!(first.name(), "title"); + let history = tracked.attr_info("history")?; + println!("history: {history:?}"); + assert_eq!(history.creation_order, Some(1)); + // Creation order is not available in a group that never tracked it. let err = file .group("untracked")? From 9bd63a82089ba9cea7f583bc26596ccdacde44c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 21:52:34 +0200 Subject: [PATCH 5/5] Add Group::info exposing H5Gget_info GroupInfo carries the storage type, the number of links, the creation order counter and the mounted flag. max_corder is the way to see that unlinking leaves the creation order counter alone, and storage_type shows when a group has moved from compact to dense storage. --- CHANGELOG.md | 1 + hdf5/src/hl.rs | 2 +- hdf5/src/hl/group.rs | 137 ++++++++++++++++++++++++++++++++++++++++++- hdf5/src/lib.rs | 6 +- 4 files changed, 141 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a58cb669..bf028515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added `GroupCreateBuilder::attr_creation_order` and `GroupCreateBuilder::attr_phase_change` with the matching `GroupCreate` getters - Added `Location::iter_attrs`, `Location::find_attr` and `Location::iter_attrs_from` with `AttrInfo`, and `Location::attr_names_by` and `Location::attrs`, to iterate attributes by name or creation order in either direction, sharing `IndexType`, `IterationOrder` and `IterationCursor` with link iteration - Added `Location::attr_by_index` and `Location::attr_info` +- Added `Group::info` with `GroupInfo` and `GroupStorageType`, exposing `H5Gget_info` ## hdf5-derive unreleased ## hdf5-types unreleased ## hdf5-sys unreleased diff --git a/hdf5/src/hl.rs b/hdf5/src/hl.rs index 7711ad44..c6e9492f 100644 --- a/hdf5/src/hl.rs +++ b/hdf5/src/hl.rs @@ -30,7 +30,7 @@ pub use self::{ dataspace::Dataspace, datatype::{Conversion, Datatype}, file::{File, FileBuilder, OpenMode}, - group::{Group, GroupBuilder, LinkInfo, LinkType}, + group::{Group, GroupBuilder, GroupInfo, GroupStorageType, LinkInfo, LinkType}, iteration::{IndexType, IterationCursor, IterationOrder}, location::{Location, LocationInfo, LocationToken, LocationType}, object::Object, diff --git a/hdf5/src/hl/group.rs b/hdf5/src/hl/group.rs index a4e842fa..8df7ac59 100644 --- a/hdf5/src/hl/group.rs +++ b/hdf5/src/hl/group.rs @@ -3,7 +3,10 @@ use std::ops::Deref; use hdf5_sys::{ h5d::H5Dopen2, - h5g::{H5G_info_t, H5Gcreate_anon, H5Gcreate2, H5Gget_create_plist, H5Gget_info, H5Gopen2}, + h5g::{ + H5G_info_t, H5G_storage_type_t, H5Gcreate_anon, H5Gcreate2, H5Gget_create_plist, + H5Gget_info, H5Gopen2, + }, h5l::{ H5L_SAME_LOC, H5L_info_t, H5L_type_t, H5Lcreate_external, H5Lcreate_hard, H5Lcreate_soft, H5Ldelete, H5Lexists, H5Literate, H5Lmove, @@ -84,6 +87,33 @@ impl Group { self.len() == 0 } + /// Returns information about the group. + /// + /// # Examples + /// + /// ``` + /// use hdf5_metno::plist::group_create::LinkCreationOrder; + /// use hdf5_metno::{File, GroupStorageType}; + /// + /// let file = File::with_options().with_fapl(|p| p.core_filebacked(false)).create("group_info.h5")?; + /// let group = file + /// .create_group_builder() + /// .with_gcpl(|gcpl| gcpl.link_creation_order(LinkCreationOrder::Tracked)) + /// .create("g")?; + /// group.create_group("a")?; + /// group.create_group("b")?; + /// group.unlink("a")?; + /// + /// let info = group.info()?; + /// assert_eq!(info.storage_type, GroupStorageType::Compact); + /// assert_eq!(info.nlinks, 1); + /// assert_eq!(info.max_corder, 2); + /// # Ok::<(), hdf5_metno::Error>(()) + /// ``` + pub fn info(&self) -> Result { + group_info(self.id())?.try_into() + } + /// Create a new group in a file or group. pub fn create_group(&self, name: &str) -> Result { // TODO: &mut self? @@ -417,6 +447,55 @@ impl GroupBuilder { } } +/// How the links of a group are stored. +/// +/// Corresponds to `H5G_storage_type_t`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GroupStorageType { + /// Links in a symbol table, the original group format. + SymbolTable, + /// Links as messages in the object header. + Compact, + /// Links in a fractal heap with B-tree indexes. + Dense, +} + +/// Information about a group. +/// +/// Corresponds to `H5G_info_t`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct GroupInfo { + /// How the links are stored. + pub storage_type: GroupStorageType, + /// Number of links in the group. + pub nlinks: u64, + /// Creation order position the next link receives. Unlinking does not lower it, so + /// it counts every link ever created in a group that tracks link creation order. + /// Zero in a group that does not track it. + pub max_corder: i64, + /// Whether a file is mounted on the group. + pub mounted: bool, +} + +impl TryFrom for GroupInfo { + type Error = Error; + + fn try_from(info: H5G_info_t) -> Result { + let storage_type = match info.storage_type { + H5G_storage_type_t::H5G_STORAGE_TYPE_SYMBOL_TABLE => GroupStorageType::SymbolTable, + H5G_storage_type_t::H5G_STORAGE_TYPE_COMPACT => GroupStorageType::Compact, + H5G_storage_type_t::H5G_STORAGE_TYPE_DENSE => GroupStorageType::Dense, + storage_type => fail!("Unknown group storage type: {:?}", storage_type), + }; + Ok(Self { + storage_type, + nlinks: info.nlinks, + max_corder: info.max_corder, + mounted: info.mounted > 0, + }) + } +} + /// The type of an object link. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinkType { @@ -999,6 +1078,62 @@ pub mod tests { }) } + #[test] + pub fn test_group_info() { + with_tmp_file(|file| { + // A default group is a symbol table before 2.0 and a compact new-style group from 2.0 + let default_storage = if cfg!(feature = "2.0.0") { + GroupStorageType::Compact + } else { + GroupStorageType::SymbolTable + }; + let untracked = file.create_group("untracked").unwrap(); + let expected = GroupInfo { + storage_type: default_storage, + nlinks: 0, + max_corder: 0, + mounted: false, + }; + assert_eq!(untracked.info().unwrap(), expected); + untracked.create_group("a").unwrap(); + assert_eq!(untracked.info().unwrap(), GroupInfo { nlinks: 1, ..expected }); + + let tracked = file + .create_group_builder() + .with_gcpl(|gcpl| gcpl.link_creation_order(LinkCreationOrder::Tracked)) + .create("tracked") + .unwrap(); + for name in ["a", "b", "c"] { + tracked.create_group(name).unwrap(); + } + let expected = GroupInfo { + storage_type: GroupStorageType::Compact, + nlinks: 3, + max_corder: 3, + mounted: false, + }; + assert_eq!(tracked.info().unwrap(), expected); + + // Unlinking leaves the creation order counter alone + tracked.unlink("b").unwrap(); + assert_eq!(tracked.info().unwrap(), GroupInfo { nlinks: 2, ..expected }); + tracked.create_group("d").unwrap(); + assert_eq!(tracked.info().unwrap(), GroupInfo { nlinks: 3, max_corder: 4, ..expected }); + let d = tracked.find_link(IndexType::Name, IterationOrder::Increasing, |name, info| { + if name == "d" { Ok(Some(info.creation_order)) } else { Ok(None) } + }); + assert_eq!(d.unwrap(), Some(Some(3))); + + // More links than the compact limit move the group to dense storage + for i in 0..8 { + tracked.create_group(&format!("dense{i}")).unwrap(); + } + let info = tracked.info().unwrap(); + assert_eq!(info.storage_type, GroupStorageType::Dense); + assert_eq!((info.nlinks, info.max_corder), (11, 12)); + }) + } + #[test] pub fn test_len() { with_tmp_file(|file| { diff --git a/hdf5/src/lib.rs b/hdf5/src/lib.rs index 1d214d49..2b879a2b 100644 --- a/hdf5/src/lib.rs +++ b/hdf5/src/lib.rs @@ -64,9 +64,9 @@ mod export { AttributeBuilderEmptyShape, ByteReader, CommittedDatatype, Container, Conversion, Dataset, DatasetBuilder, DatasetBuilderData, DatasetBuilderEmpty, DatasetBuilderEmptyShape, DatasetType, Dataspace, Datatype, File, FileBuilder, Group, - GroupBuilder, IndexType, IterationCursor, IterationOrder, LinkInfo, LinkType, Location, - LocationInfo, LocationToken, LocationType, Object, OpenMode, PropertyList, Reader, - Writer, + GroupBuilder, GroupInfo, GroupStorageType, IndexType, IterationCursor, IterationOrder, + LinkInfo, LinkType, Location, LocationInfo, LocationToken, LocationType, Object, + OpenMode, PropertyList, Reader, Writer, references::{ObjectReference, ObjectReference1, ReferencedObject}, }, };