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