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    /// Open an existing sub-group by name (read mode).
89    pub fn group(&self, name: &str) -> Result<H5Group> {
90        let full_name = if self.name == "/" {
91            format!("/{}", name)
92        } else {
93            format!("{}/{}", self.name, name)
94        };
95
96        // Verify the group exists by consulting the reader's actual group
97        // set (derived from link records), not inferred dataset prefixes.
98        // This opens empty groups, attribute-only groups, and
99        // subgroup-only groups, which have no datasets beneath them.
100        let inner = borrow_inner(&self.file_inner);
101        if let H5FileInner::Reader(reader) = &*inner {
102            let group_path = full_name.trim_start_matches('/');
103            if !reader.has_group(group_path) {
104                return Err(Hdf5Error::NotFound(full_name));
105            }
106        }
107        drop(inner);
108
109        Ok(H5Group {
110            file_inner: clone_inner(&self.file_inner),
111            name: full_name,
112        })
113    }
114
115    /// List dataset names that are direct children of this group.
116    pub fn dataset_names(&self) -> Result<Vec<String>> {
117        let inner = borrow_inner(&self.file_inner);
118        let all_names = match &*inner {
119            H5FileInner::Reader(reader) => reader
120                .dataset_names()
121                .iter()
122                .map(|s| s.to_string())
123                .collect::<Vec<_>>(),
124            H5FileInner::Writer(writer) => writer
125                .dataset_names()
126                .iter()
127                .map(|s| s.to_string())
128                .collect::<Vec<_>>(),
129            H5FileInner::Closed => return Ok(vec![]),
130        };
131
132        let prefix = if self.name == "/" {
133            String::new()
134        } else {
135            format!("{}/", self.name.trim_start_matches('/'))
136        };
137
138        let mut result = Vec::new();
139        for name in &all_names {
140            let stripped = if prefix.is_empty() {
141                name.as_str()
142            } else if let Some(rest) = name.strip_prefix(&prefix) {
143                rest
144            } else {
145                continue;
146            };
147            // Only direct children (no further '/')
148            if !stripped.contains('/') {
149                result.push(stripped.to_string());
150            }
151        }
152        Ok(result)
153    }
154
155    /// Create a variable-length string dataset and write data within this group.
156    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
157        let full_name = if self.name == "/" {
158            name.to_string()
159        } else {
160            let trimmed = self.name.trim_start_matches('/');
161            format!("{}/{}", trimmed, name)
162        };
163
164        let mut inner = borrow_inner_mut(&self.file_inner);
165        match &mut *inner {
166            H5FileInner::Writer(writer) => {
167                let idx = writer.create_vlen_string_dataset(&full_name, strings)?;
168                if self.name != "/" {
169                    writer.assign_dataset_to_group(&self.name, idx)?;
170                }
171                Ok(())
172            }
173            H5FileInner::Reader(_) => {
174                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
175            }
176            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
177        }
178    }
179
180    /// Create a chunked, compressed variable-length string dataset within this group.
181    pub fn write_vlen_strings_compressed(
182        &self,
183        name: &str,
184        strings: &[&str],
185        chunk_size: usize,
186        pipeline: FilterPipeline,
187    ) -> Result<()> {
188        let full_name = if self.name == "/" {
189            name.to_string()
190        } else {
191            let trimmed = self.name.trim_start_matches('/');
192            format!("{}/{}", trimmed, name)
193        };
194
195        let mut inner = borrow_inner_mut(&self.file_inner);
196        match &mut *inner {
197            H5FileInner::Writer(writer) => {
198                let idx = writer.create_vlen_string_dataset_compressed(
199                    &full_name, strings, chunk_size, pipeline,
200                )?;
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 an empty chunked vlen string dataset ready for incremental appends.
214    pub fn create_appendable_vlen_dataset(
215        &self,
216        name: &str,
217        chunk_size: usize,
218        pipeline: Option<FilterPipeline>,
219    ) -> Result<()> {
220        let full_name = if self.name == "/" {
221            name.to_string()
222        } else {
223            let trimmed = self.name.trim_start_matches('/');
224            format!("{}/{}", trimmed, name)
225        };
226
227        let mut inner = borrow_inner_mut(&self.file_inner);
228        match &mut *inner {
229            H5FileInner::Writer(writer) => {
230                let idx = writer
231                    .create_appendable_vlen_string_dataset(&full_name, chunk_size, pipeline)?;
232                if self.name != "/" {
233                    writer.assign_dataset_to_group(&self.name, idx)?;
234                }
235                Ok(())
236            }
237            H5FileInner::Reader(_) => {
238                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
239            }
240            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
241        }
242    }
243
244    /// Append variable-length strings to an existing chunked vlen string dataset.
245    pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
246        let full_name = if self.name == "/" {
247            name.to_string()
248        } else {
249            let trimmed = self.name.trim_start_matches('/');
250            format!("{}/{}", trimmed, name)
251        };
252
253        let mut inner = borrow_inner_mut(&self.file_inner);
254        match &mut *inner {
255            H5FileInner::Writer(writer) => {
256                let ds_index = writer
257                    .dataset_index(&full_name)
258                    .ok_or_else(|| Hdf5Error::NotFound(full_name.clone()))?;
259                writer.append_vlen_strings(ds_index, strings)?;
260                Ok(())
261            }
262            H5FileInner::Reader(_) => {
263                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
264            }
265            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
266        }
267    }
268
269    /// List sub-group names that are direct children of this group.
270    pub fn group_names(&self) -> Result<Vec<String>> {
271        let prefix = if self.name == "/" {
272            String::new()
273        } else {
274            format!("{}/", self.name.trim_start_matches('/'))
275        };
276
277        let mut groups = std::collections::BTreeSet::new();
278        let inner = borrow_inner(&self.file_inner);
279        match &*inner {
280            // Read mode: list immediate child groups from the reader's
281            // actual group set (link records), so empty / attribute-only /
282            // subgroup-only child groups are included.
283            H5FileInner::Reader(reader) => {
284                for path in reader.group_paths() {
285                    let stripped = if prefix.is_empty() {
286                        path.as_str()
287                    } else if let Some(rest) = path.strip_prefix(&prefix) {
288                        rest
289                    } else {
290                        continue;
291                    };
292                    if stripped.is_empty() {
293                        continue;
294                    }
295                    // Immediate child only: take the first path component.
296                    let child = match stripped.find('/') {
297                        Some(pos) => &stripped[..pos],
298                        None => stripped,
299                    };
300                    groups.insert(child.to_string());
301                }
302            }
303            // Write mode: no link-record store; infer from dataset paths.
304            H5FileInner::Writer(writer) => {
305                for name in writer.dataset_names() {
306                    let stripped = if prefix.is_empty() {
307                        name
308                    } else if let Some(rest) = name.strip_prefix(&prefix) {
309                        rest
310                    } else {
311                        continue;
312                    };
313                    if let Some(pos) = stripped.find('/') {
314                        groups.insert(stripped[..pos].to_string());
315                    }
316                }
317            }
318            H5FileInner::Closed => return Ok(vec![]),
319        }
320        Ok(groups.into_iter().collect())
321    }
322
323    /// Add (or replace) a string attribute on this group.
324    ///
325    /// This is the standard way to mark a NeXus class, e.g.
326    /// `grp.set_attr_string("NX_class", "NXdetector")`.
327    pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
328        self.add_attr(AttributeMessage::scalar_string(name, value))
329    }
330
331    /// Add (or replace) a numeric scalar attribute on this group.
332    pub fn set_attr_numeric<T: H5Type>(&self, name: &str, value: &T) -> Result<()> {
333        let es = T::element_size();
334        // Safety: `T: H5Type` is a `Copy` numeric primitive whose byte
335        // representation is exactly `element_size()` wide.
336        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
337        self.add_attr(AttributeMessage::scalar_numeric(
338            name,
339            T::hdf5_type(),
340            raw.to_vec(),
341        ))
342    }
343
344    /// Route an attribute to the writer: the root group goes to the
345    /// file-level attribute list, any other group to its own header.
346    fn add_attr(&self, attr: AttributeMessage) -> Result<()> {
347        let mut inner = borrow_inner_mut(&self.file_inner);
348        match &mut *inner {
349            H5FileInner::Writer(writer) => {
350                if self.name == "/" {
351                    writer.add_root_attribute(attr);
352                } else {
353                    writer.add_group_attribute(&self.name, attr)?;
354                }
355                Ok(())
356            }
357            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
358                "cannot write attributes in read mode".into(),
359            )),
360            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
361        }
362    }
363
364    /// List this group's attribute names (read mode).
365    pub fn attr_names(&self) -> Result<Vec<String>> {
366        let inner = borrow_inner(&self.file_inner);
367        match &*inner {
368            H5FileInner::Reader(reader) => {
369                if self.name == "/" {
370                    Ok(reader.root_attr_names())
371                } else {
372                    Ok(reader.group_attr_names(self.name.trim_start_matches('/')))
373                }
374            }
375            _ => Err(Hdf5Error::InvalidState(
376                "attr_names is only available in read mode".into(),
377            )),
378        }
379    }
380
381    /// Read one of this group's attributes as a string (read mode).
382    pub fn attr_string(&self, name: &str) -> Result<String> {
383        let mut inner = borrow_inner_mut(&self.file_inner);
384        match &mut *inner {
385            H5FileInner::Reader(reader) => {
386                let attr = if self.name == "/" {
387                    reader.root_attr(name)
388                } else {
389                    reader.group_attr(self.name.trim_start_matches('/'), name)
390                }
391                .ok_or_else(|| Hdf5Error::NotFound(name.to_string()))?
392                .clone();
393                Ok(reader.attr_string_value(&attr)?)
394            }
395            _ => Err(Hdf5Error::InvalidState(
396                "attr_string is only available in read mode".into(),
397            )),
398        }
399    }
400}