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