Skip to main content

rust_hdf5/format/messages/
attribute.rs

1//! Attribute message (type 0x0C) -- describes an attribute attached to an object.
2//!
3//! Binary layout (version 3, no shared datatypes):
4//!   Byte 0:    version = 3
5//!   Byte 1:    flags (0 for non-shared)
6//!   Bytes 2-3: name_size (u16 LE, including null terminator)
7//!   Bytes 4-5: datatype_size (u16 LE)
8//!   Bytes 6-7: dataspace_size (u16 LE)
9//!   Byte 8:    name character set encoding (0=ASCII, 1=UTF-8)
10//!   <name: name_size bytes, null-terminated>
11//!   <encoded datatype message: datatype_size bytes>
12//!   <encoded dataspace message: dataspace_size bytes>
13//!   <raw attribute data>
14
15use crate::format::messages::dataspace::DataspaceMessage;
16use crate::format::messages::datatype::DatatypeMessage;
17use crate::format::{FormatContext, FormatError, FormatResult};
18
19const ATTR_VERSION: u8 = 3;
20
21/// An HDF5 attribute message.
22#[derive(Debug, Clone, PartialEq)]
23pub struct AttributeMessage {
24    /// Attribute name.
25    pub name: String,
26    /// Datatype of the attribute value.
27    pub datatype: DatatypeMessage,
28    /// Dataspace (scalar or simple).
29    pub dataspace: DataspaceMessage,
30    /// Raw attribute value data.
31    pub data: Vec<u8>,
32}
33
34impl AttributeMessage {
35    /// Create a scalar string attribute with the given name and value.
36    ///
37    /// Uses a null-terminated UTF-8 fixed-length string datatype with
38    /// size = value.len() + 1 (for the null terminator), and a scalar
39    /// dataspace.
40    pub fn scalar_string(name: &str, value: &str) -> Self {
41        let str_size = (value.len() + 1) as u32; // +1 for null terminator
42        let datatype = DatatypeMessage::fixed_string_utf8(str_size);
43        let dataspace = DataspaceMessage::scalar();
44
45        // Data: string bytes + null terminator
46        let mut data = Vec::with_capacity(str_size as usize);
47        data.extend_from_slice(value.as_bytes());
48        data.push(0); // null terminator
49
50        Self {
51            name: name.to_string(),
52            datatype,
53            dataspace,
54            data,
55        }
56    }
57
58    /// Create a scalar numeric attribute with raw bytes as value.
59    pub fn scalar_numeric(name: &str, datatype: DatatypeMessage, data: Vec<u8>) -> Self {
60        Self {
61            name: name.to_string(),
62            datatype,
63            dataspace: DataspaceMessage::scalar(),
64            data,
65        }
66    }
67
68    /// Create a numeric array attribute with a simple dataspace.
69    ///
70    /// `dims` are the dimension sizes (e.g. `&[3]` for the 1-D array
71    /// attributes AreaDetector writes). `data` is the row-major raw bytes and
72    /// must hold `product(dims) * datatype.element_size()` bytes — the caller
73    /// owns that invariant. An empty `dims` yields a scalar dataspace; prefer
74    /// [`Self::scalar_numeric`] for that case.
75    pub fn array_numeric(
76        name: &str,
77        datatype: DatatypeMessage,
78        dims: &[u64],
79        data: Vec<u8>,
80    ) -> Self {
81        debug_assert_eq!(
82            data.len() as u64,
83            dims.iter().product::<u64>() * datatype.element_size() as u64,
84            "array_numeric data length must equal product(dims) * element_size"
85        );
86        Self {
87            name: name.to_string(),
88            datatype,
89            dataspace: DataspaceMessage::simple(dims),
90            data,
91        }
92    }
93
94    /// Encode the attribute message into a byte vector.
95    ///
96    /// The result is the raw payload for an object header message of type
97    /// 0x0C (MSG_ATTRIBUTE). It does NOT include the object header message
98    /// envelope (type, size, flags bytes); that is handled by the caller.
99    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
100        let encoded_dt = self.datatype.encode(ctx);
101        let encoded_ds = self.dataspace.encode(ctx);
102
103        // Name with null terminator
104        let name_bytes = self.name.as_bytes();
105        let name_size = name_bytes.len() + 1; // +1 for null terminator
106
107        // Total: 9 (header) + name_size + datatype_size + dataspace_size + data_size
108        let total = 9 + name_size + encoded_dt.len() + encoded_ds.len() + self.data.len();
109        let mut buf = Vec::with_capacity(total);
110
111        // Byte 0: version
112        buf.push(ATTR_VERSION);
113
114        // Byte 1: flags (0 = non-shared)
115        buf.push(0x00);
116
117        // Bytes 2-3: name size (u16 LE)
118        buf.extend_from_slice(&(name_size as u16).to_le_bytes());
119
120        // Bytes 4-5: datatype size (u16 LE)
121        buf.extend_from_slice(&(encoded_dt.len() as u16).to_le_bytes());
122
123        // Bytes 6-7: dataspace size (u16 LE)
124        buf.extend_from_slice(&(encoded_ds.len() as u16).to_le_bytes());
125
126        // Byte 8: name character set encoding (1 = UTF-8)
127        buf.push(0x01);
128
129        // Name (null-terminated)
130        buf.extend_from_slice(name_bytes);
131        buf.push(0x00);
132
133        // Encoded datatype
134        buf.extend_from_slice(&encoded_dt);
135
136        // Encoded dataspace
137        buf.extend_from_slice(&encoded_ds);
138
139        // Raw data
140        buf.extend_from_slice(&self.data);
141
142        debug_assert_eq!(buf.len(), total);
143        buf
144    }
145
146    /// Decode an attribute message from a byte buffer.
147    ///
148    /// Supports versions 1, 2, and 3:
149    /// - v1: 8-byte header, each field padded to 8-byte alignment
150    /// - v2: 8-byte header, no alignment padding
151    /// - v3: 9-byte header (adds charset byte), no alignment padding
152    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
153        if buf.len() < 8 {
154            return Err(FormatError::BufferTooShort {
155                needed: 8,
156                available: buf.len(),
157            });
158        }
159
160        let version = buf[0];
161        if !(1..=ATTR_VERSION).contains(&version) {
162            return Err(FormatError::InvalidVersion(version));
163        }
164
165        // flags at buf[1]
166        let name_size = u16::from_le_bytes([buf[2], buf[3]]) as usize;
167        let datatype_size = u16::from_le_bytes([buf[4], buf[5]]) as usize;
168        let dataspace_size = u16::from_le_bytes([buf[6], buf[7]]) as usize;
169
170        let mut pos = if version >= 3 {
171            // v3 has charset byte at offset 8
172            9
173        } else {
174            // v1, v2: no charset byte
175            8
176        };
177
178        // v1 pads each field to 8-byte alignment
179        let align = if version == 1 { 8 } else { 1 };
180
181        // Name
182        let needed = pos + name_size;
183        if buf.len() < needed {
184            return Err(FormatError::BufferTooShort {
185                needed,
186                available: buf.len(),
187            });
188        }
189        // Strip trailing null
190        let name_end = if name_size > 0 && buf[pos + name_size - 1] == 0 {
191            pos + name_size - 1
192        } else {
193            pos + name_size
194        };
195        let name = String::from_utf8_lossy(&buf[pos..name_end]).to_string();
196        pos += name_size;
197        // v1 alignment
198        if align > 1 {
199            pos = (pos + align - 1) & !(align - 1);
200        }
201
202        // Datatype
203        let needed = pos + datatype_size;
204        if buf.len() < needed {
205            return Err(FormatError::BufferTooShort {
206                needed,
207                available: buf.len(),
208            });
209        }
210        let (datatype, _) = DatatypeMessage::decode(&buf[pos..pos + datatype_size], ctx)?;
211        pos += datatype_size;
212        if align > 1 {
213            pos = (pos + align - 1) & !(align - 1);
214        }
215
216        // Dataspace
217        let needed = pos + dataspace_size;
218        if buf.len() < needed {
219            return Err(FormatError::BufferTooShort {
220                needed,
221                available: buf.len(),
222            });
223        }
224        let (dataspace, _) = DataspaceMessage::decode(&buf[pos..pos + dataspace_size], ctx)?;
225        pos += dataspace_size;
226        if align > 1 {
227            pos = (pos + align - 1) & !(align - 1);
228        }
229
230        // Data: remaining bytes = datatype.element_size() * number_of_elements
231        let num_elements: u64 = if dataspace.dims.is_empty() {
232            1 // scalar
233        } else {
234            // dims are file-derived; saturate so a crafted attribute with
235            // absurd dimensions is rejected by the buffer check below
236            // instead of overflowing.
237            dataspace
238                .dims
239                .iter()
240                .fold(1u64, |acc, &d| acc.saturating_mul(d))
241        };
242        let data_size = num_elements
243            .saturating_mul(datatype.element_size() as u64)
244            .min(usize::MAX as u64) as usize;
245        let needed = pos.saturating_add(data_size);
246        if buf.len() < needed {
247            return Err(FormatError::BufferTooShort {
248                needed,
249                available: buf.len(),
250            });
251        }
252        let data = buf[pos..pos + data_size].to_vec();
253        pos += data_size;
254
255        Ok((
256            Self {
257                name,
258                datatype,
259                dataspace,
260                data,
261            },
262            pos,
263        ))
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    fn ctx() -> FormatContext {
272        FormatContext {
273            sizeof_addr: 8,
274            sizeof_size: 8,
275        }
276    }
277
278    #[test]
279    fn scalar_string_roundtrip() {
280        let msg = AttributeMessage::scalar_string("my_attr", "hello");
281        let encoded = msg.encode(&ctx());
282        let (decoded, consumed) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
283        assert_eq!(consumed, encoded.len());
284        assert_eq!(decoded.name, "my_attr");
285        assert_eq!(decoded.data, b"hello\0");
286        assert_eq!(decoded, msg);
287    }
288
289    #[test]
290    fn scalar_string_empty() {
291        let msg = AttributeMessage::scalar_string("empty", "");
292        let encoded = msg.encode(&ctx());
293        let (decoded, consumed) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
294        assert_eq!(consumed, encoded.len());
295        assert_eq!(decoded.name, "empty");
296        assert_eq!(decoded.data, b"\0");
297        assert_eq!(decoded, msg);
298    }
299
300    #[test]
301    fn version_is_three() {
302        let msg = AttributeMessage::scalar_string("test", "val");
303        let encoded = msg.encode(&ctx());
304        assert_eq!(encoded[0], 3);
305    }
306
307    #[test]
308    fn decode_buffer_too_short() {
309        let buf = [0u8; 4];
310        let err = AttributeMessage::decode(&buf, &ctx()).unwrap_err();
311        match err {
312            FormatError::BufferTooShort { .. } => {}
313            other => panic!("unexpected error: {:?}", other),
314        }
315    }
316
317    #[test]
318    fn decode_bad_version() {
319        let msg = AttributeMessage::scalar_string("x", "y");
320        let mut encoded = msg.encode(&ctx());
321        encoded[0] = 0; // invalid version
322        let err = AttributeMessage::decode(&encoded, &ctx()).unwrap_err();
323        match err {
324            FormatError::InvalidVersion(0) => {}
325            other => panic!("unexpected error: {:?}", other),
326        }
327    }
328
329    #[test]
330    fn array_numeric_1d_roundtrip() {
331        use crate::format::messages::datatype::DatatypeMessage;
332        // Three int32 values, 1-D array attribute (NDArrayDimOffset-style).
333        let vals: [i32; 3] = [10, -20, 30];
334        let mut data = Vec::new();
335        for v in vals {
336            data.extend_from_slice(&v.to_le_bytes());
337        }
338        let msg = AttributeMessage::array_numeric(
339            "dim_offset",
340            DatatypeMessage::i32_type(),
341            &[3],
342            data.clone(),
343        );
344        assert_eq!(msg.dataspace.dims, vec![3]);
345        let encoded = msg.encode(&ctx());
346        let (decoded, consumed) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
347        assert_eq!(consumed, encoded.len());
348        assert_eq!(decoded.name, "dim_offset");
349        assert_eq!(decoded.dataspace.dims, vec![3]);
350        assert_eq!(decoded.data, data);
351        assert_eq!(decoded, msg);
352    }
353
354    #[test]
355    fn scalar_string_utf8_content() {
356        let msg = AttributeMessage::scalar_string("desc", "caf\u{00e9}");
357        let encoded = msg.encode(&ctx());
358        let (decoded, _) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
359        assert_eq!(decoded.name, "desc");
360        // "caf\u{e9}" is 5 bytes in UTF-8 + null = 6
361        assert_eq!(decoded.data.len(), 6);
362        assert_eq!(&decoded.data[..5], "caf\u{00e9}".as_bytes());
363        assert_eq!(decoded.data[5], 0);
364    }
365}