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