use crate::checksum::jenkins_lookup3;
use crate::error::{Error, Result};
use crate::io::Cursor;
use crate::messages::shared::SharedMessage;
use crate::messages::{parse_message, HdfMessage};
use crate::storage::Storage;
const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
const OCHK_SIGNATURE: [u8; 4] = *b"OCHK";
const MSG_TYPE_CONTINUATION: u16 = 0x0010;
const MSG_TYPE_NIL: u16 = 0x0000;
#[derive(Debug, Clone)]
pub struct ObjectHeader {
pub version: u8,
pub messages: Vec<HdfMessage>,
pub reference_count: u32,
pub modification_time: Option<u32>,
}
impl ObjectHeader {
pub fn parse_at(data: &[u8], address: u64, offset_size: u8, length_size: u8) -> Result<Self> {
let mut cursor = Cursor::new(data);
cursor.set_position(address);
let sig = cursor.peek_bytes(4)?;
if sig == OHDR_SIGNATURE {
Self::parse_v2(&cursor, address, offset_size, length_size)
} else {
Self::parse_v1(&cursor, address, offset_size, length_size)
}
}
pub fn parse_at_storage(
storage: &dyn Storage,
address: u64,
offset_size: u8,
length_size: u8,
) -> Result<Self> {
let prefix = storage.read_range(address, 64)?;
if prefix.len() < 5 {
return Err(Error::UnexpectedEof {
offset: address,
needed: 5,
available: prefix.len() as u64,
});
}
if prefix.as_ref()[..4] == OHDR_SIGNATURE {
Self::parse_v2_storage(storage, address, offset_size, length_size)
} else {
Self::parse_v1_storage(storage, address, offset_size, length_size)
}
}
pub fn resolve_shared_messages(
&mut self,
data: &[u8],
offset_size: u8,
length_size: u8,
) -> Result<()> {
let old_messages = std::mem::take(&mut self.messages);
let mut resolved = Vec::with_capacity(old_messages.len());
for msg in old_messages {
match msg {
HdfMessage::Shared(SharedMessage::SharedInOhdr { address }) => {
match Self::parse_at(data, address, offset_size, length_size) {
Ok(target_header) => {
for target_msg in target_header.messages {
match target_msg {
HdfMessage::Nil
| HdfMessage::ObjectHeaderContinuation
| HdfMessage::Shared(_) => continue,
other => {
resolved.push(other);
break;
}
}
}
}
Err(_) => {
resolved
.push(HdfMessage::Shared(SharedMessage::SharedInOhdr { address }));
}
}
}
HdfMessage::Shared(SharedMessage::SharedInSohm { .. }) => {
self.messages = resolved;
return Err(Error::Other(
"SOHM table lookup not yet supported — file uses shared object header messages".to_string(),
));
}
other => resolved.push(other),
}
}
self.messages = resolved;
Ok(())
}
pub fn resolve_shared_messages_storage(
&mut self,
storage: &dyn Storage,
offset_size: u8,
length_size: u8,
) -> Result<()> {
let old_messages = std::mem::take(&mut self.messages);
let mut resolved = Vec::with_capacity(old_messages.len());
for msg in old_messages {
match msg {
HdfMessage::Shared(SharedMessage::SharedInOhdr { address }) => {
match Self::parse_at_storage(storage, address, offset_size, length_size) {
Ok(target_header) => {
for target_msg in target_header.messages {
match target_msg {
HdfMessage::Nil
| HdfMessage::ObjectHeaderContinuation
| HdfMessage::Shared(_) => continue,
other => {
resolved.push(other);
break;
}
}
}
}
Err(_) => {
resolved
.push(HdfMessage::Shared(SharedMessage::SharedInOhdr { address }));
}
}
}
HdfMessage::Shared(SharedMessage::SharedInSohm { .. }) => {
self.messages = resolved;
return Err(Error::Other(
"SOHM table lookup not yet supported — file uses shared object header messages".to_string(),
));
}
other => resolved.push(other),
}
}
self.messages = resolved;
Ok(())
}
fn parse_v1(base: &Cursor<'_>, address: u64, offset_size: u8, length_size: u8) -> Result<Self> {
let mut cursor = base.at_offset(address)?;
let version = cursor.read_u8()?;
if version != 1 {
return Err(Error::UnsupportedObjectHeaderVersion(version));
}
let _reserved = cursor.read_u8()?;
let num_messages = cursor.read_u16_le()?;
let reference_count = cursor.read_u32_le()?;
let header_data_size = cursor.read_u32_le()? as u64;
let _reserved2 = cursor.read_u32_le()?;
let messages_start = cursor.position();
let messages_end = messages_start + header_data_size;
let mut messages: Vec<HdfMessage> = Vec::with_capacity(num_messages as usize);
let mut continuations: Vec<(u64, u64)> = Vec::new();
Self::read_v1_messages(
base,
messages_start,
messages_end,
offset_size,
length_size,
&mut messages,
&mut continuations,
)?;
while let Some((cont_offset, cont_length)) = continuations.pop() {
let cont_end = cont_offset + cont_length;
Self::read_v1_messages(
base,
cont_offset,
cont_end,
offset_size,
length_size,
&mut messages,
&mut continuations,
)?;
}
Ok(ObjectHeader {
version: 1,
messages,
reference_count,
modification_time: None,
})
}
fn parse_v1_storage(
storage: &dyn Storage,
address: u64,
offset_size: u8,
length_size: u8,
) -> Result<Self> {
let header = storage.read_range(address, 16)?;
let mut cursor = Cursor::new(header.as_ref());
let version = cursor.read_u8()?;
if version != 1 {
return Err(Error::UnsupportedObjectHeaderVersion(version));
}
let _reserved = cursor.read_u8()?;
let num_messages = cursor.read_u16_le()?;
let reference_count = cursor.read_u32_le()?;
let header_data_size = cursor.read_u32_le()? as u64;
let _reserved2 = cursor.read_u32_le()?;
let first_chunk = storage.read_range(address, (16 + header_data_size) as usize)?;
let mut messages = Vec::with_capacity(num_messages as usize);
let mut continuations = Vec::new();
Self::read_v1_messages_from_slice(
&first_chunk.as_ref()[16..],
offset_size,
length_size,
&mut messages,
&mut continuations,
)?;
while let Some((cont_offset, cont_length)) = continuations.pop() {
let chunk = storage.read_range(cont_offset, cont_length as usize)?;
Self::read_v1_messages_from_slice(
chunk.as_ref(),
offset_size,
length_size,
&mut messages,
&mut continuations,
)?;
}
Ok(ObjectHeader {
version: 1,
messages,
reference_count,
modification_time: None,
})
}
fn read_v1_messages(
base: &Cursor<'_>,
start: u64,
end: u64,
offset_size: u8,
length_size: u8,
messages: &mut Vec<HdfMessage>,
continuations: &mut Vec<(u64, u64)>,
) -> Result<()> {
let mut cursor = base.at_offset(start)?;
while cursor.position() + 8 <= end {
let msg_type = cursor.read_u16_le()?;
let msg_data_size = cursor.read_u16_le()? as usize;
let msg_flags = cursor.read_u8()?;
let _reserved = cursor.read_bytes(3)?;
if cursor.position() + msg_data_size as u64 > end {
return Err(Error::InvalidData(format!(
"v1 message data ({} bytes) extends past header chunk end",
msg_data_size
)));
}
if msg_type == MSG_TYPE_NIL {
cursor.skip(msg_data_size)?;
messages.push(HdfMessage::Nil);
continue;
}
let msg_data = cursor.read_bytes(msg_data_size)?;
let is_shared = (msg_flags & 0x02) != 0;
if is_shared {
let shared_msg = crate::messages::shared::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
messages.push(HdfMessage::Shared(shared_msg));
} else if msg_type == MSG_TYPE_CONTINUATION {
let cont = crate::messages::continuation::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
continuations.push((cont.offset, cont.length));
messages.push(HdfMessage::ObjectHeaderContinuation);
} else {
let parsed = parse_message(
msg_type,
msg_data.len(),
&mut Cursor::new(msg_data),
offset_size,
length_size,
)?;
messages.push(parsed);
}
}
Ok(())
}
fn parse_v2(base: &Cursor<'_>, address: u64, offset_size: u8, length_size: u8) -> Result<Self> {
let mut cursor = base.at_offset(address)?;
let sig = cursor.read_bytes(4)?;
if sig != OHDR_SIGNATURE {
return Err(Error::InvalidObjectHeaderSignature);
}
let version = cursor.read_u8()?;
if version != 2 {
return Err(Error::UnsupportedObjectHeaderVersion(version));
}
let flags = cursor.read_u8()?;
let modification_time = if (flags & 0x20) != 0 {
let _access_time = cursor.read_u32_le()?;
let mod_time = cursor.read_u32_le()?;
let _change_time = cursor.read_u32_le()?;
let _birth_time = cursor.read_u32_le()?;
Some(mod_time)
} else {
None
};
if (flags & 0x10) != 0 {
let _max_compact = cursor.read_u16_le()?;
let _min_dense = cursor.read_u16_le()?;
}
let size_field_width = 1usize << (flags & 0x03);
let chunk0_data_size = cursor.read_uvar(size_field_width)?;
let creation_order_tracked = (flags & 0x04) != 0;
let messages_start = cursor.position();
let chunk0_end = messages_start + chunk0_data_size;
let checksum_start = address as usize;
let checksum_end = chunk0_end as usize; let stored_checksum = {
let mut ck = base.at_offset(chunk0_end)?;
ck.read_u32_le()?
};
let computed = jenkins_lookup3(&base.data()[checksum_start..checksum_end]);
if computed != stored_checksum {
return Err(Error::ChecksumMismatch {
expected: stored_checksum,
actual: computed,
});
}
let mut messages: Vec<HdfMessage> = Vec::new();
let mut continuations: Vec<(u64, u64)> = Vec::new();
Self::read_v2_messages(
base,
messages_start,
chunk0_end,
offset_size,
length_size,
creation_order_tracked,
&mut messages,
&mut continuations,
)?;
while let Some((cont_offset, cont_length)) = continuations.pop() {
Self::read_v2_continuation_chunk(
base,
cont_offset,
cont_length,
offset_size,
length_size,
creation_order_tracked,
&mut messages,
&mut continuations,
)?;
}
Ok(ObjectHeader {
version: 2,
messages,
reference_count: 0, modification_time,
})
}
fn parse_v2_storage(
storage: &dyn Storage,
address: u64,
offset_size: u8,
length_size: u8,
) -> Result<Self> {
let prefix = storage.read_range(address, 64)?;
let mut cursor = Cursor::new(prefix.as_ref());
let sig = cursor.read_bytes(4)?;
if sig != OHDR_SIGNATURE {
return Err(Error::InvalidObjectHeaderSignature);
}
let version = cursor.read_u8()?;
if version != 2 {
return Err(Error::UnsupportedObjectHeaderVersion(version));
}
let flags = cursor.read_u8()?;
let modification_time = if (flags & 0x20) != 0 {
let _access_time = cursor.read_u32_le()?;
let mod_time = cursor.read_u32_le()?;
let _change_time = cursor.read_u32_le()?;
let _birth_time = cursor.read_u32_le()?;
Some(mod_time)
} else {
None
};
if (flags & 0x10) != 0 {
let _max_compact = cursor.read_u16_le()?;
let _min_dense = cursor.read_u16_le()?;
}
let size_field_width = 1usize << (flags & 0x03);
let chunk0_data_size = cursor.read_uvar(size_field_width)?;
let creation_order_tracked = (flags & 0x04) != 0;
let messages_start = cursor.position() as usize;
let chunk0_end = messages_start + chunk0_data_size as usize;
let chunk = storage.read_range(address, chunk0_end + 4)?;
let stored_checksum = u32::from_le_bytes(
chunk.as_ref()[chunk0_end..chunk0_end + 4]
.try_into()
.unwrap(),
);
let computed = jenkins_lookup3(&chunk.as_ref()[..chunk0_end]);
if computed != stored_checksum {
return Err(Error::ChecksumMismatch {
expected: stored_checksum,
actual: computed,
});
}
let mut messages = Vec::new();
let mut continuations = Vec::new();
Self::read_v2_messages_from_slice(
&chunk.as_ref()[messages_start..chunk0_end],
offset_size,
length_size,
creation_order_tracked,
&mut messages,
&mut continuations,
)?;
while let Some((cont_offset, cont_length)) = continuations.pop() {
Self::read_v2_continuation_chunk_storage(
storage,
cont_offset,
cont_length,
offset_size,
length_size,
creation_order_tracked,
&mut messages,
&mut continuations,
)?;
}
Ok(ObjectHeader {
version: 2,
messages,
reference_count: 0,
modification_time,
})
}
#[allow(clippy::too_many_arguments)]
fn read_v2_messages(
base: &Cursor<'_>,
start: u64,
end: u64,
offset_size: u8,
length_size: u8,
creation_order_tracked: bool,
messages: &mut Vec<HdfMessage>,
continuations: &mut Vec<(u64, u64)>,
) -> Result<()> {
let mut cursor = base.at_offset(start)?;
let min_envelope = if creation_order_tracked { 6 } else { 4 };
while cursor.position() + min_envelope as u64 <= end {
let msg_type = cursor.read_u8()? as u16;
let msg_data_size = cursor.read_u16_le()? as usize;
let msg_flags = cursor.read_u8()?;
if creation_order_tracked {
let _creation_order = cursor.read_u16_le()?;
}
if msg_type == MSG_TYPE_NIL {
if msg_data_size == 0
&& base.data()[cursor.position() as usize..end as usize]
.iter()
.all(|byte| *byte == 0)
{
break;
}
cursor.skip(msg_data_size)?;
messages.push(HdfMessage::Nil);
continue;
}
if cursor.position() + msg_data_size as u64 > end {
return Err(Error::InvalidData(format!(
"v2 message data ({} bytes) extends past chunk end",
msg_data_size
)));
}
let msg_data = cursor.read_bytes(msg_data_size)?;
let is_shared = (msg_flags & 0x02) != 0;
if is_shared {
let shared_msg = crate::messages::shared::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
messages.push(HdfMessage::Shared(shared_msg));
} else if msg_type == MSG_TYPE_CONTINUATION {
let cont = crate::messages::continuation::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
continuations.push((cont.offset, cont.length));
messages.push(HdfMessage::ObjectHeaderContinuation);
} else {
let parsed = parse_message(
msg_type,
msg_data.len(),
&mut Cursor::new(msg_data),
offset_size,
length_size,
)?;
messages.push(parsed);
}
}
Ok(())
}
fn read_v1_messages_from_slice(
data: &[u8],
offset_size: u8,
length_size: u8,
messages: &mut Vec<HdfMessage>,
continuations: &mut Vec<(u64, u64)>,
) -> Result<()> {
let mut cursor = Cursor::new(data);
while cursor.remaining() >= 8 {
let msg_type = cursor.read_u16_le()?;
let msg_data_size = cursor.read_u16_le()? as usize;
let msg_flags = cursor.read_u8()?;
let _reserved = cursor.read_bytes(3)?;
if cursor.remaining() < msg_data_size as u64 {
return Err(Error::InvalidData(format!(
"v1 message data ({} bytes) extends past header chunk end",
msg_data_size
)));
}
if msg_type == MSG_TYPE_NIL {
cursor.skip(msg_data_size)?;
messages.push(HdfMessage::Nil);
continue;
}
let msg_data = cursor.read_bytes(msg_data_size)?;
let is_shared = (msg_flags & 0x02) != 0;
if is_shared {
let shared_msg = crate::messages::shared::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
messages.push(HdfMessage::Shared(shared_msg));
} else if msg_type == MSG_TYPE_CONTINUATION {
let cont = crate::messages::continuation::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
continuations.push((cont.offset, cont.length));
messages.push(HdfMessage::ObjectHeaderContinuation);
} else {
let parsed = parse_message(
msg_type,
msg_data.len(),
&mut Cursor::new(msg_data),
offset_size,
length_size,
)?;
messages.push(parsed);
}
}
Ok(())
}
fn read_v2_messages_from_slice(
data: &[u8],
offset_size: u8,
length_size: u8,
creation_order_tracked: bool,
messages: &mut Vec<HdfMessage>,
continuations: &mut Vec<(u64, u64)>,
) -> Result<()> {
let mut cursor = Cursor::new(data);
let min_envelope = if creation_order_tracked { 6 } else { 4 };
while cursor.remaining() >= min_envelope as u64 {
let msg_type = cursor.read_u8()? as u16;
let msg_data_size = cursor.read_u16_le()? as usize;
let msg_flags = cursor.read_u8()?;
if creation_order_tracked {
let _creation_order = cursor.read_u16_le()?;
}
if msg_type == MSG_TYPE_NIL {
if msg_data_size == 0
&& data[cursor.position() as usize..]
.iter()
.all(|byte| *byte == 0)
{
break;
}
cursor.skip(msg_data_size)?;
messages.push(HdfMessage::Nil);
continue;
}
if cursor.remaining() < msg_data_size as u64 {
return Err(Error::InvalidData(format!(
"v2 message data ({} bytes) extends past chunk end",
msg_data_size
)));
}
let msg_data = cursor.read_bytes(msg_data_size)?;
let is_shared = (msg_flags & 0x02) != 0;
if is_shared {
let shared_msg = crate::messages::shared::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
messages.push(HdfMessage::Shared(shared_msg));
} else if msg_type == MSG_TYPE_CONTINUATION {
let cont = crate::messages::continuation::parse(
&mut Cursor::new(msg_data),
offset_size,
length_size,
msg_data_size,
)?;
continuations.push((cont.offset, cont.length));
messages.push(HdfMessage::ObjectHeaderContinuation);
} else {
let parsed = parse_message(
msg_type,
msg_data.len(),
&mut Cursor::new(msg_data),
offset_size,
length_size,
)?;
messages.push(parsed);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn read_v2_continuation_chunk_storage(
storage: &dyn Storage,
cont_offset: u64,
cont_length: u64,
offset_size: u8,
length_size: u8,
creation_order_tracked: bool,
messages: &mut Vec<HdfMessage>,
continuations: &mut Vec<(u64, u64)>,
) -> Result<()> {
let chunk = storage.read_range(cont_offset, cont_length as usize)?;
if chunk.len() < 8 || chunk.as_ref()[..4] != OCHK_SIGNATURE {
return Err(Error::InvalidObjectHeaderSignature);
}
let messages_end = chunk.len() - 4;
let stored_checksum = u32::from_le_bytes(
chunk.as_ref()[messages_end..messages_end + 4]
.try_into()
.unwrap(),
);
let computed = jenkins_lookup3(&chunk.as_ref()[..messages_end]);
if computed != stored_checksum {
return Err(Error::ChecksumMismatch {
expected: stored_checksum,
actual: computed,
});
}
Self::read_v2_messages_from_slice(
&chunk.as_ref()[4..messages_end],
offset_size,
length_size,
creation_order_tracked,
messages,
continuations,
)
}
#[allow(clippy::too_many_arguments)]
fn read_v2_continuation_chunk(
base: &Cursor<'_>,
cont_offset: u64,
cont_length: u64,
offset_size: u8,
length_size: u8,
creation_order_tracked: bool,
messages: &mut Vec<HdfMessage>,
continuations: &mut Vec<(u64, u64)>,
) -> Result<()> {
let mut cursor = base.at_offset(cont_offset)?;
let sig = cursor.read_bytes(4)?;
if sig != OCHK_SIGNATURE {
return Err(Error::InvalidObjectHeaderSignature);
}
let chunk_end = cont_offset + cont_length;
let messages_end = chunk_end - 4;
let messages_start = cursor.position();
let checksum_start = cont_offset as usize;
let checksum_end = messages_end as usize;
let stored_checksum = {
let mut ck = base.at_offset(messages_end)?;
ck.read_u32_le()?
};
let computed = jenkins_lookup3(&base.data()[checksum_start..checksum_end]);
if computed != stored_checksum {
return Err(Error::ChecksumMismatch {
expected: stored_checksum,
actual: computed,
});
}
Self::read_v2_messages(
base,
messages_start,
messages_end,
offset_size,
length_size,
creation_order_tracked,
messages,
continuations,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checksum::jenkins_lookup3;
fn build_v1_header(raw_messages: &[(u16, u8, &[u8])], ref_count: u32) -> Vec<u8> {
let data_size: usize = raw_messages
.iter()
.map(|(_, _, payload)| 8 + payload.len()) .sum();
let mut buf = Vec::new();
buf.push(1);
buf.push(0);
buf.extend_from_slice(&(raw_messages.len() as u16).to_le_bytes());
buf.extend_from_slice(&ref_count.to_le_bytes());
buf.extend_from_slice(&(data_size as u32).to_le_bytes());
buf.extend_from_slice(&[0u8; 4]);
for (type_id, flags, payload) in raw_messages {
buf.extend_from_slice(&type_id.to_le_bytes());
buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
buf.push(*flags);
buf.extend_from_slice(&[0u8; 3]); buf.extend_from_slice(payload);
}
buf
}
fn build_v2_header(
header_flags: u8,
raw_messages: &[(u8, u8, &[u8])],
timestamps: Option<[u32; 4]>,
phase_change: Option<(u16, u16)>,
) -> Vec<u8> {
let creation_order = (header_flags & 0x04) != 0;
let envelope_size: usize = if creation_order { 6 } else { 4 };
let msg_data_size: usize = raw_messages
.iter()
.map(|(_, _, payload)| envelope_size + payload.len())
.sum();
let mut buf = Vec::new();
buf.extend_from_slice(&OHDR_SIGNATURE);
buf.push(2);
buf.push(header_flags);
if let Some(ts) = timestamps {
for &t in &ts {
buf.extend_from_slice(&t.to_le_bytes());
}
}
if let Some((max_compact, min_dense)) = phase_change {
buf.extend_from_slice(&max_compact.to_le_bytes());
buf.extend_from_slice(&min_dense.to_le_bytes());
}
let size_width = 1usize << (header_flags & 0x03);
match size_width {
1 => buf.push(msg_data_size as u8),
2 => buf.extend_from_slice(&(msg_data_size as u16).to_le_bytes()),
4 => buf.extend_from_slice(&(msg_data_size as u32).to_le_bytes()),
8 => buf.extend_from_slice(&(msg_data_size as u64).to_le_bytes()),
_ => unreachable!(),
}
for (type_id, mflags, payload) in raw_messages {
buf.push(*type_id);
buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
buf.push(*mflags);
if creation_order {
buf.extend_from_slice(&0u16.to_le_bytes());
}
buf.extend_from_slice(payload);
}
let ck = jenkins_lookup3(&buf);
buf.extend_from_slice(&ck.to_le_bytes());
buf
}
fn build_v2_ochk(raw_messages: &[(u8, u8, &[u8])], creation_order: bool) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&OCHK_SIGNATURE);
for (type_id, mflags, payload) in raw_messages {
buf.push(*type_id);
buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
buf.push(*mflags);
if creation_order {
buf.extend_from_slice(&0u16.to_le_bytes());
}
buf.extend_from_slice(payload);
}
let ck = jenkins_lookup3(&buf);
buf.extend_from_slice(&ck.to_le_bytes());
buf
}
#[test]
fn v1_empty_header() {
let data = build_v1_header(&[], 1);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.version, 1);
assert_eq!(hdr.reference_count, 1);
assert!(hdr.messages.is_empty());
assert!(hdr.modification_time.is_none());
}
#[test]
fn v1_nil_message() {
let data = build_v1_header(&[(0x0000, 0, &[0u8; 4])], 1);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
assert!(matches!(hdr.messages[0], HdfMessage::Nil));
}
#[test]
fn v1_unknown_message() {
let payload = [0xAA, 0xBB, 0xCC];
let data = build_v1_header(&[(0x00FF, 0, &payload)], 2);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.reference_count, 2);
assert_eq!(hdr.messages.len(), 1);
match &hdr.messages[0] {
HdfMessage::Unknown { type_id, data } => {
assert_eq!(*type_id, 0x00FF);
assert_eq!(data.as_slice(), &payload);
}
other => panic!("expected Unknown, got {:?}", other),
}
}
#[test]
fn v1_symbol_table_message() {
let mut payload = Vec::new();
payload.extend_from_slice(&0x1000u64.to_le_bytes());
payload.extend_from_slice(&0x2000u64.to_le_bytes());
let data = build_v1_header(&[(0x0011, 0, &payload)], 1);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
match &hdr.messages[0] {
HdfMessage::SymbolTable(st) => {
assert_eq!(st.btree_address, 0x1000);
assert_eq!(st.heap_address, 0x2000);
}
other => panic!("expected SymbolTable, got {:?}", other),
}
}
#[test]
fn v1_continuation_message() {
let unknown_payload = [0xDD; 2];
let mut cont_chunk = Vec::new();
cont_chunk.extend_from_slice(&0x00FEu16.to_le_bytes());
cont_chunk.extend_from_slice(&(unknown_payload.len() as u16).to_le_bytes());
cont_chunk.push(0);
cont_chunk.extend_from_slice(&[0u8; 3]);
cont_chunk.extend_from_slice(&unknown_payload);
let main_header_base_size = 16; let cont_msg_envelope_size = 8 + 16; let cont_chunk_offset = (main_header_base_size + cont_msg_envelope_size) as u64;
let mut cont_payload = Vec::new();
cont_payload.extend_from_slice(&cont_chunk_offset.to_le_bytes()); cont_payload.extend_from_slice(&(cont_chunk.len() as u64).to_le_bytes());
let main_header = build_v1_header(&[(MSG_TYPE_CONTINUATION, 0, &cont_payload)], 1);
let mut file_data = main_header;
assert_eq!(file_data.len() as u64, cont_chunk_offset);
file_data.extend_from_slice(&cont_chunk);
let hdr = ObjectHeader::parse_at(&file_data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 2);
assert!(matches!(
hdr.messages[0],
HdfMessage::ObjectHeaderContinuation
));
match &hdr.messages[1] {
HdfMessage::Unknown { type_id, data } => {
assert_eq!(*type_id, 0x00FE);
assert_eq!(data.as_slice(), &unknown_payload);
}
other => panic!("expected Unknown from continuation, got {:?}", other),
}
}
#[test]
fn v1_nonzero_address_offset() {
let prefix_pad = vec![0xFFu8; 64];
let header = build_v1_header(&[(0x00AA, 0, &[0x01])], 3);
let mut file_data = prefix_pad;
file_data.extend_from_slice(&header);
let hdr = ObjectHeader::parse_at(&file_data, 64, 8, 8).unwrap();
assert_eq!(hdr.version, 1);
assert_eq!(hdr.reference_count, 3);
assert_eq!(hdr.messages.len(), 1);
}
#[test]
fn v1_bad_version() {
let mut data = build_v1_header(&[], 1);
data[0] = 3; let err = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap_err();
assert!(matches!(err, Error::UnsupportedObjectHeaderVersion(3)));
}
#[test]
fn v2_empty_header() {
let data = build_v2_header(0x00, &[], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.version, 2);
assert!(hdr.messages.is_empty());
assert!(hdr.modification_time.is_none());
}
#[test]
fn v2_nil_message() {
let data = build_v2_header(0x00, &[(0x00, 0, &[0u8; 3])], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
assert!(matches!(hdr.messages[0], HdfMessage::Nil));
}
#[test]
fn v2_unknown_message() {
let payload = [0x11, 0x22];
let data = build_v2_header(0x00, &[(0xFE, 0, &payload)], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
match &hdr.messages[0] {
HdfMessage::Unknown { type_id, data } => {
assert_eq!(*type_id, 0x00FE);
assert_eq!(data.as_slice(), &payload);
}
other => panic!("expected Unknown, got {:?}", other),
}
}
#[test]
fn v2_with_timestamps() {
let flags = 0x20;
let ts = [1000u32, 2000, 3000, 4000]; let data = build_v2_header(flags, &[], Some(ts), None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.modification_time, Some(2000));
}
#[test]
fn v2_with_phase_change() {
let flags = 0x10;
let data = build_v2_header(flags, &[], None, Some((8, 6)));
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert!(hdr.messages.is_empty());
}
#[test]
fn v2_with_creation_order() {
let flags = 0x04;
let payload = [0xAA];
let data = build_v2_header(flags, &[(0xFE, 0, &payload)], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
match &hdr.messages[0] {
HdfMessage::Unknown { type_id, .. } => assert_eq!(*type_id, 0x00FE),
other => panic!("expected Unknown, got {:?}", other),
}
}
#[test]
fn v2_2byte_size_field() {
let flags = 0x01;
let payload = [0x42; 5];
let data = build_v2_header(flags, &[(0xFE, 0, &payload)], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
}
#[test]
fn v2_4byte_size_field() {
let flags = 0x02;
let data = build_v2_header(flags, &[(0xFE, 0, &[0x01])], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
}
#[test]
fn v2_8byte_size_field() {
let flags = 0x03;
let data = build_v2_header(flags, &[(0xFE, 0, &[0x01])], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 1);
}
#[test]
fn v2_checksum_mismatch() {
let mut data = build_v2_header(0x00, &[(0xFE, 0, &[0x01])], None, None);
let last = data.len() - 1;
data[last] ^= 0xFF;
let err = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap_err();
assert!(matches!(err, Error::ChecksumMismatch { .. }));
}
#[test]
fn v2_continuation_chunk() {
let unknown_payload = [0xCC; 3];
let ochk = build_v2_ochk(&[(0xFD, 0, &unknown_payload)], false);
let mut cont_payload = vec![0u8; 16];
let ohdr_size = 4 + 1 + 1 + 1 + (4 + cont_payload.len()) + 4;
let ochk_offset = ohdr_size as u64;
cont_payload.clear();
cont_payload.extend_from_slice(&ochk_offset.to_le_bytes());
cont_payload.extend_from_slice(&(ochk.len() as u64).to_le_bytes());
let ohdr = build_v2_header(0x00, &[(0x10, 0, &cont_payload)], None, None);
assert_eq!(ohdr.len(), ohdr_size);
let mut file_data = ohdr;
file_data.extend_from_slice(&ochk);
let hdr = ObjectHeader::parse_at(&file_data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 2);
assert!(matches!(
hdr.messages[0],
HdfMessage::ObjectHeaderContinuation
));
match &hdr.messages[1] {
HdfMessage::Unknown { type_id, data } => {
assert_eq!(*type_id, 0x00FD);
assert_eq!(data.as_slice(), &unknown_payload);
}
other => panic!("expected Unknown from OCHK, got {:?}", other),
}
}
#[test]
fn v2_ochk_checksum_mismatch() {
let unknown_payload = [0xCC; 3];
let mut ochk = build_v2_ochk(&[(0xFD, 0, &unknown_payload)], false);
let last = ochk.len() - 1;
ochk[last] ^= 0xFF;
let ohdr_size = 4 + 1 + 1 + 1 + (4 + 16) + 4; let ochk_offset = ohdr_size as u64;
let mut cont_payload = Vec::new();
cont_payload.extend_from_slice(&ochk_offset.to_le_bytes());
cont_payload.extend_from_slice(&(ochk.len() as u64).to_le_bytes());
let ohdr = build_v2_header(0x00, &[(0x10, 0, &cont_payload)], None, None);
let mut file_data = ohdr;
file_data.extend_from_slice(&ochk);
let err = ObjectHeader::parse_at(&file_data, 0, 8, 8).unwrap_err();
assert!(matches!(err, Error::ChecksumMismatch { .. }));
}
#[test]
fn v2_multiple_messages() {
let p1 = [0x01, 0x02];
let p2 = [0x03, 0x04, 0x05];
let data = build_v2_header(0x00, &[(0xA0, 0, &p1), (0xA1, 0, &p2)], None, None);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 2);
match &hdr.messages[0] {
HdfMessage::Unknown { type_id, .. } => assert_eq!(*type_id, 0x00A0),
other => panic!("expected Unknown 0xA0, got {:?}", other),
}
match &hdr.messages[1] {
HdfMessage::Unknown { type_id, .. } => assert_eq!(*type_id, 0x00A1),
other => panic!("expected Unknown 0xA1, got {:?}", other),
}
}
#[test]
fn v2_zero_length_nil_before_more_messages() {
let p1 = [0xAA];
let p2 = [0xBB];
let data = build_v2_header(
0x04,
&[(0xFE, 0, &p1), (0x00, 0, &[]), (0xFD, 0, &p2)],
None,
None,
);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.messages.len(), 3);
assert!(matches!(hdr.messages[0], HdfMessage::Unknown { .. }));
assert!(matches!(hdr.messages[1], HdfMessage::Nil));
assert!(matches!(hdr.messages[2], HdfMessage::Unknown { .. }));
}
#[test]
fn v2_nonzero_address() {
let prefix_pad = vec![0u8; 128];
let ohdr = build_v2_header(0x00, &[(0xFE, 0, &[0x42])], None, None);
let mut file_data = prefix_pad;
file_data.extend_from_slice(&ohdr);
let hdr = ObjectHeader::parse_at(&file_data, 128, 8, 8).unwrap();
assert_eq!(hdr.version, 2);
assert_eq!(hdr.messages.len(), 1);
}
#[test]
fn v2_all_flags_combined() {
let flags = 0x20 | 0x10 | 0x04 | 0x01;
let ts = [100u32, 200, 300, 400];
let payload = [0xBB];
let data = build_v2_header(flags, &[(0xFE, 0, &payload)], Some(ts), Some((12, 8)));
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.version, 2);
assert_eq!(hdr.modification_time, Some(200));
assert_eq!(hdr.messages.len(), 1);
}
#[test]
fn v1_multiple_messages() {
let p1 = [0xAA; 4];
let p2 = [0xBB; 8];
let data = build_v1_header(&[(0x00FF, 0, &p1), (0x00FE, 0, &p2)], 5);
let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
assert_eq!(hdr.version, 1);
assert_eq!(hdr.reference_count, 5);
assert_eq!(hdr.messages.len(), 2);
}
#[test]
fn v1_4byte_offsets() {
let mut payload = Vec::new();
payload.extend_from_slice(&0x1000u32.to_le_bytes());
payload.extend_from_slice(&0x2000u32.to_le_bytes());
let data = build_v1_header(&[(0x0011, 0, &payload)], 1);
let hdr = ObjectHeader::parse_at(&data, 0, 4, 4).unwrap();
assert_eq!(hdr.messages.len(), 1);
match &hdr.messages[0] {
HdfMessage::SymbolTable(st) => {
assert_eq!(st.btree_address, 0x1000);
assert_eq!(st.heap_address, 0x2000);
}
other => panic!("expected SymbolTable, got {:?}", other),
}
}
}