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;
22
23use crate::error::{Hdf5Error, Result};
24use crate::file::{borrow_inner_mut, clone_inner, H5FileInner, SharedInner};
25use crate::types::VarLenUnicode;
26
27/// A handle to an HDF5 attribute.
28///
29/// After creating an attribute via [`AttrBuilder::create`], use
30/// [`write_scalar`](Self::write_scalar) or [`write_string`](Self::write_string)
31/// to set its value.
32///
33/// In read mode, use [`read_string`](Self::read_string) to read string attributes.
34pub struct H5Attribute {
35    file_inner: SharedInner,
36    ds_index: usize,
37    name: String,
38    /// The decoded attribute message for read-mode handles (carries the
39    /// datatype, needed to resolve variable-length string values).
40    read_attr: Option<AttributeMessage>,
41}
42
43impl H5Attribute {
44    /// Create a read-mode attribute handle from a decoded attribute message.
45    pub(crate) fn new_reader(file_inner: SharedInner, attr_msg: AttributeMessage) -> Self {
46        Self {
47            file_inner,
48            ds_index: usize::MAX,
49            name: attr_msg.name.clone(),
50            read_attr: Some(attr_msg),
51        }
52    }
53
54    /// Return the attribute name.
55    pub fn name(&self) -> &str {
56        &self.name
57    }
58
59    /// Write a scalar value to the attribute.
60    ///
61    /// For `VarLenUnicode`, this writes a fixed-length string attribute
62    /// whose size is determined by the string value.
63    pub fn write_scalar(&self, value: &VarLenUnicode) -> Result<()> {
64        let attr_msg = AttributeMessage::scalar_string(&self.name, &value.0);
65
66        let mut inner = borrow_inner_mut(&self.file_inner);
67        match &mut *inner {
68            H5FileInner::Writer(writer) => {
69                writer.add_dataset_attribute(self.ds_index, attr_msg)?;
70                Ok(())
71            }
72            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
73                "cannot write attributes in read mode".into(),
74            )),
75            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
76        }
77    }
78
79    /// Write a string value to the attribute (convenience method).
80    pub fn write_string(&self, value: &str) -> Result<()> {
81        self.write_scalar(&VarLenUnicode(value.to_string()))
82    }
83
84    /// Write a numeric scalar attribute.
85    ///
86    /// ```no_run
87    /// # use rust_hdf5::H5File;
88    /// let file = H5File::create("num_attr.h5").unwrap();
89    /// let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
90    /// ds.write_raw(&[0.0f32; 10]).unwrap();
91    /// let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
92    /// attr.write_numeric(&3.14f64).unwrap();
93    /// ```
94    pub fn write_numeric<T: crate::types::H5Type>(&self, value: &T) -> Result<()> {
95        let es = T::element_size();
96        let raw = unsafe { std::slice::from_raw_parts(value as *const T as *const u8, es) };
97        let attr_msg = AttributeMessage::scalar_numeric(&self.name, T::hdf5_type(), raw.to_vec());
98
99        let mut inner = borrow_inner_mut(&self.file_inner);
100        match &mut *inner {
101            H5FileInner::Writer(writer) => {
102                writer.add_dataset_attribute(self.ds_index, attr_msg)?;
103                Ok(())
104            }
105            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
106                "cannot write attributes in read mode".into(),
107            )),
108            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
109        }
110    }
111
112    /// Read a numeric scalar attribute.
113    ///
114    /// ```no_run
115    /// # use rust_hdf5::H5File;
116    /// let file = H5File::open("num_attr.h5").unwrap();
117    /// let ds = file.dataset("data").unwrap();
118    /// let attr = ds.attr("scale").unwrap();
119    /// let val: f64 = attr.read_numeric().unwrap();
120    /// ```
121    pub fn read_numeric<T: crate::types::H5Type>(&self) -> Result<T> {
122        let data = self
123            .read_attr
124            .as_ref()
125            .map(|a| &a.data)
126            .ok_or_else(|| Hdf5Error::InvalidState("attribute has no read data".into()))?;
127        let es = T::element_size();
128        if data.len() < es {
129            return Err(Hdf5Error::TypeMismatch(format!(
130                "attribute data {} bytes, need {} for type",
131                data.len(),
132                es
133            )));
134        }
135        unsafe {
136            let mut val = std::mem::MaybeUninit::<T>::uninit();
137            std::ptr::copy_nonoverlapping(data.as_ptr(), val.as_mut_ptr() as *mut u8, es);
138            Ok(val.assume_init())
139        }
140    }
141
142    /// Read the attribute value as a string.
143    ///
144    /// Handles both fixed-length string attributes and variable-length
145    /// string attributes (h5py's default), resolving a vlen value through
146    /// the global heap.
147    pub fn read_string(&self) -> Result<String> {
148        let attr = self.read_attr.as_ref().ok_or_else(|| {
149            Hdf5Error::InvalidState("attribute has no read data (write-mode handle?)".into())
150        })?;
151        let mut inner = borrow_inner_mut(&self.file_inner);
152        match &mut *inner {
153            H5FileInner::Reader(reader) => Ok(reader.attr_string_value(attr)?),
154            _ => {
155                // No reader available — fall back to the raw fixed-length
156                // interpretation.
157                let end = attr
158                    .data
159                    .iter()
160                    .position(|&b| b == 0)
161                    .unwrap_or(attr.data.len());
162                Ok(String::from_utf8_lossy(&attr.data[..end]).to_string())
163            }
164        }
165    }
166
167    /// Read the raw attribute data bytes.
168    pub fn read_raw(&self) -> Result<Vec<u8>> {
169        self.read_attr
170            .as_ref()
171            .map(|a| a.data.clone())
172            .ok_or_else(|| {
173                Hdf5Error::InvalidState("attribute has no read data (write-mode handle?)".into())
174            })
175    }
176}
177
178/// A fluent builder for creating attributes on datasets.
179///
180/// Obtained from [`H5Dataset::new_attr::<T>()`](crate::dataset::H5Dataset::new_attr).
181pub struct AttrBuilder<'a, T> {
182    file_inner: &'a SharedInner,
183    ds_index: usize,
184    _shape_set: bool,
185    _marker: PhantomData<T>,
186}
187
188impl<'a, T> AttrBuilder<'a, T> {
189    pub(crate) fn new(file_inner: &'a SharedInner, ds_index: usize) -> Self {
190        Self {
191            file_inner,
192            ds_index,
193            _shape_set: false,
194            _marker: PhantomData,
195        }
196    }
197
198    /// Set the attribute shape. Use `()` for a scalar attribute.
199    #[must_use]
200    pub fn shape<S>(mut self, _shape: S) -> Self {
201        // For now we only support scalar attributes.
202        self._shape_set = true;
203        self
204    }
205
206    /// Create the attribute with the given name.
207    ///
208    /// The attribute is created but does not yet have a value.
209    /// Call [`H5Attribute::write_scalar`] to set the value.
210    pub fn create(self, name: &str) -> Result<H5Attribute> {
211        Ok(H5Attribute {
212            file_inner: clone_inner(self.file_inner),
213            ds_index: self.ds_index,
214            name: name.to_string(),
215            read_attr: None,
216        })
217    }
218}