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).
520    pub fn set_attr_array_numeric<T: H5Type>(&self, name: &str, values: &[T]) -> Result<()> {
521        let es = T::element_size();
522        // Safety: `T: H5Type` is a `Copy` POD numeric whose byte width is `es`.
523        let raw =
524            unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
525        let dims = [values.len() as u64];
526        self.add_attr(AttributeMessage::array_numeric(
527            name,
528            T::hdf5_type(),
529            &dims,
530            raw.to_vec(),
531        ))
532    }
533
534    /// Add (or replace) a variable-length UTF-8 string **array** attribute on
535    /// this group, read back by h5py as a 1-D array of `str` — the array
536    /// counterpart of [`set_attr_string`](Self::set_attr_string).
537    pub fn set_attr_string_array(&self, name: &str, values: &[&str]) -> Result<()> {
538        // The vlen array message needs `&mut writer` (global-heap allocation),
539        // so we route it the same way `set_attr_string` does rather than through
540        // `add_attr` (which re-borrows the writer).
541        let mut inner = borrow_inner_mut(&self.file_inner);
542        match &mut *inner {
543            H5FileInner::Writer(writer) => {
544                let attr = writer.vlen_string_array_attribute(name, values)?;
545                if self.name == "/" {
546                    writer.add_root_attribute(attr);
547                } else {
548                    writer.add_group_attribute(&self.name, attr)?;
549                }
550                Ok(())
551            }
552            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
553                "cannot write attributes in read mode".into(),
554            )),
555            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
556        }
557    }
558
559    /// Route an attribute to the writer: the root group goes to the
560    /// file-level attribute list, any other group to its own header.
561    fn add_attr(&self, attr: AttributeMessage) -> Result<()> {
562        let inner = borrow_inner(&self.file_inner);
563        match &*inner {
564            H5FileInner::Writer(writer) => {
565                if self.name == "/" {
566                    writer.add_root_attribute(attr);
567                } else {
568                    writer.add_group_attribute(&self.name, attr)?;
569                }
570                Ok(())
571            }
572            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
573                "cannot write attributes in read mode".into(),
574            )),
575            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
576        }
577    }
578
579    /// List this group's attribute names (read mode).
580    pub fn attr_names(&self) -> Result<Vec<String>> {
581        let inner = borrow_inner(&self.file_inner);
582        match &*inner {
583            H5FileInner::Reader(reader) => {
584                if self.name == "/" {
585                    Ok(reader.root_attr_names())
586                } else {
587                    Ok(reader.group_attr_names(self.name.trim_start_matches('/')))
588                }
589            }
590            _ => Err(Hdf5Error::InvalidState(
591                "attr_names is only available in read mode".into(),
592            )),
593        }
594    }
595
596    /// Read one of this group's attributes as a string (read mode).
597    pub fn attr_string(&self, name: &str) -> Result<String> {
598        let mut inner = borrow_inner_mut(&self.file_inner);
599        match &mut *inner {
600            H5FileInner::Reader(reader) => {
601                let attr = if self.name == "/" {
602                    reader.root_attr(name)
603                } else {
604                    reader.group_attr(self.name.trim_start_matches('/'), name)
605                }
606                .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
607                .clone();
608                Ok(reader.attr_string_value(&attr)?)
609            }
610            _ => Err(Hdf5Error::InvalidState(
611                "attr_string is only available in read mode".into(),
612            )),
613        }
614    }
615}