Skip to main content

rust_hdf5/
group.rs

1//! Group support.
2//!
3//! Groups are containers for datasets and other groups, forming a
4//! hierarchical namespace within an HDF5 file.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use rust_hdf5::H5File;
10//!
11//! let file = H5File::create("groups.h5").unwrap();
12//! let root = file.root_group();
13//! let grp = root.create_group("detector").unwrap();
14//! let ds = grp.new_dataset::<f32>()
15//!     .shape(&[10])
16//!     .create("temperature")
17//!     .unwrap();
18//! ```
19
20use crate::dataset::{DatasetBuilder, H5Dataset};
21use crate::error::{Hdf5Error, Result};
22use crate::file::{borrow_inner, borrow_inner_mut, clone_inner, H5FileInner, SharedInner};
23use crate::format::messages::attribute::AttributeMessage;
24use crate::format::messages::filter::FilterPipeline;
25use crate::types::H5Type;
26
27/// A handle to an HDF5 group.
28///
29/// Groups are containers for datasets and other groups. The root group
30/// is always available via [`H5File::root_group`](crate::file::H5File::root_group).
31pub struct H5Group {
32    file_inner: SharedInner,
33    /// The absolute path of this group (e.g., "/" or "/detector").
34    name: String,
35}
36
37impl H5Group {
38    /// Create a new group handle.
39    pub(crate) fn new(file_inner: SharedInner, name: String) -> Self {
40        Self { file_inner, name }
41    }
42
43    /// Return the name (path) of this group.
44    pub fn name(&self) -> &str {
45        &self.name
46    }
47
48    /// Start building a new dataset in this group.
49    ///
50    /// The dataset will be registered as a child of this group in the
51    /// HDF5 file hierarchy.
52    pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
53        DatasetBuilder::new_in_group(clone_inner(&self.file_inner), self.name.clone())
54    }
55
56    /// Create a sub-group within this group.
57    ///
58    /// Creates a real HDF5 group with its own object header.
59    pub fn create_group(&self, name: &str) -> Result<H5Group> {
60        let full_name = if self.name == "/" {
61            format!("/{}", name)
62        } else {
63            format!("{}/{}", self.name, name)
64        };
65
66        let inner = borrow_inner(&self.file_inner);
67        match &*inner {
68            H5FileInner::Writer(writer) => {
69                writer.create_group(&self.name, name)?;
70            }
71            H5FileInner::Reader(_) => {
72                return Err(Hdf5Error::InvalidState(
73                    "cannot create groups in read mode".into(),
74                ));
75            }
76            H5FileInner::Closed => {
77                return Err(Hdf5Error::InvalidState("file is closed".into()));
78            }
79        }
80        drop(inner);
81
82        Ok(H5Group {
83            file_inner: clone_inner(&self.file_inner),
84            name: full_name,
85        })
86    }
87
88    /// Create a hard link in this group: an additional name `link_name`
89    /// for the object that already exists at `target_path`.
90    ///
91    /// No data is copied — the link and its target share one object, just
92    /// as `h5py` / libhdf5 hard links do. `target_path` may be given with
93    /// or without a leading `/` and must name an existing dataset or group.
94    /// This is the NeXus-style way to expose a dataset at a second
95    /// canonical location (e.g. `/entry/data/data`) without duplicating it.
96    ///
97    /// ```no_run
98    /// use rust_hdf5::H5File;
99    ///
100    /// let file = H5File::create("nexus.h5").unwrap();
101    /// let inst = file.root_group().create_group("instrument").unwrap();
102    /// inst.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
103    /// let data = file.root_group().create_group("data").unwrap();
104    /// // /data/data is now a hard link to /instrument/data — no copy.
105    /// data.link("data", "/instrument/data").unwrap();
106    /// ```
107    pub fn link(&self, link_name: &str, target_path: &str) -> Result<()> {
108        let inner = borrow_inner(&self.file_inner);
109        match &*inner {
110            H5FileInner::Writer(writer) => {
111                writer.create_hard_link(&self.name, link_name, target_path)?;
112                Ok(())
113            }
114            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
115                "cannot create hard links in read mode".into(),
116            )),
117            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
118        }
119    }
120
121    /// Open an existing sub-group by name (read mode).
122    pub fn group(&self, name: &str) -> Result<H5Group> {
123        let full_name = if self.name == "/" {
124            format!("/{}", name)
125        } else {
126            format!("{}/{}", self.name, name)
127        };
128
129        // Verify the group exists by consulting the reader's actual group
130        // set (derived from link records), not inferred dataset prefixes.
131        // This opens empty groups, attribute-only groups, and
132        // subgroup-only groups, which have no datasets beneath them.
133        let inner = borrow_inner(&self.file_inner);
134        let full_name = match &*inner {
135            H5FileInner::Reader(reader) => {
136                let group_path = full_name.trim_start_matches('/');
137                if !reader.has_group(group_path) {
138                    return Err(Hdf5Error::NotFound(full_name));
139                }
140                full_name
141            }
142            // In write mode the handle stores the tree path, so a path
143            // through hard links resolves once here and every operation
144            // made through the handle lands on the link's target.
145            H5FileInner::Writer(writer) => writer.canonical_group_path(&full_name),
146            H5FileInner::Closed => full_name,
147        };
148        drop(inner);
149
150        Ok(H5Group {
151            file_inner: clone_inner(&self.file_inner),
152            name: full_name,
153        })
154    }
155
156    /// List dataset names that are direct children of this group.
157    pub fn dataset_names(&self) -> Result<Vec<String>> {
158        let inner = borrow_inner(&self.file_inner);
159        let all_names = match &*inner {
160            H5FileInner::Reader(reader) => reader
161                .dataset_names()
162                .iter()
163                .map(|s| s.to_string())
164                .collect::<Vec<_>>(),
165            H5FileInner::Writer(writer) => writer
166                .dataset_names()
167                .iter()
168                .map(|s| s.to_string())
169                .collect::<Vec<_>>(),
170            H5FileInner::Closed => return Ok(vec![]),
171        };
172
173        let prefix = if self.name == "/" {
174            String::new()
175        } else {
176            format!("{}/", self.name.trim_start_matches('/'))
177        };
178
179        let mut result = Vec::new();
180        for name in &all_names {
181            let stripped = if prefix.is_empty() {
182                name.as_str()
183            } else if let Some(rest) = name.strip_prefix(&prefix) {
184                rest
185            } else {
186                continue;
187            };
188            // Only direct children (no further '/')
189            if !stripped.contains('/') {
190                result.push(stripped.to_string());
191            }
192        }
193        Ok(result)
194    }
195
196    /// Create a variable-length string dataset and write data within this group.
197    ///
198    /// Returns a writer-mode handle to the created dataset so attributes can be
199    /// attached to it (e.g. units, descriptions) just like a dataset created
200    /// via [`new_dataset`](Self::new_dataset).
201    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
202        let full_name = if self.name == "/" {
203            name.to_string()
204        } else {
205            let trimmed = self.name.trim_start_matches('/');
206            format!("{}/{}", trimmed, name)
207        };
208
209        let inner = borrow_inner(&self.file_inner);
210        match &*inner {
211            H5FileInner::Writer(writer) => {
212                let idx = writer.create_vlen_string_dataset(&full_name, strings)?;
213                if self.name != "/" {
214                    writer.assign_dataset_to_group(&self.name, idx)?;
215                }
216                let (shape, element_size, chunked, btree2, fixed_array) =
217                    writer.dataset_handle_parts(idx);
218                Ok(H5Dataset::new_writer(
219                    clone_inner(&self.file_inner),
220                    idx,
221                    shape,
222                    element_size,
223                    chunked,
224                    btree2,
225                    fixed_array,
226                ))
227            }
228            H5FileInner::Reader(_) => {
229                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
230            }
231            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
232        }
233    }
234
235    /// Create a variable-length byte-array dataset and write data within this
236    /// group.
237    ///
238    /// Each `&[u8]` becomes one element of variable length, stored as a vlen
239    /// sequence of `u8`. h5py reads it back as an array of `uint8` arrays.
240    /// Returns a writer-mode handle so attributes can be attached, like
241    /// [`write_vlen_strings`](Self::write_vlen_strings).
242    pub fn write_vlen_bytes(&self, name: &str, items: &[&[u8]]) -> Result<H5Dataset> {
243        let full_name = if self.name == "/" {
244            name.to_string()
245        } else {
246            let trimmed = self.name.trim_start_matches('/');
247            format!("{}/{}", trimmed, name)
248        };
249
250        let inner = borrow_inner(&self.file_inner);
251        match &*inner {
252            H5FileInner::Writer(writer) => {
253                let idx = writer.create_vlen_bytes_dataset(&full_name, items)?;
254                if self.name != "/" {
255                    writer.assign_dataset_to_group(&self.name, idx)?;
256                }
257                let (shape, element_size, chunked, btree2, fixed_array) =
258                    writer.dataset_handle_parts(idx);
259                Ok(H5Dataset::new_writer(
260                    clone_inner(&self.file_inner),
261                    idx,
262                    shape,
263                    element_size,
264                    chunked,
265                    btree2,
266                    fixed_array,
267                ))
268            }
269            H5FileInner::Reader(_) => {
270                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
271            }
272            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
273        }
274    }
275
276    /// Create a chunked, compressed variable-length string dataset within this group.
277    ///
278    /// Returns a writer-mode handle to the created dataset so attributes can be
279    /// attached to it, like [`write_vlen_strings`](Self::write_vlen_strings).
280    pub fn write_vlen_strings_compressed(
281        &self,
282        name: &str,
283        strings: &[&str],
284        chunk_size: usize,
285        pipeline: FilterPipeline,
286    ) -> Result<H5Dataset> {
287        let full_name = if self.name == "/" {
288            name.to_string()
289        } else {
290            let trimmed = self.name.trim_start_matches('/');
291            format!("{}/{}", trimmed, name)
292        };
293
294        let inner = borrow_inner(&self.file_inner);
295        match &*inner {
296            H5FileInner::Writer(writer) => {
297                let idx = writer.create_vlen_string_dataset_compressed(
298                    &full_name, strings, chunk_size, pipeline,
299                )?;
300                if self.name != "/" {
301                    writer.assign_dataset_to_group(&self.name, idx)?;
302                }
303                let (shape, element_size, chunked, btree2, fixed_array) =
304                    writer.dataset_handle_parts(idx);
305                Ok(H5Dataset::new_writer(
306                    clone_inner(&self.file_inner),
307                    idx,
308                    shape,
309                    element_size,
310                    chunked,
311                    btree2,
312                    fixed_array,
313                ))
314            }
315            H5FileInner::Reader(_) => {
316                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
317            }
318            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
319        }
320    }
321
322    /// Create an empty chunked vlen string dataset ready for incremental appends.
323    ///
324    /// Returns a writer-mode handle to the created dataset so attributes can be
325    /// attached before or between [`append_vlen_strings`](Self::append_vlen_strings)
326    /// calls.
327    pub fn create_appendable_vlen_dataset(
328        &self,
329        name: &str,
330        chunk_size: usize,
331        pipeline: Option<FilterPipeline>,
332    ) -> Result<H5Dataset> {
333        let full_name = if self.name == "/" {
334            name.to_string()
335        } else {
336            let trimmed = self.name.trim_start_matches('/');
337            format!("{}/{}", trimmed, name)
338        };
339
340        let inner = borrow_inner(&self.file_inner);
341        match &*inner {
342            H5FileInner::Writer(writer) => {
343                let idx = writer
344                    .create_appendable_vlen_string_dataset(&full_name, chunk_size, pipeline)?;
345                if self.name != "/" {
346                    writer.assign_dataset_to_group(&self.name, idx)?;
347                }
348                let (shape, element_size, chunked, btree2, fixed_array) =
349                    writer.dataset_handle_parts(idx);
350                Ok(H5Dataset::new_writer(
351                    clone_inner(&self.file_inner),
352                    idx,
353                    shape,
354                    element_size,
355                    chunked,
356                    btree2,
357                    fixed_array,
358                ))
359            }
360            H5FileInner::Reader(_) => {
361                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
362            }
363            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
364        }
365    }
366
367    /// Append variable-length strings to an existing chunked vlen string dataset.
368    pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
369        let full_name = if self.name == "/" {
370            name.to_string()
371        } else {
372            let trimmed = self.name.trim_start_matches('/');
373            format!("{}/{}", trimmed, name)
374        };
375
376        let inner = borrow_inner(&self.file_inner);
377        match &*inner {
378            H5FileInner::Writer(writer) => {
379                let ds_index = writer
380                    .dataset_index(&full_name)
381                    .ok_or_else(|| Hdf5Error::NotFound(full_name.clone()))?;
382                writer.append_vlen_strings(ds_index, strings)?;
383                Ok(())
384            }
385            H5FileInner::Reader(_) => {
386                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
387            }
388            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
389        }
390    }
391
392    /// Reopen a writer-mode handle to a dataset in this group by name.
393    ///
394    /// Mirrors [`H5File::dataset_writer`](crate::file::H5File::dataset_writer)
395    /// but resolves `name` relative to this group, so a dataset created here
396    /// (including via the vlen-string helpers) can be reopened to attach
397    /// attributes or append chunks. `name` is the link name within this group.
398    pub fn dataset_writer(&self, name: &str) -> Result<H5Dataset> {
399        let full_name = if self.name == "/" {
400            name.to_string()
401        } else {
402            let trimmed = self.name.trim_start_matches('/');
403            format!("{}/{}", trimmed, name)
404        };
405
406        let inner = borrow_inner(&self.file_inner);
407        match &*inner {
408            H5FileInner::Writer(writer) => {
409                let index = writer
410                    .dataset_index(&full_name)
411                    .ok_or_else(|| Hdf5Error::NotFound(full_name.clone()))?;
412                let (shape, element_size, chunked, btree2, fixed_array) =
413                    writer.dataset_handle_parts(index);
414                Ok(H5Dataset::new_writer(
415                    clone_inner(&self.file_inner),
416                    index,
417                    shape,
418                    element_size,
419                    chunked,
420                    btree2,
421                    fixed_array,
422                ))
423            }
424            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
425                "cannot open a dataset_writer in read mode; use dataset() instead".into(),
426            )),
427            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
428        }
429    }
430
431    /// List sub-group names that are direct children of this group.
432    pub fn group_names(&self) -> Result<Vec<String>> {
433        let prefix = if self.name == "/" {
434            String::new()
435        } else {
436            format!("{}/", self.name.trim_start_matches('/'))
437        };
438
439        let mut groups = std::collections::BTreeSet::new();
440        let inner = borrow_inner(&self.file_inner);
441        match &*inner {
442            // Read mode: list immediate child groups from the reader's
443            // actual group set (link records), so empty / attribute-only /
444            // subgroup-only child groups are included.
445            H5FileInner::Reader(reader) => {
446                for path in reader.group_paths() {
447                    let stripped = if prefix.is_empty() {
448                        path.as_str()
449                    } else if let Some(rest) = path.strip_prefix(&prefix) {
450                        rest
451                    } else {
452                        continue;
453                    };
454                    if stripped.is_empty() {
455                        continue;
456                    }
457                    // Immediate child only: take the first path component.
458                    let child = match stripped.find('/') {
459                        Some(pos) => &stripped[..pos],
460                        None => stripped,
461                    };
462                    groups.insert(child.to_string());
463                }
464            }
465            // Write mode: no link-record store; infer from dataset paths.
466            H5FileInner::Writer(writer) => {
467                for name in writer.dataset_names() {
468                    let stripped = if prefix.is_empty() {
469                        name.as_str()
470                    } else if let Some(rest) = name.strip_prefix(&prefix) {
471                        rest
472                    } else {
473                        continue;
474                    };
475                    if let Some(pos) = stripped.find('/') {
476                        groups.insert(stripped[..pos].to_string());
477                    }
478                }
479            }
480            H5FileInner::Closed => return Ok(vec![]),
481        }
482        Ok(groups.into_iter().collect())
483    }
484
485    /// Add (or replace) a string attribute on this group.
486    ///
487    /// This is the standard way to mark a NeXus class, e.g.
488    /// `grp.set_attr_string("NX_class", "NXdetector")`. The value is stored as
489    /// a variable-length UTF-8 string (read back as a Python `str` by h5py),
490    /// not a fixed-length string.
491    pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
492        let inner = borrow_inner(&self.file_inner);
493        match &*inner {
494            H5FileInner::Writer(writer) => {
495                writer.set_vlen_string_attribute(self.attr_target(), name, value)?;
496                Ok(())
497            }
498            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
499                "cannot write attributes in read mode".into(),
500            )),
501            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
502        }
503    }
504
505    /// Add (or replace) a numeric scalar attribute on this group.
506    pub fn set_attr_numeric<T: H5Type>(&self, name: &str, value: &T) -> Result<()> {
507        let es = T::element_size();
508        // Safety: `T: H5Type` is a `Copy` numeric primitive whose byte
509        // representation is exactly `element_size()` wide.
510        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
511        self.add_attr(AttributeMessage::scalar_numeric(
512            name,
513            T::hdf5_type(),
514            raw.to_vec(),
515        ))
516    }
517
518    /// Add (or replace) a numeric (or bool) **array** attribute on this group.
519    ///
520    /// The values are written as a 1-D HDF5 array attribute (simple dataspace
521    /// `[values.len()]`), read back by h5py as a numpy array — the array
522    /// counterpart of [`set_attr_numeric`](Self::set_attr_numeric). For a
523    /// multi-dimensional shape use
524    /// [`set_attr_array_numeric_nd`](Self::set_attr_array_numeric_nd).
525    pub fn set_attr_array_numeric<T: H5Type>(&self, name: &str, values: &[T]) -> Result<()> {
526        self.set_attr_array_numeric_nd(name, values, &[values.len()])
527    }
528
529    /// Add (or replace) a numeric (or bool) **N-dimensional array** attribute on
530    /// this group.
531    ///
532    /// `shape` gives the dataspace dimensions; `values` is the row-major data
533    /// and its length must equal the product of `shape` (an empty `shape` is a
534    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
535    /// of that shape. [`set_attr_array_numeric`](Self::set_attr_array_numeric)
536    /// is the 1-D convenience form.
537    pub fn set_attr_array_numeric_nd<T: H5Type>(
538        &self,
539        name: &str,
540        values: &[T],
541        shape: &[usize],
542    ) -> Result<()> {
543        let n: usize = shape.iter().product();
544        if values.len() != n {
545            return Err(Hdf5Error::InvalidState(format!(
546                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
547                values.len()
548            )));
549        }
550        let es = T::element_size();
551        // Safety: `T: H5Type` is a `Copy` POD numeric whose byte width is `es`.
552        let raw =
553            unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
554        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
555        self.add_attr(AttributeMessage::array_numeric(
556            name,
557            T::hdf5_type(),
558            &dims,
559            raw.to_vec(),
560        ))
561    }
562
563    /// Add (or replace) a variable-length UTF-8 string **array** attribute on
564    /// this group, read back by h5py as a 1-D array of `str` — the array
565    /// counterpart of [`set_attr_string`](Self::set_attr_string). For a
566    /// multi-dimensional shape use
567    /// [`set_attr_string_array_nd`](Self::set_attr_string_array_nd).
568    pub fn set_attr_string_array(&self, name: &str, values: &[&str]) -> Result<()> {
569        self.set_attr_string_array_nd(name, values, &[values.len()])
570    }
571
572    /// Add (or replace) a variable-length UTF-8 string **N-dimensional array**
573    /// attribute on this group.
574    ///
575    /// `shape` gives the dataspace dimensions; `values` is the row-major data
576    /// and its length must equal the product of `shape` (an empty `shape` is a
577    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
578    /// of Python `str` with that shape.
579    /// [`set_attr_string_array`](Self::set_attr_string_array) is the 1-D
580    /// convenience form.
581    pub fn set_attr_string_array_nd(
582        &self,
583        name: &str,
584        values: &[&str],
585        shape: &[usize],
586    ) -> Result<()> {
587        let n: usize = shape.iter().product();
588        if values.len() != n {
589            return Err(Hdf5Error::InvalidState(format!(
590                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
591                values.len()
592            )));
593        }
594        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
595        // The vlen array message needs `&mut writer` (global-heap allocation),
596        // so we route it the same way `set_attr_string` does rather than through
597        // `add_attr` (which re-borrows the writer).
598        let mut inner = borrow_inner_mut(&self.file_inner);
599        match &mut *inner {
600            H5FileInner::Writer(writer) => {
601                writer.set_vlen_string_array_attribute(self.attr_target(), name, values, &dims)?;
602                Ok(())
603            }
604            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
605                "cannot write attributes in read mode".into(),
606            )),
607            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
608        }
609    }
610
611    /// The writer-side attribute list this group's attributes live in: the
612    /// root group's is the file-level list, any other group's is its own.
613    fn attr_target(&self) -> crate::io::writer::AttrTarget<'_> {
614        if self.name == "/" {
615            crate::io::writer::AttrTarget::Root
616        } else {
617            crate::io::writer::AttrTarget::Group(&self.name)
618        }
619    }
620
621    /// Route an attribute to the writer: the root group goes to the
622    /// file-level attribute list, any other group to its own header.
623    fn add_attr(&self, attr: AttributeMessage) -> Result<()> {
624        let inner = borrow_inner(&self.file_inner);
625        match &*inner {
626            H5FileInner::Writer(writer) => {
627                writer.set_attribute(self.attr_target(), attr)?;
628                Ok(())
629            }
630            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
631                "cannot write attributes in read mode".into(),
632            )),
633            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
634        }
635    }
636
637    /// List this group's attribute names (read mode).
638    pub fn attr_names(&self) -> Result<Vec<String>> {
639        let inner = borrow_inner(&self.file_inner);
640        match &*inner {
641            H5FileInner::Reader(reader) => {
642                if self.name == "/" {
643                    Ok(reader.root_attr_names())
644                } else {
645                    Ok(reader.group_attr_names(self.name.trim_start_matches('/')))
646                }
647            }
648            _ => Err(Hdf5Error::InvalidState(
649                "attr_names is only available in read mode".into(),
650            )),
651        }
652    }
653
654    /// Read one of this group's attributes as a string (read mode).
655    pub fn attr_string(&self, name: &str) -> Result<String> {
656        let mut inner = borrow_inner_mut(&self.file_inner);
657        match &mut *inner {
658            H5FileInner::Reader(reader) => {
659                let attr = if self.name == "/" {
660                    reader.root_attr(name)
661                } else {
662                    reader.group_attr(self.name.trim_start_matches('/'), name)
663                }
664                .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
665                .clone();
666                Ok(reader.attr_string_value(&attr)?)
667            }
668            _ => Err(Hdf5Error::InvalidState(
669                "attr_string is only available in read mode".into(),
670            )),
671        }
672    }
673}