use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use crate::checksum::jenkins_lookup3;
use crate::chunked_write::{ChunkOptions, build_chunked_data_at_ext};
use crate::data_layout::DataLayout;
use crate::dataspace::{Dataspace, DataspaceType};
use crate::error::Error;
use crate::file_lock::{self, FileLocking};
use crate::file_space_info::{FileSpaceInfo, FileSpaceStrategy};
use crate::file_writer::{
LENGTH_SIZE, OFFSET_SIZE, build_chunked_dataset_oh, build_dataset_oh, make_link,
};
use crate::filters::ChunkContext;
use crate::free_space::FreeList;
use crate::free_space_manager::{self, FreeSection, FsmHeader, fshd_len, serialize_file_fsm};
use crate::group_v2::resolve_group_entries;
use crate::link_message::{LinkMessage, LinkTarget};
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::signature;
use crate::superblock::Superblock;
use crate::type_builders::{AttrValue, DatasetBuilder, build_attr_message};
const UNDEF: u64 = u64::MAX;
const MAX_COMPACT_ATTRS: usize = 8;
const MAX_COPY_DEPTH: u32 = 1000;
const MAX_LINK_GRAPH_NODES: u32 = 1 << 24;
const MAX_OH_CHUNKS: usize = 256;
type PathKey = Vec<String>;
pub struct EditSession {
handle: fs::File,
data: Vec<u8>,
sb_sig_off: usize,
superblock: Superblock,
pending_datasets: Vec<(PathKey, DatasetBuilder)>,
pending_writes: Vec<(PathKey, DatasetBuilder)>,
pending_groups: Vec<PathKey>,
pending_group_attrs: Vec<(PathKey, GroupAttrOp)>,
pending_deletes: Vec<PathKey>,
pending_copies: Vec<(PathKey, PathKey)>,
pending_cross_copies: Vec<(PathKey, CopyTree)>,
free: FreeList,
persist: Option<PersistState>,
}
struct PersistState {
strategy: FileSpaceStrategy,
threshold: u64,
page_size: u64,
old_blocks: Vec<(u64, u64)>,
}
impl EditSession {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
Self::open_with_locking(path, FileLocking::Enabled)
}
pub fn open_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self, Error> {
let path = path.as_ref();
let mut handle = fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(Error::Io)?;
file_lock::acquire_exclusive(&handle, locking, path)?;
let mut data = Vec::new();
handle.read_to_end(&mut data).map_err(Error::Io)?;
let sb_sig_off = signature::find_signature(&data)?;
let superblock = Superblock::parse(&data, sb_sig_off)?;
if superblock.version > 3 {
return Err(Error::EditUnsupported("unsupported superblock version"));
}
if superblock.offset_size != OFFSET_SIZE || superblock.length_size != LENGTH_SIZE {
return Err(Error::EditUnsupported(
"only 8-byte offsets and lengths are supported for in-place editing",
));
}
if superblock.base_address != 0 {
return Err(Error::EditUnsupported(
"files with a userblock (non-zero base address) are not editable in place yet",
));
}
let mut session = Self {
handle,
data,
sb_sig_off,
superblock,
pending_datasets: Vec::new(),
pending_writes: Vec::new(),
pending_groups: Vec::new(),
pending_group_attrs: Vec::new(),
pending_deletes: Vec::new(),
pending_copies: Vec::new(),
pending_cross_copies: Vec::new(),
free: FreeList::new(),
persist: None,
};
session.load_persisted_free_space();
Ok(session)
}
fn load_persisted_free_space(&mut self) {
if self.superblock.version < 2 {
return; }
let Some(ext_rel) = self.superblock.superblock_extension_address else {
return;
};
if ext_rel == UNDEF {
return;
}
let Ok(ext_addr) = usize::try_from(ext_rel) else {
return;
};
let Some(info) = self.extension_fsinfo(ext_addr) else {
return;
};
if !info.persist {
return;
}
let os = self.superblock.offset_size;
if let Ok(mut sections) =
free_space_manager::read_persisted_sections(&self.data, &info.manager_addrs, 0, os)
{
let file_len = self.data.len() as u64;
sections.sort_by_key(|s| s.addr);
let mut prev_end = 0u64;
for s in sections {
let Some(end) = s.addr.checked_add(s.size) else {
continue;
};
if s.size == 0 || end > file_len || s.addr < prev_end {
continue;
}
prev_end = end;
self.free.free(s.addr, s.size);
}
}
let mut old_blocks = Vec::new();
if let Ok(spans) = self.oh_chunk_spans(ext_addr) {
old_blocks.extend(spans);
}
for &m in &info.manager_addrs {
if m == UNDEF {
continue;
}
let Ok(m_us) = usize::try_from(m) else {
continue;
};
let Some(slice) = self.data.get(m_us..) else {
continue;
};
if let Ok(h) = FsmHeader::parse(slice, os) {
old_blocks.push((m, fshd_len(os)));
if h.fsse_addr != UNDEF
&& h.fsse_addr
.checked_add(h.fsse_used)
.is_some_and(|end| end <= self.data.len() as u64)
{
old_blocks.push((h.fsse_addr, h.fsse_used));
}
}
}
self.persist = Some(PersistState {
strategy: info.strategy,
threshold: info.threshold,
page_size: info.page_size,
old_blocks,
});
}
fn extension_fsinfo(&self, ext_addr: usize) -> Option<FileSpaceInfo> {
let os = self.superblock.offset_size;
let ls = self.superblock.length_size;
let base = self.superblock.base_address;
let oh = ObjectHeader::parse_with_base(&self.data, ext_addr, os, ls, base).ok()?;
let msg = oh
.messages
.iter()
.find(|m| m.msg_type == MessageType::FileSpaceInfo)?;
FileSpaceInfo::parse(&msg.data, os, ls).ok()
}
pub fn create_dataset(&mut self, path: &str) -> &mut DatasetBuilder {
let mut comps = split_path(path);
let leaf = comps.pop().unwrap_or_default();
self.pending_datasets
.push((comps, DatasetBuilder::new(&leaf)));
&mut self.pending_datasets.last_mut().unwrap().1
}
pub fn write_dataset(&mut self, path: &str) -> &mut DatasetBuilder {
let comps = split_path(path);
let leaf = comps.last().cloned().unwrap_or_default();
self.pending_writes
.push((comps, DatasetBuilder::new(&leaf)));
&mut self.pending_writes.last_mut().unwrap().1
}
pub fn create_group(&mut self, path: &str) {
self.pending_groups.push(split_path(path));
}
pub fn set_group_attr(&mut self, path: &str, name: &str, value: AttrValue) -> &mut Self {
self.pending_group_attrs.push((
split_path(path),
GroupAttrOp::Set {
name: name.to_string(),
value,
},
));
self
}
pub fn remove_group_attr(&mut self, path: &str, name: &str) -> &mut Self {
self.pending_group_attrs.push((
split_path(path),
GroupAttrOp::Remove {
name: name.to_string(),
},
));
self
}
pub fn delete(&mut self, path: &str) {
self.pending_deletes.push(split_path(path));
}
pub fn copy(&mut self, src: &str, dst: &str) {
self.pending_copies.push((split_path(src), split_path(dst)));
}
pub fn copy_from(
&mut self,
source: &crate::reader::File,
src: &str,
dst: &str,
) -> Result<(), Error> {
let src_data = source.in_memory_image().ok_or(Error::EditUnsupported(
"cross-file copy requires a buffered source file (File::open or File::from_bytes), not a streaming one",
))?;
let src_sb = source.superblock();
if src_sb.offset_size != OFFSET_SIZE || src_sb.length_size != LENGTH_SIZE {
return Err(Error::EditUnsupported(
"cross-file copy requires the source file to use 8-byte offsets and lengths",
));
}
if source.base_address() != 0 {
return Err(Error::EditUnsupported(
"cross-file copy requires the source file to have no userblock (base address 0)",
));
}
let src = split_path(src);
if src.is_empty() {
return Err(Error::EditUnsupported("cannot copy the root group"));
}
let dst = split_path(dst);
if dst.is_empty() {
return Err(Error::EditUnsupported("copy destination path is empty"));
}
let src_addr = crate::group_v2::resolve_path_any(src_data, src_sb, &src.join("/"))
.map_err(|_| Error::EditUnsupported("copy source does not exist in the source file"))?;
let src_addr = usize::try_from(src_addr)
.map_err(|_| Error::EditUnsupported("source address exceeds this platform"))?;
let tree = Self::read_copy_subtree(src_data, src_addr, 0, true)?;
self.pending_cross_copies.push((dst, tree));
Ok(())
}
pub fn commit(&mut self) -> Result<(), Error> {
if self.pending_datasets.is_empty()
&& self.pending_writes.is_empty()
&& self.pending_groups.is_empty()
&& self.pending_group_attrs.is_empty()
&& self.pending_deletes.is_empty()
&& self.pending_copies.is_empty()
&& self.pending_cross_copies.is_empty()
{
return Ok(());
}
let writes = std::mem::take(&mut self.pending_writes);
let mut inplace_writes: Vec<(usize, Vec<u8>)> = Vec::new();
let mut moving_writes: Vec<(PathKey, String, MovingWrite)> = Vec::new();
let mut write_targets: Vec<PathKey> = Vec::new();
let mut incoming_links: Option<Option<HashMap<u64, u32>>> = None;
for (full, db) in writes {
if full.is_empty() {
return Err(Error::EditUnsupported("cannot overwrite the root group"));
}
if write_targets.contains(&full) {
return Err(Error::EditUnsupported(
"the same dataset is overwritten twice in one commit; use separate commits",
));
}
let path_str = full.join("/");
let addr = crate::group_v2::resolve_path_any(&self.data, &self.superblock, &path_str)
.map_err(|_| {
Error::EditUnsupported("nothing to overwrite at the given path")
})?;
let addr = usize::try_from(addr)
.map_err(|_| Error::EditUnsupported("dataset address exceeds this platform"))?;
let fd = flatten_dataset(db)?;
match Self::prepare_write(&self.data, addr, &fd)? {
WritePlan::InPlace { data_addr, raw } => inplace_writes.push((data_addr, raw)),
WritePlan::Moving(mw) => {
let counts = incoming_links
.get_or_insert_with(|| self.count_incoming_hard_links())
.as_ref();
match counts.and_then(|c| c.get(&(addr as u64))) {
Some(&1) => {}
_ => {
return Err(Error::EditUnsupported(
"overwriting a dataset that resizes or relocates its header is \
only supported when it has a single hard link",
));
}
}
let leaf = full.last().unwrap().clone();
let parent = full[..full.len() - 1].to_vec();
moving_writes.push((parent, leaf, mw));
}
}
write_targets.push(full);
}
if moving_writes.is_empty()
&& self.pending_datasets.is_empty()
&& self.pending_groups.is_empty()
&& self.pending_group_attrs.is_empty()
&& self.pending_deletes.is_empty()
&& self.pending_copies.is_empty()
&& self.pending_cross_copies.is_empty()
{
for (data_addr, raw) in &inplace_writes {
self.write_at(*data_addr, raw)?;
}
self.handle.sync_all().map_err(Error::Io)?;
return Ok(());
}
let mut nodes: BTreeMap<PathKey, Node> = BTreeMap::new();
nodes.entry(PathKey::new()).or_default(); let mut add_targets: Vec<PathKey> = Vec::new();
let mut attr_targets: Vec<PathKey> = Vec::new();
for path in std::mem::take(&mut self.pending_groups) {
if path.is_empty() {
return Err(Error::EditUnsupported("cannot create the root group"));
}
ensure_ancestors(&mut nodes, &path);
nodes.entry(path.clone()).or_default().is_new = true;
add_targets.push(path);
}
for (parent, db) in std::mem::take(&mut self.pending_datasets) {
let mut full = parent.clone();
full.push(db.name.clone());
add_targets.push(full);
ensure_ancestors(&mut nodes, &parent);
nodes.entry(parent).or_default().datasets.push(db);
}
for (parent, leaf, mw) in moving_writes {
ensure_ancestors(&mut nodes, &parent);
nodes.entry(parent).or_default().writes.push((leaf, mw));
}
for (path, op) in std::mem::take(&mut self.pending_group_attrs) {
ensure_ancestors(&mut nodes, &path);
nodes.entry(path.clone()).or_default().attr_ops.push(op);
attr_targets.push(path);
}
for (src, dst) in std::mem::take(&mut self.pending_copies) {
if src.is_empty() {
return Err(Error::EditUnsupported("cannot copy the root group"));
}
if dst.is_empty() {
return Err(Error::EditUnsupported("copy destination path is empty"));
}
if is_prefix(&src, &dst) {
return Err(Error::EditUnsupported(
"cannot copy an object into itself or its own subtree",
));
}
let src_str = src.join("/");
let src_addr =
crate::group_v2::resolve_path_any(&self.data, &self.superblock, &src_str)
.map_err(|_| Error::EditUnsupported("copy source does not exist"))?;
let src_addr = usize::try_from(src_addr)
.map_err(|_| Error::EditUnsupported("source address exceeds this platform"))?;
let tree = Self::read_copy_subtree(&self.data, src_addr, 0, false)?;
add_targets.push(dst.clone());
let leaf = dst.last().unwrap().clone();
let parent = dst[..dst.len() - 1].to_vec();
ensure_ancestors(&mut nodes, &parent);
nodes.entry(parent).or_default().copies.push((leaf, tree));
}
for (dst, tree) in std::mem::take(&mut self.pending_cross_copies) {
if dst.is_empty() {
return Err(Error::EditUnsupported("copy destination path is empty"));
}
add_targets.push(dst.clone());
let leaf = dst.last().unwrap().clone();
let parent = dst[..dst.len() - 1].to_vec();
ensure_ancestors(&mut nodes, &parent);
nodes.entry(parent).or_default().copies.push((leaf, tree));
}
let deletes = std::mem::take(&mut self.pending_deletes);
let mut deleted_addrs: Vec<usize> = Vec::new();
for (i, d) in deletes.iter().enumerate() {
if d.is_empty() {
return Err(Error::EditUnsupported("cannot delete the root group"));
}
let path_str = d.join("/");
let del_addr =
crate::group_v2::resolve_path_any(&self.data, &self.superblock, &path_str)
.map_err(|_| Error::EditUnsupported("nothing to delete at the given path"))?;
if let Ok(a) = usize::try_from(del_addr) {
deleted_addrs.push(a);
}
for t in &add_targets {
if is_prefix(d, t) || is_prefix(t, d) {
return Err(Error::EditUnsupported(
"a deletion overlaps an addition in the same commit; use separate commits",
));
}
}
for t in &attr_targets {
if is_prefix(d, t) {
return Err(Error::EditUnsupported(
"a deletion overlaps a group-attribute edit in the same commit; use separate commits",
));
}
}
for t in &write_targets {
if is_prefix(d, t) {
return Err(Error::EditUnsupported(
"a deletion overlaps a value overwrite in the same commit; use separate commits",
));
}
}
for (j, d2) in deletes.iter().enumerate() {
if i != j && is_prefix(d, d2) {
return Err(Error::EditUnsupported(
"overlapping deletions in one commit; delete the common parent only",
));
}
}
let parent = d[..d.len() - 1].to_vec();
ensure_ancestors(&mut nodes, &parent);
nodes
.entry(parent)
.or_default()
.deletes
.push(d.last().unwrap().clone());
}
let keys: Vec<PathKey> = nodes.keys().cloned().collect();
let mut superseded_addrs: Vec<usize> = Vec::new();
for key in &keys {
let is_new = nodes[key].is_new;
if is_new {
nodes.get_mut(key).unwrap().base_region = fresh_group_region();
} else {
let path_str = key.join("/");
let addr =
crate::group_v2::resolve_path_any(&self.data, &self.superblock, &path_str)
.map_err(|_| {
Error::EditUnsupported(
"a target group does not exist; create it first in this session",
)
})?;
let addr = usize::try_from(addr)
.map_err(|_| Error::EditUnsupported("group address exceeds this platform"))?;
let info = self.inspect_group(addr)?;
superseded_addrs.push(addr);
let node = nodes.get_mut(key).unwrap();
node.base_region = info.region;
node.existing_links = info.link_names;
}
}
for key in &keys {
let node = nodes.get_mut(key).unwrap();
let ops = std::mem::take(&mut node.attr_ops);
if !ops.is_empty() {
let region = std::mem::take(&mut node.base_region);
node.base_region = apply_group_attr_ops(®ion, &ops)?;
}
}
let mut children: BTreeMap<PathKey, Vec<PathKey>> = BTreeMap::new();
for key in &keys {
if !key.is_empty() {
let parent = key[..key.len() - 1].to_vec();
children.entry(parent).or_default().push(key.clone());
}
}
for key in &keys {
let node = &nodes[key];
let mut adding: Vec<&str> = Vec::new();
for db in &node.datasets {
adding.push(&db.name);
}
for child in children.get(key).into_iter().flatten() {
if nodes[child].is_new {
adding.push(child.last().unwrap());
}
}
for (leaf, _) in &node.copies {
adding.push(leaf);
}
for (i, name) in adding.iter().enumerate() {
if node.existing_links.iter().any(|n| n == name) || adding[..i].contains(name) {
return Err(Error::EditUnsupported(
"a link with this name already exists in the target group",
));
}
}
}
let mut flat: BTreeMap<PathKey, Vec<FlatDataset>> = BTreeMap::new();
for key in &keys {
let dbs = std::mem::take(&mut nodes.get_mut(key).unwrap().datasets);
let mut v = Vec::with_capacity(dbs.len());
for db in dbs {
v.push(flatten_dataset(db)?);
}
flat.insert(key.clone(), v);
}
let mut to_free: Vec<(u64, u64)> = Vec::new();
deleted_addrs.sort_unstable();
deleted_addrs.dedup();
if let Some(incoming) = self.count_incoming_hard_links() {
for &a in &deleted_addrs {
self.collect_free_spans(a, 0, &incoming, &mut to_free);
}
}
for &a in &superseded_addrs {
if let Ok(spans) = self.oh_chunk_spans(a) {
to_free.extend(spans);
}
}
for key in &keys {
for (leaf, mw) in &nodes[key].writes {
if let MovingWrite::Contiguous {
old_extent: Some(extent),
..
} = mw
{
to_free.push(*extent);
}
let mut full = key.clone();
full.push(leaf.clone());
let path_str = full.join("/");
if let Ok(addr) =
crate::group_v2::resolve_path_any(&self.data, &self.superblock, &path_str)
{
if let Ok(a) = usize::try_from(addr) {
if let Ok(spans) = self.oh_chunk_spans(a) {
to_free.extend(spans);
}
}
}
}
}
retain_disjoint_in_bounds(&mut to_free, self.data.len() as u64);
let mut new_addr: BTreeMap<PathKey, u64> = BTreeMap::new();
let mut by_depth = keys.clone();
by_depth.sort_by_key(|k| std::cmp::Reverse(k.len())); for key in &by_depth {
let (mut region, deletes, copies, writes) = {
let node = nodes.get_mut(key).unwrap();
(
std::mem::take(&mut node.base_region),
std::mem::take(&mut node.deletes),
std::mem::take(&mut node.copies),
std::mem::take(&mut node.writes),
)
};
for name in &deletes {
region = remove_link_from_region(®ion, name)?;
}
for (leaf, tree) in copies {
let root = self.write_copy_subtree(&tree)?;
region.extend_from_slice(&encode_link_message(&leaf, root));
}
for fd in flat.remove(key).into_iter().flatten() {
let oh = if fd.chunk_options.is_chunked() || fd.maxshape.is_some() {
self.build_chunked_dataset(&fd)?
} else {
let data_addr = self.alloc_or_append(&fd.raw)?;
build_dataset_oh(
&fd.dt,
&fd.ds,
data_addr,
fd.raw.len() as u64,
&fd.attrs,
None,
)
};
let oh_addr = self.alloc_or_append(&oh)?;
region.extend_from_slice(&encode_link_message(&fd.name, oh_addr));
}
for (leaf, mw) in &writes {
let new_oh = self.write_moving(mw)?;
patch_link_target(&mut region, leaf, new_oh)?;
}
for child in children.get(key).into_iter().flatten() {
let child_name = child.last().unwrap();
let child_addr = new_addr[child];
if nodes[child].is_new {
region.extend_from_slice(&encode_link_message(child_name, child_addr));
} else {
patch_link_target(&mut region, child_name, child_addr)?;
}
}
let oh = build_v2_object_header(®ion);
let addr = self.alloc_or_append(&oh)?;
new_addr.insert(key.clone(), addr);
}
for (data_addr, raw) in &inplace_writes {
self.write_at(*data_addr, raw)?;
}
let new_root = new_addr[&PathKey::new()];
if self.persist.is_some() {
return self.commit_persisting(new_root, to_free);
}
for (a, l) in to_free.drain(..) {
self.free.free(a, l);
}
let cur_eof = self.data.len() as u64;
let trunc_to = self.free.take_trailing(cur_eof);
let new_eof = trunc_to.unwrap_or(cur_eof);
self.handle.sync_all().map_err(Error::Io)?;
if self.superblock.version >= 2 {
let mut new_sb = self.superblock.clone();
new_sb.root_group_address = new_root;
new_sb.eof_address = new_eof;
new_sb.consistency_flags = 0;
let sb_bytes = new_sb.serialize();
self.write_at(self.sb_sig_off, &sb_bytes)?;
self.handle.sync_all().map_err(Error::Io)?;
self.superblock = new_sb;
} else {
self.repoint_v0v1_root(new_root, new_eof)?;
self.handle.sync_all().map_err(Error::Io)?;
self.superblock.root_group_address = new_root;
self.superblock.eof_address = new_eof;
}
if let Some(cut) = trunc_to {
self.handle.set_len(cut).map_err(Error::Io)?;
#[expect(
clippy::cast_possible_truncation,
reason = "cut is a shrink target <= the current file length, which equals \
self.data.len() (a usize)"
)]
self.data.truncate(cut as usize);
self.handle.sync_all().map_err(Error::Io)?;
}
Ok(())
}
fn commit_persisting(&mut self, new_root: u64, to_free: Vec<(u64, u64)>) -> Result<(), Error> {
let os = self.superblock.offset_size;
let (strategy, threshold, page_size, old_blocks) = {
let ps = self
.persist
.as_ref()
.expect("commit_persisting is only called when persistence is armed");
(
ps.strategy,
ps.threshold,
ps.page_size,
ps.old_blocks.clone(),
)
};
let mut post = self.free.clone();
for &(a, l) in &to_free {
post.free(a, l);
}
for &(a, l) in &old_blocks {
post.free(a, l);
}
let sections: Vec<FreeSection> = post
.sections()
.into_iter()
.map(|(addr, size)| FreeSection { addr, size })
.collect();
let old_ext_rel = self
.superblock
.superblock_extension_address
.filter(|&a| a != UNDEF)
.ok_or(Error::EditUnsupported(
"a persisting file has no superblock extension to update",
))?;
let old_ext_addr = usize::try_from(old_ext_rel)
.map_err(|_| Error::EditUnsupported("extension address exceeds this platform"))?;
let placeholder =
FileSpaceInfo::persistent_single_manager(strategy, threshold, page_size, 0, 0);
let ext_len =
build_v2_object_header(&self.rewrite_extension_region(old_ext_addr, &placeholder)?)
.len() as u64;
let ext_addr = self.data.len() as u64;
let fshd_addr = ext_addr + ext_len;
let (ext_oh, fsm_blocks, final_eof) = if sections.is_empty() {
let info = FileSpaceInfo::persistent_empty(strategy, threshold, page_size);
let ext_oh =
build_v2_object_header(&self.rewrite_extension_region(old_ext_addr, &info)?);
let final_eof = ext_addr + ext_oh.len() as u64;
(ext_oh, None, final_eof)
} else {
let fsse_addr = fshd_addr + fshd_len(os);
let eoa_pre_fsm = fshd_addr;
let info = FileSpaceInfo::persistent_single_manager(
strategy,
threshold,
page_size,
fshd_addr,
eoa_pre_fsm,
);
let ext_oh =
build_v2_object_header(&self.rewrite_extension_region(old_ext_addr, &info)?);
debug_assert_eq!(
ext_oh.len() as u64,
ext_len,
"extension length must be stable across the placeholder and real messages"
);
let (fshd, fsse) = serialize_file_fsm(§ions, fshd_addr, fsse_addr, os);
let final_eof = fsse_addr + fsse.len() as u64;
(ext_oh, Some((fshd, fsse)), final_eof)
};
let written_ext = self.append(&ext_oh)?;
debug_assert_eq!(written_ext, ext_addr);
let mut new_old_blocks = vec![(ext_addr, ext_oh.len() as u64)];
if let Some((fshd, fsse)) = fsm_blocks {
let wf = self.append(&fshd)?;
debug_assert_eq!(wf, fshd_addr);
new_old_blocks.push((fshd_addr, fshd.len() as u64));
let ws = self.append(&fsse)?;
new_old_blocks.push((ws, fsse.len() as u64));
}
self.handle.sync_all().map_err(Error::Io)?;
let mut new_sb = self.superblock.clone();
new_sb.root_group_address = new_root;
new_sb.eof_address = final_eof;
new_sb.superblock_extension_address = Some(ext_addr);
new_sb.consistency_flags = 0;
let sb_bytes = new_sb.serialize();
self.write_at(self.sb_sig_off, &sb_bytes)?;
self.handle.sync_all().map_err(Error::Io)?;
self.superblock = new_sb;
self.free = post;
self.persist = Some(PersistState {
strategy,
threshold,
page_size,
old_blocks: new_old_blocks,
});
Ok(())
}
fn rewrite_extension_region(
&self,
ext_addr: usize,
info: &FileSpaceInfo,
) -> Result<Vec<u8>, Error> {
let region = Self::gather_oh_messages(&self.data, ext_addr)?;
let new_body = info.serialize();
let new_len = u16::try_from(new_body.len())
.map_err(|_| Error::EditUnsupported("File Space Info message too large"))?;
let mut out = Vec::with_capacity(region.len());
let mut p = 0;
let mut replaced = false;
while let Some((msg_type, _body, body_end)) = next_message(®ion, p)? {
if msg_type == MessageType::FileSpaceInfo {
out.push(region[p]); out.extend_from_slice(&new_len.to_le_bytes());
out.push(region[p + 3]); out.extend_from_slice(&new_body);
replaced = true;
} else {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if !replaced {
return Err(Error::EditUnsupported(
"a persisting file's superblock extension has no File Space Info message",
));
}
Ok(out)
}
fn repoint_v0v1_root(&mut self, new_root: u64, new_eof: u64) -> Result<(), Error> {
let os = self.superblock.offset_size as usize;
let var_start = if self.superblock.version == 0 { 24 } else { 28 };
let base = self.sb_sig_off + var_start;
let eof_off = base + 2 * os;
let ste = base + 4 * os;
let oh_addr_off = ste + os;
let cache_off = ste + 2 * os;
self.write_at(eof_off, &new_eof.to_le_bytes()[..os])?;
self.write_at(cache_off, &[0u8; 4])?; self.write_at(cache_off + 8, &[0u8; 16])?; self.write_at(oh_addr_off, &new_root.to_le_bytes()[..os])?;
Ok(())
}
fn oh_region(d: &[u8], addr: usize) -> Result<(usize, usize), Error> {
if d.len() < addr + 6 || &d[addr..addr + 4] != b"OHDR" || d[addr + 4] != 2 {
return Err(Error::EditUnsupported(
"an object does not use a version 2 object header",
));
}
let flags = d[addr + 5];
if flags & 0x04 != 0 {
return Err(Error::EditUnsupported(
"an object tracks message creation order (not supported in place yet)",
));
}
let mut pos = addr + 6;
if flags & 0x20 != 0 {
pos += 16; }
if flags & 0x10 != 0 {
pos += 4; }
let size_width = match flags & 0x03 {
0 => 1usize,
1 => 2,
2 => 4,
_ => 8,
};
if d.len() < pos + size_width {
return Err(Error::EditUnsupported("truncated object header"));
}
let chunk0_size = read_le(&d[pos..pos + size_width]);
pos += size_width;
let region_start = pos;
let region_end = region_start
.checked_add(chunk0_size)
.filter(|&e| e + 4 <= d.len())
.ok_or(Error::EditUnsupported("truncated object header"))?;
Ok((region_start, region_end))
}
fn gather_oh_messages(d: &[u8], addr: usize) -> Result<Vec<u8>, Error> {
let (rs, re) = Self::oh_region(d, addr)?;
let mut out = Vec::new();
let mut chunks: Vec<(usize, usize)> = vec![(rs, re)];
let mut i = 0;
while i < chunks.len() {
if chunks.len() > MAX_OH_CHUNKS {
return Err(Error::EditUnsupported(
"object header has too many continuation chunks",
));
}
let (cs, ce) = chunks[i];
i += 1;
let region = &d[..ce];
let mut p = cs;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::ObjectHeaderContinuation {
if body_end - body < (OFFSET_SIZE + LENGTH_SIZE) as usize {
return Err(Error::EditUnsupported("malformed continuation message"));
}
let off = u64::from_le_bytes(d[body..body + 8].try_into().unwrap());
let len = u64::from_le_bytes(d[body + 8..body + 16].try_into().unwrap());
let off = usize::try_from(off).map_err(|_| {
Error::EditUnsupported("continuation address exceeds this platform")
})?;
let len = usize::try_from(len).map_err(|_| {
Error::EditUnsupported("continuation length exceeds this platform")
})?;
let blk_end = off
.checked_add(len)
.filter(|&e| e <= d.len() && len >= 8)
.ok_or(Error::EditUnsupported("continuation block out of bounds"))?;
if &d[off..off + 4] != b"OCHK" {
return Err(Error::EditUnsupported(
"invalid continuation block signature",
));
}
chunks.push((off + 4, blk_end - 4));
} else {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
}
Ok(out)
}
fn reconstruct_v1_group(&self, addr: usize) -> Result<GroupInfo, Error> {
let os = self.superblock.offset_size;
let ls = self.superblock.length_size;
let base = self.superblock.base_address;
let oh = ObjectHeader::parse_with_base(&self.data, addr, os, ls, base)?;
if oh
.messages
.iter()
.any(|m| m.msg_type == MessageType::DataLayout)
{
return Err(Error::EditUnsupported(
"a target path names a dataset, not a group",
));
}
let entries = resolve_group_entries(&self.data, &oh, os, ls, base)?;
let mut region = fresh_group_region();
let mut link_names = Vec::with_capacity(entries.len());
for e in &entries {
region.extend_from_slice(&encode_link_message(&e.name, e.object_header_address));
link_names.push(e.name.clone());
}
for m in &oh.messages {
if m.msg_type == MessageType::Attribute {
if m.flags != 0 {
return Err(Error::EditUnsupported(
"a v0/v1 group has a shared attribute message (not convertible in place yet)",
));
}
if m.data.len() > u16::MAX as usize {
return Err(Error::EditUnsupported(
"a v0/v1 group attribute is too large to convert in place",
));
}
#[expect(
clippy::cast_possible_truncation,
reason = "message type ids are a small enum that fits the 1-byte v2 type field"
)]
region.push(MessageType::Attribute.to_u16() as u8);
#[expect(
clippy::cast_possible_truncation,
reason = "attribute body length fits the 2-byte message-size field (oversized \
bodies are rejected above)"
)]
region.extend_from_slice(&(m.data.len() as u16).to_le_bytes());
region.push(0); region.extend_from_slice(&m.data);
}
}
Ok(GroupInfo { region, link_names })
}
fn inspect_group(&self, addr: usize) -> Result<GroupInfo, Error> {
if self.data.len() < addr + 4 || self.data[addr..addr + 4] != *b"OHDR" {
return self.reconstruct_v1_group(addr);
}
let mut region = Self::gather_oh_messages(&self.data, addr)?;
let mut p = 0;
let mut has_link_info = false;
let mut link_names = Vec::new();
while let Some((msg_type, body, body_end)) = next_message(®ion, p)? {
match msg_type {
MessageType::LinkInfo => {
has_link_info = true;
let mut q = body + 2;
if body_end - body >= 2 && region[body + 1] & 0x01 != 0 {
q += 8;
}
if q + 8 <= body_end {
let heap_addr = u64::from_le_bytes(region[q..q + 8].try_into().unwrap());
if heap_addr != u64::MAX {
return Err(Error::EditUnsupported(
"a target group uses dense (fractal-heap) link storage (not supported in place yet)",
));
}
}
}
MessageType::Link => {
if let Ok(link) = LinkMessage::parse(®ion[body..body_end], OFFSET_SIZE) {
link_names.push(link.name);
}
}
MessageType::DataLayout => {
return Err(Error::EditUnsupported(
"a target path names a dataset, not a group",
));
}
_ => {}
}
p = body_end;
}
if !has_link_info {
return Err(Error::EditUnsupported(
"a target group's object header has no link-info message",
));
}
ensure_group_info(&mut region)?;
Ok(GroupInfo { region, link_names })
}
fn prepare_write(d: &[u8], addr: usize, fd: &FlatDataset) -> Result<WritePlan, Error> {
if fd.chunk_options.is_chunked() || fd.maxshape.is_some() {
return Err(Error::EditUnsupported(
"write_dataset overwrites values only; it cannot make a dataset \
chunked, filtered, or extensible",
));
}
if !fd.attrs.is_empty() {
return Err(Error::EditUnsupported(
"write_dataset overwrites values only; it cannot set attributes \
(set them with a separate edit)",
));
}
let region = Self::gather_oh_messages(d, addr)?;
let mut datatype: Option<(usize, usize)> = None;
let mut dataspace: Option<(usize, usize)> = None;
let mut layout: Option<(usize, usize)> = None;
let mut has_filter = false;
let mut has_link = false;
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(®ion, p)? {
match msg_type {
MessageType::Datatype => datatype = Some((body, body_end)),
MessageType::Dataspace => dataspace = Some((body, body_end)),
MessageType::DataLayout => layout = Some((body, body_end)),
MessageType::FilterPipeline => has_filter = true,
MessageType::Link | MessageType::LinkInfo | MessageType::SymbolTable => {
has_link = true;
}
_ => {}
}
p = body_end;
}
if has_link {
return Err(Error::EditUnsupported(
"write_dataset target is a group, not a dataset",
));
}
if has_filter {
return Err(Error::EditUnsupported(
"filtered datasets cannot be overwritten in place yet",
));
}
let (dt_b, dt_e) =
datatype.ok_or(Error::EditUnsupported("dataset header has no datatype"))?;
let (ds_b, ds_e) =
dataspace.ok_or(Error::EditUnsupported("dataset header has no dataspace"))?;
let (lb, le) = layout.ok_or(Error::EditUnsupported("dataset header has no data layout"))?;
let (disk_dt, _) = crate::datatype::Datatype::parse(®ion[dt_b..dt_e])
.map_err(|_| Error::EditUnsupported("dataset header datatype could not be parsed"))?;
if disk_dt != fd.dt {
return Err(Error::EditUnsupported(
"write_dataset datatype does not match the on-disk dataset (overwrite, not retype)",
));
}
let disk_ds = Dataspace::parse(®ion[ds_b..ds_e], LENGTH_SIZE)
.map_err(|_| Error::EditUnsupported("dataset header dataspace could not be parsed"))?;
if disk_ds.space_type != fd.ds.space_type
|| disk_ds.rank != fd.ds.rank
|| disk_ds.dimensions != fd.ds.dimensions
{
return Err(Error::EditUnsupported(
"write_dataset shape does not match the on-disk dataset (overwrite, not reshape)",
));
}
if le - lb < 2 {
return Err(Error::EditUnsupported("malformed data-layout message"));
}
let version = region[lb];
if version != 3 && version != 4 {
return Err(Error::EditUnsupported(
"an unsupported data-layout version cannot be overwritten in place yet",
));
}
match region[lb + 1] {
0 => Ok(WritePlan::Moving(MovingWrite::Compact {
region,
raw: fd.raw.clone(),
})),
1 => {
if le - lb < 18 {
return Err(Error::EditUnsupported("malformed contiguous data layout"));
}
let addr_off = lb + 2;
let data_addr =
u64::from_le_bytes(region[addr_off..addr_off + 8].try_into().unwrap());
let data_size = u64::from_le_bytes(region[lb + 10..lb + 18].try_into().unwrap());
if data_addr != UNDEF && data_size == fd.raw.len() as u64 {
if let Ok(start) = usize::try_from(data_addr) {
if start
.checked_add(fd.raw.len())
.is_some_and(|e| e <= d.len())
{
return Ok(WritePlan::InPlace {
data_addr: start,
raw: fd.raw.clone(),
});
}
}
}
let old_extent = if data_addr != UNDEF && data_size > 0 {
Some((data_addr, data_size))
} else {
None
};
Ok(WritePlan::Moving(MovingWrite::Contiguous {
region,
addr_off,
raw: fd.raw.clone(),
old_extent,
}))
}
_ => Err(Error::EditUnsupported(
"chunked datasets cannot be overwritten in place yet",
)),
}
}
fn read_object(d: &[u8], addr: usize) -> Result<ObjModel, Error> {
let region = Self::gather_oh_messages(d, addr)?;
let mut dense = false;
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(®ion, p)? {
if msg_type == MessageType::AttributeInfo {
let ai = crate::attribute_info::AttributeInfoMessage::parse(
®ion[body..body_end],
OFFSET_SIZE,
)
.map_err(|_| {
Error::EditUnsupported(
"a source attribute-info message could not be parsed for copying",
)
})?;
if ai.fractal_heap_address.is_some() {
dense = true;
}
}
p = body_end;
}
let dense_attrs = if dense {
let header =
ObjectHeader::parse_with_base(d, addr, OFFSET_SIZE, LENGTH_SIZE, 0).map_err(|_| {
Error::EditUnsupported(
"a source object header with dense attributes could not be parsed for copying",
)
})?;
let attrs = crate::attribute::extract_attributes_full(
d,
&header,
OFFSET_SIZE,
LENGTH_SIZE,
)
.map_err(|_| {
Error::EditUnsupported(
"a source object's dense (fractal-heap) attributes could not be read for copying",
)
})?;
if !crate::file_writer::dense_attrs_fit(&attrs) {
return Err(Error::EditUnsupported(
"an object's dense (fractal-heap) attribute set is too large to reproduce (would need fractal-heap indirect blocks)",
));
}
attrs
} else {
Vec::new()
};
let mut layout: Option<(usize, usize)> = None; let mut has_link_info = false;
let mut children: Vec<(String, u64)> = Vec::new();
let mut kept: Vec<u8> = Vec::new();
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(®ion, p)? {
let mut keep = true;
match msg_type {
MessageType::AttributeInfo => {
if dense {
keep = false;
}
}
MessageType::Attribute => {
if dense {
keep = false;
}
}
MessageType::LinkInfo => {
has_link_info = true;
let mut q = body + 2;
if body_end - body >= 2 && region[body + 1] & 0x01 != 0 {
q += 8;
}
if q + 8 <= body_end {
let heap_addr = u64::from_le_bytes(region[q..q + 8].try_into().unwrap());
if heap_addr != u64::MAX {
return Err(Error::EditUnsupported(
"a group uses dense (fractal-heap) link storage (not supported in place yet)",
));
}
}
}
MessageType::Link => {
keep = false;
match LinkMessage::parse(®ion[body..body_end], OFFSET_SIZE) {
Ok(LinkMessage {
name,
link_target:
LinkTarget::Hard {
object_header_address,
},
..
}) => children.push((name, object_header_address)),
_ => {
return Err(Error::EditUnsupported(
"a group contains a soft/external link (not copyable in place yet)",
));
}
}
}
MessageType::DataLayout => {
layout = Some((kept.len() + (body - p), body_end - body));
}
_ => {}
}
if keep {
kept.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if let Some((lbody, lsize)) = layout {
let version = kept[lbody];
if !(version == 3 || version == 4) || lsize < 2 {
return Err(Error::EditUnsupported(
"an unsupported data-layout version cannot be copied in place yet",
));
}
let class = kept[lbody + 1];
match class {
0 => Ok(ObjModel::DatasetVerbatim {
region: kept,
dense_attrs,
}),
1 => {
if lbody + 18 > kept.len() {
return Err(Error::EditUnsupported("malformed contiguous data layout"));
}
let data_addr =
u64::from_le_bytes(kept[lbody + 2..lbody + 10].try_into().unwrap());
let data_size =
u64::from_le_bytes(kept[lbody + 10..lbody + 18].try_into().unwrap());
Ok(ObjModel::DatasetContiguous {
region: kept,
addr_off: lbody + 2,
data_addr,
data_size,
dense_attrs,
})
}
_ => Err(Error::EditUnsupported(
"chunked datasets cannot be copied in place yet",
)),
}
} else if has_link_info {
ensure_group_info(&mut kept)?;
Ok(ObjModel::Group {
non_link_region: kept,
children,
dense_attrs,
})
} else {
Err(Error::EditUnsupported(
"an object is neither a contiguous/compact dataset nor a group",
))
}
}
fn read_copy_subtree(
d: &[u8],
addr: usize,
depth: u32,
cross_file: bool,
) -> Result<CopyTree, Error> {
if depth >= MAX_COPY_DEPTH {
return Err(Error::EditUnsupported(
"copy source nests too deeply (possible hard-link cycle)",
));
}
match Self::read_object(d, addr)? {
ObjModel::DatasetVerbatim {
region,
dense_attrs,
} => {
if cross_file {
reject_foreign_addresses(®ion)?;
reject_foreign_dense_attrs(&dense_attrs)?;
}
Ok(CopyTree::DatasetVerbatim {
region,
dense_attrs,
})
}
ObjModel::DatasetContiguous {
region,
addr_off,
data_addr,
data_size,
dense_attrs,
} => {
if cross_file {
reject_foreign_addresses(®ion)?;
reject_foreign_dense_attrs(&dense_attrs)?;
}
let start = usize::try_from(data_addr)
.map_err(|_| Error::EditUnsupported("data address exceeds this platform"))?;
let len = usize::try_from(data_size)
.map_err(|_| Error::EditUnsupported("data size exceeds this platform"))?;
let end = start
.checked_add(len)
.filter(|&e| e <= d.len())
.ok_or(Error::EditUnsupported("dataset data is out of bounds"))?;
Ok(CopyTree::DatasetContiguous {
region,
addr_off,
data: d[start..end].to_vec(),
dense_attrs,
})
}
ObjModel::Group {
non_link_region,
children,
dense_attrs,
} => {
if cross_file {
reject_foreign_addresses(&non_link_region)?;
reject_foreign_dense_attrs(&dense_attrs)?;
}
let mut kids = Vec::with_capacity(children.len());
for (name, child) in children {
let child = usize::try_from(child).map_err(|_| {
Error::EditUnsupported("child address exceeds this platform")
})?;
kids.push((
name,
Self::read_copy_subtree(d, child, depth + 1, cross_file)?,
));
}
Ok(CopyTree::Group {
non_link_region,
children: kids,
dense_attrs,
})
}
}
}
fn write_copy_subtree(&mut self, node: &CopyTree) -> Result<u64, Error> {
match node {
CopyTree::DatasetVerbatim {
region,
dense_attrs,
} => {
let mut region = region.clone();
self.append_dense_attrs(&mut region, dense_attrs)?;
let oh = build_v2_object_header(®ion);
self.alloc_or_append(&oh)
}
CopyTree::DatasetContiguous {
region,
addr_off,
data,
dense_attrs,
} => {
let new_data_addr = self.alloc_or_append(data)?;
let mut region = region.clone();
region[*addr_off..*addr_off + 8].copy_from_slice(&new_data_addr.to_le_bytes());
self.append_dense_attrs(&mut region, dense_attrs)?;
let oh = build_v2_object_header(®ion);
self.alloc_or_append(&oh)
}
CopyTree::Group {
non_link_region,
children,
dense_attrs,
} => {
let mut region = non_link_region.clone();
for (name, child) in children {
let new_child = self.write_copy_subtree(child)?;
region.extend_from_slice(&encode_link_message(name, new_child));
}
self.append_dense_attrs(&mut region, dense_attrs)?;
let oh = build_v2_object_header(®ion);
self.alloc_or_append(&oh)
}
}
}
fn append_dense_attrs(
&mut self,
region: &mut Vec<u8>,
attrs: &[crate::attribute::AttributeMessage],
) -> Result<(), Error> {
if attrs.is_empty() {
return Ok(());
}
let base = self.data.len() as u64;
let blob = crate::file_writer::build_dense_attrs(attrs, base);
let written = self.append(&blob.blob)?;
debug_assert_eq!(
written, base,
"dense attribute blob must land at the base address it was built for",
);
region.extend_from_slice(®ion_message(
MessageType::AttributeInfo,
&blob.attr_info_message,
));
Ok(())
}
fn write_moving(&mut self, mw: &MovingWrite) -> Result<u64, Error> {
match mw {
MovingWrite::Contiguous {
region,
addr_off,
raw,
..
} => {
let new_data_addr = self.alloc_or_append(raw)?;
let mut region = region.clone();
region[*addr_off..*addr_off + 8].copy_from_slice(&new_data_addr.to_le_bytes());
let size_off = *addr_off + 8;
region[size_off..size_off + 8].copy_from_slice(&(raw.len() as u64).to_le_bytes());
let oh = build_v2_object_header(®ion);
self.alloc_or_append(&oh)
}
MovingWrite::Compact { region, raw } => {
let region = rebuild_compact_layout_region(region, raw)?;
let oh = build_v2_object_header(®ion);
self.alloc_or_append(&oh)
}
}
}
fn append(&mut self, bytes: &[u8]) -> Result<u64, Error> {
let addr = self.data.len() as u64;
self.handle.seek(SeekFrom::Start(addr)).map_err(Error::Io)?;
self.handle.write_all(bytes).map_err(Error::Io)?;
self.data.extend_from_slice(bytes);
Ok(addr)
}
fn write_at(&mut self, offset: usize, bytes: &[u8]) -> Result<(), Error> {
self.handle
.seek(SeekFrom::Start(offset as u64))
.map_err(Error::Io)?;
self.handle.write_all(bytes).map_err(Error::Io)?;
self.data[offset..offset + bytes.len()].copy_from_slice(bytes);
Ok(())
}
fn alloc_or_append(&mut self, bytes: &[u8]) -> Result<u64, Error> {
if let Some(addr) = self.free.alloc(bytes.len() as u64) {
self.write_at(
usize::try_from(addr).map_err(|_| {
Error::EditUnsupported("free-region address exceeds this platform")
})?,
bytes,
)?;
Ok(addr)
} else {
self.append(bytes)
}
}
fn build_chunked_dataset(&mut self, fd: &FlatDataset) -> Result<Vec<u8>, Error> {
let base = self.data.len() as u64;
let chunk_dims = fd.chunk_options.resolve_chunk_dims(&fd.ds.dimensions);
let ctx = ChunkContext::from_datatype(&chunk_dims, &fd.dt);
let result = build_chunked_data_at_ext(
&fd.raw,
&fd.ds.dimensions,
ctx,
&fd.chunk_options,
base,
fd.maxshape.as_deref(),
)?;
let written = self.append(&result.data_bytes)?;
debug_assert_eq!(
written, base,
"chunk blob must land at the base address it was built for",
);
Ok(build_chunked_dataset_oh(
&fd.dt,
&fd.ds,
&result.layout_message,
result.pipeline_message.as_deref(),
&fd.attrs,
None,
))
}
fn oh_chunk_spans(&self, addr: usize) -> Result<Vec<(u64, u64)>, Error> {
let (rs, re) = Self::oh_region(&self.data, addr)?;
let d = &self.data;
let mut spans: Vec<(u64, u64)> = vec![(addr as u64, (re + 4 - addr) as u64)];
let mut chunks: Vec<(usize, usize)> = vec![(rs, re)];
let mut i = 0;
while i < chunks.len() {
if chunks.len() > MAX_OH_CHUNKS {
return Err(Error::EditUnsupported(
"object header has too many continuation chunks",
));
}
let (cs, ce) = chunks[i];
i += 1;
let region = &d[..ce];
let mut p = cs;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::ObjectHeaderContinuation {
if body_end - body < (OFFSET_SIZE + LENGTH_SIZE) as usize {
return Err(Error::EditUnsupported("malformed continuation message"));
}
let off = u64::from_le_bytes(d[body..body + 8].try_into().unwrap());
let len = u64::from_le_bytes(d[body + 8..body + 16].try_into().unwrap());
let off_us = usize::try_from(off).map_err(|_| {
Error::EditUnsupported("continuation address exceeds this platform")
})?;
let len_us = usize::try_from(len).map_err(|_| {
Error::EditUnsupported("continuation length exceeds this platform")
})?;
let blk_end = off_us
.checked_add(len_us)
.filter(|&e| e <= d.len() && len_us >= 8)
.ok_or(Error::EditUnsupported("continuation block out of bounds"))?;
if d[off_us..off_us + 4] != *b"OCHK" {
return Err(Error::EditUnsupported(
"invalid continuation block signature",
));
}
spans.push((off, len));
chunks.push((off_us + 4, blk_end - 4));
}
p = body_end;
}
}
Ok(spans)
}
fn count_incoming_hard_links(&self) -> Option<HashMap<u64, u32>> {
let os = self.superblock.offset_size;
let ls = self.superblock.length_size;
let base = self.superblock.base_address;
let mut counts: HashMap<u64, u32> = HashMap::new();
let mut visited: HashSet<u64> = HashSet::new();
let mut stack: Vec<u64> = vec![self.superblock.root_group_address];
let mut budget = MAX_LINK_GRAPH_NODES;
while let Some(addr) = stack.pop() {
if !visited.insert(addr) {
continue; }
if budget == 0 {
return None; }
budget -= 1;
let off = usize::try_from(addr).ok()?;
let header = ObjectHeader::parse_with_base(&self.data, off, os, ls, base).ok()?;
let is_group = header.messages.iter().any(|m| {
matches!(
m.msg_type,
MessageType::SymbolTable | MessageType::Link | MessageType::LinkInfo
)
});
if !is_group {
continue;
}
let entries = resolve_group_entries(&self.data, &header, os, ls, base).ok()?;
for e in entries {
let child = e.object_header_address.checked_add(base)?;
*counts.entry(child).or_insert(0) += 1;
stack.push(child);
}
}
Some(counts)
}
fn collect_free_spans(
&self,
addr: usize,
depth: u32,
incoming: &HashMap<u64, u32>,
out: &mut Vec<(u64, u64)>,
) {
if depth >= MAX_COPY_DEPTH {
return;
}
if incoming.get(&(addr as u64)) != Some(&1) {
return;
}
let spans = match self.oh_chunk_spans(addr) {
Ok(s) => s,
Err(_) => return,
};
match Self::read_object(&self.data, addr) {
Ok(ObjModel::DatasetVerbatim { .. }) => out.extend(spans),
Ok(ObjModel::DatasetContiguous {
data_addr,
data_size,
..
}) => {
out.extend(spans);
if data_addr != u64::MAX && data_size > 0 {
if let (Ok(start), Ok(len)) =
(usize::try_from(data_addr), usize::try_from(data_size))
{
if start.checked_add(len).is_some_and(|e| e <= self.data.len()) {
out.push((data_addr, data_size));
}
}
}
}
Ok(ObjModel::Group { children, .. }) => {
out.extend(spans);
for (_, child) in children {
if let Ok(c) = usize::try_from(child) {
self.collect_free_spans(c, depth + 1, incoming, out);
}
}
}
Err(_) => {
if let Some(storage) = self.chunked_storage_spans(addr) {
out.extend(spans);
out.extend(storage);
}
}
}
}
fn chunked_storage_spans(&self, addr: usize) -> Option<Vec<(u64, u64)>> {
let region = Self::gather_oh_messages(&self.data, addr).ok()?;
let mut layout_msg: Option<(usize, usize)> = None;
let mut dataspace_msg: Option<(usize, usize)> = None;
let mut p = 0;
loop {
match next_message(®ion, p) {
Ok(Some((msg_type, body, body_end))) => {
match msg_type {
MessageType::DataLayout => layout_msg = Some((body, body_end)),
MessageType::Dataspace => dataspace_msg = Some((body, body_end)),
_ => {}
}
p = body_end;
}
Ok(None) => break,
Err(_) => return None,
}
}
let (lb, le) = layout_msg?;
let (db, de) = dataspace_msg?;
let layout = DataLayout::parse(®ion[lb..le], OFFSET_SIZE, LENGTH_SIZE).ok()?;
if !matches!(layout, DataLayout::Chunked { .. }) {
return None;
}
let dataspace = Dataspace::parse(®ion[db..de], LENGTH_SIZE).ok()?;
let mut spans = crate::chunked_read::collect_chunked_storage_spans(
&self.data,
&layout,
&dataspace,
OFFSET_SIZE,
LENGTH_SIZE,
)
.ok()?;
if !spans_disjoint_in_bounds(&mut spans, self.data.len() as u64) {
return None;
}
Some(spans)
}
}
#[derive(Default)]
struct Node {
is_new: bool,
datasets: Vec<DatasetBuilder>,
attr_ops: Vec<GroupAttrOp>,
deletes: Vec<String>,
copies: Vec<(String, CopyTree)>,
writes: Vec<(String, MovingWrite)>,
base_region: Vec<u8>,
existing_links: Vec<String>,
}
enum GroupAttrOp {
Set { name: String, value: AttrValue },
Remove { name: String },
}
enum ObjModel {
DatasetVerbatim {
region: Vec<u8>,
dense_attrs: Vec<crate::attribute::AttributeMessage>,
},
DatasetContiguous {
region: Vec<u8>,
addr_off: usize,
data_addr: u64,
data_size: u64,
dense_attrs: Vec<crate::attribute::AttributeMessage>,
},
Group {
non_link_region: Vec<u8>,
children: Vec<(String, u64)>,
dense_attrs: Vec<crate::attribute::AttributeMessage>,
},
}
enum CopyTree {
DatasetVerbatim {
region: Vec<u8>,
dense_attrs: Vec<crate::attribute::AttributeMessage>,
},
DatasetContiguous {
region: Vec<u8>,
addr_off: usize,
data: Vec<u8>,
dense_attrs: Vec<crate::attribute::AttributeMessage>,
},
Group {
non_link_region: Vec<u8>,
children: Vec<(String, CopyTree)>,
dense_attrs: Vec<crate::attribute::AttributeMessage>,
},
}
struct GroupInfo {
region: Vec<u8>,
link_names: Vec<String>,
}
enum WritePlan {
InPlace { data_addr: usize, raw: Vec<u8> },
Moving(MovingWrite),
}
enum MovingWrite {
Contiguous {
region: Vec<u8>,
addr_off: usize,
raw: Vec<u8>,
old_extent: Option<(u64, u64)>,
},
Compact { region: Vec<u8>, raw: Vec<u8> },
}
struct FlatDataset {
name: String,
dt: crate::datatype::Datatype,
ds: Dataspace,
raw: Vec<u8>,
attrs: Vec<crate::attribute::AttributeMessage>,
chunk_options: ChunkOptions,
maxshape: Option<Vec<u64>>,
}
fn split_path(path: &str) -> PathKey {
path.split('/')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
fn ensure_ancestors(nodes: &mut BTreeMap<PathKey, Node>, path: &[String]) {
for len in 0..=path.len() {
nodes.entry(path[..len].to_vec()).or_default();
}
}
fn spans_disjoint_in_bounds(spans: &mut [(u64, u64)], eof: u64) -> bool {
for &(addr, len) in spans.iter() {
match addr.checked_add(len) {
Some(end) if len > 0 && end <= eof => {}
_ => return false,
}
}
spans.sort_unstable_by_key(|&(addr, _)| addr);
spans.windows(2).all(|w| w[0].0 + w[0].1 <= w[1].0)
}
fn retain_disjoint_in_bounds(spans: &mut Vec<(u64, u64)>, eof: u64) {
spans.retain(|&(addr, len)| len > 0 && addr.checked_add(len).is_some_and(|e| e <= eof));
spans.sort_unstable_by_key(|&(addr, _)| addr);
let mut kept_end = 0u64;
spans.retain(|&(addr, len)| {
if addr >= kept_end {
kept_end = addr + len;
true
} else {
false }
});
}
fn flatten_dataset(db: DatasetBuilder) -> Result<FlatDataset, Error> {
if db.name.is_empty() {
return Err(Error::EditUnsupported("dataset path has an empty name"));
}
if db.reference_targets.is_some() {
return Err(Error::EditUnsupported(
"object-reference datasets cannot be added in place yet",
));
}
#[cfg(feature = "provenance")]
if db.provenance.is_some() {
return Err(Error::EditUnsupported(
"provenance datasets cannot be added in place yet",
));
}
let dt = db
.datatype
.ok_or(Error::EditUnsupported("dataset has no datatype/data"))?;
let shape = db
.shape
.ok_or(Error::EditUnsupported("dataset has no shape"))?;
if shape.contains(&0) {
return Err(Error::EditUnsupported(
"empty (zero-element) datasets cannot be added in place yet",
));
}
let raw = db
.data
.ok_or(Error::EditUnsupported("dataset has no data"))?;
let elem = dt.type_size() as u64;
if elem > 0 {
let expected = shape
.iter()
.try_fold(1u64, |acc, &d| acc.checked_mul(d))
.and_then(|n| n.checked_mul(elem));
match expected {
Some(expected) if raw.len() as u64 == expected => {}
Some(_) => {
return Err(Error::EditUnsupported(
"dataset data length does not match its shape",
));
}
None => {
return Err(Error::EditUnsupported(
"dataset shape is too large to address on this platform",
));
}
}
}
let chunked = db.chunk_options.is_chunked() || db.maxshape.is_some();
if chunked {
db.chunk_options
.validate_geometry(&shape, db.maxshape.as_deref())
.map_err(Error::EditUnsupported)?;
#[cfg(not(feature = "deflate"))]
if db.chunk_options.deflate_level.is_some() {
return Err(Error::EditUnsupported(
"deflate compression requires the `deflate` crate feature",
));
}
let chunk_dims = db.chunk_options.resolve_chunk_dims(&shape);
let ctx = ChunkContext::from_datatype(&chunk_dims, &dt);
db.chunk_options
.build_pipeline(
ctx.element_size,
&chunk_dims,
ctx.element_type,
ctx.scale_offset_type,
)
.map_err(|_| {
Error::EditUnsupported(
"this dataset's filter pipeline cannot be added in place \
(an unsupported filter, an incompatible datatype, or a \
compression feature that is not enabled)",
)
})?;
}
if db
.attrs
.iter()
.any(|(_, v)| matches!(v, AttrValue::VarLenAsciiArray(_)))
{
return Err(Error::EditUnsupported(
"variable-length attributes cannot be added in place yet",
));
}
if db.attrs.len() > MAX_COMPACT_ATTRS {
return Err(Error::EditUnsupported(
"datasets with dense (many) attributes cannot be added in place yet",
));
}
if make_link(&db.name, 0).serialize(OFFSET_SIZE).len() > u16::MAX as usize {
return Err(Error::EditUnsupported(
"dataset name is too long to encode as a link message",
));
}
let ds = Dataspace {
space_type: if shape.is_empty() {
DataspaceType::Scalar
} else {
DataspaceType::Simple
},
#[expect(
clippy::cast_possible_truncation,
reason = "dataspace rank fits the 1-byte dimensionality field (HDF5 caps rank at 32)"
)]
rank: shape.len() as u8,
dimensions: shape,
max_dimensions: db.maxshape.clone(),
};
let attrs = db
.attrs
.iter()
.map(|(n, v)| build_attr_message(n, v))
.collect();
Ok(FlatDataset {
name: db.name,
dt,
ds,
raw,
attrs,
chunk_options: db.chunk_options,
maxshape: db.maxshape,
})
}
const GROUP_INFO_BODY: [u8; 2] = [0, 0];
fn region_message(msg_type: MessageType, body: &[u8]) -> Vec<u8> {
let mut m = Vec::with_capacity(4 + body.len());
#[expect(
clippy::cast_possible_truncation,
reason = "message type ids are a small enum that fits the 1-byte v2 type field"
)]
m.push(msg_type.to_u16() as u8);
#[expect(
clippy::cast_possible_truncation,
reason = "callers pass bodies that fit the 2-byte message-size field (see doc comment)"
)]
m.extend_from_slice(&(body.len() as u16).to_le_bytes());
m.push(0); m.extend_from_slice(body);
m
}
fn fresh_group_region() -> Vec<u8> {
let mut li = Vec::with_capacity(18);
li.push(0); li.push(0); li.extend_from_slice(&u64::MAX.to_le_bytes()); li.extend_from_slice(&u64::MAX.to_le_bytes()); let mut region = region_message(MessageType::LinkInfo, &li);
region.extend_from_slice(®ion_message(MessageType::GroupInfo, &GROUP_INFO_BODY));
region
}
fn ensure_group_info(region: &mut Vec<u8>) -> Result<(), Error> {
let mut p = 0;
while let Some((msg_type, _body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::GroupInfo {
return Ok(());
}
p = body_end;
}
region.extend_from_slice(®ion_message(MessageType::GroupInfo, &GROUP_INFO_BODY));
Ok(())
}
fn encode_link_message(name: &str, addr: u64) -> Vec<u8> {
let body = make_link(name, addr).serialize(OFFSET_SIZE);
region_message(MessageType::Link, &body)
}
fn patch_link_target(region: &mut [u8], name: &str, new_addr: u64) -> Result<(), Error> {
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::Link {
if let Ok(link) = LinkMessage::parse(®ion[body..body_end], OFFSET_SIZE) {
if link.name == name {
return match link.link_target {
LinkTarget::Hard { .. } => {
let ofs = body_end - OFFSET_SIZE as usize;
region[ofs..body_end].copy_from_slice(&new_addr.to_le_bytes());
Ok(())
}
_ => Err(Error::EditUnsupported(
"a group on the edited path is reached by a soft/external link",
)),
};
}
}
}
p = body_end;
}
Err(Error::EditUnsupported(
"expected child link not found in parent group",
))
}
fn rebuild_compact_layout_region(region: &[u8], raw: &[u8]) -> Result<Vec<u8>, Error> {
if raw.len() > u16::MAX as usize {
return Err(Error::EditUnsupported(
"compact dataset data is too large to overwrite in place",
));
}
let mut out = Vec::with_capacity(region.len() + raw.len());
let mut p = 0;
let mut replaced = false;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::DataLayout {
if body_end - body < 2 || region[body + 1] != 0 {
return Err(Error::EditUnsupported(
"compact-layout overwrite found a non-compact data layout",
));
}
let mut layout = Vec::with_capacity(4 + raw.len());
layout.push(region[body]); layout.push(0); #[expect(
clippy::cast_possible_truncation,
reason = "raw.len() bounded to u16::MAX above"
)]
layout.extend_from_slice(&(raw.len() as u16).to_le_bytes());
layout.extend_from_slice(raw);
out.push(region[p]);
#[expect(
clippy::cast_possible_truncation,
reason = "layout body length is 4 + raw.len() <= u16::MAX + 4, and an OH \
message size that overflows u16 is itself malformed"
)]
out.extend_from_slice(&(layout.len() as u16).to_le_bytes());
out.push(region[p + 3]);
out.extend_from_slice(&layout);
replaced = true;
} else {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if p < region.len() {
out.extend_from_slice(®ion[p..]);
}
if !replaced {
return Err(Error::EditUnsupported(
"compact dataset header has no data-layout message",
));
}
Ok(out)
}
fn remove_link_from_region(region: &[u8], name: &str) -> Result<Vec<u8>, Error> {
let mut out = Vec::with_capacity(region.len());
let mut p = 0;
let mut removed = false;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
let mut skip = false;
if msg_type == MessageType::Link {
if let Ok(link) = LinkMessage::parse(®ion[body..body_end], OFFSET_SIZE) {
if link.name == name {
skip = true;
removed = true;
}
}
}
if !skip {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if p < region.len() {
out.extend_from_slice(®ion[p..]);
}
if !removed {
return Err(Error::EditUnsupported(
"link to delete not found in its parent group",
));
}
Ok(out)
}
fn apply_group_attr_ops(region: &[u8], ops: &[GroupAttrOp]) -> Result<Vec<u8>, Error> {
let mut out = region.to_vec();
let mut wrote_attr = false;
for op in ops {
out = match op {
GroupAttrOp::Set { name, value } => {
wrote_attr = true;
set_attr_in_region(&out, name, value)?
}
GroupAttrOp::Remove { name } => remove_attr_from_region(&out, name)?,
};
}
if wrote_attr && compact_attr_count(&out)? > MAX_COMPACT_ATTRS {
return Err(Error::EditUnsupported(
"group attributes would exceed compact storage; dense attribute edits are not supported in place yet",
));
}
Ok(out)
}
fn set_attr_in_region(region: &[u8], name: &str, value: &AttrValue) -> Result<Vec<u8>, Error> {
let new_msg = encode_attr_message(name, value)?;
let mut out = Vec::with_capacity(region.len() + new_msg.len());
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
match msg_type {
MessageType::AttributeInfo => {
return Err(Error::EditUnsupported(
"a target group uses dense (fractal-heap) attribute storage (not supported in place yet)",
));
}
MessageType::Attribute => {
let attr_name = parse_compact_attr_name(region, p, body, body_end)?;
if attr_name == name {
p = body_end;
continue;
}
}
_ => {}
}
out.extend_from_slice(®ion[p..body_end]);
p = body_end;
}
out.extend_from_slice(&new_msg);
if p < region.len() {
out.extend_from_slice(®ion[p..]);
}
Ok(out)
}
fn remove_attr_from_region(region: &[u8], name: &str) -> Result<Vec<u8>, Error> {
let mut out = Vec::with_capacity(region.len());
let mut p = 0;
let mut removed = false;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
let mut skip = false;
match msg_type {
MessageType::AttributeInfo => {
return Err(Error::EditUnsupported(
"a target group uses dense (fractal-heap) attribute storage (not supported in place yet)",
));
}
MessageType::Attribute => {
let attr_name = parse_compact_attr_name(region, p, body, body_end)?;
if attr_name == name {
skip = true;
removed = true;
}
}
_ => {}
}
if !skip {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if p < region.len() {
out.extend_from_slice(®ion[p..]);
}
if !removed {
return Err(Error::EditUnsupported(
"group attribute to remove was not found",
));
}
Ok(out)
}
fn compact_attr_count(region: &[u8]) -> Result<usize, Error> {
let mut count = 0usize;
let mut p = 0;
while let Some((msg_type, _body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::AttributeInfo {
return Err(Error::EditUnsupported(
"a target group uses dense (fractal-heap) attribute storage (not supported in place yet)",
));
}
if msg_type == MessageType::Attribute {
count += 1;
}
p = body_end;
}
Ok(count)
}
fn parse_compact_attr_name(
region: &[u8],
msg_start: usize,
body: usize,
body_end: usize,
) -> Result<String, Error> {
if region[msg_start + 3] != 0 {
return Err(Error::EditUnsupported(
"a target group has a shared attribute message (not editable in place yet)",
));
}
crate::attribute::AttributeMessage::parse(®ion[body..body_end], LENGTH_SIZE)
.map(|attr| attr.name)
.map_err(|_| Error::EditUnsupported("a target group has an unreadable attribute message"))
}
fn encode_attr_message(name: &str, value: &AttrValue) -> Result<Vec<u8>, Error> {
if matches!(value, AttrValue::VarLenAsciiArray(_)) {
return Err(Error::EditUnsupported(
"variable-length group attributes cannot be edited in place yet",
));
}
let body = build_attr_message(name, value).serialize(LENGTH_SIZE);
if body.len() > u16::MAX as usize {
return Err(Error::EditUnsupported(
"group attribute is too large to encode in place",
));
}
Ok(region_message(MessageType::Attribute, &body))
}
fn is_prefix(a: &[String], b: &[String]) -> bool {
a.len() <= b.len() && b[..a.len()] == *a
}
fn next_message(region: &[u8], p: usize) -> Result<Option<(MessageType, usize, usize)>, Error> {
if p + 4 > region.len() {
return Ok(None);
}
let msg_type = MessageType::from_u16(region[p] as u16);
let msg_size = u16::from_le_bytes([region[p + 1], region[p + 2]]) as usize;
let body = p + 4;
let body_end = body + msg_size;
if body_end > region.len() {
return Err(Error::EditUnsupported("malformed object header message"));
}
Ok(Some((msg_type, body, body_end)))
}
const MSG_FLAG_SHARED: u8 = 0x02;
fn reject_foreign_addresses(region: &[u8]) -> Result<(), Error> {
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
if region[p + 3] & MSG_FLAG_SHARED != 0 {
return Err(Error::EditUnsupported(
"a shared (committed/SOHM) object-header message cannot be copied to another file yet",
));
}
match msg_type {
MessageType::Datatype => {
let (dt, _) =
crate::datatype::Datatype::parse(®ion[body..body_end]).map_err(|_| {
Error::EditUnsupported("a source datatype could not be parsed for copying")
})?;
if datatype_copies_foreign_address(&dt) {
return Err(Error::EditUnsupported(
"variable-length or reference datasets cannot be copied to another file yet",
));
}
}
MessageType::Attribute => {
let attr =
crate::attribute::AttributeMessage::parse(®ion[body..body_end], LENGTH_SIZE)
.map_err(|_| {
Error::EditUnsupported(
"a source attribute could not be parsed for copying",
)
})?;
if datatype_copies_foreign_address(&attr.datatype) {
return Err(Error::EditUnsupported(
"variable-length or reference attributes cannot be copied to another file yet",
));
}
}
_ => {}
}
p = body_end;
}
Ok(())
}
fn reject_foreign_dense_attrs(attrs: &[crate::attribute::AttributeMessage]) -> Result<(), Error> {
for attr in attrs {
if datatype_copies_foreign_address(&attr.datatype) {
return Err(Error::EditUnsupported(
"variable-length or reference dense (fractal-heap) attributes cannot be copied to another file yet",
));
}
}
Ok(())
}
fn datatype_copies_foreign_address(dt: &crate::datatype::Datatype) -> bool {
use crate::datatype::Datatype;
match dt {
Datatype::VariableLength { .. } | Datatype::Reference { .. } => true,
Datatype::Compound { members, .. } => members
.iter()
.any(|m| datatype_copies_foreign_address(&m.datatype)),
Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } => {
datatype_copies_foreign_address(base_type)
}
_ => false,
}
}
fn build_v2_object_header(region: &[u8]) -> Vec<u8> {
let total = region.len();
let (flags, width) = if total <= 255 {
(0u8, 1usize)
} else if total <= 65535 {
(1u8, 2)
} else {
(2u8, 4)
};
let mut buf = Vec::with_capacity(8 + total + 4);
buf.extend_from_slice(b"OHDR");
buf.push(2); buf.push(flags);
#[expect(
clippy::cast_possible_truncation,
reason = "width was selected just above to be the smallest field that holds total"
)]
match width {
1 => buf.push(total as u8),
2 => buf.extend_from_slice(&(total as u16).to_le_bytes()),
_ => buf.extend_from_slice(&(total as u32).to_le_bytes()),
}
buf.extend_from_slice(region);
let checksum = jenkins_lookup3(&buf);
buf.extend_from_slice(&checksum.to_le_bytes());
buf
}
#[expect(
clippy::cast_possible_truncation,
reason = "callers parse in-file sizes/offsets bounded by the in-memory image; downstream \
slicing is length-checked, so a malformed oversized field errors rather than reads OOB"
)]
fn read_le(bytes: &[u8]) -> usize {
let mut v = 0u64;
for (i, &b) in bytes.iter().enumerate() {
v |= (b as u64) << (8 * i);
}
v as usize
}
#[cfg(test)]
mod tests {
use super::*;
fn region_types(region: &[u8]) -> Vec<MessageType> {
let mut out = Vec::new();
let mut p = 0;
while let Some((mt, _, end)) = next_message(region, p).unwrap() {
out.push(mt);
p = end;
}
out
}
#[test]
fn fresh_group_region_pairs_link_info_with_group_info() {
let types = region_types(&fresh_group_region());
assert_eq!(types, vec![MessageType::LinkInfo, MessageType::GroupInfo]);
}
#[test]
fn ensure_group_info_appends_when_missing() {
let li_body = {
let mut b = vec![0u8, 0];
b.extend_from_slice(&u64::MAX.to_le_bytes());
b.extend_from_slice(&u64::MAX.to_le_bytes());
b
};
let mut region = region_message(MessageType::LinkInfo, &li_body);
ensure_group_info(&mut region).unwrap();
assert_eq!(
region_types(®ion),
vec![MessageType::LinkInfo, MessageType::GroupInfo]
);
let mut p = 0;
while let Some((mt, body, end)) = next_message(®ion, p).unwrap() {
if mt == MessageType::GroupInfo {
assert_eq!(®ion[body..end], &GROUP_INFO_BODY);
}
p = end;
}
}
#[test]
fn ensure_group_info_is_idempotent() {
let mut region = fresh_group_region();
let before = region.clone();
ensure_group_info(&mut region).unwrap();
assert_eq!(region, before);
}
#[test]
fn reject_foreign_addresses_refuses_any_shared_message() {
let mut shared = region_message(MessageType::Dataspace, &[0u8; 8]);
shared[3] = MSG_FLAG_SHARED; let err = reject_foreign_addresses(&shared).unwrap_err();
assert!(err.to_string().contains("shared"), "got: {err}");
let plain = region_message(MessageType::Dataspace, &[0u8; 8]);
reject_foreign_addresses(&plain).unwrap();
}
fn compact_layout_body(version: u8, data: &[u8]) -> Vec<u8> {
let mut b = vec![version, 0];
b.extend_from_slice(&(data.len() as u16).to_le_bytes());
b.extend_from_slice(data);
b
}
#[test]
fn rebuild_compact_layout_replaces_inline_data_only() {
let mut region = region_message(MessageType::Dataspace, &[0xAB; 8]);
region.extend_from_slice(®ion_message(
MessageType::DataLayout,
&compact_layout_body(3, &[1, 2, 3, 4]),
));
region.extend_from_slice(®ion_message(MessageType::Attribute, &[0xCD; 5]));
let out = rebuild_compact_layout_region(®ion, &[9, 8, 7, 6]).unwrap();
assert_eq!(
region_types(&out),
vec![
MessageType::Dataspace,
MessageType::DataLayout,
MessageType::Attribute,
]
);
let mut p = 0;
while let Some((mt, body, end)) = next_message(&out, p).unwrap() {
match mt {
MessageType::Dataspace => assert_eq!(&out[body..end], &[0xAB; 8]),
MessageType::DataLayout => {
assert_eq!(out[body], 3, "version preserved");
assert_eq!(out[body + 1], 0, "still compact");
let size = u16::from_le_bytes([out[body + 2], out[body + 3]]) as usize;
assert_eq!(size, 4);
assert_eq!(&out[body + 4..body + 4 + size], &[9, 8, 7, 6]);
}
MessageType::Attribute => assert_eq!(&out[body..end], &[0xCD; 5]),
other => panic!("unexpected message {other:?}"),
}
p = end;
}
}
#[test]
fn rebuild_compact_layout_refuses_non_compact() {
let mut region = region_message(MessageType::DataLayout, &{
let mut b = vec![3u8, 1]; b.extend_from_slice(&0u64.to_le_bytes());
b.extend_from_slice(&0u64.to_le_bytes());
b
});
region.extend_from_slice(®ion_message(MessageType::Dataspace, &[0; 8]));
let err = rebuild_compact_layout_region(®ion, &[1, 2]).unwrap_err();
assert!(err.to_string().contains("non-compact"), "got: {err}");
}
#[test]
fn commit_clears_a_stale_consistency_flag() {
use crate::writer::FileBuilder;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("stale_flag.h5");
let mut b = FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1, 2, 3]);
b.write(&path).unwrap();
{
let mut data = std::fs::read(&path).unwrap();
let off = signature::find_signature(&data).unwrap();
let mut sb = Superblock::parse(&data, off).unwrap();
assert!(
sb.version >= 2,
"FileBuilder should emit a v2/v3 superblock"
);
sb.consistency_flags = 0x05;
let bytes = sb.serialize();
data[off..off + bytes.len()].copy_from_slice(&bytes);
std::fs::write(&path, &data).unwrap();
assert_eq!(
Superblock::parse(&data, off).unwrap().consistency_flags,
0x05
);
}
{
let mut s = EditSession::open(&path).unwrap();
s.create_dataset("e").with_i32_data(&[4, 5]);
s.commit().unwrap();
}
let data = std::fs::read(&path).unwrap();
let off = signature::find_signature(&data).unwrap();
assert_eq!(
Superblock::parse(&data, off).unwrap().consistency_flags,
0,
"commit must clear the stale consistency flag"
);
}
}