#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
use crate::address::BaseAddress;
use crate::btree_v2::{
BTreeV2Header, collect_btree_v2_records, collect_btree_v2_records_from_source,
};
use crate::convert::TryToUsize;
use crate::error::{FormatError, ResolveError};
use crate::fractal_heap::FractalHeapHeader;
use crate::group_v1::{self, GroupEntry};
use crate::link_info::LinkInfoMessage;
use crate::link_message::{LinkMessage, LinkTarget, link_is_named};
use crate::message_type::MessageType;
use crate::object_header::{MessageFilter, ObjectHeader};
use crate::source::{BaseOffsetSource, Source, frame};
use crate::superblock::Superblock;
use crate::symbol_table::SymbolTableMessage;
pub fn resolve_v2_group_entries(
file_data: &[u8],
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
) -> Result<Vec<GroupEntry>, FormatError> {
let link_info = find_link_info(object_header, offset_size)?;
if let Some(fh_addr) = link_info.fractal_heap_address {
let framed = frame(file_data, base_address)?;
resolve_dense_entries(framed, &link_info, fh_addr, offset_size, length_size)
} else {
resolve_compact_entries(object_header, offset_size)
}
}
fn resolve_compact_entries(
object_header: &ObjectHeader,
offset_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let mut entries = Vec::new();
for msg in &object_header.messages {
if msg.msg_type == MessageType::Link {
let link = LinkMessage::parse(&msg.data, offset_size)?;
if let LinkTarget::Hard {
object_header_address,
} = link.link_target
{
entries.push(GroupEntry {
name: link.name,
object_header_address,
cache_type: 0,
});
}
}
}
Ok(entries)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChildLookup {
Found(u64),
Absent,
NotAGroup,
}
impl ChildLookup {
fn of(base: BaseAddress, address: Option<u64>) -> Result<Self, FormatError> {
Ok(match address {
Some(address) => ChildLookup::Found(base.absolute(address)?),
None => ChildLookup::Absent,
})
}
}
pub(crate) fn find_child_address(
file_data: &[u8],
group_address: u64,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
name: &str,
) -> Result<ChildLookup, FormatError> {
let mut saw_link = false;
let header = {
let mut wanted = wanted_link_only(name, &mut saw_link);
ObjectHeader::parse_filtered(
file_data,
group_address.to_usize()?,
offset_size,
length_size,
base_address,
MessageFilter::Only(&mut wanted),
)?
};
if !filtered_is_group(&header, saw_link) {
return Ok(ChildLookup::NotAGroup);
}
if holds_compact_links(&header, offset_size)? {
return ChildLookup::of(
base_address,
scan_compact_links(&header, offset_size, name)?,
);
}
ChildLookup::of(
base_address,
resolve_group_entries(file_data, &header, offset_size, length_size, base_address)?
.into_iter()
.find(|e| e.name == name)
.map(|e| e.object_header_address),
)
}
pub(crate) fn find_child_address_from_source<S: Source + ?Sized>(
source: &S,
group_address: u64,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
name: &str,
) -> Result<ChildLookup, FormatError> {
let mut saw_link = false;
let header = {
let mut wanted = wanted_link_only(name, &mut saw_link);
ObjectHeader::parse_from_source_filtered(
source,
group_address,
offset_size,
length_size,
base_address,
MessageFilter::Only(&mut wanted),
)?
};
if !filtered_is_group(&header, saw_link) {
return Ok(ChildLookup::NotAGroup);
}
if holds_compact_links(&header, offset_size)? {
return ChildLookup::of(
base_address,
scan_compact_links(&header, offset_size, name)?,
);
}
ChildLookup::of(
base_address,
resolve_group_entries_from_source(source, &header, offset_size, length_size, base_address)?
.into_iter()
.find(|e| e.name == name)
.map(|e| e.object_header_address),
)
}
fn wanted_link_only<'a>(
name: &'a str,
saw_link: &'a mut bool,
) -> impl FnMut(MessageType, &[u8]) -> bool + 'a {
move |ty, body| {
if ty != MessageType::Link {
return true;
}
*saw_link = true;
link_is_named(body, name)
}
}
fn holds_compact_links(header: &ObjectHeader, offset_size: u8) -> Result<bool, FormatError> {
Ok(!is_v1_group(header)
&& find_link_info(header, offset_size)?
.fractal_heap_address
.is_none())
}
fn filtered_is_group(header: &ObjectHeader, saw_link: bool) -> bool {
saw_link || is_group(header)
}
fn scan_compact_links(
object_header: &ObjectHeader,
offset_size: u8,
name: &str,
) -> Result<Option<u64>, FormatError> {
let mut found = None;
for msg in &object_header.messages {
if msg.msg_type != MessageType::Link {
continue;
}
let addr = LinkMessage::hard_link_address_if_named(&msg.data, offset_size, name)?;
found = found.or(addr);
}
Ok(found)
}
fn resolve_dense_entries(
file_data: &[u8],
link_info: &LinkInfoMessage,
fh_addr: u64,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let fh = FractalHeapHeader::parse(file_data, fh_addr.to_usize()?, offset_size, length_size)?;
let btree_addr = link_info
.btree_name_index_address
.ok_or_else(|| FormatError::PathNotFound(String::from("no B-tree v2 name index")))?;
let btree_hdr =
BTreeV2Header::parse(file_data, btree_addr.to_usize()?, offset_size, length_size)?;
let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
let mut heap = fh.object_reader(offset_size, length_size);
let mut entries = Vec::new();
for record in &records {
let id_offset = if btree_hdr.tree_type == 5 {
4 } else {
8 };
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];
let link_data = heap.read(file_data, id_bytes)?;
let link = LinkMessage::parse(&link_data, offset_size)?;
if let LinkTarget::Hard {
object_header_address,
} = link.link_target
{
entries.push(GroupEntry {
name: link.name,
object_header_address,
cache_type: 0,
});
}
}
Ok(entries)
}
fn find_link_info(
object_header: &ObjectHeader,
offset_size: u8,
) -> Result<LinkInfoMessage, FormatError> {
for msg in &object_header.messages {
if msg.msg_type == MessageType::LinkInfo {
return LinkInfoMessage::parse(&msg.data, offset_size);
}
}
Ok(LinkInfoMessage {
max_creation_order: None,
fractal_heap_address: None,
btree_name_index_address: None,
btree_creation_order_address: None,
})
}
fn is_v2_group(object_header: &ObjectHeader) -> bool {
object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::LinkInfo || m.msg_type == MessageType::Link)
}
fn is_v1_group(object_header: &ObjectHeader) -> bool {
object_header
.messages
.iter()
.any(|m| m.msg_type == MessageType::SymbolTable)
}
pub(crate) fn is_group(object_header: &ObjectHeader) -> bool {
is_v1_group(object_header) || is_v2_group(object_header)
}
fn walked_prefix(components: &[&str], i: usize) -> String {
components[..i].join("/")
}
pub fn resolve_path_any(
file_data: &[u8],
superblock: &Superblock,
path: &str,
) -> Result<u64, ResolveError> {
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if components.is_empty() {
return Ok(superblock.root_group_address);
}
let os = superblock.offset_size;
let ls = superblock.length_size;
let base = superblock.base_address;
let mut current_addr = superblock.root_group_address;
for (i, component) in components.iter().enumerate() {
match find_child_address(file_data, current_addr, os, ls, base, component)? {
ChildLookup::Found(abs_addr) => {
if i == components.len() - 1 {
return Ok(abs_addr);
}
current_addr = abs_addr;
}
ChildLookup::Absent => {
return Err(FormatError::PathNotFound(String::from(*component)).into());
}
ChildLookup::NotAGroup => {
return Err(ResolveError::NotAGroup(walked_prefix(&components, i)));
}
}
}
Ok(current_addr)
}
pub fn resolve_group_entries(
file_data: &[u8],
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
) -> Result<Vec<GroupEntry>, FormatError> {
if is_v1_group(object_header) {
let sym_msg = object_header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
group_v1::resolve_v1_group_entries(file_data, &stm, offset_size, length_size, base_address)
} else if is_v2_group(object_header) {
resolve_v2_group_entries(
file_data,
object_header,
offset_size,
length_size,
base_address,
)
} else {
Err(FormatError::PathNotFound(String::from(
"object header is not a group",
)))
}
}
pub fn resolve_path_any_from_source<S: Source + ?Sized>(
source: &S,
superblock: &Superblock,
path: &str,
) -> Result<u64, ResolveError> {
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if components.is_empty() {
return Ok(superblock.root_group_address);
}
let os = superblock.offset_size;
let ls = superblock.length_size;
let base = superblock.base_address;
let mut current_addr = superblock.root_group_address;
for (i, component) in components.iter().enumerate() {
match find_child_address_from_source(source, current_addr, os, ls, base, component)? {
ChildLookup::Found(abs_addr) => {
if i == components.len() - 1 {
return Ok(abs_addr);
}
current_addr = abs_addr;
}
ChildLookup::Absent => {
return Err(FormatError::PathNotFound(String::from(*component)).into());
}
ChildLookup::NotAGroup => {
return Err(ResolveError::NotAGroup(walked_prefix(&components, i)));
}
}
}
Ok(current_addr)
}
pub fn resolve_group_entries_from_source<S: Source + ?Sized>(
source: &S,
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
) -> Result<Vec<GroupEntry>, FormatError> {
if is_v1_group(object_header) {
let sym_msg = object_header
.messages
.iter()
.find(|m| m.msg_type == MessageType::SymbolTable)
.ok_or_else(|| FormatError::PathNotFound(String::from("no symbol table message")))?;
let stm = SymbolTableMessage::parse(&sym_msg.data, offset_size)?;
group_v1::resolve_v1_group_entries_from_source(
source,
&stm,
offset_size,
length_size,
base_address,
)
} else if is_v2_group(object_header) {
resolve_v2_group_entries_from_source(
source,
object_header,
offset_size,
length_size,
base_address,
)
} else {
Err(FormatError::PathNotFound(String::from(
"object header is not a group",
)))
}
}
fn resolve_v2_group_entries_from_source<S: Source + ?Sized>(
source: &S,
object_header: &ObjectHeader,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
) -> Result<Vec<GroupEntry>, FormatError> {
let link_info = find_link_info(object_header, offset_size)?;
if let Some(fh_addr) = link_info.fractal_heap_address {
let framed = BaseOffsetSource {
inner: source,
base: base_address,
};
resolve_dense_entries_from_source(&framed, &link_info, fh_addr, offset_size, length_size)
} else {
resolve_compact_entries(object_header, offset_size)
}
}
fn resolve_dense_entries_from_source<S: Source + ?Sized>(
source: &S,
link_info: &LinkInfoMessage,
fh_addr: u64,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let fh = FractalHeapHeader::parse_from_source(source, fh_addr, offset_size, length_size)?;
let btree_addr = link_info
.btree_name_index_address
.ok_or_else(|| FormatError::PathNotFound(String::from("no B-tree v2 name index")))?;
let btree_hdr = BTreeV2Header::parse_from_source(source, btree_addr, offset_size, length_size)?;
let records =
collect_btree_v2_records_from_source(source, &btree_hdr, offset_size, length_size)?;
let mut heap = fh.object_reader(offset_size, length_size);
let mut entries = Vec::new();
for record in &records {
let id_offset = if btree_hdr.tree_type == 5 { 4 } else { 8 };
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];
let link_data = heap.read_from_source(source, id_bytes)?;
let link = LinkMessage::parse(&link_data, offset_size)?;
if let LinkTarget::Hard {
object_header_address,
} = link.link_target
{
entries.push(GroupEntry {
name: link.name,
object_header_address,
cache_type: 0,
});
}
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_layout::DataLayout;
use crate::data_read;
use crate::dataspace::Dataspace;
use crate::datatype::Datatype;
use crate::signature;
fn extract_dataset(
_file_data: &[u8],
hdr: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> (Datatype, Dataspace, DataLayout) {
let dt_data = &hdr
.messages
.iter()
.find(|m| m.msg_type == MessageType::Datatype)
.unwrap()
.data;
let ds_data = &hdr
.messages
.iter()
.find(|m| m.msg_type == MessageType::Dataspace)
.unwrap()
.data;
let dl_data = &hdr
.messages
.iter()
.find(|m| m.msg_type == MessageType::DataLayout)
.unwrap()
.data;
let (dt, _) = Datatype::parse(dt_data).unwrap();
let ds = Dataspace::parse(ds_data, length_size).unwrap();
let dl = DataLayout::parse(dl_data, offset_size, length_size).unwrap();
(dt, ds, dl)
}
#[test]
fn compact_storage_link_messages() {
let link_data = {
let mut d = Vec::new();
d.push(1); d.push(0x00); d.push(4); d.extend_from_slice(b"test");
d.extend_from_slice(&0x1000u64.to_le_bytes()); d
};
let oh = ObjectHeader {
version: 2,
messages: vec![
crate::object_header::HeaderMessage {
msg_type: MessageType::LinkInfo,
size: 18,
flags: 0,
creation_order: None,
data: {
let mut d = Vec::new();
d.push(0); d.push(0); d.extend_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); d.extend_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); d
},
},
crate::object_header::HeaderMessage {
msg_type: MessageType::Link,
size: link_data.len(),
flags: 0,
creation_order: None,
data: link_data,
},
],
reference_count: None,
flags: 0,
access_time: None,
modification_time: None,
change_time: None,
birth_time: None,
};
let entries = resolve_v2_group_entries(&[], &oh, 8, 8, BaseAddress::ZERO).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "test");
assert_eq!(entries[0].object_header_address, 0x1000);
}
fn header_of(messages: Vec<(MessageType, Vec<u8>)>) -> ObjectHeader {
ObjectHeader {
version: 2,
messages: messages
.into_iter()
.map(|(msg_type, data)| crate::object_header::HeaderMessage {
msg_type,
size: data.len(),
flags: 0,
creation_order: None,
data,
})
.collect(),
reference_count: None,
flags: 0,
access_time: None,
modification_time: None,
change_time: None,
birth_time: None,
}
}
fn compact_link_info() -> (MessageType, Vec<u8>) {
let mut d = vec![0, 0]; d.extend_from_slice(&u64::MAX.to_le_bytes()); d.extend_from_slice(&u64::MAX.to_le_bytes()); (MessageType::LinkInfo, d)
}
fn dense_link_info_message() -> (MessageType, Vec<u8>) {
let mut d = vec![0, 0];
d.extend_from_slice(&0x1000u64.to_le_bytes()); d.extend_from_slice(&0x2000u64.to_le_bytes()); (MessageType::LinkInfo, d)
}
#[test]
fn a_filtered_out_link_still_makes_the_header_a_compact_group() {
let no_link_info = header_of(vec![]);
assert!(
filtered_is_group(&no_link_info, true),
"a header whose links the filter dropped is still a group"
);
assert!(
holds_compact_links(&no_link_info, 8).unwrap(),
"and one holding its links compactly, so the scan answers it"
);
assert!(
!filtered_is_group(&no_link_info, false),
"a header that held no link and no link info is no group of ours"
);
}
#[test]
fn a_dense_group_is_never_scanned_for_compact_links() {
let dense = header_of(vec![dense_link_info_message()]);
assert!(
filtered_is_group(&dense, true),
"a dense group is a group, so a lookup goes on to ask the question below"
);
assert!(
!holds_compact_links(&dense, 8).unwrap(),
"a fractal heap decides, not the presence of a link message"
);
}
#[test]
fn scanning_compact_links_answers_for_hard_links_alone() {
let mut hard = vec![1u8, 0x00, 4];
hard.extend_from_slice(b"data");
hard.extend_from_slice(&0x4000u64.to_le_bytes());
let mut soft = vec![1u8, 0x08, 1, 4];
soft.extend_from_slice(b"soft");
soft.extend_from_slice(&4u16.to_le_bytes());
soft.extend_from_slice(b"/abc");
let header = header_of(vec![
compact_link_info(),
(MessageType::Link, soft),
(MessageType::Link, hard),
]);
assert_eq!(
scan_compact_links(&header, 8, "data").unwrap(),
Some(0x4000)
);
assert_eq!(
scan_compact_links(&header, 8, "soft").unwrap(),
None,
"a soft link names a path, not an object header"
);
assert_eq!(scan_compact_links(&header, 8, "absent").unwrap(), None);
}
#[test]
fn integration_v2_groups_temperature() {
let file_data: &[u8] = include_bytes!("../tests/data/unattributed/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
assert!(sb.version >= 2);
let addr = resolve_path_any(file_data, &sb, "sensors/temperature").unwrap();
let hdr =
ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
let raw = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_f64(&raw, &dt).unwrap();
assert_eq!(values, vec![22.5, 23.1, 21.8]);
}
#[test]
fn integration_v2_groups_humidity() {
let file_data: &[u8] = include_bytes!("../tests/data/unattributed/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "sensors/humidity").unwrap();
let hdr =
ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
let raw = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_i32(&raw, &dt).unwrap();
assert_eq!(values, vec![45, 50, 55]);
}
#[test]
fn integration_v2_many_links() {
let file_data: &[u8] = include_bytes!("../tests/data/unattributed/v2_many_links.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "dataset_015").unwrap();
let hdr =
ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
let raw = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_f64(&raw, &dt).unwrap();
assert_eq!(values, vec![15.0]);
}
#[test]
fn integration_resolve_path_any_v1() {
let file_data: &[u8] = include_bytes!("../tests/data/unattributed/two_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "group1/values").unwrap();
let hdr =
ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
let raw = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_i32(&raw, &dt).unwrap();
assert_eq!(values, vec![10, 20, 30]);
}
#[test]
fn integration_resolve_path_any_v2() {
let file_data: &[u8] = include_bytes!("../tests/data/unattributed/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let addr = resolve_path_any(file_data, &sb, "sensors/temperature").unwrap();
let hdr =
ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
let (dt, ds, dl) = extract_dataset(file_data, &hdr, sb.offset_size, sb.length_size);
let raw = data_read::read_raw_data(file_data, &dl, &ds, &dt).unwrap();
let values = data_read::read_as_f64(&raw, &dt).unwrap();
assert_eq!(values, vec![22.5, 23.1, 21.8]);
}
#[test]
fn path_not_found_v2() {
let file_data: &[u8] = include_bytes!("../tests/data/unattributed/v2_groups.h5");
let sig_offset = signature::find_signature(file_data).unwrap();
let sb = Superblock::parse(file_data, sig_offset).unwrap();
let err = resolve_path_any(file_data, &sb, "nonexistent").unwrap_err();
assert!(matches!(
err,
ResolveError::Format(FormatError::PathNotFound(_))
));
}
#[cfg(feature = "std")]
#[test]
fn a_link_target_that_overflows_the_base_address_is_refused() {
const UB: u64 = 512;
let mut b = crate::writer::FileBuilder::new();
b.with_userblock(UB);
b.create_dataset("d")
.with_i32_data(&[1, 2, 3])
.with_shape(&[3]);
let mut bytes = b.finish().unwrap();
let root_oh = bytes
.windows(4)
.position(|w| w == b"OHDR")
.expect("a latest-format file has a version 2 root object header");
let link = bytes
.windows(2)
.position(|w| w == [1, b'd'])
.expect("the root header holds a one-character link name");
let target = link + 2;
let stored = u64::from_le_bytes(bytes[target..target + 8].try_into().unwrap());
assert_eq!(
stored + UB,
bytes
.windows(4)
.enumerate()
.filter(|(_, w)| *w == b"OHDR")
.nth(1)
.expect("the dataset has an object header too")
.0 as u64,
"the bytes after the link name must be the dataset's stored address"
);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ub.h5");
std::fs::write(&path, &bytes).unwrap();
for read in [
crate::reader::File::from_bytes(bytes.clone()).unwrap(),
crate::reader::File::open_streaming(&path).unwrap(),
] {
assert_eq!(read.dataset("d").unwrap().read::<i32>().unwrap(), [1, 2, 3]);
}
bytes[target..target + 8].copy_from_slice(&u64::MAX.to_le_bytes());
let (chunk_at, chunk_len) = crate::edit::read_oh_chunks(
&crate::source::BytesSource::new(&bytes[..]),
root_oh as u64,
BaseAddress::ZERO,
)
.expect("the root header is readable")[0]
.span;
let end = (chunk_at + chunk_len).to_usize().unwrap() - 4;
let repaired = crate::checksum::jenkins_lookup3(&bytes[chunk_at.to_usize().unwrap()..end]);
bytes[end..end + 4].copy_from_slice(&repaired.to_le_bytes());
std::fs::write(&path, &bytes).unwrap();
for (backend, file) in [
("buffered", crate::reader::File::from_bytes(bytes).unwrap()),
(
"streaming",
crate::reader::File::open_streaming(&path).unwrap(),
),
] {
let err = file.dataset("d").unwrap_err();
assert!(
matches!(
err,
crate::Error::Format(FormatError::OffsetOverflow {
offset: u64::MAX,
length: UB,
})
),
"{backend}: expected the base-address addition to be refused, got {err:?}"
);
}
}
}
#[cfg(test)]
mod huge_link_tests {
use super::*;
use crate::fractal_heap::{huge_index_decodes, reset_huge_index_decodes};
use crate::signature;
use crate::source::BytesSource;
fn file_with_huge_links(count: usize) -> Vec<u8> {
std::fs::read(crate::test_data::path(&format!("c/huge_links_{count}.h5"))).unwrap()
}
fn dense_link_info(bytes: &[u8]) -> (LinkInfoMessage, u64, u8, u8) {
let sig = signature::find_signature(bytes).unwrap();
let superblock = Superblock::parse(bytes, sig).unwrap();
let (offset_size, length_size) = (superblock.offset_size, superblock.length_size);
let group_addr = resolve_path_any(bytes, &superblock, "g").unwrap();
let header = ObjectHeader::parse(
bytes,
group_addr.to_usize().unwrap(),
offset_size,
length_size,
)
.unwrap();
let link_info = find_link_info(&header, offset_size).unwrap();
let fh_addr = link_info
.fractal_heap_address
.expect("this many long-named links are stored densely");
(link_info, fh_addr, offset_size, length_size)
}
#[test]
fn a_dense_link_walk_parses_its_huge_object_index_once() {
const COUNT: usize = 12;
let bytes = file_with_huge_links(COUNT);
let (link_info, fh_addr, offset_size, length_size) = dense_link_info(&bytes);
reset_huge_index_decodes();
let buffered =
resolve_dense_entries(&bytes, &link_info, fh_addr, offset_size, length_size).unwrap();
assert_eq!(buffered.len(), COUNT, "the walk must still read every link");
assert_eq!(
huge_index_decodes(),
1,
"the buffered walk parsed the huge-object index per link rather than per walk"
);
reset_huge_index_decodes();
let source = BytesSource::new(bytes);
let streamed = resolve_dense_entries_from_source(
&source,
&link_info,
fh_addr,
offset_size,
length_size,
)
.unwrap();
assert_eq!(streamed.len(), COUNT);
assert_eq!(
huge_index_decodes(),
1,
"the streaming walk parsed the huge-object index per link rather than per walk"
);
let names: Vec<&str> = buffered.iter().map(|e| e.name.as_str()).collect();
for (i, entry) in streamed.iter().enumerate() {
assert!(
names.contains(&entry.name.as_str()),
"link {i} differs between the two backends"
);
}
}
}