fix(format): locate the address in version-1 shared messages

A version-1 shared message reference is version, type, six reserved bytes
and then an old-style symbol table entry: link-name offset (length size),
object header address, cache type, reserved, scratch. We read the address
straight after the reserved bytes, i.e. the link-name offset, and the
committed datatype lookup failed with InvalidObjectHeaderVersion (the bytes
checked in tcompound.h5: name offset 0x10, then 0x590 = /type1). Datasets
of 1.4/1.6-era files that use a committed datatype were unreadable.

Skip the name offset. parse_shared_ref has no length size, so add
parse_shared_ref_sized and use it in every internal caller;
parse_shared_ref keeps its signature and assumes length size == offset
size. The old parse_v1_ref unit test encoded the wrong layout and now uses
the real bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-25 21:55:31 -05:00
co-authored by Claude Opus 5.5
parent 36356ba8a1
commit c7092722aa
7 changed files with 127 additions and 13 deletions
+3 -2
View File
@@ -97,7 +97,7 @@ impl AttributeMessage {
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)?;
let shared_ref = shared_message::parse_shared_ref_sized(bytes, offset_size, length_size)?;
shared_message::resolve_shared_message(
file_data,
&shared_ref,
@@ -407,7 +407,8 @@ pub fn extract_attributes_full(
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 shared_ref =
shared_message::parse_shared_ref_sized(&msg.data, offset_size, length_size)?;
let resolved_data = shared_message::resolve_shared_message(
file_data,
&shared_ref,
+38 -11
View File
@@ -154,13 +154,29 @@ pub fn is_shared(msg_flags: u8) -> bool {
///
/// When the shared flag is set on a message, the data contains a reference
/// instead of the actual message content.
///
/// Assumes the file's length size equals its offset size, which only matters
/// for version-1 references; use [`parse_shared_ref_sized`] when the
/// superblock's length size is known.
pub fn parse_shared_ref(data: &[u8], offset_size: u8) -> Result<SharedMessageRef, FormatError> {
parse_shared_ref_sized(data, offset_size, offset_size)
}
/// [`parse_shared_ref`] with the superblock's length size, which locates the
/// object header address in a version-1 reference.
pub fn parse_shared_ref_sized(
data: &[u8],
offset_size: u8,
length_size: u8,
) -> Result<SharedMessageRef, FormatError> {
ensure_len(data, 0, 2)?;
let version = data[0];
let ref_type = data[1];
// Layouts (HDF5 spec IV.A.2 "Shared Message", and libhdf5's decoder):
// v1: version, type, reserved(6), address — always "committed"
// v1: version, type, reserved(6), then an old-style symbol table
// entry: link-name offset(length_size), object header address,
// cache type(4), reserved(4), scratch(16) — always "committed"
// v2: version, type, address — always "committed"
// v3: version, type, then a fractal-heap ID if type == SOHM, otherwise
// an address
@@ -177,7 +193,7 @@ pub fn parse_shared_ref(data: &[u8], offset_size: u8) -> Result<SharedMessageRef
})
};
match version {
1 => address_at(2 + 6),
1 => address_at(2 + 6 + length_size as usize),
2 => address_at(2),
3 if ref_type == SHARE_TYPE_SOHM => {
ensure_len(data, 2, FHEAP_ID_LEN)?;
@@ -434,7 +450,7 @@ pub fn message_data_with_sohm<'a>(
if !is_shared(msg.flags) {
return Ok(Cow::Borrowed(&msg.data));
}
let shared_ref = parse_shared_ref(&msg.data, offset_size)?;
let shared_ref = parse_shared_ref_sized(&msg.data, offset_size, length_size)?;
let table = if shared_ref.heap_id.is_some() {
load_sohm_table(file_data, offset_size, length_size)?
} else {
@@ -514,7 +530,7 @@ pub fn message_data<'a>(
if !is_shared(msg.flags) {
return Ok(Cow::Borrowed(&msg.data));
}
let shared_ref = parse_shared_ref(&msg.data, offset_size)?;
let shared_ref = parse_shared_ref_sized(&msg.data, offset_size, length_size)?;
resolve_shared_message(
file_data,
&shared_ref,
@@ -649,15 +665,26 @@ mod tests {
#[test]
fn parse_v1_ref() {
let mut data = Vec::new();
data.push(1); // version
data.push(0); // type
data.extend_from_slice(&[0u8; 6]); // reserved
data.extend_from_slice(&0x5678u64.to_le_bytes());
// Datatype message of `/group1/dset2` in HDF5's `tcompound.h5`
// (written in 2000): version 1, six reserved bytes, then an old-style
// symbol table entry — link-name offset 0x10, object header address
// 0x590 (the committed datatype `/type1`), cache type, reserved and
// scratch.
let mut data = vec![1, 0, 0, 0, 0, 0, 0, 0];
data.extend_from_slice(&0x10u64.to_le_bytes());
data.extend_from_slice(&0x590u64.to_le_bytes());
data.extend_from_slice(&[0; 24]);
let shared = parse_shared_ref(&data, 8).unwrap();
let shared = parse_shared_ref_sized(&data, 8, 8).unwrap();
assert_eq!(shared.version, 1);
assert_eq!(shared.object_header_address, Some(0x5678));
assert_eq!(shared.object_header_address, Some(0x590));
// The name offset is a length: 4 bytes here, then an 8-byte address.
let mut data = vec![1, 0, 0, 0, 0, 0, 0, 0];
data.extend_from_slice(&0x10u32.to_le_bytes());
data.extend_from_slice(&0x590u64.to_le_bytes());
let shared = parse_shared_ref_sized(&data, 8, 4).unwrap();
assert_eq!(shared.object_header_address, Some(0x590));
}
#[test]