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/CHANGELOG.md b/CHANGELOG.md index 82b0c4c4..c1489c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - 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` +- 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 ## hdf5-sys unreleased 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() +} diff --git a/hdf5/src/hl.rs b/hdf5/src/hl.rs index 54d7524f..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, 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 4b2a9065..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; @@ -419,31 +420,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, } @@ -463,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 { @@ -489,97 +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, traversal_order: TraversalOrder, 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: 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, + 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: FnMut(&str, LinkInfo) -> Result>, + { + enum Stop { + Found(B), + Error(Error), + Panic(Box), + } + + struct OpData { + op: F, + stop: Option>, } - // 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, + + // 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(), - traversal_order.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(), TraversalOrder::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 @@ -629,23 +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, 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)) @@ -857,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| { @@ -1130,4 +1359,188 @@ 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 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 empty = file.create_group("empty").unwrap(); + assert!( + empty.member_names_by(IndexType::Name, IterationOrder::Native).unwrap().is_empty() + ); + }) + } + + #[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 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 untracked = file.create_group("b").unwrap(); + untracked.new_dataset::().create("foo").unwrap(); + 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/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/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 bccc0dd2..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, 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}, }, }; 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;