A dataset created from a committed (named) datatype stores only a shared-
message reference to it. The facade parsed those reference bytes as the
datatype itself, producing `Time { size: 0 }` and unreadable data, and an
attribute using a committed datatype was silently dropped.
- shared_message::parse_shared_ref had the encoding wrong: it skipped six
reserved bytes for version 2 (only version 1 has them) and had the version 3
types inverted (1 is the SOHM heap, 2 is "committed, in another object
header"). Verified against h5py 3.16 / HDF5 2.0, which writes
`02 02 <address>` under both default and latest libver bounds. Resolution
now dispatches on which field the reference carries.
- New shared_message::message_data resolves a header message through the
indirection; the reader, lazy and mmap facades use it for datatype,
dataspace and filter-pipeline messages.
- AttributeMessage honours the v2/v3 flags (bit 0 datatype shared, bit 1
dataspace shared) via the new parse_in_file, used everywhere file data is
available. Parsing a shared attribute without file access is now
FormatError::UnresolvedSharedMessage instead of a garbage datatype.
- h5py interop test covering both libver settings.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
901 lines
31 KiB
Rust
901 lines
31 KiB
Rust
//! 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<u8>,
|
||
}
|
||
|
||
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<AttributeMessage, FormatError> {
|
||
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<AttributeMessage, FormatError> {
|
||
Self::parse_impl(data, length_size, Some((file_data, offset_size)))
|
||
}
|
||
|
||
fn parse_impl(
|
||
data: &[u8],
|
||
length_size: u8,
|
||
file: Option<(&[u8], u8)>,
|
||
) -> Result<AttributeMessage, FormatError> {
|
||
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<Cow<'a, [u8]>, 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<AttributeMessage, FormatError> {
|
||
// 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<AttributeMessage, FormatError> {
|
||
// 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<AttributeMessage, FormatError> {
|
||
// 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<u8> {
|
||
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<u8> {
|
||
self.serialize_version(3, length_size)
|
||
}
|
||
|
||
fn serialize_version(&self, version: u8, length_size: u8) -> Vec<u8> {
|
||
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<Vec<f64>, FormatError> {
|
||
data_read::read_as_f64(&self.raw_data, &self.datatype)
|
||
}
|
||
|
||
/// Read attribute value as i64 values.
|
||
pub fn read_as_i64(&self) -> Result<Vec<i64>, FormatError> {
|
||
data_read::read_as_i64(&self.raw_data, &self.datatype)
|
||
}
|
||
|
||
/// Read attribute value as u64 values.
|
||
pub fn read_as_u64(&self) -> Result<Vec<u64>, 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<String, FormatError> {
|
||
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<Vec<String>, 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<Vec<String>, 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<u8> {
|
||
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<Vec<AttributeMessage>, 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<Vec<AttributeMessage>, 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<Option<AttributeInfoMessage>, 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<Vec<AttributeMessage>, 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<u8> {
|
||
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<u8> {
|
||
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<u8> {
|
||
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<u8> {
|
||
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<u8> {
|
||
// 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"]);
|
||
}
|
||
}
|