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}