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