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}