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        if let H5FileInner::Reader(reader) = &*inner {
135            let group_path = full_name.trim_start_matches('/');
136            if !reader.has_group(group_path) {
137                return Err(Hdf5Error::NotFound(full_name));
138            }
139        }
140        drop(inner);
141
142        Ok(H5Group {
143            file_inner: clone_inner(&self.file_inner),
144            name: full_name,
145        })
146    }
147
148    /// List dataset names that are direct children of this group.
149    pub fn dataset_names(&self) -> Result<Vec<String>> {
150        let inner = borrow_inner(&self.file_inner);
151        let all_names = match &*inner {
152            H5FileInner::Reader(reader) => reader
153                .dataset_names()
154                .iter()
155                .map(|s| s.to_string())
156                .collect::<Vec<_>>(),
157            H5FileInner::Writer(writer) => writer
158                .dataset_names()
159                .iter()
160                .map(|s| s.to_string())
161                .collect::<Vec<_>>(),
162            H5FileInner::Closed => return Ok(vec![]),
163        };
164
165        let prefix = if self.name == "/" {
166            String::new()
167        } else {
168            format!("{}/", self.name.trim_start_matches('/'))
169        };
170
171        let mut result = Vec::new();
172        for name in &all_names {
173            let stripped = if prefix.is_empty() {
174                name.as_str()
175            } else if let Some(rest) = name.strip_prefix(&prefix) {
176                rest
177            } else {
178                continue;
179            };
180            // Only direct children (no further '/')
181            if !stripped.contains('/') {
182                result.push(stripped.to_string());
183            }
184        }
185        Ok(result)
186    }
187
188    /// Create a variable-length string dataset and write data within this group.
189    ///
190    /// Returns a writer-mode handle to the created dataset so attributes can be
191    /// attached to it (e.g. units, descriptions) just like a dataset created
192    /// via [`new_dataset`](Self::new_dataset).
193    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
194        let full_name = if self.name == "/" {
195            name.to_string()
196        } else {
197            let trimmed = self.name.trim_start_matches('/');
198            format!("{}/{}", trimmed, name)
199        };
200
201        let inner = borrow_inner(&self.file_inner);
202        match &*inner {
203            H5FileInner::Writer(writer) => {
204                let idx = writer.create_vlen_string_dataset(&full_name, strings)?;
205                if self.name != "/" {
206                    writer.assign_dataset_to_group(&self.name, idx)?;
207                }
208                let (shape, element_size, chunked, btree2, fixed_array) =
209                    writer.dataset_handle_parts(idx);
210                Ok(H5Dataset::new_writer(
211                    clone_inner(&self.file_inner),
212                    idx,
213                    shape,
214                    element_size,
215                    chunked,
216                    btree2,
217                    fixed_array,
218                ))
219            }
220            H5FileInner::Reader(_) => {
221                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
222            }
223            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
224        }
225    }
226
227    /// Create a variable-length byte-array dataset and write data within this
228    /// group.
229    ///
230    /// Each `&[u8]` becomes one element of variable length, stored as a vlen
231    /// sequence of `u8`. h5py reads it back as an array of `uint8` arrays.
232    /// Returns a writer-mode handle so attributes can be attached, like
233    /// [`write_vlen_strings`](Self::write_vlen_strings).
234    pub fn write_vlen_bytes(&self, name: &str, items: &[&[u8]]) -> Result<H5Dataset> {
235        let full_name = if self.name == "/" {
236            name.to_string()
237        } else {
238            let trimmed = self.name.trim_start_matches('/');
239            format!("{}/{}", trimmed, name)
240        };
241
242        let inner = borrow_inner(&self.file_inner);
243        match &*inner {
244            H5FileInner::Writer(writer) => {
245                let idx = writer.create_vlen_bytes_dataset(&full_name, items)?;
246                if self.name != "/" {
247                    writer.assign_dataset_to_group(&self.name, idx)?;
248                }
249                let (shape, element_size, chunked, btree2, fixed_array) =
250                    writer.dataset_handle_parts(idx);
251                Ok(H5Dataset::new_writer(
252                    clone_inner(&self.file_inner),
253                    idx,
254                    shape,
255                    element_size,
256                    chunked,
257                    btree2,
258                    fixed_array,
259                ))
260            }
261            H5FileInner::Reader(_) => {
262                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
263            }
264            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
265        }
266    }
267
268    /// Create a chunked, compressed variable-length string dataset within this group.
269    ///
270    /// Returns a writer-mode handle to the created dataset so attributes can be
271    /// attached to it, like [`write_vlen_strings`](Self::write_vlen_strings).
272    pub fn write_vlen_strings_compressed(
273        &self,
274        name: &str,
275        strings: &[&str],
276        chunk_size: usize,
277        pipeline: FilterPipeline,
278    ) -> Result<H5Dataset> {
279        let full_name = if self.name == "/" {
280            name.to_string()
281        } else {
282            let trimmed = self.name.trim_start_matches('/');
283            format!("{}/{}", trimmed, name)
284        };
285
286        let inner = borrow_inner(&self.file_inner);
287        match &*inner {
288            H5FileInner::Writer(writer) => {
289                let idx = writer.create_vlen_string_dataset_compressed(
290                    &full_name, strings, chunk_size, pipeline,
291                )?;
292                if self.name != "/" {
293                    writer.assign_dataset_to_group(&self.name, idx)?;
294                }
295                let (shape, element_size, chunked, btree2, fixed_array) =
296                    writer.dataset_handle_parts(idx);
297                Ok(H5Dataset::new_writer(
298                    clone_inner(&self.file_inner),
299                    idx,
300                    shape,
301                    element_size,
302                    chunked,
303                    btree2,
304                    fixed_array,
305                ))
306            }
307            H5FileInner::Reader(_) => {
308                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
309            }
310            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
311        }
312    }
313
314    /// Create an empty chunked vlen string dataset ready for incremental appends.
315    ///
316    /// Returns a writer-mode handle to the created dataset so attributes can be
317    /// attached before or between [`append_vlen_strings`](Self::append_vlen_strings)
318    /// calls.
319    pub fn create_appendable_vlen_dataset(
320        &self,
321        name: &str,
322        chunk_size: usize,
323        pipeline: Option<FilterPipeline>,
324    ) -> Result<H5Dataset> {
325        let full_name = if self.name == "/" {
326            name.to_string()
327        } else {
328            let trimmed = self.name.trim_start_matches('/');
329            format!("{}/{}", trimmed, name)
330        };
331
332        let inner = borrow_inner(&self.file_inner);
333        match &*inner {
334            H5FileInner::Writer(writer) => {
335                let idx = writer
336                    .create_appendable_vlen_string_dataset(&full_name, chunk_size, pipeline)?;
337                if self.name != "/" {
338                    writer.assign_dataset_to_group(&self.name, idx)?;
339                }
340                let (shape, element_size, chunked, btree2, fixed_array) =
341                    writer.dataset_handle_parts(idx);
342                Ok(H5Dataset::new_writer(
343                    clone_inner(&self.file_inner),
344                    idx,
345                    shape,
346                    element_size,
347                    chunked,
348                    btree2,
349                    fixed_array,
350                ))
351            }
352            H5FileInner::Reader(_) => {
353                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
354            }
355            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
356        }
357    }
358
359    /// Append variable-length strings to an existing chunked vlen string dataset.
360    pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
361        let full_name = if self.name == "/" {
362            name.to_string()
363        } else {
364            let trimmed = self.name.trim_start_matches('/');
365            format!("{}/{}", trimmed, name)
366        };
367
368        let inner = borrow_inner(&self.file_inner);
369        match &*inner {
370            H5FileInner::Writer(writer) => {
371                let ds_index = writer
372                    .dataset_index(&full_name)
373                    .ok_or_else(|| Hdf5Error::NotFound(full_name.clone()))?;
374                writer.append_vlen_strings(ds_index, strings)?;
375                Ok(())
376            }
377            H5FileInner::Reader(_) => {
378                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
379            }
380            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
381        }
382    }
383
384    /// Reopen a writer-mode handle to a dataset in this group by name.
385    ///
386    /// Mirrors [`H5File::dataset_writer`](crate::file::H5File::dataset_writer)
387    /// but resolves `name` relative to this group, so a dataset created here
388    /// (including via the vlen-string helpers) can be reopened to attach
389    /// attributes or append chunks. `name` is the link name within this group.
390    pub fn dataset_writer(&self, name: &str) -> Result<H5Dataset> {
391        let full_name = if self.name == "/" {
392            name.to_string()
393        } else {
394            let trimmed = self.name.trim_start_matches('/');
395            format!("{}/{}", trimmed, name)
396        };
397
398        let inner = borrow_inner(&self.file_inner);
399        match &*inner {
400            H5FileInner::Writer(writer) => {
401                let index = writer
402                    .dataset_index(&full_name)
403                    .ok_or_else(|| Hdf5Error::NotFound(full_name.clone()))?;
404                let (shape, element_size, chunked, btree2, fixed_array) =
405                    writer.dataset_handle_parts(index);
406                Ok(H5Dataset::new_writer(
407                    clone_inner(&self.file_inner),
408                    index,
409                    shape,
410                    element_size,
411                    chunked,
412                    btree2,
413                    fixed_array,
414                ))
415            }
416            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
417                "cannot open a dataset_writer in read mode; use dataset() instead".into(),
418            )),
419            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
420        }
421    }
422
423    /// List sub-group names that are direct children of this group.
424    pub fn group_names(&self) -> Result<Vec<String>> {
425        let prefix = if self.name == "/" {
426            String::new()
427        } else {
428            format!("{}/", self.name.trim_start_matches('/'))
429        };
430
431        let mut groups = std::collections::BTreeSet::new();
432        let inner = borrow_inner(&self.file_inner);
433        match &*inner {
434            // Read mode: list immediate child groups from the reader's
435            // actual group set (link records), so empty / attribute-only /
436            // subgroup-only child groups are included.
437            H5FileInner::Reader(reader) => {
438                for path in reader.group_paths() {
439                    let stripped = if prefix.is_empty() {
440                        path.as_str()
441                    } else if let Some(rest) = path.strip_prefix(&prefix) {
442                        rest
443                    } else {
444                        continue;
445                    };
446                    if stripped.is_empty() {
447                        continue;
448                    }
449                    // Immediate child only: take the first path component.
450                    let child = match stripped.find('/') {
451                        Some(pos) => &stripped[..pos],
452                        None => stripped,
453                    };
454                    groups.insert(child.to_string());
455                }
456            }
457            // Write mode: no link-record store; infer from dataset paths.
458            H5FileInner::Writer(writer) => {
459                for name in writer.dataset_names() {
460                    let stripped = if prefix.is_empty() {
461                        name.as_str()
462                    } else if let Some(rest) = name.strip_prefix(&prefix) {
463                        rest
464                    } else {
465                        continue;
466                    };
467                    if let Some(pos) = stripped.find('/') {
468                        groups.insert(stripped[..pos].to_string());
469                    }
470                }
471            }
472            H5FileInner::Closed => return Ok(vec![]),
473        }
474        Ok(groups.into_iter().collect())
475    }
476
477    /// Add (or replace) a string attribute on this group.
478    ///
479    /// This is the standard way to mark a NeXus class, e.g.
480    /// `grp.set_attr_string("NX_class", "NXdetector")`. The value is stored as
481    /// a variable-length UTF-8 string (read back as a Python `str` by h5py),
482    /// not a fixed-length string.
483    pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
484        let inner = borrow_inner(&self.file_inner);
485        match &*inner {
486            H5FileInner::Writer(writer) => {
487                let attr = writer.vlen_string_attribute(name, value)?;
488                if self.name == "/" {
489                    writer.add_root_attribute(attr);
490                } else {
491                    writer.add_group_attribute(&self.name, attr)?;
492                }
493                Ok(())
494            }
495            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
496                "cannot write attributes in read mode".into(),
497            )),
498            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
499        }
500    }
501
502    /// Add (or replace) a numeric scalar attribute on this group.
503    pub fn set_attr_numeric<T: H5Type>(&self, name: &str, value: &T) -> Result<()> {
504        let es = T::element_size();
505        // Safety: `T: H5Type` is a `Copy` numeric primitive whose byte
506        // representation is exactly `element_size()` wide.
507        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
508        self.add_attr(AttributeMessage::scalar_numeric(
509            name,
510            T::hdf5_type(),
511            raw.to_vec(),
512        ))
513    }
514
515    /// Add (or replace) a numeric (or bool) **array** attribute on this group.
516    ///
517    /// The values are written as a 1-D HDF5 array attribute (simple dataspace
518    /// `[values.len()]`), read back by h5py as a numpy array — the array
519    /// counterpart of [`set_attr_numeric`](Self::set_attr_numeric). For a
520    /// multi-dimensional shape use
521    /// [`set_attr_array_numeric_nd`](Self::set_attr_array_numeric_nd).
522    pub fn set_attr_array_numeric<T: H5Type>(&self, name: &str, values: &[T]) -> Result<()> {
523        self.set_attr_array_numeric_nd(name, values, &[values.len()])
524    }
525
526    /// Add (or replace) a numeric (or bool) **N-dimensional array** attribute on
527    /// this group.
528    ///
529    /// `shape` gives the dataspace dimensions; `values` is the row-major data
530    /// and its length must equal the product of `shape` (an empty `shape` is a
531    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
532    /// of that shape. [`set_attr_array_numeric`](Self::set_attr_array_numeric)
533    /// is the 1-D convenience form.
534    pub fn set_attr_array_numeric_nd<T: H5Type>(
535        &self,
536        name: &str,
537        values: &[T],
538        shape: &[usize],
539    ) -> Result<()> {
540        let n: usize = shape.iter().product();
541        if values.len() != n {
542            return Err(Hdf5Error::InvalidState(format!(
543                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
544                values.len()
545            )));
546        }
547        let es = T::element_size();
548        // Safety: `T: H5Type` is a `Copy` POD numeric whose byte width is `es`.
549        let raw =
550            unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
551        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
552        self.add_attr(AttributeMessage::array_numeric(
553            name,
554            T::hdf5_type(),
555            &dims,
556            raw.to_vec(),
557        ))
558    }
559
560    /// Add (or replace) a variable-length UTF-8 string **array** attribute on
561    /// this group, read back by h5py as a 1-D array of `str` — the array
562    /// counterpart of [`set_attr_string`](Self::set_attr_string). For a
563    /// multi-dimensional shape use
564    /// [`set_attr_string_array_nd`](Self::set_attr_string_array_nd).
565    pub fn set_attr_string_array(&self, name: &str, values: &[&str]) -> Result<()> {
566        self.set_attr_string_array_nd(name, values, &[values.len()])
567    }
568
569    /// Add (or replace) a variable-length UTF-8 string **N-dimensional array**
570    /// attribute on this group.
571    ///
572    /// `shape` gives the dataspace dimensions; `values` is the row-major data
573    /// and its length must equal the product of `shape` (an empty `shape` is a
574    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
575    /// of Python `str` with that shape.
576    /// [`set_attr_string_array`](Self::set_attr_string_array) is the 1-D
577    /// convenience form.
578    pub fn set_attr_string_array_nd(
579        &self,
580        name: &str,
581        values: &[&str],
582        shape: &[usize],
583    ) -> Result<()> {
584        let n: usize = shape.iter().product();
585        if values.len() != n {
586            return Err(Hdf5Error::InvalidState(format!(
587                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
588                values.len()
589            )));
590        }
591        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
592        // The vlen array message needs `&mut writer` (global-heap allocation),
593        // so we route it the same way `set_attr_string` does rather than through
594        // `add_attr` (which re-borrows the writer).
595        let mut inner = borrow_inner_mut(&self.file_inner);
596        match &mut *inner {
597            H5FileInner::Writer(writer) => {
598                let attr = writer.vlen_string_array_attribute(name, values, &dims)?;
599                if self.name == "/" {
600                    writer.add_root_attribute(attr);
601                } else {
602                    writer.add_group_attribute(&self.name, attr)?;
603                }
604                Ok(())
605            }
606            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
607                "cannot write attributes in read mode".into(),
608            )),
609            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
610        }
611    }
612
613    /// Route an attribute to the writer: the root group goes to the
614    /// file-level attribute list, any other group to its own header.
615    fn add_attr(&self, attr: AttributeMessage) -> Result<()> {
616        let inner = borrow_inner(&self.file_inner);
617        match &*inner {
618            H5FileInner::Writer(writer) => {
619                if self.name == "/" {
620                    writer.add_root_attribute(attr);
621                } else {
622                    writer.add_group_attribute(&self.name, attr)?;
623                }
624                Ok(())
625            }
626            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
627                "cannot write attributes in read mode".into(),
628            )),
629            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
630        }
631    }
632
633    /// List this group's attribute names (read mode).
634    pub fn attr_names(&self) -> Result<Vec<String>> {
635        let inner = borrow_inner(&self.file_inner);
636        match &*inner {
637            H5FileInner::Reader(reader) => {
638                if self.name == "/" {
639                    Ok(reader.root_attr_names())
640                } else {
641                    Ok(reader.group_attr_names(self.name.trim_start_matches('/')))
642                }
643            }
644            _ => Err(Hdf5Error::InvalidState(
645                "attr_names is only available in read mode".into(),
646            )),
647        }
648    }
649
650    /// Read one of this group's attributes as a string (read mode).
651    pub fn attr_string(&self, name: &str) -> Result<String> {
652        let mut inner = borrow_inner_mut(&self.file_inner);
653        match &mut *inner {
654            H5FileInner::Reader(reader) => {
655                let attr = if self.name == "/" {
656                    reader.root_attr(name)
657                } else {
658                    reader.group_attr(self.name.trim_start_matches('/'), name)
659                }
660                .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
661                .clone();
662                Ok(reader.attr_string_value(&attr)?)
663            }
664            _ => Err(Hdf5Error::InvalidState(
665                "attr_string is only available in read mode".into(),
666            )),
667        }
668    }
669}