use std::collections::{BTreeMap, BTreeSet};
use crate::address::BaseAddress;
use crate::attribute::AttributeMessage;
use crate::checksum::jenkins_lookup3;
use crate::data_layout::{COMPACT_DATA_OFFSET, DataLayout};
use crate::datatype::{
Datatype, class_may_hold_object_address, datatype_holds_object_address,
embedded_reference_slots, stored_object_references,
};
use crate::edit::read_oh_chunks;
use crate::error::Error;
use crate::group_v2::resolve_group_entries_from_source;
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::source::Source;
use crate::superblock::Superblock;
#[derive(Debug, Default)]
pub(crate) struct Plan {
data_writes: Vec<(u64, u64)>,
header_writes: BTreeMap<(u64, u64), Vec<(usize, u64)>>,
proved_free_of_references: bool,
}
pub(crate) trait PatchTarget {
fn read(&self, at: u64, len: usize) -> Result<Vec<u8>, Error>;
fn write(&mut self, at: u64, bytes: &[u8]) -> Result<(), Error>;
}
impl Plan {
pub(crate) fn is_empty(&self) -> bool {
self.data_writes.is_empty() && self.header_writes.is_empty()
}
pub(crate) fn proved_free_of_references(&self) -> bool {
self.proved_free_of_references
}
pub(crate) fn apply(&self, target: &mut impl PatchTarget) -> Result<(), Error> {
for &(at, value) in &self.data_writes {
target.write(at, &value.to_le_bytes())?;
}
for (&(at, len), edits) in &self.header_writes {
let len = usize::try_from(len)
.map_err(|_| Error::EditUnsupported("object header chunk exceeds this platform"))?;
let body_len = len
.checked_sub(4)
.ok_or(Error::EditUnsupported("object header chunk is too short"))?;
let mut chunk = target.read(at, len)?;
let mut from = body_len;
for &(offset, value) in edits {
let end = offset.checked_add(8).filter(|&e| e <= body_len).ok_or(
Error::EditUnsupported(
"a stored reference sits outside the object header chunk holding it",
),
)?;
chunk[offset..end].copy_from_slice(&value.to_le_bytes());
from = from.min(offset);
}
let checksum = jenkins_lookup3(&chunk[..body_len]);
chunk[body_len..].copy_from_slice(&checksum.to_le_bytes());
target.write(at + from as u64, &chunk[from..])?;
}
Ok(())
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.data_writes.len() + self.header_writes.values().map(Vec::len).sum::<usize>()
}
#[cfg(test)]
pub(crate) fn sites(&self) -> Vec<u64> {
let headers = self
.header_writes
.iter()
.flat_map(|(&(at, _), edits)| edits.iter().map(move |&(o, _)| at + o as u64));
self.data_writes
.iter()
.map(|&(at, _)| at)
.chain(headers)
.collect()
}
}
std::thread_local! {
static WALKS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn reset_walks() {
WALKS.with(|count| count.set(0));
}
#[cfg(test)]
pub(crate) fn walks() -> usize {
WALKS.with(|count| count.get())
}
pub(crate) fn plan<S: Source + ?Sized>(
src: &S,
superblock: &Superblock,
relocations: &BTreeMap<u64, u64>,
budget: u32,
) -> Result<Plan, Error> {
let mut plan = Plan::default();
if relocations.is_empty() {
return Ok(plan);
}
WALKS.with(|count| count.set(count.get() + 1));
let os = superblock.offset_size;
let ls = superblock.length_size;
let base = superblock.base_address;
let mut visited: BTreeSet<u64> = BTreeSet::new();
let mut stack: Vec<u64> = vec![superblock.root_group_address];
let mut budget = budget;
let mut saw_reference = false;
let mut complete = true;
while let Some(addr) = stack.pop() {
if !visited.insert(addr) {
continue; }
if relocations.contains_key(&addr) {
complete = false;
continue;
}
if budget == 0 {
complete = false;
break;
}
budget -= 1;
let outcome = scan_object(src, addr, base, relocations, &mut plan)?;
saw_reference |= outcome.holds_a_reference;
complete &= outcome.fully_read;
if !outcome.descend {
continue;
}
let Ok(header) = ObjectHeader::parse_from_source(src, addr, os, ls, base) else {
complete = false;
continue;
};
if !outcome.header_located {
saw_reference |= scan_parsed_header(src, &header, base, relocations, &mut plan);
}
let Ok(entries) = resolve_group_entries_from_source(src, &header, os, ls, base) else {
complete = false;
continue;
};
for e in entries {
if let Ok(child) = base.absolute(e.object_header_address) {
stack.push(child);
}
}
}
plan.proved_free_of_references = complete && !saw_reference;
Ok(plan)
}
fn scan_parsed_header<S: Source + ?Sized>(
src: &S,
header: &ObjectHeader,
base: BaseAddress,
relocations: &BTreeMap<u64, u64>,
plan: &mut Plan,
) -> bool {
use crate::shared_message::SharedResolver as _;
let mut element_dt = None;
let mut layout = None;
for message in &header.messages {
match message.msg_type {
MessageType::Datatype => {
let resolved;
let encoded = if message.flags & crate::edit::MSG_FLAG_SHARED != 0 {
let framed = crate::source::BaseOffsetSource { inner: src, base };
let resolver = crate::shared_message::SourceResolver::new(
&framed,
crate::file_writer::OFFSET_SIZE,
crate::file_writer::LENGTH_SIZE,
None,
);
match resolver.resolve(&message.data, MessageType::Datatype) {
Ok(bytes) => {
resolved = bytes;
&resolved[..]
}
Err(_) => continue,
}
} else {
&message.data[..]
};
if encoded
.first()
.is_some_and(|&b| crate::datatype::class_may_hold_object_address(b))
{
if let Ok((dt, _)) = Datatype::parse(encoded) {
element_dt = Some(dt);
}
}
}
MessageType::DataLayout => {
layout = DataLayout::parse(
&message.data,
crate::file_writer::OFFSET_SIZE,
crate::file_writer::LENGTH_SIZE,
)
.ok();
}
_ => {}
}
}
let Some(dt) = element_dt else {
return false;
};
let holds = datatype_holds_object_address(&dt);
let (
Some(slots),
Some(DataLayout::Contiguous {
address: Some(a),
size,
}),
) = (element_slots(&dt), layout)
else {
return holds;
};
let (Ok(at), Ok(want)) = (base.absolute(a), usize::try_from(size)) else {
return holds;
};
let Ok(raw) = src.read_exact_at(at, want) else {
return holds;
};
collect_slots(
&dt,
&slots,
&raw,
at,
base,
relocations,
&mut plan.data_writes,
);
holds
}
struct Scanned {
descend: bool,
holds_a_reference: bool,
fully_read: bool,
header_located: bool,
}
fn scan_object<S: Source + ?Sized>(
src: &S,
addr: u64,
base: BaseAddress,
relocations: &BTreeMap<u64, u64>,
plan: &mut Plan,
) -> Result<Scanned, Error> {
let Ok(chunks) = read_oh_chunks(src, addr, base) else {
return Ok(Scanned {
descend: true,
holds_a_reference: false,
fully_read: false,
header_located: false,
});
};
use crate::shared_message::SharedResolver as _;
let framed = crate::source::BaseOffsetSource { inner: src, base };
let resolver = crate::shared_message::SourceResolver::new(
&framed,
crate::file_writer::OFFSET_SIZE,
crate::file_writer::LENGTH_SIZE,
None,
);
let mut element_dt: Option<Datatype> = None;
let mut layout_msg: Option<(&[u8], u64)> = None;
let mut out = Scanned {
descend: false,
holds_a_reference: false,
fully_read: true,
header_located: true,
};
let mut committed: Option<Vec<u8>> = None;
let mut edits: Vec<(u64, u64)> = Vec::new();
for chunk in &chunks {
let layout = chunk.layout();
let (region, mut p) = chunk.message_region();
while let Some((msg_type, body, body_end)) = layout.next_message(region, p)? {
let body_at = chunk.span.0 + body as u64;
let shared = region[p + 3] & crate::edit::MSG_FLAG_SHARED != 0;
match msg_type {
MessageType::SymbolTable | MessageType::Link | MessageType::LinkInfo => {
out.descend = true;
}
MessageType::Datatype => {
let encoded = if shared {
match resolver.resolve(®ion[body..body_end], MessageType::Datatype) {
Ok(bytes) => committed.insert(bytes),
Err(_) => {
out.fully_read = false;
p = body_end;
continue;
}
}
} else {
®ion[body..body_end]
};
if encoded.is_empty() || !class_may_hold_object_address(encoded[0]) {
p = body_end;
continue;
}
match Datatype::parse(encoded) {
Ok((dt, _)) => {
out.holds_a_reference |= datatype_holds_object_address(&dt);
element_dt = Some(dt);
}
Err(_) => out.fully_read = false,
}
}
MessageType::DataLayout => {
layout_msg = Some((®ion[body..body_end], body_at));
}
MessageType::AttributeInfo
if crate::edit::attribute_info_is_dense(®ion[body..body_end]) =>
{
out.fully_read = false;
}
MessageType::Attribute if shared => out.fully_read = false,
MessageType::Attribute => {
let Ok((attr, data_off)) = AttributeMessage::parse_resolving_at(
®ion[body..body_end],
crate::file_writer::LENGTH_SIZE,
&resolver,
) else {
out.fully_read = false;
p = body_end;
continue;
};
out.holds_a_reference |= datatype_holds_object_address(&attr.datatype);
if let Some(slots) = element_slots(&attr.datatype) {
edits.clear();
collect_slots(
&attr.datatype,
&slots,
&attr.raw_data,
body_at + data_off as u64,
base,
relocations,
&mut edits,
);
record_header_edits(plan, chunk.span, &edits);
}
}
_ => {}
}
p = body_end;
}
}
let (Some(dt), Some((layout_body, layout_at))) = (element_dt, layout_msg) else {
return Ok(out);
};
let Some(slots) = element_slots(&dt) else {
return Ok(out);
};
let Ok(dl) = DataLayout::parse(
layout_body,
crate::file_writer::OFFSET_SIZE,
crate::file_writer::LENGTH_SIZE,
) else {
return Ok(out);
};
match dl {
DataLayout::Contiguous {
address: Some(a),
size,
} => {
let Ok(at) = base.absolute(a) else {
return Ok(out);
};
let Ok(want) = usize::try_from(size) else {
return Ok(out);
};
let Ok(raw) = src.read_exact_at(at, want) else {
return Ok(out);
};
collect_slots(
&dt,
&slots,
&raw,
at,
base,
relocations,
&mut plan.data_writes,
);
}
DataLayout::Compact { data } => {
edits.clear();
collect_slots(
&dt,
&slots,
&data,
layout_at + COMPACT_DATA_OFFSET as u64,
base,
relocations,
&mut edits,
);
if let Some(span) = chunks
.iter()
.map(|c| c.span)
.find(|&(a, l)| layout_at >= a && layout_at - a < l)
{
record_header_edits(plan, span, &edits);
}
}
_ => {}
}
Ok(out)
}
fn record_header_edits(plan: &mut Plan, span: (u64, u64), edits: &[(u64, u64)]) {
for &(at, value) in edits {
let Some(offset) = at.checked_sub(span.0).and_then(|o| usize::try_from(o).ok()) else {
continue;
};
plan.header_writes
.entry(span)
.or_default()
.push((offset, value));
}
}
fn element_slots(dt: &Datatype) -> Option<Vec<usize>> {
embedded_reference_slots(dt).filter(|s| !s.is_empty())
}
pub(crate) fn attribute_references_are_repointable(dt: &Datatype) -> bool {
element_slots(dt).is_some()
}
fn collect_slots(
dt: &Datatype,
slots: &[usize],
raw: &[u8],
raw_at: u64,
base: BaseAddress,
relocations: &BTreeMap<u64, u64>,
out: &mut Vec<(u64, u64)>,
) {
for (offset, stored) in stored_object_references(raw, dt.type_size() as usize, slots) {
if stored == 0 || stored == u64::MAX {
continue;
}
let Ok(abs) = base.absolute(stored) else {
continue;
};
let Some(&new) = relocations.get(&abs) else {
continue;
};
let Ok(value) = base.relative(new) else {
continue;
};
out.push((raw_at + offset as u64, value));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::attribute::AttributeMessage;
use crate::datatype::ReferenceType;
use crate::edit::build_v2_object_header;
use crate::source::BytesSource;
use crate::type_builders::{AttrValue, build_attr_message, make_object_reference_type};
const HEADER_AT: u64 = 4096;
fn message_record(msg_type: MessageType, body: &[u8]) -> Vec<u8> {
let mut record = vec![msg_type.to_u16() as u8, 0, 0, 0];
record[1..3].copy_from_slice(&(body.len() as u16).to_le_bytes());
record.extend_from_slice(body);
record
}
fn plain_region(bytes: Vec<u8>) -> crate::edit::OhRegion {
crate::edit::OhRegion::new(bytes, crate::edit::OhHeaderProps::PLAIN)
}
fn reference_attr(name: &str, address: u64) -> AttributeMessage {
let mut attr = build_attr_message(name, &AttrValue::U64(address));
attr.datatype = make_object_reference_type();
attr
}
fn image_with_header(region: &[u8]) -> (BytesSource<Vec<u8>>, Vec<u8>) {
let header = build_v2_object_header(&plain_region(region.to_vec())).unwrap();
let mut bytes = vec![0xAAu8; HEADER_AT as usize];
bytes.extend_from_slice(&header);
(BytesSource::new(bytes.clone()), bytes)
}
struct Bytes(Vec<u8>);
impl PatchTarget for Bytes {
fn read(&self, at: u64, len: usize) -> Result<Vec<u8>, Error> {
let at = at as usize;
Ok(self.0[at..at + len].to_vec())
}
fn write(&mut self, at: u64, bytes: &[u8]) -> Result<(), Error> {
let at = at as usize;
self.0[at..at + bytes.len()].copy_from_slice(bytes);
Ok(())
}
}
fn attribute_info(fractal_heap: Option<u64>) -> Vec<u8> {
let mut body = vec![0u8, 0x00];
body.extend_from_slice(&fractal_heap.unwrap_or(u64::MAX).to_le_bytes());
body.extend_from_slice(&u64::MAX.to_le_bytes());
body
}
struct Recording {
bytes: Bytes,
writes: Vec<(u64, usize)>,
}
impl PatchTarget for Recording {
fn read(&self, at: u64, len: usize) -> Result<Vec<u8>, Error> {
self.bytes.read(at, len)
}
fn write(&mut self, at: u64, bytes: &[u8]) -> Result<(), Error> {
self.writes.push((at, bytes.len()));
self.bytes.write(at, bytes)
}
}
fn scan(src: &BytesSource<Vec<u8>>, relocations: &[(u64, u64)]) -> (Plan, Scanned) {
let mut plan = Plan::default();
let map: BTreeMap<u64, u64> = relocations.iter().copied().collect();
let scanned = scan_object(src, HEADER_AT, BaseAddress::ZERO, &map, &mut plan).unwrap();
(plan, scanned)
}
#[test]
fn an_inline_reference_attribute_is_repointed_and_the_chunk_resealed() {
let region = message_record(
MessageType::Attribute,
&reference_attr("target", 300).serialize_v3(crate::file_writer::LENGTH_SIZE),
);
let (src, bytes) = image_with_header(®ion);
let (plan, scanned) = scan(&src, &[(300, 900)]);
assert!(scanned.holds_a_reference);
assert!(scanned.fully_read);
assert_eq!(plan.len(), 1);
assert!(plan.data_writes.is_empty());
assert_eq!(plan.header_writes.len(), 1);
let site = plan.sites()[0] as usize;
assert_eq!(
u64::from_le_bytes(bytes[site..site + 8].try_into().unwrap()),
300,
"the plan must aim at the bytes that hold the address it read"
);
let mut target = Bytes(bytes.clone());
plan.apply(&mut target).unwrap();
assert_eq!(target.0.len(), bytes.len());
let rescanned = BytesSource::new(target.0.clone());
let (again, _) = scan(&rescanned, &[(300, 900)]);
assert!(again.is_empty(), "the address should already be 900");
let (moved_again, _) = scan(&rescanned, &[(900, 1500)]);
assert_eq!(moved_again.len(), 1, "and it should now read as 900");
assert_checksum_holds(&target.0);
}
fn assert_checksum_holds(bytes: &[u8]) {
let chunk = &bytes[HEADER_AT as usize..];
let body = &chunk[..chunk.len() - 4];
assert_eq!(
u32::from_le_bytes(chunk[chunk.len() - 4..].try_into().unwrap()),
jenkins_lookup3(body),
"the header chunk must be resealed after its bytes change"
);
}
#[test]
fn a_compact_reference_dataset_is_repointed_in_place() {
let mut region = message_record(
MessageType::Datatype,
&make_object_reference_type().serialize(),
);
let mut layout = vec![3u8, 0];
layout.extend_from_slice(&8u16.to_le_bytes());
layout.extend_from_slice(&300u64.to_le_bytes());
region.extend_from_slice(&message_record(MessageType::DataLayout, &layout));
let (src, bytes) = image_with_header(®ion);
let (plan, scanned) = scan(&src, &[(300, 900)]);
assert!(scanned.holds_a_reference);
assert_eq!(plan.len(), 1);
assert!(plan.data_writes.is_empty(), "compact data is in the header");
let mut target = Bytes(bytes);
plan.apply(&mut target).unwrap();
assert_checksum_holds(&target.0);
let (again, _) = scan(&BytesSource::new(target.0.clone()), &[(900, 1500)]);
assert_eq!(again.len(), 1);
}
#[test]
fn the_two_undefined_addresses_are_never_repointed() {
for sentinel in [0u64, u64::MAX] {
let region = message_record(
MessageType::Attribute,
&reference_attr("target", sentinel).serialize_v3(crate::file_writer::LENGTH_SIZE),
);
let (src, _) = image_with_header(®ion);
let (plan, scanned) = scan(&src, &[(sentinel, 900), (0, 900)]);
assert!(
plan.is_empty(),
"the sentinel {sentinel:#x} names no object and must be left alone"
);
assert!(
scanned.holds_a_reference,
"it is still a reference datatype"
);
}
}
#[test]
fn a_reference_this_cannot_address_leaves_the_file_unproven_and_untouched() {
let mut attr = build_attr_message("target", &AttrValue::U64(300));
attr.datatype = Datatype::Reference {
size: 16,
ref_type: ReferenceType::Object,
};
let region = message_record(
MessageType::Attribute,
&attr.serialize_v3(crate::file_writer::LENGTH_SIZE),
);
let (src, _) = image_with_header(®ion);
let (plan, scanned) = scan(&src, &[(300, 900)]);
assert!(plan.is_empty());
assert!(
scanned.holds_a_reference,
"an unmappable reference is still a reference: the file must never be \
proved free of them"
);
}
#[test]
fn dense_attribute_storage_leaves_the_object_unproven() {
let dense = message_record(MessageType::AttributeInfo, &attribute_info(Some(4096)));
let (src, _) = image_with_header(&dense);
let (plan, scanned) = scan(&src, &[(300, 900)]);
assert!(plan.is_empty(), "there is nothing here this can address");
assert!(
!scanned.fully_read,
"a dense attribute set is unread, so the object cannot be counted \
towards a file proved free of references"
);
let compact = message_record(MessageType::AttributeInfo, &attribute_info(None));
let (src, _) = image_with_header(&compact);
let (_, scanned) = scan(&src, &[(300, 900)]);
assert!(
scanned.fully_read,
"an Attribute Info message is not dense storage by itself"
);
}
#[test]
fn a_shared_attribute_record_leaves_the_object_unproven() {
let attr = reference_attr("target", 300).serialize_v3(crate::file_writer::LENGTH_SIZE);
let mut record = message_record(MessageType::Attribute, &attr);
record[3] = crate::edit::MSG_FLAG_SHARED;
let (src, _) = image_with_header(&record);
let (plan, scanned) = scan(&src, &[(300, 900)]);
assert!(
plan.is_empty(),
"the message body is a pointer into the shared table, not the value"
);
assert!(
!scanned.fully_read,
"an unread attribute leaves the object unproven"
);
}
#[test]
fn a_header_chunk_is_written_from_the_byte_that_changed() {
let region = message_record(
MessageType::Attribute,
&reference_attr("target", 300).serialize_v3(crate::file_writer::LENGTH_SIZE),
);
let (src, bytes) = image_with_header(®ion);
let (plan, _) = scan(&src, &[(300, 900)]);
let site = plan.sites()[0];
let mut target = Recording {
bytes: Bytes(bytes),
writes: Vec::new(),
};
plan.apply(&mut target).unwrap();
assert_eq!(
target.writes.len(),
1,
"one chunk, one write — value and checksum together"
);
assert_eq!(
target.writes[0].0, site,
"the write starts at the repointed address, not at the chunk"
);
}
#[test]
fn a_committed_datatype_is_resolved_through_the_base_address() {
for base in [BaseAddress::ZERO, BaseAddress::new(1024)] {
const TYPE_AT: u64 = 2048;
let committed = build_v2_object_header(&plain_region(message_record(
MessageType::Datatype,
&make_object_reference_type().serialize(),
)))
.unwrap();
let mut shared = message_record(
MessageType::Datatype,
&crate::shared_message::encode_committed_ref(
base.relative(TYPE_AT).unwrap(),
crate::file_writer::OFFSET_SIZE,
),
);
shared[3] = crate::edit::MSG_FLAG_SHARED;
let mut layout = vec![3u8, 0];
layout.extend_from_slice(&8u16.to_le_bytes());
layout.extend_from_slice(&(300u64).to_le_bytes());
shared.extend_from_slice(&message_record(MessageType::DataLayout, &layout));
let dataset = build_v2_object_header(&plain_region(shared)).unwrap();
let mut bytes = vec![0xAAu8; TYPE_AT as usize];
bytes.extend_from_slice(&committed);
bytes.resize(HEADER_AT as usize, 0xAA);
bytes.extend_from_slice(&dataset);
let mut plan = Plan::default();
let map: BTreeMap<u64, u64> =
[(300 + base.get(), 900 + base.get())].into_iter().collect();
let scanned =
scan_object(&BytesSource::new(bytes), HEADER_AT, base, &map, &mut plan).unwrap();
assert!(
scanned.holds_a_reference,
"base {base:?}: the committed type must be followed far enough to \
see it names a reference"
);
assert_eq!(
plan.len(),
1,
"base {base:?}: the element must be repointed"
);
}
}
#[test]
fn a_vacated_header_is_not_scanned_even_when_a_link_still_names_it() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("vacated.h5");
let mut b = crate::writer::FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1, 2, 3]);
b.create_dataset("refs").with_path_references(&["d"]);
b.write(&path).unwrap();
let file = crate::File::open(&path).unwrap();
let superblock = file.superblock().clone();
let root = superblock.root_group_address;
let stored = u64::from_le_bytes(
file.dataset("refs").unwrap().read_raw().unwrap()[..8]
.try_into()
.unwrap(),
);
drop(file);
let source = crate::source::BytesSource::new(std::fs::read(&path).unwrap());
let moved: BTreeMap<u64, u64> = [(stored, stored + 4096)].into_iter().collect();
let reached = super::plan(&source, &superblock, &moved, 1 << 20).unwrap();
assert_eq!(reached.len(), 1, "the walk must reach `refs` at all");
let mut vacated = moved.clone();
vacated.insert(root, root + 8192);
let skipped = super::plan(&source, &superblock, &vacated, 1 << 20).unwrap();
assert!(
skipped.is_empty(),
"a vacated header must not be scanned, nor descended through"
);
assert!(
!skipped.proved_free_of_references(),
"and skipping it leaves the file unproven rather than proven clean"
);
}
#[test]
fn an_address_no_relocation_names_is_left_alone() {
let region = message_record(
MessageType::Attribute,
&reference_attr("target", 300).serialize_v3(crate::file_writer::LENGTH_SIZE),
);
let (src, _) = image_with_header(®ion);
let (plan, _) = scan(&src, &[(301, 900), (299, 900)]);
assert!(
plan.is_empty(),
"only the exact vacated address is repointed; a neighbour is a \
different object"
);
}
}