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/CHANGELOG.md b/CHANGELOG.md index 05870896..bf028515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,13 @@ - 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 +- 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/examples/link_order.rs b/hdf5/examples/creation_order.rs similarity index 56% rename from hdf5/examples/link_order.rs rename to hdf5/examples/creation_order.rs index bcc268a1..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::{File, IndexType, IterationOrder, LinkCursor, LinkType, MajorErrorCode, Result}; +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")?; @@ -59,7 +67,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()); @@ -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")? diff --git a/hdf5/src/hl.rs b/hdf5/src/hl.rs index c2e30817..c6e9492f 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; @@ -17,7 +18,7 @@ pub mod selection; pub use self::{ attribute::{ - Attribute, AttributeBuilder, AttributeBuilderData, AttributeBuilderEmpty, + AttrInfo, Attribute, AttributeBuilder, AttributeBuilderData, AttributeBuilderEmpty, AttributeBuilderEmptyShape, }, committed_datatype::CommittedDatatype, @@ -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, GroupInfo, GroupStorageType, LinkInfo, LinkType}, + iteration::{IndexType, IterationCursor, IterationOrder}, location::{Location, LocationInfo, LocationToken, LocationType}, object::Object, plist::PropertyList, 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/group.rs b/hdf5/src/hl/group.rs index 0f48707f..8df7ac59 100644 --- a/hdf5/src/hl/group.rs +++ b/hdf5/src/hl/group.rs @@ -1,13 +1,12 @@ -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}, + 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, @@ -18,6 +17,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::*; @@ -87,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? @@ -420,101 +447,52 @@ 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`]. +/// How the links of a group are stored. /// -/// Corresponds to `H5_iter_order_t`. +/// Corresponds to `H5G_storage_type_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, - } - } +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, } -/// A position in a link iteration. +/// Information about a group. /// -/// 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. +/// Corresponds to `H5G_info_t`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct LinkCursor { - index_type: IndexType, - iteration_order: IterationOrder, - position: u64, +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 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 } - } +impl TryFrom for GroupInfo { + type Error = Error; - /// 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 + 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, + }) } } @@ -599,7 +577,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 +626,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 +646,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 +666,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 +786,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}; @@ -1162,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| { @@ -1505,7 +1477,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..57df3cf9 --- /dev/null +++ b/hdf5/src/hl/iteration.rs @@ -0,0 +1,204 @@ +//! 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`. 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 { + /// 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/hl/location.rs b/hdf5/src/hl/location.rs index fcc54b8e..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, 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}, @@ -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,219 @@ impl Location { Attribute::from_id(h5try!(H5Aopen(self.id(), name.as_ptr(), H5P_DEFAULT))) } - /// Return the names of all attributes on the object. + /// Opens the attribute at `index` along `index_type` in `iteration_order`. /// /// # Errors /// - /// Returns an error if an underlying library call fails. + /// 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> { - Attribute::attr_names(self) + 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 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 +590,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 +850,248 @@ 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); + }) + } + + #[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)> { + 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 8be9d785..2b879a2b 100644 --- a/hdf5/src/lib.rs +++ b/hdf5/src/lib.rs @@ -60,13 +60,13 @@ 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, - GroupBuilder, IndexType, IterationOrder, LinkCursor, 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}, }, };