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;
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 mut inner = borrow_inner_mut(&self.file_inner);
67        match &mut *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 mut inner = borrow_inner_mut(&self.file_inner);
109        match &mut *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    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
190        let full_name = if self.name == "/" {
191            name.to_string()
192        } else {
193            let trimmed = self.name.trim_start_matches('/');
194            format!("{}/{}", trimmed, name)
195        };
196
197        let mut inner = borrow_inner_mut(&self.file_inner);
198        match &mut *inner {
199            H5FileInner::Writer(writer) => {
200                let idx = writer.create_vlen_string_dataset(&full_name, strings)?;
201                if self.name != "/" {
202                    writer.assign_dataset_to_group(&self.name, idx)?;
203                }
204                Ok(())
205            }
206            H5FileInner::Reader(_) => {
207                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
208            }
209            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
210        }
211    }
212
213    /// Create a chunked, compressed variable-length string dataset within this group.
214    pub fn write_vlen_strings_compressed(
215        &self,
216        name: &str,
217        strings: &[&str],
218        chunk_size: usize,
219        pipeline: FilterPipeline,
220    ) -> Result<()> {
221        let full_name = if self.name == "/" {
222            name.to_string()
223        } else {
224            let trimmed = self.name.trim_start_matches('/');
225            format!("{}/{}", trimmed, name)
226        };
227
228        let mut inner = borrow_inner_mut(&self.file_inner);
229        match &mut *inner {
230            H5FileInner::Writer(writer) => {
231                let idx = writer.create_vlen_string_dataset_compressed(
232                    &full_name, strings, chunk_size, pipeline,
233                )?;
234                if self.name != "/" {
235                    writer.assign_dataset_to_group(&self.name, idx)?;
236                }
237                Ok(())
238            }
239            H5FileInner::Reader(_) => {
240                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
241            }
242            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
243        }
244    }
245
246    /// Create an empty chunked vlen string dataset ready for incremental appends.
247    pub fn create_appendable_vlen_dataset(
248        &self,
249        name: &str,
250        chunk_size: usize,
251        pipeline: Option<FilterPipeline>,
252    ) -> Result<()> {
253        let full_name = if self.name == "/" {
254            name.to_string()
255        } else {
256            let trimmed = self.name.trim_start_matches('/');
257            format!("{}/{}", trimmed, name)
258        };
259
260        let mut inner = borrow_inner_mut(&self.file_inner);
261        match &mut *inner {
262            H5FileInner::Writer(writer) => {
263                let idx = writer
264                    .create_appendable_vlen_string_dataset(&full_name, chunk_size, pipeline)?;
265                if self.name != "/" {
266                    writer.assign_dataset_to_group(&self.name, idx)?;
267                }
268                Ok(())
269            }
270            H5FileInner::Reader(_) => {
271                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
272            }
273            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
274        }
275    }
276
277    /// Append variable-length strings to an existing chunked vlen string dataset.
278    pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
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 mut inner = borrow_inner_mut(&self.file_inner);
287        match &mut *inner {
288            H5FileInner::Writer(writer) => {
289                let ds_index = writer
290                    .dataset_index(&full_name)
291                    .ok_or_else(|| Hdf5Error::NotFound(full_name.clone()))?;
292                writer.append_vlen_strings(ds_index, strings)?;
293                Ok(())
294            }
295            H5FileInner::Reader(_) => {
296                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
297            }
298            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
299        }
300    }
301
302    /// List sub-group names that are direct children of this group.
303    pub fn group_names(&self) -> Result<Vec<String>> {
304        let prefix = if self.name == "/" {
305            String::new()
306        } else {
307            format!("{}/", self.name.trim_start_matches('/'))
308        };
309
310        let mut groups = std::collections::BTreeSet::new();
311        let inner = borrow_inner(&self.file_inner);
312        match &*inner {
313            // Read mode: list immediate child groups from the reader's
314            // actual group set (link records), so empty / attribute-only /
315            // subgroup-only child groups are included.
316            H5FileInner::Reader(reader) => {
317                for path in reader.group_paths() {
318                    let stripped = if prefix.is_empty() {
319                        path.as_str()
320                    } else if let Some(rest) = path.strip_prefix(&prefix) {
321                        rest
322                    } else {
323                        continue;
324                    };
325                    if stripped.is_empty() {
326                        continue;
327                    }
328                    // Immediate child only: take the first path component.
329                    let child = match stripped.find('/') {
330                        Some(pos) => &stripped[..pos],
331                        None => stripped,
332                    };
333                    groups.insert(child.to_string());
334                }
335            }
336            // Write mode: no link-record store; infer from dataset paths.
337            H5FileInner::Writer(writer) => {
338                for name in writer.dataset_names() {
339                    let stripped = if prefix.is_empty() {
340                        name
341                    } else if let Some(rest) = name.strip_prefix(&prefix) {
342                        rest
343                    } else {
344                        continue;
345                    };
346                    if let Some(pos) = stripped.find('/') {
347                        groups.insert(stripped[..pos].to_string());
348                    }
349                }
350            }
351            H5FileInner::Closed => return Ok(vec![]),
352        }
353        Ok(groups.into_iter().collect())
354    }
355
356    /// Add (or replace) a string attribute on this group.
357    ///
358    /// This is the standard way to mark a NeXus class, e.g.
359    /// `grp.set_attr_string("NX_class", "NXdetector")`.
360    pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
361        self.add_attr(AttributeMessage::scalar_string(name, value))
362    }
363
364    /// Add (or replace) a numeric scalar attribute on this group.
365    pub fn set_attr_numeric<T: H5Type>(&self, name: &str, value: &T) -> Result<()> {
366        let es = T::element_size();
367        // Safety: `T: H5Type` is a `Copy` numeric primitive whose byte
368        // representation is exactly `element_size()` wide.
369        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
370        self.add_attr(AttributeMessage::scalar_numeric(
371            name,
372            T::hdf5_type(),
373            raw.to_vec(),
374        ))
375    }
376
377    /// Route an attribute to the writer: the root group goes to the
378    /// file-level attribute list, any other group to its own header.
379    fn add_attr(&self, attr: AttributeMessage) -> Result<()> {
380        let mut inner = borrow_inner_mut(&self.file_inner);
381        match &mut *inner {
382            H5FileInner::Writer(writer) => {
383                if self.name == "/" {
384                    writer.add_root_attribute(attr);
385                } else {
386                    writer.add_group_attribute(&self.name, attr)?;
387                }
388                Ok(())
389            }
390            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
391                "cannot write attributes in read mode".into(),
392            )),
393            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
394        }
395    }
396
397    /// List this group's attribute names (read mode).
398    pub fn attr_names(&self) -> Result<Vec<String>> {
399        let inner = borrow_inner(&self.file_inner);
400        match &*inner {
401            H5FileInner::Reader(reader) => {
402                if self.name == "/" {
403                    Ok(reader.root_attr_names())
404                } else {
405                    Ok(reader.group_attr_names(self.name.trim_start_matches('/')))
406                }
407            }
408            _ => Err(Hdf5Error::InvalidState(
409                "attr_names is only available in read mode".into(),
410            )),
411        }
412    }
413
414    /// Read one of this group's attributes as a string (read mode).
415    pub fn attr_string(&self, name: &str) -> Result<String> {
416        let mut inner = borrow_inner_mut(&self.file_inner);
417        match &mut *inner {
418            H5FileInner::Reader(reader) => {
419                let attr = if self.name == "/" {
420                    reader.root_attr(name)
421                } else {
422                    reader.group_attr(self.name.trim_start_matches('/'), name)
423                }
424                .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
425                .clone();
426                Ok(reader.attr_string_value(&attr)?)
427            }
428            _ => Err(Hdf5Error::InvalidState(
429                "attr_string is only available in read mode".into(),
430            )),
431        }
432    }
433}