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}