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 fixed-shape multi-dimensional grid dataset that fills at
217 /// explicit positions as frames arrive.
218 ///
219 /// Unlike [`create_streaming_dataset`](Self::create_streaming_dataset),
220 /// which appends frames along a single unlimited leading axis, this
221 /// creates a dataset of the full bounded shape `dims` (no unlimited axis)
222 /// and lets you place each frame at an arbitrary chunk position with
223 /// [`write_chunk_at`](Self::write_chunk_at). This mirrors AreaDetector's
224 /// "extra dimensions" layout, where a scan of known size (e.g.
225 /// `[Na, Nb, H, W]`) is filled in odometer order. `chunk` is the per-chunk
226 /// shape of the same rank (typically `[1, …, 1, H, W]`). Returns the
227 /// dataset index.
228 pub fn create_grid_dataset<T: H5Type>(
229 &mut self,
230 name: &str,
231 dims: &[u64],
232 chunk: &[u64],
233 ) -> Result<usize> {
234 let idx = self
235 .inner
236 .create_grid_dataset(name, T::hdf5_type(), dims, chunk)?;
237 Ok(idx)
238 }
239
240 /// Create a hard link: an additional name for a dataset or group that
241 /// already exists in the file.
242 ///
243 /// No data is copied — the link and its target share one object header,
244 /// exactly as `h5py` / libhdf5 hard links do. This is the NeXus-style way
245 /// to expose a streaming dataset at an aliased path.
246 ///
247 /// * `parent_group_path` — full path of the group that will hold the
248 /// link (`"/"` for the root group).
249 /// * `link_name` — leaf name of the new link within that group.
250 /// * `target_path` — full path of an existing dataset or group.
251 ///
252 /// # Visibility relative to SWMR mode
253 ///
254 /// A link created **before** [`start_swmr`](Self::start_swmr) is committed
255 /// by `start_swmr` and is visible to SWMR readers for the whole streaming
256 /// window. A link created **after** `start_swmr` is committed only by
257 /// [`close`](Self::close); it does not appear to readers that attach
258 /// during the live SWMR window. Create layout links before `start_swmr`
259 /// when readers must resolve them while streaming.
260 pub fn create_hard_link(
261 &mut self,
262 parent_group_path: &str,
263 link_name: &str,
264 target_path: &str,
265 ) -> Result<()> {
266 self.inner
267 .writer_mut()
268 .create_hard_link(parent_group_path, link_name, target_path)?;
269 Ok(())
270 }
271
272 /// Create a group in the file hierarchy.
273 ///
274 /// * `parent_group_path` — full path of the parent group (`"/"` for the
275 /// root group).
276 /// * `name` — leaf name of the new group.
277 ///
278 /// A nested NeXus layout is built one level at a time, parent first:
279 ///
280 /// ```no_run
281 /// # use rust_hdf5::swmr::SwmrFileWriter;
282 /// # let mut writer = SwmrFileWriter::create("stream.h5").unwrap();
283 /// writer.create_group("/", "entry").unwrap();
284 /// writer.create_group("/entry", "data").unwrap();
285 /// ```
286 ///
287 /// Like [`create_hard_link`](Self::create_hard_link), a group created
288 /// before [`start_swmr`](Self::start_swmr) is visible to SWMR readers for
289 /// the whole streaming window; one created after is committed only by
290 /// [`close`](Self::close).
291 pub fn create_group(&mut self, parent_group_path: &str, name: &str) -> Result<()> {
292 self.inner
293 .writer_mut()
294 .create_group(parent_group_path, name)?;
295 Ok(())
296 }
297
298 /// Set a string attribute on a group, or on the root group when
299 /// `group_path` is `"/"`.
300 ///
301 /// This is the NeXus way to tag a group with its class — for example
302 /// `set_group_attr_string("/entry", "NX_class", "NXentry")`. An existing
303 /// attribute of the same name is replaced.
304 ///
305 /// Attributes must be set before [`start_swmr`](Self::start_swmr):
306 /// object headers are frozen while readers stream, so every attribute
307 /// setter is refused once SWMR is active — libhdf5's rule for SWMR
308 /// writes too.
309 pub fn set_group_attr_string(
310 &mut self,
311 group_path: &str,
312 name: &str,
313 value: &str,
314 ) -> Result<()> {
315 self.inner.writer_mut().set_vlen_string_attribute(
316 group_attr_target(group_path),
317 name,
318 value,
319 )?;
320 Ok(())
321 }
322
323 /// Set a numeric scalar attribute on a group, or on the root group when
324 /// `group_path` is `"/"`. An existing attribute of the same name is
325 /// replaced. Refused after [`start_swmr`](Self::start_swmr) — see
326 /// [`set_group_attr_string`](Self::set_group_attr_string).
327 pub fn set_group_attr_numeric<T: H5Type>(
328 &mut self,
329 group_path: &str,
330 name: &str,
331 value: &T,
332 ) -> Result<()> {
333 let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
334 self.inner
335 .writer_mut()
336 .set_attribute(group_attr_target(group_path), attr)?;
337 Ok(())
338 }
339
340 /// Create a fixed-shape (non-streaming) dataset and write all its data in
341 /// one call. Returns the dataset index.
342 ///
343 /// This is for the NeXus metadata that surrounds the image stream —
344 /// coordinate axes, detector geometry, and (with `dims = &[]`) scalar
345 /// values such as `/entry/instrument/detector/distance`. Unlike a
346 /// streaming dataset, it is written once and not appended to.
347 pub fn write_dataset<T: H5Type>(
348 &mut self,
349 name: &str,
350 dims: &[u64],
351 data: &[T],
352 ) -> Result<usize> {
353 let expected: u64 = if dims.is_empty() {
354 1
355 } else {
356 dims.iter().product()
357 };
358 if data.len() as u64 != expected {
359 return Err(crate::error::Hdf5Error::InvalidState(format!(
360 "write_dataset: data has {} elements but shape {dims:?} needs {expected}",
361 data.len()
362 )));
363 }
364 let idx = self
365 .inner
366 .writer_mut()
367 .create_dataset(name, T::hdf5_type(), dims)?;
368 self.inner
369 .writer_mut()
370 .write_dataset_raw(idx, slice_to_bytes(data))?;
371 Ok(idx)
372 }
373
374 /// Create a variable-length string dataset (one element per string).
375 /// Returns the dataset index. Useful for NeXus metadata such as
376 /// `/entry/start_time` or per-frame timestamp arrays.
377 pub fn write_string_dataset(&mut self, name: &str, strings: &[&str]) -> Result<usize> {
378 let idx = self
379 .inner
380 .writer_mut()
381 .create_vlen_string_dataset(name, strings)?;
382 Ok(idx)
383 }
384
385 /// Set a string attribute on a dataset, addressed by its index. The
386 /// NeXus way to record `units`, `long_name`, `signal`, etc. An existing
387 /// attribute of the same name is replaced. Refused after
388 /// [`start_swmr`](Self::start_swmr) — see
389 /// [`set_group_attr_string`](Self::set_group_attr_string).
390 pub fn set_dataset_attr_string(
391 &mut self,
392 ds_index: usize,
393 name: &str,
394 value: &str,
395 ) -> Result<()> {
396 self.inner.writer_mut().set_vlen_string_attribute(
397 crate::io::writer::AttrTarget::Dataset(ds_index),
398 name,
399 value,
400 )?;
401 Ok(())
402 }
403
404 /// Set a numeric scalar attribute on a dataset, addressed by its index.
405 /// An existing attribute of the same name is replaced. Refused after
406 /// [`start_swmr`](Self::start_swmr) — see
407 /// [`set_group_attr_string`](Self::set_group_attr_string).
408 pub fn set_dataset_attr_numeric<T: H5Type>(
409 &mut self,
410 ds_index: usize,
411 name: &str,
412 value: &T,
413 ) -> Result<()> {
414 let attr = AttributeMessage::scalar_numeric(name, T::hdf5_type(), scalar_to_bytes(value));
415 self.inner
416 .writer_mut()
417 .add_dataset_attribute(ds_index, attr)?;
418 Ok(())
419 }
420
421 /// Set a numeric array attribute on a dataset, addressed by its index.
422 ///
423 /// `dims` are the dimension sizes (e.g. `&[3]` for a 1-D array) and the
424 /// number of `values` must equal their product. This is the SWMR
425 /// counterpart of [`H5Attribute::write_array`](crate::H5Attribute::write_array),
426 /// for the length-ndims `int32` array attributes AreaDetector writes
427 /// (`NDArrayDimOffset`, `NDArrayDimBinning`, `NDArrayDimReverse`). An
428 /// existing attribute of the same name is replaced.
429 ///
430 /// In SWMR mode, attributes can only be added before
431 /// [`start_swmr`](Self::start_swmr); HDF5 forbids adding dataset
432 /// attributes once the file is in SWMR write mode. Resolve a dataset path
433 /// to its index with [`dataset_index`](Self::dataset_index).
434 pub fn set_dataset_attr_array<T: H5Type>(
435 &mut self,
436 ds_index: usize,
437 name: &str,
438 dims: &[u64],
439 values: &[T],
440 ) -> Result<()> {
441 // Product of an empty shape is 1 (a scalar holds one element).
442 let expected: u64 = dims.iter().product();
443 if values.len() as u64 != expected {
444 return Err(crate::error::Hdf5Error::InvalidState(format!(
445 "set_dataset_attr_array: {} values but shape {dims:?} needs {expected}",
446 values.len()
447 )));
448 }
449 let attr = AttributeMessage::array_numeric(
450 name,
451 T::hdf5_type(),
452 dims,
453 slice_to_bytes(values).to_vec(),
454 );
455 self.inner
456 .writer_mut()
457 .add_dataset_attribute(ds_index, attr)?;
458 Ok(())
459 }
460
461 /// Set the fill value of a streaming dataset, addressed by its index.
462 ///
463 /// Call this before the first [`append_frame`](Self::append_frame): it
464 /// determines the value of chunk regions that are never written (a
465 /// partial final band, or unwritten tiles).
466 pub fn set_dataset_fill_value<T: H5Type>(&mut self, ds_index: usize, value: &T) -> Result<()> {
467 self.inner
468 .writer_mut()
469 .set_dataset_fill_value(ds_index, scalar_to_bytes(value))?;
470 Ok(())
471 }
472
473 /// Place an existing dataset inside a group.
474 ///
475 /// By default a dataset created through this writer lives at the root
476 /// level; this moves its link record into `group_path` (which must
477 /// already exist). The group must be created before `start_swmr` for the
478 /// placement to be visible to readers during streaming.
479 pub fn assign_dataset_to_group(&mut self, group_path: &str, ds_index: usize) -> Result<()> {
480 self.inner
481 .writer_mut()
482 .assign_dataset_to_group(group_path, ds_index)?;
483 Ok(())
484 }
485
486 /// Signal the start of SWMR mode.
487 pub fn start_swmr(&mut self) -> Result<()> {
488 self.inner.start_swmr()?;
489 Ok(())
490 }
491
492 /// Append a frame of raw data to a streaming dataset.
493 ///
494 /// The data size must match one frame (product of frame_dims * element_size).
495 pub fn append_frame(&mut self, ds_index: usize, data: &[u8]) -> Result<()> {
496 self.inner.append_frame(ds_index, data)?;
497 Ok(())
498 }
499
500 /// Write one frame at an explicit chunk position of a grid dataset
501 /// created with [`create_grid_dataset`](Self::create_grid_dataset).
502 ///
503 /// `chunk_coords` are in units of chunks (row-major over the chunk grid)
504 /// and `data` must be exactly one full chunk (`product(chunk) *
505 /// element_size` bytes; edge chunks are zero-padded by the caller). The
506 /// logical extent is fixed, so positions may be written in any order and
507 /// unwritten positions read back as fill. As with the streaming path,
508 /// call [`flush`](Self::flush) to make writes visible to SWMR readers and
509 /// set dataset attributes before [`start_swmr`](Self::start_swmr).
510 pub fn write_chunk_at(
511 &mut self,
512 ds_index: usize,
513 chunk_coords: &[u64],
514 data: &[u8],
515 ) -> Result<()> {
516 self.inner.write_chunk_at(ds_index, chunk_coords, data)?;
517 Ok(())
518 }
519
520 /// Flush all dataset index structures to disk with SWMR ordering.
521 pub fn flush(&mut self) -> Result<()> {
522 self.inner.flush()?;
523 Ok(())
524 }
525
526 /// Close and finalize the file.
527 pub fn close(self) -> Result<()> {
528 self.inner.close()?;
529 Ok(())
530 }
531}
532
533/// SWMR reader for monitoring a streaming HDF5 file.
534///
535/// Opens a file being written by a concurrent [`SwmrFileWriter`] and
536/// periodically calls [`refresh`](Self::refresh) to pick up new data.
537///
538/// ```no_run
539/// use rust_hdf5::swmr::SwmrFileReader;
540///
541/// let mut reader = SwmrFileReader::open("stream.h5").unwrap();
542///
543/// loop {
544/// reader.refresh().unwrap();
545/// let names = reader.dataset_names();
546/// if let Some(shape) = reader.dataset_shape("frames").ok() {
547/// println!("frames shape: {:?}", shape);
548/// if shape[0] > 0 {
549/// let data = reader.read_dataset_raw("frames").unwrap();
550/// println!("got {} bytes", data.len());
551/// break;
552/// }
553/// }
554/// std::thread::sleep(std::time::Duration::from_millis(100));
555/// }
556/// ```
557pub struct SwmrFileReader {
558 reader: Hdf5Reader,
559}
560
561impl SwmrFileReader {
562 /// Open an HDF5 file for SWMR reading using the env-var-derived
563 /// locking policy. Takes a shared lock so it coexists with the
564 /// downgraded shared lock held by [`SwmrFileWriter`] after
565 /// `start_swmr`, and with other concurrent SWMR readers.
566 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
567 let reader = Hdf5Reader::open_swmr(path.as_ref())?;
568 Ok(Self { reader })
569 }
570
571 /// Open an HDF5 file for SWMR reading with an explicit locking policy.
572 pub fn open_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self> {
573 let reader = Hdf5Reader::open_swmr_with_locking(path.as_ref(), locking)?;
574 Ok(Self { reader })
575 }
576
577 /// Re-read the superblock and dataset metadata from disk.
578 ///
579 /// Call this periodically to pick up new data written by the concurrent
580 /// SWMR writer.
581 pub fn refresh(&mut self) -> Result<()> {
582 self.reader.refresh()?;
583 Ok(())
584 }
585
586 /// Return the names of all datasets.
587 pub fn dataset_names(&self) -> Vec<String> {
588 self.reader
589 .dataset_names()
590 .iter()
591 .map(|s| s.to_string())
592 .collect()
593 }
594
595 /// Return the current shape of a dataset.
596 pub fn dataset_shape(&self, name: &str) -> Result<Vec<u64>> {
597 Ok(self.reader.dataset_shape(name)?)
598 }
599
600 /// Read the raw bytes of a dataset.
601 pub fn read_dataset_raw(&mut self, name: &str) -> Result<Vec<u8>> {
602 Ok(self.reader.read_dataset_raw(name)?)
603 }
604
605 /// Read a dataset as a typed vector.
606 ///
607 /// `T` must have the dataset's exact element width — the same
608 /// width-checked byte reinterpretation as
609 /// [`H5Dataset::read_raw`](crate::dataset::H5Dataset::read_raw), with no
610 /// datatype conversion. Use
611 /// [`dataset_element_size`](Self::dataset_element_size) to size-check a
612 /// type at runtime.
613 pub fn read_dataset<T: H5Type>(&mut self, name: &str) -> Result<Vec<T>> {
614 self.check_element_width::<T>(name)?;
615 bytes_to_typed(self.reader.read_dataset_raw(name)?)
616 }
617
618 /// Read a slice (hyperslab) of a dataset as raw bytes.
619 ///
620 /// `starts[d]` is the first index along dimension `d`, `counts[d]` is how
621 /// many. For a streaming dataset this reads only the chunks the slice
622 /// overlaps — the efficient way for a live viewer to fetch the latest
623 /// frame without re-reading the whole stream.
624 pub fn read_slice_raw(
625 &mut self,
626 name: &str,
627 starts: &[u64],
628 counts: &[u64],
629 ) -> Result<Vec<u8>> {
630 Ok(self.reader.read_slice(name, starts, counts)?)
631 }
632
633 /// Read a slice (hyperslab) of a dataset as a typed vector.
634 /// See [`read_slice_raw`](Self::read_slice_raw); `T`'s width is checked
635 /// like [`read_dataset`](Self::read_dataset).
636 pub fn read_slice<T: H5Type>(
637 &mut self,
638 name: &str,
639 starts: &[u64],
640 counts: &[u64],
641 ) -> Result<Vec<T>> {
642 self.check_element_width::<T>(name)?;
643 bytes_to_typed(self.reader.read_slice(name, starts, counts)?)
644 }
645
646 /// The width gate for the typed reads: without it, reinterpreting the
647 /// raw bytes splits or merges elements — an f64 dataset read as `i32`
648 /// passed the divisibility check and silently returned twice as many
649 /// garbage values.
650 fn check_element_width<T: H5Type>(&self, name: &str) -> Result<()> {
651 let stored = self.dataset_element_size(name)?;
652 if T::element_size() != stored {
653 return Err(crate::error::Hdf5Error::TypeMismatch(format!(
654 "read type has element size {} but dataset has element size {stored}",
655 T::element_size(),
656 )));
657 }
658 Ok(())
659 }
660
661 /// Read a variable-length string dataset.
662 pub fn read_vlen_strings(&mut self, name: &str) -> Result<Vec<String>> {
663 Ok(self.reader.read_vlen_strings(name)?)
664 }
665
666 /// Element size of a dataset's datatype, in bytes — enough to size a
667 /// read buffer without knowing the concrete element type at compile time.
668 pub fn dataset_element_size(&self, name: &str) -> Result<usize> {
669 self.reader
670 .dataset_info(name)
671 .map(|i| i.datatype.element_size() as usize)
672 .ok_or_else(|| crate::error::Hdf5Error::NotFound(name.to_string()))
673 }
674
675 /// All group paths in the file.
676 pub fn group_paths(&self) -> Vec<String> {
677 self.reader.group_paths().iter().cloned().collect()
678 }
679
680 /// Whether a group exists. A leading `/` is tolerated.
681 pub fn has_group(&self, group_path: &str) -> bool {
682 self.reader.has_group(group_path.trim_start_matches('/'))
683 }
684
685 /// Names of the attributes on a dataset.
686 pub fn dataset_attr_names(&self, name: &str) -> Result<Vec<String>> {
687 Ok(self.reader.dataset_attr_names(name)?)
688 }
689
690 /// Read a dataset's attribute as a string (e.g. `units`, `NX_class`).
691 pub fn dataset_attr_string(&mut self, dataset: &str, attr: &str) -> Result<String> {
692 let a = self.reader.dataset_attr(dataset, attr)?.clone();
693 Ok(self.reader.attr_string_value(&a)?)
694 }
695
696 /// Names of the attributes on a group, or on the root group when
697 /// `group_path` is `"/"`. A leading `/` is tolerated.
698 pub fn group_attr_names(&self, group_path: &str) -> Vec<String> {
699 if group_path == "/" {
700 self.reader.root_attr_names()
701 } else {
702 self.reader
703 .group_attr_names(group_path.trim_start_matches('/'))
704 }
705 }
706
707 /// Read a group's attribute as a string (the NeXus `NX_class` etc.), or
708 /// a root attribute when `group_path` is `"/"`. A leading `/` is tolerated.
709 pub fn group_attr_string(&mut self, group_path: &str, attr: &str) -> Result<String> {
710 let a = if group_path == "/" {
711 self.reader.root_attr(attr)
712 } else {
713 self.reader
714 .group_attr(group_path.trim_start_matches('/'), attr)
715 }
716 .ok_or_else(|| crate::error::Hdf5Error::NotFound(attr.to_string()))?
717 .clone();
718 Ok(self.reader.attr_string_value(&a)?)
719 }
720}
721
722/// The writer-side attribute list a group path names: `"/"` is the
723/// file-level (root) list, anything else the group's own.
724fn group_attr_target(group_path: &str) -> crate::io::writer::AttrTarget<'_> {
725 if group_path == "/" {
726 crate::io::writer::AttrTarget::Root
727 } else {
728 crate::io::writer::AttrTarget::Group(group_path)
729 }
730}
731
732/// Reinterpret a raw byte buffer as a typed vector. The buffer length must
733/// be a whole multiple of `T`'s element size.
734fn bytes_to_typed<T: H5Type>(raw: Vec<u8>) -> Result<Vec<T>> {
735 let es = T::element_size();
736 if es == 0 || !raw.len().is_multiple_of(es) {
737 return Err(crate::error::Hdf5Error::TypeMismatch(format!(
738 "raw data size {} is not a multiple of element size {es}",
739 raw.len()
740 )));
741 }
742 let count = raw.len() / es;
743 let mut result = Vec::<T>::with_capacity(count);
744 // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
745 // bytes wide, so the byte buffer is a valid array of `count` `T`s.
746 unsafe {
747 std::ptr::copy_nonoverlapping(raw.as_ptr(), result.as_mut_ptr() as *mut u8, raw.len());
748 result.set_len(count);
749 }
750 Ok(result)
751}
752
753/// Raw bytes of one `H5Type` scalar.
754fn scalar_to_bytes<T: H5Type>(value: &T) -> Vec<u8> {
755 let es = T::element_size();
756 // Safety: `T: H5Type` is a `Copy` POD primitive exactly `element_size()`
757 // bytes wide.
758 unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) }.to_vec()
759}
760
761/// Raw bytes of an `H5Type` slice, in element order.
762fn slice_to_bytes<T: H5Type>(data: &[T]) -> &[u8] {
763 // Safety: as `scalar_to_bytes`; the slice is a contiguous POD array.
764 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
765}