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