Skip to main content

rust_hdf5/
swmr.rs

1//! Single Writer / Multiple Reader (SWMR) API.
2//!
3//! Provides a high-level wrapper around the SWMR protocol for streaming
4//! frame-based data (e.g., area detector images).
5
6use std::path::Path;
7
8use crate::format::messages::attribute::AttributeMessage;
9use crate::io::locking::FileLocking;
10use crate::io::Hdf5Reader;
11use crate::io::SwmrWriter as IoSwmrWriter;
12
13use crate::error::Result;
14use crate::types::H5Type;
15
16/// SWMR writer for streaming frame-based data to an HDF5 file.
17///
18/// Usage:
19/// ```no_run
20/// use rust_hdf5::swmr::SwmrFileWriter;
21///
22/// let mut writer = SwmrFileWriter::create("stream.h5").unwrap();
23/// let ds = writer.create_streaming_dataset::<f32>("frames", &[256, 256]).unwrap();
24/// writer.start_swmr().unwrap();
25///
26/// // Write frames
27/// let frame_data = vec![0.0f32; 256 * 256];
28/// let raw: Vec<u8> = frame_data.iter()
29///     .flat_map(|v| v.to_le_bytes())
30///     .collect();
31/// writer.append_frame(ds, &raw).unwrap();
32/// writer.flush().unwrap();
33///
34/// writer.close().unwrap();
35/// ```
36pub struct SwmrFileWriter {
37    inner: IoSwmrWriter,
38}
39
40impl SwmrFileWriter {
41    /// Create a new HDF5 file for SWMR streaming using the env-var-derived
42    /// locking policy.
43    pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
44        let inner = IoSwmrWriter::create(path.as_ref())?;
45        Ok(Self { inner })
46    }
47
48    /// Create a new HDF5 file for SWMR streaming with an explicit locking
49    /// policy. The writer holds an exclusive lock until [`Self::start_swmr`]
50    /// is called, at which point the lock is downgraded to shared so
51    /// concurrent SWMR readers can attach.
52    pub fn create_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
53        let inner = IoSwmrWriter::create_with_locking(path.as_ref(), locking)?;
54        Ok(Self { inner })
55    }
56
57    /// Reopen a cleanly-closed HDF5 file to resume SWMR streaming.
58    ///
59    /// Existing datasets are reconstructed; locate them with
60    /// [`dataset_index`](Self::dataset_index), call [`start_swmr`](Self::start_swmr)
61    /// to re-enter SWMR mode, then continue with [`append_frame`](Self::append_frame).
62    /// Appending to a multi-frame-chunk dataset (`chunk[0] > 1`) after reopen
63    /// is rejected — its final partial band was zero-padded at the original
64    /// close. Recovering a crashed (never cleanly closed) file is not supported.
65    pub fn open_append<P: AsRef<Path>>(path: P) -> Result<Self> {
66        let inner = IoSwmrWriter::open_append(path.as_ref())?;
67        Ok(Self { inner })
68    }
69
70    /// Reopen a cleanly-closed HDF5 file to resume SWMR streaming with an
71    /// explicit locking policy. See [`Self::open_append`].
72    pub fn open_append_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
73        let inner = IoSwmrWriter::open_append_with_locking(path.as_ref(), locking)?;
74        Ok(Self { inner })
75    }
76
77    /// Return the index of a dataset by name, or `None` if absent.
78    ///
79    /// Mainly used after [`open_append`](Self::open_append) to recover the
80    /// index of a reconstructed dataset for [`append_frame`](Self::append_frame).
81    pub fn dataset_index(&self, name: &str) -> Option<usize> {
82        self.inner.dataset_index(name)
83    }
84
85    /// Create a streaming dataset.
86    ///
87    /// The dataset will have shape `[0, frame_dims...]` initially, with
88    /// chunk dimensions `[1, frame_dims...]` and unlimited first dimension.
89    ///
90    /// Returns the dataset index for use with `append_frame`.
91    pub fn create_streaming_dataset<T: H5Type>(
92        &mut self,
93        name: &str,
94        frame_dims: &[u64],
95    ) -> Result<usize> {
96        let datatype = T::hdf5_type();
97        let idx = self
98            .inner
99            .create_streaming_dataset(name, datatype, frame_dims)?;
100        Ok(idx)
101    }
102
103    /// Create a streaming dataset whose frames are compressed.
104    ///
105    /// Like [`create_streaming_dataset`](Self::create_streaming_dataset) but
106    /// each appended frame is run through `pipeline` (e.g.
107    /// `FilterPipeline::deflate(4)`). SWMR appends and in-place header
108    /// updates work the same as for uncompressed streaming datasets.
109    pub fn create_streaming_dataset_compressed<T: H5Type>(
110        &mut self,
111        name: &str,
112        frame_dims: &[u64],
113        pipeline: crate::format::messages::filter::FilterPipeline,
114    ) -> Result<usize> {
115        let idx = self.inner.create_streaming_dataset_compressed(
116            name,
117            T::hdf5_type(),
118            frame_dims,
119            pipeline,
120        )?;
121        Ok(idx)
122    }
123
124    /// Create a streaming dataset whose frames are split into fixed-size
125    /// chunk tiles.
126    ///
127    /// `frame_dims` is the per-frame shape (e.g. `[1024, 1024]`);
128    /// `frame_chunk` is the tile shape within a frame (e.g. `[256, 256]`),
129    /// of the same rank. The on-disk chunk shape becomes
130    /// `[1, frame_chunk...]`, so each frame is stored as
131    /// `product(frame_dims / frame_chunk)` chunks instead of one. This
132    /// mirrors area-detector tiling controls such as NDFileHDF5's
133    /// `nRowChunks` / `nColChunks`: it changes only the partial-read
134    /// granularity and compression unit, not the stored data.
135    /// [`append_frame`](Self::append_frame) accepts a whole frame and
136    /// splits it into tiles automatically.
137    pub fn create_streaming_dataset_tiled<T: H5Type>(
138        &mut self,
139        name: &str,
140        frame_dims: &[u64],
141        frame_chunk: &[u64],
142    ) -> Result<usize> {
143        let idx = self.inner.create_streaming_dataset_tiled(
144            name,
145            T::hdf5_type(),
146            frame_dims,
147            frame_chunk,
148        )?;
149        Ok(idx)
150    }
151
152    /// Create a compressed streaming dataset whose frames are split into
153    /// fixed-size chunk tiles. See
154    /// [`create_streaming_dataset_tiled`](Self::create_streaming_dataset_tiled)
155    /// for the meaning of `frame_chunk`; each tile is the compression unit.
156    pub fn create_streaming_dataset_tiled_compressed<T: H5Type>(
157        &mut self,
158        name: &str,
159        frame_dims: &[u64],
160        frame_chunk: &[u64],
161        pipeline: crate::format::messages::filter::FilterPipeline,
162    ) -> Result<usize> {
163        let idx = self.inner.create_streaming_dataset_tiled_compressed(
164            name,
165            T::hdf5_type(),
166            frame_dims,
167            frame_chunk,
168            pipeline,
169        )?;
170        Ok(idx)
171    }
172
173    /// Create a streaming dataset with full control over the chunk shape,
174    /// including the frame axis.
175    ///
176    /// `chunk` is the complete per-chunk shape, of rank
177    /// `frame_dims.len() + 1`: `chunk[0]` frames per chunk (the NDFileHDF5
178    /// `nFramesChunks` control) and `chunk[1..]` the per-frame tile shape
179    /// (`nRowChunks` / `nColChunks`). When `chunk[0] > 1`,
180    /// [`append_frame`](Self::append_frame) buffers whole frames until a
181    /// chunk band fills; the final partial band is written (zero-padded) at
182    /// [`close`](Self::close), and the dataset's logical frame count always
183    /// equals the exact number of frames appended.
184    pub fn create_streaming_dataset_chunked<T: H5Type>(
185        &mut self,
186        name: &str,
187        frame_dims: &[u64],
188        chunk: &[u64],
189    ) -> Result<usize> {
190        let idx =
191            self.inner
192                .create_streaming_dataset_chunked(name, T::hdf5_type(), frame_dims, chunk)?;
193        Ok(idx)
194    }
195
196    /// Compressed variant of
197    /// [`create_streaming_dataset_chunked`](Self::create_streaming_dataset_chunked);
198    /// each chunk is filtered independently through `pipeline`.
199    pub fn create_streaming_dataset_chunked_compressed<T: H5Type>(
200        &mut self,
201        name: &str,
202        frame_dims: &[u64],
203        chunk: &[u64],
204        pipeline: crate::format::messages::filter::FilterPipeline,
205    ) -> Result<usize> {
206        let idx = self.inner.create_streaming_dataset_chunked_compressed(
207            name,
208            T::hdf5_type(),
209            frame_dims,
210            chunk,
211            pipeline,
212        )?;
213        Ok(idx)
214    }
215
216    /// Create a hard link: an additional name for a dataset or group that
217    /// already exists in the file.
218    ///
219    /// No data is copied — the link and its target share one object header,
220    /// exactly as `h5py` / libhdf5 hard links do. This is the NeXus-style way
221    /// to expose a streaming dataset at an aliased path.
222    ///
223    /// * `parent_group_path` — full path of the group that will hold the
224    ///   link (`"/"` for the root group).
225    /// * `link_name` — leaf name of the new link within that group.
226    /// * `target_path` — full path of an existing dataset or group.
227    ///
228    /// # Visibility relative to SWMR mode
229    ///
230    /// A link created **before** [`start_swmr`](Self::start_swmr) is committed
231    /// by `start_swmr` and is visible to SWMR readers for the whole streaming
232    /// window. A link created **after** `start_swmr` is committed only by
233    /// [`close`](Self::close); it does not appear to readers that attach
234    /// during the live SWMR window. Create layout links before `start_swmr`
235    /// when readers must resolve them while streaming.
236    pub fn create_hard_link(
237        &mut self,
238        parent_group_path: &str,
239        link_name: &str,
240        target_path: &str,
241    ) -> Result<()> {
242        self.inner
243            .writer_mut()
244            .create_hard_link(parent_group_path, link_name, target_path)?;
245        Ok(())
246    }
247
248    /// Create a group in the file hierarchy.
249    ///
250    /// * `parent_group_path` — full path of the parent group (`"/"` for the
251    ///   root group).
252    /// * `name` — leaf name of the new group.
253    ///
254    /// A nested NeXus layout is built one level at a time, parent first:
255    ///
256    /// ```no_run
257    /// # use rust_hdf5::swmr::SwmrFileWriter;
258    /// # let mut writer = SwmrFileWriter::create("stream.h5").unwrap();
259    /// writer.create_group("/", "entry").unwrap();
260    /// writer.create_group("/entry", "data").unwrap();
261    /// ```
262    ///
263    /// Like [`create_hard_link`](Self::create_hard_link), a group created
264    /// before [`start_swmr`](Self::start_swmr) is visible to SWMR readers for
265    /// the whole streaming window; one created after is committed only by
266    /// [`close`](Self::close).
267    pub fn create_group(&mut self, parent_group_path: &str, name: &str) -> Result<()> {
268        self.inner
269            .writer_mut()
270            .create_group(parent_group_path, name)?;
271        Ok(())
272    }
273
274    /// Set a string attribute on a group, or on the root group when
275    /// `group_path` is `"/"`.
276    ///
277    /// This is the NeXus way to tag a group with its class — for example
278    /// `set_group_attr_string("/entry", "NX_class", "NXentry")`. An existing
279    /// attribute of the same name is replaced.
280    ///
281    /// The same SWMR visibility rule as [`create_group`](Self::create_group)
282    /// applies: set before [`start_swmr`](Self::start_swmr) for the attribute
283    /// to be visible to readers during streaming.
284    pub fn set_group_attr_string(
285        &mut self,
286        group_path: &str,
287        name: &str,
288        value: &str,
289    ) -> Result<()> {
290        let attr = AttributeMessage::scalar_string(name, value);
291        if group_path == "/" {
292            self.inner.writer_mut().add_root_attribute(attr);
293        } else {
294            self.inner
295                .writer_mut()
296                .add_group_attribute(group_path, attr)?;
297        }
298        Ok(())
299    }
300
301    /// Set a numeric scalar attribute on a group, or on the root group when
302    /// `group_path` is `"/"`. An existing attribute of the same name is
303    /// replaced. See [`set_group_attr_string`](Self::set_group_attr_string)
304    /// for the SWMR visibility rule.
305    pub fn set_group_attr_numeric<T: H5Type>(
306        &mut self,
307        group_path: &str,
308        name: &str,
309        value: &T,
310    ) -> Result<()> {
311        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
312        if group_path == "/" {
313            self.inner.writer_mut().add_root_attribute(attr);
314        } else {
315            self.inner
316                .writer_mut()
317                .add_group_attribute(group_path, attr)?;
318        }
319        Ok(())
320    }
321
322    /// Create a fixed-shape (non-streaming) dataset and write all its data in
323    /// one call. Returns the dataset index.
324    ///
325    /// This is for the NeXus metadata that surrounds the image stream —
326    /// coordinate axes, detector geometry, and (with `dims = &[]`) scalar
327    /// values such as `/entry/instrument/detector/distance`. Unlike a
328    /// streaming dataset, it is written once and not appended to.
329    pub fn write_dataset<T: H5Type>(
330        &mut self,
331        name: &str,
332        dims: &[u64],
333        data: &[T],
334    ) -> Result<usize> {
335        let expected: u64 = if dims.is_empty() {
336            1
337        } else {
338            dims.iter().product()
339        };
340        if data.len() as u64 != expected {
341            return Err(crate::error::Hdf5Error::InvalidState(format!(
342                "write_dataset: data has {} elements but shape {dims:?} needs {expected}",
343                data.len()
344            )));
345        }
346        let idx = self
347            .inner
348            .writer_mut()
349            .create_dataset(name, T::hdf5_type(), dims)?;
350        self.inner
351            .writer_mut()
352            .write_dataset_raw(idx, slice_to_bytes(data))?;
353        Ok(idx)
354    }
355
356    /// Create a variable-length string dataset (one element per string).
357    /// Returns the dataset index. Useful for NeXus metadata such as
358    /// `/entry/start_time` or per-frame timestamp arrays.
359    pub fn write_string_dataset(&mut self, name: &str, strings: &[&str]) -> Result<usize> {
360        let idx = self
361            .inner
362            .writer_mut()
363            .create_vlen_string_dataset(name, strings)?;
364        Ok(idx)
365    }
366
367    /// Set a string attribute on a dataset, addressed by its index. The
368    /// NeXus way to record `units`, `long_name`, `signal`, etc. An existing
369    /// attribute of the same name is replaced.
370    pub fn set_dataset_attr_string(
371        &mut self,
372        ds_index: usize,
373        name: &str,
374        value: &str,
375    ) -> Result<()> {
376        self.inner
377            .writer_mut()
378            .add_dataset_attribute(ds_index, AttributeMessage::scalar_string(name, value))?;
379        Ok(())
380    }
381
382    /// Set a numeric scalar attribute on a dataset, addressed by its index.
383    /// An existing attribute of the same name is replaced.
384    pub fn set_dataset_attr_numeric<T: H5Type>(
385        &mut self,
386        ds_index: usize,
387        name: &str,
388        value: &T,
389    ) -> Result<()> {
390        let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
391        self.inner
392            .writer_mut()
393            .add_dataset_attribute(ds_index, attr)?;
394        Ok(())
395    }
396
397    /// Set a numeric array attribute on a dataset, addressed by its index.
398    ///
399    /// `dims` are the dimension sizes (e.g. `&[3]` for a 1-D array) and the
400    /// number of `values` must equal their product. This is the SWMR
401    /// counterpart of [`H5Attribute::write_array`](crate::H5Attribute::write_array),
402    /// for the length-ndims `int32` array attributes AreaDetector writes
403    /// (`NDArrayDimOffset`, `NDArrayDimBinning`, `NDArrayDimReverse`). An
404    /// existing attribute of the same name is replaced.
405    ///
406    /// In SWMR mode, attributes can only be added before
407    /// [`start_swmr`](Self::start_swmr); HDF5 forbids adding dataset
408    /// attributes once the file is in SWMR write mode. Resolve a dataset path
409    /// to its index with [`dataset_index`](Self::dataset_index).
410    pub fn set_dataset_attr_array<T: H5Type>(
411        &mut self,
412        ds_index: usize,
413        name: &str,
414        dims: &[u64],
415        values: &[T],
416    ) -> Result<()> {
417        // Product of an empty shape is 1 (a scalar holds one element).
418        let expected: u64 = dims.iter().product();
419        if values.len() as u64 != expected {
420            return Err(crate::error::Hdf5Error::InvalidState(format!(
421                "set_dataset_attr_array: {} values but shape {dims:?} needs {expected}",
422                values.len()
423            )));
424        }
425        let attr = AttributeMessage::array_numeric(
426            name,
427            T::hdf5_type(),
428            dims,
429            slice_to_bytes(values).to_vec(),
430        );
431        self.inner
432            .writer_mut()
433            .add_dataset_attribute(ds_index, attr)?;
434        Ok(())
435    }
436
437    /// Set the fill value of a streaming dataset, addressed by its index.
438    ///
439    /// Call this before the first [`append_frame`](Self::append_frame): it
440    /// determines the value of chunk regions that are never written (a
441    /// partial final band, or unwritten tiles).
442    pub fn set_dataset_fill_value<T: H5Type>(&mut self, ds_index: usize, value: &T) -> Result<()> {
443        self.inner
444            .writer_mut()
445            .set_dataset_fill_value(ds_index, scalar_to_bytes(value))?;
446        Ok(())
447    }
448
449    /// Place an existing dataset inside a group.
450    ///
451    /// By default a dataset created through this writer lives at the root
452    /// level; this moves its link record into `group_path` (which must
453    /// already exist). The group must be created before `start_swmr` for the
454    /// placement to be visible to readers during streaming.
455    pub fn assign_dataset_to_group(&mut self, group_path: &str, ds_index: usize) -> Result<()> {
456        self.inner
457            .writer_mut()
458            .assign_dataset_to_group(group_path, ds_index)?;
459        Ok(())
460    }
461
462    /// Signal the start of SWMR mode.
463    pub fn start_swmr(&mut self) -> Result<()> {
464        self.inner.start_swmr()?;
465        Ok(())
466    }
467
468    /// Append a frame of raw data to a streaming dataset.
469    ///
470    /// The data size must match one frame (product of frame_dims * element_size).
471    pub fn append_frame(&mut self, ds_index: usize, data: &[u8]) -> Result<()> {
472        self.inner.append_frame(ds_index, data)?;
473        Ok(())
474    }
475
476    /// Flush all dataset index structures to disk with SWMR ordering.
477    pub fn flush(&mut self) -> Result<()> {
478        self.inner.flush()?;
479        Ok(())
480    }
481
482    /// Close and finalize the file.
483    pub fn close(self) -> Result<()> {
484        self.inner.close()?;
485        Ok(())
486    }
487}
488
489/// SWMR reader for monitoring a streaming HDF5 file.
490///
491/// Opens a file being written by a concurrent [`SwmrFileWriter`] and
492/// periodically calls [`refresh`](Self::refresh) to pick up new data.
493///
494/// ```no_run
495/// use rust_hdf5::swmr::SwmrFileReader;
496///
497/// let mut reader = SwmrFileReader::open("stream.h5").unwrap();
498///
499/// loop {
500///     reader.refresh().unwrap();
501///     let names = reader.dataset_names();
502///     if let Some(shape) = reader.dataset_shape("frames").ok() {
503///         println!("frames shape: {:?}", shape);
504///         if shape[0] > 0 {
505///             let data = reader.read_dataset_raw("frames").unwrap();
506///             println!("got {} bytes", data.len());
507///             break;
508///         }
509///     }
510///     std::thread::sleep(std::time::Duration::from_millis(100));
511/// }
512/// ```
513pub struct SwmrFileReader {
514    reader: Hdf5Reader,
515}
516
517impl SwmrFileReader {
518    /// Open an HDF5 file for SWMR reading using the env-var-derived
519    /// locking policy. Takes a shared lock so it coexists with the
520    /// downgraded shared lock held by [`SwmrFileWriter`] after
521    /// `start_swmr`, and with other concurrent SWMR readers.
522    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
523        let reader = Hdf5Reader::open_swmr(path.as_ref())?;
524        Ok(Self { reader })
525    }
526
527    /// Open an HDF5 file for SWMR reading with an explicit locking policy.
528    pub fn open_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
529        let reader = Hdf5Reader::open_swmr_with_locking(path.as_ref(), locking)?;
530        Ok(Self { reader })
531    }
532
533    /// Re-read the superblock and dataset metadata from disk.
534    ///
535    /// Call this periodically to pick up new data written by the concurrent
536    /// SWMR writer.
537    pub fn refresh(&mut self) -> Result<()> {
538        self.reader.refresh()?;
539        Ok(())
540    }
541
542    /// Return the names of all datasets.
543    pub fn dataset_names(&self) -> Vec<String> {
544        self.reader
545            .dataset_names()
546            .iter()
547            .map(|s| s.to_string())
548            .collect()
549    }
550
551    /// Return the current shape of a dataset.
552    pub fn dataset_shape(&self, name: &str) -> Result<Vec<u64>> {
553        Ok(self.reader.dataset_shape(name)?)
554    }
555
556    /// Read the raw bytes of a dataset.
557    pub fn read_dataset_raw(&mut self, name: &str) -> Result<Vec<u8>> {
558        Ok(self.reader.read_dataset_raw(name)?)
559    }
560
561    /// Read a dataset as a typed vector.
562    pub fn read_dataset<T: H5Type>(&mut self, name: &str) -> Result<Vec<T>> {
563        bytes_to_typed(self.reader.read_dataset_raw(name)?)
564    }
565
566    /// Read a slice (hyperslab) of a dataset as raw bytes.
567    ///
568    /// `starts[d]` is the first index along dimension `d`, `counts[d]` is how
569    /// many. For a streaming dataset this reads only the chunks the slice
570    /// overlaps — the efficient way for a live viewer to fetch the latest
571    /// frame without re-reading the whole stream.
572    pub fn read_slice_raw(
573        &mut self,
574        name: &str,
575        starts: &[u64],
576        counts: &[u64],
577    ) -> Result<Vec<u8>> {
578        Ok(self.reader.read_slice(name, starts, counts)?)
579    }
580
581    /// Read a slice (hyperslab) of a dataset as a typed vector.
582    /// See [`read_slice_raw`](Self::read_slice_raw).
583    pub fn read_slice<T: H5Type>(
584        &mut self,
585        name: &str,
586        starts: &[u64],
587        counts: &[u64],
588    ) -> Result<Vec<T>> {
589        bytes_to_typed(self.reader.read_slice(name, starts, counts)?)
590    }
591
592    /// Read a variable-length string dataset.
593    pub fn read_vlen_strings(&mut self, name: &str) -> Result<Vec<String>> {
594        Ok(self.reader.read_vlen_strings(name)?)
595    }
596
597    /// Element size of a dataset's datatype, in bytes — enough to size a
598    /// read buffer without knowing the concrete element type at compile time.
599    pub fn dataset_element_size(&self, name: &str) -> Result<usize> {
600        self.reader
601            .dataset_info(name)
602            .map(|i| i.datatype.element_size() as usize)
603            .ok_or_else(|| crate::error::Hdf5Error::NotFound(name.to_string()))
604    }
605
606    /// All group paths in the file.
607    pub fn group_paths(&self) -> Vec<String> {
608        self.reader.group_paths().iter().cloned().collect()
609    }
610
611    /// Whether a group exists. A leading `/` is tolerated.
612    pub fn has_group(&self, group_path: &str) -> bool {
613        self.reader.has_group(group_path.trim_start_matches('/'))
614    }
615
616    /// Names of the attributes on a dataset.
617    pub fn dataset_attr_names(&self, name: &str) -> Result<Vec<String>> {
618        Ok(self.reader.dataset_attr_names(name)?)
619    }
620
621    /// Read a dataset's attribute as a string (e.g. `units`, `NX_class`).
622    pub fn dataset_attr_string(&mut self, dataset: &str, attr: &str) -> Result<String> {
623        let a = self.reader.dataset_attr(dataset, attr)?.clone();
624        Ok(self.reader.attr_string_value(&a)?)
625    }
626
627    /// Names of the attributes on a group, or on the root group when
628    /// `group_path` is `"/"`. A leading `/` is tolerated.
629    pub fn group_attr_names(&self, group_path: &str) -> Vec<String> {
630        if group_path == "/" {
631            self.reader.root_attr_names()
632        } else {
633            self.reader
634                .group_attr_names(group_path.trim_start_matches('/'))
635        }
636    }
637
638    /// Read a group's attribute as a string (the NeXus `NX_class` etc.), or
639    /// a root attribute when `group_path` is `"/"`. A leading `/` is tolerated.
640    pub fn group_attr_string(&mut self, group_path: &str, attr: &str) -> Result<String> {
641        let a = if group_path == "/" {
642            self.reader.root_attr(attr)
643        } else {
644            self.reader
645                .group_attr(group_path.trim_start_matches('/'), attr)
646        }
647        .ok_or_else(|| crate::error::Hdf5Error::NotFound(attr.to_string()))?
648        .clone();
649        Ok(self.reader.attr_string_value(&a)?)
650    }
651}
652
653/// Reinterpret a raw byte buffer as a typed vector. The buffer length must
654/// be a whole multiple of `T`'s element size.
655fn bytes_to_typed<T: H5Type>(raw: Vec<u8>) -> Result<Vec<T>> {
656    let es = T::element_size();
657    if es == 0 || !raw.len().is_multiple_of(es) {
658        return Err(crate::error::Hdf5Error::TypeMismatch(format!(
659            "raw data size {} is not a multiple of element size {es}",
660            raw.len()
661        )));
662    }
663    let count = raw.len() / es;
664    let mut result = Vec::<T>::with_capacity(count);
665    // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
666    // bytes wide, so the byte buffer is a valid array of `count` `T`s.
667    unsafe {
668        std::ptr::copy_nonoverlapping(raw.as_ptr(), result.as_mut_ptr() as *mut u8, raw.len());
669        result.set_len(count);
670    }
671    Ok(result)
672}
673
674/// Raw bytes of one `H5Type` scalar.
675fn scalar_to_bytes<T: H5Type>(value: &T) -> Vec<u8> {
676    let es = T::element_size();
677    // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
678    // bytes wide.
679    unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) }.to_vec()
680}
681
682/// Raw bytes of an `H5Type` slice, in element order.
683fn slice_to_bytes<T: H5Type>(data: &[T]) -> &[u8] {
684    // Safety: as `scalar_to_bytes`; the slice is a contiguous POD array.
685    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
686}