use crate::format::messages::dataspace::DataspaceMessage;
use crate::format::messages::datatype::DatatypeMessage;
use crate::format::{FormatContext, FormatError, FormatResult, LibverBound};
const ATTR_VERSION: u8 = 3;
pub const ATTR_FLAG_TYPE_SHARED: u8 = 0x01;
pub const ATTR_FLAG_SPACE_SHARED: u8 = 0x02;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedAttribute {
pub body: Vec<u8>,
pub datatype_at: usize,
pub dataspace_at: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AttributeMessage {
pub name: String,
pub datatype: DatatypeMessage,
pub dataspace: DataspaceMessage,
pub data: Vec<u8>,
}
impl AttributeMessage {
pub fn scalar_string(name: &str, value: &str) -> Self {
let str_size = (value.len() + 1) as u32; let datatype = DatatypeMessage::fixed_string_utf8(str_size);
let dataspace = DataspaceMessage::scalar();
let mut data = Vec::with_capacity(str_size as usize);
data.extend_from_slice(value.as_bytes());
data.push(0);
Self {
name: name.to_string(),
datatype,
dataspace,
data,
}
}
pub fn scalar_numeric(name: &str, datatype: DatatypeMessage, data: Vec<u8>) -> Self {
Self {
name: name.to_string(),
datatype,
dataspace: DataspaceMessage::scalar(),
data,
}
}
pub fn array_numeric(
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
data: Vec<u8>,
) -> Self {
debug_assert_eq!(
data.len() as u64,
dims.iter().product::<u64>() * datatype.element_size() as u64,
"array_numeric data length must equal product(dims) * element_size"
);
Self {
name: name.to_string(),
datatype,
dataspace: DataspaceMessage::simple(dims),
data,
}
}
pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
self.encode_at(ctx, LibverBound::Earliest)
}
pub fn encode_at(&self, ctx: &FormatContext, libver: LibverBound) -> Vec<u8> {
self.encode_for(ctx, libver, crate::format::ObjectFormat::Modern)
}
fn encode_v1(&self, ctx: &FormatContext, libver: LibverBound) -> Vec<u8> {
fn pad_to_8(buf: &mut Vec<u8>) {
let padded = (buf.len() + 7) & !7;
buf.resize(padded, 0);
}
let encoded_dt = self.datatype.encode_at(ctx, libver);
let encoded_ds = self
.dataspace
.encode_for(ctx, crate::format::ObjectFormat::Legacy);
let name_bytes = self.name.as_bytes();
let name_size = name_bytes.len() + 1;
let mut buf = Vec::with_capacity(8 + name_size + encoded_dt.len() + encoded_ds.len() + 24);
buf.push(1); buf.push(0); buf.extend_from_slice(&(name_size as u16).to_le_bytes());
buf.extend_from_slice(&(encoded_dt.len() as u16).to_le_bytes());
buf.extend_from_slice(&(encoded_ds.len() as u16).to_le_bytes());
buf.extend_from_slice(name_bytes);
buf.push(0);
pad_to_8(&mut buf);
buf.extend_from_slice(&encoded_dt);
pad_to_8(&mut buf);
buf.extend_from_slice(&encoded_ds);
pad_to_8(&mut buf);
buf.extend_from_slice(&self.data);
buf
}
pub fn encode_for(
&self,
ctx: &FormatContext,
libver: LibverBound,
format: crate::format::ObjectFormat,
) -> Vec<u8> {
if format.attribute_version() == 1 {
return self.encode_v1(ctx, libver);
}
let encoded_dt = self.datatype.encode_at(ctx, libver);
let encoded_ds = self.dataspace.encode_for(ctx, format);
self.encode_with_fields(0x00, &encoded_dt, &encoded_ds).body
}
pub fn encode_with_fields(
&self,
flags: u8,
datatype: &[u8],
dataspace: &[u8],
) -> EncodedAttribute {
let name_bytes = self.name.as_bytes();
let name_size = name_bytes.len() + 1;
let total = 9 + name_size + datatype.len() + dataspace.len() + self.data.len();
let mut buf = Vec::with_capacity(total);
buf.push(ATTR_VERSION);
buf.push(flags);
buf.extend_from_slice(&(name_size as u16).to_le_bytes());
buf.extend_from_slice(&(datatype.len() as u16).to_le_bytes());
buf.extend_from_slice(&(dataspace.len() as u16).to_le_bytes());
buf.push(0x01);
buf.extend_from_slice(name_bytes);
buf.push(0x00);
let datatype_at = buf.len();
buf.extend_from_slice(datatype);
let dataspace_at = buf.len();
buf.extend_from_slice(dataspace);
buf.extend_from_slice(&self.data);
debug_assert_eq!(buf.len(), total);
EncodedAttribute {
body: buf,
datatype_at,
dataspace_at,
}
}
pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
let AttributeHeader {
name,
datatype_size,
dataspace_size,
align,
mut pos,
} = AttributeHeader::decode(buf)?;
let needed = pos + datatype_size;
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let (datatype, _) = DatatypeMessage::decode(&buf[pos..pos + datatype_size], ctx)?;
pos += datatype_size;
if align > 1 {
pos = (pos + align - 1) & !(align - 1);
}
let needed = pos + dataspace_size;
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let (dataspace, _) = DataspaceMessage::decode(&buf[pos..pos + dataspace_size], ctx)?;
pos += dataspace_size;
if align > 1 {
pos = (pos + align - 1) & !(align - 1);
}
let num_elements: u64 = if dataspace.dims.is_empty() {
1 } else {
dataspace
.dims
.iter()
.fold(1u64, |acc, &d| acc.saturating_mul(d))
};
let data_size = num_elements
.saturating_mul(datatype.element_size() as u64)
.min(usize::MAX as u64) as usize;
let needed = pos.saturating_add(data_size);
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let data = buf[pos..pos + data_size].to_vec();
pos += data_size;
Ok((
Self {
name,
datatype,
dataspace,
data,
},
pos,
))
}
}
struct AttributeHeader {
name: String,
datatype_size: usize,
dataspace_size: usize,
align: usize,
pos: usize,
}
impl AttributeHeader {
fn decode(buf: &[u8]) -> FormatResult<Self> {
if buf.len() < 8 {
return Err(FormatError::BufferTooShort {
needed: 8,
available: buf.len(),
});
}
let version = buf[0];
if !(1..=ATTR_VERSION).contains(&version) {
return Err(FormatError::InvalidVersion(version));
}
let flags = buf[1];
if flags & (ATTR_FLAG_TYPE_SHARED | ATTR_FLAG_SPACE_SHARED) != 0 {
let what = if flags & ATTR_FLAG_TYPE_SHARED != 0 {
"datatype"
} else {
"dataspace"
};
return Err(FormatError::UnsupportedFeature(format!(
"attribute whose {what} is a shared-message reference"
)));
}
let name_size = u16::from_le_bytes([buf[2], buf[3]]) as usize;
let datatype_size = u16::from_le_bytes([buf[4], buf[5]]) as usize;
let dataspace_size = u16::from_le_bytes([buf[6], buf[7]]) as usize;
let mut pos = if version >= 3 {
9
} else {
8
};
let align = if version == 1 { 8 } else { 1 };
let needed = pos + name_size;
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let name_end = if name_size > 0 && buf[pos + name_size - 1] == 0 {
pos + name_size - 1
} else {
pos + name_size
};
let name = String::from_utf8_lossy(&buf[pos..name_end]).to_string();
pos += name_size;
if align > 1 {
pos = (pos + align - 1) & !(align - 1);
}
Ok(Self {
name,
datatype_size,
dataspace_size,
align,
pos,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AttributeEntry {
body: AttributeBody,
creation_index: Option<u16>,
}
#[derive(Debug, Clone, PartialEq)]
enum AttributeBody {
Readable(AttributeMessage),
Unreadable {
name: String,
raw: Vec<u8>,
reason: String,
},
}
impl AttributeEntry {
pub fn parse(buf: &[u8], ctx: &FormatContext) -> FormatResult<Self> {
let body = match AttributeMessage::decode(buf, ctx) {
Ok((attr, _)) => AttributeBody::Readable(attr),
Err(payload_err) => {
let header = AttributeHeader::decode(buf)?;
AttributeBody::Unreadable {
name: header.name,
raw: buf.to_vec(),
reason: payload_err.to_string(),
}
}
};
Ok(Self {
body,
creation_index: None,
})
}
pub fn with_creation_index(mut self, creation_index: Option<u16>) -> Self {
self.creation_index = creation_index;
self
}
pub fn set_creation_index(&mut self, creation_index: Option<u16>) {
self.creation_index = creation_index;
}
pub fn creation_index(&self) -> Option<u16> {
self.creation_index
}
pub fn name(&self) -> &str {
match &self.body {
AttributeBody::Readable(attr) => &attr.name,
AttributeBody::Unreadable { name, .. } => name,
}
}
pub fn decoded(&self) -> Result<&AttributeMessage, &str> {
match &self.body {
AttributeBody::Readable(attr) => Ok(attr),
AttributeBody::Unreadable { reason, .. } => Err(reason),
}
}
pub fn readable(&self) -> Option<&AttributeMessage> {
self.decoded().ok()
}
pub fn unreadable_reason(&self) -> Option<&str> {
self.decoded().err()
}
pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
self.encode_at(ctx, LibverBound::Earliest)
}
pub fn encode_at(&self, ctx: &FormatContext, libver: LibverBound) -> Vec<u8> {
self.encode_for(ctx, libver, crate::format::ObjectFormat::Modern)
}
pub fn encode_for(
&self,
ctx: &FormatContext,
libver: LibverBound,
format: crate::format::ObjectFormat,
) -> Vec<u8> {
match &self.body {
AttributeBody::Readable(attr) => attr.encode_for(ctx, libver, format),
AttributeBody::Unreadable { raw, .. } => raw.clone(),
}
}
}
impl From<AttributeMessage> for AttributeEntry {
fn from(attr: AttributeMessage) -> Self {
Self {
body: AttributeBody::Readable(attr),
creation_index: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> FormatContext {
FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
}
}
#[test]
fn scalar_string_roundtrip() {
let msg = AttributeMessage::scalar_string("my_attr", "hello");
let encoded = msg.encode(&ctx());
let (decoded, consumed) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded.name, "my_attr");
assert_eq!(decoded.data, b"hello\0");
assert_eq!(decoded, msg);
}
#[test]
fn scalar_string_empty() {
let msg = AttributeMessage::scalar_string("empty", "");
let encoded = msg.encode(&ctx());
let (decoded, consumed) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded.name, "empty");
assert_eq!(decoded.data, b"\0");
assert_eq!(decoded, msg);
}
#[test]
fn version_is_three() {
let msg = AttributeMessage::scalar_string("test", "val");
let encoded = msg.encode(&ctx());
assert_eq!(encoded[0], 3);
}
#[test]
fn decode_buffer_too_short() {
let buf = [0u8; 4];
let err = AttributeMessage::decode(&buf, &ctx()).unwrap_err();
match err {
FormatError::BufferTooShort { .. } => {}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn decode_bad_version() {
let msg = AttributeMessage::scalar_string("x", "y");
let mut encoded = msg.encode(&ctx());
encoded[0] = 0; let err = AttributeMessage::decode(&encoded, &ctx()).unwrap_err();
match err {
FormatError::InvalidVersion(0) => {}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn array_numeric_1d_roundtrip() {
use crate::format::messages::datatype::DatatypeMessage;
let vals: [i32; 3] = [10, -20, 30];
let mut data = Vec::new();
for v in vals {
data.extend_from_slice(&v.to_le_bytes());
}
let msg = AttributeMessage::array_numeric(
"dim_offset",
DatatypeMessage::i32_type(),
&[3],
data.clone(),
);
assert_eq!(msg.dataspace.dims, vec![3]);
let encoded = msg.encode(&ctx());
let (decoded, consumed) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
assert_eq!(consumed, encoded.len());
assert_eq!(decoded.name, "dim_offset");
assert_eq!(decoded.dataspace.dims, vec![3]);
assert_eq!(decoded.data, data);
assert_eq!(decoded.dataspace.max_dims, Some(vec![3]));
assert_eq!(decoded.datatype, msg.datatype);
assert_eq!(decoded.dataspace.class, msg.dataspace.class);
}
#[test]
fn scalar_string_utf8_content() {
let msg = AttributeMessage::scalar_string("desc", "caf\u{00e9}");
let encoded = msg.encode(&ctx());
let (decoded, _) = AttributeMessage::decode(&encoded, &ctx()).unwrap();
assert_eq!(decoded.name, "desc");
assert_eq!(decoded.data.len(), 6);
assert_eq!(&decoded.data[..5], "caf\u{00e9}".as_bytes());
assert_eq!(decoded.data[5], 0);
}
#[test]
fn a_legacy_attribute_matches_the_bytes_libhdf5_wrote() {
let ctx = FormatContext::default_v3();
let attr = AttributeMessage::scalar_numeric(
"ra",
DatatypeMessage::i64_type(),
42i64.to_le_bytes().to_vec(),
);
let buf = attr.encode_for(
&ctx,
LibverBound::Earliest,
crate::format::ObjectFormat::Legacy,
);
assert_eq!(
buf,
vec![
0x01, 0x00, 0x03, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x72, 0x61, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x10, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
);
let (back, consumed) = AttributeMessage::decode(&buf, &ctx).unwrap();
assert_eq!(consumed, buf.len());
assert_eq!(back.name, "ra");
assert_eq!(back.data, 42i64.to_le_bytes().to_vec());
}
#[test]
fn a_legacy_attribute_round_trips_at_every_field_padding() {
let ctx = FormatContext::default_v3();
for name in ["a", "ab", "abcdefg", "abcdefgh", "abcdefghi"] {
let attr = AttributeMessage::array_numeric(
name,
DatatypeMessage::i32_type(),
&[3],
vec![1u8, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0],
);
let buf = attr.encode_for(
&ctx,
LibverBound::Earliest,
crate::format::ObjectFormat::Legacy,
);
assert_eq!(buf[0], 1, "{name}");
let (back, consumed) = AttributeMessage::decode(&buf, &ctx).unwrap();
assert_eq!(consumed, buf.len(), "{name}");
assert_eq!(back.name, name);
assert_eq!(back.data, attr.data, "{name}");
assert_eq!(back.dataspace.dims, vec![3], "{name}");
}
}
}