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