Skip to main content

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}