//! HDF5 Attribute message parsing (message type 0x000C). #[cfg(not(feature = "std"))] use alloc::{borrow::Cow, string::String, vec::Vec}; #[cfg(feature = "std")] use std::borrow::Cow; use crate::attribute_info::AttributeInfoMessage; use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records}; use crate::data_read; use crate::dataspace::Dataspace; use crate::datatype::Datatype; use crate::error::FormatError; use crate::fractal_heap::FractalHeapHeader; use crate::message_type::MessageType; use crate::object_header::ObjectHeader; use crate::shared_message; use crate::vl_data; /// A parsed HDF5 attribute message. #[derive(Debug, Clone)] pub struct AttributeMessage { /// Attribute name. pub name: String, /// Attribute datatype. pub datatype: Datatype, /// Attribute dataspace. pub dataspace: Dataspace, /// Raw attribute value data. pub raw_data: Vec, } fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> { match offset.checked_add(needed) { Some(end) if end <= data.len() => Ok(()), _ => Err(FormatError::UnexpectedEof { expected: offset.saturating_add(needed), available: data.len(), }), } } /// Round up to the next multiple of 8. fn pad8(x: usize) -> usize { (x + 7) & !7 } impl AttributeMessage { /// Parse an attribute message from raw message bytes. /// /// `length_size` is needed for dataspace dimension parsing. pub fn parse(data: &[u8], length_size: u8) -> Result { Self::parse_impl(data, length_size, None) } /// [`AttributeMessage::parse`] with access to the rest of the file, which /// is needed when the attribute's datatype or dataspace is *shared* (v2/v3 /// flag bits 0/1) — e.g. an attribute created with a committed datatype. /// In that case the embedded bytes are a reference to the real message, /// not the message. Without file access such an attribute is an error /// rather than a garbage datatype. pub fn parse_in_file( data: &[u8], file_data: &[u8], offset_size: u8, length_size: u8, ) -> Result { Self::parse_impl(data, length_size, Some((file_data, offset_size))) } fn parse_impl( data: &[u8], length_size: u8, file: Option<(&[u8], u8)>, ) -> Result { ensure_len(data, 0, 2)?; let version = data[0]; match version { 1 => Self::parse_v1(data, length_size), 2 => Self::parse_v2(data, length_size, file), 3 => Self::parse_v3(data, length_size, file), _ => Err(FormatError::InvalidAttributeVersion(version)), } } /// The bytes of an embedded datatype/dataspace message, following the /// shared-message reference when `shared` is set. fn embedded_message<'a>( bytes: &'a [u8], shared: bool, msg_type: MessageType, length_size: u8, file: Option<(&[u8], u8)>, ) -> Result, FormatError> { if !shared { return Ok(Cow::Borrowed(bytes)); } let (file_data, offset_size) = file.ok_or(FormatError::UnresolvedSharedMessage)?; let shared_ref = shared_message::parse_shared_ref(bytes, offset_size)?; shared_message::resolve_shared_message( file_data, &shared_ref, msg_type, offset_size, length_size, ) .map(Cow::Owned) } fn parse_v1(data: &[u8], length_size: u8) -> Result { // version(1) + reserved(1) + name_size(2) + datatype_size(2) + dataspace_size(2) = 8 ensure_len(data, 0, 8)?; let name_size = u16::from_le_bytes([data[2], data[3]]) as usize; let datatype_size = u16::from_le_bytes([data[4], data[5]]) as usize; let dataspace_size = u16::from_le_bytes([data[6], data[7]]) as usize; let mut pos = 8; // Name (padded to 8-byte boundary) ensure_len(data, pos, name_size)?; let name = extract_name(&data[pos..pos + name_size]); pos += pad8(name_size); // Datatype (padded to 8-byte boundary) ensure_len(data, pos, datatype_size)?; let (datatype, _) = Datatype::parse(&data[pos..pos + datatype_size])?; pos += pad8(datatype_size); // Dataspace (padded to 8-byte boundary) ensure_len(data, pos, dataspace_size)?; let dataspace = Dataspace::parse(&data[pos..pos + dataspace_size], length_size)?; pos += pad8(dataspace_size); // Raw data: num_elements × type_size bytes let raw_data = compute_raw_data(data, pos, &dataspace, &datatype); Ok(AttributeMessage { name, datatype, dataspace, raw_data, }) } fn parse_v2( data: &[u8], length_size: u8, file: Option<(&[u8], u8)>, ) -> Result { // Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared. let flags = data.get(1).copied().unwrap_or(0); // version(1) + flags(1) + name_size(2) + datatype_size(2) + dataspace_size(2) = 8 ensure_len(data, 0, 8)?; let name_size = u16::from_le_bytes([data[2], data[3]]) as usize; let datatype_size = u16::from_le_bytes([data[4], data[5]]) as usize; let dataspace_size = u16::from_le_bytes([data[6], data[7]]) as usize; let mut pos = 8; // Name (NO padding) ensure_len(data, pos, name_size)?; let name = extract_name(&data[pos..pos + name_size]); pos += name_size; // Datatype (NO padding) ensure_len(data, pos, datatype_size)?; let dt_bytes = Self::embedded_message( &data[pos..pos + datatype_size], flags & 0x01 != 0, MessageType::Datatype, length_size, file, )?; let (datatype, _) = Datatype::parse(&dt_bytes)?; pos += datatype_size; // Dataspace (NO padding) ensure_len(data, pos, dataspace_size)?; let ds_bytes = Self::embedded_message( &data[pos..pos + dataspace_size], flags & 0x02 != 0, MessageType::Dataspace, length_size, file, )?; let dataspace = Dataspace::parse(&ds_bytes, length_size)?; pos += dataspace_size; let raw_data = compute_raw_data(data, pos, &dataspace, &datatype); Ok(AttributeMessage { name, datatype, dataspace, raw_data, }) } fn parse_v3( data: &[u8], length_size: u8, file: Option<(&[u8], u8)>, ) -> Result { // Flags: bit 0 = datatype is shared, bit 1 = dataspace is shared. let flags = data.get(1).copied().unwrap_or(0); // version(1) + flags(1) + name_size(2) + datatype_size(2) + dataspace_size(2) + encoding(1) = 9 ensure_len(data, 0, 9)?; let name_size = u16::from_le_bytes([data[2], data[3]]) as usize; let datatype_size = u16::from_le_bytes([data[4], data[5]]) as usize; let dataspace_size = u16::from_le_bytes([data[6], data[7]]) as usize; let _encoding = data[8]; // 0=ASCII, 1=UTF-8 let mut pos = 9; // Name (NO padding) ensure_len(data, pos, name_size)?; let name = extract_name(&data[pos..pos + name_size]); pos += name_size; // Datatype (NO padding) ensure_len(data, pos, datatype_size)?; let dt_bytes = Self::embedded_message( &data[pos..pos + datatype_size], flags & 0x01 != 0, MessageType::Datatype, length_size, file, )?; let (datatype, _) = Datatype::parse(&dt_bytes)?; pos += datatype_size; // Dataspace (NO padding) ensure_len(data, pos, dataspace_size)?; let ds_bytes = Self::embedded_message( &data[pos..pos + dataspace_size], flags & 0x02 != 0, MessageType::Dataspace, length_size, file, )?; let dataspace = Dataspace::parse(&ds_bytes, length_size)?; pos += dataspace_size; let raw_data = compute_raw_data(data, pos, &dataspace, &datatype); Ok(AttributeMessage { name, datatype, dataspace, raw_data, }) } /// Serialize attribute message (v2 format, no padding). pub fn serialize(&self, length_size: u8) -> Vec { self.serialize_version(2, length_size) } /// Serialize attribute message as v3 (adds character set encoding byte). pub fn serialize_v3(&self, length_size: u8) -> Vec { self.serialize_version(3, length_size) } fn serialize_version(&self, version: u8, length_size: u8) -> Vec { let name_bytes = { let mut n = self.name.as_bytes().to_vec(); n.push(0); // null terminator n }; let dt_bytes = self.datatype.serialize(); let ds_bytes = self.dataspace.serialize(length_size); let mut buf = Vec::new(); buf.push(version); buf.push(0); // flags buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes()); buf.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); buf.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); if version >= 3 { buf.push(0x00); // character set encoding: ASCII } buf.extend_from_slice(&name_bytes); buf.extend_from_slice(&dt_bytes); buf.extend_from_slice(&ds_bytes); buf.extend_from_slice(&self.raw_data); buf } /// Read attribute value as f64 values. pub fn read_as_f64(&self) -> Result, FormatError> { data_read::read_as_f64(&self.raw_data, &self.datatype) } /// Read attribute value as i64 values. pub fn read_as_i64(&self) -> Result, FormatError> { data_read::read_as_i64(&self.raw_data, &self.datatype) } /// Read attribute value as u64 values. pub fn read_as_u64(&self) -> Result, FormatError> { data_read::read_as_u64(&self.raw_data, &self.datatype) } /// Read attribute value as a single string (first element). pub fn read_as_string(&self) -> Result { let strings = data_read::read_as_strings(&self.raw_data, &self.datatype)?; Ok(strings.into_iter().next().unwrap_or_default()) } /// Read attribute value as a vector of fixed-length strings. pub fn read_as_strings(&self) -> Result, FormatError> { data_read::read_as_strings(&self.raw_data, &self.datatype) } /// Read variable-length string attribute values. /// /// Needs the full file data and offset/length sizes from the superblock /// because VL strings store their data in the global heap. pub fn read_vl_strings( &self, file_data: &[u8], offset_size: u8, length_size: u8, ) -> Result, FormatError> { let num_elements = self.dataspace.num_elements(); vl_data::read_vl_strings( file_data, &self.raw_data, num_elements, offset_size, length_size, ) } } /// Compute raw data size based on dataspace and datatype, then extract from message bytes. fn compute_raw_data( data: &[u8], pos: usize, dataspace: &Dataspace, datatype: &Datatype, ) -> Vec { let num_elements = dataspace.num_elements() as usize; let elem_size = datatype.type_size() as usize; let expected_size = num_elements.saturating_mul(elem_size); let available = data.len().saturating_sub(pos); let take = expected_size.min(available); if take > 0 { data[pos..pos + take].to_vec() } else if available > 0 { // Fallback: take whatever is available (e.g., for VL types where type_size may not match) data[pos..].to_vec() } else { Vec::new() } } /// Extract a name from raw bytes, stripping null terminator. fn extract_name(bytes: &[u8]) -> String { let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); String::from_utf8_lossy(&bytes[..end]).into_owned() } /// Extract all attribute messages from an object header. pub fn extract_attributes( header: &ObjectHeader, length_size: u8, ) -> Result, FormatError> { let mut attrs = Vec::new(); for msg in &header.messages { if msg.msg_type == MessageType::Attribute { let attr = AttributeMessage::parse(&msg.data, length_size)?; attrs.push(attr); } } Ok(attrs) } /// Find a specific attribute by name. pub fn find_attribute<'a>( attrs: &'a [AttributeMessage], name: &str, ) -> Option<&'a AttributeMessage> { attrs.iter().find(|a| a.name == name) } /// Extract all attributes from an object header, supporting both compact and dense storage. /// /// This function handles: /// - Compact attributes: inline Attribute messages (0x000C) in the object header /// - Dense attributes: AttributeInfo message (0x0015) pointing to fractal heap + B-tree v2 /// - Shared messages: resolves shared datatype references for attribute messages /// /// Use this instead of `extract_attributes` when reading files that may use dense storage /// (e.g., objects with many attributes, typically >8). pub fn extract_attributes_full( file_data: &[u8], header: &ObjectHeader, offset_size: u8, length_size: u8, ) -> Result, FormatError> { let mut attrs = Vec::new(); // Collect compact attributes (inline in OH) for msg in &header.messages { if msg.msg_type == MessageType::Attribute { if shared_message::is_shared(msg.flags) { // Shared attribute: resolve the reference to get actual attribute data let shared_ref = shared_message::parse_shared_ref(&msg.data, offset_size)?; let resolved_data = shared_message::resolve_shared_message( file_data, &shared_ref, MessageType::Attribute, offset_size, length_size, )?; let attr = AttributeMessage::parse_in_file( &resolved_data, file_data, offset_size, length_size, )?; attrs.push(attr); } else { let attr = AttributeMessage::parse_in_file( &msg.data, file_data, offset_size, length_size, )?; attrs.push(attr); } } } // Check for dense attributes via AttributeInfo message let attr_info = find_attribute_info(header, offset_size)?; if let Some(info) = attr_info && let Some(fh_addr) = info.fractal_heap_address { let dense_attrs = extract_dense_attributes(file_data, &info, fh_addr, offset_size, length_size)?; attrs.extend(dense_attrs); } Ok(attrs) } /// Find and parse the Attribute Info message from an object header. fn find_attribute_info( header: &ObjectHeader, offset_size: u8, ) -> Result, FormatError> { for msg in &header.messages { if msg.msg_type == MessageType::AttributeInfo { let info = AttributeInfoMessage::parse(&msg.data, offset_size)?; return Ok(Some(info)); } } Ok(None) } /// Extract attributes from dense storage (fractal heap + B-tree v2). fn extract_dense_attributes( file_data: &[u8], attr_info: &AttributeInfoMessage, fh_addr: u64, offset_size: u8, length_size: u8, ) -> Result, FormatError> { // Parse fractal heap let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?; // Parse B-tree v2 for name index (type 8) let btree_addr = attr_info .btree_name_index_address .ok_or(FormatError::UnexpectedEof { expected: 1, available: 0, })?; let btree_hdr = BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?; let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?; let mut attrs = Vec::new(); for record in &records { // Per HDF5 spec, both type 8 and type 9 records start with heap_id: // Type 8: heap_id(8) + msg_flags(1) + creation_order(4) + hash(4) // Type 9: heap_id(8) + msg_flags(1) + creation_order(4) let id_offset = 0; if record.data.len() < id_offset + fh.heap_id_length as usize { continue; } let id_bytes = &record.data[id_offset..id_offset + fh.heap_id_length as usize]; // Read attribute message from fractal heap let attr_data = fh.read_managed_object(file_data, id_bytes, offset_size)?; // The data in the heap is a complete attribute message let attr = AttributeMessage::parse_in_file(&attr_data, file_data, offset_size, length_size)?; attrs.push(attr); } Ok(attrs) } #[cfg(test)] mod tests { use super::*; /// Build a datatype header for testing (8 bytes). fn build_dt_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec { let mut buf = vec![0u8; 8]; buf[0] = (class & 0x0F) | ((version & 0x0F) << 4); buf[1] = bf[0]; buf[2] = bf[1]; buf[3] = bf[2]; buf[4..8].copy_from_slice(&size.to_le_bytes()); buf } /// Build an f64 LE datatype message. fn build_f64_dt() -> Vec { let mut buf = build_dt_header(1, 1, [0x00, 0x00, 0x02], 8); let mut props = [0u8; 12]; props[2..4].copy_from_slice(&64u16.to_le_bytes()); // bit_precision props[4] = 52; // exp_location props[5] = 11; // exp_size props[6] = 0; // mant_location props[7] = 52; // mant_size props[8..12].copy_from_slice(&1023u32.to_le_bytes()); // exp_bias buf.extend_from_slice(&props); buf } /// Build a scalar dataspace (v2). fn build_scalar_ds() -> Vec { vec![2, 0, 0, 0] // version=2, rank=0, flags=0, type=0(scalar) } /// Build a simple 1D dataspace (v1). fn build_simple_ds_v1(dim: u64) -> Vec { let mut buf = vec![1u8, 1, 0, 0, 0, 0, 0, 0]; // version=1, rank=1, flags=0, reserved(5) buf.extend_from_slice(&dim.to_le_bytes()); buf } /// Build a fixed-length string datatype. fn build_string_dt(size: u32) -> Vec { // class=3, version=1, padding=NullPad(1), charset=ASCII(0) → bf0=0x01 build_dt_header(3, 1, [0x01, 0, 0], size) } #[test] fn parse_v1_attribute_f64_scalar() { let name = b"temp\0"; let dt_bytes = build_f64_dt(); let ds_bytes = build_scalar_ds(); let name_size = name.len(); let dt_size = dt_bytes.len(); let ds_size = ds_bytes.len(); let mut data = Vec::new(); data.push(1); // version data.push(0); // reserved data.extend_from_slice(&(name_size as u16).to_le_bytes()); data.extend_from_slice(&(dt_size as u16).to_le_bytes()); data.extend_from_slice(&(ds_size as u16).to_le_bytes()); // Name padded to 8 bytes data.extend_from_slice(name); if data.len() % 8 != 0 || data.len() == 8 { // Pad name to 8-byte boundary from start of name let name_start = 8; let name_padded = pad8(name_size); while data.len() < name_start + name_padded { data.push(0); } } // Datatype padded to 8 bytes let dt_start = data.len(); data.extend_from_slice(&dt_bytes); let dt_padded = pad8(dt_size); while data.len() < dt_start + dt_padded { data.push(0); } // Dataspace padded to 8 bytes let ds_start = data.len(); data.extend_from_slice(&ds_bytes); let ds_padded = pad8(ds_size); while data.len() < ds_start + ds_padded { data.push(0); } // Raw data: f64 value 98.6 data.extend_from_slice(&98.6f64.to_le_bytes()); let attr = AttributeMessage::parse(&data, 8).unwrap(); assert_eq!(attr.name, "temp"); assert_eq!(attr.dataspace.num_elements(), 1); let vals = attr.read_as_f64().unwrap(); assert_eq!(vals.len(), 1); assert!((vals[0] - 98.6).abs() < 1e-10); } #[test] fn parse_v2_attribute_fixed_string() { let name = b"label\0"; let dt_bytes = build_string_dt(5); let ds_bytes = build_scalar_ds(); let mut data = Vec::new(); data.push(2); // version data.push(0); // flags data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); // No padding in v2 data.extend_from_slice(name); data.extend_from_slice(&dt_bytes); data.extend_from_slice(&ds_bytes); // Raw data: "hello" data.extend_from_slice(b"hello"); let attr = AttributeMessage::parse(&data, 8).unwrap(); assert_eq!(attr.name, "label"); let s = attr.read_as_string().unwrap(); assert_eq!(s, "hello"); } #[test] fn parse_v3_attribute_utf8() { let name = b"note\0"; let dt_bytes = build_string_dt(3); let ds_bytes = build_scalar_ds(); let mut data = Vec::new(); data.push(3); // version data.push(0); // flags data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); data.push(1); // encoding = UTF-8 data.extend_from_slice(name); data.extend_from_slice(&dt_bytes); data.extend_from_slice(&ds_bytes); data.extend_from_slice(b"abc"); let attr = AttributeMessage::parse(&data, 8).unwrap(); assert_eq!(attr.name, "note"); let s = attr.read_as_string().unwrap(); assert_eq!(s, "abc"); } #[test] fn parse_v2_attribute_1d_array() { let name = b"vals\0"; let dt_bytes = build_f64_dt(); let ds_bytes = build_simple_ds_v1(3); let mut data = Vec::new(); data.push(2); // version data.push(0); // flags data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(name); data.extend_from_slice(&dt_bytes); data.extend_from_slice(&ds_bytes); // 3 f64 values data.extend_from_slice(&1.0f64.to_le_bytes()); data.extend_from_slice(&2.0f64.to_le_bytes()); data.extend_from_slice(&3.0f64.to_le_bytes()); let attr = AttributeMessage::parse(&data, 8).unwrap(); assert_eq!(attr.name, "vals"); let vals = attr.read_as_f64().unwrap(); assert_eq!(vals, vec![1.0, 2.0, 3.0]); } #[test] fn parse_v1_padding_alignment() { // Verify v1 pads name, dt, ds each to 8 bytes let name = b"x\0"; // 2 bytes → pad to 8 let dt_bytes = build_f64_dt(); // 20 bytes → pad to 24 let ds_bytes = build_scalar_ds(); // 4 bytes → pad to 8 let mut data = Vec::new(); data.push(1); // version data.push(0); // reserved data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); // Name padded to 8 data.extend_from_slice(name); data.resize(8 + pad8(name.len()), 0); // DT padded to 8 let dt_start = data.len(); data.extend_from_slice(&dt_bytes); data.resize(dt_start + pad8(dt_bytes.len()), 0); // DS padded to 8 let ds_start = data.len(); data.extend_from_slice(&ds_bytes); data.resize(ds_start + pad8(ds_bytes.len()), 0); // raw data data.extend_from_slice(&42.0f64.to_le_bytes()); let attr = AttributeMessage::parse(&data, 8).unwrap(); assert_eq!(attr.name, "x"); let vals = attr.read_as_f64().unwrap(); assert_eq!(vals, vec![42.0]); } #[test] fn parse_v2_no_padding() { // Same as parse_v2_attribute_fixed_string but verifying no padding let name = b"ab\0"; // 3 bytes, no padding let dt_bytes = build_string_dt(2); // 8 bytes, no padding let ds_bytes = build_scalar_ds(); // 4 bytes, no padding let mut data = Vec::new(); data.push(2); data.push(0); data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(name); data.extend_from_slice(&dt_bytes); data.extend_from_slice(&ds_bytes); data.extend_from_slice(b"hi"); let attr = AttributeMessage::parse(&data, 8).unwrap(); assert_eq!(attr.name, "ab"); assert_eq!(attr.read_as_string().unwrap(), "hi"); } #[test] fn truncated_attribute_error() { let data = [1u8]; // too short let err = AttributeMessage::parse(&data, 8).unwrap_err(); assert!(matches!(err, FormatError::UnexpectedEof { .. })); } #[test] fn invalid_version_error() { let data = [5u8, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let err = AttributeMessage::parse(&data, 8).unwrap_err(); assert_eq!(err, FormatError::InvalidAttributeVersion(5)); } #[test] fn extract_attributes_from_header() { // Build a fake ObjectHeader with 3 attribute messages let mut msgs = Vec::new(); for i in 0..3 { let name = format!("attr{}\0", i); let dt_bytes = build_f64_dt(); let ds_bytes = build_scalar_ds(); let mut attr_data = Vec::new(); attr_data.push(2); // version attr_data.push(0); attr_data.extend_from_slice(&(name.len() as u16).to_le_bytes()); attr_data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); attr_data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); attr_data.extend_from_slice(name.as_bytes()); attr_data.extend_from_slice(&dt_bytes); attr_data.extend_from_slice(&ds_bytes); attr_data.extend_from_slice(&((i as f64) * 1.0).to_le_bytes()); msgs.push(crate::object_header::HeaderMessage { msg_type: MessageType::Attribute, size: attr_data.len(), flags: 0, creation_order: None, data: attr_data, }); } let header = ObjectHeader { version: 2, messages: msgs, reference_count: None, flags: 0, access_time: None, modification_time: None, change_time: None, birth_time: None, }; let attrs = extract_attributes(&header, 8).unwrap(); assert_eq!(attrs.len(), 3); assert_eq!(attrs[0].name, "attr0"); assert_eq!(attrs[1].name, "attr1"); assert_eq!(attrs[2].name, "attr2"); } #[test] fn find_attribute_by_name() { let name = b"target\0"; let dt_bytes = build_f64_dt(); let ds_bytes = build_scalar_ds(); let mut attr_data = Vec::new(); attr_data.push(2); attr_data.push(0); attr_data.extend_from_slice(&(name.len() as u16).to_le_bytes()); attr_data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); attr_data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); attr_data.extend_from_slice(name); attr_data.extend_from_slice(&dt_bytes); attr_data.extend_from_slice(&ds_bytes); attr_data.extend_from_slice(&99.0f64.to_le_bytes()); let attr = AttributeMessage::parse(&attr_data, 8).unwrap(); let attrs = vec![attr]; assert!(find_attribute(&attrs, "target").is_some()); assert!(find_attribute(&attrs, "missing").is_none()); } #[test] fn read_as_f64_scalar() { let name = b"v\0"; let dt_bytes = build_f64_dt(); let ds_bytes = build_scalar_ds(); let mut data = Vec::new(); data.push(2); data.push(0); data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(name); data.extend_from_slice(&dt_bytes); data.extend_from_slice(&ds_bytes); data.extend_from_slice(&3.25f64.to_le_bytes()); let attr = AttributeMessage::parse(&data, 8).unwrap(); let vals = attr.read_as_f64().unwrap(); assert_eq!(vals, vec![3.25]); } #[test] fn read_as_string_fixed() { let name = b"s\0"; let dt_bytes = build_string_dt(5); let ds_bytes = build_scalar_ds(); let mut data = Vec::new(); data.push(2); data.push(0); data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(name); data.extend_from_slice(&dt_bytes); data.extend_from_slice(&ds_bytes); data.extend_from_slice(b"world"); let attr = AttributeMessage::parse(&data, 8).unwrap(); assert_eq!(attr.read_as_string().unwrap(), "world"); } #[test] fn read_as_strings_array() { let name = b"arr\0"; let dt_bytes = build_string_dt(4); let ds_bytes = build_simple_ds_v1(2); let mut data = Vec::new(); data.push(2); data.push(0); data.extend_from_slice(&(name.len() as u16).to_le_bytes()); data.extend_from_slice(&(dt_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(&(ds_bytes.len() as u16).to_le_bytes()); data.extend_from_slice(name); data.extend_from_slice(&dt_bytes); data.extend_from_slice(&ds_bytes); data.extend_from_slice(b"abcdEFGH"); let attr = AttributeMessage::parse(&data, 8).unwrap(); let strs = attr.read_as_strings().unwrap(); assert_eq!(strs, vec!["abcd", "EFGH"]); } }