rust_hdf5/attribute.rs
1//! Attribute support.
2//!
3//! Attributes are small metadata items attached to datasets (or groups).
4//! They are created via the [`AttrBuilder`] API obtained from
5//! [`H5Dataset::new_attr`](crate::dataset::H5Dataset::new_attr).
6//!
7//! # Example
8//!
9//! ```no_run
10//! use rust_hdf5::H5File;
11//! use rust_hdf5::types::VarLenUnicode;
12//!
13//! let file = H5File::create("attrs.h5").unwrap();
14//! let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
15//! let attr = ds.new_attr::<VarLenUnicode>().shape(()).create("units").unwrap();
16//! attr.write_scalar(&VarLenUnicode("meters".to_string())).unwrap();
17//! ```
18
19use std::marker::PhantomData;
20
21use crate::format::messages::attribute::AttributeMessage;
22use crate::format::messages::datatype::DatatypeMessage;
23use crate::format::reference::Reference;
24
25use crate::error::{Hdf5Error, Result};
26use crate::file::{borrow_inner, borrow_inner_mut, clone_inner, H5FileInner, SharedInner};
27use crate::types::VarLenUnicode;
28
29/// The scalar attribute message for a caller-supplied datatype and raw value,
30/// which every `set_attr_typed` / `set_*_attr_typed` setter builds. `value`
31/// must be exactly one element of `datatype`.
32pub(crate) fn typed_scalar_attr(
33 name: &str,
34 datatype: DatatypeMessage,
35 value: Vec<u8>,
36) -> Result<AttributeMessage> {
37 if value.len() as u64 != u64::from(datatype.element_size()) {
38 return Err(Hdf5Error::InvalidState(format!(
39 "attribute '{name}' was given {} bytes for a datatype whose element is {}",
40 value.len(),
41 datatype.element_size()
42 )));
43 }
44 Ok(AttributeMessage::scalar_numeric(name, datatype, value))
45}
46
47/// A handle to an HDF5 attribute.
48///
49/// After creating an attribute via [`AttrBuilder::create`], use
50/// [`write_scalar`](Self::write_scalar) or [`write_string`](Self::write_string)
51/// to set its value.
52///
53/// In read mode, use [`read_string`](Self::read_string) to read string attributes.
54pub struct H5Attribute {
55 file_inner: SharedInner,
56 ds_index: usize,
57 name: String,
58 /// Dimensions for write-mode array attributes (empty = scalar). Set from
59 /// [`AttrBuilder::shape`] and consumed by [`write_array`](Self::write_array).
60 write_dims: Vec<usize>,
61 /// The decoded attribute message for read-mode handles (carries the
62 /// datatype, needed to resolve variable-length string values).
63 read_attr: Option<AttributeMessage>,
64}
65
66impl H5Attribute {
67 /// Create a read-mode attribute handle from a decoded attribute message.
68 pub(crate) fn new_reader(file_inner: SharedInner, attr_msg: AttributeMessage) -> Self {
69 Self {
70 file_inner,
71 ds_index: usize::MAX,
72 name: attr_msg.name.clone(),
73 write_dims: Vec::new(),
74 read_attr: Some(attr_msg),
75 }
76 }
77
78 /// Return the attribute name.
79 pub fn name(&self) -> &str {
80 &self.name
81 }
82
83 /// Write a scalar value to the attribute.
84 ///
85 /// For `VarLenUnicode`, this writes a variable-length UTF-8 string
86 /// attribute — the string is stored in a global heap and h5py reads it
87 /// back as a Python `str` (matching the `VarLenUnicode` type name).
88 pub fn write_scalar(&self, value: &VarLenUnicode) -> Result<()> {
89 let inner = borrow_inner(&self.file_inner);
90 match &*inner {
91 H5FileInner::Writer(writer) => {
92 writer.set_vlen_string_attribute(
93 crate::io::writer::AttrTarget::Dataset(self.ds_index),
94 &self.name,
95 &value.0,
96 )?;
97 Ok(())
98 }
99 H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
100 "cannot write attributes in read mode".into(),
101 )),
102 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
103 }
104 }
105
106 /// Write a string value to the attribute (convenience method).
107 pub fn write_string(&self, value: &str) -> Result<()> {
108 self.write_scalar(&VarLenUnicode(value.to_string()))
109 }
110
111 /// Write a variable-length UTF-8 string **array** attribute.
112 ///
113 /// The attribute is given its shape via [`AttrBuilder::shape`] (any rank;
114 /// empty = scalar) and `values` must hold exactly the product of those
115 /// dimensions, supplied in row-major order. Every element is stored in one
116 /// shared global heap collection; h5py reads the attribute back as a numpy
117 /// array of Python `str` with that shape. The string-array counterpart of
118 /// [`write_string`](Self::write_string) / [`write_array`](Self::write_array).
119 pub fn write_string_array(&self, values: &[&str]) -> Result<()> {
120 // Product of an empty shape is 1 (a scalar holds one element).
121 let expected: usize = self.write_dims.iter().product();
122 if values.len() != expected {
123 return Err(Hdf5Error::InvalidState(format!(
124 "attribute '{}' shape {:?} needs {} elements, got {}",
125 self.name,
126 self.write_dims,
127 expected,
128 values.len()
129 )));
130 }
131 let dims_u64: Vec<u64> = self.write_dims.iter().map(|&d| d as u64).collect();
132 let mut inner = borrow_inner_mut(&self.file_inner);
133 match &mut *inner {
134 H5FileInner::Writer(writer) => {
135 writer.set_vlen_string_array_attribute(
136 crate::io::writer::AttrTarget::Dataset(self.ds_index),
137 &self.name,
138 values,
139 &dims_u64,
140 )?;
141 Ok(())
142 }
143 H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
144 "cannot write attributes in read mode".into(),
145 )),
146 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
147 }
148 }
149
150 /// Write an object-reference attribute — h5py's
151 /// `ds.attrs['source'] = f['/data'].ref`.
152 ///
153 /// The attribute is given its shape via [`AttrBuilder::shape`] (empty =
154 /// the scalar shape a single reference takes) and `paths` must hold
155 /// exactly the product of those dimensions, in row-major order. Each path
156 /// names a dataset or a group (`/` is the root group) and must already
157 /// exist; what reaches the file is the target's object header address,
158 /// which is assigned when the file is finalized.
159 ///
160 /// ```no_run
161 /// # use rust_hdf5::H5File;
162 /// let file = H5File::create("refs.h5").unwrap();
163 /// let ds = file.new_dataset::<f32>().shape(&[4]).create("data").unwrap();
164 /// let attr = ds.new_attr::<u64>().shape(()).create("self").unwrap();
165 /// attr.write_object_references(&["/data"]).unwrap();
166 /// ```
167 pub fn write_object_references(&self, paths: &[&str]) -> Result<()> {
168 // Product of an empty shape is 1 (a scalar holds one element).
169 let expected: usize = self.write_dims.iter().product();
170 if paths.len() != expected {
171 return Err(Hdf5Error::InvalidState(format!(
172 "attribute '{}' shape {:?} needs {} references, got {}",
173 self.name,
174 self.write_dims,
175 expected,
176 paths.len()
177 )));
178 }
179 let dims_u64: Vec<u64> = self.write_dims.iter().map(|&d| d as u64).collect();
180 let inner = borrow_inner(&self.file_inner);
181 match &*inner {
182 H5FileInner::Writer(writer) => {
183 writer.set_object_reference_attribute(
184 crate::io::writer::AttrTarget::Dataset(self.ds_index),
185 &self.name,
186 paths,
187 &dims_u64,
188 )?;
189 Ok(())
190 }
191 H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
192 "cannot write attributes in read mode".into(),
193 )),
194 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
195 }
196 }
197
198 /// Write a numeric scalar attribute.
199 ///
200 /// ```no_run
201 /// # use rust_hdf5::H5File;
202 /// let file = H5File::create("num_attr.h5").unwrap();
203 /// let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
204 /// ds.write_raw(&[0.0f32; 10]).unwrap();
205 /// let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
206 /// attr.write_numeric(&3.14f64).unwrap();
207 /// ```
208 pub fn write_numeric<T: crate::types::H5Type>(&self, value: &T) -> Result<()> {
209 let es = T::element_size();
210 let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
211 let attr_msg = AttributeMessage::scalar_numeric(&self.name, T::hdf5_type(), raw.to_vec());
212
213 let inner = borrow_inner(&self.file_inner);
214 match &*inner {
215 H5FileInner::Writer(writer) => {
216 writer.add_dataset_attribute(self.ds_index, attr_msg)?;
217 Ok(())
218 }
219 H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
220 "cannot write attributes in read mode".into(),
221 )),
222 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
223 }
224 }
225
226 /// Write a numeric array attribute.
227 ///
228 /// The number of `values` must equal the product of the dimensions set
229 /// via [`AttrBuilder::shape`]; if no shape was set the attribute is a
230 /// scalar and exactly one value is required. The on-disk datatype is
231 /// `T::hdf5_type()` and the dataspace is the simple dataspace described by
232 /// the shape — matching the 1-D `int32` array attributes AreaDetector
233 /// writes (e.g. `NDArrayDimOffset`, `Binning`, `Reverse`).
234 ///
235 /// ```no_run
236 /// # use rust_hdf5::H5File;
237 /// let file = H5File::create("arr_attr.h5").unwrap();
238 /// let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
239 /// ds.write_raw(&[0.0f32; 10]).unwrap();
240 /// let attr = ds.new_attr::<i32>().shape([3]).create("dim_offset").unwrap();
241 /// attr.write_array(&[0i32, 4, 8]).unwrap();
242 /// ```
243 pub fn write_array<T: crate::types::H5Type>(&self, values: &[T]) -> Result<()> {
244 // Product of an empty shape is 1 (a scalar holds one element).
245 let expected: usize = self.write_dims.iter().product();
246 if values.len() != expected {
247 return Err(Hdf5Error::InvalidState(format!(
248 "attribute '{}' shape {:?} needs {} elements, got {}",
249 self.name,
250 self.write_dims,
251 expected,
252 values.len()
253 )));
254 }
255
256 let es = T::element_size();
257 // Safety: `T: H5Type` is a `Copy` numeric primitive with a defined
258 // byte representation; `element_size()` matches `size_of::<T>()`. The
259 // slice borrows `values` only for this call.
260 let raw =
261 unsafe { std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * es) };
262 let dims_u64: Vec<u64> = self.write_dims.iter().map(|&d| d as u64).collect();
263 let attr_msg =
264 AttributeMessage::array_numeric(&self.name, T::hdf5_type(), &dims_u64, raw.to_vec());
265
266 let inner = borrow_inner(&self.file_inner);
267 match &*inner {
268 H5FileInner::Writer(writer) => {
269 writer.add_dataset_attribute(self.ds_index, attr_msg)?;
270 Ok(())
271 }
272 H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
273 "cannot write attributes in read mode".into(),
274 )),
275 H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
276 }
277 }
278
279 /// Read a numeric scalar attribute.
280 ///
281 /// ```no_run
282 /// # use rust_hdf5::H5File;
283 /// let file = H5File::open("num_attr.h5").unwrap();
284 /// let ds = file.dataset("data").unwrap();
285 /// let attr = ds.attr("scale").unwrap();
286 /// let val: f64 = attr.read_numeric().unwrap();
287 /// ```
288 pub fn read_numeric<T: crate::types::H5Type>(&self) -> Result<T> {
289 let attr = self
290 .read_attr
291 .as_ref()
292 .ok_or_else(|| Hdf5Error::InvalidState("attribute has no read data".into()))?;
293 // The byte image is reinterpreted as `T` below, so the stored
294 // datatype must be exactly what `T` writes — the same message
295 // `write_numeric` would emit. This is what rejects reading a
296 // big-endian or differently-classed attribute as `T` bit-for-bit
297 // garbage; a converting read is `read_numeric_as`.
298 let expected = T::hdf5_type();
299 if attr.datatype != expected {
300 return Err(Hdf5Error::TypeMismatch(format!(
301 "attribute datatype '{}' is not the requested type's datatype '{}'; \
302 use read_numeric_as for a converting read or read_raw for the bytes",
303 attr.datatype, expected
304 )));
305 }
306 let data = &attr.data;
307 let es = T::element_size();
308 if data.len() < es {
309 return Err(Hdf5Error::TypeMismatch(format!(
310 "attribute data {} bytes, need {} for type",
311 data.len(),
312 es
313 )));
314 }
315 unsafe {
316 let mut val = std::mem::MaybeUninit::<T>::uninit();
317 std::ptr::copy_nonoverlapping(data.as_ptr(), val.as_mut_ptr() as *mut u8, es);
318 Ok(val.assume_init())
319 }
320 }
321
322 /// Read a numeric attribute as `T`, converting each element from the
323 /// stored datatype — the attribute counterpart of
324 /// [`H5Dataset::read_numeric_as`](crate::dataset::H5Dataset::read_numeric_as).
325 ///
326 /// Returns every element (one for a scalar attribute), with the same
327 /// policy: integer → integer is checked and errors with the element index
328 /// and value instead of wrapping, `f32` → `f64` widens exactly, and
329 /// `f64` → `f32`, float ↔ integer, and non-numeric datatypes are
330 /// rejected. Big-endian sources are decoded per the stored byte order,
331 /// which [`read_numeric`](Self::read_numeric) refuses.
332 pub fn read_numeric_as<T: crate::dataset::ReadNumeric>(&self) -> Result<Vec<T>> {
333 let attr = self
334 .read_attr
335 .as_ref()
336 .ok_or_else(|| Hdf5Error::InvalidState("attribute has no read data".into()))?;
337 let kind = crate::dataset::numeric::classify(&attr.datatype)?;
338 crate::dataset::numeric::convert(kind, &attr.data)
339 }
340
341 /// Read the attribute value as a string.
342 ///
343 /// Handles both fixed-length string attributes and variable-length
344 /// string attributes (h5py's default), resolving a vlen value through
345 /// the global heap.
346 pub fn read_string(&self) -> Result<String> {
347 let attr = self.read_attr.as_ref().ok_or_else(|| {
348 Hdf5Error::InvalidState("attribute has no read data (write-mode handle?)".into())
349 })?;
350 let mut inner = borrow_inner_mut(&self.file_inner);
351 match &mut *inner {
352 H5FileInner::Reader(reader) => Ok(reader.attr_string_value(attr)?),
353 _ => {
354 // No reader available, so a variable-length value cannot be
355 // resolved through the heap — but a fixed-length one is right
356 // here, and its padding rule is read the same way it is on the
357 // reader path.
358 Ok(crate::io::reader::fixed_string_attr_value(attr)?)
359 }
360 }
361 }
362
363 /// Return the attribute datatype as parsed from the file (read mode only).
364 ///
365 /// Mirrors [`H5Dataset::datatype`](crate::dataset::H5Dataset::datatype):
366 /// it exposes the full datatype — class (integer vs floating-point vs
367 /// string vs compound …), signedness, byte order and bit precision — so
368 /// callers mapping an attribute to a NumPy / Arrow dtype need not infer a
369 /// type from the byte width, which cannot distinguish `u8` from `i8` (both
370 /// 1 byte) or `i32` from `f32` (both 4 bytes).
371 ///
372 /// # Errors
373 ///
374 /// Returns an error for a write-mode handle, which carries no decoded
375 /// attribute message.
376 ///
377 /// ```no_run
378 /// # use rust_hdf5::{H5File, DatatypeMessage};
379 /// let file = H5File::open("data.h5").unwrap();
380 /// let ds = file.dataset("image").unwrap();
381 /// let attr = ds.attr("scale").unwrap();
382 /// match attr.datatype().unwrap() {
383 /// DatatypeMessage::FloatingPoint { size, .. } => println!("float: {size} bytes"),
384 /// other => println!("other type: {other}"),
385 /// }
386 /// ```
387 pub fn datatype(&self) -> Result<DatatypeMessage> {
388 self.read_attr
389 .as_ref()
390 .map(|a| a.datatype.clone())
391 .ok_or_else(|| {
392 Hdf5Error::InvalidState("attribute has no read data (write-mode handle?)".into())
393 })
394 }
395
396 /// Read a reference attribute's elements, each resolved to the object it
397 /// names — the attribute counterpart of
398 /// [`H5Dataset::read_references`](crate::dataset::H5Dataset::read_references).
399 ///
400 /// ```no_run
401 /// # use rust_hdf5::H5File;
402 /// let file = H5File::open("refs.h5").unwrap();
403 /// let ds = file.dataset("image").unwrap();
404 /// let target = ds.attr("source").unwrap().read_references().unwrap();
405 /// println!("{:?}", target[0].path());
406 /// ```
407 pub fn read_references(&self) -> Result<Vec<Reference>> {
408 let attr = self.read_attr.as_ref().ok_or_else(|| {
409 Hdf5Error::InvalidState("attribute has no read data (write-mode handle?)".into())
410 })?;
411 let mut inner = borrow_inner_mut(&self.file_inner);
412 match &mut *inner {
413 H5FileInner::Reader(reader) => Ok(reader.attr_references(attr)?),
414 // References name file addresses, so resolving them needs the
415 // reader's view of the file.
416 _ => Err(Hdf5Error::InvalidState(
417 "cannot read references from an attribute in write mode".into(),
418 )),
419 }
420 }
421
422 /// Read the raw attribute data bytes.
423 pub fn read_raw(&self) -> Result<Vec<u8>> {
424 self.read_attr
425 .as_ref()
426 .map(|a| a.data.clone())
427 .ok_or_else(|| {
428 Hdf5Error::InvalidState("attribute has no read data (write-mode handle?)".into())
429 })
430 }
431}
432
433/// Shapes accepted by [`AttrBuilder::shape`].
434///
435/// `()` selects a scalar attribute (rank 0). A slice, array, or `Vec` of
436/// `usize` selects a simple dataspace with those dimension sizes — e.g.
437/// `[3]` for a 1-D array attribute of length 3.
438pub trait AttrShape {
439 /// Dimension sizes for the attribute; empty for a scalar.
440 fn attr_dims(&self) -> Vec<usize>;
441}
442
443impl AttrShape for () {
444 fn attr_dims(&self) -> Vec<usize> {
445 Vec::new()
446 }
447}
448
449impl AttrShape for &[usize] {
450 fn attr_dims(&self) -> Vec<usize> {
451 self.to_vec()
452 }
453}
454
455impl<const N: usize> AttrShape for [usize; N] {
456 fn attr_dims(&self) -> Vec<usize> {
457 self.to_vec()
458 }
459}
460
461impl<const N: usize> AttrShape for &[usize; N] {
462 fn attr_dims(&self) -> Vec<usize> {
463 self.to_vec()
464 }
465}
466
467impl AttrShape for Vec<usize> {
468 fn attr_dims(&self) -> Vec<usize> {
469 self.clone()
470 }
471}
472
473/// A fluent builder for creating attributes on datasets.
474///
475/// Obtained from [`H5Dataset::new_attr::<T>()`](crate::dataset::H5Dataset::new_attr).
476pub struct AttrBuilder<'a, T> {
477 file_inner: &'a SharedInner,
478 ds_index: usize,
479 dims: Vec<usize>,
480 _marker: PhantomData<T>,
481}
482
483impl<'a, T> AttrBuilder<'a, T> {
484 pub(crate) fn new(file_inner: &'a SharedInner, ds_index: usize) -> Self {
485 Self {
486 file_inner,
487 ds_index,
488 dims: Vec::new(),
489 _marker: PhantomData,
490 }
491 }
492
493 /// Set the attribute shape.
494 ///
495 /// Use `()` for a scalar attribute, or an array/slice of dimension sizes
496 /// for an array attribute (e.g. `[3]` for a 1-D array of length 3). The
497 /// shape is consumed by [`H5Attribute::write_array`]; the scalar writers
498 /// ([`write_scalar`](H5Attribute::write_scalar),
499 /// [`write_numeric`](H5Attribute::write_numeric)) ignore it.
500 #[must_use]
501 pub fn shape<S: AttrShape>(mut self, shape: S) -> Self {
502 self.dims = shape.attr_dims();
503 self
504 }
505
506 /// Create the attribute with the given name.
507 ///
508 /// The attribute is created but does not yet have a value.
509 /// Call [`H5Attribute::write_scalar`] or
510 /// [`H5Attribute::write_array`] to set the value.
511 pub fn create(self, name: &str) -> Result<H5Attribute> {
512 Ok(H5Attribute {
513 file_inner: clone_inner(self.file_inner),
514 ds_index: self.ds_index,
515 name: name.to_string(),
516 write_dims: self.dims,
517 read_attr: None,
518 })
519 }
520}