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::{DatasetAccess, DatasetBuilder, H5Dataset};
21use crate::error::{Hdf5Error, Result};
22use crate::file::{borrow_inner, borrow_inner_mut, clone_inner, H5FileInner, SharedInner};
23use crate::format::creation_order::CreationOrder;
24use crate::format::messages::attribute::AttributeMessage;
25use crate::format::messages::filter::FilterPipeline;
26use crate::format::messages::link::LinkTarget;
27use crate::format::storage_kind::{AttributeStorage, LinkStorage};
28use crate::io::reader::LinkClass;
29use crate::types::H5Type;
30
31/// A handle to an HDF5 group.
32///
33/// Groups are containers for datasets and other groups. The root group
34/// is always available via [`H5File::root_group`](crate::file::H5File::root_group).
35pub struct H5Group {
36    file_inner: SharedInner,
37    /// The absolute path of this group (e.g., "/" or "/detector").
38    name: String,
39}
40
41impl H5Group {
42    /// Create a new group handle.
43    pub(crate) fn new(file_inner: SharedInner, name: String) -> Self {
44        Self { file_inner, name }
45    }
46
47    /// Return the name (path) of this group.
48    pub fn name(&self) -> &str {
49        &self.name
50    }
51
52    /// Start building a new dataset in this group.
53    ///
54    /// The dataset will be registered as a child of this group in the
55    /// HDF5 file hierarchy.
56    pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
57        DatasetBuilder::new_in_group(clone_inner(&self.file_inner), self.name.clone())
58    }
59
60    /// Create a sub-group within this group.
61    ///
62    /// Creates a real HDF5 group with its own object header.
63    pub fn create_group(&self, name: &str) -> Result<H5Group> {
64        let full_name = if self.name == "/" {
65            format!("/{}", name)
66        } else {
67            format!("{}/{}", self.name, name)
68        };
69
70        let inner = borrow_inner(&self.file_inner);
71        match &*inner {
72            H5FileInner::Writer(writer) => {
73                writer.create_group(&self.name, name)?;
74            }
75            H5FileInner::Reader(_) => {
76                return Err(Hdf5Error::InvalidState(
77                    "cannot create groups in read mode".into(),
78                ));
79            }
80            H5FileInner::Closed => {
81                return Err(Hdf5Error::InvalidState("file is closed".into()));
82            }
83        }
84        drop(inner);
85
86        Ok(H5Group {
87            file_inner: clone_inner(&self.file_inner),
88            name: full_name,
89        })
90    }
91
92    /// Create a hard link in this group: an additional name `link_name`
93    /// for the object that already exists at `target_path`.
94    ///
95    /// No data is copied — the link and its target share one object, just
96    /// as `h5py` / libhdf5 hard links do. `target_path` may be given with
97    /// or without a leading `/` and must name an existing dataset or group.
98    /// This is the NeXus-style way to expose a dataset at a second
99    /// canonical location (e.g. `/entry/data/data`) without duplicating it.
100    ///
101    /// ```no_run
102    /// use rust_hdf5::H5File;
103    ///
104    /// let file = H5File::create("nexus.h5").unwrap();
105    /// let inst = file.root_group().create_group("instrument").unwrap();
106    /// inst.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
107    /// let data = file.root_group().create_group("data").unwrap();
108    /// // /data/data is now a hard link to /instrument/data — no copy.
109    /// data.link("data", "/instrument/data").unwrap();
110    /// ```
111    pub fn link(&self, link_name: &str, target_path: &str) -> Result<()> {
112        let inner = borrow_inner(&self.file_inner);
113        match &*inner {
114            H5FileInner::Writer(writer) => {
115                writer.create_hard_link(&self.name, link_name, target_path)?;
116                Ok(())
117            }
118            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
119                "cannot create hard links in read mode".into(),
120            )),
121            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
122        }
123    }
124
125    /// Create a soft link — `H5Lcreate_soft`, h5py's `h5py.SoftLink`.
126    ///
127    /// The link stores `target_path` as text and HDF5 resolves it on every
128    /// traversal, so it may name an object that does not exist yet, or one
129    /// that never will: unlike [`link`](Self::link), nothing is checked here
130    /// and a dangling soft link is a legal file.
131    ///
132    /// ```no_run
133    /// use rust_hdf5::H5File;
134    ///
135    /// let file = H5File::create("soft.h5").unwrap();
136    /// file.new_dataset::<i32>().shape([8]).create("orig").unwrap();
137    /// file.root_group().create_soft_link("alias", "/orig").unwrap();
138    /// ```
139    pub fn create_soft_link(&self, link_name: &str, target_path: &str) -> Result<()> {
140        self.create_symbolic_link(
141            link_name,
142            LinkTarget::Soft {
143                target: target_path.to_string(),
144            },
145        )
146    }
147
148    /// Create an external link — `H5Lcreate_external`, h5py's
149    /// `h5py.ExternalLink`.
150    ///
151    /// The link names `target_path` inside `target_file`; neither is opened
152    /// here, and libhdf5 resolves `target_file` against the directory holding
153    /// *this* file, so a relative name is the portable form. As with
154    /// [`create_soft_link`](Self::create_soft_link) the link may dangle: a
155    /// file that is not there and an object that is not there are both legal.
156    ///
157    /// ```no_run
158    /// use rust_hdf5::H5File;
159    ///
160    /// let file = H5File::create("master.h5").unwrap();
161    /// file.root_group()
162    ///     .create_external_link("ext", "payload.h5", "/data")
163    ///     .unwrap();
164    /// ```
165    pub fn create_external_link(
166        &self,
167        link_name: &str,
168        target_file: &str,
169        target_path: &str,
170    ) -> Result<()> {
171        self.create_symbolic_link(
172            link_name,
173            LinkTarget::External {
174                file: target_file.to_string(),
175                path: target_path.to_string(),
176            },
177        )
178    }
179
180    /// Commit a datatype in this group under `name` — `H5Tcommit2`, h5py's
181    /// `group["name"] = dtype`.
182    ///
183    /// The type becomes an object of its own, so datasets can be built on it
184    /// with [`DatasetBuilder::committed_type`](crate::dataset::DatasetBuilder::committed_type)
185    /// and share one definition instead of each carrying a copy. A committed
186    /// datatype no dataset ever uses is still a complete object, and h5py
187    /// reads it back as a `Datatype`.
188    ///
189    /// ```no_run
190    /// use rust_hdf5::H5File;
191    /// use rust_hdf5::format::messages::datatype::DatatypeMessage;
192    ///
193    /// let file = H5File::create("committed.h5").unwrap();
194    /// let grp = file.create_group("types").unwrap();
195    /// grp.commit_datatype("temperature", DatatypeMessage::f64_type()).unwrap();
196    /// ```
197    pub fn commit_datatype(
198        &self,
199        name: &str,
200        datatype: crate::format::messages::datatype::DatatypeMessage,
201    ) -> Result<()> {
202        let full_name = if self.name == "/" {
203            name.to_string()
204        } else {
205            format!("{}/{}", self.name.trim_start_matches('/'), name)
206        };
207        let inner = borrow_inner(&self.file_inner);
208        match &*inner {
209            H5FileInner::Writer(writer) => {
210                writer.commit_datatype(&full_name, datatype)?;
211                Ok(())
212            }
213            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
214                "cannot commit datatypes in read mode".into(),
215            )),
216            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
217        }
218    }
219
220    /// Both symbolic-link constructors, behind one writer-mode check.
221    fn create_symbolic_link(&self, link_name: &str, target: LinkTarget) -> Result<()> {
222        let inner = borrow_inner(&self.file_inner);
223        match &*inner {
224            H5FileInner::Writer(writer) => {
225                writer.create_symbolic_link(&self.name, link_name, target)?;
226                Ok(())
227            }
228            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
229                "cannot create links in read mode".into(),
230            )),
231            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
232        }
233    }
234
235    /// Open an existing sub-group by name (read mode).
236    pub fn group(&self, name: &str) -> Result<H5Group> {
237        let full_name = if self.name == "/" {
238            format!("/{}", name)
239        } else {
240            format!("{}/{}", self.name, name)
241        };
242
243        // Verify the group exists by consulting the reader's actual group
244        // set (derived from link records), not inferred dataset prefixes.
245        // This opens empty groups, attribute-only groups, and
246        // subgroup-only groups, which have no datasets beneath them.
247        let inner = borrow_inner(&self.file_inner);
248        let full_name = match &*inner {
249            H5FileInner::Reader(reader) => {
250                let group_path = full_name.trim_start_matches('/');
251                // A group handle lists and reads through the reader it was
252                // made from, and that reader is one file. Opening the target
253                // file directly is the way across; saying so beats handing
254                // back a handle whose listings would all come up empty.
255                if let Some(edge) = reader.external_edge(group_path) {
256                    return Err(Hdf5Error::Unsupported(format!(
257                        "'{}' resolves through the external link '{}' to '{}' in '{}'; \
258                         a group handle does not cross into another file — open '{}' \
259                         and start from there (datasets read through the link)",
260                        full_name, edge.link, edge.path, edge.file, edge.file
261                    )));
262                }
263                if !reader.has_group(group_path) {
264                    return Err(Hdf5Error::NotFound(full_name));
265                }
266                // Store the traversed path, as write mode does below: the
267                // handle's listings key off it, so a group reached through a
268                // soft link or a group hard link lists the same children as
269                // the group it names.
270                format!("/{}", reader.canonical_path(group_path))
271            }
272            // In write mode the handle stores the tree path, so a path
273            // through hard links resolves once here and every operation
274            // made through the handle lands on the link's target.
275            H5FileInner::Writer(writer) => writer.canonical_group_path(&full_name),
276            H5FileInner::Closed => full_name,
277        };
278        drop(inner);
279
280        Ok(H5Group {
281            file_inner: clone_inner(&self.file_inner),
282            name: full_name,
283        })
284    }
285
286    /// List dataset names that are direct children of this group.
287    pub fn dataset_names(&self) -> Result<Vec<String>> {
288        let inner = borrow_inner(&self.file_inner);
289        let all_names = match &*inner {
290            H5FileInner::Reader(reader) => reader
291                .dataset_names()
292                .iter()
293                .map(|s| s.to_string())
294                .collect::<Vec<_>>(),
295            H5FileInner::Writer(writer) => writer
296                .dataset_names()
297                .iter()
298                .map(|s| s.to_string())
299                .collect::<Vec<_>>(),
300            H5FileInner::Closed => return Ok(vec![]),
301        };
302
303        let prefix = if self.name == "/" {
304            String::new()
305        } else {
306            format!("{}/", self.name.trim_start_matches('/'))
307        };
308
309        let mut result = Vec::new();
310        for name in &all_names {
311            let stripped = if prefix.is_empty() {
312                name.as_str()
313            } else if let Some(rest) = name.strip_prefix(&prefix) {
314                rest
315            } else {
316                continue;
317            };
318            // Only direct children (no further '/')
319            if !stripped.contains('/') {
320                result.push(stripped.to_string());
321            }
322        }
323        Ok(result)
324    }
325
326    /// Create a variable-length string dataset and write data within this group.
327    ///
328    /// Returns a writer-mode handle to the created dataset so attributes can be
329    /// attached to it (e.g. units, descriptions) just like a dataset created
330    /// via [`new_dataset`](Self::new_dataset). The datatype declares UTF-8;
331    /// [`write_vlen_strings_ascii`](Self::write_vlen_strings_ascii) is the
332    /// ASCII-declaring twin, as on [`H5File`](crate::H5File).
333    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
334        self.write_vlen_strings_charset(name, strings, 1)
335    }
336
337    /// Create a variable-length **ASCII** string dataset within this group.
338    ///
339    /// The group-level twin of
340    /// [`H5File::write_vlen_strings_ascii`](crate::H5File::write_vlen_strings_ascii),
341    /// with the same rejection of a string the ASCII declaration would
342    /// misdescribe.
343    pub fn write_vlen_strings_ascii(&self, name: &str, strings: &[&str]) -> Result<H5Dataset> {
344        self.write_vlen_strings_charset(name, strings, 0)
345    }
346
347    /// The single owner of group-level vlen-string dataset creation: the two
348    /// public entry points differ only in the character set they declare.
349    fn write_vlen_strings_charset(
350        &self,
351        name: &str,
352        strings: &[&str],
353        charset: u8,
354    ) -> Result<H5Dataset> {
355        let full_name = if self.name == "/" {
356            name.to_string()
357        } else {
358            let trimmed = self.name.trim_start_matches('/');
359            format!("{}/{}", trimmed, name)
360        };
361
362        let inner = borrow_inner(&self.file_inner);
363        match &*inner {
364            H5FileInner::Writer(writer) => {
365                let idx = writer.create_vlen_string_dataset(&full_name, strings, charset)?;
366                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
367                Ok(H5Dataset::new_writer(
368                    clone_inner(&self.file_inner),
369                    idx,
370                    parts,
371                ))
372            }
373            H5FileInner::Reader(_) => {
374                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
375            }
376            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
377        }
378    }
379
380    /// Create a variable-length byte-array dataset and write data within this
381    /// group.
382    ///
383    /// Each `&[u8]` becomes one element of variable length, stored as a vlen
384    /// sequence of `u8`. h5py reads it back as an array of `uint8` arrays.
385    /// Returns a writer-mode handle so attributes can be attached, like
386    /// [`write_vlen_strings`](Self::write_vlen_strings).
387    ///
388    /// The `u8` case of [`write_vlen_numeric`](Self::write_vlen_numeric).
389    pub fn write_vlen_bytes(&self, name: &str, items: &[&[u8]]) -> Result<H5Dataset> {
390        self.write_vlen_numeric(name, items)
391    }
392
393    /// Create a variable-length numeric-sequence dataset within this group.
394    ///
395    /// The group-level twin of
396    /// [`H5File::write_vlen_numeric`](crate::H5File::write_vlen_numeric).
397    pub fn write_vlen_numeric<T: H5Type>(&self, name: &str, items: &[&[T]]) -> Result<H5Dataset> {
398        let images = crate::dataset::vlen_sequence_images(items)?;
399        let images: Vec<&[u8]> = images.iter().map(|c| c.as_ref()).collect();
400        let full_name = if self.name == "/" {
401            name.to_string()
402        } else {
403            let trimmed = self.name.trim_start_matches('/');
404            format!("{}/{}", trimmed, name)
405        };
406
407        let inner = borrow_inner(&self.file_inner);
408        match &*inner {
409            H5FileInner::Writer(writer) => {
410                let idx =
411                    writer.create_vlen_sequence_dataset(&full_name, T::hdf5_type(), &images)?;
412                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
413                Ok(H5Dataset::new_writer(
414                    clone_inner(&self.file_inner),
415                    idx,
416                    parts,
417                ))
418            }
419            H5FileInner::Reader(_) => {
420                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
421            }
422            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
423        }
424    }
425
426    /// Create a chunked, compressed variable-length string dataset within this group.
427    ///
428    /// Returns a writer-mode handle to the created dataset so attributes can be
429    /// attached to it, like [`write_vlen_strings`](Self::write_vlen_strings).
430    pub fn write_vlen_strings_compressed(
431        &self,
432        name: &str,
433        strings: &[&str],
434        chunk_size: usize,
435        pipeline: FilterPipeline,
436    ) -> Result<H5Dataset> {
437        let full_name = if self.name == "/" {
438            name.to_string()
439        } else {
440            let trimmed = self.name.trim_start_matches('/');
441            format!("{}/{}", trimmed, name)
442        };
443
444        let inner = borrow_inner(&self.file_inner);
445        match &*inner {
446            H5FileInner::Writer(writer) => {
447                let idx = writer.create_vlen_string_dataset_compressed(
448                    &full_name, strings, chunk_size, pipeline,
449                )?;
450                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
451                Ok(H5Dataset::new_writer(
452                    clone_inner(&self.file_inner),
453                    idx,
454                    parts,
455                ))
456            }
457            H5FileInner::Reader(_) => {
458                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
459            }
460            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
461        }
462    }
463
464    /// Create an empty chunked vlen string dataset ready for incremental appends.
465    ///
466    /// Returns a writer-mode handle to the created dataset so attributes can be
467    /// attached before or between [`append_vlen_strings`](Self::append_vlen_strings)
468    /// calls.
469    pub fn create_appendable_vlen_dataset(
470        &self,
471        name: &str,
472        chunk_size: usize,
473        pipeline: Option<FilterPipeline>,
474    ) -> Result<H5Dataset> {
475        let full_name = if self.name == "/" {
476            name.to_string()
477        } else {
478            let trimmed = self.name.trim_start_matches('/');
479            format!("{}/{}", trimmed, name)
480        };
481
482        let inner = borrow_inner(&self.file_inner);
483        match &*inner {
484            H5FileInner::Writer(writer) => {
485                let idx = writer
486                    .create_appendable_vlen_string_dataset(&full_name, chunk_size, pipeline)?;
487                let parts = writer.dataset_handle_parts(idx, &DatasetAccess::default())?;
488                Ok(H5Dataset::new_writer(
489                    clone_inner(&self.file_inner),
490                    idx,
491                    parts,
492                ))
493            }
494            H5FileInner::Reader(_) => {
495                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
496            }
497            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
498        }
499    }
500
501    /// Append variable-length strings to an existing chunked vlen string dataset.
502    pub fn append_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
503        let full_name = if self.name == "/" {
504            name.to_string()
505        } else {
506            let trimmed = self.name.trim_start_matches('/');
507            format!("{}/{}", trimmed, name)
508        };
509
510        let inner = borrow_inner(&self.file_inner);
511        match &*inner {
512            H5FileInner::Writer(writer) => {
513                let ds_index = writer.open_dataset_index(&full_name)?;
514                writer.append_vlen_strings(ds_index, strings)?;
515                Ok(())
516            }
517            H5FileInner::Reader(_) => {
518                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
519            }
520            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
521        }
522    }
523
524    /// Reopen a writer-mode handle to a dataset in this group by name.
525    ///
526    /// Mirrors [`H5File::dataset_writer`](crate::file::H5File::dataset_writer)
527    /// but resolves `name` relative to this group, so a dataset created here
528    /// (including via the vlen-string helpers) can be reopened to attach
529    /// attributes or append chunks. `name` is the link name within this group.
530    pub fn dataset_writer(&self, name: &str) -> Result<H5Dataset> {
531        self.dataset_writer_with(name, DatasetAccess::default())
532    }
533
534    /// [`dataset_writer`](Self::dataset_writer) under named dataset-access
535    /// properties, mirroring
536    /// [`H5File::dataset_writer_with`](crate::file::H5File::dataset_writer_with)
537    /// — which is where the properties that reach a write are described.
538    pub fn dataset_writer_with(&self, name: &str, access: DatasetAccess) -> Result<H5Dataset> {
539        access.validate()?;
540        let full_name = if self.name == "/" {
541            name.to_string()
542        } else {
543            let trimmed = self.name.trim_start_matches('/');
544            format!("{}/{}", trimmed, name)
545        };
546
547        let inner = borrow_inner(&self.file_inner);
548        match &*inner {
549            H5FileInner::Writer(writer) => {
550                let index = writer.open_dataset_index(&full_name)?;
551                let parts = writer.dataset_handle_parts(index, &access)?;
552                Ok(H5Dataset::new_writer(
553                    clone_inner(&self.file_inner),
554                    index,
555                    parts,
556                ))
557            }
558            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
559                "cannot open a dataset_writer in read mode; use dataset() instead".into(),
560            )),
561            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
562        }
563    }
564
565    /// List sub-group names that are direct children of this group.
566    pub fn group_names(&self) -> Result<Vec<String>> {
567        let prefix = if self.name == "/" {
568            String::new()
569        } else {
570            format!("{}/", self.name.trim_start_matches('/'))
571        };
572
573        let mut groups = std::collections::BTreeSet::new();
574        let inner = borrow_inner(&self.file_inner);
575        match &*inner {
576            // Read mode: list immediate child groups from the reader's
577            // actual group set (link records), so empty / attribute-only /
578            // subgroup-only child groups are included.
579            H5FileInner::Reader(reader) => {
580                for path in reader.group_paths() {
581                    let stripped = if prefix.is_empty() {
582                        path.as_str()
583                    } else if let Some(rest) = path.strip_prefix(&prefix) {
584                        rest
585                    } else {
586                        continue;
587                    };
588                    if stripped.is_empty() {
589                        continue;
590                    }
591                    // Immediate child only: take the first path component.
592                    let child = match stripped.find('/') {
593                        Some(pos) => &stripped[..pos],
594                        None => stripped,
595                    };
596                    groups.insert(child.to_string());
597                }
598            }
599            // Write mode: no link-record store; infer from dataset paths.
600            H5FileInner::Writer(writer) => {
601                for name in writer.dataset_names() {
602                    let stripped = if prefix.is_empty() {
603                        name.as_str()
604                    } else if let Some(rest) = name.strip_prefix(&prefix) {
605                        rest
606                    } else {
607                        continue;
608                    };
609                    if let Some(pos) = stripped.find('/') {
610                        groups.insert(stripped[..pos].to_string());
611                    }
612                }
613            }
614            H5FileInner::Closed => return Ok(vec![]),
615        }
616        Ok(groups.into_iter().collect())
617    }
618
619    /// List every link that is a direct child of this group — hard, soft and
620    /// external alike, in name order.
621    ///
622    /// This is the listing of *links* (`H5Lget_name_by_idx`, h5py's
623    /// `grp.keys()`), not of the objects they reach:
624    /// [`dataset_names`](Self::dataset_names) and
625    /// [`group_names`](Self::group_names) answer the object question, and a
626    /// soft or external link appears here whether or not its target resolves.
627    /// Pair it with [`link_class`](Self::link_class) to tell the kinds apart.
628    pub fn link_names(&self) -> Result<Vec<String>> {
629        let prefix = if self.name == "/" {
630            String::new()
631        } else {
632            format!("{}/", self.name.trim_start_matches('/'))
633        };
634
635        let inner = borrow_inner(&self.file_inner);
636        match &*inner {
637            H5FileInner::Reader(reader) => {
638                let mut names = std::collections::BTreeSet::new();
639                for path in reader.links().keys() {
640                    let stripped = if prefix.is_empty() {
641                        path.as_str()
642                    } else if let Some(rest) = path.strip_prefix(&prefix) {
643                        rest
644                    } else {
645                        continue;
646                    };
647                    if !stripped.is_empty() && !stripped.contains('/') {
648                        names.insert(stripped.to_string());
649                    }
650                }
651                Ok(names.into_iter().collect())
652            }
653            // A hard link to one of the writer's own objects is in the object
654            // listings, so those are the union — plus every link that names a
655            // path instead: a soft or external link made this session, or one
656            // a reopened file brought in.
657            H5FileInner::Writer(writer) => {
658                let mut names = std::collections::BTreeSet::new();
659                for (path, _) in writer.path_link_classes() {
660                    let stripped = if prefix.is_empty() {
661                        path.as_str()
662                    } else if let Some(rest) = path.strip_prefix(&prefix) {
663                        rest
664                    } else {
665                        continue;
666                    };
667                    if !stripped.is_empty() && !stripped.contains('/') {
668                        names.insert(stripped.to_string());
669                    }
670                }
671                drop(inner);
672                names.extend(self.dataset_names()?);
673                names.extend(self.group_names()?);
674                names.extend(self.named_datatype_names()?);
675                Ok(names.into_iter().collect())
676            }
677            H5FileInner::Closed => Ok(vec![]),
678        }
679    }
680
681    /// The committed (named) datatypes that are direct children of this
682    /// group, in the order the catalog holds them.
683    pub fn named_datatype_names(&self) -> Result<Vec<String>> {
684        let inner = borrow_inner(&self.file_inner);
685        let all = match &*inner {
686            H5FileInner::Reader(reader) => reader
687                .named_datatype_names()
688                .iter()
689                .map(|s| s.to_string())
690                .collect::<Vec<_>>(),
691            H5FileInner::Writer(writer) => writer.committed_datatype_names(),
692            H5FileInner::Closed => return Ok(vec![]),
693        };
694        Ok(all
695            .iter()
696            .filter_map(|path| self.direct_child(path))
697            .collect())
698    }
699
700    /// Open a committed (named) datatype that is a child of this group.
701    pub fn named_datatype(&self, name: &str) -> Result<crate::H5NamedDatatype> {
702        let full_name = if self.name == "/" {
703            name.to_string()
704        } else {
705            format!("{}/{}", self.name.trim_start_matches('/'), name)
706        };
707        let mut inner = borrow_inner_mut(&self.file_inner);
708        match &mut *inner {
709            H5FileInner::Reader(reader) => {
710                reader.named_datatype_info(&full_name)?;
711                drop(inner);
712                Ok(crate::H5NamedDatatype::new_reader(
713                    clone_inner(&self.file_inner),
714                    full_name,
715                ))
716            }
717            H5FileInner::Writer(_) => Err(Hdf5Error::InvalidState(
718                "committed datatypes are readable only in read mode".into(),
719            )),
720            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
721        }
722    }
723
724    /// `path` as a direct child name of this group, or `None` when it is not
725    /// one.
726    fn direct_child(&self, path: &str) -> Option<String> {
727        let prefix = if self.name == "/" {
728            String::new()
729        } else {
730            format!("{}/", self.name.trim_start_matches('/'))
731        };
732        let stripped = if prefix.is_empty() {
733            path
734        } else {
735            path.strip_prefix(&prefix)?
736        };
737        if stripped.is_empty() || stripped.contains('/') {
738            None
739        } else {
740            Some(stripped.to_string())
741        }
742    }
743
744    /// The class of the link `name` in this group, carrying the value
745    /// `H5Lget_val` returns for the classes that have one — the target path
746    /// of a soft link, the file and path of an external link.
747    ///
748    /// # Errors
749    ///
750    /// [`Hdf5Error::NotFound`] when this group holds no link of that name.
751    pub fn link_class(&self, name: &str) -> Result<LinkClass> {
752        let full_name = if self.name == "/" {
753            name.to_string()
754        } else {
755            format!("{}/{}", self.name.trim_start_matches('/'), name)
756        };
757        let mut inner = borrow_inner_mut(&self.file_inner);
758        match &mut *inner {
759            H5FileInner::Reader(reader) => reader
760                .link_class(&full_name)
761                .cloned()
762                .ok_or(Hdf5Error::NotFound(full_name)),
763            // Anything the writer holds by a path — created here or carried
764            // in by a reopen — answers with the class it was made with; every
765            // other name it knows reaches an object of its own, so it is hard.
766            H5FileInner::Writer(writer) => {
767                let by_path = writer
768                    .path_link_classes()
769                    .into_iter()
770                    .find(|(p, _)| *p == full_name)
771                    .map(|(_, class)| class);
772                drop(inner);
773                match by_path {
774                    Some(class) => Ok(class),
775                    None if self.link_names()?.iter().any(|n| n == name) => Ok(LinkClass::Hard),
776                    None => Err(Hdf5Error::NotFound(full_name)),
777                }
778            }
779            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
780        }
781    }
782
783    /// Why the dataset `name` in this group cannot be read, or `None` when it
784    /// can be.
785    ///
786    /// A dataset whose datatype (or any other message its payload depends on)
787    /// this crate cannot decode is still listed by
788    /// [`dataset_names`](Self::dataset_names) — the file contains it — and
789    /// this says what stands in the way. Opening it through
790    /// [`H5File::dataset`](crate::H5File::dataset) fails with
791    /// [`Hdf5Error::Unsupported`] carrying the same text.
792    pub fn unreadable_reason(&self, name: &str) -> Result<Option<String>> {
793        let full_name = if self.name == "/" {
794            name.to_string()
795        } else {
796            format!("{}/{}", self.name.trim_start_matches('/'), name)
797        };
798        let mut inner = borrow_inner_mut(&self.file_inner);
799        match &mut *inner {
800            H5FileInner::Reader(reader) => {
801                Ok(reader.unreadable_reason(&full_name).map(str::to_string))
802            }
803            // The writer only holds datasets it built itself.
804            H5FileInner::Writer(_) => Ok(None),
805            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
806        }
807    }
808
809    /// Add (or replace) a string attribute on this group.
810    ///
811    /// This is the standard way to mark a NeXus class, e.g.
812    /// `grp.set_attr_string("NX_class", "NXdetector")`. The value is stored as
813    /// a variable-length UTF-8 string (read back as a Python `str` by h5py),
814    /// not a fixed-length string.
815    pub fn set_attr_string(&self, name: &str, value: &str) -> Result<()> {
816        let inner = borrow_inner(&self.file_inner);
817        match &*inner {
818            H5FileInner::Writer(writer) => {
819                writer.set_vlen_string_attribute(self.attr_target(), name, value)?;
820                Ok(())
821            }
822            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
823                "cannot write attributes in read mode".into(),
824            )),
825            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
826        }
827    }
828
829    /// Add (or replace) a numeric scalar attribute on this group.
830    pub fn set_attr_numeric<T: H5Type>(&self, name: &str, value: &T) -> Result<()> {
831        let es = T::element_size();
832        // Safety: `T: H5Type` is a `Copy` numeric primitive whose byte
833        // representation is exactly `element_size()` wide.
834        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
835        self.add_attr(AttributeMessage::scalar_numeric(
836            name,
837            T::hdf5_type(),
838            raw.to_vec(),
839        ))
840    }
841
842    /// Add (or replace) a numeric (or bool) **array** attribute on this group.
843    ///
844    /// The values are written as a 1-D HDF5 array attribute (simple dataspace
845    /// `[values.len()]`), read back by h5py as a numpy array — the array
846    /// counterpart of [`set_attr_numeric`](Self::set_attr_numeric). For a
847    /// multi-dimensional shape use
848    /// [`set_attr_array_numeric_nd`](Self::set_attr_array_numeric_nd).
849    pub fn set_attr_array_numeric<T: H5Type>(&self, name: &str, values: &[T]) -> Result<()> {
850        self.set_attr_array_numeric_nd(name, values, &[values.len()])
851    }
852
853    /// Add (or replace) a numeric (or bool) **N-dimensional array** attribute on
854    /// this group.
855    ///
856    /// `shape` gives the dataspace dimensions; `values` is the row-major data
857    /// and its length must equal the product of `shape` (an empty `shape` is a
858    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
859    /// of that shape. [`set_attr_array_numeric`](Self::set_attr_array_numeric)
860    /// is the 1-D convenience form.
861    pub fn set_attr_array_numeric_nd<T: H5Type>(
862        &self,
863        name: &str,
864        values: &[T],
865        shape: &[usize],
866    ) -> Result<()> {
867        let n: usize = shape.iter().product();
868        if values.len() != n {
869            return Err(Hdf5Error::InvalidState(format!(
870                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
871                values.len()
872            )));
873        }
874        let es = T::element_size();
875        // Safety: `T: H5Type` is a `Copy` POD numeric whose byte width is `es`.
876        let raw =
877            unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
878        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
879        self.add_attr(AttributeMessage::array_numeric(
880            name,
881            T::hdf5_type(),
882            &dims,
883            raw.to_vec(),
884        ))
885    }
886
887    /// Add (or replace) a variable-length UTF-8 string **array** attribute on
888    /// this group, read back by h5py as a 1-D array of `str` — the array
889    /// counterpart of [`set_attr_string`](Self::set_attr_string). For a
890    /// multi-dimensional shape use
891    /// [`set_attr_string_array_nd`](Self::set_attr_string_array_nd).
892    pub fn set_attr_string_array(&self, name: &str, values: &[&str]) -> Result<()> {
893        self.set_attr_string_array_nd(name, values, &[values.len()])
894    }
895
896    /// Add (or replace) a variable-length UTF-8 string **N-dimensional array**
897    /// attribute on this group.
898    ///
899    /// `shape` gives the dataspace dimensions; `values` is the row-major data
900    /// and its length must equal the product of `shape` (an empty `shape` is a
901    /// scalar, requiring exactly one value). Read back by h5py as a numpy array
902    /// of Python `str` with that shape.
903    /// [`set_attr_string_array`](Self::set_attr_string_array) is the 1-D
904    /// convenience form.
905    pub fn set_attr_string_array_nd(
906        &self,
907        name: &str,
908        values: &[&str],
909        shape: &[usize],
910    ) -> Result<()> {
911        let n: usize = shape.iter().product();
912        if values.len() != n {
913            return Err(Hdf5Error::InvalidState(format!(
914                "attribute '{name}' shape {shape:?} needs {n} elements, got {}",
915                values.len()
916            )));
917        }
918        let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
919        // The vlen array message needs `&mut writer` (global-heap allocation),
920        // so we route it the same way `set_attr_string` does rather than through
921        // `add_attr` (which re-borrows the writer).
922        let mut inner = borrow_inner_mut(&self.file_inner);
923        match &mut *inner {
924            H5FileInner::Writer(writer) => {
925                writer.set_vlen_string_array_attribute(self.attr_target(), name, values, &dims)?;
926                Ok(())
927            }
928            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
929                "cannot write attributes in read mode".into(),
930            )),
931            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
932        }
933    }
934
935    /// Add (or replace) an object-reference attribute on this group — h5py's
936    /// `g.attrs['owner'] = f['/data'].ref`.
937    ///
938    /// `path` names a dataset or a group (`/` is the root group) and must
939    /// already exist. The attribute takes the scalar shape h5py gives a single
940    /// reference; [`set_attr_object_references`](Self::set_attr_object_references)
941    /// is the array form. What reaches the file is the target's object header
942    /// address, which is assigned when the file is finalized.
943    pub fn set_attr_object_reference(&self, name: &str, path: &str) -> Result<()> {
944        self.set_reference_attr(name, &[path], &[])
945    }
946
947    /// Add (or replace) a 1-D array of object references as an attribute on
948    /// this group — the array counterpart of
949    /// [`set_attr_object_reference`](Self::set_attr_object_reference).
950    pub fn set_attr_object_references(&self, name: &str, paths: &[&str]) -> Result<()> {
951        self.set_reference_attr(name, paths, &[paths.len() as u64])
952    }
953
954    /// Route a reference attribute to the writer, the way
955    /// [`add_attr`](Self::add_attr) routes every other kind.
956    fn set_reference_attr(&self, name: &str, paths: &[&str], dims: &[u64]) -> Result<()> {
957        let inner = borrow_inner(&self.file_inner);
958        match &*inner {
959            H5FileInner::Writer(writer) => {
960                writer.set_object_reference_attribute(self.attr_target(), name, paths, dims)?;
961                Ok(())
962            }
963            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
964                "cannot write attributes in read mode".into(),
965            )),
966            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
967        }
968    }
969
970    /// The writer-side attribute list this group's attributes live in: the
971    /// root group's is the file-level list, any other group's is its own.
972    fn attr_target(&self) -> crate::io::writer::AttrTarget<'_> {
973        if self.name == "/" {
974            crate::io::writer::AttrTarget::Root
975        } else {
976            crate::io::writer::AttrTarget::Group(&self.name)
977        }
978    }
979
980    /// Route an attribute to the writer: the root group goes to the
981    /// file-level attribute list, any other group to its own header.
982    fn add_attr(&self, attr: AttributeMessage) -> Result<()> {
983        let inner = borrow_inner(&self.file_inner);
984        match &*inner {
985            H5FileInner::Writer(writer) => {
986                writer.set_attribute(self.attr_target(), attr)?;
987                Ok(())
988            }
989            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
990                "cannot write attributes in read mode".into(),
991            )),
992            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
993        }
994    }
995
996    /// List this group's attribute names (read mode).
997    pub fn attr_names(&self) -> Result<Vec<String>> {
998        let mut inner = borrow_inner_mut(&self.file_inner);
999        match &mut *inner {
1000            H5FileInner::Reader(reader) => {
1001                if self.name == "/" {
1002                    Ok(reader.root_attr_names()?)
1003                } else {
1004                    Ok(reader.group_attr_names(self.name.trim_start_matches('/'))?)
1005                }
1006            }
1007            _ => Err(Hdf5Error::InvalidState(
1008                "attr_names is only available in read mode".into(),
1009            )),
1010        }
1011    }
1012
1013    /// This group's own attribute creation-order policy — the equivalent of
1014    /// `H5Pget_attr_creation_order(gid.get_create_plist())` — `-` when
1015    /// neither `TRACKED` nor `INDEXED` is set, and never derived from
1016    /// whether the group currently holds any attributes: a group can track
1017    /// creation order and still be empty (read mode).
1018    pub fn attr_creation_order(&self) -> Result<CreationOrder> {
1019        let inner = borrow_inner(&self.file_inner);
1020        match &*inner {
1021            H5FileInner::Reader(reader) => Ok(if self.name == "/" {
1022                reader.root_attr_creation_order()
1023            } else {
1024                reader.group_attr_creation_order(self.name.trim_start_matches('/'))
1025            }),
1026            _ => Err(Hdf5Error::InvalidState(
1027                "attr_creation_order is only available in read mode".into(),
1028            )),
1029        }
1030    }
1031
1032    /// This group's own compact-vs-dense attribute storage — the equivalent
1033    /// of `h5py.h5o.get_info(gid.id).meta_size.attr.index_size` being
1034    /// nonzero.
1035    pub fn attr_storage(&self) -> Result<AttributeStorage> {
1036        let inner = borrow_inner(&self.file_inner);
1037        match &*inner {
1038            H5FileInner::Reader(reader) => Ok(if self.name == "/" {
1039                reader.root_attr_storage()
1040            } else {
1041                reader.group_attr_storage(self.name.trim_start_matches('/'))
1042            }),
1043            _ => Err(Hdf5Error::InvalidState(
1044                "attr_storage is only available in read mode".into(),
1045            )),
1046        }
1047    }
1048
1049    /// This group's own object-header attribute count — the equivalent of
1050    /// `h5py.h5o.get_info(gid.id).num_attrs`, which need not equal
1051    /// [`attr_names`](Self::attr_names)'s length when the set could not be
1052    /// read whole.
1053    pub fn header_attr_count(&self) -> Result<u64> {
1054        let inner = borrow_inner(&self.file_inner);
1055        match &*inner {
1056            H5FileInner::Reader(reader) => Ok(if self.name == "/" {
1057                reader.root_header_attr_count()?
1058            } else {
1059                reader.group_header_attr_count(self.name.trim_start_matches('/'))?
1060            }),
1061            _ => Err(Hdf5Error::InvalidState(
1062                "header_attr_count is only available in read mode".into(),
1063            )),
1064        }
1065    }
1066
1067    /// This group's own link creation-order policy — the equivalent of
1068    /// `H5Pget_link_creation_order(gid.get_create_plist())`. A fact about
1069    /// the group's own `Link Info` message, independent of
1070    /// [`attr_creation_order`](Self::attr_creation_order): a group can track
1071    /// one without the other.
1072    pub fn link_creation_order(&self) -> Result<CreationOrder> {
1073        let inner = borrow_inner(&self.file_inner);
1074        match &*inner {
1075            H5FileInner::Reader(reader) => Ok(if self.name == "/" {
1076                reader.root_link_creation_order()
1077            } else {
1078                reader.group_link_creation_order(self.name.trim_start_matches('/'))
1079            }),
1080            _ => Err(Hdf5Error::InvalidState(
1081                "link_creation_order is only available in read mode".into(),
1082            )),
1083        }
1084    }
1085
1086    /// This group's own link storage kind — the equivalent of libhdf5's
1087    /// `H5Gget_info(gid).storage_type`: `SymbolTable` for a pre-1.8 group
1088    /// (a v1 B-tree plus local heap, present regardless of the object
1089    /// header's own version), `Compact` while links live as messages in the
1090    /// header, or `Dense` once the phase change moves the whole set into a
1091    /// fractal heap plus name index.
1092    pub fn link_storage(&self) -> Result<LinkStorage> {
1093        let inner = borrow_inner(&self.file_inner);
1094        match &*inner {
1095            H5FileInner::Reader(reader) => Ok(if self.name == "/" {
1096                reader.root_link_storage()
1097            } else {
1098                reader.group_link_storage(self.name.trim_start_matches('/'))
1099            }),
1100            _ => Err(Hdf5Error::InvalidState(
1101                "link_storage is only available in read mode".into(),
1102            )),
1103        }
1104    }
1105
1106    /// Why this group's attribute `name` cannot be read, or `None` when it
1107    /// can be. The attribute counterpart of
1108    /// [`unreadable_reason`](Self::unreadable_reason)'s shape for datasets:
1109    /// an attribute this crate cannot decode stays in
1110    /// [`attr_names`](Self::attr_names) and answers here.
1111    pub fn attr_unreadable_reason(&self, name: &str) -> Result<Option<String>> {
1112        let inner = borrow_inner(&self.file_inner);
1113        match &*inner {
1114            H5FileInner::Reader(reader) => Ok(if self.name == "/" {
1115                reader.root_attr_unreadable_reason(name)
1116            } else {
1117                reader.group_attr_unreadable_reason(self.name.trim_start_matches('/'), name)
1118            }
1119            .map(str::to_string)),
1120            _ => Err(Hdf5Error::InvalidState(
1121                "attr_unreadable_reason is only available in read mode".into(),
1122            )),
1123        }
1124    }
1125
1126    /// Why this group's attribute *set* cannot be listed, or `None` when it
1127    /// can be.
1128    ///
1129    /// The object-scope counterpart of
1130    /// [`attr_unreadable_reason`](Self::attr_unreadable_reason): a failure
1131    /// that belongs to no single name — a dense set whose heap or name index
1132    /// will not read — leaves nothing to list, so
1133    /// [`attr_names`](Self::attr_names) returns it as an error and this
1134    /// reports it without one.
1135    pub fn attrs_unreadable_reason(&self) -> Result<Option<String>> {
1136        let inner = borrow_inner(&self.file_inner);
1137        match &*inner {
1138            H5FileInner::Reader(reader) => Ok(if self.name == "/" {
1139                reader.root_attrs_unreadable_reason()
1140            } else {
1141                reader.group_attrs_unreadable_reason(self.name.trim_start_matches('/'))
1142            }
1143            .map(str::to_string)),
1144            _ => Err(Hdf5Error::InvalidState(
1145                "attrs_unreadable_reason is only available in read mode".into(),
1146            )),
1147        }
1148    }
1149
1150    /// Read one of this group's attributes as a string (read mode).
1151    pub fn attr_string(&self, name: &str) -> Result<String> {
1152        let mut inner = borrow_inner_mut(&self.file_inner);
1153        match &mut *inner {
1154            H5FileInner::Reader(reader) => {
1155                let attr = if self.name == "/" {
1156                    reader.root_attr(name)
1157                } else {
1158                    reader.group_attr(self.name.trim_start_matches('/'), name)
1159                }?
1160                .clone();
1161                Ok(reader.attr_string_value(&attr)?)
1162            }
1163            _ => Err(Hdf5Error::InvalidState(
1164                "attr_string is only available in read mode".into(),
1165            )),
1166        }
1167    }
1168}