From 83ced6eb32b7d5fc5ff7ed2e16c0898198dfbf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 19:01:09 +0200 Subject: [PATCH 1/4] Export IndexType and IterationOrder for Group::iter_visit Both enums were private to hl::group, so iter_visit could only be called with Default::default() for the order arguments. The previously private TraversalOrder is renamed to IndexType with variants Name and CreationOrder, matching H5_index_t. --- CHANGELOG.md | 1 + hdf5/src/hl.rs | 2 +- hdf5/src/hl/group.rs | 57 +++++++++++++++++++++++++++++++++++--------- hdf5/src/lib.rs | 4 ++-- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b0c4c4..ef4936f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Changed `DatasetBuilder::empty_as` and `DatasetBuilder::with_data_as` to accept an existing `Datatype` or `CommittedDatatype` as well as a `TypeDescriptor`, through the new `DatasetType`, so a dataset can be created with a committed datatype. - Added `FileCreateBuilder::sizes` and `Sizeof` to set the offset and length sizes of a file (breaking change, `SizeofInfo` holds `Sizeof` instead of `usize`) - 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` ## hdf5-derive unreleased ## hdf5-types unreleased ## hdf5-sys unreleased diff --git a/hdf5/src/hl.rs b/hdf5/src/hl.rs index 54d7524f..536c8305 100644 --- a/hdf5/src/hl.rs +++ b/hdf5/src/hl.rs @@ -29,7 +29,7 @@ pub use self::{ dataspace::Dataspace, datatype::{Conversion, Datatype}, file::{File, FileBuilder, OpenMode}, - group::{Group, GroupBuilder, LinkInfo, LinkType}, + group::{Group, GroupBuilder, IndexType, IterationOrder, LinkInfo, LinkType}, location::{Location, LocationInfo, LocationToken, LocationType}, object::Object, plist::PropertyList, diff --git a/hdf5/src/hl/group.rs b/hdf5/src/hl/group.rs index 4b2a9065..920efc7b 100644 --- a/hdf5/src/hl/group.rs +++ b/hdf5/src/hl/group.rs @@ -419,31 +419,44 @@ 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 TraversalOrder { +pub enum IndexType { + /// Index on link names. Name, - Creation, + /// Index on link creation order. + CreationOrder, } -impl Default for TraversalOrder { +impl Default for IndexType { fn default() -> Self { Self::Name } } -impl From for H5_index_t { - fn from(v: TraversalOrder) -> Self { +impl From for H5_index_t { + fn from(v: IndexType) -> Self { match v { - TraversalOrder::Name => Self::H5_INDEX_NAME, - TraversalOrder::Creation => Self::H5_INDEX_CRT_ORDER, + 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, } @@ -505,8 +518,7 @@ impl From<&H5L_info_t> for LinkInfo { impl Group { /// Visits all objects in the group pub fn iter_visit( - &self, iteration_order: IterationOrder, traversal_order: TraversalOrder, mut val: G, - mut op: F, + &self, iteration_order: IterationOrder, index_type: IndexType, mut val: G, mut op: F, ) -> Result where F: Fn(&Self, &str, LinkInfo, &mut G) -> bool, @@ -549,7 +561,7 @@ impl Group { h5call!(H5Literate( self.id(), - traversal_order.into(), + index_type.into(), iteration_order.into(), iter_pos, callback_fn, @@ -563,7 +575,7 @@ impl Group { where F: Fn(&Self, &str, LinkInfo, &mut G) -> bool, { - self.iter_visit(IterationOrder::default(), TraversalOrder::default(), val, op) + self.iter_visit(IterationOrder::default(), IndexType::default(), val, op) } fn get_all_of_type(&self, loc_type: LocationType) -> Result> { @@ -641,6 +653,7 @@ impl Group { #[cfg(test)] pub mod tests { use crate::internal_prelude::*; + use crate::{IndexType, IterationOrder}; use hdf5_types::{IntSize, TypeDescriptor, VarLenUnicode}; #[test] @@ -1130,4 +1143,26 @@ pub mod tests { } }) } + + #[test] + pub fn test_iter_visit_order() { + with_tmp_file(|file| { + let group = file.create_group("a").unwrap(); + for name in ["foo", "123", "bar"] { + group.new_dataset::().create(name).unwrap(); + } + + let visit = |iteration_order| { + group + .iter_visit(iteration_order, IndexType::Name, vec![], |_, name, _, names| { + names.push(name.to_owned()); + true + }) + .unwrap() + }; + + assert_eq!(visit(IterationOrder::Increasing), ["123", "bar", "foo"]); + assert_eq!(visit(IterationOrder::Decreasing), ["foo", "bar", "123"]); + }) + } } diff --git a/hdf5/src/lib.rs b/hdf5/src/lib.rs index bccc0dd2..7d8a18c7 100644 --- a/hdf5/src/lib.rs +++ b/hdf5/src/lib.rs @@ -64,8 +64,8 @@ mod export { AttributeBuilderEmptyShape, ByteReader, CommittedDatatype, Container, Conversion, Dataset, DatasetBuilder, DatasetBuilderData, DatasetBuilderEmpty, DatasetBuilderEmptyShape, DatasetType, Dataspace, Datatype, File, FileBuilder, Group, - GroupBuilder, LinkInfo, LinkType, Location, LocationInfo, LocationToken, LocationType, - Object, OpenMode, PropertyList, Reader, Writer, + GroupBuilder, IndexType, IterationOrder, LinkInfo, LinkType, Location, LocationInfo, + LocationToken, LocationType, Object, OpenMode, PropertyList, Reader, Writer, references::{ObjectReference, ObjectReference1, ReferencedObject}, }, }; From f304fb66e151539e3d0da21ebbfd3b51f2e12d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 18:07:23 +0200 Subject: [PATCH 2/4] Add link_creation_order to FileCreate and GroupCreate --- CHANGELOG.md | 1 + hdf5/src/hl/group.rs | 48 +++++++++++++++++++++++++++ hdf5/src/hl/plist/common.rs | 55 +++++++++++++++++++++++++++++++ hdf5/src/hl/plist/file_create.rs | 39 ++++++++++++++++++---- hdf5/src/hl/plist/group_create.rs | 32 +++++++++++++++++- hdf5/tests/test_plist.rs | 24 ++++++++++++++ 6 files changed, 192 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4936f7..6f8f2a17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Added `FileCreateBuilder::sizes` and `Sizeof` to set the offset and length sizes of a file (breaking change, `SizeofInfo` holds `Sizeof` instead of `usize`) - 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` +- Added `FileCreateBuilder::link_creation_order`, `GroupCreateBuilder::link_creation_order`, the matching getters and `LinkCreationOrder`, so a group can track and index link creation order ## hdf5-derive unreleased ## hdf5-types unreleased ## hdf5-sys unreleased diff --git a/hdf5/src/hl/group.rs b/hdf5/src/hl/group.rs index 920efc7b..314a99a1 100644 --- a/hdf5/src/hl/group.rs +++ b/hdf5/src/hl/group.rs @@ -652,6 +652,7 @@ impl Group { #[cfg(test)] pub mod tests { + use crate::hl::plist::common::LinkCreationOrder; use crate::internal_prelude::*; use crate::{IndexType, IterationOrder}; use hdf5_types::{IntSize, TypeDescriptor, VarLenUnicode}; @@ -1165,4 +1166,51 @@ pub mod tests { assert_eq!(visit(IterationOrder::Decreasing), ["foo", "bar", "123"]); }) } + + #[test] + pub fn test_iter_visit_creation_order() { + with_tmp_file(|file| { + let group = file + .create_group_builder() + .with_gcpl(|gcpl| gcpl.link_creation_order(LinkCreationOrder::Tracked)) + .create("a") + .unwrap(); + for name in ["foo", "123", "bar"] { + group.new_dataset::().create(name).unwrap(); + } + + let visit = |iteration_order| { + group + .iter_visit( + iteration_order, + IndexType::CreationOrder, + vec![], + |_, name, info, names| { + names.push((name.to_owned(), info.creation_order)); + true + }, + ) + .unwrap() + }; + + let foo = ("foo".to_owned(), Some(0)); + let num = ("123".to_owned(), Some(1)); + let bar = ("bar".to_owned(), Some(2)); + assert_eq!(visit(IterationOrder::Increasing), [foo.clone(), num.clone(), bar.clone()]); + assert_eq!(visit(IterationOrder::Decreasing), [bar, num, foo]); + + let untracked = file.create_group("b").unwrap(); + untracked.new_dataset::().create("foo").unwrap(); + assert!( + untracked + .iter_visit( + IterationOrder::Native, + IndexType::CreationOrder, + (), + |_, _, _, _| true + ) + .is_err() + ); + }) + } } diff --git a/hdf5/src/hl/plist/common.rs b/hdf5/src/hl/plist/common.rs index 85b87487..4f48b53b 100644 --- a/hdf5/src/hl/plist/common.rs +++ b/hdf5/src/hl/plist/common.rs @@ -1,3 +1,5 @@ +use std::os::raw::c_uint; + use hdf5_sys::h5p::{H5P_CRT_ORDER_INDEXED, H5P_CRT_ORDER_TRACKED}; use bitflags::bitflags; @@ -45,3 +47,56 @@ bitflags! { const INDEXED = H5P_CRT_ORDER_INDEXED as _; } } + +/// Tracking of link creation order in a group. +/// +/// By default link creation order is not recorded. `Tracked` records the order in +/// which links are created and allows the group to be traversed by +/// [`IndexType::CreationOrder`](crate::IndexType::CreationOrder). `Indexed` also +/// maintains an index for that traversal. +/// +/// The setting is fixed in the creation property list. HDF5 provides no way to +/// turn on tracking or build the index after the group exists. +/// +/// # Examples +/// +/// ``` +/// use hdf5_metno::plist::GroupCreateBuilder; +/// use hdf5_metno::plist::group_create::LinkCreationOrder; +/// +/// let gcpl = GroupCreateBuilder::new().link_creation_order(LinkCreationOrder::Indexed).finish()?; +/// assert_eq!(gcpl.link_creation_order(), LinkCreationOrder::Indexed); +/// # Ok::<(), hdf5_metno::Error>(()) +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum LinkCreationOrder { + /// Link creation order is not recorded. + #[default] + Untracked, + /// Link creation order is recorded. + Tracked, + /// Link creation order is recorded and indexed. + Indexed, +} + +impl LinkCreationOrder { + pub(crate) fn from_flags(flags: c_uint) -> Self { + if flags & H5P_CRT_ORDER_INDEXED != 0 { + Self::Indexed + } else if flags & H5P_CRT_ORDER_TRACKED != 0 { + Self::Tracked + } else { + Self::Untracked + } + } +} + +impl From for c_uint { + fn from(v: LinkCreationOrder) -> Self { + match v { + LinkCreationOrder::Untracked => 0, + LinkCreationOrder::Tracked => H5P_CRT_ORDER_TRACKED, + LinkCreationOrder::Indexed => H5P_CRT_ORDER_TRACKED | H5P_CRT_ORDER_INDEXED, + } + } +} diff --git a/hdf5/src/hl/plist/file_create.rs b/hdf5/src/hl/plist/file_create.rs index 9f5aba05..972e2263 100644 --- a/hdf5/src/hl/plist/file_create.rs +++ b/hdf5/src/hl/plist/file_create.rs @@ -13,11 +13,12 @@ use hdf5_sys::h5o::{ }; use hdf5_sys::h5p::{ H5Pcreate, H5Pget_attr_creation_order, H5Pget_attr_phase_change, H5Pget_istore_k, - H5Pget_obj_track_times, H5Pget_shared_mesg_index, H5Pget_shared_mesg_nindexes, - H5Pget_shared_mesg_phase_change, H5Pget_sizes, H5Pget_sym_k, H5Pget_userblock, - H5Pset_attr_creation_order, H5Pset_attr_phase_change, H5Pset_istore_k, H5Pset_obj_track_times, - H5Pset_shared_mesg_index, H5Pset_shared_mesg_nindexes, H5Pset_shared_mesg_phase_change, - H5Pset_sizes, H5Pset_sym_k, H5Pset_userblock, + H5Pget_link_creation_order, H5Pget_obj_track_times, H5Pget_shared_mesg_index, + H5Pget_shared_mesg_nindexes, H5Pget_shared_mesg_phase_change, H5Pget_sizes, H5Pget_sym_k, + H5Pget_userblock, H5Pset_attr_creation_order, H5Pset_attr_phase_change, H5Pset_istore_k, + H5Pset_link_creation_order, H5Pset_obj_track_times, H5Pset_shared_mesg_index, + H5Pset_shared_mesg_nindexes, H5Pset_shared_mesg_phase_change, H5Pset_sizes, H5Pset_sym_k, + H5Pset_userblock, }; #[cfg(feature = "1.10.1")] use hdf5_sys::h5p::{ @@ -26,7 +27,7 @@ use hdf5_sys::h5p::{ }; use crate::globals::H5P_FILE_CREATE; -pub use crate::hl::plist::common::{AttrCreationOrder, AttrPhaseChange}; +pub use crate::hl::plist::common::{AttrCreationOrder, AttrPhaseChange, LinkCreationOrder}; use crate::internal_prelude::*; /// File creation properties. @@ -68,6 +69,7 @@ impl Debug for FileCreate { formatter.field("obj_track_times", &self.obj_track_times()); formatter.field("attr_phase_change", &self.attr_phase_change()); formatter.field("attr_creation_order", &self.attr_creation_order()); + formatter.field("link_creation_order", &self.link_creation_order()); #[cfg(feature = "1.10.1")] { formatter.field("file_space_page_size", &self.file_space_page_size()); @@ -269,6 +271,7 @@ pub struct FileCreateBuilder { obj_track_times: Option, attr_phase_change: Option, attr_creation_order: Option, + link_creation_order: Option, #[cfg(feature = "1.10.1")] file_space_page_size: Option, #[cfg(feature = "1.10.1")] @@ -296,6 +299,7 @@ impl FileCreateBuilder { let apc = plist.get_attr_phase_change()?; builder.attr_phase_change(apc.max_compact, apc.min_dense); builder.attr_creation_order(plist.get_attr_creation_order()?); + builder.link_creation_order(plist.get_link_creation_order()?); #[cfg(feature = "1.10.1")] { builder.file_space_page_size(plist.get_file_space_page_size()?); @@ -409,6 +413,14 @@ impl FileCreateBuilder { self } + /// Sets whether link creation order is tracked and indexed in the root group. + /// + /// See [`LinkCreationOrder`] for the available settings. + pub fn link_creation_order(&mut self, link_creation_order: LinkCreationOrder) -> &mut Self { + self.link_creation_order = Some(link_creation_order); + self + } + #[cfg(feature = "1.10.1")] /// Sets the file space page size. /// @@ -471,6 +483,9 @@ impl FileCreateBuilder { if let Some(v) = self.attr_creation_order { h5try!(H5Pset_attr_creation_order(id, v.bits() as _)); } + if let Some(v) = self.link_creation_order { + h5try!(H5Pset_link_creation_order(id, v.into())); + } #[cfg(feature = "1.10.1")] { if let Some(v) = self.file_space_page_size { @@ -675,6 +690,18 @@ impl FileCreate { self.get_attr_creation_order().unwrap_or_default() } + #[doc(hidden)] + pub fn get_link_creation_order(&self) -> Result { + h5get!(H5Pget_link_creation_order(self.id()): c_uint).map(LinkCreationOrder::from_flags) + } + + /// Returns whether link creation order is tracked and indexed in the root group. + /// + /// Returns [`LinkCreationOrder::Untracked`] if the property cannot be read. + pub fn link_creation_order(&self) -> LinkCreationOrder { + self.get_link_creation_order().unwrap_or_default() + } + /// Retrieves the file space page size. #[cfg(feature = "1.10.1")] pub fn file_space_page_size(&self) -> u64 { diff --git a/hdf5/src/hl/plist/group_create.rs b/hdf5/src/hl/plist/group_create.rs index 281f6c52..f1083f65 100644 --- a/hdf5/src/hl/plist/group_create.rs +++ b/hdf5/src/hl/plist/group_create.rs @@ -3,9 +3,13 @@ use std::fmt::{self, Debug}; use std::ops::Deref; -use hdf5_sys::h5p::{H5Pcreate, H5Pget_obj_track_times, H5Pset_obj_track_times}; +use hdf5_sys::h5p::{ + H5Pcreate, H5Pget_link_creation_order, H5Pget_obj_track_times, H5Pset_link_creation_order, + H5Pset_obj_track_times, +}; use crate::globals::H5P_GROUP_CREATE; +pub use crate::hl::plist::common::LinkCreationOrder; use crate::internal_prelude::*; /// Group creation properties. @@ -44,6 +48,7 @@ impl Debug for GroupCreate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut formatter = f.debug_struct("GroupCreate"); formatter.field("obj_track_times", &self.obj_track_times()); + formatter.field("link_creation_order", &self.link_creation_order()); formatter.finish() } } @@ -68,6 +73,7 @@ impl Eq for GroupCreate {} #[derive(Clone, Debug, Default)] pub struct GroupCreateBuilder { obj_track_times: Option, + link_creation_order: Option, } impl GroupCreateBuilder { @@ -80,6 +86,7 @@ impl GroupCreateBuilder { pub fn from_plist(plist: &GroupCreate) -> Result { let mut builder = Self::default(); builder.obj_track_times(plist.get_obj_track_times()?); + builder.link_creation_order(plist.get_link_creation_order()?); Ok(builder) } @@ -91,10 +98,21 @@ impl GroupCreateBuilder { self } + /// Sets whether link creation order is tracked and indexed. + /// + /// See [`LinkCreationOrder`] for the available settings. + pub fn link_creation_order(&mut self, link_creation_order: LinkCreationOrder) -> &mut Self { + self.link_creation_order = Some(link_creation_order); + self + } + fn populate_plist(&self, id: hid_t) -> Result<()> { if let Some(v) = self.obj_track_times { h5try!(H5Pset_obj_track_times(id, hbool_t::from(v))); } + if let Some(v) = self.link_creation_order { + h5try!(H5Pset_link_creation_order(id, v.into())); + } Ok(()) } @@ -138,4 +156,16 @@ impl GroupCreate { pub fn obj_track_times(&self) -> bool { self.get_obj_track_times().unwrap_or(true) } + + #[doc(hidden)] + pub fn get_link_creation_order(&self) -> Result { + h5get!(H5Pget_link_creation_order(self.id()): c_uint).map(LinkCreationOrder::from_flags) + } + + /// Returns whether link creation order is tracked and indexed. + /// + /// Returns [`LinkCreationOrder::Untracked`] if the property cannot be read. + pub fn link_creation_order(&self) -> LinkCreationOrder { + self.get_link_creation_order().unwrap_or_default() + } } diff --git a/hdf5/tests/test_plist.rs b/hdf5/tests/test_plist.rs index f1dc7908..ea05cde8 100644 --- a/hdf5/tests/test_plist.rs +++ b/hdf5/tests/test_plist.rs @@ -166,6 +166,15 @@ fn test_fcpl_attr_creation_order() -> hdf5::Result<()> { Ok(()) } +#[test] +fn test_fcpl_link_creation_order() -> hdf5::Result<()> { + assert_eq!(FC::try_new()?.get_link_creation_order()?, LinkCreationOrder::Untracked); + assert_eq!(FC::try_new()?.link_creation_order(), LinkCreationOrder::Untracked); + test_pl!(FC, link_creation_order: LinkCreationOrder::Tracked); + test_pl!(FC, link_creation_order: LinkCreationOrder::Indexed); + Ok(()) +} + #[test] #[cfg(feature = "1.10.1")] fn test_fcpl_set_file_space_page_size() -> hdf5::Result<()> { @@ -655,6 +664,21 @@ fn test_gcpl_obj_track_times() -> hdf5::Result<()> { Ok(()) } +#[test] +fn test_gcpl_link_creation_order() -> hdf5::Result<()> { + assert_eq!(GC::try_new()?.get_link_creation_order()?, LinkCreationOrder::Untracked); + assert_eq!(GC::try_new()?.link_creation_order(), LinkCreationOrder::Untracked); + test_pl!(GC, link_creation_order: LinkCreationOrder::Tracked); + test_pl!(GC, link_creation_order: LinkCreationOrder::Indexed); + assert_eq!( + GCB::from_plist(&GCB::new().link_creation_order(LinkCreationOrder::Indexed).finish()?)? + .finish()? + .link_creation_order(), + LinkCreationOrder::Indexed + ); + Ok(()) +} + type DC = DatasetCreate; type DCB = DatasetCreateBuilder; From 9638c6fed9c60da2c5b2495034bc72d35763a0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 19:01:09 +0200 Subject: [PATCH 3/4] Rework Group::iter_visit around H5Literate index_type now comes before iteration_order, as in H5Literate2. Until IndexType and IterationOrder were exported the order arguments could only be passed as Default::default(), so the reorder breaks no caller. The closure takes the link name and LinkInfo and returns Result<()>. Its error is returned from iter_visit instead of being reported as a stop with a partial result. The group argument is dropped since H5Literate always passes back the group being iterated, and the accumulator argument is dropped since the closure can capture its own state. find_link takes a closure returning Result> and stops at the first Some, returning the value. Together with the error this maps onto the continue, stop and fail returns of the C callback. iter_visit_from takes a LinkCursor and returns the cursor of the next link together with the value, so a stopped iteration can be resumed. The cursor carries the index type and iteration order it counts along, so it cannot be reused with a different traversal. A cursor at or past the last link yields None instead of the HDF5 out of bounds error. A panic in the closure is resumed after H5Literate returns instead of being reported as an HDF5 error. --- CHANGELOG.md | 2 + hdf5/src/hl.rs | 2 +- hdf5/src/hl/group.rs | 560 ++++++++++++++++++++------ hdf5/src/hl/plist/attribute_create.rs | 6 +- hdf5/src/hl/plist/link_create.rs | 18 +- hdf5/src/lib.rs | 5 +- 6 files changed, 465 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f8f2a17..c1489c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Added `FileCreateBuilder::sizes` and `Sizeof` to set the offset and length sizes of a file (breaking change, `SizeofInfo` holds `Sizeof` instead of `usize`) - 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 `FileCreateBuilder::link_creation_order`, `GroupCreateBuilder::link_creation_order`, the matching getters and `LinkCreationOrder`, so a group can track and index link creation order ## hdf5-derive unreleased ## hdf5-types unreleased diff --git a/hdf5/src/hl.rs b/hdf5/src/hl.rs index 536c8305..c2e30817 100644 --- a/hdf5/src/hl.rs +++ b/hdf5/src/hl.rs @@ -29,7 +29,7 @@ pub use self::{ dataspace::Dataspace, datatype::{Conversion, Datatype}, file::{File, FileBuilder, OpenMode}, - group::{Group, GroupBuilder, IndexType, IterationOrder, LinkInfo, LinkType}, + group::{Group, GroupBuilder, IndexType, IterationOrder, LinkCursor, LinkInfo, LinkType}, location::{Location, LocationInfo, LocationToken, LocationType}, object::Object, plist::PropertyList, diff --git a/hdf5/src/hl/group.rs b/hdf5/src/hl/group.rs index 314a99a1..546ce291 100644 --- a/hdf5/src/hl/group.rs +++ b/hdf5/src/hl/group.rs @@ -1,6 +1,7 @@ +use std::any::Any; use std::fmt::{self, Debug}; use std::ops::Deref; -use std::panic; +use std::panic::{self, AssertUnwindSafe}; use std::ptr::addr_of_mut; use hdf5_sys::{ @@ -8,11 +9,11 @@ use hdf5_sys::{ h5d::H5Dopen2, h5g::{H5G_info_t, H5Gcreate_anon, H5Gcreate2, H5Gget_create_plist, H5Gget_info, H5Gopen2}, h5l::{ - H5L_SAME_LOC, H5L_info_t, H5L_iterate_t, H5L_type_t, H5Lcreate_external, H5Lcreate_hard, - H5Lcreate_soft, H5Ldelete, H5Lexists, H5Literate, H5Lmove, + H5L_SAME_LOC, H5L_info_t, H5L_type_t, H5Lcreate_external, H5Lcreate_hard, H5Lcreate_soft, + H5Ldelete, H5Lexists, H5Literate, H5Lmove, }, h5p::{H5Pcreate, H5Pset_create_intermediate_group}, - h5t::{H5T_cset_t, H5Tcommit2, H5Topen2}, + h5t::{H5Tcommit2, H5Topen2}, }; use crate::globals::H5P_LINK_CREATE; @@ -476,6 +477,47 @@ impl From for H5_iter_order_t { } } +/// 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 { @@ -502,96 +544,244 @@ impl From for LinkType { pub struct LinkInfo { pub link_type: LinkType, pub creation_order: Option, - pub is_utf8: bool, + /// Encoding of the link name. HDF5 values other than UTF-8 are reported as ASCII. + pub char_encoding: CharEncoding, } impl From<&H5L_info_t> for LinkInfo { fn from(link: &H5L_info_t) -> Self { let link_type = link.type_.into(); let creation_order = if link.corder_valid == 1 { Some(link.corder) } else { None }; - let is_utf8 = link.cset == H5T_cset_t::H5T_CSET_UTF8; - Self { link_type, creation_order, is_utf8 } + let char_encoding = CharEncoding::try_from(link.cset).unwrap_or(CharEncoding::Ascii); + Self { link_type, creation_order, char_encoding } } } /// Iteration methods impl Group { - /// Visits all objects in the group - pub fn iter_visit( - &self, iteration_order: IterationOrder, index_type: IndexType, mut val: G, mut op: F, - ) -> Result + /// Visits every link of the group, non-recursively. + /// + /// The links are traversed along `index_type` in `iteration_order`, and `op` is + /// called with the name and the [`LinkInfo`] of each link. Use + /// [`find_link`](Self::find_link) to stop early. + /// + /// # Errors + /// + /// Returns the first error returned by `op`. Returns the HDF5 error if the + /// iteration itself fails, for example when traversing by + /// [`IndexType::CreationOrder`] on a group that does not track link creation order. + /// + /// # 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::{File, IndexType, IterationOrder}; + /// + /// let file = File::with_options().with_fapl(|p| p.core_filebacked(false)).create("iter_visit.h5")?; + /// file.create_group("b")?; + /// file.create_group("a")?; + /// + /// let mut names = vec![]; + /// file.iter_visit(IndexType::Name, IterationOrder::Decreasing, |name, _| { + /// names.push(name.to_owned()); + /// Ok(()) + /// })?; + /// assert_eq!(names, ["b", "a"]); + /// # Ok::<(), hdf5_metno::Error>(()) + /// ``` + pub fn iter_visit( + &self, index_type: IndexType, iteration_order: IterationOrder, mut op: F, + ) -> Result<()> + where + F: FnMut(&str, LinkInfo) -> Result<()>, + { + self.iter_visit_from(LinkCursor::start(index_type, iteration_order), |name, info| { + op(name, info)?; + Ok(None::<()>) + })?; + Ok(()) + } + + /// Visits every link of the group by name in native order. + /// + /// Equivalent to [`iter_visit`](Self::iter_visit) with [`IndexType::Name`] and + /// [`IterationOrder::Native`]. + pub fn iter_visit_default(&self, op: F) -> Result<()> + where + F: FnMut(&str, LinkInfo) -> Result<()>, + { + self.iter_visit(IndexType::default(), IterationOrder::default(), op) + } + + /// Visits the links of the group until `op` returns a value. + /// + /// The links are traversed along `index_type` in `iteration_order`, and `op` is + /// called with the name and the [`LinkInfo`] of each link until it returns `Some`. + /// That value is returned, or `None` once every link was visited. + /// + /// # Errors + /// + /// As for [`iter_visit`](Self::iter_visit). + /// + /// # Examples + /// + /// ``` + /// use hdf5_metno::{File, IndexType, IterationOrder, LinkType}; + /// + /// let file = File::with_options().with_fapl(|p| p.core_filebacked(false)).create("find_link.h5")?; + /// file.create_group("b")?; + /// file.link_soft("b", "a")?; + /// + /// let first_hard = file.find_link(IndexType::Name, IterationOrder::Increasing, |name, info| { + /// Ok((info.link_type == LinkType::Hard).then(|| name.to_owned())) + /// })?; + /// assert_eq!(first_hard, Some("b".to_owned())); + /// # Ok::<(), hdf5_metno::Error>(()) + /// ``` + pub fn find_link( + &self, index_type: IndexType, iteration_order: IterationOrder, op: F, + ) -> Result> + where + F: FnMut(&str, LinkInfo) -> Result>, + { + match self.iter_visit_from(LinkCursor::start(index_type, iteration_order), op)? { + Some((value, _)) => Ok(Some(value)), + None => Ok(None), + } + } + + /// Visits the links of the group from `cursor` onwards until `op` returns a value. + /// + /// Behaves like [`find_link`](Self::find_link). The value is returned together + /// with the cursor of the next link, so the iteration can be resumed by passing + /// that cursor back. Returns `None` once every link was visited, including when + /// `cursor` is already at or past the last link. + /// + /// # Errors + /// + /// As for [`iter_visit`](Self::iter_visit). + /// + /// # Examples + /// + /// ``` + /// use hdf5_metno::{File, IndexType, IterationOrder, LinkCursor}; + /// + /// 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 names = vec![]; + /// while let Some((name, next)) = + /// file.iter_visit_from(cursor, |name, _| Ok(Some(name.to_owned())))? + /// { + /// names.push(name); + /// cursor = next; + /// } + /// assert_eq!(names, ["b", "c"]); + /// assert_eq!(cursor.position(), 3); + /// # Ok::<(), hdf5_metno::Error>(()) + /// ``` + pub fn iter_visit_from( + &self, cursor: LinkCursor, op: F, + ) -> Result> where - F: Fn(&Self, &str, LinkInfo, &mut G) -> bool, + F: FnMut(&str, LinkInfo) -> Result>, { - /// Struct used to pass a tuple - struct Vtable<'a, F, D> { - f: &'a mut F, - d: &'a mut D, + enum Stop { + Found(B), + Error(Error), + Panic(Box), } - // Maps a closure to a C callback - // - // This function will be called multiple times, but never concurrently - unsafe extern "C" fn callback( - id: hid_t, name: *const c_char, info: *const H5L_info_t, op_data: *mut c_void, + + 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(&Group, &str, LinkInfo, &mut G) -> bool, + F: FnMut(&str, LinkInfo) -> Result>, { - panic::catch_unwind(|| { - let vtable = op_data.cast::>(); - let vtable = unsafe { vtable.as_mut().expect("iter_visit: null op_data ptr") }; - unsafe { name.as_ref().expect("iter_visit: null name ptr") }; + // 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) }; - let info = unsafe { info.as_ref().expect("iter_visit: null info ptr") }; - let handle = Handle::try_borrow(id).expect("iter_visit: unable to create a handle"); - let group = Group::from_handle(handle); - let ret = - (vtable.f)(&group, name.to_string_lossy().as_ref(), info.into(), vtable.d); - i32::from(!ret) - }) - .unwrap_or(-1) + // 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 + } + } } - let callback_fn: H5L_iterate_t = Some(callback::); - let iter_pos: *mut hsize_t = &mut 0_u64; - - // Store our references on the heap - let mut vtable = Vtable { f: &mut op, d: &mut val }; - let other_data = addr_of_mut!(vtable).cast::(); + // 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); + } - h5call!(H5Literate( + let mut data = OpData { op, stop: None }; + let mut position: hsize_t = cursor.position; + let ret = h5call!(H5Literate( self.id(), - index_type.into(), - iteration_order.into(), - iter_pos, - callback_fn, - other_data - )) - .map(|_| val) - } - - /// Visits all objects in the group using default iteration/traversal order. - pub fn iter_visit_default(&self, val: G, op: F) -> Result - where - F: Fn(&Self, &str, LinkInfo, &mut G) -> bool, - { - self.iter_visit(IterationOrder::default(), IndexType::default(), val, op) + 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) + } + } } fn get_all_of_type(&self, loc_type: LocationType) -> Result> { - self.iter_visit_default(vec![], |group, name, _info, objects| { - if let Ok(info) = group.loc_info_by_name(name) { - if info.loc_type == loc_type { - if let Ok(loc) = group.open_by_token(info.token) { - objects.push(loc); - return true; // ok, object extracted and pushed - } - } else { - return true; // ok, object is of another type, skipped - } + let mut objects = vec![]; + self.iter_visit_default(|name, _| { + let info = self.loc_info_by_name(name)?; + if info.loc_type == loc_type { + objects.push(self.open_by_token(info.token)?); } - false // an error occurred somewhere along the way - }) + Ok(()) + })?; + Ok(objects) } /// Returns all groups in the group, non-recursively @@ -641,25 +831,51 @@ impl Group { CommittedDatatype::from_id(h5try!(H5Topen2(self.id(), name.as_ptr(), H5P_DEFAULT))) } - /// Returns the names of all objects in the group, non-recursively. + /// Returns the names of all links in the group by name in native order, non-recursively. pub fn member_names(&self) -> Result> { - self.iter_visit_default(vec![], |_, name, _, names| { + self.member_names_by(IndexType::default(), IterationOrder::default()) + } + + /// Returns the names of all links in the group along `index_type` in `iteration_order`. + pub fn member_names_by( + &self, index_type: IndexType, iteration_order: IterationOrder, + ) -> Result> { + let mut names = vec![]; + self.iter_visit(index_type, iteration_order, |name, _| { names.push(name.to_owned()); - true - }) + Ok(()) + })?; + Ok(names) + } + + /// Returns the name and [`LinkInfo`] of all links in the group along `index_type` in + /// `iteration_order`. + pub fn links( + &self, index_type: IndexType, iteration_order: IterationOrder, + ) -> Result> { + let mut links = vec![]; + self.iter_visit(index_type, iteration_order, |name, info| { + links.push((name.to_owned(), info)); + Ok(()) + })?; + Ok(links) } } #[cfg(test)] pub mod tests { use crate::hl::plist::common::LinkCreationOrder; + use crate::hl::plist::file_access::FileCloseDegree; + #[cfg(feature = "1.10.2")] + use crate::hl::plist::file_access::LibraryVersion; + use crate::hl::plist::link_create::CharEncoding; use crate::internal_prelude::*; - use crate::{IndexType, IterationOrder}; + use crate::{IndexType, IterationOrder, LinkCursor, LinkType}; use hdf5_types::{IntSize, TypeDescriptor, VarLenUnicode}; + use std::panic::{self, AssertUnwindSafe}; #[test] pub fn test_debug() { - use crate::hl::plist::file_access::FileCloseDegree; with_tmp_path(|path| { let file = File::with_options() .with_fapl(|fapl| fapl.fclose_degree(FileCloseDegree::Strong)) @@ -871,7 +1087,6 @@ pub mod tests { #[cfg(feature = "1.10.2")] #[test] pub fn test_group_track_times_disabled() { - use crate::hl::plist::file_access::LibraryVersion; // exercise both the minimum v18 format and the newest one for low in [LibraryVersion::V18, LibraryVersion::latest()] { with_tmp_path(|path| { @@ -1152,18 +1367,14 @@ pub mod tests { for name in ["foo", "123", "bar"] { group.new_dataset::().create(name).unwrap(); } + let names = |order| group.member_names_by(IndexType::Name, order).unwrap(); + assert_eq!(names(IterationOrder::Increasing), ["123", "bar", "foo"]); + assert_eq!(names(IterationOrder::Decreasing), ["foo", "bar", "123"]); - let visit = |iteration_order| { - group - .iter_visit(iteration_order, IndexType::Name, vec![], |_, name, _, names| { - names.push(name.to_owned()); - true - }) - .unwrap() - }; - - assert_eq!(visit(IterationOrder::Increasing), ["123", "bar", "foo"]); - assert_eq!(visit(IterationOrder::Decreasing), ["foo", "bar", "123"]); + let empty = file.create_group("empty").unwrap(); + assert!( + empty.member_names_by(IndexType::Name, IterationOrder::Native).unwrap().is_empty() + ); }) } @@ -1179,38 +1390,157 @@ pub mod tests { group.new_dataset::().create(name).unwrap(); } - let visit = |iteration_order| { - group - .iter_visit( - iteration_order, - IndexType::CreationOrder, - vec![], - |_, name, info, names| { - names.push((name.to_owned(), info.creation_order)); - true - }, - ) - .unwrap() + let link = |name: &str, order| { + let info = LinkInfo { + link_type: LinkType::Hard, + creation_order: Some(order), + char_encoding: CharEncoding::Ascii, + }; + (name.to_owned(), info) + }; + let foo = link("foo", 0); + let num = link("123", 1); + let bar = link("bar", 2); + let links = |order| group.links(IndexType::CreationOrder, order).unwrap(); + assert_eq!(links(IterationOrder::Increasing), [foo.clone(), num.clone(), bar.clone()]); + assert_eq!(links(IterationOrder::Decreasing), [bar, num, foo]); + + // A default group is a symbol table before 2.0 (BADVALUE, no creation order index) + // and a new-style group from 2.0 (NOTFOUND, creation order not tracked) + let not_tracked = if cfg!(feature = "2.0.0") { + MinorErrorCode::NotFound + } else { + MinorErrorCode::BadValue }; - - let foo = ("foo".to_owned(), Some(0)); - let num = ("123".to_owned(), Some(1)); - let bar = ("bar".to_owned(), Some(2)); - assert_eq!(visit(IterationOrder::Increasing), [foo.clone(), num.clone(), bar.clone()]); - assert_eq!(visit(IterationOrder::Decreasing), [bar, num, foo]); - let untracked = file.create_group("b").unwrap(); untracked.new_dataset::().create("foo").unwrap(); - assert!( - untracked - .iter_visit( - IterationOrder::Native, - IndexType::CreationOrder, - (), - |_, _, _, _| true - ) - .is_err() - ); + let err = untracked + .member_names_by(IndexType::CreationOrder, IterationOrder::Native) + .unwrap_err(); + assert!(err.contains_major(MajorErrorCode::SymbolTable), "{err:?}"); + assert!(err.contains_minor(not_tracked), "{err:?}"); + }) + } + + #[test] + pub fn test_find_link() { + with_tmp_file(|file| { + for name in ["a", "b", "c"] { + file.create_group(name).unwrap(); + } + + let find = |wanted: &str| { + let mut visited = vec![]; + let found = file + .find_link(IndexType::Name, IterationOrder::Increasing, |name, info| { + visited.push(name.to_owned()); + if name == wanted { + Ok(Some((name.to_owned(), info.link_type))) + } else { + Ok(None) + } + }) + .unwrap(); + (found, visited) + }; + + let (found, visited) = find("b"); + assert_eq!(found, Some(("b".to_owned(), LinkType::Hard))); + 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_visit_error() { + with_tmp_file(|file| { + for name in ["a", "b", "c"] { + file.create_group(name).unwrap(); + } + + let mut visited = vec![]; + let err = file + .iter_visit(IndexType::Name, IterationOrder::Increasing, |name, _| { + visited.push(name.to_owned()); + if name == "b" { Err("stop".into()) } else { Ok(()) } + }) + .unwrap_err(); + assert_eq!(visited, ["a", "b"]); + assert!(matches!(err, Error::Internal(ref msg) if msg == "stop"), "{err:?}"); + assert!(err.stack().is_none()); + }) + } + + #[test] + pub fn test_iter_visit_from() { + with_tmp_file(|file| { + for name in ["a", "b", "c"] { + file.create_group(name).unwrap(); + } + let start = LinkCursor::start(IndexType::Name, IterationOrder::Increasing); + + let stop_at = |cursor, wanted: &str| { + let mut visited = vec![]; + let stopped = file + .iter_visit_from(cursor, |name, _| { + visited.push(name.to_owned()); + if name == wanted { Ok(Some(name.len())) } else { Ok(None) } + }) + .unwrap(); + (stopped, visited) + }; + + let (stopped, visited) = stop_at(start, "b"); + assert_eq!(visited, ["a", "b"]); + assert_eq!(stopped, Some((1, start.skip(2)))); + + let (stopped, visited) = stop_at(start.skip(2), "c"); + assert_eq!(visited, ["c"]); + assert_eq!(stopped, Some((1, start.skip(3)))); + + let (stopped, visited) = stop_at(start.skip(2), "z"); + assert_eq!(visited, ["c"]); + assert_eq!(stopped, None); + + for past_end in [start.skip(3), start.skip(9)] { + let (stopped, visited) = stop_at(past_end, "a"); + assert!(visited.is_empty()); + assert_eq!(stopped, None); + } + }) + } + + #[test] + pub fn test_iter_visit_panic() { + with_tmp_file(|file| { + file.create_group("a").unwrap(); + let payload = panic::catch_unwind(AssertUnwindSafe(|| { + file.iter_visit_default(|_, _| -> Result<()> { panic!("boom") }) + })) + .unwrap_err(); + assert_eq!(payload.downcast_ref::<&str>(), Some(&"boom")); + + let lock_is_free = std::thread::spawn(|| { + crate::sync::LOCK.try_lock_for(std::time::Duration::from_secs(30)).is_some() + }); + assert!(lock_is_free.join().unwrap()); + assert_eq!(file.member_names().unwrap(), ["a"]); + }) + } + + #[test] + pub fn test_iterators_unresolvable_link() { + with_tmp_file(|file| { + file.create_group("a").unwrap(); + file.link_soft("missing", "dangling").unwrap(); + + assert_eq!(file.member_names().unwrap(), ["a", "dangling"]); + let err = file.groups().unwrap_err(); + assert!(err.contains_major(MajorErrorCode::SymbolTable), "{err:?}"); + assert!(err.contains_minor(MinorErrorCode::NotFound), "{err:?}"); }) } } diff --git a/hdf5/src/hl/plist/attribute_create.rs b/hdf5/src/hl/plist/attribute_create.rs index 1fe170f2..4d211b83 100644 --- a/hdf5/src/hl/plist/attribute_create.rs +++ b/hdf5/src/hl/plist/attribute_create.rs @@ -135,11 +135,7 @@ impl AttributeCreate { #[doc(hidden)] pub fn get_char_encoding(&self) -> Result { - Ok(match h5get!(H5Pget_char_encoding(self.id()): H5T_cset_t)? { - H5T_CSET_ASCII => CharEncoding::Ascii, - H5T_CSET_UTF8 => CharEncoding::Utf8, - encoding => fail!("Unknown char encoding: {:?}", encoding), - }) + h5get!(H5Pget_char_encoding(self.id()): H5T_cset_t)?.try_into() } /// Returns the character encoding of the attribute name. diff --git a/hdf5/src/hl/plist/link_create.rs b/hdf5/src/hl/plist/link_create.rs index 3c95c570..88d44f94 100644 --- a/hdf5/src/hl/plist/link_create.rs +++ b/hdf5/src/hl/plist/link_create.rs @@ -78,6 +78,18 @@ pub enum CharEncoding { Utf8, } +impl TryFrom for CharEncoding { + type Error = Error; + + fn try_from(encoding: H5T_cset_t) -> Result { + match encoding { + H5T_CSET_ASCII => Ok(Self::Ascii), + H5T_CSET_UTF8 => Ok(Self::Utf8), + encoding => fail!("Unknown char encoding: {:?}", encoding), + } + } +} + /// Builder used to create link create property list. #[derive(Clone, Debug, Default)] pub struct LinkCreateBuilder { @@ -168,11 +180,7 @@ impl LinkCreate { #[doc(hidden)] pub fn get_char_encoding(&self) -> Result { - Ok(match h5get!(H5Pget_char_encoding(self.id()): H5T_cset_t)? { - H5T_CSET_ASCII => CharEncoding::Ascii, - H5T_CSET_UTF8 => CharEncoding::Utf8, - encoding => fail!("Unknown char encoding: {:?}", encoding), - }) + h5get!(H5Pget_char_encoding(self.id()): H5T_cset_t)?.try_into() } /// Returns the character encoding used to create links. diff --git a/hdf5/src/lib.rs b/hdf5/src/lib.rs index 7d8a18c7..8be9d785 100644 --- a/hdf5/src/lib.rs +++ b/hdf5/src/lib.rs @@ -64,8 +64,9 @@ mod export { AttributeBuilderEmptyShape, ByteReader, CommittedDatatype, Container, Conversion, Dataset, DatasetBuilder, DatasetBuilderData, DatasetBuilderEmpty, DatasetBuilderEmptyShape, DatasetType, Dataspace, Datatype, File, FileBuilder, Group, - GroupBuilder, IndexType, IterationOrder, LinkInfo, LinkType, Location, LocationInfo, - LocationToken, LocationType, Object, OpenMode, PropertyList, Reader, Writer, + GroupBuilder, IndexType, IterationOrder, LinkCursor, LinkInfo, LinkType, Location, + LocationInfo, LocationToken, LocationType, Object, OpenMode, PropertyList, Reader, + Writer, references::{ObjectReference, ObjectReference1, ReferencedObject}, }, }; From 1265e9262d9b30eb94571243e88a3060ea89638d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Beck=20K=C3=B6nig?= Date: Mon, 7 Sep 2026 19:58:38 +0200 Subject: [PATCH 4/4] Add link order example --- .github/workflows/ci.yml | 1 + hdf5/examples/link_order.rs | 86 +++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 hdf5/examples/link_order.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ebba481..c322eb1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,6 +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 }} 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/link_order.rs new file mode 100644 index 00000000..bcc268a1 --- /dev/null +++ b/hdf5/examples/link_order.rs @@ -0,0 +1,86 @@ +//! List the links 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. + +use hdf5::plist::group_create::LinkCreationOrder; +use hdf5::{File, IndexType, IterationOrder, LinkCursor, LinkType, MajorErrorCode, Result}; +use hdf5_metno as hdf5; + +const FILE_NAME: &str = "link_order.h5"; +const VARIABLES: [&str; 5] = ["time", "latitude", "longitude", "temperature", "pressure"]; +const PAGE_SIZE: usize = 3; + +fn write() -> Result<()> { + let file = File::create(FILE_NAME)?; + + let tracked = file + .create_group_builder() + .with_gcpl(|gcpl| gcpl.link_creation_order(LinkCreationOrder::Tracked)) + .create("tracked")?; + for name in VARIABLES { + tracked.new_dataset::().shape(24).create(name)?; + } + tracked.link_soft("temperature", "temp")?; + + // A group created with the defaults has only the name index. + let untracked = file.create_group("untracked")?; + for name in VARIABLES { + untracked.new_dataset::().shape(24).create(name)?; + } + Ok(()) +} + +fn read() -> Result<()> { + let file = File::open(FILE_NAME)?; + let tracked = file.group("tracked")?; + + // member_names() and iter_visit_default() walk the name index. + println!("by name: {:?}", tracked.member_names()?); + + // The creation index returns the writer's order, and LinkInfo carries the position in it. + let mut by_creation = vec![]; + tracked.iter_visit(IndexType::CreationOrder, IterationOrder::Increasing, |name, info| { + let order = info.creation_order.expect("the group tracks link creation order"); + println!("created {order}: {name}"); + by_creation.push(name.to_owned()); + Ok(()) + })?; + assert_eq!(by_creation, ["time", "latitude", "longitude", "temperature", "pressure", "temp"]); + + // find_link stops at the first link the closure returns a value for. + let first_soft = + tracked.find_link(IndexType::CreationOrder, IterationOrder::Increasing, |name, info| { + if info.link_type == LinkType::Soft { Ok(Some(name.to_owned())) } else { Ok(None) } + })?; + println!("first soft link: {first_soft:?}"); + 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 page = vec![]; + while let Some(((), next)) = tracked.iter_visit_from(cursor, |name, _| { + page.push(name.to_owned()); + if page.len() == PAGE_SIZE { Ok(Some(())) } else { Ok(None) } + })? { + println!("page from {}: {page:?}", cursor.position()); + page.clear(); + cursor = next; + } + + // Creation order is not available in a group that never tracked it. + let err = file + .group("untracked")? + .iter_visit(IndexType::CreationOrder, IterationOrder::Increasing, |_, _| Ok(())) + .expect_err("the group does not track link creation order"); + assert!(err.contains_major(MajorErrorCode::SymbolTable)); + println!("untracked group: {err}"); + Ok(()) +} + +fn main() -> Result<()> { + write()?; + read() +}