Skip to main content

rustyhdf5_format/
attribute.rs

1//! HDF5 Attribute message parsing (message type 0x000C).
2
3#[cfg(not(feature = "std"))]
4use alloc::{string::String, vec::Vec};
5
6use crate::attribute_info::AttributeInfoMessage;
7use crate::btree_v2::{collect_btree_v2_records, BTreeV2Header};
8use crate::data_read;
9use crate::dataspace::Dataspace;
10use crate::datatype::Datatype;
11use crate::error::FormatError;
12use crate::fractal_heap::FractalHeapHeader;
13use crate::message_type::MessageType;
14use crate::object_header::ObjectHeader;
15use crate::shared_message;
16use crate::vl_data;
17
18/// A parsed HDF5 attribute message.
19#[derive(Debug, Clone)]
20pub struct AttributeMessage {
21    /// Attribute name.
22    pub name: String,
23    /// Attribute datatype.
24    pub datatype: Datatype,
25    /// Attribute dataspace.
26    pub dataspace: Dataspace,
27    /// Raw attribute value data.
28    pub raw_data: Vec<u8>,
29}
30
31fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
32    match offset.checked_add(needed) {
33        Some(end) if end <= data.len() => Ok(()),
34        _ => Err(FormatError::UnexpectedEof {
35            expected: offset.saturating_add(needed),
36            available: data.len(),
37        }),
38    }
39}
40
41/// Round up to the next multiple of 8.
42fn pad8(x: usize) -> usize {
43    (x + 7) & !7
44}
45
46impl AttributeMessage {
47    /// Parse an attribute message from raw message bytes.
48    ///
49    /// `length_size` is needed for dataspace dimension parsing.
50    pub fn parse(data: &[u8], length_size: u8) -> Result<AttributeMessage, FormatError> {
51        ensure_len(data, 0, 2)?;
52        let version = data[0];
53
54        match version {
55            1 => Self::parse_v1(data, length_size),
56            2 => Self::parse_v2(data, length_size),
57            3 => Self::parse_v3(data, length_size),
58            _ => Err(FormatError::InvalidAttributeVersion(version)),
59        }
60    }
61
62    fn parse_v1(data: &[u8], length_size: u8) -> Result<AttributeMessage, FormatError> {
63        // version(1) + reserved(1) + name_size(2) + datatype_size(2) + dataspace_size(2) = 8
64        ensure_len(data, 0, 8)?;
65        let name_size = u16::from_le_bytes([data[2], data[3]]) as usize;
66        let datatype_size = u16::from_le_bytes([data[4], data[5]]) as usize;
67        let dataspace_size = u16::from_le_bytes([data[6], data[7]]) as usize;
68
69        let mut pos = 8;
70
71        // Name (padded to 8-byte boundary)
72        ensure_len(data, pos, name_size)?;
73        let name = extract_name(&data[pos..pos + name_size]);
74        pos += pad8(name_size);
75
76        // Datatype (padded to 8-byte boundary)
77        ensure_len(data, pos, datatype_size)?;
78        let (datatype, _) = Datatype::parse(&data[pos..pos + datatype_size])?;
79        pos += pad8(datatype_size);
80
81        // Dataspace (padded to 8-byte boundary)
82        ensure_len(data, pos, dataspace_size)?;
83        let dataspace = Dataspace::parse(&data[pos..pos + dataspace_size], length_size)?;
84        pos += pad8(dataspace_size);
85
86        // Raw data: num_elements × type_size bytes
87        let raw_data = compute_raw_data(data, pos, &dataspace, &datatype);
88
89        Ok(AttributeMessage {
90            name,
91            datatype,
92            dataspace,
93            raw_data,
94        })
95    }
96
97    fn parse_v2(data: &[u8], length_size: u8) -> Result<AttributeMessage, FormatError> {
98        // version(1) + flags(1) + name_size(2) + datatype_size(2) + dataspace_size(2) = 8
99        ensure_len(data, 0, 8)?;
100        let name_size = u16::from_le_bytes([data[2], data[3]]) as usize;
101        let datatype_size = u16::from_le_bytes([data[4], data[5]]) as usize;
102        let dataspace_size = u16::from_le_bytes([data[6], data[7]]) as usize;
103
104        let mut pos = 8;
105
106        // Name (NO padding)
107        ensure_len(data, pos, name_size)?;
108        let name = extract_name(&data[pos..pos + name_size]);
109        pos += name_size;
110
111        // Datatype (NO padding)
112        ensure_len(data, pos, datatype_size)?;
113        let (datatype, _) = Datatype::parse(&data[pos..pos + datatype_size])?;
114        pos += datatype_size;
115
116        // Dataspace (NO padding)
117        ensure_len(data, pos, dataspace_size)?;
118        let dataspace = Dataspace::parse(&data[pos..pos + dataspace_size], length_size)?;
119        pos += dataspace_size;
120
121        let raw_data = compute_raw_data(data, pos, &dataspace, &datatype);
122
123        Ok(AttributeMessage {
124            name,
125            datatype,
126            dataspace,
127            raw_data,
128        })
129    }
130
131    fn parse_v3(data: &[u8], length_size: u8) -> Result<AttributeMessage, FormatError> {
132        // version(1) + flags(1) + name_size(2) + datatype_size(2) + dataspace_size(2) + encoding(1) = 9
133        ensure_len(data, 0, 9)?;
134        let name_size = u16::from_le_bytes([data[2], data[3]]) as usize;
135        let datatype_size = u16::from_le_bytes([data[4], data[5]]) as usize;
136        let dataspace_size = u16::from_le_bytes([data[6], data[7]]) as usize;
137        let _encoding = data[8]; // 0=ASCII, 1=UTF-8
138
139        let mut pos = 9;
140
141        // Name (NO padding)
142        ensure_len(data, pos, name_size)?;
143        let name = extract_name(&data[pos..pos + name_size]);
144        pos += name_size;
145
146        // Datatype (NO padding)
147        ensure_len(data, pos, datatype_size)?;
148        let (datatype, _) = Datatype::parse(&data[pos..pos + datatype_size])?;
149        pos += datatype_size;
150
151        // Dataspace (NO padding)
152        ensure_len(data, pos, dataspace_size)?;
153        let dataspace = Dataspace::parse(&data[pos..pos + dataspace_size], length_size)?;
154        pos += dataspace_size;
155
156        let raw_data = compute_raw_data(data, pos, &dataspace, &datatype);
157
158        Ok(AttributeMessage {
159            name,
160            datatype,
161            dataspace,
162            raw_data,
163        })
164    }
165
166    /// Serialize attribute message (v2 format, no padding).
167    pub fn serialize(&self, length_size: u8) -> Vec<u8> {
168        self.serialize_version(2, length_size)
169    }
170
171    /// Serialize attribute message as v3 (adds character set encoding byte).
172    pub fn serialize_v3(&self, length_size: u8) -> Vec<u8> {
173        self.serialize_version(3, length_size)
174    }
175
176    fn serialize_version(&self, version: u8, length_size: u8) -> Vec<u8> {
177        let name_bytes = {
178            let mut n = self.name.as_bytes().to_vec();
179            n.push(0); // null terminator
180            n
181        };
182        let dt_bytes = self.datatype.serialize();
183        let ds_bytes = self.dataspace.serialize(length_size);
184
185        let mut buf = Vec::new();
186        buf.push(version);
187        buf.push(0); // flags
188        buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
189        buf.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
190        buf.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
191        if version >= 3 {
192            buf.push(0x00); // character set encoding: ASCII
193        }
194        buf.extend_from_slice(&name_bytes);
195        buf.extend_from_slice(&dt_bytes);
196        buf.extend_from_slice(&ds_bytes);
197        buf.extend_from_slice(&self.raw_data);
198        buf
199    }
200
201    /// Read attribute value as f64 values.
202    pub fn read_as_f64(&self) -> Result<Vec<f64>, FormatError> {
203        data_read::read_as_f64(&self.raw_data, &self.datatype)
204    }
205
206    /// Read attribute value as i64 values.
207    pub fn read_as_i64(&self) -> Result<Vec<i64>, FormatError> {
208        data_read::read_as_i64(&self.raw_data, &self.datatype)
209    }
210
211    /// Read attribute value as u64 values.
212    pub fn read_as_u64(&self) -> Result<Vec<u64>, FormatError> {
213        data_read::read_as_u64(&self.raw_data, &self.datatype)
214    }
215
216    /// Read attribute value as a single string (first element).
217    pub fn read_as_string(&self) -> Result<String, FormatError> {
218        let strings = data_read::read_as_strings(&self.raw_data, &self.datatype)?;
219        Ok(strings.into_iter().next().unwrap_or_default())
220    }
221
222    /// Read attribute value as a vector of fixed-length strings.
223    pub fn read_as_strings(&self) -> Result<Vec<String>, FormatError> {
224        data_read::read_as_strings(&self.raw_data, &self.datatype)
225    }
226
227    /// Read variable-length string attribute values.
228    ///
229    /// Needs the full file data and offset/length sizes from the superblock
230    /// because VL strings store their data in the global heap.
231    pub fn read_vl_strings(
232        &self,
233        file_data: &[u8],
234        offset_size: u8,
235        length_size: u8,
236    ) -> Result<Vec<String>, FormatError> {
237        let num_elements = self.dataspace.num_elements();
238        vl_data::read_vl_strings(
239            file_data,
240            &self.raw_data,
241            num_elements,
242            offset_size,
243            length_size,
244        )
245    }
246}
247
248/// Compute raw data size based on dataspace and datatype, then extract from message bytes.
249fn compute_raw_data(data: &[u8], pos: usize, dataspace: &Dataspace, datatype: &Datatype) -> Vec<u8> {
250    let num_elements = dataspace.num_elements() as usize;
251    let elem_size = datatype.type_size() as usize;
252    let expected_size = num_elements * elem_size;
253    let available = data.len().saturating_sub(pos);
254    let take = expected_size.min(available);
255    if take > 0 {
256        data[pos..pos + take].to_vec()
257    } else if available > 0 {
258        // Fallback: take whatever is available (e.g., for VL types where type_size may not match)
259        data[pos..].to_vec()
260    } else {
261        Vec::new()
262    }
263}
264
265/// Extract a name from raw bytes, stripping null terminator.
266fn extract_name(bytes: &[u8]) -> String {
267    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
268    String::from_utf8_lossy(&bytes[..end]).into_owned()
269}
270
271/// Extract all attribute messages from an object header.
272pub fn extract_attributes(
273    header: &ObjectHeader,
274    length_size: u8,
275) -> Result<Vec<AttributeMessage>, FormatError> {
276    let mut attrs = Vec::new();
277    for msg in &header.messages {
278        if msg.msg_type == MessageType::Attribute {
279            let attr = AttributeMessage::parse(&msg.data, length_size)?;
280            attrs.push(attr);
281        }
282    }
283    Ok(attrs)
284}
285
286/// Find a specific attribute by name.
287pub fn find_attribute<'a>(
288    attrs: &'a [AttributeMessage],
289    name: &str,
290) -> Option<&'a AttributeMessage> {
291    attrs.iter().find(|a| a.name == name)
292}
293
294/// Extract all attributes from an object header, supporting both compact and dense storage.
295///
296/// This function handles:
297/// - Compact attributes: inline Attribute messages (0x000C) in the object header
298/// - Dense attributes: AttributeInfo message (0x0015) pointing to fractal heap + B-tree v2
299/// - Shared messages: resolves shared datatype references for attribute messages
300///
301/// Use this instead of `extract_attributes` when reading files that may use dense storage
302/// (e.g., objects with many attributes, typically >8).
303pub fn extract_attributes_full(
304    file_data: &[u8],
305    header: &ObjectHeader,
306    offset_size: u8,
307    length_size: u8,
308) -> Result<Vec<AttributeMessage>, FormatError> {
309    let mut attrs = Vec::new();
310
311    // Collect compact attributes (inline in OH)
312    for msg in &header.messages {
313        if msg.msg_type == MessageType::Attribute {
314            if shared_message::is_shared(msg.flags) {
315                // Shared attribute: resolve the reference to get actual attribute data
316                let shared_ref = shared_message::parse_shared_ref(&msg.data, offset_size)?;
317                let resolved_data = shared_message::resolve_shared_message(
318                    file_data,
319                    &shared_ref,
320                    MessageType::Attribute,
321                    offset_size,
322                    length_size,
323                )?;
324                let attr = AttributeMessage::parse(&resolved_data, length_size)?;
325                attrs.push(attr);
326            } else {
327                let attr = AttributeMessage::parse(&msg.data, length_size)?;
328                attrs.push(attr);
329            }
330        }
331    }
332
333    // Check for dense attributes via AttributeInfo message
334    let attr_info = find_attribute_info(header, offset_size)?;
335    if let Some(info) = attr_info {
336        if let Some(fh_addr) = info.fractal_heap_address {
337            let dense_attrs =
338                extract_dense_attributes(file_data, &info, fh_addr, offset_size, length_size)?;
339            attrs.extend(dense_attrs);
340        }
341    }
342
343    Ok(attrs)
344}
345
346/// Find and parse the Attribute Info message from an object header.
347fn find_attribute_info(
348    header: &ObjectHeader,
349    offset_size: u8,
350) -> Result<Option<AttributeInfoMessage>, FormatError> {
351    for msg in &header.messages {
352        if msg.msg_type == MessageType::AttributeInfo {
353            let info = AttributeInfoMessage::parse(&msg.data, offset_size)?;
354            return Ok(Some(info));
355        }
356    }
357    Ok(None)
358}
359
360/// Extract attributes from dense storage (fractal heap + B-tree v2).
361fn extract_dense_attributes(
362    file_data: &[u8],
363    attr_info: &AttributeInfoMessage,
364    fh_addr: u64,
365    offset_size: u8,
366    length_size: u8,
367) -> Result<Vec<AttributeMessage>, FormatError> {
368    // Parse fractal heap
369    let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?;
370
371    // Parse B-tree v2 for name index (type 8)
372    let btree_addr = attr_info.btree_name_index_address.ok_or(
373        FormatError::UnexpectedEof {
374            expected: 1,
375            available: 0,
376        }
377    )?;
378    let btree_hdr =
379        BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?;
380    let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
381
382    let mut attrs = Vec::new();
383    for record in &records {
384        // Per HDF5 spec, both type 8 and type 9 records start with heap_id:
385        //   Type 8: heap_id(8) + msg_flags(1) + creation_order(4) + hash(4)
386        //   Type 9: heap_id(8) + msg_flags(1) + creation_order(4)
387        let id_offset = 0;
388
389        if record.data.len() < id_offset + fh.heap_id_length as usize {
390            continue;
391        }
392        let id_bytes = &record.data[id_offset..id_offset + fh.heap_id_length as usize];
393
394        // Read attribute message from fractal heap
395        let attr_data = fh.read_managed_object(file_data, id_bytes, offset_size)?;
396
397        // The data in the heap is a complete attribute message
398        let attr = AttributeMessage::parse(&attr_data, length_size)?;
399        attrs.push(attr);
400    }
401
402    Ok(attrs)
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use crate::datatype::{CharacterSet, DatatypeByteOrder, StringPadding};
409
410    /// Build a datatype header for testing (8 bytes).
411    fn build_dt_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
412        let mut buf = vec![0u8; 8];
413        buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
414        buf[1] = bf[0];
415        buf[2] = bf[1];
416        buf[3] = bf[2];
417        buf[4..8].copy_from_slice(&size.to_le_bytes());
418        buf
419    }
420
421    /// Build an f64 LE datatype message.
422    fn build_f64_dt() -> Vec<u8> {
423        let mut buf = build_dt_header(1, 1, [0x00, 0x00, 0x02], 8);
424        let mut props = [0u8; 12];
425        props[2..4].copy_from_slice(&64u16.to_le_bytes()); // bit_precision
426        props[4] = 52; // exp_location
427        props[5] = 11; // exp_size
428        props[6] = 0; // mant_location
429        props[7] = 52; // mant_size
430        props[8..12].copy_from_slice(&1023u32.to_le_bytes()); // exp_bias
431        buf.extend_from_slice(&props);
432        buf
433    }
434
435    /// Build a scalar dataspace (v2).
436    fn build_scalar_ds() -> Vec<u8> {
437        vec![2, 0, 0, 0] // version=2, rank=0, flags=0, type=0(scalar)
438    }
439
440    /// Build a simple 1D dataspace (v1).
441    fn build_simple_ds_v1(dim: u64) -> Vec<u8> {
442        let mut buf = vec![1u8, 1, 0, 0, 0, 0, 0, 0]; // version=1, rank=1, flags=0, reserved(5)
443        buf.extend_from_slice(&dim.to_le_bytes());
444        buf
445    }
446
447    /// Build a fixed-length string datatype.
448    fn build_string_dt(size: u32) -> Vec<u8> {
449        // class=3, version=1, padding=NullPad(1), charset=ASCII(0) → bf0=0x01
450        build_dt_header(3, 1, [0x01, 0, 0], size)
451    }
452
453    #[test]
454    fn parse_v1_attribute_f64_scalar() {
455        let name = b"temp\0";
456        let dt_bytes = build_f64_dt();
457        let ds_bytes = build_scalar_ds();
458
459        let name_size = name.len();
460        let dt_size = dt_bytes.len();
461        let ds_size = ds_bytes.len();
462
463        let mut data = Vec::new();
464        data.push(1); // version
465        data.push(0); // reserved
466        data.extend_from_slice(&(name_size as u16).to_le_bytes());
467        data.extend_from_slice(&(dt_size as u16).to_le_bytes());
468        data.extend_from_slice(&(ds_size as u16).to_le_bytes());
469
470        // Name padded to 8 bytes
471        data.extend_from_slice(name);
472        while data.len() % 8 != 0 || data.len() == 8 {
473            // Pad name to 8-byte boundary from start of name
474            let name_start = 8;
475            let name_padded = pad8(name_size);
476            while data.len() < name_start + name_padded {
477                data.push(0);
478            }
479            break;
480        }
481
482        // Datatype padded to 8 bytes
483        let dt_start = data.len();
484        data.extend_from_slice(&dt_bytes);
485        let dt_padded = pad8(dt_size);
486        while data.len() < dt_start + dt_padded {
487            data.push(0);
488        }
489
490        // Dataspace padded to 8 bytes
491        let ds_start = data.len();
492        data.extend_from_slice(&ds_bytes);
493        let ds_padded = pad8(ds_size);
494        while data.len() < ds_start + ds_padded {
495            data.push(0);
496        }
497
498        // Raw data: f64 value 98.6
499        data.extend_from_slice(&98.6f64.to_le_bytes());
500
501        let attr = AttributeMessage::parse(&data, 8).unwrap();
502        assert_eq!(attr.name, "temp");
503        assert_eq!(attr.dataspace.num_elements(), 1);
504        let vals = attr.read_as_f64().unwrap();
505        assert_eq!(vals.len(), 1);
506        assert!((vals[0] - 98.6).abs() < 1e-10);
507    }
508
509    #[test]
510    fn parse_v2_attribute_fixed_string() {
511        let name = b"label\0";
512        let dt_bytes = build_string_dt(5);
513        let ds_bytes = build_scalar_ds();
514
515        let mut data = Vec::new();
516        data.push(2); // version
517        data.push(0); // flags
518        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
519        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
520        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
521
522        // No padding in v2
523        data.extend_from_slice(name);
524        data.extend_from_slice(&dt_bytes);
525        data.extend_from_slice(&ds_bytes);
526
527        // Raw data: "hello"
528        data.extend_from_slice(b"hello");
529
530        let attr = AttributeMessage::parse(&data, 8).unwrap();
531        assert_eq!(attr.name, "label");
532        let s = attr.read_as_string().unwrap();
533        assert_eq!(s, "hello");
534    }
535
536    #[test]
537    fn parse_v3_attribute_utf8() {
538        let name = b"note\0";
539        let dt_bytes = build_string_dt(3);
540        let ds_bytes = build_scalar_ds();
541
542        let mut data = Vec::new();
543        data.push(3); // version
544        data.push(0); // flags
545        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
546        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
547        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
548        data.push(1); // encoding = UTF-8
549
550        data.extend_from_slice(name);
551        data.extend_from_slice(&dt_bytes);
552        data.extend_from_slice(&ds_bytes);
553        data.extend_from_slice(b"abc");
554
555        let attr = AttributeMessage::parse(&data, 8).unwrap();
556        assert_eq!(attr.name, "note");
557        let s = attr.read_as_string().unwrap();
558        assert_eq!(s, "abc");
559    }
560
561    #[test]
562    fn parse_v2_attribute_1d_array() {
563        let name = b"vals\0";
564        let dt_bytes = build_f64_dt();
565        let ds_bytes = build_simple_ds_v1(3);
566
567        let mut data = Vec::new();
568        data.push(2); // version
569        data.push(0); // flags
570        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
571        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
572        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
573
574        data.extend_from_slice(name);
575        data.extend_from_slice(&dt_bytes);
576        data.extend_from_slice(&ds_bytes);
577
578        // 3 f64 values
579        data.extend_from_slice(&1.0f64.to_le_bytes());
580        data.extend_from_slice(&2.0f64.to_le_bytes());
581        data.extend_from_slice(&3.0f64.to_le_bytes());
582
583        let attr = AttributeMessage::parse(&data, 8).unwrap();
584        assert_eq!(attr.name, "vals");
585        let vals = attr.read_as_f64().unwrap();
586        assert_eq!(vals, vec![1.0, 2.0, 3.0]);
587    }
588
589    #[test]
590    fn parse_v1_padding_alignment() {
591        // Verify v1 pads name, dt, ds each to 8 bytes
592        let name = b"x\0"; // 2 bytes → pad to 8
593        let dt_bytes = build_f64_dt(); // 20 bytes → pad to 24
594        let ds_bytes = build_scalar_ds(); // 4 bytes → pad to 8
595
596        let mut data = Vec::new();
597        data.push(1); // version
598        data.push(0); // reserved
599        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
600        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
601        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
602
603        // Name padded to 8
604        data.extend_from_slice(name);
605        data.resize(8 + pad8(name.len()), 0);
606
607        // DT padded to 8
608        let dt_start = data.len();
609        data.extend_from_slice(&dt_bytes);
610        data.resize(dt_start + pad8(dt_bytes.len()), 0);
611
612        // DS padded to 8
613        let ds_start = data.len();
614        data.extend_from_slice(&ds_bytes);
615        data.resize(ds_start + pad8(ds_bytes.len()), 0);
616
617        // raw data
618        data.extend_from_slice(&42.0f64.to_le_bytes());
619
620        let attr = AttributeMessage::parse(&data, 8).unwrap();
621        assert_eq!(attr.name, "x");
622        let vals = attr.read_as_f64().unwrap();
623        assert_eq!(vals, vec![42.0]);
624    }
625
626    #[test]
627    fn parse_v2_no_padding() {
628        // Same as parse_v2_attribute_fixed_string but verifying no padding
629        let name = b"ab\0"; // 3 bytes, no padding
630        let dt_bytes = build_string_dt(2); // 8 bytes, no padding
631        let ds_bytes = build_scalar_ds(); // 4 bytes, no padding
632
633        let mut data = Vec::new();
634        data.push(2);
635        data.push(0);
636        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
637        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
638        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
639        data.extend_from_slice(name);
640        data.extend_from_slice(&dt_bytes);
641        data.extend_from_slice(&ds_bytes);
642        data.extend_from_slice(b"hi");
643
644        let attr = AttributeMessage::parse(&data, 8).unwrap();
645        assert_eq!(attr.name, "ab");
646        assert_eq!(attr.read_as_string().unwrap(), "hi");
647    }
648
649    #[test]
650    fn truncated_attribute_error() {
651        let data = [1u8]; // too short
652        let err = AttributeMessage::parse(&data, 8).unwrap_err();
653        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
654    }
655
656    #[test]
657    fn invalid_version_error() {
658        let data = [5u8, 0, 0, 0, 0, 0, 0, 0, 0, 0];
659        let err = AttributeMessage::parse(&data, 8).unwrap_err();
660        assert_eq!(err, FormatError::InvalidAttributeVersion(5));
661    }
662
663    #[test]
664    fn extract_attributes_from_header() {
665        // Build a fake ObjectHeader with 3 attribute messages
666        let mut msgs = Vec::new();
667        for i in 0..3 {
668            let name = format!("attr{}\0", i);
669            let dt_bytes = build_f64_dt();
670            let ds_bytes = build_scalar_ds();
671
672            let mut attr_data = Vec::new();
673            attr_data.push(2); // version
674            attr_data.push(0);
675            attr_data.extend_from_slice(&(name.len() as u16).to_le_bytes());
676            attr_data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
677            attr_data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
678            attr_data.extend_from_slice(name.as_bytes());
679            attr_data.extend_from_slice(&dt_bytes);
680            attr_data.extend_from_slice(&ds_bytes);
681            attr_data.extend_from_slice(&((i as f64) * 1.0).to_le_bytes());
682
683            msgs.push(crate::object_header::HeaderMessage {
684                msg_type: MessageType::Attribute,
685                size: attr_data.len(),
686                flags: 0,
687                creation_order: None,
688                data: attr_data,
689            });
690        }
691
692        let header = ObjectHeader {
693            version: 2,
694            messages: msgs,
695            reference_count: None,
696            flags: 0,
697            access_time: None,
698            modification_time: None,
699            change_time: None,
700            birth_time: None,
701        };
702
703        let attrs = extract_attributes(&header, 8).unwrap();
704        assert_eq!(attrs.len(), 3);
705        assert_eq!(attrs[0].name, "attr0");
706        assert_eq!(attrs[1].name, "attr1");
707        assert_eq!(attrs[2].name, "attr2");
708    }
709
710    #[test]
711    fn find_attribute_by_name() {
712        let name = b"target\0";
713        let dt_bytes = build_f64_dt();
714        let ds_bytes = build_scalar_ds();
715
716        let mut attr_data = Vec::new();
717        attr_data.push(2);
718        attr_data.push(0);
719        attr_data.extend_from_slice(&(name.len() as u16).to_le_bytes());
720        attr_data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
721        attr_data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
722        attr_data.extend_from_slice(name);
723        attr_data.extend_from_slice(&dt_bytes);
724        attr_data.extend_from_slice(&ds_bytes);
725        attr_data.extend_from_slice(&99.0f64.to_le_bytes());
726
727        let attr = AttributeMessage::parse(&attr_data, 8).unwrap();
728        let attrs = vec![attr];
729
730        assert!(find_attribute(&attrs, "target").is_some());
731        assert!(find_attribute(&attrs, "missing").is_none());
732    }
733
734    #[test]
735    fn read_as_f64_scalar() {
736        let name = b"v\0";
737        let dt_bytes = build_f64_dt();
738        let ds_bytes = build_scalar_ds();
739
740        let mut data = Vec::new();
741        data.push(2);
742        data.push(0);
743        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
744        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
745        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
746        data.extend_from_slice(name);
747        data.extend_from_slice(&dt_bytes);
748        data.extend_from_slice(&ds_bytes);
749        data.extend_from_slice(&3.14f64.to_le_bytes());
750
751        let attr = AttributeMessage::parse(&data, 8).unwrap();
752        let vals = attr.read_as_f64().unwrap();
753        assert_eq!(vals, vec![3.14]);
754    }
755
756    #[test]
757    fn read_as_string_fixed() {
758        let name = b"s\0";
759        let dt_bytes = build_string_dt(5);
760        let ds_bytes = build_scalar_ds();
761
762        let mut data = Vec::new();
763        data.push(2);
764        data.push(0);
765        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
766        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
767        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
768        data.extend_from_slice(name);
769        data.extend_from_slice(&dt_bytes);
770        data.extend_from_slice(&ds_bytes);
771        data.extend_from_slice(b"world");
772
773        let attr = AttributeMessage::parse(&data, 8).unwrap();
774        assert_eq!(attr.read_as_string().unwrap(), "world");
775    }
776
777    #[test]
778    fn read_as_strings_array() {
779        let name = b"arr\0";
780        let dt_bytes = build_string_dt(4);
781        let ds_bytes = build_simple_ds_v1(2);
782
783        let mut data = Vec::new();
784        data.push(2);
785        data.push(0);
786        data.extend_from_slice(&(name.len() as u16).to_le_bytes());
787        data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes());
788        data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes());
789        data.extend_from_slice(name);
790        data.extend_from_slice(&dt_bytes);
791        data.extend_from_slice(&ds_bytes);
792        data.extend_from_slice(b"abcdEFGH");
793
794        let attr = AttributeMessage::parse(&data, 8).unwrap();
795        let strs = attr.read_as_strings().unwrap();
796        assert_eq!(strs, vec!["abcd", "EFGH"]);
797    }
798}