use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use core::num::NonZeroUsize;
use crate::address::BaseAddress;
use crate::checksum::jenkins_lookup3;
use crate::chunk_index_inplace::{Located, Store, apply_ea_append, plan_ea_append};
use crate::chunked_read::{
chunk_index_spans_from_source, enumerate_chunks_from_source, plan_dense_grid,
};
use crate::chunked_write::{
ChunkMeta, ChunkOptions, ChunkProvider, StorageAllocation, WrittenChunk, assemble_chunked_at,
build_extensible_array_at, chunked_data_len, compress_chunks, emit_chunked_data_verbatim,
extensible_array_len, full_chunk_bytes, plan_chunked_data_verbatim,
serialize_v4_extensible_array, split_into_chunks,
};
use crate::convert::TryToUsize;
use crate::data_layout::DataLayout;
use crate::dataspace::{Dataspace, DataspaceType};
use crate::datatype::{
Datatype, DatatypeByteOrder, datatype_holds_file_address, datatype_holds_object_address,
embedded_reference_slots, stored_object_references,
};
use crate::error::{Error, FormatError, OBJECT_HEADER_MESSAGE_MAX};
use crate::extensible_array::ExtensibleArrayHeader;
use crate::file_create_properties::FileCreateProperties;
use crate::file_lock::{self, FileLocking};
use crate::file_space_info::{FileSpaceInfo, FileSpaceStrategy, NUM_FILE_FSM_MANAGERS};
use crate::file_writer::{
LENGTH_SIZE, OFFSET_SIZE, build_chunked_dataset_oh, build_dataset_oh, make_link,
};
use crate::filter_pipeline::{
FILTER_DEFLATE, FILTER_FLETCHER32, FILTER_LZF, FILTER_SCALEOFFSET, FILTER_SHUFFLE,
FilterPipeline,
};
use crate::filters::{ChunkContext, FilterScratch, compress_chunk_with, decompress_chunk};
use crate::free_space::FreeList;
use crate::free_space_manager::{
self, FreeSection, FsmHeader, PageType, PagedManagerPlan, SECT_CLASS_SIMPLE, align_up,
file_fsm_blocks_len, free_sections, fshd_len, plan_paged_managers, serialize_file_fsm,
};
use crate::group_v2::resolve_group_entries_from_source;
use crate::image::{FileImage, HandleImage, MirrorImage, WriteBuffering};
use crate::libver::LibVer;
use crate::link_message::{LinkMessage, LinkTarget};
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::reader::FileAccessProperties;
use crate::shared_message::DatatypeLocation;
use crate::signature;
use crate::source::{BaseOffsetSource, BytesSource, MetadataCacheConfig, Source};
use crate::superblock::Superblock;
use crate::type_builders::{
AttrValue, DatasetBuilder, ObjectRefPatch, ObjectRefTarget, VlStringStaging,
build_attr_message, build_global_heap_collections, make_f32_type, make_f64_type, make_i8_type,
make_i16_type, make_i32_type, make_i64_type, make_u8_type, make_u16_type, make_u32_type,
make_u64_type, patch_vl_refs, patch_vl_refs_masked, write_reference_address,
};
const UNDEF: u64 = u64::MAX;
const APPEND_RESERVE_BYTES: u64 = 1 << 20;
const REFERENCE_INTO_RECLAIMED_SPACE: &str = "a reference this commit writes holds the address of an object this commit deletes, or of \
one under it; the reference would be left pointing at storage the delete can reclaim";
const REFERENCE_TO_A_MOVED_OBJECT: &str = "a reference this commit writes holds the pre-commit address of an object this commit \
rewrites elsewhere; name the target by path (`with_path_references`) so it resolves to \
where the object lands, or use separate commits";
const SHARED_ATTRIBUTE_MESSAGE: &str =
"a target object has a shared attribute message (not editable in place yet)";
const REFERENCE_ATTRIBUTE_WOULD_LEAVE_THE_HEADER: &str = "an attribute holding an object reference cannot be moved to dense \
(fractal-heap) storage, where a later commit could no longer repoint it when \
its target moves; keep this object within compact attribute storage";
const MAX_COMPACT_ATTRS: usize = crate::file_writer::DENSE_ATTR_THRESHOLD;
const MAX_COPY_DEPTH: u32 = 1000;
const MAX_LINK_GRAPH_NODES: u32 = 1 << 24;
const MAX_OH_CHUNKS: usize = 256;
const OH_PREFIX_MAX: usize = 34;
type PathKey = Vec<String>;
struct AppenderClaim {
token: u64,
path: Option<PathKey>,
}
type PendingVlAttrs = Vec<(crate::attribute::AttributeMessage, Vec<Vec<u8>>)>;
pub struct AppendBuilder {
raw: Vec<u8>,
elem_dt: Option<Datatype>,
dt_conflict: bool,
}
impl AppendBuilder {
pub(crate) fn new() -> Self {
Self {
raw: Vec::new(),
elem_dt: None,
dt_conflict: false,
}
}
pub(crate) fn raw(&self) -> &[u8] {
&self.raw
}
pub(crate) fn elem_dt(&self) -> Option<&Datatype> {
self.elem_dt.as_ref()
}
pub(crate) fn dt_conflict(&self) -> bool {
self.dt_conflict
}
pub(crate) fn head(&self, byte_len: usize) -> Self {
Self {
raw: self.raw[..byte_len.min(self.raw.len())].to_vec(),
elem_dt: self.elem_dt.clone(),
dt_conflict: self.dt_conflict,
}
}
pub(crate) fn drop_front(&mut self, byte_len: usize) {
self.raw.drain(..byte_len.min(self.raw.len()));
}
pub(crate) fn into_raw(self) -> Vec<u8> {
self.raw
}
pub(crate) fn truncate(&mut self, byte_len: usize) {
self.raw.truncate(byte_len);
}
fn set_dt(&mut self, dt: Datatype) {
match &self.elem_dt {
Some(prev) if *prev != dt => self.dt_conflict = true,
Some(_) => {}
None => self.elem_dt = Some(dt),
}
}
pub fn append_raw(&mut self, bytes: &[u8]) -> &mut Self {
self.raw.extend_from_slice(bytes);
self
}
pub fn append<T: crate::element::H5Element>(&mut self, data: &[T]) -> &mut Self {
T::append_into(self, data);
self
}
}
macro_rules! append_typed {
($($method:ident, $ty:ty, $make:ident;)*) => {
impl AppendBuilder {
$(
#[doc = concat!("Append `", stringify!($ty), "` values to the dataset.")]
pub fn $method(&mut self, data: &[$ty]) -> &mut Self {
self.set_dt($make());
self.raw.reserve(data.len() * core::mem::size_of::<$ty>());
for &v in data {
self.raw.extend_from_slice(&v.to_le_bytes());
}
self
}
)*
}
};
}
append_typed! {
append_f64, f64, make_f64_type;
append_f32, f32, make_f32_type;
append_i8, i8, make_i8_type;
append_i16, i16, make_i16_type;
append_i32, i32, make_i32_type;
append_i64, i64, make_i64_type;
append_u8, u8, make_u8_type;
append_u16, u16, make_u16_type;
append_u32, u32, make_u32_type;
append_u64, u64, make_u64_type;
}
#[derive(Default)]
struct StagedEdits {
datasets: Vec<(PathKey, FlatDataset)>,
writes: Vec<(PathKey, FlatDataset)>,
appends: Vec<(PathKey, AppendBuilder)>,
groups: Vec<PathKey>,
group_attrs: Vec<(PathKey, AttrOp)>,
dataset_attrs: Vec<(PathKey, AttrOp)>,
deletes: Vec<PathKey>,
copies: Vec<(PathKey, PathKey)>,
cross_copies: Vec<(PathKey, CopyTree)>,
dataset_at: HashMap<PathKey, usize>,
group_at: HashMap<PathKey, usize>,
}
fn child_key(parent: &[String], name: &str) -> PathKey {
let mut full = parent.to_vec();
full.push(name.to_string());
full
}
fn dataset_under(parent: &[String], name: &str, prefix: &[String]) -> bool {
match prefix.len() {
n if n <= parent.len() => parent.starts_with(prefix),
n if n == parent.len() + 1 => {
parent[..] == prefix[..parent.len()] && prefix[parent.len()] == name
}
_ => false,
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum StagedKind {
Group,
Dataset,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct StagedObject {
pub(crate) kind: StagedKind,
pub(crate) replaces_link: bool,
}
pub(crate) struct StagedChild {
pub(crate) name: String,
pub(crate) kind: StagedKind,
pub(crate) replaces_link: bool,
}
pub(crate) struct StagedMeta {
pub(crate) datatype: Datatype,
pub(crate) dimensions: Vec<u64>,
pub(crate) maxshape: Option<Vec<u64>>,
pub(crate) chunked: bool,
pub(crate) filters: Vec<(u16, bool)>,
}
#[derive(Clone, Copy, Default, PartialEq, Eq)]
struct StagedMark {
datasets: usize,
writes: usize,
appends: usize,
groups: usize,
group_attrs: usize,
dataset_attrs: usize,
deletes: usize,
copies: usize,
cross_copies: usize,
}
impl StagedEdits {
fn mark(&self) -> StagedMark {
let Self {
datasets,
writes,
appends,
groups,
group_attrs,
dataset_attrs,
deletes,
copies,
cross_copies,
dataset_at: _,
group_at: _,
} = self;
StagedMark {
datasets: datasets.len(),
writes: writes.len(),
appends: appends.len(),
groups: groups.len(),
group_attrs: group_attrs.len(),
dataset_attrs: dataset_attrs.len(),
deletes: deletes.len(),
copies: copies.len(),
cross_copies: cross_copies.len(),
}
}
fn rewind(&mut self, mark: StagedMark) {
for i in mark.datasets..self.datasets.len() {
let key = {
let (parent, fd) = &self.datasets[i];
child_key(parent, &fd.name)
};
if self.dataset_at.get(&key) == Some(&i) {
self.dataset_at.remove(&key);
}
}
for i in mark.groups..self.groups.len() {
let key = self.groups[i].clone();
if self.group_at.get(&key) == Some(&i) {
self.group_at.remove(&key);
}
}
let StagedMark {
datasets,
writes,
appends,
groups,
group_attrs,
dataset_attrs,
deletes,
copies,
cross_copies,
} = mark;
self.datasets.truncate(datasets);
self.writes.truncate(writes);
self.appends.truncate(appends);
self.groups.truncate(groups);
self.group_attrs.truncate(group_attrs);
self.dataset_attrs.truncate(dataset_attrs);
self.deletes.truncate(deletes);
self.copies.truncate(copies);
self.cross_copies.truncate(cross_copies);
}
fn push_dataset(&mut self, parent: PathKey, fd: FlatDataset) {
self.dataset_at
.entry(child_key(&parent, &fd.name))
.or_insert(self.datasets.len());
self.datasets.push((parent, fd));
}
fn push_group(&mut self, path: PathKey) {
self.group_at
.entry(path.clone())
.or_insert(self.groups.len());
self.groups.push(path);
}
fn dataset_position(&self, path: &[String]) -> Option<usize> {
let i = *self.dataset_at.get(path)?;
let (parent, fd) = self.datasets.get(i)?;
dataset_under(parent, &fd.name, path).then_some(i)
}
fn dataset_at(&self, path: &[String]) -> Option<&FlatDataset> {
let i = self.dataset_position(path)?;
self.datasets.get(i).map(|(_, fd)| fd)
}
fn dataset_at_mut(&mut self, path: &[String]) -> Option<&mut FlatDataset> {
let i = self.dataset_position(path)?;
self.datasets.get_mut(i).map(|(_, fd)| fd)
}
fn has_group_at(&self, path: &[String]) -> bool {
match self.group_at.get(path) {
Some(&i) => self.groups.get(i).is_some_and(|p| p[..] == *path),
None => false,
}
}
fn deletes_cover(&self, path: &[String]) -> bool {
self.deletes.iter().any(|d| path.starts_with(&d[..]))
}
fn deletes_hand_over(&self, path: &[String]) -> bool {
self.deletes.iter().any(|d| {
path.starts_with(&d[..]) && (d.len()..path.len()).all(|n| self.has_group_at(&path[..n]))
})
}
fn withdraw_at(&mut self, path: &[String]) -> bool {
let before = {
let Self {
datasets,
writes,
appends,
groups,
group_attrs,
dataset_attrs,
deletes,
copies,
cross_copies,
dataset_at: _,
group_at: _,
} = self;
let before = datasets.len() + groups.len();
datasets.retain(|(parent, fd)| !dataset_under(parent, &fd.name, path));
writes.retain(|(p, _)| !p.starts_with(path));
appends.retain(|(p, _)| !p.starts_with(path));
groups.retain(|p| !p.starts_with(path));
group_attrs.retain(|(p, _)| !p.starts_with(path));
dataset_attrs.retain(|(p, _)| !p.starts_with(path));
copies.retain(|(_, dst)| !dst.starts_with(path));
cross_copies.retain(|(dst, _)| !dst.starts_with(path));
let _ = &deletes;
before
};
let withdrew = self.datasets.len() + self.groups.len() < before;
if withdrew {
self.reindex();
}
withdrew
}
fn reindex(&mut self) {
self.dataset_at.clear();
self.group_at.clear();
for (i, (parent, fd)) in self.datasets.iter().enumerate() {
self.dataset_at
.entry(child_key(parent, &fd.name))
.or_insert(i);
}
for (i, path) in self.groups.iter().enumerate() {
self.group_at.entry(path.clone()).or_insert(i);
}
}
fn is_empty(&self) -> bool {
self.mark() == StagedMark::default()
}
}
pub(crate) struct WriteEngine {
image: Box<dyn FileImage>,
sb_sig_off: usize,
superblock: Superblock,
staged: StagedEdits,
appender_claims: Vec<AppenderClaim>,
next_appender_token: u64,
free: FreeList,
reserved: FreeList,
proved_free_of_references: bool,
persist: Option<PersistState>,
located: HashMap<u64, LocatedState>,
vl_overwrite_heaps: HashMap<PathKey, Vec<(u64, u64)>>,
superseded_heaps: Vec<(u64, u64)>,
inplace_undo: Vec<(usize, Vec<u8>)>,
swmr_mode: bool,
paged: Option<PagedEdit>,
committed: bool,
resolved: HashMap<String, u64>,
batched_appends: bool,
bounded: bool,
libver_ceiling: Option<LibVer>,
fsm_len: u64,
publish_attempted: bool,
staging_batch: bool,
staged_generation: u64,
held_status_flags: u32,
sync_policy: SyncPolicy,
}
struct FreeSnapshot {
free: FreeList,
reserved: FreeList,
paged: Option<(FreeList, FreeList)>,
vl_overwrite_heaps: HashMap<PathKey, Vec<(u64, u64)>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MemoryStrategy {
Bounded,
Auto,
Mirrored,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EditBacking {
Bounded,
Mirrored,
}
impl From<EditBacking> for MemoryStrategy {
fn from(backing: EditBacking) -> Self {
match backing {
EditBacking::Bounded => Self::Bounded,
EditBacking::Mirrored => Self::Mirrored,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum SyncPolicy {
#[default]
Always,
OnClose,
}
fn bounded_only_limitation(session: &WriteEngine) -> Option<&'static str> {
if session.superblock.version < 2 {
return Some(
"bounded read-write access requires a latest-format file (v2/v3 superblock); \
leave MemoryStrategy unset, or pass MemoryStrategy::Auto, to fall back to \
the whole-file mirror here",
);
}
if !session.superblock.base_address.is_zero() {
return Some(
"bounded read-write access does not support a file with a userblock \
(non-zero base address); leave MemoryStrategy unset, or pass \
MemoryStrategy::Auto, to fall back to the whole-file mirror here",
);
}
None
}
pub(crate) fn create_would_refuse_reopen(
create: &FileCreateProperties,
access: &FileAccessProperties,
) -> Option<&'static str> {
if let Some((FileSpaceStrategy::Page, false, _)) = create.file_space_strategy() {
return Some(
"a paged file (FileSpaceStrategy::Page) with persist = false cannot be reopened \
read-write, so creating one this way would write the file and then fail to open \
it; pass persist = true to with_file_space_strategy, or build the file with \
FileBuilder if it is only ever going to be read",
);
}
if create.userblock() != 0 && access.memory_strategy() == Some(MemoryStrategy::Bounded) {
return Some(
"a userblock cannot be combined with MemoryStrategy::Bounded: the bounded engine \
cannot edit a file with a non-zero base address, so creating one this way would \
write the file and then refuse to open it; drop the userblock, or leave \
MemoryStrategy unset to mirror this file",
);
}
if create.userblock() != 0
&& matches!(
create.file_space_strategy(),
Some((FileSpaceStrategy::Page, _, _))
)
{
return Some(
"a userblock cannot be combined with FileSpaceStrategy::Page: free space is not \
persisted for a file with a non-zero base address, and a paged file without it \
cannot be opened read-write — so creating one this way would write the file and \
then fail to open it; drop the userblock, or choose another file-space strategy",
);
}
if access.page_buffer_size() != 0 {
if access.sync_policy() == SyncPolicy::Always {
return Some(
"a page buffer does nothing under the default SyncPolicy::Always, whose every \
barrier is an fsync that flushes it, so creating one this way would write the \
file and then fail to open it; add \
with_sync_policy(SyncPolicy::OnClose) to the access properties, or drop the \
page buffer",
);
}
if matches!(
crate::libver::LibVer::resolve_writable(create.libver_bounds()),
Ok(v) if v < crate::libver::LibVer::V110
) {
return Some(
"a page buffer marks the file in a version-3 superblock, and these bounds \
write the 1.8 format, so creating one this way would write the file and then \
fail to open it; raise with_libver_bounds to admit LibVer::V110, or drop the \
page buffer",
);
}
let page_size = match create.file_space_strategy() {
Some((FileSpaceStrategy::Page, _, _)) => create
.file_space_page_size()
.unwrap_or(crate::file_space_info::DEFAULT_PAGE_SIZE),
_ => DEFAULT_GATHER_PAGE,
};
if (access.page_buffer_size() as u64) < page_size {
return Some(
"a page buffer smaller than the file's file-space page size would be refused \
at open — so creating one this way would write the file and then fail to \
open it; raise with_page_buffer_size",
);
}
}
None
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FreeClass {
Page(PageType),
Dead,
}
impl From<PageType> for FreeClass {
fn from(ty: PageType) -> Self {
FreeClass::Page(ty)
}
}
struct PagedEdit {
page_size: u64,
meta: FreeList,
raw: FreeList,
unclassified: FreeList,
dead: FreeList,
last: Option<PageType>,
meta_pad: Vec<(u64, u64)>,
raw_pad: Vec<(u64, u64)>,
}
impl PagedEdit {
fn begin(&mut self, image: &mut dyn FileImage, ty: PageType) -> Result<(), Error> {
let len = image.len();
if len % self.page_size != 0 {
let pad_len = self.page_size - len % self.page_size;
let pad = match self.last {
Some(prev) if prev != ty => Some(Some(prev)),
Some(_) => None, None => Some(None),
};
if let Some(prev) = pad {
let pad_at = len;
image.append(&vec![0u8; pad_len.to_usize()?])?;
match prev {
Some(PageType::Meta) => self.meta_pad.push((pad_at, pad_len)),
Some(PageType::Raw) => self.raw_pad.push((pad_at, pad_len)),
None => {} }
}
}
self.last = Some(ty);
Ok(())
}
fn new(page_size: u64) -> Self {
PagedEdit {
page_size,
meta: FreeList::new(),
raw: FreeList::new(),
unclassified: FreeList::new(),
dead: FreeList::new(),
last: None,
meta_pad: Vec::new(),
raw_pad: Vec::new(),
}
}
fn slot_list(slot: usize, addr: u64, size: u64, page_size: u64) -> Option<PageType> {
match slot {
0 => Some(PageType::Meta),
2 => Some(PageType::Raw),
6 if addr % page_size == 0 && size % page_size == 0 => Some(PageType::Raw),
_ => None,
}
}
fn route_free(
meta: &mut FreeList,
raw: &mut FreeList,
dead: &mut FreeList,
addr: u64,
size: u64,
class: FreeClass,
) {
match class {
FreeClass::Page(PageType::Meta) => meta.free(addr, size),
FreeClass::Page(PageType::Raw) => raw.free(addr, size),
FreeClass::Dead => dead.free(addr, size),
}
}
fn promote_whole_free_pages(
meta: &mut FreeList,
raw: &mut FreeList,
dead: &mut FreeList,
page_size: u64,
) {
let mut all = meta.sections();
all.extend(raw.sections());
all.extend(dead.sections());
all.sort_unstable_by_key(|&(addr, _)| addr);
let mut runs: Vec<(u64, u64)> = Vec::with_capacity(all.len());
for (addr, len) in all {
match runs.last_mut() {
Some(run) if run.0 + run.1 >= addr => {
let end = (run.0 + run.1).max(addr + len);
run.1 = end - run.0;
}
_ => runs.push((addr, len)),
}
}
for (addr, len) in runs {
let first = addr.next_multiple_of(page_size);
let last = (addr + len) / page_size * page_size;
if last <= first {
continue;
}
let span = last - first;
meta.take_range(first, span);
raw.take_range(first, span);
dead.take_range(first, span);
raw.free(first, span);
}
}
fn alloc_typed(&mut self, len: u64, ty: PageType) -> Option<u64> {
let (own, other) = match ty {
PageType::Meta => (&mut self.meta, &mut self.raw),
PageType::Raw => (&mut self.raw, &mut self.meta),
};
if let Some(addr) = own.alloc(len) {
return Some(addr);
}
let span = align_up(len, self.page_size);
let addr = other.alloc_whole_units(span, self.page_size)?;
if span > len {
own.free(addr + len, span - len);
}
Some(addr)
}
fn reusable_sections(&self) -> Vec<(u64, u64)> {
let mut out = self.meta.sections();
out.extend(self.raw.sections());
out.sort_unstable_by_key(|&(addr, _)| addr);
debug_assert!(
out.windows(2)
.all(|w| w[0].0.saturating_add(w[0].1) <= w[1].0),
"a region is free in both the metadata and the raw list, so the two \
page-type lists have stopped being disjoint"
);
out
}
}
struct PagedPostFree {
meta: FreeList,
raw: FreeList,
dead: FreeList,
unclassified: Vec<FreeSection>,
}
#[derive(Clone, Copy, Debug)]
enum Placement {
Reused { addr: u64, len: u64 },
Appended { addr: u64, len: u64 },
}
impl Placement {
fn address(self) -> u64 {
match self {
Placement::Reused { addr, .. } | Placement::Appended { addr, .. } => addr,
}
}
fn len(self) -> u64 {
match self {
Placement::Reused { len, .. } | Placement::Appended { len, .. } => len,
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum AppendTarget<'a> {
Path(&'a str),
Header(u64),
}
const APPEND_BATCH_BYTES: u64 = 1 << 20;
fn barrier_data(image: &mut dyn FileImage, sync_policy: SyncPolicy) -> Result<(), Error> {
match sync_policy {
SyncPolicy::Always => image.sync_data(),
SyncPolicy::OnClose => image.ordering_barrier(),
}
}
const WRITE_GATHER_BYTES: usize = 1 << 20;
const DEFAULT_GATHER_PAGE: u64 = crate::file_space_info::DEFAULT_PAGE_SIZE;
pub(crate) struct AppendGeometry {
pub(crate) chunk_elems: u64,
pub(crate) element_size: NonZeroUsize,
pub(crate) current_dim: u64,
pub(crate) lossy_filters: bool,
pub(crate) full_batch_elems: u64,
}
const SWMR_WRITE_FLAGS: u32 = file_lock::WRITE_ACCESS | file_lock::SWMR_WRITE_ACCESS;
pub(crate) struct LocatedState {
pub(crate) loc: Located,
pub(crate) datatype: Datatype,
pub(crate) spatial: Vec<u64>,
pub(crate) element_size: NonZeroUsize,
pub(crate) pipeline: Option<FilterPipeline>,
pub(crate) fill: crate::fill_value::PaddingFill,
}
struct PersistState {
strategy: FileSpaceStrategy,
threshold: u64,
page_size: u64,
old_blocks: Vec<(u64, u64)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SpaceAccounting {
pub logical_size: u64,
pub reusable_free_bytes: u64,
pub reusable_free_space: Vec<(u64, u64)>,
}
impl WriteEngine {
pub fn open_with_locking<P: AsRef<Path>>(path: P, locking: FileLocking) -> Result<Self, Error> {
Self::open_inner(path.as_ref(), Some(locking))
}
#[cfg(test)]
pub(crate) fn open_source_only(path: &Path) -> Result<Self, Error> {
Self::open_imaged(path, Some(FileLocking::Enabled), |handle, _len| {
Ok(Box::new(crate::image::SourceOnlyImage::new(
Self::read_mirror(handle)?,
)))
})
}
#[cfg(test)]
pub(crate) fn open_torn_writes(
path: &Path,
fails: core::ops::Range<u64>,
) -> Result<Self, Error> {
Self::open_imaged(path, Some(FileLocking::Enabled), |handle, _len| {
Ok(Box::new(crate::image::TornWriteImage::new(
Self::read_mirror(handle)?,
fails,
)))
})
}
#[cfg(test)]
pub(crate) fn open_bounded_counting(
path: &Path,
read_bytes: std::sync::Arc<std::sync::atomic::AtomicU64>,
) -> Result<Self, Error> {
let mut session = Self::open_imaged(path, Some(FileLocking::Enabled), |handle, len| {
Ok(Box::new(crate::image::CountingImage::new(
Box::new(HandleImage::new(
handle,
len,
crate::source::MetadataCacheConfig::disabled(),
)),
read_bytes,
std::sync::Arc::default(),
)))
})?;
session.batched_appends = true;
Ok(session)
}
#[cfg(test)]
pub(crate) fn open_sync_counting(
path: &Path,
policy: SyncPolicy,
syncs: std::sync::Arc<std::sync::atomic::AtomicU64>,
) -> Result<Self, Error> {
let mut session = Self::open_imaged(path, Some(FileLocking::Enabled), |handle, len| {
Ok(Box::new(crate::image::CountingImage::new(
Box::new(HandleImage::new(
handle,
len,
crate::source::MetadataCacheConfig::disabled(),
)),
std::sync::Arc::default(),
syncs,
)))
})?;
session.batched_appends = true;
session.bounded = true;
session.set_sync_policy(policy);
Ok(session)
}
pub(crate) fn open_rw_with_strategy(
path: &Path,
cache: MetadataCacheConfig,
locking: FileLocking,
strategy: MemoryStrategy,
) -> Result<Self, Error> {
if strategy == MemoryStrategy::Mirrored {
return Self::open_with_locking(path, locking);
}
let mut session = Self::open_imaged(path, Some(locking), |handle, len| {
Ok(Box::new(HandleImage::new(handle, len, cache)))
})?;
session.batched_appends = true;
session.bounded = true;
if session.paged.is_some() && session.persist.is_none() {
return Err(Error::EditUnsupported(
"read-write access to a paged file (H5F_FSPACE_STRATEGY_PAGE) requires \
persisted free space; recreate the file with \
with_file_space_strategy(FileSpaceStrategy::Page, true, ..) to grow it in place",
));
}
if let Some(reason) = bounded_only_limitation(&session) {
if strategy == MemoryStrategy::Bounded {
return Err(Error::EditUnsupported(reason));
}
drop(session);
return Self::open_with_locking(path, locking);
}
Ok(session)
}
pub(crate) fn open_swmr_writer<P: AsRef<Path>>(
path: P,
sync_policy: SyncPolicy,
) -> Result<Self, Error> {
let mut session = Self::open_inner(path.as_ref(), None)?;
session.set_sync_policy(sync_policy);
if session.superblock.version < 3
|| !session.superblock.base_address.is_zero()
|| session.persist.is_some()
{
return Err(Error::SwmrAppendUnsupported(
"SWMR writing requires a latest-format file (v3 superblock) with no userblock \
and no persisted free-space",
));
}
session.swmr_mode = true;
session.set_consistency_flags(SWMR_WRITE_FLAGS)?;
Ok(session)
}
fn set_consistency_flags(&mut self, flags: u32) -> Result<(), Error> {
self.superblock.consistency_flags = flags;
self.held_status_flags = flags;
let mut on_disk = self.superblock.clone();
on_disk.root_group_address = on_disk.base_address.relative(on_disk.root_group_address)?;
let bytes = on_disk.serialize();
self.write_at(self.sb_sig_off, &bytes)?;
self.barrier_data()?;
Ok(())
}
fn raise_crash_mark(&mut self) -> Result<(), Error> {
let raised = self
.set_consistency_flags(file_lock::WRITE_ACCESS)
.and_then(|()| self.image.sync_all());
if raised.is_err() {
let _ = self.set_consistency_flags(0);
let _ = self.image.sync_all();
}
raised
}
pub(crate) fn release_status_flags(&mut self) -> Result<(), Error> {
if self.held_status_flags == 0 {
return Ok(());
}
self.set_consistency_flags(0)?;
self.image.sync_all()
}
fn open_inner(path: &Path, lock: Option<FileLocking>) -> Result<Self, Error> {
Self::open_imaged(path, lock, |handle, _len| {
Ok(Box::new(Self::read_mirror(handle)?))
})
}
fn read_mirror(mut handle: fs::File) -> Result<MirrorImage, Error> {
handle.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
let mut data = Vec::new();
handle.read_to_end(&mut data).map_err(Error::Io)?;
Ok(MirrorImage::new(handle, data))
}
fn open_imaged(
path: &Path,
lock: Option<FileLocking>,
build: impl FnOnce(fs::File, u64) -> Result<Box<dyn FileImage>, Error>,
) -> Result<Self, Error> {
let handle = fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(Error::Io)?;
if let Some(policy) = lock {
file_lock::acquire_exclusive(&handle, policy, path)?;
}
let len = handle.metadata().map_err(Error::Io)?.len();
let probe = crate::image::BorrowedHandle::new(&handle, len);
let sb_sig_off = signature::find_signature_in(&probe)?.to_usize()?;
let mut superblock = Superblock::parse_from_source(&probe, sb_sig_off as u64)?;
if superblock.version > 3 {
return Err(Error::EditUnsupported("unsupported superblock version"));
}
file_lock::check_status_flags(
&superblock,
file_lock::OpenIntent::Write,
file_lock::OpenTarget::Path(path),
)?;
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 != BaseAddress::new(sb_sig_off as u64) {
return Err(Error::EditUnsupported(
"a file whose superblock is not located at its base address is not editable in place",
));
}
superblock.root_group_address = superblock
.base_address
.absolute(superblock.root_group_address)?;
let image = build(handle, len)?;
let mut session = Self {
image,
sb_sig_off,
superblock,
staged: StagedEdits::default(),
appender_claims: Vec::new(),
next_appender_token: 0,
free: FreeList::new(),
reserved: FreeList::new(),
proved_free_of_references: false,
persist: None,
located: HashMap::new(),
vl_overwrite_heaps: HashMap::new(),
superseded_heaps: Vec::new(),
inplace_undo: Vec::new(),
swmr_mode: false,
paged: None,
committed: false,
resolved: HashMap::new(),
batched_appends: false,
bounded: false,
libver_ceiling: None,
fsm_len: len,
publish_attempted: false,
staging_batch: false,
staged_generation: 0,
held_status_flags: 0,
sync_policy: SyncPolicy::Always,
};
session.load_persisted_free_space();
if lock.is_some() {
let page_size = session.gather_page_size();
session
.image
.set_write_buffering(WriteBuffering::Operation {
page_size,
max_bytes: WRITE_GATHER_BYTES,
})?;
}
Ok(session)
}
fn gather_page_size(&self) -> u64 {
self.paged
.as_ref()
.map_or(DEFAULT_GATHER_PAGE, |pg| pg.page_size)
}
pub(crate) fn set_page_buffer_size(&mut self, max_bytes: usize) -> Result<(), Error> {
if max_bytes == 0 {
return Ok(());
}
if self.swmr_mode {
return Err(Error::EditUnsupported(
"a SWMR writer cannot buffer its writes: its readers observe the order they \
become visible in",
));
}
let page_size = self.gather_page_size();
if (max_bytes as u64) < page_size {
return Err(Error::EditUnsupported(
"a page buffer must be at least the file's file-space page size",
));
}
if self.sync_policy == SyncPolicy::Always {
return Err(Error::EditUnsupported(
"a page buffer does nothing under SyncPolicy::Always, whose every barrier is \
an fsync that flushes it; pair with_page_buffer_size with \
with_sync_policy(SyncPolicy::OnClose), or drop it",
));
}
if self.paged.is_some() && self.persist.is_none() {
return Err(Error::EditUnsupported(
"a page buffer on a paged file needs its free space persisted: without it \
this session can neither commit nor append, so the buffer would hold nothing \
while marking the file against every reader; recreate the file with \
with_file_space_strategy(FileSpaceStrategy::Page, true, ..), or drop the \
page buffer",
));
}
if self.superblock.version < 3 {
return Err(Error::EditUnsupported(
"a page buffer marks the file for the life of the session, so that a session \
that crashes leaves one every reader refuses rather than one that reads \
clean; only a version-3 superblock carries a status-flags byte any library \
reads back, and this file's is older. Rewrite it at the 1.10 format \
(repack, or FileBuilder's default bounds), or drop the page buffer",
));
}
self.raise_crash_mark()?;
self.image.set_write_buffering(WriteBuffering::Session {
page_size,
max_bytes,
})
}
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) = self
.superblock
.base_address
.absolute(ext_rel)
.map_err(|_| ())
.and_then(|a| usize::try_from(a).map_err(|_| ()))
else {
return;
};
let Some(info) = self.extension_fsinfo(ext_addr) else {
return;
};
if !self.superblock.base_address.is_zero() {
if info.strategy == FileSpaceStrategy::Page && info.page_size > 0 {
self.paged = Some(PagedEdit::new(info.page_size));
}
return;
}
let paged = info.strategy == FileSpaceStrategy::Page && info.page_size > 0;
if paged {
self.paged = Some(PagedEdit::new(info.page_size));
}
if !info.persist {
return;
}
let os = self.superblock.offset_size;
let file_len = self.image.len();
if paged {
let page_size = info.page_size;
let mut tagged: Vec<(FreeSection, Option<PageType>)> = Vec::new();
for (slot, &m) in info.manager_addrs.iter().enumerate() {
if m == UNDEF {
continue;
}
let Ok(sections) = free_space_manager::read_persisted_sections_source(
&self.image(),
&[m],
BaseAddress::ZERO,
os,
)
.map(|(sections, _)| sections) else {
continue;
};
for s in sections {
let ty = PagedEdit::slot_list(slot, s.addr, s.size, page_size);
tagged.push((s, ty));
}
}
tagged.sort_unstable_by_key(|(s, _)| s.addr);
let mut prev_end = 0u64;
for (s, ty) in tagged {
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;
let pg = self
.paged
.as_mut()
.expect("the paged state was just installed");
match ty {
Some(ty) => PagedEdit::route_free(
&mut pg.meta,
&mut pg.raw,
&mut pg.dead,
s.addr,
s.size,
ty.into(),
),
None => pg.unclassified.free(s.addr, s.size),
}
}
let pg = self
.paged
.as_mut()
.expect("the paged state was just installed");
PagedEdit::promote_whole_free_pages(&mut pg.meta, &mut pg.raw, &mut pg.dead, page_size);
} else if let Ok(mut sections) = free_space_manager::read_persisted_sections_source(
&self.image(),
&info.manager_addrs,
BaseAddress::ZERO,
os,
)
.map(|(sections, _)| sections)
{
sections.sort_unstable_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(hdr_len) = fshd_len(os).to_usize() else {
continue;
};
let Ok(fshd) = self.image().read_metadata_at(m, hdr_len) else {
continue;
};
if let Ok(h) = FsmHeader::parse(&fshd, 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 <= file_len)
{
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_from_source(&self.image(), ext_addr as u64, os, ls, base).ok()?;
let msg = oh
.messages
.iter()
.find(|m| m.msg_type == MessageType::FileSpaceInfo)?;
FileSpaceInfo::parse(&msg.data, os, ls).ok()
}
pub(crate) fn stage_created_dataset(
&mut self,
path: &str,
mut builder: DatasetBuilder,
) -> Result<(), Error> {
let mut comps = split_path(path);
self.refuse_if_claimed(&comps)?;
self.refuse_creation_collision(&comps, StagedKind::Dataset)?;
builder.name = comps.pop().unwrap_or_default();
self.staged.push_dataset(comps, flatten_dataset(builder)?);
Ok(())
}
pub(crate) fn stage_dataset_write(
&mut self,
path: &str,
mut builder: DatasetBuilder,
) -> Result<(), Error> {
self.refuse_if_claimed(&split_path(path))?;
let comps = split_path(path);
let Some(leaf) = comps.last() else {
return Err(Error::EditUnsupported("cannot overwrite the root group"));
};
builder.name = leaf.clone();
let fd = flatten_dataset(builder)?;
Self::refuse_unsupported_overwrite(&fd)?;
self.staged.writes.push((comps, fd));
Ok(())
}
pub(crate) fn stage_dataset_append(
&mut self,
path: &str,
builder: AppendBuilder,
) -> Result<(), Error> {
let comps = split_path(path);
self.refuse_if_claimed(&comps)?;
if self.staged.dataset_at(&comps).is_some() {
return Err(Error::EditUnsupported(
"a dataset is staged at this path in the same commit, so an append through a \
handle onto the object the file holds there could only grow the object being \
replaced; append through the handle that staged the creation, or commit first",
));
}
self.refuse_lossy_partial_tail(path, &builder)?;
self.staged.appends.push((comps, builder));
Ok(())
}
fn refuse_lossy_partial_tail(&self, path: &str, builder: &AppendBuilder) -> Result<(), Error> {
if builder.raw().is_empty() {
return Ok(());
}
if self.appends_onto_a_lossy_partial_tail(path) {
return Err(Error::AppendUnsupported(LOSSY_TAIL_REFUSAL));
}
Ok(())
}
fn appends_onto_a_lossy_partial_tail(&self, path: &str) -> bool {
let Ok(addr) =
crate::group_v2::resolve_path_any_from_source(&self.image(), &self.superblock, path)
else {
return false;
};
let Ok(region) =
Self::gather_oh_messages(&self.image(), addr, self.superblock.base_address)
else {
return false;
};
let mut datatype: Option<(usize, usize)> = None;
let mut dataspace: Option<(usize, usize)> = None;
let mut layout: Option<(usize, usize)> = None;
let mut filter: Option<(usize, usize)> = None;
let mut p = 0;
while let Ok(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 => filter = Some((body, body_end)),
_ => {}
}
p = body_end;
}
let (Some((fb, fe)), Some((dt_b, dt_e)), Some((ds_b, ds_e)), Some((lb, le))) =
(filter, datatype, dataspace, layout)
else {
return false;
};
let Ok(pipeline) = FilterPipeline::parse(®ion[fb..fe]) else {
return false;
};
if pipeline_lossless(&pipeline) {
return false;
}
let Ok((disk_dt, _)) = Datatype::parse(®ion[dt_b..dt_e]) else {
return false;
};
let Ok(disk_ds) = Dataspace::parse(®ion[ds_b..ds_e], LENGTH_SIZE) else {
return false;
};
let Ok(dl) = DataLayout::parse(®ion[lb..le], OFFSET_SIZE, LENGTH_SIZE) else {
return false;
};
if disk_ds.dimensions.len() != 1 {
return false;
}
let Ok(geometry) = chunked_geometry(&disk_dt, &disk_ds, &dl) else {
return false;
};
match (geometry.spatial.first(), disk_ds.dimensions.first()) {
(Some(&chunk_elems), Some(&dim0)) if chunk_elems != 0 => dim0 % chunk_elems != 0,
_ => false,
}
}
pub(crate) fn stage_dataset_append_pending(
&mut self,
path: &str,
builder: AppendBuilder,
) -> Result<(), Error> {
let comps = split_path(path);
self.refuse_if_claimed(&comps)?;
if self.staged.dataset_at(&comps).is_none() {
self.refuse_lossy_partial_tail(path, &builder)?;
self.staged.appends.push((comps, builder));
return Ok(());
}
self.refuse_mid_batch()?;
self.extend_staged_dataset(&comps, &builder)
}
fn refuse_mid_batch(&self) -> Result<(), Error> {
if self.staging_batch {
return Err(Error::EditUnsupported(
"an edit that changes or withdraws an already-staged object cannot be made \
inside an atomic staging batch, whose undo drops additions only",
));
}
Ok(())
}
fn extend_staged_dataset(
&mut self,
comps: &[String],
builder: &AppendBuilder,
) -> Result<(), Error> {
if builder.dt_conflict() {
return Err(Error::AppendUnsupported(
"this append mixes element datatypes; use one element type per append",
));
}
let fd = self
.staged
.dataset_at_mut(comps)
.expect("caller checked a dataset is staged at this path");
if fd.vl_string_staging.is_some() || fd.reference_targets.is_some() {
return Err(Error::AppendUnsupported(
"a staged variable-length-string or object-reference dataset cannot be appended \
to before it is committed: its per-element side table is built with the \
dataset. Supply every element through the builder that creates it",
));
}
if let Some(dt) = builder.elem_dt() {
if *dt != fd.dt {
return Err(Error::AppendUnsupported(
"appended element datatype does not match the staged dataset's datatype",
));
}
}
let Some((lead, inner)) = fd.ds.dimensions.split_first() else {
return Err(Error::AppendUnsupported(
"a scalar dataset has no dimension to append along",
));
};
let mut row_bytes = u64::from(fd.dt.type_size());
for &d in inner {
row_bytes = row_bytes.checked_mul(d).ok_or(Error::AppendUnsupported(
"staged dataset row size overflows",
))?;
}
if row_bytes == 0 {
return Err(Error::AppendUnsupported(
"a staged dataset whose rows hold no bytes cannot be appended to",
));
}
let bytes = builder.raw();
if bytes.len() as u64 % row_bytes != 0 {
return Err(Error::AppendUnsupported(
"appended byte length is not a whole number of rows of the staged dataset",
));
}
let grown = lead
.checked_add(bytes.len() as u64 / row_bytes)
.ok_or(Error::AppendUnsupported("staged dataset length overflows"))?;
if let Some(max) = fd
.ds
.max_dimensions
.as_ref()
.and_then(|m| m.first().copied())
{
if max != u64::MAX && grown > max {
return Err(Error::AppendUnsupported(
"appending would grow the staged dataset past its maximum shape",
));
}
}
fd.raw.extend_from_slice(bytes);
fd.ds.dimensions[0] = grown;
#[cfg(feature = "provenance")]
fd.rebuild_provenance();
Ok(())
}
pub(crate) fn claim_for_appender(&mut self, path: Option<&str>) -> Result<u64, Error> {
let path = path.map(split_path);
if self
.appender_claims
.iter()
.any(|c| claims_conflict(c.path.as_deref(), path.as_deref()))
{
return Err(Error::EditUnsupported(
"this dataset already has a live buffered appender; two of them would interleave \
their buffers a chunk at a time",
));
}
let blocked = match path.as_deref() {
Some(p) => self.append_conflicts_with_pending(p),
None => self.has_staged_edits() || self.committed,
};
if blocked {
return Err(Error::EditUnsupported(
"this session holds staged edits that would stop a buffered appender from \
flushing; commit or discard them before opening one",
));
}
let token = self.next_appender_token;
self.next_appender_token += 1;
self.appender_claims.push(AppenderClaim { token, path });
Ok(token)
}
pub(crate) fn release_appender_claim(&mut self, token: u64) {
self.appender_claims.retain(|c| c.token != token);
}
fn refuse_if_claimed(&self, path: &[String]) -> Result<(), Error> {
let conflicts = self.appender_claims.iter().any(|c| match &c.path {
Some(p) => paths_overlap(p, path),
None => true,
});
if conflicts {
return Err(Error::EditUnsupported(
"this dataset has a live buffered appender holding elements only its own flush \
can write, and this edit would stop that flush; finish or discard the appender \
first",
));
}
Ok(())
}
pub(crate) fn is_swmr(&self) -> bool {
self.swmr_mode
}
pub fn has_staged_edits(&self) -> bool {
!self.staged.is_empty()
}
pub(crate) fn stage_atomically<R>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<R, Error>,
) -> Result<R, Error> {
let mark = self.staged.mark();
let outer = std::mem::replace(&mut self.staging_batch, true);
let result = f(self);
self.staging_batch = outer;
if result.is_err() {
self.staged.rewind(mark);
}
result
}
pub(crate) fn image_slice(&self) -> Option<&[u8]> {
self.image.as_slice()
}
pub(crate) fn image(&self) -> &dyn Source {
self.image.as_ref()
}
pub(crate) fn superblock(&self) -> &Superblock {
&self.superblock
}
pub(crate) fn libver(&self) -> LibVer {
LibVer::from_superblock_version(self.superblock.version)
}
pub(crate) fn set_libver_bounds(
&mut self,
bounds: Option<(LibVer, LibVer)>,
) -> Result<(), Error> {
self.libver_ceiling = match bounds {
Some(_) => Some(LibVer::resolve_writable(bounds).map_err(Error::Format)?),
None => None,
};
Ok(())
}
fn check_libver_admits<'a>(
&self,
datasets: impl IntoIterator<Item = &'a FlatDataset>,
) -> Result<(), Error> {
let Some(ceiling) = self.libver_ceiling else {
return Ok(());
};
if ceiling >= LibVer::V110 {
return Ok(());
}
let chunked = datasets
.into_iter()
.any(|fd| fd.chunk_options.is_chunked() || fd.maxshape.is_some());
if chunked {
return Err(Error::Format(FormatError::LibverTooOldForContent {
content: "a chunked, filtered, or resizable dataset",
needs: LibVer::V110.name(),
writing: ceiling.name(),
}));
}
Ok(())
}
pub(crate) fn edit_backing(&self) -> EditBacking {
if self.bounded {
EditBacking::Bounded
} else {
EditBacking::Mirrored
}
}
#[must_use]
pub fn space_accounting(&self) -> SpaceAccounting {
let reusable_free_space = match (&self.paged, self.reserved.is_empty()) {
(Some(pg), true) => pg.reusable_sections(),
(None, true) => self.free.sections(),
(Some(pg), false) => {
let mut raw = pg.raw.clone();
for (addr, len) in self.reserved.sections() {
raw.free(addr, len);
}
let mut out = pg.meta.sections();
out.extend(raw.sections());
out.sort_unstable_by_key(|&(addr, _)| addr);
debug_assert!(
out.windows(2).all(|w| w[0].0 < w[1].0),
"the same address is free in both the metadata and the raw \
list, so the two page-type lists have stopped being disjoint"
);
out
}
(None, false) => {
let mut free = self.free.clone();
for (addr, len) in self.reserved.sections() {
free.free(addr, len);
}
free.sections()
}
};
let reusable_free_bytes = reusable_free_space.iter().map(|(_, len)| len).sum();
SpaceAccounting {
logical_size: self.image.len(),
reusable_free_bytes,
reusable_free_space,
}
}
fn store(&mut self) -> EditStore<'_> {
EditStore {
image: self.image.as_mut(),
superblock: &mut self.superblock,
sb_sig_off: self.sb_sig_off,
paged: self.paged.as_mut(),
free: None,
sync_policy: self.sync_policy,
}
}
fn immediate_reuse_allowed(&self) -> bool {
!self.swmr_mode
}
fn reserve_for_immediate_append(&mut self, want: u64) -> Result<bool, Error> {
if want == 0 || self.persist.is_none() || self.swmr_mode {
return Ok(false);
}
if self.reserved.largest() >= want {
return Ok(true);
}
if !self.take_raw_span(want.max(APPEND_RESERVE_BYTES)) {
return Ok(false);
}
match self.commit_persisting(self.superblock.root_group_address, Vec::new()) {
Ok(()) => Ok(true),
Err(e) => {
self.release_reserve();
Err(e)
}
}
}
fn take_raw_span(&mut self, len: u64) -> bool {
let addr = match self.paged.as_mut() {
Some(pg) => pg.alloc_typed(len, PageType::Raw),
None => self.free.alloc(len),
};
match addr {
Some(addr) => {
self.reserved.free(addr, len);
true
}
None => false,
}
}
fn release_reserve(&mut self) {
if self.reserved.is_empty() {
return;
}
let spans = std::mem::replace(&mut self.reserved, FreeList::new()).sections();
match self.paged.as_mut() {
Some(pg) => {
for (addr, len) in spans {
pg.raw.free(addr, len);
}
}
None => {
for (addr, len) in spans {
self.free.free(addr, len);
}
}
}
}
fn commit_persisting_releasing_reserve(
&mut self,
new_root: u64,
to_free: Vec<(u64, u64, FreeClass)>,
) -> Result<(), Error> {
self.release_reserve();
self.commit_persisting(new_root, to_free)
}
pub(crate) fn set_sync_policy(&mut self, policy: SyncPolicy) {
self.sync_policy = policy;
}
#[cfg(test)]
pub(crate) fn sync_policy(&self) -> SyncPolicy {
self.sync_policy
}
#[cfg(test)]
pub(crate) fn set_write_buffering(&mut self, mode: WriteBuffering) -> Result<(), Error> {
self.image.set_write_buffering(mode)
}
#[cfg(test)]
pub(crate) fn issued_write_order(&self) -> Vec<(u64, u64)> {
self.image.issued_write_order()
}
fn barrier(&mut self) -> Result<(), Error> {
match self.sync_policy {
SyncPolicy::Always => self.image.sync_all(),
SyncPolicy::OnClose => self.image.ordering_barrier(),
}
}
fn barrier_data(&mut self) -> Result<(), Error> {
barrier_data(self.image.as_mut(), self.sync_policy)
}
pub(crate) fn force_sync(&mut self) -> Result<(), Error> {
self.image.sync_all()
}
pub(crate) fn finalize_persist(&mut self) -> Result<(), Error> {
if self.persist.is_none() || (self.image.len() == self.fsm_len && self.reserved.is_empty())
{
return Ok(());
}
self.commit_persisting_releasing_reserve(self.superblock.root_group_address, Vec::new())
}
fn append_prepare(&mut self, target: AppendTarget<'_>) -> Result<u64, Error> {
if !self.superblock.base_address.is_zero() {
return Err(Error::AppendInPlaceUnsupported(
"in-place append does not support a file with a userblock (non-zero base \
address); use Dataset::append_staged",
));
}
if self.superblock.version < 2 {
return Err(Error::AppendInPlaceUnsupported(
"in-place append requires a latest-format file (v2/v3 superblock); use \
Dataset::append_staged",
));
}
if self.paged.is_some() && self.persist.is_none() {
return Err(Error::AppendInPlaceUnsupported(
"in-place append is not supported on a paged file \
(H5F_FSPACE_STRATEGY_PAGE) without persisted free space; recreate the \
file with with_file_space_strategy(FileSpaceStrategy::Page, true, ..)",
));
}
match target {
AppendTarget::Path(dataset) => {
if self.append_conflicts_with_pending(&split_path(dataset)) {
return Err(Error::AppendInPlaceUnsupported(
"the dataset or an ancestor has a staged edit pending in this session; \
commit the staged edits before appending in place, or use \
Dataset::append_staged",
));
}
}
AppendTarget::Header(_) if self.has_staged_edits() || self.committed => {
return Err(Error::AppendInPlaceUnsupported(
"this append target was reached by object reference, so it names a dataset \
by object-header address, and this session has staged or committed edits \
that can move that header; re-open the dataset by path to append to it",
));
}
AppendTarget::Header(_) => {}
}
let oh_addr = match target {
AppendTarget::Path(dataset) => match self.resolved.get(dataset) {
Some(&addr) => addr,
None => {
let addr = crate::group_v2::resolve_path_any_from_source(
&self.image(),
&self.superblock,
dataset,
)
.map_err(|_| {
Error::AppendInPlaceUnsupported("nothing to append to at the given path")
})?;
self.resolved.insert(dataset.to_string(), addr);
addr
}
},
AppendTarget::Header(addr) => addr,
};
if !self.located.contains_key(&oh_addr) {
let store = self.store();
let state = locate_dataset_state(&store, oh_addr)?;
self.located.insert(oh_addr, state);
}
Ok(oh_addr)
}
pub(crate) fn append_geometry(
&mut self,
target: AppendTarget<'_>,
) -> Result<AppendGeometry, Error> {
let oh_addr = self.append_prepare(target)?;
let st = &self.located[&oh_addr];
let chunk_elems = st.loc.chunk_elems.max(1);
Ok(AppendGeometry {
chunk_elems,
element_size: st.element_size,
current_dim: st.loc.current_dim,
lossy_filters: st.pipeline.as_ref().is_some_and(|p| !pipeline_lossless(p)),
full_batch_elems: self.batch_elems(st.loc.chunk_bytes, chunk_elems),
})
}
fn batch_elems(&self, chunk_bytes: usize, chunk_elems: u64) -> u64 {
if !self.batched_appends {
return u64::MAX;
}
(APPEND_BATCH_BYTES / (chunk_bytes.max(1) as u64)).max(1) * chunk_elems
}
pub(crate) fn append_inplace_gathered(
&mut self,
target: AppendTarget<'_>,
b: &AppendBuilder,
max_phase: u8,
) -> Result<(), Error> {
if b.dt_conflict() {
return Err(Error::AppendInPlaceUnsupported(
"append mixes element types in one call; use one element type per append",
));
}
let oh_addr = self.append_prepare(target)?;
let raw = b.raw();
let new_elems = validate_gathered_append(&self.located[&oh_addr], b)?;
if new_elems == 0 {
return Ok(());
}
if self.swmr_mode {
let st = &self.located[&oh_addr];
if st.pipeline.is_some() {
return Err(Error::SwmrAppendUnsupported(
"filtered datasets are not supported for SWMR append",
));
}
let chunk_elems = st.loc.chunk_elems;
if chunk_elems == 0
|| st.loc.current_dim % chunk_elems != 0
|| new_elems % chunk_elems != 0
{
return Err(Error::SwmrAppendUnsupported(
"SWMR append must be chunk-aligned: the current length and the appended \
length must both be whole multiples of the chunk length",
));
}
}
let (chunk_elems, elem_bytes, full_batch_elems) = {
let st = &self.located[&oh_addr];
(
st.loc.chunk_elems.max(1),
st.element_size.get() as u64,
self.batch_elems(st.loc.chunk_bytes, st.loc.chunk_elems.max(1)),
)
};
let grow_visible_tail = !self.swmr_mode;
let mut done = 0u64;
while done < new_elems {
let current_dim = self.located[&oh_addr].loc.current_dim;
let to_boundary = (chunk_elems - current_dim % chunk_elems) % chunk_elems;
let take = (new_elems - done).min(to_boundary.saturating_add(full_batch_elems));
let batch =
&raw[(done * elem_bytes).to_usize()?..((done + take) * elem_bytes).to_usize()?];
let plan_result = {
let Self {
image,
superblock,
sb_sig_off,
paged,
located,
sync_policy,
..
} = self;
let st = &located[&oh_addr];
let store = EditStore {
image: image.as_mut(),
superblock,
sb_sig_off: *sb_sig_off,
paged: paged.as_mut(),
free: None,
sync_policy: *sync_policy,
};
plan_ea_append(
&store,
&st.loc,
&st.datatype,
&st.spatial,
st.element_size,
st.pipeline.as_ref(),
grow_visible_tail,
batch,
take,
st.fill.pattern(st.element_size),
)
};
let plan = plan_result.map_err(as_inplace_error)?;
let largest_blob = plan
.new_chunk_bytes
.iter()
.map(|b| b.len() as u64)
.max()
.unwrap_or(0);
self.reserve_for_immediate_append(largest_blob)?;
{
let reuse = self.immediate_reuse_allowed();
let from_reserve = self.persist.is_some() || self.paged.is_some();
let Self {
image,
superblock,
sb_sig_off,
paged,
located,
free,
reserved,
sync_policy,
..
} = self;
let st = located.get_mut(&oh_addr).expect("dataset located above");
let mut store = EditStore {
image: image.as_mut(),
superblock,
sb_sig_off: *sb_sig_off,
paged: paged.as_mut(),
free: reuse.then_some(if from_reserve { reserved } else { free }),
sync_policy: *sync_policy,
};
apply_ea_append(&mut store, &mut st.loc, &plan, max_phase)
.map_err(as_inplace_error)?;
}
if max_phase < 4 {
return Ok(());
}
done += take;
}
Ok(())
}
#[cfg(test)]
fn append_inplace_i32_phased(
&mut self,
dataset: &str,
values: &[i32],
max_phase: u8,
) -> Result<(), Error> {
let mut b = AppendBuilder::new();
b.append_i32(values);
self.append_inplace_gathered(AppendTarget::Path(dataset), &b, max_phase)
}
fn append_conflicts_with_pending(&self, target: &[String]) -> bool {
let hits = |p: &[String]| paths_overlap(target, p);
self.staged.writes.iter().any(|(p, _)| hits(p))
|| self.staged.appends.iter().any(|(p, _)| hits(p))
|| self.staged.deletes.iter().any(|p| hits(p))
|| self.staged.copies.iter().any(|(_, dst)| hits(dst))
|| self.staged.cross_copies.iter().any(|(dst, _)| hits(dst))
|| self.staged.dataset_attrs.iter().any(|(p, _)| hits(p))
|| self.staged.datasets.iter().any(|(parent, fd)| {
let mut full = parent.clone();
full.push(fd.name.clone());
paths_overlap(target, &full)
})
}
pub(crate) fn staged_object(&self, path: &str) -> Option<StagedObject> {
if self.stages_no_creations() {
return None;
}
let comps = split_path(path);
if comps.is_empty() {
return None;
}
let kind = if self.staged.dataset_at(&comps).is_some() {
StagedKind::Dataset
} else if self.staged.has_group_at(&comps) {
StagedKind::Group
} else {
return None;
};
let replaces_link = self.staged.deletes_hand_over(&comps);
if !replaces_link && self.path_in_file(&comps) {
return None;
}
Some(StagedObject {
kind,
replaces_link,
})
}
fn refuse_creation_collision(&self, comps: &[String], kind: StagedKind) -> Result<(), Error> {
if comps.is_empty() {
return Ok(());
}
if self.staged.dataset_at(comps).is_some()
|| (kind == StagedKind::Dataset && self.staged.has_group_at(comps))
{
return Err(Error::EditUnsupported(
"this session already stages an object with this name in the target group; \
delete it to withdraw that staging before staging another in its place",
));
}
if self.staged.deletes_hand_over(comps) || !self.path_in_file(comps) {
return Ok(());
}
Err(Error::EditUnsupported(
"a link with this name already exists in the target group",
))
}
fn path_in_file(&self, path: &[String]) -> bool {
let joined = path.join("/");
match self.image.as_slice() {
Some(data) => {
crate::group_v2::resolve_path_any(data, &self.superblock, &joined).is_ok()
}
None => crate::group_v2::resolve_path_any_from_source(
&self.image(),
&self.superblock,
&joined,
)
.is_ok(),
}
}
fn stages_no_creations(&self) -> bool {
self.staged.groups.is_empty() && self.staged.datasets.is_empty()
}
pub(crate) fn staged_generation(&self) -> u64 {
self.staged_generation
}
pub(crate) fn staged_dataset_meta(&self, path: &str) -> Option<StagedMeta> {
if self.staged_object(path)?.kind != StagedKind::Dataset {
return None;
}
let fd = self.staged.dataset_at(&split_path(path))?;
Some(StagedMeta {
datatype: fd.dt.clone(),
dimensions: fd.ds.dimensions.clone(),
maxshape: match &fd.ds.max_dimensions {
Some(md) if *md != fd.ds.dimensions => Some(md.clone()),
_ => None,
},
chunked: fd.chunk_options.is_chunked() || fd.maxshape.is_some(),
filters: fd
.chunk_options
.filters
.iter()
.map(|f| (f.kind.filter_id(), f.optional))
.collect(),
})
}
pub(crate) fn staged_children(&self, parent: &str) -> Vec<StagedChild> {
if self.stages_no_creations() {
return Vec::new();
}
let base = split_path(parent);
let base_deleted = self.staged.deletes_hand_over(&base) && self.staged.has_group_at(&base);
let deleted_here: HashSet<&str> = self
.staged
.deletes
.iter()
.filter(|d| d.len() == base.len() + 1 && d[..base.len()] == base[..])
.map(|d| d[base.len()].as_str())
.collect();
let mut seen: HashSet<&str> = HashSet::new();
let mut out: Vec<StagedChild> = Vec::new();
for path in &self.staged.groups {
if path.len() == base.len() + 1 && path[..base.len()] == base[..] {
let name = path[base.len()].as_str();
if seen.insert(name) {
out.push(StagedChild {
name: name.to_string(),
kind: StagedKind::Group,
replaces_link: base_deleted || deleted_here.contains(name),
});
}
}
}
for (p, fd) in &self.staged.datasets {
if p[..] == base[..] && seen.insert(fd.name.as_str()) {
out.push(StagedChild {
name: fd.name.clone(),
kind: StagedKind::Dataset,
replaces_link: base_deleted || deleted_here.contains(fd.name.as_str()),
});
}
}
out
}
pub fn create_group(&mut self, path: &str) -> Result<(), Error> {
let comps = split_path(path);
self.refuse_if_claimed(&comps)?;
self.refuse_creation_collision(&comps, StagedKind::Group)?;
self.staged.push_group(comps);
Ok(())
}
pub fn set_group_attr(
&mut self,
path: &str,
name: &str,
value: AttrValue,
) -> Result<(), Error> {
let comps = split_path(path);
self.refuse_if_claimed(&comps)?;
self.staged.group_attrs.push((
comps,
AttrOp::Set {
name: name.to_string(),
value,
},
));
Ok(())
}
pub fn remove_group_attr(&mut self, path: &str, name: &str) -> Result<(), Error> {
let comps = split_path(path);
self.refuse_if_claimed(&comps)?;
self.staged.group_attrs.push((
comps,
AttrOp::Remove {
name: name.to_string(),
},
));
Ok(())
}
pub fn set_dataset_attr(
&mut self,
path: &str,
name: &str,
value: AttrValue,
) -> Result<(), Error> {
let comps = split_path(path);
self.refuse_if_claimed(&comps)?;
self.staged.dataset_attrs.push((
comps,
AttrOp::Set {
name: name.to_string(),
value,
},
));
Ok(())
}
pub fn remove_dataset_attr(&mut self, path: &str, name: &str) -> Result<(), Error> {
let comps = split_path(path);
self.refuse_if_claimed(&comps)?;
self.staged.dataset_attrs.push((
comps,
AttrOp::Remove {
name: name.to_string(),
},
));
Ok(())
}
pub fn delete(&mut self, path: &str) -> Result<(), Error> {
let comps = split_path(path);
if comps.is_empty() {
return Err(Error::EditUnsupported(
"cannot delete the root group; delete its members instead",
));
}
self.refuse_if_claimed(&comps)?;
if self.staged.dataset_at(&comps).is_some() || self.staged.has_group_at(&comps) {
self.refuse_mid_batch()?;
self.staged.withdraw_at(&comps);
if self.staged.deletes_cover(&comps) || !self.path_in_file(&comps) {
return Ok(());
}
}
self.staged.deletes.push(comps);
Ok(())
}
pub fn copy(&mut self, src: &str, dst: &str) -> Result<(), Error> {
let (s, d) = (split_path(src), split_path(dst));
self.refuse_if_claimed(&s)?;
self.refuse_if_claimed(&d)?;
self.staged.copies.push((s, d));
Ok(())
}
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().is_zero() {
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 tree = Self::read_copy_subtree(
&BytesSource::new(src_data),
src_addr,
0,
true,
BaseAddress::ZERO,
)?;
self.refuse_if_claimed(&dst)?;
self.staged.cross_copies.push((dst, tree));
Ok(())
}
pub fn commit(&mut self) -> Result<(), Error> {
let snapshot = self.snapshot_free();
self.publish_attempted = false;
self.inplace_undo.clear();
let mut staged = std::mem::take(&mut self.staged);
let mut result = self.commit_inner(&mut staged);
if result.is_err() && !self.publish_attempted {
let restored = self.undo_inplace_writes();
self.restore_free(snapshot);
self.staged = staged;
if let Err(restore) = restored {
let refusal = result.expect_err("only a failed attempt is rolled back");
result = Err(Error::CommitPartiallyApplied {
refusal: Box::new(refusal),
restore: Box::new(restore),
});
}
} else {
self.staged_generation += 1;
}
self.inplace_undo.clear();
result
}
fn snapshot_free(&self) -> FreeSnapshot {
FreeSnapshot {
free: self.free.clone(),
reserved: self.reserved.clone(),
paged: self
.paged
.as_ref()
.map(|pg| (pg.meta.clone(), pg.raw.clone())),
vl_overwrite_heaps: self.vl_overwrite_heaps.clone(),
}
}
fn restore_free(&mut self, snapshot: FreeSnapshot) {
self.vl_overwrite_heaps = snapshot.vl_overwrite_heaps;
self.free = snapshot.free;
self.reserved = snapshot.reserved;
if let (Some(pg), Some((meta, raw))) = (self.paged.as_mut(), snapshot.paged) {
pg.meta = meta;
pg.raw = raw;
}
}
fn write_inplace_journaled(&mut self, at: usize, raw: &[u8]) -> Result<(), Error> {
let prior = self.image().read_exact_at(at as u64, raw.len())?;
self.inplace_undo.push((at, prior));
self.write_at(at, raw)
}
fn undo_inplace_writes(&mut self) -> Result<(), Error> {
let journal = core::mem::take(&mut self.inplace_undo);
if journal.is_empty() {
return Ok(());
}
let mut failed = None;
for (at, prior) in journal.into_iter().rev() {
if let Err(e) = self.write_at(at, &prior) {
failed.get_or_insert(e);
}
}
match failed {
Some(e) => Err(e),
None => self.barrier_data(),
}
}
fn commit_inner(&mut self, staged: &mut StagedEdits) -> Result<(), Error> {
if staged.is_empty() {
return Ok(());
}
self.superseded_heaps.clear();
self.invalidate_heap_provenance(staged);
if self.paged.is_some() && self.persist.is_none() {
return Err(Error::EditUnsupported(
"committing an edit to a paged file (H5F_FSPACE_STRATEGY_PAGE) requires \
persisted free space; recreate the file with \
with_file_space_strategy(FileSpaceStrategy::Page, true, ..) to edit it in place",
));
}
self.located.clear();
self.resolved.clear();
self.committed = true;
let base = self.superblock.base_address;
let mut inplace_writes: Vec<(usize, OverwriteBytes)> = Vec::new();
let mut moving_writes: Vec<(PathKey, String, u64, MovingWrite)> = Vec::new();
let mut write_targets: Vec<PathKey> = Vec::new();
let mut incoming_links: Option<Option<HashMap<u64, u32>>> = None;
for (full, fd) in &staged.writes {
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_from_source(
&self.image(),
&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"))?;
match Self::prepare_write(&self.image(), addr as u64, fd, base, full)? {
WritePlan::InPlace { data_addr, bytes } => {
inplace_writes.push((data_addr, bytes));
}
WritePlan::InPlaceChunks { writes } => inplace_writes.extend(
writes
.into_iter()
.map(|(at, raw)| (at, OverwriteBytes::ready(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, addr as u64, mw));
}
}
write_targets.push(full.clone());
}
for (full, ab) in &staged.appends {
if full.is_empty() {
return Err(Error::AppendUnsupported("cannot append to the root group"));
}
if ab.raw.is_empty() {
continue; }
if write_targets.contains(full) {
return Err(Error::AppendUnsupported(
"the same dataset is edited more than once in one commit; use separate commits",
));
}
let path_str = full.join("/");
let addr = crate::group_v2::resolve_path_any_from_source(
&self.image(),
&self.superblock,
&path_str,
)
.map_err(|_| Error::AppendUnsupported("nothing to append to at the given path"))?;
let addr = usize::try_from(addr)
.map_err(|_| Error::AppendUnsupported("dataset address exceeds this platform"))?;
let mw = Self::prepare_append(&self.image(), addr as u64, ab, base)?;
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::AppendUnsupported(
"appending relocates the dataset header; 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, addr as u64, mw));
write_targets.push(full.clone());
}
if !staged.dataset_attrs.is_empty() {
let mut order: Vec<PathKey> = Vec::new();
let mut ops_by_path: HashMap<&PathKey, Vec<&AttrOp>> = HashMap::new();
for (path, op) in &staged.dataset_attrs {
if !ops_by_path.contains_key(path) {
order.push(path.clone());
}
ops_by_path.entry(path).or_default().push(op);
}
for full in order {
let ops = ops_by_path.remove(&full).unwrap();
if full.is_empty() {
return Err(Error::EditUnsupported(
"cannot set a dataset attribute on the root group; use set_group_attr",
));
}
if write_targets.contains(&full) {
return Err(Error::EditUnsupported(
"the same dataset is edited more than once in one commit (an attribute \
edit plus another edit); use separate commits",
));
}
let path_str = full.join("/");
let addr = crate::group_v2::resolve_path_any_from_source(
&self.image(),
&self.superblock,
&path_str,
)
.map_err(|_| {
Error::EditUnsupported("nothing to set an attribute on at the given path")
})?;
let addr = usize::try_from(addr)
.map_err(|_| Error::EditUnsupported("dataset address exceeds this platform"))?;
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(
"editing a dataset attribute relocates its header; only supported \
when it has a single hard link",
));
}
}
let region = Self::gather_oh_messages(&self.image(), addr as u64, base)?;
let edits = plan_attr_ops(&self.image(), base, Some(addr as u64), ®ion, &ops)?;
let leaf = full.last().unwrap().clone();
let parent = full[..full.len() - 1].to_vec();
moving_writes.push((
parent,
leaf,
addr as u64,
MovingWrite::AttrEdit {
region: edits.region,
attrs: edits.attrs,
},
));
write_targets.push(full);
}
}
let mut moved_headers: Vec<u64> =
moving_writes.iter().map(|&(_, _, addr, _)| addr).collect();
debug_assert!(
inplace_writes.iter().all(|(_, b)| b.vlen.is_none()),
"a staged variable-length overwrite must relocate, not write in place"
);
if moving_writes.is_empty()
&& staged.datasets.is_empty()
&& staged.groups.is_empty()
&& staged.group_attrs.is_empty()
&& staged.deletes.is_empty()
&& staged.copies.is_empty()
&& staged.cross_copies.is_empty()
{
drop(std::mem::take(staged));
for (data_addr, bytes) in &inplace_writes {
let raw = self.resolve_overwrite_bytes(bytes)?;
self.write_inplace_journaled(*data_addr, &raw)?;
}
self.barrier()?;
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 copy_sources: Vec<PathKey> = Vec::new();
let mut attr_targets: Vec<PathKey> = Vec::new();
for path in &staged.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.clone());
}
for (parent, fd) in &staged.datasets {
let mut full = parent.clone();
full.push(fd.name.clone());
add_targets.push(full);
ensure_ancestors(&mut nodes, parent);
}
for (parent, leaf, old_oh, mw) in moving_writes {
ensure_ancestors(&mut nodes, &parent);
nodes
.entry(parent)
.or_default()
.writes
.push((leaf, old_oh, mw));
}
for (path, _) in &staged.group_attrs {
ensure_ancestors(&mut nodes, path);
attr_targets.push(path.clone());
}
for (src, dst) in &staged.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_from_source(
&self.image(),
&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.image(), src_addr as u64, 0, false, base)?;
copy_sources.push(src.clone());
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, _) in &staged.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().cross_copies.push(leaf);
}
let delete_targets = &staged.deletes;
let mut deleted_addrs: Vec<usize> = Vec::new();
for (i, d) in delete_targets.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_from_source(
&self.image(),
&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);
}
let recreated = add_targets.iter().any(|t| t == d);
if recreated
&& !nodes
.iter()
.all(|(key, node)| !is_prefix(d, key) || node.is_new)
{
return Err(Error::EditUnsupported(
"a staged edit names a group at or under a replaced path that this \
commit does not itself create; create it in the same commit, or use \
separate commits",
));
}
if recreated {
for t in ©_sources {
if is_prefix(d, t) {
return Err(Error::EditUnsupported(
"a copy in this commit reads from a path the same commit \
replaces; use separate commits",
));
}
}
}
for t in &add_targets {
if recreated && is_prefix(d, t) {
continue;
}
if is_prefix(d, t) || is_prefix(t, d) {
return Err(Error::EditUnsupported(
"a deletion overlaps an addition in the same commit; \
replace the path instead, or use separate commits",
));
}
}
for t in &attr_targets {
if recreated && is_prefix(d, t) {
continue;
}
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 staged edit to a dataset in the same \
commit; use separate commits",
));
}
}
for (j, d2) in delete_targets.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<(PathKey, 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_from_source(
&self.image(),
&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"))?;
if !key.is_empty() {
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)).copied()) {
Some(1) => {}
Some(_) => {
return Err(Error::EditUnsupported(
"editing a group relocates its object header; only supported \
when it has a single hard link",
));
}
None => {
return Err(Error::EditUnsupported(
"editing a group relocates its object header, and this file's \
links could not be walked to establish that nothing else \
names it",
));
}
}
}
let info = self.inspect_group(addr)?;
superseded_addrs.push((key.clone(), addr));
let node = nodes.get_mut(key).unwrap();
node.base_region = info.region;
node.existing_links = info.link_names;
}
}
moved_headers.extend(superseded_addrs.iter().map(|&(_, a)| a as u64));
let attrs_by_group = group_by_parent(staged.group_attrs.iter().map(|(p, op)| (p, op)));
let existing_group_addrs: HashMap<PathKey, u64> = superseded_addrs
.iter()
.map(|(key, addr)| (key.clone(), *addr as u64))
.collect();
for key in &keys {
if let Some(ops) = attrs_by_group.get(key) {
let region = std::mem::take(&mut nodes.get_mut(key).unwrap().base_region);
let edits = plan_attr_ops(
&self.image(),
base,
existing_group_addrs.get(key).copied(),
®ion,
ops,
)?;
let node = nodes.get_mut(key).unwrap();
node.base_region = edits.region;
node.attrs = edits.attrs;
}
}
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());
}
}
let datasets_by_group = group_by_parent(staged.datasets.iter().map(|(p, fd)| (p, fd)));
for key in &keys {
let node = &nodes[key];
let mut adding: Vec<&str> = Vec::new();
for fd in datasets_by_group.get(key).into_iter().flatten() {
adding.push(&fd.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 leaf in &node.cross_copies {
adding.push(leaf);
}
for (i, name) in adding.iter().enumerate() {
let survives = node.existing_links.iter().any(|n| n == name)
&& !node.deletes.iter().any(|n| n == name);
if survives || adding[..i].contains(name) {
return Err(Error::EditUnsupported(
"a link with this name already exists in the target group",
));
}
}
}
self.check_libver_admits(staged.datasets.iter().map(|(_, fd)| fd))?;
let mut deleted_free: Vec<(u64, u64, FreeClass)> = Vec::new();
deleted_addrs.sort_unstable();
deleted_addrs.dedup();
if !deleted_addrs.is_empty() {
if let Some(incoming) = self.count_incoming_hard_links() {
for &a in &deleted_addrs {
self.collect_free_spans(a, 0, &incoming, &mut deleted_free);
}
}
}
let for_copied = InvalidatedAddresses {
removed: deleted_free
.iter()
.map(|&(off, len, _)| (off, len))
.collect(),
moved: Vec::new(),
base: self.superblock.base_address,
};
{
let image = self.image();
let framed = BaseOffsetSource {
inner: image,
base: self.superblock.base_address,
};
for key in &keys {
for (_, tree) in &nodes[key].copies {
screen_copied_references(tree, &for_copied, &framed)?;
}
}
}
let for_supplied = InvalidatedAddresses {
moved: moved_headers,
..for_copied
};
for (_, fd) in staged.datasets.iter().chain(&staged.writes) {
screen_resolved_references(&fd.dt, &fd.raw, &for_supplied)?;
}
if staged.datasets.iter().chain(&staged.writes).any(|(_, fd)| {
datatype_holds_object_address(&fd.dt)
|| fd
.attrs
.iter()
.any(|a| datatype_holds_object_address(&a.datatype))
}) || !staged.copies.is_empty()
|| !staged.cross_copies.is_empty()
{
self.proved_free_of_references = false;
}
Self::preflight_reference_targets(
&keys,
&datasets_by_group,
&nodes,
&add_targets,
&write_targets,
delete_targets,
&for_supplied,
&self.image(),
&self.superblock,
)?;
let mut taken = std::mem::take(staged);
let delete_targets = std::mem::take(&mut taken.deletes);
let mut flat = group_by_parent(taken.datasets.drain(..));
for (dst, tree) in taken.cross_copies.drain(..) {
let leaf = dst.last().unwrap().clone();
let parent = dst[..dst.len() - 1].to_vec();
nodes.get_mut(&parent).unwrap().copies.push((leaf, tree));
}
let mut to_free: Vec<(u64, u64, FreeClass)> = deleted_free;
for &(_, a) in &superseded_addrs {
if let Ok(spans) = self.oh_chunk_spans(a) {
to_free.extend(meta_spans(spans));
}
}
for key in &keys {
for (_leaf, old_oh, mw) in &nodes[key].writes {
match mw {
MovingWrite::Contiguous {
old_extent: Some(extent),
..
} => to_free.push((extent.0, extent.1, FreeClass::Page(PageType::Raw))),
MovingWrite::Chunked { old_addr, .. } => {
if let Ok(a) = usize::try_from(*old_addr) {
if let Some(spans) = self.chunked_storage_spans(a) {
to_free.extend(spans);
}
}
}
MovingWrite::AppendedChunks {
old_addr,
old_tail_extent,
kept_chunks,
..
} => {
if let Ok(a) = usize::try_from(*old_addr) {
if let Some(spans) = self.chunked_index_spans(a) {
let data: Vec<(u64, u64)> = kept_chunks
.iter()
.filter_map(|c| {
Some((base.absolute(c.address).ok()?, c.compressed_size))
})
.chain(*old_tail_extent)
.collect();
let class = if self.index_is_provably_raw(&data, &spans) {
FreeClass::Page(PageType::Raw)
} else {
FreeClass::Dead
};
to_free.extend(spans.into_iter().map(|(a, l)| (a, l, class)));
}
}
if let Some(ext) = old_tail_extent {
to_free.push((ext.0, ext.1, FreeClass::Page(PageType::Raw)));
}
}
_ => {}
}
if let Ok(a) = usize::try_from(*old_oh) {
if let Ok(spans) = self.oh_chunk_spans(a) {
to_free.extend(meta_spans(spans));
}
}
}
}
retain_disjoint_in_bounds(&mut to_free, self.image.len());
let mut path_addr: BTreeMap<PathKey, u64> = BTreeMap::new();
let mut relocations: BTreeMap<u64, 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, attrs) = {
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),
std::mem::take(&mut node.attrs),
)
};
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, base.relative(root)?));
}
let mut group_datasets: Vec<FlatDataset> =
flat.remove(key).into_iter().flatten().collect();
group_datasets.sort_by_key(|fd| fd.reference_targets.is_some());
for mut fd in group_datasets {
for (idx, collections) in std::mem::take(&mut fd.vl_attrs) {
let addrs = self.place_vl_collections(&collections)?;
patch_vl_refs(&mut fd.attrs[idx].raw_data, &addrs);
}
if let Some(patches) = fd.reference_targets.take() {
for patch in &patches {
let addr = Self::resolve_reference_target(
&patch.target,
&path_addr,
&nodes,
&add_targets,
&write_targets,
&delete_targets,
&for_supplied,
&self.image(),
&self.superblock,
)?;
write_reference_address(&mut fd.raw, patch.byte_offset, addr);
}
}
let oh = if fd.chunk_options.is_chunked() || fd.maxshape.is_some() {
self.build_chunked_dataset(&fd)?
} else {
if let Some(staging) = fd.vl_string_staging.take() {
if !staging.collections.is_empty() {
let addrs = self.place_vl_collections(&staging.collections)?;
patch_vl_refs_masked(&mut fd.raw, &staging.patch_offsets, &addrs);
}
}
let data_addr = if fd.raw.is_empty() {
u64::MAX
} else {
base.relative(self.alloc_or_append_typed(&fd.raw, PageType::Raw)?)?
};
let attr_info = self.place_dense_attrs_if_needed(&fd)?;
build_dataset_oh(
&fd.dt,
&DatatypeLocation::Inline,
&fd.ds,
data_addr,
fd.raw.len() as u64,
&fd.attrs,
attr_info.as_deref(),
fd.fill.as_deref(),
self.libver(),
)?
};
let oh_addr = self.alloc_or_append_typed(&oh, PageType::Meta)?;
region.extend_from_slice(&encode_link_message(&fd.name, base.relative(oh_addr)?));
let mut full = key.clone();
full.push(fd.name.clone());
path_addr.insert(full, oh_addr);
}
for (leaf, old_oh, mw) in &writes {
let new_oh = self.write_moving(mw)?;
patch_link_target(&mut region, leaf, base.relative(new_oh)?)?;
relocations.insert(*old_oh, new_oh);
}
for child in children.get(key).into_iter().flatten() {
let child_name = child.last().unwrap();
let child_addr = base.relative(path_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)?;
}
}
self.place_edited_attrs(&mut region, attrs)?;
let oh = build_v2_object_header(®ion)?;
let addr = self.alloc_or_append_typed(&oh, PageType::Meta)?;
path_addr.insert(key.clone(), addr);
}
for (data_addr, bytes) in &inplace_writes {
let raw = self.resolve_overwrite_bytes(bytes)?;
self.write_inplace_journaled(*data_addr, &raw)?;
}
for (key, old) in &superseded_addrs {
if let Some(&new) = path_addr.get(key) {
relocations.insert(*old as u64, new);
}
}
let new_root = path_addr[&PathKey::new()];
to_free.extend(
self.superseded_heaps
.drain(..)
.map(|(addr, len)| (addr, len, FreeClass::Page(PageType::Meta))),
);
retain_disjoint_in_bounds(&mut to_free, self.image.len());
if self.persist.is_some() {
self.commit_persisting_releasing_reserve(new_root, to_free)?;
return self.repoint_stored_references(&relocations);
}
for (a, l, _) in to_free.drain(..) {
self.free.free(a, l);
}
let cur_eof = self.image.len();
let trunc_to = self.free.take_trailing(cur_eof);
let new_eof = trunc_to.unwrap_or(cur_eof);
self.barrier()?;
if self.superblock.version >= 2 {
let mut new_sb = self.superblock.clone();
new_sb.root_group_address = base.relative(new_root)?;
new_sb.eof_address = new_eof;
new_sb.consistency_flags = self.held_status_flags;
let sb_bytes = new_sb.serialize();
self.publish_attempted = true;
self.write_at(self.sb_sig_off, &sb_bytes)?;
self.barrier()?;
new_sb.root_group_address = new_root;
self.superblock = new_sb;
} else {
self.publish_attempted = true;
self.repoint_v0v1_root(base.relative(new_root)?, new_eof)?;
self.barrier()?;
self.superblock.root_group_address = new_root;
self.superblock.eof_address = new_eof;
}
if let Some(cut) = trunc_to {
self.image.truncate(cut)?;
self.barrier()?;
}
self.repoint_stored_references(&relocations)
}
fn repoint_stored_references(&mut self, relocations: &BTreeMap<u64, u64>) -> Result<(), Error> {
if self.proved_free_of_references {
return Ok(());
}
let plan = crate::reference_patch::plan(
&self.image(),
&self.superblock,
relocations,
MAX_LINK_GRAPH_NODES,
)?;
if plan.proved_free_of_references() {
self.proved_free_of_references = true;
}
if plan.is_empty() {
return Ok(());
}
plan.apply(self)?;
self.barrier()
}
fn commit_persisting(
&mut self,
new_root: u64,
to_free: Vec<(u64, u64, FreeClass)>,
) -> Result<(), Error> {
if self.paged.is_some() {
return self.commit_persisting_paged(new_root, to_free);
}
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 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 (post, placed_at, tail_len) = self.flat_tail_layout(&to_free, &old_blocks, ext_len, os);
let reused = placed_at.is_some();
let ext_addr = placed_at.unwrap_or_else(|| self.image.len());
let sections = free_sections(&post);
let fshd_addr = ext_addr + ext_len;
let fsse_addr = fshd_addr + fshd_len(os);
let final_eof = if reused {
self.image.len()
} else {
ext_addr + tail_len
};
let (ext_oh, fsm_blocks) = 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)?)?;
(ext_oh, None)
} else {
let eoa_pre_fsm = if reused { final_eof } else { 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)?)?;
let (fshd, fsse) =
serialize_file_fsm(§ions, fshd_addr, fsse_addr, os, SECT_CLASS_SIMPLE);
(ext_oh, Some((fshd, fsse)))
};
debug_assert_eq!(
ext_oh.len() as u64,
ext_len,
"extension length must be stable across the placeholder and real messages"
);
let region = (ext_addr, tail_len);
self.write_tail_block(region, ext_addr, &ext_oh)?;
if let Some((fshd, fsse)) = fsm_blocks {
self.write_tail_block(region, fshd_addr, &fshd)?;
self.write_tail_block(region, fsse_addr, &fsse)?;
}
let new_old_blocks = vec![(ext_addr, tail_len)];
self.barrier()?;
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 = self.held_status_flags;
let sb_bytes = new_sb.serialize();
self.publish_attempted = true;
self.write_at(self.sb_sig_off, &sb_bytes)?;
self.barrier()?;
self.superblock = new_sb;
self.free = post;
self.persist = Some(PersistState {
strategy,
threshold,
page_size,
old_blocks: new_old_blocks,
});
self.fsm_len = self.image.len();
Ok(())
}
fn flat_post_free(
&self,
to_free: &[(u64, u64, FreeClass)],
old_blocks: &[(u64, u64)],
) -> FreeList {
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);
}
post
}
fn flat_tail_layout(
&mut self,
to_free: &[(u64, u64, FreeClass)],
old_blocks: &[(u64, u64)],
ext_len: u64,
os: u8,
) -> (FreeList, Option<u64>, u64) {
const ROUNDS: usize = 4;
let probe = self.flat_post_free(to_free, old_blocks);
let appended_len = ext_len + file_fsm_blocks_len(&free_sections(&probe), os);
let mut proposed = appended_len;
for _ in 0..ROUNDS {
let Some(at) = self.free.alloc(proposed) else {
break;
};
let post = self.flat_post_free(to_free, old_blocks);
let len = ext_len + file_fsm_blocks_len(&free_sections(&post), os);
if len <= proposed {
return (post, Some(at), proposed);
}
self.free.free(at, proposed);
proposed = len;
}
(probe, None, appended_len)
}
fn commit_persisting_paged(
&mut self,
new_root: u64,
to_free: Vec<(u64, u64, FreeClass)>,
) -> 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(),
)
};
self.pad_to_page()?;
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_managers(
strategy,
threshold,
page_size,
[UNDEF; NUM_FILE_FSM_MANAGERS],
0,
);
let ext_len =
build_v2_object_header(&self.rewrite_extension_region(old_ext_addr, &placeholder)?)?
.len() as u64;
let placed = self.tail_layout(&to_free, &old_blocks, ext_len, page_size, os);
let reused = placed.is_some();
let (post, plan, ext_addr, blocks_len) = match placed {
Some(layout) => layout,
None => {
self.begin_page(PageType::Meta)?;
let at = self.image.len();
let post = self.paged_post_free(&to_free, &old_blocks);
let plan = plan_paged_managers(
&free_sections(&post.meta),
&free_sections(&post.raw),
&post.unclassified,
page_size,
at + ext_len,
os,
);
let blocks_len = plan.end_of_managers.max(at + ext_len) - at;
(post, plan, at, blocks_len)
}
};
let final_eof = if reused {
self.image.len()
} else {
align_up(ext_addr + blocks_len, page_size)
};
let ext_oh = if plan.is_empty() {
let info = FileSpaceInfo::persistent_empty(strategy, threshold, page_size);
build_v2_object_header(&self.rewrite_extension_region(old_ext_addr, &info)?)?
} else {
let info = FileSpaceInfo::persistent_managers(
strategy, threshold, page_size, plan.slots, final_eof,
);
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 region = (ext_addr, blocks_len);
self.write_tail_block(region, ext_addr, &ext_oh)?;
for b in &plan.blocks {
let (fshd, fsse) =
serialize_file_fsm(&b.sections, b.fshd_addr, b.fsse_addr, os, b.class);
self.write_tail_block(region, b.fshd_addr, &fshd)?;
self.write_tail_block(region, b.fsse_addr, &fsse)?;
}
self.pad_zeros_to(final_eof)?;
let new_old_blocks = vec![(ext_addr, blocks_len)];
self.barrier()?;
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 = self.held_status_flags;
let sb_bytes = new_sb.serialize();
self.publish_attempted = true;
self.write_at(self.sb_sig_off, &sb_bytes)?;
self.barrier()?;
self.superblock = new_sb;
if let Some(pg) = self.paged.as_mut() {
pg.meta = post.meta;
pg.raw = post.raw;
pg.dead = post.dead;
pg.meta_pad.clear();
pg.raw_pad.clear();
if !reused {
pg.last = Some(PageType::Meta);
pg.meta
.free(ext_addr + blocks_len, final_eof - (ext_addr + blocks_len));
}
}
self.persist = Some(PersistState {
strategy,
threshold,
page_size,
old_blocks: new_old_blocks,
});
self.fsm_len = self.image.len();
Ok(())
}
fn pad_to_page(&mut self) -> Result<(), Error> {
let len = self.image.len();
let pad = match &self.paged {
Some(pg) if len % pg.page_size != 0 => {
Some((pg.last, pg.page_size - len % pg.page_size))
}
_ => None,
};
if let Some((last, pad_len)) = pad {
let pad_at = len;
self.append(&vec![0u8; pad_len.to_usize()?])?;
if let Some(pg) = self.paged.as_mut() {
match last {
Some(PageType::Meta) => pg.meta_pad.push((pad_at, pad_len)),
Some(PageType::Raw) => pg.raw_pad.push((pad_at, pad_len)),
None => {}
}
}
}
Ok(())
}
fn paged_post_free(
&self,
to_free: &[(u64, u64, FreeClass)],
old_blocks: &[(u64, u64)],
) -> PagedPostFree {
let pg = self
.paged
.as_ref()
.expect("commit_persisting_paged is only called on a paged file");
let (mut meta, mut raw, mut dead) = (pg.meta.clone(), pg.raw.clone(), pg.dead.clone());
let unclassified = free_sections(&pg.unclassified);
let mut free = |a: u64, l: u64, class: FreeClass| {
PagedEdit::route_free(&mut meta, &mut raw, &mut dead, a, l, class);
};
for &(a, l) in &pg.meta_pad {
free(a, l, PageType::Meta.into());
}
for &(a, l) in &pg.raw_pad {
free(a, l, PageType::Raw.into());
}
for &(a, l, class) in to_free {
free(a, l, class);
}
for &(a, l) in old_blocks {
free(a, l, PageType::Meta.into());
}
PagedEdit::promote_whole_free_pages(&mut meta, &mut raw, &mut dead, pg.page_size);
PagedPostFree {
meta,
raw,
dead,
unclassified,
}
}
fn tail_layout(
&mut self,
to_free: &[(u64, u64, FreeClass)],
old_blocks: &[(u64, u64)],
ext_len: u64,
page_size: u64,
os: u8,
) -> Option<(PagedPostFree, PagedManagerPlan, u64, u64)> {
const ROUNDS: usize = 4;
let probe = self.paged_post_free(to_free, old_blocks);
let mut proposed = ext_len
+ plan_paged_managers(
&free_sections(&probe.meta),
&free_sections(&probe.raw),
&probe.unclassified,
page_size,
0,
os,
)
.end_of_managers;
for _ in 0..ROUNDS {
let pg = self
.paged
.as_mut()
.expect("commit_persisting_paged is only called on a paged file");
let at = pg.alloc_typed(proposed, PageType::Meta)?;
let post = self.paged_post_free(to_free, old_blocks);
let plan = plan_paged_managers(
&free_sections(&post.meta),
&free_sections(&post.raw),
&post.unclassified,
page_size,
at + ext_len,
os,
);
let blocks_len = plan.end_of_managers.max(at + ext_len) - at;
if blocks_len == proposed {
return Some((post, plan, at, blocks_len));
}
let pg = self
.paged
.as_mut()
.expect("the paged state outlives this loop");
PagedEdit::route_free(
&mut pg.meta,
&mut pg.raw,
&mut pg.dead,
at,
proposed,
PageType::Meta.into(),
);
proposed = blocks_len;
}
None
}
fn write_tail_block(
&mut self,
region: (u64, u64),
addr: u64,
bytes: &[u8],
) -> Result<(), Error> {
let (start, len) = region;
if addr < start || addr + bytes.len() as u64 > start + len {
return Err(Error::Format(FormatError::SerializationError(format!(
"a commit's tail reserved [{start}, {}) but placed {} bytes at {addr}",
start + len,
bytes.len()
))));
}
if addr == self.image.len() {
let written = self.append(bytes)?;
debug_assert_eq!(written, addr, "an appended block must land at end-of-file");
return Ok(());
}
self.write_at(
usize::try_from(addr)
.map_err(|_| Error::EditUnsupported("tail address exceeds this platform"))?,
bytes,
)
}
fn pad_zeros_to(&mut self, target: u64) -> Result<(), Error> {
let len = self.image.len();
if target > len {
let pad = (target - len).to_usize()?;
self.append(&vec![0u8; pad])?;
}
debug_assert_eq!(self.image.len(), target);
Ok(())
}
fn rewrite_extension_region(
&self,
ext_addr: usize,
info: &FileSpaceInfo,
) -> Result<Vec<u8>, Error> {
let region =
Self::gather_oh_messages(&self.image(), ext_addr as u64, self.superblock.base_address)?;
rewrite_extension_region_bytes(®ion, info)
}
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 gather_oh_messages<S: Source + ?Sized>(
src: &S,
addr: u64,
base: BaseAddress,
) -> Result<Vec<u8>, Error> {
let mut out = Vec::new();
for chunk in read_oh_chunks(src, addr, base)? {
let (region, mut p) = chunk.message_region();
while let Some((msg_type, _body, body_end)) = next_message(region, p)? {
if msg_type != MessageType::ObjectHeaderContinuation {
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_from_source(&self.image(), addr as u64, 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_from_source(&self.image(), &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() > OBJECT_HEADER_MESSAGE_MAX {
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> {
let sig = self.image().read_metadata_at(addr as u64, 4);
if sig.as_deref() != Ok(&b"OHDR"[..]) {
return self.reconstruct_v1_group(addr);
}
let mut region =
Self::gather_oh_messages(&self.image(), addr as u64, self.superblock.base_address)?;
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 refuse_unsupported_overwrite(fd: &FlatDataset) -> Result<(), 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)",
));
}
if fd.fill.is_some() {
return Err(Error::EditUnsupported(
"write_dataset overwrites values only; it cannot change the fill \
value (set it when the dataset is created)",
));
}
if fd.reference_targets.is_some() {
return Err(Error::EditUnsupported(
"write_dataset cannot overwrite an object-reference dataset's \
data in place yet",
));
}
Ok(())
}
fn prepare_write<S: Source + ?Sized>(
src: &S,
addr: u64,
fd: &FlatDataset,
base: BaseAddress,
path: &PathKey,
) -> Result<WritePlan, Error> {
debug_assert!(
Self::refuse_unsupported_overwrite(fd).is_ok(),
"a staged write reached prepare_write without refuse_unsupported_overwrite"
);
let region = Self::gather_oh_messages(src, addr, base)?;
let mut datatype: Option<(usize, usize)> = None;
let mut dataspace: Option<(usize, usize)> = None;
let mut layout: Option<(usize, usize)> = None;
let mut filter: Option<(usize, usize)> = None;
let mut fill_msg: Option<(MessageType, usize, usize)> = None;
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 => filter = Some((body, body_end)),
MessageType::FillValue
if !matches!(fill_msg, Some((MessageType::FillValue, ..))) =>
{
fill_msg = Some((msg_type, body, body_end));
}
MessageType::FillValueOld if fill_msg.is_none() => {
fill_msg = Some((msg_type, body, body_end));
}
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",
));
}
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,
bytes: staged_bytes(fd, path),
})),
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 fd.vl_string_staging.is_none()
&& data_addr != UNDEF
&& data_size == fd.raw.len() as u64
{
if let Some(start) = base
.absolute(data_addr)
.ok()
.and_then(|a| usize::try_from(a).ok())
{
if start
.checked_add(fd.raw.len())
.is_some_and(|e| e as u64 <= src.len())
{
return Ok(WritePlan::InPlace {
data_addr: start,
bytes: staged_bytes(fd, path),
});
}
}
}
let old_extent = if data_addr != UNDEF && data_size > 0 {
Some((base.absolute(data_addr)?, data_size))
} else {
None
};
Ok(WritePlan::Moving(MovingWrite::Contiguous {
region,
addr_off,
bytes: staged_bytes(fd, path),
old_extent,
}))
}
2 => {
let dl =
DataLayout::parse(®ion[lb..le], OFFSET_SIZE, LENGTH_SIZE).map_err(|_| {
Error::EditUnsupported("dataset header data layout could not be parsed")
})?;
let DataLayout::Chunked {
version: lversion,
chunk_index_type,
..
} = dl
else {
return Err(Error::EditUnsupported("dataset is not chunked"));
};
if !chunk_index_enumerable(lversion, chunk_index_type) {
return Err(Error::EditUnsupported(
"a chunked dataset with a version-2 B-tree or unknown chunk index \
cannot be overwritten in place yet",
));
}
let ChunkedGeometry {
spatial,
element_size,
raw_size,
maxshape,
} = chunked_geometry(&fd.dt, &disk_ds, &dl)?;
let padding = fill_msg
.map_or(crate::fill_value::PaddingFill::Zero, |(mt, b, e)| {
crate::fill_value::PaddingFill::from_message(mt, ®ion[b..e])
});
let pipeline_message: Option<Vec<u8>> =
filter.map(|(fb, fe)| region[fb..fe].to_vec());
if let Some(pm) = &pipeline_message {
let pipeline = FilterPipeline::parse(pm).map_err(|_| {
Error::EditUnsupported("dataset filter pipeline could not be parsed")
})?;
if !pipeline_reencodable(&pipeline) {
return Err(Error::EditUnsupported(
"a chunked dataset using a filter this engine cannot re-encode \
cannot be overwritten in place yet",
));
}
}
if fd.vl_string_staging.is_some() {
return Ok(WritePlan::Moving(MovingWrite::Chunked {
region,
shape: disk_ds.dimensions.clone(),
chunk_dims: spatial,
element_size,
maxshape,
pipeline_message,
payload: ChunkPayload::Deferred {
bytes: staged_bytes(fd, path),
padding,
dt: fd.dt.clone(),
},
old_addr: addr,
}));
}
let new_chunk_bytes = split_and_encode_chunks(
&fd.raw,
&disk_ds.dimensions,
&spatial,
element_size,
&padding,
pipeline_message.as_deref(),
&fd.dt,
)?;
let base_off = usize::try_from(base.get()).map_err(|_| {
Error::EditUnsupported("userblock base address exceeds this platform")
})?;
if let Some(writes) = try_inplace_chunk_writes(
&BaseOffsetSource { inner: src, base },
&dl,
&disk_ds,
&spatial,
raw_size,
&new_chunk_bytes,
) {
let writes = writes
.into_iter()
.map(|(off, b)| (off + base_off, b))
.collect();
return Ok(WritePlan::InPlaceChunks { writes });
}
Ok(WritePlan::Moving(MovingWrite::Chunked {
region,
shape: disk_ds.dimensions.clone(),
chunk_dims: spatial,
element_size,
maxshape,
pipeline_message,
payload: ChunkPayload::Encoded(new_chunk_bytes),
old_addr: addr,
}))
}
_ => Err(Error::EditUnsupported(
"an unsupported data-layout class cannot be overwritten in place yet",
)),
}
}
fn prepare_append<S: Source + ?Sized>(
src: &S,
addr: u64,
ab: &AppendBuilder,
base: BaseAddress,
) -> Result<MovingWrite, Error> {
if ab.dt_conflict {
return Err(Error::AppendUnsupported(
"append mixes element types in one builder; use one element type per \
append_dataset call",
));
}
let region = Self::gather_oh_messages(src, addr, base)?;
let mut datatype: Option<(usize, usize)> = None;
let mut dataspace: Option<(usize, usize)> = None;
let mut layout: Option<(usize, usize)> = None;
let mut filter: Option<(usize, usize)> = None;
let mut fill_msg: Option<(MessageType, usize, usize)> = None;
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 => filter = Some((body, body_end)),
MessageType::FillValue
if !matches!(fill_msg, Some((MessageType::FillValue, ..))) =>
{
fill_msg = Some((msg_type, body, body_end));
}
MessageType::FillValueOld if fill_msg.is_none() => {
fill_msg = Some((msg_type, body, body_end));
}
MessageType::Link | MessageType::LinkInfo | MessageType::SymbolTable => {
has_link = true;
}
_ => {}
}
p = body_end;
}
if has_link {
return Err(Error::AppendUnsupported(
"append target is a group, not a dataset",
));
}
let (dt_b, dt_e) =
datatype.ok_or(Error::AppendUnsupported("dataset header has no datatype"))?;
let (ds_b, ds_e) =
dataspace.ok_or(Error::AppendUnsupported("dataset header has no dataspace"))?;
let (lb, le) = layout.ok_or(Error::AppendUnsupported(
"dataset header has no data layout",
))?;
let (disk_dt, _) = Datatype::parse(®ion[dt_b..dt_e])
.map_err(|_| Error::AppendUnsupported("dataset header datatype could not be parsed"))?;
let disk_ds = Dataspace::parse(®ion[ds_b..ds_e], LENGTH_SIZE).map_err(|_| {
Error::AppendUnsupported("dataset header dataspace could not be parsed")
})?;
let dl = DataLayout::parse(®ion[lb..le], OFFSET_SIZE, LENGTH_SIZE).map_err(|_| {
Error::AppendUnsupported("dataset header data layout could not be parsed")
})?;
let DataLayout::Chunked {
version: lversion,
chunk_index_type,
btree_address,
..
} = &dl
else {
return Err(Error::AppendUnsupported(
"append requires a chunked dataset",
));
};
if *lversion != 4 || *chunk_index_type != Some(4) {
return Err(Error::AppendUnsupported(
"append requires an Extensible-Array-indexed chunked dataset (a single \
unlimited dimension under the latest format)",
));
}
if disk_ds.space_type != DataspaceType::Simple || disk_ds.dimensions.len() != 1 {
return Err(Error::AppendUnsupported(
"append requires a rank-1 dataset in this release",
));
}
match &disk_ds.max_dimensions {
Some(md) if md.first() == Some(&u64::MAX) => {}
_ => {
return Err(Error::AppendUnsupported(
"append requires a dataset that is unlimited along its first dimension",
));
}
}
let ChunkedGeometry {
spatial,
element_size,
..
} = chunked_geometry(&disk_dt, &disk_ds, &dl)?;
let chunk_elems = spatial[0];
if chunk_elems == 0 {
return Err(Error::AppendUnsupported(
"append requires a nonzero chunk length",
));
}
if ab.raw.len() % element_size != 0 {
return Err(Error::AppendUnsupported(
"appended byte length is not a whole number of elements",
));
}
match &ab.elem_dt {
Some(expected) if *expected != disk_dt => {
return Err(Error::AppendUnsupported(
"append datatype does not match the on-disk dataset (wrong element \
type or byte order)",
));
}
Some(_) => {}
None => {
if !datatype_is_raw_appendable(&disk_dt) {
return Err(Error::AppendUnsupported(
"append_raw onto this dataset's datatype (non-little-endian, \
variable-length, or reference) could misencode the bytes; use a \
typed append",
));
}
}
}
let new_elems = (ab.raw.len() / element_size) as u64;
let current_dim0 = disk_ds.dimensions[0];
let new_dim0 = current_dim0
.checked_add(new_elems)
.ok_or(Error::AppendUnsupported(
"append would overflow the dataset dimension",
))?;
let pipeline_message: Option<Vec<u8>> = filter.map(|(fb, fe)| region[fb..fe].to_vec());
let has_filters = pipeline_message.is_some();
let pipeline = match &pipeline_message {
Some(pm) => {
let parsed = FilterPipeline::parse(pm).map_err(|_| {
Error::AppendUnsupported("dataset filter pipeline could not be parsed")
})?;
if !pipeline_reencodable(&parsed) {
return Err(Error::AppendUnsupported(
"dataset uses a filter this engine cannot re-encode",
));
}
Some(parsed)
}
None => None,
};
if base.get() > src.len() {
return Err(Error::AppendUnsupported(
"userblock base address past end-of-file",
));
}
let view = BaseOffsetSource { inner: src, base };
if let Some(idx_addr) = *btree_address {
let hdr =
ExtensibleArrayHeader::parse_from_source(&view, idx_addr, OFFSET_SIZE, LENGTH_SIZE)
.map_err(|_| {
Error::AppendUnsupported(
"dataset extensible-array header could not be parsed",
)
})?;
if (hdr.client_id == 1) != has_filters {
return Err(Error::AppendUnsupported(
"dataset filter metadata is inconsistent (chunk-index client id \
disagrees with the filter pipeline)",
));
}
}
let infos = enumerate_chunks_from_source(&view, &dl, &disk_ds, OFFSET_SIZE, LENGTH_SIZE)
.map_err(|_| Error::AppendUnsupported("dataset chunk index could not be enumerated"))?;
let grid = plan_dense_grid(infos, &disk_ds.dimensions, &spatial).ok_or(
Error::AppendUnsupported(
"dataset has a sparse or inconsistent chunk grid; cannot append",
),
)?;
let grid_order = grid.grid_order;
let n_full = usize::try_from(current_dim0 / chunk_elems)
.map_err(|_| Error::AppendUnsupported("chunk count exceeds this platform"))?;
let has_partial = current_dim0 % chunk_elems != 0;
if has_partial
&& let Some(pl) = &pipeline
&& !pipeline_lossless(pl)
{
return Err(Error::AppendUnsupported(LOSSY_TAIL_REFUSAL));
}
let mut kept_chunks: Vec<WrittenChunk> = Vec::with_capacity(n_full);
for ci in grid_order.iter().take(n_full) {
kept_chunks.push(WrittenChunk {
address: ci.address,
compressed_size: u64::from(ci.chunk_size),
filter_mask: ci.filter_mask,
});
}
let mut tail_raw: Vec<u8> = Vec::new();
let mut old_tail_extent: Option<(u64, u64)> = None;
if has_partial {
let partial = &grid_order[n_full];
let len = partial.chunk_size as usize;
partial
.address
.checked_add(len as u64)
.filter(|&e| e <= view.len())
.ok_or(Error::AppendUnsupported(
"trailing chunk extends past end-of-file",
))?;
let stored = view
.read_exact_at(partial.address, len)
.map_err(|_| Error::AppendUnsupported("trailing chunk could not be read"))?;
let full = if let Some(pl) = &pipeline {
let ctx = ChunkContext::from_datatype(&spatial, &disk_dt)?;
decompress_chunk(&stored, pl, ctx, partial.filter_mask).map_err(Error::Format)?
} else {
stored
};
let live_elems = usize::try_from(current_dim0 % chunk_elems)
.map_err(|_| Error::AppendUnsupported("chunk length exceeds this platform"))?;
let live_bytes = live_elems * element_size.get();
if full.len() < live_bytes {
return Err(Error::AppendUnsupported(
"trailing chunk decoded shorter than its live element count",
));
}
tail_raw.extend_from_slice(&full[..live_bytes]);
old_tail_extent = Some((
base.absolute(partial.address)?,
u64::from(partial.chunk_size),
));
}
tail_raw.extend_from_slice(&ab.raw);
let tail_len_elems = new_dim0 - (n_full as u64) * chunk_elems;
let padding = fill_msg.map_or(crate::fill_value::PaddingFill::Zero, |(mt, b, e)| {
crate::fill_value::PaddingFill::from_message(mt, ®ion[b..e])
});
let split = split_into_chunks(
&tail_raw,
&[tail_len_elems],
&spatial,
element_size,
padding.pattern(element_size),
)
.map_err(Error::Format)?;
let new_chunk_bytes: Vec<Vec<u8>> = if let Some(pl) = &pipeline {
let ctx = ChunkContext::from_datatype(&spatial, &disk_dt)?;
let mut out = Vec::with_capacity(split.len());
let mut scratch = FilterScratch::new();
for buf in &split {
out.push(compress_chunk_with(&mut scratch, buf, pl, ctx).map_err(Error::Format)?);
}
out
} else {
split
};
let mut grown = disk_ds.clone();
grown.dimensions[0] = new_dim0;
let new_dataspace_body = grown.serialize(LENGTH_SIZE);
#[expect(
clippy::cast_possible_truncation,
reason = "spatial chunk dims come from the on-disk u32 chunk_dimensions, so they fit u32"
)]
let chunk_dims_u32: Vec<u32> = spatial.iter().map(|&dm| dm as u32).collect();
Ok(MovingWrite::AppendedChunks {
region,
new_dataspace_body,
chunk_dims_u32,
element_size,
has_filters,
kept_chunks,
new_chunk_bytes,
old_addr: addr,
old_tail_extent,
})
}
fn read_object<S: Source + ?Sized>(
src: &S,
addr: u64,
base: BaseAddress,
) -> Result<ObjModel, Error> {
let region = Self::gather_oh_messages(src, addr, base)?;
reject_external_storage(®ion)?;
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 attrs = read_object_attrs(src, addr, base)?;
crate::file_writer::dense_attrs_check(&attrs).map_err(Error::Format)?;
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,
})
}
2 => Ok(ObjModel::DatasetChunked {
region: kept,
dense_attrs,
}),
_ => Err(Error::EditUnsupported(
"an unsupported data-layout class 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<S: Source + ?Sized>(
src: &S,
addr: u64,
depth: u32,
cross_file: bool,
base: BaseAddress,
) -> 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(src, addr, base)? {
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 data = if data_addr == UNDEF {
None
} else {
let start = base.absolute(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"))?;
start
.checked_add(len as u64)
.filter(|&e| e <= src.len())
.ok_or(Error::EditUnsupported("dataset data is out of bounds"))?;
Some(
src.read_exact_at(start, len)
.map_err(|_| Error::EditUnsupported("dataset data is out of bounds"))?,
)
};
Ok(CopyTree::DatasetContiguous {
region,
addr_off,
data,
dense_attrs,
})
}
ObjModel::DatasetChunked {
region,
dense_attrs,
} => {
if cross_file {
reject_foreign_addresses(®ion)?;
reject_foreign_dense_attrs(&dense_attrs)?;
}
let ChunkedHeaderParts {
dt,
ds,
layout,
pipeline_message,
} = parse_chunked_header(®ion)?;
let DataLayout::Chunked {
version: lversion,
chunk_index_type,
..
} = layout
else {
return Err(Error::EditUnsupported("dataset is not chunked"));
};
if !chunk_index_enumerable(lversion, chunk_index_type) {
return Err(Error::EditUnsupported(
"a chunked dataset with a version-2 B-tree or unknown chunk index \
cannot be copied in place yet",
));
}
let ChunkedGeometry {
spatial: chunk_dims,
element_size,
raw_size: _,
maxshape,
} = chunked_geometry(&dt, &ds, &layout)?;
let dview = BaseOffsetSource { inner: src, base };
let infos =
enumerate_chunks_from_source(&dview, &layout, &ds, OFFSET_SIZE, LENGTH_SIZE)?;
let grid = plan_dense_grid(infos, &ds.dimensions, &chunk_dims).ok_or(
Error::EditUnsupported(
"a chunked dataset with unallocated (sparse) chunks cannot be copied in place yet",
),
)?;
if grid.grid_order.is_empty() {
return Err(Error::EditUnsupported(
"an empty chunked dataset cannot be copied in place yet",
));
}
let mut meta = Vec::with_capacity(grid.grid_order.len());
let mut chunk_bytes = Vec::with_capacity(grid.grid_order.len());
for ci in &grid.grid_order {
let len = ci.chunk_size as usize;
ci.address
.checked_add(len as u64)
.filter(|&e| e <= dview.len())
.ok_or(Error::EditUnsupported("chunk data is out of bounds"))?;
chunk_bytes.push(
dview
.read_exact_at(ci.address, len)
.map_err(|_| Error::EditUnsupported("chunk data is out of bounds"))?,
);
meta.push(ChunkMeta {
compressed_size: ci.chunk_size as u64,
filter_mask: ci.filter_mask,
});
}
Ok(CopyTree::DatasetChunked {
region,
shape: ds.dimensions.clone(),
chunk_dims,
element_size,
maxshape,
pipeline_message,
meta,
chunk_bytes,
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 = base.absolute(child).map_err(|_| {
Error::EditUnsupported("child address exceeds this platform")
})?;
kids.push((
name,
Self::read_copy_subtree(src, child, depth + 1, cross_file, base)?,
));
}
Ok(CopyTree::Group {
non_link_region,
children: kids,
dense_attrs,
})
}
}
}
fn write_copy_subtree(&mut self, node: &CopyTree) -> Result<u64, Error> {
let base = self.superblock.base_address;
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_typed(&oh, PageType::Meta)
}
CopyTree::DatasetContiguous {
region,
addr_off,
data,
dense_attrs,
} => {
let mut region = region.clone();
if let Some(data) = data {
let new_data_addr = self.alloc_or_append_typed(data, PageType::Raw)?;
region[*addr_off..*addr_off + 8]
.copy_from_slice(&base.relative(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_typed(&oh, PageType::Meta)
}
CopyTree::DatasetChunked {
region,
shape,
chunk_dims,
element_size,
maxshape,
pipeline_message,
meta,
chunk_bytes,
dense_attrs,
} => self.write_chunked_relocatable(
region,
shape,
chunk_dims,
*element_size,
maxshape.as_deref(),
pipeline_message.as_deref(),
meta,
chunk_bytes,
dense_attrs,
),
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, base.relative(new_child)?));
}
self.append_dense_attrs(&mut region, dense_attrs)?;
let oh = build_v2_object_header(®ion)?;
self.alloc_or_append_typed(&oh, PageType::Meta)
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "the chunked rebuild needs the full geometry, \
pipeline, and chunk payloads; bundling them into a struct would only move the list"
)]
fn write_chunked_relocatable(
&mut self,
region: &[u8],
shape: &[u64],
chunk_dims: &[u64],
element_size: NonZeroUsize,
maxshape: Option<&[u64]>,
pipeline_message: Option<&[u8]>,
meta: &[ChunkMeta],
chunk_bytes: &[Vec<u8>],
dense_attrs: &[crate::attribute::AttributeMessage],
) -> Result<u64, Error> {
let sizing = plan_chunked_data_verbatim(
meta,
shape,
chunk_dims,
element_size,
pipeline_message,
0,
maxshape,
)?;
let (_addr, layout_message) =
self.place_relocatable(sizing.plan.total_len, PageType::Raw, |stored_base| {
let layout = plan_chunked_data_verbatim(
meta,
shape,
chunk_dims,
element_size,
pipeline_message,
stored_base,
maxshape,
)?;
let mut buf =
Vec::with_capacity(usize::try_from(layout.plan.total_len).unwrap_or(0));
emit_chunked_data_verbatim(
&mut buf,
&layout.plan,
&SliceChunkProvider {
chunks: chunk_bytes,
},
)?;
Ok((buf, layout.layout_message))
})?;
let mut new_region = replace_layout_message(region, &layout_message)?;
self.append_dense_attrs(&mut new_region, dense_attrs)?;
let oh = build_v2_object_header(&new_region)?;
self.alloc_or_append_typed(&oh, PageType::Meta)
}
fn append_dense_attrs(
&mut self,
region: &mut Vec<u8>,
attrs: &[crate::attribute::AttributeMessage],
) -> Result<(), Error> {
if attrs.is_empty() {
return Ok(());
}
let attr_info_message = self.place_dense_attrs(attrs)?;
region.extend_from_slice(®ion_message(
MessageType::AttributeInfo,
&attr_info_message,
));
Ok(())
}
fn place_dense_attrs_if_needed(&mut self, fd: &FlatDataset) -> Result<Option<Vec<u8>>, Error> {
if !fd.attrs_are_dense {
return Ok(None);
}
self.place_dense_attrs(&fd.attrs).map(Some)
}
fn place_dense_attrs(
&mut self,
attrs: &[crate::attribute::AttributeMessage],
) -> Result<Vec<u8>, Error> {
let plan = crate::file_writer::dense_attrs_plan(attrs);
let (_addr, attr_info_message) =
self.place_relocatable(plan.blob_len(), PageType::Meta, |stored_base| {
let blob = plan.build(stored_base);
Ok((blob.blob, blob.attr_info_message))
})?;
Ok(attr_info_message)
}
fn place_edited_attrs(
&mut self,
region: &mut Vec<u8>,
attrs: EditedAttrs,
) -> Result<(), Error> {
match attrs {
EditedAttrs::Compact(pending) => {
for (mut msg, collections) in pending {
let addrs = self.place_vl_collections(&collections)?;
patch_vl_refs(&mut msg.raw_data, &addrs);
region.extend_from_slice(®ion_message(
MessageType::Attribute,
&msg.serialize(LENGTH_SIZE),
));
}
Ok(())
}
EditedAttrs::Dense(mut dense) => {
for (idx, collections) in std::mem::take(&mut dense.vl) {
let addrs = self.place_vl_collections(&collections)?;
patch_vl_refs(&mut dense.attrs[idx].raw_data, &addrs);
}
self.append_dense_attrs(region, &dense.attrs)
}
}
}
fn write_moving(&mut self, mw: &MovingWrite) -> Result<u64, Error> {
let base = self.superblock.base_address;
match mw {
MovingWrite::Contiguous {
region,
addr_off,
bytes,
..
} => {
let raw = self.resolve_overwrite_bytes(bytes)?;
let new_data_addr = self.alloc_or_append_typed(&raw, PageType::Raw)?;
let mut region = region.clone();
region[*addr_off..*addr_off + 8]
.copy_from_slice(&base.relative(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_typed(&oh, PageType::Meta)
}
MovingWrite::Compact { region, bytes } => {
let raw = self.resolve_overwrite_bytes(bytes)?;
let region = rebuild_compact_layout_region(region, &raw)?;
let oh = build_v2_object_header(®ion)?;
self.alloc_or_append_typed(&oh, PageType::Meta)
}
MovingWrite::Chunked {
region,
shape,
chunk_dims,
element_size,
maxshape,
pipeline_message,
payload,
..
} => {
let deferred;
let chunk_bytes = match payload {
ChunkPayload::Encoded(chunk_bytes) => chunk_bytes,
ChunkPayload::Deferred { bytes, padding, dt } => {
let raw = self.resolve_overwrite_bytes(bytes)?;
deferred = split_and_encode_chunks(
&raw,
shape,
chunk_dims,
*element_size,
padding,
pipeline_message.as_deref(),
dt,
)?;
&deferred
}
};
let meta: Vec<ChunkMeta> = chunk_bytes
.iter()
.map(|c| ChunkMeta {
compressed_size: c.len() as u64,
filter_mask: 0,
})
.collect();
self.write_chunked_relocatable(
region,
shape,
chunk_dims,
*element_size,
maxshape.as_deref(),
pipeline_message.as_deref(),
&meta,
chunk_bytes,
&[],
)
}
MovingWrite::AppendedChunks {
region,
new_dataspace_body,
chunk_dims_u32,
element_size,
has_filters,
kept_chunks,
new_chunk_bytes,
..
} => self.write_appended_chunks(
region,
new_dataspace_body,
chunk_dims_u32,
*element_size,
*has_filters,
kept_chunks,
new_chunk_bytes,
),
MovingWrite::AttrEdit { region, attrs } => {
let mut region = region.clone();
self.place_edited_attrs(&mut region, attrs.clone())?;
let oh = build_v2_object_header(®ion)?;
self.alloc_or_append_typed(&oh, PageType::Meta)
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "the append rebuild needs the header region, grown dataspace, chunk \
geometry, and both chunk sets; bundling them into a struct would only move the list"
)]
fn write_appended_chunks(
&mut self,
region: &[u8],
new_dataspace_body: &[u8],
chunk_dims_u32: &[u32],
element_size: NonZeroUsize,
has_filters: bool,
kept_chunks: &[WrittenChunk],
new_chunk_bytes: &[Vec<u8>],
) -> Result<u64, Error> {
let base = self.superblock.base_address;
let chunk_bytes =
full_chunk_bytes(chunk_dims_u32.iter().map(|&d| u64::from(d)), element_size);
let chunk_total: u64 = new_chunk_bytes.iter().map(|cb| cb.len() as u64).sum();
let placed_chunks = |blob_stored: u64| -> Vec<WrittenChunk> {
let mut combined: Vec<WrittenChunk> = kept_chunks.to_vec();
let mut offset = blob_stored;
for cb in new_chunk_bytes {
combined.push(WrittenChunk {
address: offset,
compressed_size: cb.len() as u64,
filter_mask: 0,
});
offset += cb.len() as u64;
}
combined
};
let sizing = placed_chunks(0);
let ea_len = extensible_array_len(
&crate::chunked_write::IndexSlots::dense(&sizing),
chunk_bytes,
OFFSET_SIZE,
LENGTH_SIZE,
has_filters,
);
let ea =
|slots: &crate::chunked_write::IndexSlots<'_>, at: u64| -> Result<Vec<u8>, Error> {
build_extensible_array_at(
slots,
chunk_bytes,
OFFSET_SIZE,
LENGTH_SIZE,
has_filters,
at,
)
.map_err(Error::Format)
};
let blob = match self.paged {
Some(_) => self.alloc_free(chunk_total + ea_len, PageType::Raw),
None => None,
};
let ea_stored = match blob {
Some(addr) => {
let placed = (|| -> Result<u64, Error> {
let blob_stored = base.relative(addr)?;
let combined = placed_chunks(blob_stored);
let mut buf =
Vec::with_capacity(usize::try_from(chunk_total + ea_len).unwrap_or(0));
for cb in new_chunk_bytes {
buf.extend_from_slice(cb);
}
buf.extend_from_slice(&ea(
&crate::chunked_write::IndexSlots::dense(&combined),
blob_stored + chunk_total,
)?);
self.place(
Placement::Reused {
addr,
len: chunk_total + ea_len,
},
&buf,
)?;
Ok(blob_stored + chunk_total)
})();
match placed {
Ok(ea_stored) => ea_stored,
Err(e) => {
self.release_raw_alloc(addr, chunk_total + ea_len);
return Err(e);
}
}
}
None => {
let mut combined: Vec<WrittenChunk> = kept_chunks.to_vec();
for cb in new_chunk_bytes {
let abs = self.alloc_or_append_typed(cb, PageType::Raw)?;
combined.push(WrittenChunk {
address: base.relative(abs)?,
compressed_size: cb.len() as u64,
filter_mask: 0,
});
}
let (ea_addr, ()) = self.place_relocatable(ea_len, PageType::Raw, |at| {
Ok((
ea(&crate::chunked_write::IndexSlots::dense(&combined), at)?,
(),
))
})?;
base.relative(ea_addr)?
}
};
#[expect(
clippy::cast_possible_truncation,
reason = "element size is a datatype byte width that fits u32"
)]
let layout_body = serialize_v4_extensible_array(
chunk_dims_u32,
ea_stored,
OFFSET_SIZE,
element_size.get() as u32,
);
let region = replace_dataspace_message(region, new_dataspace_body)?;
let region = replace_layout_message(®ion, &layout_body)?;
let oh = build_v2_object_header(®ion)?;
self.alloc_or_append_typed(&oh, PageType::Meta)
}
fn append(&mut self, bytes: &[u8]) -> Result<u64, Error> {
self.image.append(bytes)
}
fn write_at(&mut self, offset: usize, bytes: &[u8]) -> Result<(), Error> {
self.image.write_at(offset as u64, bytes)
}
fn begin_page(&mut self, ty: PageType) -> Result<(), Error> {
let Self { image, paged, .. } = self;
match paged.as_mut() {
Some(pg) => pg.begin(image.as_mut(), ty),
None => Ok(()),
}
}
fn alloc_or_append_typed(&mut self, bytes: &[u8], ty: PageType) -> Result<u64, Error> {
let at = self.reserve(bytes.len() as u64, ty)?;
self.place(at, bytes)
}
fn reserve(&mut self, len: u64, ty: PageType) -> Result<Placement, Error> {
if let Some(addr) = self.alloc_free(len, ty) {
return Ok(Placement::Reused { addr, len });
}
self.begin_page(ty)?;
Ok(Placement::Appended {
addr: self.image.len(),
len,
})
}
fn alloc_free(&mut self, len: u64, ty: PageType) -> Option<u64> {
let Some(pg) = self.paged.as_mut() else {
return self.free.alloc(len);
};
pg.alloc_typed(len, ty)
}
fn release_raw_alloc(&mut self, addr: u64, len: u64) {
match self.paged.as_mut() {
Some(pg) => pg.raw.free(addr, len),
None => self.free.free(addr, len),
}
}
fn place(&mut self, at: Placement, bytes: &[u8]) -> Result<u64, Error> {
if bytes.len() as u64 != at.len() {
return Err(Error::Format(FormatError::SerializationError(format!(
"a placement reserved {} bytes but built {}",
at.len(),
bytes.len()
))));
}
match at {
Placement::Reused { addr, .. } => {
self.write_at(
usize::try_from(addr).map_err(|_| {
Error::EditUnsupported("free-region address exceeds this platform")
})?,
bytes,
)?;
Ok(addr)
}
Placement::Appended { addr, .. } => {
let written = self.append(bytes)?;
debug_assert_eq!(
written, addr,
"an appended placement must land at end-of-file"
);
Ok(written)
}
}
}
fn place_relocatable<T>(
&mut self,
len: u64,
ty: PageType,
build: impl FnOnce(u64) -> Result<(Vec<u8>, T), Error>,
) -> Result<(u64, T), Error> {
let at = self.reserve(len, ty)?;
let (bytes, extra) = build(self.superblock.base_address.relative(at.address())?)?;
let addr = self.place(at, &bytes)?;
Ok((addr, extra))
}
fn place_vl_collections(&mut self, collections: &[Vec<u8>]) -> Result<Vec<u64>, Error> {
collections
.iter()
.map(|collection| {
let addr = self.alloc_or_append_typed(collection, PageType::Meta)?;
self.superblock
.base_address
.relative(addr)
.map_err(Error::from)
})
.collect()
}
fn resolve_overwrite_bytes<'b>(
&mut self,
bytes: &'b OverwriteBytes,
) -> Result<Cow<'b, [u8]>, Error> {
let Some(vlen) = &bytes.vlen else {
return Ok(Cow::Borrowed(&bytes.raw));
};
let staging = &vlen.staging;
let superseded = self
.vl_overwrite_heaps
.get(&vlen.path)
.cloned()
.unwrap_or_default();
let mut raw = bytes.raw.clone();
let base = self.superblock.base_address;
let addrs = self.place_vl_collections(&staging.collections)?;
patch_vl_refs_masked(&mut raw, &staging.patch_offsets, &addrs);
let placed = addrs
.iter()
.zip(&staging.collections)
.map(|(&a, c)| Ok((base.absolute(a)?, c.len() as u64)))
.collect::<Result<Vec<_>, FormatError>>()?;
self.vl_overwrite_heaps.insert(vlen.path.clone(), placed);
self.superseded_heaps.extend(superseded);
Ok(Cow::Owned(raw))
}
fn invalidate_heap_provenance(&mut self, staged: &StagedEdits) {
if self.vl_overwrite_heaps.is_empty() {
return;
}
let aliasing_edit = !staged.copies.is_empty()
|| !staged.cross_copies.is_empty()
|| staged
.writes
.iter()
.chain(&staged.datasets)
.any(|(_, fd)| {
fd.vl_string_staging.is_none()
&& datatype_holds_file_address(&fd.dt)
});
if aliasing_edit {
self.vl_overwrite_heaps.clear();
return;
}
for path in &staged.deletes {
self.vl_overwrite_heaps
.retain(|recorded, _| !paths_overlap(recorded, path));
}
}
fn resolve_reference_target(
target: &ObjectRefTarget,
path_addr: &BTreeMap<PathKey, u64>,
nodes: &BTreeMap<PathKey, Node>,
add_targets: &[PathKey],
write_targets: &[PathKey],
delete_targets: &[PathKey],
invalidated: &InvalidatedAddresses,
src: &(impl Source + ?Sized),
superblock: &Superblock,
) -> Result<u64, Error> {
let path = match target {
ObjectRefTarget::Raw(addr) => {
if let Some(refusal) = invalidated.refusal(*addr) {
return Err(Error::EditUnsupported(refusal));
}
return Ok(*addr);
}
ObjectRefTarget::Path(path) => path,
};
let base = superblock.base_address;
let key = split_path(path);
if let Some(&addr) = path_addr.get(&key) {
return base.relative(addr).map_err(Error::from);
}
if nodes.contains_key(&key)
|| add_targets.iter().any(|t| is_prefix(t, &key))
|| write_targets.contains(&key)
{
return Err(Error::EditUnsupported(
"an object-reference dataset targets a path this commit is still writing; \
use separate commits",
));
}
if delete_targets.iter().any(|d| is_prefix(d, &key)) {
return Err(Error::EditUnsupported(
"an object-reference dataset targets an object this commit deletes, or one \
under it; the reference would be left pointing at storage the delete can \
reclaim",
));
}
match crate::group_v2::resolve_path_any_from_source(src, superblock, path) {
Ok(addr) => base.relative(addr).map_err(Error::from),
Err(_) => Ok(UNDEF),
}
}
fn preflight_reference_targets(
keys: &[PathKey],
flat: &BTreeMap<&PathKey, Vec<&FlatDataset>>,
nodes: &BTreeMap<PathKey, Node>,
add_targets: &[PathKey],
write_targets: &[PathKey],
delete_targets: &[PathKey],
invalidated: &InvalidatedAddresses,
src: &(impl Source + ?Sized),
superblock: &Superblock,
) -> Result<(), Error> {
let mut by_depth = keys.to_vec();
by_depth.sort_by_key(|k| std::cmp::Reverse(k.len()));
let mut sim_addr: BTreeMap<PathKey, u64> = BTreeMap::new();
for key in &by_depth {
if let Some(datasets) = flat.get(key) {
let mut ordered: Vec<&FlatDataset> = datasets.to_vec();
ordered.sort_by_key(|fd| fd.reference_targets.is_some());
for fd in ordered {
if let Some(patches) = &fd.reference_targets {
for patch in patches {
Self::resolve_reference_target(
&patch.target,
&sim_addr,
nodes,
add_targets,
write_targets,
delete_targets,
invalidated,
src,
superblock,
)?;
}
}
let mut full = key.clone();
full.push(fd.name.clone());
sim_addr.insert(full, superblock.base_address.get());
}
}
sim_addr.insert(key.clone(), superblock.base_address.get());
}
Ok(())
}
fn build_chunked_dataset(&mut self, fd: &FlatDataset) -> Result<Vec<u8>, Error> {
let chunk_dims = fd.chunk_options.resolve_chunk_dims(&fd.ds.dimensions);
let ctx = ChunkContext::from_datatype(&chunk_dims, &fd.dt)?;
let elem = crate::convert::nonzero_usize_from(ctx.element_size)?;
let fill = crate::fill_value::FillPattern::new(fd.fill.as_deref(), elem);
let set = compress_chunks(
&fd.raw,
&fd.ds.dimensions,
ctx,
&fd.chunk_options,
fd.maxshape.as_deref(),
fill,
StorageAllocation::Allocated,
)?;
let (_addr, (layout_message, pipeline_message)) =
self.place_relocatable(chunked_data_len(&set)?, PageType::Raw, |stored_base| {
let result = assemble_chunked_at(&set, stored_base)?;
Ok((
result.data_bytes,
(result.layout_message, result.pipeline_message),
))
})?;
let attr_info = self.place_dense_attrs_if_needed(fd)?;
Ok(build_chunked_dataset_oh(
&fd.dt,
&DatatypeLocation::Inline,
&fd.ds,
&layout_message,
pipeline_message.as_deref(),
&fd.attrs,
attr_info.as_deref(),
fd.fill.as_deref(),
)?)
}
fn oh_chunk_spans(&self, addr: usize) -> Result<Vec<(u64, u64)>, Error> {
Ok(
read_oh_chunks(&self.image(), addr as u64, self.superblock.base_address)?
.into_iter()
.map(|chunk| chunk.span)
.collect(),
)
}
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_from_source(&self.image(), off as u64, 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_from_source(&self.image(), &header, os, ls, base).ok()?;
for e in entries {
let child = base.absolute(e.object_header_address).ok()?;
*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, FreeClass)>,
) {
let base = self.superblock.base_address;
let file_len = self.image().len();
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.image(), addr as u64, self.superblock.base_address) {
Ok(ObjModel::DatasetVerbatim { .. }) => out.extend(meta_spans(spans)),
Ok(ObjModel::DatasetContiguous {
data_addr,
data_size,
..
}) => {
out.extend(meta_spans(spans));
if data_addr != u64::MAX && data_size > 0 {
if let (Some(abs), Ok(len)) =
(base.absolute(data_addr).ok(), usize::try_from(data_size))
{
if let Ok(start) = usize::try_from(abs) {
if start.checked_add(len).is_some_and(|e| e as u64 <= file_len) {
out.push((abs, data_size, FreeClass::Page(PageType::Raw)));
}
}
}
}
}
Ok(ObjModel::Group { children, .. }) => {
out.extend(meta_spans(spans));
for (_, child) in children {
if let Some(c) = base
.absolute(child)
.ok()
.and_then(|a| usize::try_from(a).ok())
{
self.collect_free_spans(c, depth + 1, incoming, out);
}
}
}
Ok(ObjModel::DatasetChunked { .. }) => {
if let Some(storage) = self.chunked_storage_spans(addr) {
out.extend(meta_spans(spans));
out.extend(storage);
}
}
Err(_) => {}
}
}
fn chunked_storage_spans(&self, addr: usize) -> Option<Vec<(u64, u64, FreeClass)>> {
let region =
Self::gather_oh_messages(&self.image(), addr as u64, self.superblock.base_address)
.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 base = self.superblock.base_address;
let split = crate::chunked_read::collect_chunked_storage_spans(
&BaseOffsetSource {
inner: &self.image(),
base,
},
&layout,
&dataspace,
OFFSET_SIZE,
LENGTH_SIZE,
)
.ok()?;
let mut data: Vec<(u64, u64)> = Vec::with_capacity(split.data.len());
for (addr, len) in split.data {
data.push((base.absolute(addr).ok()?, len));
}
let mut index: Vec<(u64, u64)> = Vec::with_capacity(split.index.len());
for (addr, len) in split.index {
index.push((base.absolute(addr).ok()?, len));
}
let mut plain: Vec<(u64, u64)> = data.iter().chain(index.iter()).copied().collect();
if !spans_disjoint_in_bounds(&mut plain, self.image.len()) {
return None;
}
let index_class = if self.index_is_provably_raw(&data, &index) {
FreeClass::Page(PageType::Raw)
} else {
FreeClass::Dead
};
let mut spans: Vec<(u64, u64, FreeClass)> = data
.iter()
.map(|&(a, l)| (a, l, FreeClass::Page(PageType::Raw)))
.collect();
spans.extend(index.into_iter().map(|(a, l)| (a, l, index_class)));
Some(spans)
}
fn index_is_provably_raw(&self, data: &[(u64, u64)], index: &[(u64, u64)]) -> bool {
match &self.paged {
None => true,
Some(paged) => {
index_abuts_chunk_data(data, index)
&& !index_touches_page_zero(index, paged.page_size)
}
}
}
fn chunked_index_spans(&self, addr: usize) -> Option<Vec<(u64, u64)>> {
let region =
Self::gather_oh_messages(&self.image(), addr as u64, self.superblock.base_address)
.ok()?;
let mut layout_msg: Option<(usize, usize)> = None;
let mut p = 0;
loop {
match next_message(®ion, p) {
Ok(Some((msg_type, body, body_end))) => {
if msg_type == MessageType::DataLayout {
layout_msg = Some((body, body_end));
}
p = body_end;
}
Ok(None) => break,
Err(_) => return None,
}
}
let (lb, le) = layout_msg?;
let layout = DataLayout::parse(®ion[lb..le], OFFSET_SIZE, LENGTH_SIZE).ok()?;
if !matches!(layout, DataLayout::Chunked { .. }) {
return None;
}
let base = self.superblock.base_address;
let mut spans = chunk_index_spans_from_source(
&BaseOffsetSource {
inner: &self.image(),
base,
},
&layout,
OFFSET_SIZE,
LENGTH_SIZE,
)
.ok()?;
for (a, _) in &mut spans {
*a = base.absolute(*a).ok()?;
}
if !spans_disjoint_in_bounds(&mut spans, self.image.len()) {
return None;
}
Some(spans)
}
}
#[derive(Default)]
struct Node {
is_new: bool,
deletes: Vec<String>,
copies: Vec<(String, CopyTree)>,
cross_copies: Vec<String>,
writes: Vec<(String, u64, MovingWrite)>,
base_region: Vec<u8>,
existing_links: Vec<String>,
attrs: EditedAttrs,
}
enum AttrOp {
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>,
},
DatasetChunked {
region: Vec<u8>,
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: Option<Vec<u8>>,
dense_attrs: Vec<crate::attribute::AttributeMessage>,
},
DatasetChunked {
region: Vec<u8>,
shape: Vec<u64>,
chunk_dims: Vec<u64>,
element_size: NonZeroUsize,
maxshape: Option<Vec<u64>>,
pipeline_message: Option<Vec<u8>>,
meta: Vec<ChunkMeta>,
chunk_bytes: Vec<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 InvalidatedAddresses {
removed: Vec<(u64, u64)>,
moved: Vec<u64>,
base: BaseAddress,
}
impl InvalidatedAddresses {
fn is_empty(&self) -> bool {
self.removed.is_empty() && self.moved.is_empty()
}
fn refusal(&self, stored: u64) -> Option<&'static str> {
if stored == 0 || stored == UNDEF {
return None;
}
let abs = self.base.absolute(stored).ok()?;
if self
.removed
.iter()
.any(|&(off, len)| abs >= off && abs - off < len)
{
return Some(REFERENCE_INTO_RECLAIMED_SPACE);
}
if self.moved.contains(&abs) {
return Some(REFERENCE_TO_A_MOVED_OBJECT);
}
None
}
}
struct GroupInfo {
region: Vec<u8>,
link_names: Vec<String>,
}
struct OverwriteBytes {
raw: Vec<u8>,
vlen: Option<VlenOverwrite>,
}
struct VlenOverwrite {
staging: VlStringStaging,
path: PathKey,
}
impl OverwriteBytes {
fn ready(raw: Vec<u8>) -> Self {
Self { raw, vlen: None }
}
}
enum WritePlan {
InPlace {
data_addr: usize,
bytes: OverwriteBytes,
},
InPlaceChunks { writes: Vec<(usize, Vec<u8>)> },
Moving(MovingWrite),
}
enum MovingWrite {
Contiguous {
region: Vec<u8>,
addr_off: usize,
bytes: OverwriteBytes,
old_extent: Option<(u64, u64)>,
},
Compact {
region: Vec<u8>,
bytes: OverwriteBytes,
},
Chunked {
region: Vec<u8>,
shape: Vec<u64>,
chunk_dims: Vec<u64>,
element_size: NonZeroUsize,
maxshape: Option<Vec<u64>>,
pipeline_message: Option<Vec<u8>>,
payload: ChunkPayload,
old_addr: u64,
},
AppendedChunks {
region: Vec<u8>,
new_dataspace_body: Vec<u8>,
chunk_dims_u32: Vec<u32>,
element_size: NonZeroUsize,
has_filters: bool,
kept_chunks: Vec<WrittenChunk>,
new_chunk_bytes: Vec<Vec<u8>>,
old_addr: u64,
old_tail_extent: Option<(u64, u64)>,
},
AttrEdit { region: Vec<u8>, attrs: EditedAttrs },
}
enum ChunkPayload {
Encoded(Vec<Vec<u8>>),
Deferred {
bytes: OverwriteBytes,
padding: crate::fill_value::PaddingFill,
dt: crate::datatype::Datatype,
},
}
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>>,
vl_attrs: Vec<(usize, Vec<Vec<u8>>)>,
attrs_are_dense: bool,
vl_string_staging: Option<VlStringStaging>,
reference_targets: Option<Vec<ObjectRefPatch>>,
fill: Option<Vec<u8>>,
#[cfg(feature = "provenance")]
provenance: Option<StagedProvenance>,
}
#[cfg(feature = "provenance")]
struct StagedProvenance {
inputs: crate::provenance::Provenance,
attrs_start: usize,
}
#[cfg(feature = "provenance")]
impl FlatDataset {
fn rebuild_provenance(&mut self) {
let Some(prov) = &self.provenance else {
return;
};
self.attrs.truncate(prov.attrs_start);
let rebuilt = prov.inputs.build_attrs(&self.raw);
self.attrs.extend(rebuilt);
}
}
struct EditStore<'a> {
image: &'a mut dyn FileImage,
superblock: &'a mut Superblock,
sb_sig_off: usize,
paged: Option<&'a mut PagedEdit>,
free: Option<&'a mut FreeList>,
sync_policy: SyncPolicy,
}
impl EditStore<'_> {
fn append_into_raw_page(&mut self, bytes: &[u8]) -> Result<u64, Error> {
if let Some(pg) = self.paged.as_deref_mut() {
pg.begin(self.image, PageType::Raw)?;
}
self.image.append(bytes)
}
}
impl crate::source::Source for EditStore<'_> {
fn len(&self) -> u64 {
self.image.len()
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), crate::error::FormatError> {
self.image.read_at(offset, buf)
}
fn read_metadata_at(
&self,
offset: u64,
len: usize,
) -> Result<Vec<u8>, crate::error::FormatError> {
self.image.read_metadata_at(offset, len)
}
fn metadata_cache_stats(&self) -> Option<crate::source::MetadataCacheStats> {
self.image.metadata_cache_stats()
}
fn reset_metadata_cache_stats(&self) {
self.image.reset_metadata_cache_stats();
}
}
impl Store for EditStore<'_> {
fn offset_size(&self) -> u8 {
self.superblock.offset_size
}
fn length_size(&self) -> u8 {
self.superblock.length_size
}
fn alloc_raw(&mut self, bytes: &[u8]) -> Result<u64, Error> {
if let Some(free) = self.free.as_deref_mut() {
if let Some(addr) = free.alloc(bytes.len() as u64) {
self.image.write_at(addr, bytes)?;
return Ok(addr);
}
}
self.append_into_raw_page(bytes)
}
fn write_at(&mut self, offset: u64, bytes: &[u8]) -> Result<(), Error> {
self.image.write_at(offset, bytes)
}
fn patch_superblock_eof(&mut self) -> Result<(), Error> {
let eof = self.image.len();
self.superblock.eof_address = eof;
let bytes = self.superblock.serialize();
self.write_at(self.sb_sig_off as u64, &bytes)
}
fn sync(&mut self) -> Result<(), Error> {
barrier_data(self.image, self.sync_policy)
}
}
fn paths_overlap(a: &[String], b: &[String]) -> bool {
a.starts_with(b) || b.starts_with(a)
}
fn claims_conflict(a: Option<&[String]>, b: Option<&[String]>) -> bool {
match (a, b) {
(Some(x), Some(y)) => x == y,
_ => true,
}
}
pub(crate) fn as_inplace_error(e: Error) -> Error {
match e {
Error::AppendUnsupported(m) => Error::AppendInPlaceUnsupported(m),
other => other,
}
}
pub(crate) fn validate_gathered_append(st: &LocatedState, b: &AppendBuilder) -> Result<u64, Error> {
let raw = b.raw();
if raw.len() % st.element_size != 0 {
return Err(Error::AppendInPlaceUnsupported(
"appended byte length is not a whole number of elements",
));
}
match b.elem_dt() {
Some(expected) if *expected != st.datatype => {
return Err(Error::AppendInPlaceUnsupported(
"append datatype does not match the on-disk dataset (wrong element \
type or byte order)",
));
}
Some(_) => {}
None => {
if !datatype_is_raw_appendable(&st.datatype) {
return Err(Error::AppendInPlaceUnsupported(
"append_raw onto this dataset's datatype (non-little-endian, \
variable-length, or reference) could misencode the bytes; use a \
typed append",
));
}
}
}
Ok((raw.len() / st.element_size) as u64)
}
pub(crate) fn locate_dataset_state<F: Store>(
file: &F,
oh_addr: u64,
) -> Result<LocatedState, Error> {
let result = Located::locate_at(file, oh_addr, Error::AppendInPlaceUnsupported)?;
if result.located.chunk_elems == 0 {
return Err(Error::AppendInPlaceUnsupported(
"in-place append requires a nonzero chunk length",
));
}
let (dt_off, dt_size) = result.spans.datatype;
let dt_bytes = file
.read_metadata_at(dt_off, dt_size)
.map_err(|_| Error::AppendInPlaceUnsupported("dataset datatype could not be parsed"))?;
let (datatype, _) = Datatype::parse(&dt_bytes)
.map_err(|_| Error::AppendInPlaceUnsupported("dataset datatype could not be parsed"))?;
let pipeline = match result.spans.filter {
Some((fb, fsize)) => {
let fp_bytes = file.read_metadata_at(fb, fsize).map_err(|_| {
Error::AppendInPlaceUnsupported("dataset filter pipeline could not be parsed")
})?;
let parsed = FilterPipeline::parse(&fp_bytes).map_err(|_| {
Error::AppendInPlaceUnsupported("dataset filter pipeline could not be parsed")
})?;
if !pipeline_reencodable(&parsed) {
return Err(Error::AppendInPlaceUnsupported(
"dataset uses a filter this engine cannot re-encode",
));
}
Some(parsed)
}
None => None,
};
let element_size = result.located.elem_bytes;
let spatial = vec![result.located.chunk_elems];
let fill = match result.spans.fill {
Some((msg_type, off, size)) => match file.read_metadata_at(off, size) {
Ok(body) => crate::fill_value::PaddingFill::from_message(msg_type, &body),
Err(_) => crate::fill_value::PaddingFill::Unknown,
},
None => crate::fill_value::PaddingFill::Zero,
};
Ok(LocatedState {
loc: result.located,
datatype,
spatial,
element_size,
pipeline,
fill,
})
}
fn split_path(path: &str) -> PathKey {
path.split('/')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
fn group_by_parent<K: Ord, T>(items: impl IntoIterator<Item = (K, T)>) -> BTreeMap<K, Vec<T>> {
let mut out: BTreeMap<K, Vec<T>> = BTreeMap::new();
for (parent, item) in items {
out.entry(parent).or_default().push(item);
}
out
}
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, FreeClass)>, 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 index_abuts_chunk_data(data: &[(u64, u64)], index: &[(u64, u64)]) -> bool {
if index.is_empty() {
return true;
}
let (Some(index_start), Some(index_end)) = (
index.iter().map(|&(a, _)| a).min(),
index.iter().filter_map(|&(a, l)| a.checked_add(l)).max(),
) else {
return false;
};
data.iter().any(|&(addr, len)| {
len > 0 && (addr.checked_add(len) == Some(index_start) || addr == index_end)
})
}
fn index_touches_page_zero(index: &[(u64, u64)], page_size: u64) -> bool {
if page_size == 0 {
return !index.is_empty();
}
index.iter().any(|&(addr, len)| len > 0 && addr < page_size)
}
fn meta_spans(spans: Vec<(u64, u64)>) -> impl Iterator<Item = (u64, u64, FreeClass)> {
spans
.into_iter()
.map(|(a, l)| (a, l, FreeClass::Page(PageType::Meta)))
}
fn flatten_dataset(db: DatasetBuilder) -> Result<FlatDataset, Error> {
if db.name.is_empty() {
return Err(Error::EditUnsupported("dataset path has an empty name"));
}
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"))?;
let is_empty = shape.contains(&0);
let chunked = db.chunk_options.is_chunked() || db.maxshape.is_some();
if db.allocation == StorageAllocation::Unallocated {
return Err(Error::EditUnsupported(
"a dataset with unallocated storage cannot be added to an existing file in place",
));
}
if db.vl_string_staging.is_some() && chunked {
return Err(Error::EditUnsupported(
"chunked or extensible variable-length-string datasets cannot be added in place yet",
));
}
if db.reference_targets.is_some() && chunked {
return Err(Error::EditUnsupported(
"chunked or extensible object-reference datasets cannot be added in place yet",
));
}
let raw = if is_empty {
db.data.unwrap_or_default()
} else {
db.data
.ok_or(Error::EditUnsupported("dataset has no data"))?
};
let elem_size = dt.element_size_usize()?;
let elem = elem_size.get() as u64;
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",
));
}
}
if chunked {
db.chunk_options
.validate_geometry(&shape, db.maxshape.as_deref())
.map_err(Error::EditUnsupported)?;
db.chunk_options
.refuse_unavailable_filters()
.map_err(Error::EditUnsupported)?;
let chunk_dims = db.chunk_options.resolve_chunk_dims(&shape);
let ctx = ChunkContext::from_datatype(&chunk_dims, &dt)?;
db.chunk_options
.build_pipeline(
&ctx,
crate::fill_value::FillPattern::new(
db.fill.as_deref(),
crate::convert::nonzero_usize_from(ctx.element_size)?,
),
)
.map_err(|_| {
Error::EditUnsupported(
"this dataset's filter pipeline cannot be added in place \
(an unsupported filter, an incompatible datatype, or a \
fill value the filter cannot record)",
)
})?;
}
if make_link(&db.name, 0).serialize(OFFSET_SIZE).len() > OBJECT_HEADER_MESSAGE_MAX {
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 mut attrs: Vec<crate::attribute::AttributeMessage> = Vec::with_capacity(db.attrs.len());
for (n, v) in &db.attrs {
attrs.push(v.to_message(n));
}
let mut vl_attrs: Vec<(usize, Vec<Vec<u8>>)> = Vec::new();
for (i, (_, v)) in db.attrs.iter().enumerate() {
if let Some(strings) = v.var_len_strings() {
vl_attrs.push((i, build_global_heap_collections(strings)));
}
}
#[cfg(feature = "provenance")]
let provenance = db.provenance.as_ref().map(|prov| {
let inputs = crate::provenance::Provenance {
creator: prov.creator.clone(),
timestamp: prov.timestamp.clone(),
source: prov.source.clone(),
};
let attrs_start = attrs.len();
attrs.extend(inputs.build_attrs(&raw));
StagedProvenance {
inputs,
attrs_start,
}
});
let attrs_are_dense = crate::file_writer::needs_dense_attrs(&attrs);
if attrs_are_dense {
crate::file_writer::dense_attrs_check(&attrs).map_err(Error::Format)?;
}
if db.datatype_location.is_committed()
|| attrs.iter().any(|a| a.datatype_location.is_committed())
{
return Err(Error::EditUnsupported(
"a dataset or attribute naming a committed (shared) datatype cannot be added in \
place; write the file with FileBuilder instead",
));
}
if let Some(fill) = &db.fill {
let expected = elem.to_usize()?;
if fill.len() != expected {
return Err(Error::Format(FormatError::FillValueSizeMismatch {
expected,
actual: fill.len(),
}));
}
}
Ok(FlatDataset {
name: db.name,
dt,
ds,
raw,
attrs,
chunk_options: db.chunk_options,
maxshape: db.maxshape,
vl_attrs,
attrs_are_dense,
vl_string_staging: db.vl_string_staging,
reference_targets: db.reference_targets,
fill: db.fill,
#[cfg(feature = "provenance")]
provenance,
})
}
const GROUP_INFO_BODY: [u8; 2] = [0, 0];
fn chunk_index_enumerable(version: u8, chunk_index_type: Option<u8>) -> bool {
matches!((version, chunk_index_type), (3, _) | (4, Some(1..=4)))
}
pub(crate) fn pipeline_reencodable(pipeline: &FilterPipeline) -> bool {
pipeline.filters.iter().all(|f| match f.filter_id {
FILTER_DEFLATE | FILTER_SHUFFLE | FILTER_FLETCHER32 | FILTER_SCALEOFFSET | FILTER_LZF => {
true
}
#[cfg(feature = "zfp")]
crate::filter_pipeline::FILTER_ZFP => true,
_ => false,
})
}
pub(crate) fn pipeline_lossless(pipeline: &FilterPipeline) -> bool {
pipeline.filters.iter().all(|f| match f.filter_id {
FILTER_DEFLATE | FILTER_SHUFFLE | FILTER_FLETCHER32 | FILTER_LZF => true,
FILTER_SCALEOFFSET => matches!(
crate::scaleoffset::scale_offset_mode(&f.client_data),
Some((crate::scaleoffset::ScaleOffset::Integer(_), _))
),
_ => false,
})
}
pub(crate) const LOSSY_TAIL_REFUSAL: &str = "this dataset's filter pipeline is lossy (ZFP, or float D-scale scale-offset), and its \
length is not a whole multiple of the chunk length: growing that trailing chunk would \
decode and re-encode values that are already committed, changing them. Append whole \
chunks from a chunk-aligned length instead";
fn replace_layout_message(region: &[u8], new_layout_body: &[u8]) -> Result<Vec<u8>, Error> {
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(region, p)? {
if msg_type == MessageType::DataLayout && !replaced {
out.extend_from_slice(®ion_message(MessageType::DataLayout, new_layout_body));
replaced = true;
} else {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if !replaced {
return Err(Error::EditUnsupported(
"chunked dataset header has no data-layout message to relocate",
));
}
Ok(out)
}
fn replace_dataspace_message(region: &[u8], new_dataspace_body: &[u8]) -> Result<Vec<u8>, Error> {
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(region, p)? {
if msg_type == MessageType::Dataspace && !replaced {
out.extend_from_slice(®ion_message(MessageType::Dataspace, new_dataspace_body));
replaced = true;
} else {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if !replaced {
return Err(Error::AppendUnsupported(
"dataset header has no dataspace message to grow",
));
}
Ok(out)
}
pub(crate) fn datatype_is_raw_appendable(dt: &Datatype) -> bool {
match dt {
Datatype::FixedPoint { byte_order, .. }
| Datatype::FloatingPoint { byte_order, .. }
| Datatype::Time { byte_order, .. }
| Datatype::BitField { byte_order, .. } => *byte_order == DatatypeByteOrder::LittleEndian,
Datatype::String { .. } | Datatype::Opaque { .. } => true,
Datatype::Enumeration { base_type, .. } | Datatype::Array { base_type, .. } => {
datatype_is_raw_appendable(base_type)
}
Datatype::Compound { members, .. } => members
.iter()
.all(|m| datatype_is_raw_appendable(&m.datatype)),
Datatype::VariableLength { .. } | Datatype::Reference { .. } => false,
}
}
struct ChunkedHeaderParts {
dt: crate::datatype::Datatype,
ds: Dataspace,
layout: DataLayout,
pipeline_message: Option<Vec<u8>>,
}
fn parse_chunked_header(region: &[u8]) -> Result<ChunkedHeaderParts, Error> {
let mut datatype: Option<(usize, usize)> = None;
let mut dataspace: Option<(usize, usize)> = None;
let mut layout: Option<(usize, usize)> = None;
let mut pipeline: Option<(usize, usize)> = None;
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(region, 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 => pipeline = Some((body, body_end)),
_ => {}
}
p = body_end;
}
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 (dt, _) = crate::datatype::Datatype::parse(®ion[dt_b..dt_e])
.map_err(|_| Error::EditUnsupported("dataset header datatype could not be parsed"))?;
let ds = Dataspace::parse(®ion[ds_b..ds_e], LENGTH_SIZE)
.map_err(|_| Error::EditUnsupported("dataset header dataspace could not be parsed"))?;
let dl = DataLayout::parse(®ion[lb..le], OFFSET_SIZE, LENGTH_SIZE)
.map_err(|_| Error::EditUnsupported("dataset header data layout could not be parsed"))?;
if !matches!(dl, DataLayout::Chunked { .. }) {
return Err(Error::EditUnsupported("dataset is not chunked"));
}
let pipeline_message = pipeline.map(|(b, e)| region[b..e].to_vec());
Ok(ChunkedHeaderParts {
dt,
ds,
layout: dl,
pipeline_message,
})
}
struct ChunkedGeometry {
spatial: Vec<u64>,
element_size: NonZeroUsize,
raw_size: u64,
maxshape: Option<Vec<u64>>,
}
fn chunked_geometry(
dt: &crate::datatype::Datatype,
ds: &Dataspace,
layout: &DataLayout,
) -> Result<ChunkedGeometry, Error> {
let DataLayout::Chunked {
chunk_dimensions, ..
} = layout
else {
return Err(Error::EditUnsupported("dataset is not chunked"));
};
let rank = ds.dimensions.len();
if chunk_dimensions.len() <= rank {
return Err(Error::EditUnsupported(
"chunked layout has malformed dimensions",
));
}
let spatial: Vec<u64> = chunk_dimensions[..rank]
.iter()
.map(|&c| u64::from(c))
.collect();
let element_size = dt.element_size_usize()?;
let raw_size = spatial
.iter()
.copied()
.product::<u64>()
.saturating_mul(element_size.get() as u64);
let maxshape = ds
.max_dimensions
.as_ref()
.filter(|ms| *ms != &ds.dimensions)
.cloned();
Ok(ChunkedGeometry {
spatial,
element_size,
raw_size,
maxshape,
})
}
fn staged_bytes(fd: &FlatDataset, path: &PathKey) -> OverwriteBytes {
OverwriteBytes {
raw: fd.raw.clone(),
vlen: fd.vl_string_staging.clone().map(|staging| VlenOverwrite {
staging,
path: path.clone(),
}),
}
}
fn split_and_encode_chunks(
raw: &[u8],
shape: &[u64],
chunk_dims: &[u64],
element_size: NonZeroUsize,
padding: &crate::fill_value::PaddingFill,
pipeline_message: Option<&[u8]>,
dt: &crate::datatype::Datatype,
) -> Result<Vec<Vec<u8>>, Error> {
let split = split_into_chunks(
raw,
shape,
chunk_dims,
element_size,
padding.pattern(element_size),
)
.map_err(Error::Format)?;
let Some(pm) = pipeline_message else {
return Ok(split);
};
let pipeline = FilterPipeline::parse(pm)
.map_err(|_| Error::EditUnsupported("dataset filter pipeline could not be parsed"))?;
let ctx = ChunkContext::from_datatype(chunk_dims, dt)?;
let mut encoded = Vec::with_capacity(split.len());
let mut scratch = FilterScratch::new();
for buf in &split {
encoded.push(compress_chunk_with(&mut scratch, buf, &pipeline, ctx)?);
}
Ok(encoded)
}
fn try_inplace_chunk_writes<S: Source + ?Sized>(
src: &S,
layout: &DataLayout,
ds: &Dataspace,
spatial: &[u64],
raw_size: u64,
new_bytes: &[Vec<u8>],
) -> Option<Vec<(usize, Vec<u8>)>> {
let infos = enumerate_chunks_from_source(src, layout, ds, OFFSET_SIZE, LENGTH_SIZE).ok()?;
let grid = plan_dense_grid(infos, &ds.dimensions, spatial)?;
if grid.grid_order.len() != new_bytes.len() {
return None;
}
let mut writes = Vec::with_capacity(new_bytes.len() + 1);
let mut spans: Vec<(u64, u64)> = Vec::with_capacity(new_bytes.len() + 1);
let mut any_shrunk = false;
for (ci, bytes) in grid.grid_order.iter().zip(new_bytes.iter()) {
if ci.filter_mask != 0 {
return None;
}
let new_len = bytes.len() as u64;
let slot = u64::from(ci.chunk_size);
if new_len > slot {
return None;
}
if new_len < slot {
any_shrunk = true;
}
let start = usize::try_from(ci.address).ok()?;
start
.checked_add(bytes.len())
.filter(|&e| e as u64 <= src.len())?;
writes.push((start, bytes.clone()));
spans.push((ci.address, new_len));
}
if any_shrunk {
let (index_addr, index_bytes) = try_rebuild_index_in_place(
src,
layout,
ds,
spatial,
raw_size,
&grid.grid_order,
new_bytes,
)?;
spans.push((index_addr as u64, index_bytes.len() as u64));
writes.push((index_addr, index_bytes));
}
if !spans_disjoint_in_bounds(&mut spans, src.len()) {
return None;
}
Some(writes)
}
#[allow(clippy::too_many_arguments)]
fn try_rebuild_index_in_place<S: Source + ?Sized>(
src: &S,
layout: &DataLayout,
ds: &Dataspace,
spatial: &[u64],
raw_size: u64,
grid_order: &[crate::chunked_read::ChunkInfo],
new_bytes: &[Vec<u8>],
) -> Option<(usize, Vec<u8>)> {
let DataLayout::Chunked {
btree_address: Some(index_addr),
chunk_index_type,
version,
..
} = layout
else {
return None;
};
let written: Vec<crate::chunked_write::WrittenChunk> = grid_order
.iter()
.zip(new_bytes)
.map(|(ci, b)| crate::chunked_write::WrittenChunk {
address: ci.address,
compressed_size: b.len() as u64,
filter_mask: 0,
})
.collect();
let (_, slot_of_chunk, index_slots) = crate::chunked_write::plan_index_slots(
&ds.dimensions,
spatial,
ds.max_dimensions.as_deref(),
raw_size,
true,
crate::chunked_write::StorageAllocation::Allocated,
)
.ok()?;
let slots =
crate::chunked_write::IndexSlots::new(&written, &slot_of_chunk, index_slots).ok()?;
let new_index = match (version, chunk_index_type) {
(4, Some(3)) => crate::chunked_write::build_fixed_array_at(
&slots,
raw_size,
OFFSET_SIZE,
LENGTH_SIZE,
true,
*index_addr,
),
(4, Some(4)) => crate::chunked_write::build_extensible_array_at(
&slots,
raw_size,
OFFSET_SIZE,
LENGTH_SIZE,
true,
*index_addr,
)
.ok()?,
_ => return None,
};
let mut spans =
crate::chunked_read::chunk_index_spans_from_source(src, layout, OFFSET_SIZE, LENGTH_SIZE)
.ok()?;
if spans.is_empty() {
return None;
}
spans.sort_unstable_by_key(|&(a, _)| a);
if spans[0].0 != *index_addr {
return None;
}
let mut end = *index_addr;
for &(a, l) in &spans {
if a != end {
return None; }
end = a.checked_add(l)?;
}
if new_index.len() as u64 != end - *index_addr {
return None;
}
let start = usize::try_from(*index_addr).ok()?;
start
.checked_add(new_index.len())
.filter(|&e| e as u64 <= src.len())?;
Some((start, new_index))
}
struct SliceChunkProvider<'a> {
chunks: &'a [Vec<u8>],
}
impl ChunkProvider for SliceChunkProvider<'_> {
fn chunk_bytes(&self, index: usize, out: &mut Vec<u8>) -> Result<(), FormatError> {
let chunk = self.chunks.get(index).ok_or_else(|| {
FormatError::ChunkedReadError("chunk index out of range for in-memory provider".into())
})?;
out.extend_from_slice(chunk);
Ok(())
}
}
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 ensure_attribute_info(region: &mut Vec<u8>) -> Result<(), Error> {
let mut has_attrs = false;
let mut p = 0;
while let Some((msg_type, _body, body_end)) = next_message(region, p)? {
match msg_type {
MessageType::AttributeInfo => return Ok(()),
MessageType::Attribute => has_attrs = true,
_ => {}
}
p = body_end;
}
if has_attrs {
region.extend_from_slice(®ion_message(
MessageType::AttributeInfo,
&crate::file_writer::compact_attribute_info_message(),
));
}
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",
))
}
const COMPACT_LAYOUT_PREAMBLE: usize = 4;
fn rebuild_compact_layout_region(region: &[u8], raw: &[u8]) -> Result<Vec<u8>, Error> {
if raw.len() > OBJECT_HEADER_MESSAGE_MAX - COMPACT_LAYOUT_PREAMBLE {
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(COMPACT_LAYOUT_PREAMBLE + raw.len());
layout.push(region[body]); layout.push(0); #[expect(
clippy::cast_possible_truncation,
reason = "raw.len() bounded below the u16 inline-size field 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 = "the guard above bounds COMPACT_LAYOUT_PREAMBLE + raw.len(), this \
body's exact length, to the 2-byte message-size field"
)]
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_compact_attr_ops(
region: &[u8],
ops: &[&AttrOp],
) -> Result<(Vec<u8>, PendingVlAttrs), Error> {
let mut out = region.to_vec();
let mut pending_vl: PendingVlAttrs = Vec::new();
for op in ops {
match op {
AttrOp::Set { name, value } => {
pending_vl.retain(|(msg, _)| &msg.name != name);
if let Some(strings) = value.var_len_strings() {
out = remove_attr_from_region(&out, name, false)?;
let msg = build_attr_message(name, value);
if msg.serialize(LENGTH_SIZE).len() > OBJECT_HEADER_MESSAGE_MAX {
return Err(Error::EditUnsupported(
"attribute is too large to encode in place",
));
}
pending_vl.push((msg, build_global_heap_collections(strings)));
} else {
out = set_attr_in_region(&out, name, value)?;
}
}
AttrOp::Remove { name } => {
let before = pending_vl.len();
pending_vl.retain(|(msg, _)| &msg.name != name);
if pending_vl.len() == before {
out = remove_attr_from_region(&out, name, true)?;
}
}
}
}
Ok((out, pending_vl))
}
struct AttrEdits {
region: Vec<u8>,
attrs: EditedAttrs,
}
#[derive(Clone)]
enum EditedAttrs {
Compact(PendingVlAttrs),
Dense(DenseAttrEdit),
}
impl Default for EditedAttrs {
fn default() -> Self {
Self::Compact(Vec::new())
}
}
#[derive(Clone)]
struct DenseAttrEdit {
attrs: Vec<crate::attribute::AttributeMessage>,
vl: Vec<(usize, Vec<Vec<u8>>)>,
}
fn plan_attr_ops<S: Source + ?Sized>(
src: &S,
base: BaseAddress,
addr: Option<u64>,
region: &[u8],
ops: &[&AttrOp],
) -> Result<AttrEdits, Error> {
let dense_now = region_uses_dense_attrs(region)?;
if !dense_now {
let oversized = ops.iter().any(|op| match op {
AttrOp::Set { name, value } => {
build_attr_message(name, value).serialize(LENGTH_SIZE).len()
> OBJECT_HEADER_MESSAGE_MAX
}
AttrOp::Remove { .. } => false,
});
if !oversized {
let (out, pending_vl) = apply_compact_attr_ops(region, ops)?;
let only_removes = !ops.iter().any(|op| matches!(op, AttrOp::Set { .. }));
if only_removes || compact_attr_count(&out)? + pending_vl.len() <= MAX_COMPACT_ATTRS {
return Ok(AttrEdits {
region: out,
attrs: EditedAttrs::Compact(pending_vl),
});
}
}
}
if region_has_shared_attr(region)? {
return Err(Error::EditUnsupported(SHARED_ATTRIBUTE_MESSAGE));
}
let existing = match addr {
Some(addr) => read_object_attrs(src, addr, base)?,
None => Vec::new(),
};
let mut set: Vec<(crate::attribute::AttributeMessage, Option<Vec<Vec<u8>>>)> =
existing.into_iter().map(|a| (a, None)).collect();
for op in ops {
match op {
AttrOp::Set { name, value } => {
let msg = build_attr_message(name, value);
let collections = value.var_len_strings().map(build_global_heap_collections);
match set.iter_mut().find(|(a, _)| &a.name == name) {
Some(slot) => *slot = (msg, collections),
None => set.push((msg, collections)),
}
}
AttrOp::Remove { name } => {
let before = set.len();
set.retain(|(a, _)| &a.name != name);
if set.len() == before {
return Err(Error::EditUnsupported("attribute to remove was not found"));
}
}
}
}
let region = strip_attr_messages(region)?;
if set.is_empty() {
return Ok(AttrEdits {
region,
attrs: EditedAttrs::default(),
});
}
let mut attrs = Vec::with_capacity(set.len());
let mut vl = Vec::new();
for (i, (msg, collections)) in set.into_iter().enumerate() {
if let Some(collections) = collections {
vl.push((i, collections));
}
attrs.push(msg);
}
if !dense_now {
for attr in &attrs {
if crate::reference_patch::attribute_references_are_repointable(&attr.datatype) {
return Err(Error::EditUnsupported(
REFERENCE_ATTRIBUTE_WOULD_LEAVE_THE_HEADER,
));
}
}
}
crate::file_writer::dense_attrs_check(&attrs).map_err(Error::Format)?;
Ok(AttrEdits {
region,
attrs: EditedAttrs::Dense(DenseAttrEdit { attrs, vl }),
})
}
fn region_uses_dense_attrs(region: &[u8]) -> Result<bool, Error> {
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::AttributeInfo
&& attribute_info_is_dense(®ion[body..body_end])
{
return Ok(true);
}
p = body_end;
}
Ok(false)
}
fn region_has_shared_attr(region: &[u8]) -> Result<bool, Error> {
let mut p = 0;
while let Some((msg_type, _body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::Attribute && region[p + 3] != 0 {
return Ok(true);
}
p = body_end;
}
Ok(false)
}
fn strip_attr_messages(region: &[u8]) -> Result<Vec<u8>, Error> {
let mut out = Vec::with_capacity(region.len());
let mut p = 0;
while let Some((msg_type, _body, body_end)) = next_message(region, p)? {
if !matches!(
msg_type,
MessageType::Attribute | MessageType::AttributeInfo
) {
out.extend_from_slice(®ion[p..body_end]);
}
p = body_end;
}
if p < region.len() {
out.extend_from_slice(®ion[p..]);
}
Ok(out)
}
fn read_object_attrs<S: Source + ?Sized>(
src: &S,
addr: u64,
base: BaseAddress,
) -> Result<Vec<crate::attribute::AttributeMessage>, Error> {
let header = ObjectHeader::parse_from_source(src, addr, OFFSET_SIZE, LENGTH_SIZE, base)
.map_err(|_| Error::EditUnsupported("an object header could not be parsed"))?;
if base.get() > src.len() {
return Err(Error::EditUnsupported(
"this file's userblock is larger than the file itself",
));
}
let framed = BaseOffsetSource { inner: src, base };
crate::attribute::extract_attributes_full_from_source(
&framed,
&header,
OFFSET_SIZE,
LENGTH_SIZE,
)
.map_err(|_| {
Error::EditUnsupported("an object's dense (fractal-heap) attributes could not be read")
})
}
pub(crate) fn attribute_info_is_dense(body: &[u8]) -> bool {
match crate::attribute_info::AttributeInfoMessage::parse(body, OFFSET_SIZE) {
Ok(ai) => ai.fractal_heap_address.is_some(),
Err(_) => true,
}
}
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)? {
if msg_type == 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, required: bool) -> 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::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 && required {
return Err(Error::EditUnsupported("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::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(SHARED_ATTRIBUTE_MESSAGE));
}
crate::attribute::message_name(®ion[body..body_end])
.map_err(|_| Error::EditUnsupported("a target object has an unreadable attribute message"))
}
fn encode_attr_message(name: &str, value: &AttrValue) -> Result<Vec<u8>, Error> {
debug_assert!(
value.var_len_strings().is_none(),
"a variable-length attribute must be intercepted by apply_compact_attr_ops before reaching encode_attr_message"
);
let body = build_attr_message(name, value).serialize(LENGTH_SIZE);
if body.len() > OBJECT_HEADER_MESSAGE_MAX {
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
}
pub(crate) fn rewrite_extension_region_bytes(
region: &[u8],
info: &FileSpaceInfo,
) -> Result<Vec<u8>, Error> {
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(region, 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 oh_region_at(prefix: &[u8], addr: u64, file_len: u64) -> Result<(u64, u64), Error> {
if prefix.len() < 6 || &prefix[..4] != b"OHDR" || prefix[4] != 2 {
return Err(Error::EditUnsupported(
"an object does not use a version 2 object header",
));
}
let flags = prefix[5];
if flags & 0x04 != 0 {
return Err(Error::EditUnsupported(
"an object tracks message creation order (not supported in place yet)",
));
}
let mut pos = 6usize;
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 prefix.len() < pos + size_width {
return Err(Error::EditUnsupported("truncated object header"));
}
let chunk0_size = read_le(&prefix[pos..pos + size_width]) as u64;
pos += size_width;
let region_start = addr
.checked_add(pos as u64)
.ok_or(Error::EditUnsupported("truncated object header"))?;
let region_end = region_start
.checked_add(chunk0_size)
.filter(|e| e.checked_add(4).is_some_and(|end| end <= file_len))
.ok_or(Error::EditUnsupported("truncated object header"))?;
Ok((region_start, region_end))
}
pub(crate) struct OhChunk {
pub(crate) span: (u64, u64),
buf: Vec<u8>,
messages_start: usize,
}
impl OhChunk {
pub(crate) fn message_region(&self) -> (&[u8], usize) {
(&self.buf, self.messages_start)
}
}
fn read_oh_chunk0<S: Source + ?Sized>(src: &S, addr: u64) -> Result<OhChunk, Error> {
let file_len = src.len();
let window = file_len
.saturating_sub(addr)
.min(OH_PREFIX_MAX as u64)
.to_usize()?;
let prefix = src.read_metadata_at(addr, window)?;
let (rs, re) = oh_region_at(&prefix, addr, file_len)?;
let len = (re - addr).to_usize()?;
Ok(OhChunk {
span: (addr, len as u64 + 4),
buf: src.read_metadata_at(addr, len)?,
messages_start: (rs - addr).to_usize()?,
})
}
pub(crate) fn read_oh_chunks<S: Source + ?Sized>(
src: &S,
addr: u64,
base: BaseAddress,
) -> Result<Vec<OhChunk>, Error> {
let mut chunks = vec![read_oh_chunk0(src, addr)?];
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 mut found = Vec::new();
let (region, mut p) = chunks[i].message_region();
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
if msg_type == MessageType::ObjectHeaderContinuation {
found.push(read_oh_continuation(src, region, body, body_end, base)?);
}
p = body_end;
}
i += 1;
chunks.extend(found);
}
Ok(chunks)
}
fn read_oh_continuation<S: Source + ?Sized>(
src: &S,
region: &[u8],
body: usize,
body_end: usize,
base: BaseAddress,
) -> Result<OhChunk, Error> {
if body_end - body < (OFFSET_SIZE + LENGTH_SIZE) as usize {
return Err(Error::EditUnsupported("malformed continuation message"));
}
let off = u64::from_le_bytes(region[body..body + 8].try_into().unwrap());
let len = u64::from_le_bytes(region[body + 8..body + 16].try_into().unwrap());
let off = base
.absolute(off)
.map_err(|_| Error::EditUnsupported("continuation address overflow"))?;
let end = off
.checked_add(len)
.filter(|&e| e <= src.len() && len >= 8)
.ok_or(Error::EditUnsupported("continuation block out of bounds"))?;
let want = (end - off)
.to_usize()
.map_err(|_| Error::EditUnsupported("continuation length exceeds this platform"))?;
let mut buf = src.read_metadata_at(off, want)?;
if buf[..4] != *b"OCHK" {
return Err(Error::EditUnsupported(
"invalid continuation block signature",
));
}
buf.truncate(want - 4);
Ok(OhChunk {
span: (off, len),
buf,
messages_start: 4,
})
}
pub(crate) 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)))
}
pub(crate) const MSG_FLAG_SHARED: u8 = 0x02;
fn reject_external_storage(region: &[u8]) -> Result<(), Error> {
let mut p = 0;
while let Some((msg_type, _, body_end)) = next_message(region, p)? {
if msg_type == MessageType::ExternalDataFiles {
return Err(Error::EditUnsupported(
"a dataset stores its elements in external files (H5Pset_external), \
which a copy cannot reproduce -- its data lives in files this crate \
does not read",
));
}
p = body_end;
}
Ok(())
}
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_holds_file_address(&dt) {
return Err(Error::EditUnsupported(
"variable-length or reference datasets cannot be copied to another file yet",
));
}
}
MessageType::Attribute => {
if crate::attribute::message_shares_a_field(®ion[body..body_end]) {
return Err(Error::EditUnsupported(
"an attribute with a committed (shared) datatype cannot be copied to another file yet",
));
}
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_holds_file_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_holds_file_address(&attr.datatype) {
return Err(Error::EditUnsupported(
"variable-length or reference dense (fractal-heap) attributes cannot be copied to another file yet",
));
}
}
Ok(())
}
fn screen_resolved_references(
dt: &Datatype,
raw: &[u8],
invalidated: &InvalidatedAddresses,
) -> Result<(), Error> {
if !datatype_holds_object_address(dt) {
return Ok(());
}
let Some(slots) = embedded_reference_slots(dt).filter(|slots| !slots.is_empty()) else {
return Err(Error::EditUnsupported(
"a reference this commit writes sits in a datatype whose addresses this screen \
cannot read — a width other than eight, a dataset-region reference, a \
variable length of them, or a compound holding one beside a reference it can \
read — so it cannot be checked against what this commit vacates; supply an \
8-byte object reference (`with_reference_data`)",
));
};
for (_, stored) in stored_object_references(raw, dt.type_size() as usize, &slots) {
if let Some(refusal) = invalidated.refusal(stored) {
return Err(Error::EditUnsupported(refusal));
}
}
Ok(())
}
fn screen_copied_references(
tree: &CopyTree,
invalidated: &InvalidatedAddresses,
src: &(impl Source + ?Sized),
) -> Result<(), Error> {
if invalidated.is_empty() {
return Ok(());
}
use crate::shared_message::SharedResolver as _;
let resolver = crate::shared_message::SourceResolver::new(src, OFFSET_SIZE, LENGTH_SIZE);
let (region, dense_attrs) = match tree {
CopyTree::DatasetVerbatim {
region,
dense_attrs,
}
| CopyTree::DatasetContiguous {
region,
dense_attrs,
..
}
| CopyTree::DatasetChunked {
region,
dense_attrs,
..
}
| CopyTree::Group {
non_link_region: region,
dense_attrs,
..
} => (region, dense_attrs),
};
for attr in dense_attrs {
screen_resolved_references(&attr.datatype, &attr.raw_data, invalidated)?;
}
let mut element_dt: Option<Datatype> = None;
let mut compact: Option<Vec<u8>> = None;
let mut p = 0;
while let Some((msg_type, body, body_end)) = next_message(region, p)? {
let shared = region[p + 3] & MSG_FLAG_SHARED != 0;
match msg_type {
MessageType::Datatype => {
let committed;
let encoded = if shared {
committed = resolver
.resolve(®ion[body..body_end], MessageType::Datatype)
.map_err(|_| {
Error::EditUnsupported(
"a copy in this commit names a committed (shared) datatype that \
could not be read, so its elements cannot be screened against \
the same commit's deletions; use separate commits",
)
})?;
&committed[..]
} else {
®ion[body..body_end]
};
let (dt, _) = Datatype::parse(encoded).map_err(|_| {
Error::EditUnsupported("a source datatype could not be parsed for copying")
})?;
element_dt = Some(dt);
}
MessageType::DataLayout => {
if let Ok(DataLayout::Compact { data }) =
DataLayout::parse(®ion[body..body_end], OFFSET_SIZE, LENGTH_SIZE)
{
compact = Some(data);
}
}
MessageType::Attribute => {
if shared {
return Err(Error::EditUnsupported(
"a copy in this commit carries a shared (SOHM) attribute message, whose \
elements cannot be screened against the same commit's deletions; use \
separate commits",
));
}
let attr = crate::attribute::AttributeMessage::parse_resolving(
®ion[body..body_end],
LENGTH_SIZE,
&resolver,
)
.map_err(|_| {
Error::EditUnsupported("a source attribute could not be parsed for copying")
})?;
screen_resolved_references(&attr.datatype, &attr.raw_data, invalidated)?;
}
_ => {}
}
p = body_end;
}
match tree {
CopyTree::DatasetVerbatim { .. } => match (&element_dt, &compact) {
(Some(dt), Some(data)) => screen_resolved_references(dt, data, invalidated)?,
(Some(dt), None) if datatype_holds_object_address(dt) => {
return Err(Error::EditUnsupported(
"a compact object-reference dataset's elements could not be read to screen \
them against this commit's deletions; use separate commits",
));
}
_ => {}
},
CopyTree::DatasetContiguous { data, .. } => {
if let (Some(dt), Some(data)) = (&element_dt, data) {
screen_resolved_references(dt, data, invalidated)?;
}
}
CopyTree::DatasetChunked { .. } => {
if element_dt
.as_ref()
.is_some_and(datatype_holds_object_address)
{
return Err(Error::EditUnsupported(
"a chunked object-reference dataset cannot be copied in a commit that also \
deletes objects: its addresses live inside chunks this path does not \
decode; use separate commits",
));
}
}
CopyTree::Group { children, .. } => {
for (_, child) in children {
screen_copied_references(child, invalidated, src)?;
}
}
}
Ok(())
}
pub(crate) fn build_v2_object_header(region: &[u8]) -> Result<Vec<u8>, Error> {
let mut owned = region.to_vec();
ensure_attribute_info(&mut owned)?;
Ok(build_v2_object_header_verbatim(&owned))
}
fn build_v2_object_header_verbatim(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
}
impl crate::reference_patch::PatchTarget for WriteEngine {
fn read(&self, at: u64, len: usize) -> Result<Vec<u8>, Error> {
self.image().read_exact_at(at, len).map_err(Error::Format)
}
fn write(&mut self, at: u64, bytes: &[u8]) -> Result<(), Error> {
self.image.write_at(at, bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_chunk_index_is_placed_by_the_chunk_data_that_abuts_it() {
assert!(index_abuts_chunk_data(&[(100, 60)], &[(160, 40)]));
assert!(index_abuts_chunk_data(&[(200, 60)], &[(160, 40)]));
assert!(index_abuts_chunk_data(
&[(100, 60), (900, 60)],
&[(160, 40)]
));
assert!(index_abuts_chunk_data(
&[(100, 60)],
&[(180, 20), (160, 20)]
));
assert!(!index_abuts_chunk_data(&[(9000, 60)], &[(160, 40)]));
assert!(!index_abuts_chunk_data(&[(100, 59)], &[(160, 40)]));
assert!(!index_abuts_chunk_data(&[], &[(160, 40)]));
assert!(index_abuts_chunk_data(&[], &[]));
assert!(!index_abuts_chunk_data(&[(160, 0)], &[(160, 40)]));
}
#[test]
fn a_chunk_index_in_page_zero_is_never_raw() {
const PAGE: u64 = 512;
let c_written = [(48u64, 80u64), (512, 512)];
assert!(index_abuts_chunk_data(&[(1024, 512)], &c_written));
assert!(index_touches_page_zero(&c_written, PAGE));
assert!(!index_touches_page_zero(&[(1024, 200)], PAGE));
assert!(index_touches_page_zero(&[(511, 200)], PAGE));
assert!(!index_touches_page_zero(&[(512, 200)], PAGE));
assert!(!index_touches_page_zero(&[(0, 0)], PAGE));
assert!(!index_touches_page_zero(&[], PAGE));
assert!(index_touches_page_zero(&[(1024, 200)], 0));
}
#[test]
fn a_page_that_is_wholly_free_or_dead_is_promoted_whole() {
const PAGE: u64 = 4096;
let (mut meta, mut raw, mut dead) = (FreeList::new(), FreeList::new(), FreeList::new());
meta.free(PAGE, 2048);
dead.free(PAGE + 2048, 2048);
dead.free(2 * PAGE, PAGE);
raw.free(3 * PAGE + 1024, 1024);
PagedEdit::promote_whole_free_pages(&mut meta, &mut raw, &mut dead, PAGE);
assert_eq!(
raw.sections(),
[(PAGE, 2 * PAGE), (3 * PAGE + 1024, 1024)],
"the two empty pages join the raw list as one run; the partial page stays"
);
assert!(
meta.sections().is_empty(),
"the promoted page left the metadata list"
);
assert!(
dead.sections().is_empty(),
"every dead byte was inside a promoted page"
);
}
#[test]
fn dead_space_short_of_a_whole_page_is_not_promoted() {
const PAGE: u64 = 4096;
let (mut meta, mut raw, mut dead) = (FreeList::new(), FreeList::new(), FreeList::new());
dead.free(PAGE, 512);
raw.free(PAGE + 512, 1024);
PagedEdit::promote_whole_free_pages(&mut meta, &mut raw, &mut dead, PAGE);
assert_eq!(dead.sections(), [(PAGE, 512)]);
assert_eq!(raw.sections(), [(PAGE + 512, 1024)]);
}
#[test]
fn a_shared_attribute_message_is_told_from_a_private_one_by_its_flags() {
let body = crate::type_builders::build_attr_message("a", &AttrValue::I64(1))
.serialize(LENGTH_SIZE);
let private = region_message(MessageType::Attribute, &body);
assert!(!region_has_shared_attr(&private).unwrap());
let mut shared = private.clone();
shared[3] = 0x02; assert!(region_has_shared_attr(&shared).unwrap());
let mut other = region_message(MessageType::Dataspace, &body);
other[3] = 0x02;
assert!(!region_has_shared_attr(&other).unwrap());
}
fn reference_attr(name: &str, address: u64) -> crate::attribute::AttributeMessage {
let mut attr = crate::type_builders::build_attr_message(name, &AttrValue::U64(address));
attr.datatype = Datatype::Reference {
size: 8,
ref_type: crate::datatype::ReferenceType::Object,
};
assert_eq!(
attr.raw_data,
address.to_le_bytes(),
"the value is the address"
);
attr
}
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 inline_attr_region(attr: &crate::attribute::AttributeMessage) -> Vec<u8> {
message_record(MessageType::Attribute, &attr.serialize_v3(LENGTH_SIZE))
}
fn compact_reference_region(address: u64) -> Vec<u8> {
let mut region = message_record(
MessageType::Datatype,
&crate::type_builders::make_object_reference_type().serialize(),
);
let mut layout = vec![3u8, 0];
layout.extend_from_slice(&8u16.to_le_bytes());
layout.extend_from_slice(&address.to_le_bytes());
region.extend_from_slice(&message_record(MessageType::DataLayout, &layout));
region
}
#[test]
fn a_copied_compact_reference_dataset_is_screened() {
let empty = BytesSource::new(Vec::new());
let invalidated = InvalidatedAddresses {
removed: vec![(248, 71)],
moved: Vec::new(),
base: BaseAddress::ZERO,
};
for (address, refused) in [(248u64, true), (318, true), (319, false)] {
let tree = CopyTree::DatasetVerbatim {
region: compact_reference_region(address),
dense_attrs: Vec::new(),
};
let got = screen_copied_references(&tree, &invalidated, &empty);
assert_eq!(got.is_err(), refused, "compact element {address}: {got:?}");
}
let no_layout = CopyTree::DatasetVerbatim {
region: message_record(
MessageType::Datatype,
&crate::type_builders::make_object_reference_type().serialize(),
),
dense_attrs: Vec::new(),
};
let err = screen_copied_references(&no_layout, &invalidated, &empty).unwrap_err();
assert!(
err.to_string().contains("could not be read to screen"),
"got: {err}"
);
}
#[test]
fn a_datatype_whose_reference_slots_do_not_fit_is_refused() {
use crate::datatype::{CompoundMember, ReferenceType};
let dt = Datatype::Compound {
size: 8,
members: vec![CompoundMember {
name: "r".to_string(),
byte_offset: 4,
datatype: Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
},
}],
};
assert!(
embedded_reference_slots(&dt).is_none(),
"the fixture must be one the walker cannot map"
);
let raw = [0u8; 8];
let invalidated = InvalidatedAddresses {
removed: vec![(248, 71)],
moved: Vec::new(),
base: BaseAddress::ZERO,
};
let err = screen_resolved_references(&dt, &raw, &invalidated).unwrap_err();
assert!(
err.to_string().contains("this screen cannot read"),
"got: {err}"
);
let nothing = InvalidatedAddresses {
removed: Vec::new(),
moved: Vec::new(),
base: BaseAddress::ZERO,
};
assert!(screen_resolved_references(&dt, &raw, ¬hing).is_err());
}
#[test]
fn a_variable_length_of_object_references_is_refused() {
use crate::datatype::{CharacterSet, ReferenceType};
let of_references = Datatype::VariableLength {
is_string: false,
padding: None,
charset: None,
base_type: Box::new(Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
}),
};
let of_strings = crate::type_builders::make_vlen_string_type(CharacterSet::Utf8);
let raw = vec![0u8; 32];
let invalidated = InvalidatedAddresses {
removed: vec![(248, 71)],
moved: Vec::new(),
base: BaseAddress::ZERO,
};
let err = screen_resolved_references(&of_references, &raw, &invalidated).unwrap_err();
assert!(
err.to_string().contains("this screen cannot read"),
"got: {err}"
);
assert!(
screen_resolved_references(&of_strings, &raw, &invalidated).is_ok(),
"a variable-length string points at a heap no delete reclaims"
);
}
#[test]
fn an_object_reference_wider_than_eight_bytes_is_refused() {
use crate::datatype::ReferenceType;
let dt = Datatype::Reference {
size: 16,
ref_type: ReferenceType::Object,
};
assert!(
embedded_reference_slots(&dt).is_none(),
"the walker must report a width it cannot map, not an empty slot list"
);
let mut raw = vec![0u8; 16];
raw[..8].copy_from_slice(&300u64.to_le_bytes());
let invalidated = InvalidatedAddresses {
removed: vec![(248, 71)],
moved: Vec::new(),
base: BaseAddress::ZERO,
};
let err = screen_resolved_references(&dt, &raw, &invalidated).unwrap_err();
assert!(
err.to_string().contains("this screen cannot read"),
"got: {err}"
);
let nothing = InvalidatedAddresses {
removed: Vec::new(),
moved: Vec::new(),
base: BaseAddress::ZERO,
};
assert!(screen_resolved_references(&dt, &raw, ¬hing).is_err());
}
#[test]
fn a_file_proved_free_of_references_is_walked_once_and_no_more() {
use crate::reference_patch::{reset_walks, walks};
use tempfile::tempdir;
let dir = tempdir().unwrap();
let commit_three = |path: &std::path::Path| {
let session = crate::File::open_rw(path).unwrap();
for i in 0..3 {
session
.root()
.create_dataset(&format!("added{i}"), |b| {
b.with_i32_data(&[i]);
})
.unwrap();
session.commit().unwrap();
}
};
let plain = dir.path().join("plain.h5");
let mut b = crate::writer::FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1, 2, 3]);
b.write(&plain).unwrap();
reset_walks();
commit_three(&plain);
assert_eq!(
walks(),
1,
"the first commit's walk proves the file reference-free; the rest \
must take its word for it"
);
let referencing = dir.path().join("referencing.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(&referencing).unwrap();
reset_walks();
commit_three(&referencing);
assert_eq!(
walks(),
3,
"a file that holds a reference is never proved free of one, so every \
commit walks it"
);
}
#[test]
fn a_raw_reference_target_is_screened_like_a_path_one() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("raw_target.h5");
let mut b = crate::writer::FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1, 2, 3]);
b.write(&path).unwrap();
let engine = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let nodes: BTreeMap<PathKey, Node> = BTreeMap::new();
let path_addr: BTreeMap<PathKey, u64> = BTreeMap::new();
let resolve = |address: u64, removed: Vec<(u64, u64)>, moved: Vec<u64>| {
WriteEngine::resolve_reference_target(
&ObjectRefTarget::Raw(address),
&path_addr,
&nodes,
&[],
&[],
&[],
&InvalidatedAddresses {
removed,
moved,
base: BaseAddress::ZERO,
},
&engine.image(),
engine.superblock(),
)
};
assert!(
resolve(300, vec![(248, 71)], Vec::new()).is_err(),
"an address inside a reclaimed span is refused"
);
assert!(
resolve(300, Vec::new(), vec![300]).is_err(),
"an address this commit rewrites elsewhere is refused"
);
assert_eq!(
resolve(300, vec![(400, 71)], vec![299, 301]).unwrap(),
300,
"an address outside every reclaimed span and every moved header is carried through"
);
assert_eq!(
resolve(300, Vec::new(), Vec::new()).unwrap(),
300,
"a commit that reclaims nothing screens nothing"
);
assert_eq!(resolve(0, vec![(0, 4096)], vec![0]).unwrap(), 0);
assert_eq!(
resolve(UNDEF, vec![(0, u64::MAX)], vec![UNDEF]).unwrap(),
UNDEF
);
}
#[test]
fn a_copied_reference_attribute_is_screened_in_both_storages() {
let empty = BytesSource::new(Vec::new());
let invalidated = InvalidatedAddresses {
removed: vec![(248, 71)],
moved: Vec::new(),
base: BaseAddress::ZERO,
};
for (address, refused) in [(248u64, true), (318, true), (319, false), (247, false)] {
let attr = reference_attr("target", address);
let dense = CopyTree::DatasetVerbatim {
region: Vec::new(),
dense_attrs: vec![attr.clone()],
};
let inline = CopyTree::DatasetVerbatim {
region: inline_attr_region(&attr),
dense_attrs: Vec::new(),
};
for (storage, tree) in [("dense", &dense), ("inline", &inline)] {
let got = screen_copied_references(tree, &invalidated, &empty);
assert_eq!(
got.is_err(),
refused,
"{storage} attribute at {address}: {got:?}"
);
}
}
}
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 append_inplace_crash_consistency_partial_tail_prefix() {
use crate::reader::File as PureFile;
use crate::writer::FileBuilder;
use tempfile::tempdir;
let build = |path: &std::path::Path, n: i32, chunk: u64, deflate: bool| {
let data: Vec<i32> = (0..n).collect();
let mut b = FileBuilder::new();
let d = b
.create_dataset("d")
.with_i32_data(&data)
.with_shape(&[n as u64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[chunk]);
if deflate {
d.with_deflate(6);
}
b.write(path).unwrap();
};
for (n, chunk, add, deflate) in [
(6i32, 4u64, 5i32, false),
(9, 2, 6, false),
(6, 4, 5, true),
(9, 4, 3, true),
] {
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
build(&base, n, chunk, deflate);
for max_phase in 1u8..=4 {
let p = dir
.path()
.join(format!("crash_{n}_{chunk}_{deflate}_{max_phase}.h5"));
std::fs::copy(&base, &p).unwrap();
{
let mut s = WriteEngine::open_with_locking(&p, FileLocking::Enabled).unwrap();
s.append_inplace_i32_phased("d", &(n..n + add).collect::<Vec<_>>(), max_phase)
.unwrap();
}
let expected_len = if max_phase == 4 { n + add } else { n };
let f = PureFile::from_bytes(std::fs::read(&p).unwrap()).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_i32().unwrap(),
(0..expected_len).collect::<Vec<_>>(),
"inconsistent view after crash at phase {max_phase} (n={n}, chunk={chunk}, \
deflate={deflate})"
);
}
}
}
#[cfg(feature = "zfp")]
#[test]
fn append_onto_a_lossy_partial_tail_is_refused() {
use crate::reader::File as PureFile;
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let committed: Vec<f64> = vec![0.5, 1.5, 2.5, 3.5, 4.5, 5.5];
let build = |path: &std::path::Path| {
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_f64_data(&committed)
.with_shape(&[6])
.with_maxshape(&[u64::MAX])
.with_chunks(&[4])
.with_zfp(8.0);
b.write(path).unwrap();
};
let p = dir.path().join("inplace.h5");
build(&p);
let before = std::fs::read(&p).unwrap();
{
let f = crate::reader::File::open_rw(&p).unwrap();
let err = f
.dataset("d")
.unwrap()
.append(&[99.0f64, 98.0, 97.0])
.expect_err("a lossy pipeline must not have its trailing chunk re-encoded");
assert!(
matches!(&err, Error::AppendInPlaceUnsupported(m) if m.contains("lossy")),
"got: {err:?}"
);
}
assert_eq!(
std::fs::read(&p).unwrap(),
before,
"the refusal wrote bytes"
);
assert_eq!(
PureFile::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_f64()
.unwrap(),
committed
);
let p = dir.path().join("aligned.h5");
{
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_f64_data(&committed[..4])
.with_shape(&[4])
.with_maxshape(&[u64::MAX])
.with_chunks(&[4])
.with_zfp(8.0);
b.write(&p).unwrap();
}
{
let f = crate::reader::File::open_rw(&p).unwrap();
f.dataset("d").unwrap().append(&[9.5f64, 8.5, 7.5]).unwrap();
}
let back = PureFile::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_f64()
.unwrap();
assert_eq!(back[..4], committed[..4], "an untouched chunk changed");
let p = dir.path().join("staged.h5");
build(&p);
let before = std::fs::read(&p).unwrap();
{
let f = crate::reader::File::open_rw(&p).unwrap();
let err = f
.dataset("d")
.unwrap()
.append_staged(|b| {
b.append_f64(&[99.0, 98.0, 97.0]);
})
.expect_err("a staged append must not re-encode the committed tail either");
assert!(
matches!(&err, Error::AppendUnsupported(m) if m.contains("lossy")),
"got: {err:?}"
);
f.close().unwrap();
}
assert_eq!(
std::fs::read(&p).unwrap(),
before,
"the refusal wrote bytes"
);
let p = dir.path().join("buffered.h5");
build(&p);
{
let f = crate::reader::File::open_rw(&p).unwrap();
let mut ds = f.dataset("d").unwrap();
let err = ds
.buffered_appender()
.expect_err("its first write would re-encode the committed tail");
assert!(
matches!(&err, Error::AppendInPlaceUnsupported(m) if m.contains("lossy")),
"got: {err:?}"
);
}
}
#[test]
fn a_lossy_scale_offset_mode_is_refused_and_a_lossless_one_is_not() {
use crate::reader::File as PureFile;
use crate::scaleoffset::ScaleOffset;
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let p = dir.path().join("dscale.h5");
{
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_f64_data(&[0.5, 1.5, 2.5, 3.5, 4.5, 5.5])
.with_shape(&[6])
.with_maxshape(&[u64::MAX])
.with_chunks(&[4])
.with_scale_offset(ScaleOffset::FloatDScale(1));
b.write(&p).unwrap();
}
{
let f = crate::reader::File::open_rw(&p).unwrap();
let err = f
.dataset("d")
.unwrap()
.append(&[99.0f64, 98.0, 97.0])
.expect_err("float D-scale is lossy");
assert!(
matches!(&err, Error::AppendInPlaceUnsupported(m) if m.contains("lossy")),
"got: {err:?}"
);
}
let p = dir.path().join("int.h5");
{
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..6).collect::<Vec<_>>())
.with_shape(&[6])
.with_maxshape(&[u64::MAX])
.with_chunks(&[4])
.with_scale_offset(ScaleOffset::Integer(0));
b.write(&p).unwrap();
}
{
let f = crate::reader::File::open_rw(&p).unwrap();
let mut ds = f.dataset("d").unwrap();
ds.append(&[6i32, 7, 8]).unwrap();
ds.append(&[9i32]).unwrap();
}
assert_eq!(
PureFile::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap(),
(0..10).collect::<Vec<_>>()
);
}
#[test]
fn append_inplace_crash_consistency_filtered_partial_last_chunk() {
use crate::reader::File as PureFile;
use crate::writer::FileBuilder;
use tempfile::tempdir;
let build = |path: &std::path::Path, n: i32, chunk: u64| {
let data: Vec<i32> = (0..n).collect();
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&data)
.with_shape(&[n as u64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[chunk])
.with_shuffle()
.with_deflate(4);
b.write(path).unwrap();
};
for (n, chunk, add) in [(8i32, 4u64, 2i32), (8, 4, 9), (0, 4, 3)] {
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
build(&base, n, chunk);
for max_phase in 1u8..=4 {
let p = dir
.path()
.join(format!("crash_f_{n}_{chunk}_{max_phase}.h5"));
std::fs::copy(&base, &p).unwrap();
{
let mut s = WriteEngine::open_with_locking(&p, FileLocking::Enabled).unwrap();
s.append_inplace_i32_phased("d", &(n..n + add).collect::<Vec<_>>(), max_phase)
.unwrap();
}
let expected_len = if max_phase == 4 { n + add } else { n };
let f = PureFile::from_bytes(std::fs::read(&p).unwrap()).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_i32().unwrap(),
(0..expected_len).collect::<Vec<_>>(),
"inconsistent view after crash at phase {max_phase} (n={n}, chunk={chunk}, \
add={add})"
);
}
}
}
fn build_unit_chunked(path: &std::path::Path, n: i32) {
use crate::writer::FileBuilder;
let data: Vec<i32> = (0..n).collect();
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&data)
.with_shape(&[n as u64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[1]);
b.write(path).unwrap();
}
fn append_stopped_at(
base: &std::path::Path,
out: &std::path::Path,
values: std::ops::Range<i32>,
max_phase: u8,
) {
std::fs::copy(base, out).unwrap();
let mut s = WriteEngine::open_with_locking(out, FileLocking::Enabled).unwrap();
s.append_inplace_i32_phased("d", &values.collect::<Vec<_>>(), max_phase)
.unwrap();
}
#[test]
fn append_inplace_crash_consistency_across_ea_boundaries() {
use crate::reader::File as PureFile;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
let (n, target) = (50i32, 250i32);
build_unit_chunked(&base, n);
for max_phase in 1u8..=4 {
let p = dir.path().join(format!("crash_ea_{max_phase}.h5"));
append_stopped_at(&base, &p, n..target, max_phase);
let expected_len = if max_phase == 4 { target } else { n };
let f = PureFile::from_bytes(std::fs::read(&p).unwrap()).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_i32().unwrap(),
(0..expected_len).collect::<Vec<_>>(),
"inconsistent view after crash at phase {max_phase}"
);
}
}
#[test]
fn paged_open_seeds_each_manager_by_slot() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("paged_seed.h5");
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..1000).collect::<Vec<i32>>())
.with_shape(&[1000]);
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(4096);
b.write(&path).unwrap();
let on_disk: u64 = crate::reader::File::open(&path)
.unwrap()
.persisted_free_space()
.iter()
.map(|&(_, l)| l)
.sum();
let s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let pg = s.paged.as_ref().expect("a paged file installs paged state");
assert_eq!(pg.page_size, 4096);
assert!(
!pg.meta.sections().is_empty(),
"SUPER (slot 0) sections seed the metadata list"
);
assert!(
!pg.raw.sections().is_empty(),
"DRAW (slot 2) sections seed the raw list, not the metadata list"
);
let mut all = pg.reusable_sections();
all.extend(pg.unclassified.sections());
assert!(
pg.unclassified.sections().is_empty(),
"our own paged writer files nothing whose page type is unknown"
);
let flat: u64 = all.iter().map(|&(_, l)| l).sum();
assert_eq!(
flat, on_disk,
"the split lists hold exactly the file's free space"
);
all.sort_by_key(|&(a, _)| a);
let mut prev_end = 0u64;
for (addr, len) in all {
assert!(addr >= prev_end, "the per-type lists do not overlap");
prev_end = addr + len;
}
}
#[test]
#[cfg(all(not(target_pointer_width = "32"), target_endian = "little"))]
fn a_c_written_chunk_index_is_not_reclaimed_as_raw() {
use hdf5::plist::file_create::FileSpaceStrategy as CStrategy;
use tempfile::tempdir;
const PAGE: u64 = 4096;
let dir = tempdir().unwrap();
let path = dir.path().join("c_paged_index.h5");
{
let f = hdf5::FileBuilder::new()
.with_fapl(|fapl| fapl.libver_v110())
.with_fcpl(|fcpl| {
fcpl.file_space_strategy(CStrategy::FreeSpaceManager {
paged: true,
persist: true,
threshold: 1,
})
.file_space_page_size(PAGE)
})
.create(&path)
.unwrap();
let ds = f
.new_dataset::<i32>()
.shape(hdf5::SimpleExtents::resizable(vec![8192]))
.chunk((512,))
.create("victim")
.unwrap();
ds.write_raw(&(0..8192i32).collect::<Vec<i32>>()).unwrap();
f.new_dataset::<i32>()
.shape((4,))
.create("keep")
.unwrap()
.write_raw(&[1i32, 2, 3, 4])
.unwrap();
f.close().unwrap();
}
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let page_size = s.paged.as_ref().expect("a paged file").page_size;
assert_eq!(page_size, PAGE);
let victim_addr =
crate::group_v2::resolve_path_any(s.image.as_slice().unwrap(), &s.superblock, "victim")
.unwrap();
let index_spans = s
.chunked_index_spans(usize::try_from(victim_addr).unwrap())
.expect("the C library's extensible-array index is enumerable");
assert!(!index_spans.is_empty());
let mut meta_pages: Vec<u64> = vec![0];
for (addr, len) in &index_spans {
for p in (addr / PAGE)..=((addr + len - 1) / PAGE) {
meta_pages.push(p);
}
}
meta_pages.sort_unstable();
meta_pages.dedup();
s.delete("/victim").unwrap();
s.commit().unwrap();
let mut db = crate::type_builders::DatasetBuilder::new("added");
db.with_f64_data(&[2.5f64; 8]).with_shape(&[8]);
s.stage_created_dataset("/added", db).unwrap();
s.commit().unwrap();
drop(s);
let f = crate::reader::File::open(&path).unwrap();
let added = f.dataset("added").unwrap();
let crate::Layout::Contiguous {
address: Some(addr),
size,
} = added.layout().unwrap()
else {
panic!("a small f64 dataset is stored contiguously");
};
for p in (addr / PAGE)..=((addr + size - 1) / PAGE) {
assert!(
!meta_pages.contains(&p),
"the new dataset's raw data landed at ({addr}, {size}), in page {p}, \
which held live metadata before the edit: {meta_pages:?}"
);
}
assert_eq!(added.read_f64().unwrap(), vec![2.5f64; 8]);
assert_eq!(
f.dataset("keep").unwrap().read_i32().unwrap(),
vec![1, 2, 3, 4]
);
}
#[test]
fn padding_a_tail_page_of_unknown_type_records_nothing() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
const PAGE: u64 = 4096;
let dir = tempdir().unwrap();
let build = |path: &std::path::Path| {
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..1000).collect::<Vec<i32>>())
.with_shape(&[1000]);
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(PAGE);
b.write(path).unwrap();
};
let unknown = dir.path().join("unknown_tail.h5");
build(&unknown);
let mut s = WriteEngine::open_with_locking(&unknown, FileLocking::Enabled).unwrap();
s.append(&[0u8; 100]).unwrap(); assert!(s.paged.as_ref().unwrap().last.is_none());
s.pad_to_page().unwrap();
let pg = s.paged.as_ref().unwrap();
assert_eq!(s.image.len() % PAGE, 0, "the file is padded to a page");
assert!(
pg.meta_pad.is_empty() && pg.raw_pad.is_empty(),
"padding a tail page of unknown type must claim no page type"
);
drop(s);
let known = dir.path().join("known_tail.h5");
build(&known);
let mut s = WriteEngine::open_with_locking(&known, FileLocking::Enabled).unwrap();
s.begin_page(PageType::Meta).unwrap();
s.append(&[0u8; 100]).unwrap();
s.pad_to_page().unwrap();
let pg = s.paged.as_ref().unwrap();
assert_eq!(
pg.meta_pad.len(),
1,
"a known metadata tail records its padding as metadata free space"
);
assert!(pg.raw_pad.is_empty());
}
#[test]
#[cfg(all(not(target_pointer_width = "32"), target_endian = "little"))]
fn a_generic_large_section_is_only_reusable_as_whole_pages() {
use hdf5::plist::file_create::FileSpaceStrategy as CStrategy;
use tempfile::tempdir;
const PAGE: u64 = 512;
let dir = tempdir().unwrap();
let path = dir.path().join("c_generic_large.h5");
{
let f = hdf5::FileBuilder::new()
.with_fapl(|fapl| fapl.libver_v110())
.with_fcpl(|fcpl| {
fcpl.file_space_strategy(CStrategy::FreeSpaceManager {
paged: true,
persist: true,
threshold: 1,
})
.file_space_page_size(PAGE)
})
.create(&path)
.unwrap();
let ds = f.new_dataset::<f64>().shape((64,)).create("d").unwrap();
ds.write_raw(&vec![1.0f64; 64]).unwrap();
let a = ds
.new_attr::<i64>()
.shape((512,))
.create("big_attr")
.unwrap();
a.write_raw(&vec![7i64; 512]).unwrap();
f.close().unwrap();
}
let opened = crate::reader::File::open(&path).unwrap();
let info = opened.file_space_info().expect("a persisting file").clone();
drop(opened);
assert_eq!(info.page_size, PAGE);
let bytes = std::fs::read(&path).unwrap();
let src = crate::source::BytesSource::new(bytes.as_slice());
let slot6 = info.manager_addrs[6];
assert_ne!(slot6, UNDEF, "the C library populated the large manager");
let (sections, _) = free_space_manager::read_persisted_sections_source(
&src,
&[slot6],
BaseAddress::ZERO,
8,
)
.unwrap();
let fragments: Vec<&FreeSection> = sections
.iter()
.filter(|s| s.addr % PAGE != 0 || s.size % PAGE != 0)
.collect();
assert!(
!fragments.is_empty(),
"the premise of this test: the C library files sub-page fragments in \
its generic-large manager, but this file has none ({sections:?})"
);
let s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let pg = s.paged.as_ref().expect("a paged file installs paged state");
let unclassified = pg.unclassified.sections();
let reusable = pg.reusable_sections();
for f in &fragments {
assert!(
unclassified.contains(&(f.addr, f.size)),
"fragment ({}, {}) must be recorded as unclassified, not lost",
f.addr,
f.size
);
assert!(
!reusable
.iter()
.any(|&(a, l)| a < f.addr + f.size && f.addr < a + l),
"fragment ({}, {}) must not be offered to any allocation: {reusable:?}",
f.addr,
f.size
);
}
}
#[test]
fn a_paged_allocation_only_crosses_page_types_over_whole_free_pages() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
const PAGE: u64 = 4096;
fn session(
path: &std::path::Path,
meta: Option<(u64, u64)>,
raw: Option<(u64, u64)>,
) -> WriteEngine {
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..4000).collect::<Vec<i32>>())
.with_shape(&[4000]);
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(PAGE);
b.write(path).unwrap();
let mut s = WriteEngine::open_with_locking(path, FileLocking::Enabled).unwrap();
let pg = s.paged.as_mut().expect("a paged file installs paged state");
pg.meta = FreeList::new();
pg.raw = FreeList::new();
if let Some((addr, len)) = meta {
pg.meta.free(addr, len);
}
if let Some((addr, len)) = raw {
pg.raw.free(addr, len);
}
s
}
let dir = tempdir().unwrap();
let hole_a = (PAGE + 512, 2048);
let hole_b = (2 * PAGE + 512, 2048);
let mut s = session(&dir.path().join("a.h5"), Some(hole_a), None);
assert!(
matches!(
s.reserve(1024, PageType::Raw).unwrap(),
Placement::Appended { .. }
),
"a raw allocation must not be served out of a live metadata page"
);
drop(s);
let mut s = session(&dir.path().join("b.h5"), None, Some(hole_b));
assert!(
matches!(
s.reserve(1024, PageType::Meta).unwrap(),
Placement::Appended { .. }
),
"a metadata allocation must not be served out of a live raw page"
);
drop(s);
let mut s = session(&dir.path().join("c.h5"), Some(hole_a), Some(hole_b));
assert!(
matches!(
s.reserve(1024, PageType::Raw).unwrap(),
Placement::Reused { addr, .. } if addr == hole_b.0
),
"a raw allocation takes the raw hole"
);
assert!(
matches!(
s.reserve(1024, PageType::Meta).unwrap(),
Placement::Reused { addr, .. } if addr == hole_a.0
),
"a metadata allocation takes the metadata hole"
);
drop(s);
let mut s = session(&dir.path().join("d.h5"), Some((PAGE, PAGE)), None);
assert!(
matches!(
s.reserve(1024, PageType::Raw).unwrap(),
Placement::Reused { addr, .. } if addr == PAGE
),
"a raw allocation may open an empty metadata page"
);
let pg = s.paged.as_ref().expect("still paged");
assert_eq!(
pg.raw.sections(),
[(PAGE + 1024, PAGE - 1024)],
"the rest of the claimed page is free space of the claiming type"
);
assert!(
pg.meta.sections().is_empty(),
"the page left the list it was claimed from"
);
drop(s);
let mut s = session(
&dir.path().join("e.h5"),
None,
Some((PAGE + 512, 3 * PAGE - 512)),
);
assert!(
matches!(
s.reserve(1024, PageType::Meta).unwrap(),
Placement::Reused { addr, .. } if addr == 2 * PAGE
),
"the claim must begin at the empty page, not at the run's start"
);
let pg = s.paged.as_ref().expect("still paged");
assert_eq!(
pg.raw.sections(),
[(PAGE + 512, PAGE - 512), (3 * PAGE, PAGE)],
"the fragment below the claimed page and the page above it both stay free"
);
assert_eq!(
pg.meta.sections(),
[(2 * PAGE + 1024, PAGE - 1024)],
"the rest of the claimed page is free space of the claiming type"
);
}
fn free_total(s: &WriteEngine) -> u64 {
let pg = s.paged.as_ref().expect("a paged session");
pg.meta
.sections()
.into_iter()
.chain(pg.raw.sections())
.map(|(_, len)| len)
.sum()
}
#[test]
fn a_paged_tail_takes_exactly_the_space_it_fills() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
const PAGE: u64 = 4096;
const EXT_LEN: u64 = 100;
let dir = tempdir().unwrap();
let path = dir.path().join("tail_exact.h5");
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..4000).collect::<Vec<i32>>())
.with_shape(&[4000]);
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(PAGE);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let os = s.superblock.offset_size;
let mut placed = 0usize;
for hole in 120..420u64 {
{
let pg = s.paged.as_mut().expect("a paged file installs paged state");
pg.meta = FreeList::new();
pg.raw = FreeList::new();
pg.unclassified = FreeList::new();
pg.meta.free(PAGE, hole);
}
let free_before = free_total(&s);
let layout = s.tail_layout(&[], &[], EXT_LEN, PAGE, os);
let free_after = free_total(&s);
match layout {
Some((_, _, at, blocks_len)) => {
placed += 1;
assert_eq!(
free_before - free_after,
blocks_len,
"hole {hole}: the tail took {} bytes at {at} to write {blocks_len}",
free_before - free_after
);
}
None => assert_eq!(
free_after, free_before,
"hole {hole}: a tail that declines to place itself must hand back \
everything it tried"
),
}
}
assert!(
placed > 0,
"the sweep must reach holes the tail can actually use, or it asserts \
nothing about placement"
);
}
#[test]
fn a_flat_tail_takes_exactly_the_extent_it_records() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
const EXT_LEN: u64 = 100;
const HOLE_AT: u64 = 512;
let dir = tempdir().unwrap();
let path = dir.path().join("flat_tail_exact.h5");
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..4000).collect::<Vec<i32>>())
.with_shape(&[4000]);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 0);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let os = s.superblock.offset_size;
let free_total =
|s: &WriteEngine| -> u64 { s.free.sections().into_iter().map(|(_, len)| len).sum() };
let (mut placed, mut declined, mut with_slack) = (0usize, 0usize, 0usize);
for hole in 120..420u64 {
s.free = FreeList::new();
s.free.free(HOLE_AT, hole);
let free_before = free_total(&s);
let (post, at, tail_len) = s.flat_tail_layout(&[], &[], EXT_LEN, os);
let free_after = free_total(&s);
let written = EXT_LEN + file_fsm_blocks_len(&free_sections(&post), os);
match at {
Some(at) => {
placed += 1;
assert_eq!(
free_before - free_after,
tail_len,
"hole {hole}: the tail took {} bytes at {at} for an extent of \
{tail_len}",
free_before - free_after
);
if written < tail_len {
with_slack += 1;
}
}
None => {
declined += 1;
assert_eq!(
free_after, free_before,
"hole {hole}: a tail that declines to place itself must hand \
back everything it tried"
);
assert_eq!(
tail_len, written,
"hole {hole}: a tail with nowhere to go is appended, and the \
length it reports is what it will write there"
);
}
}
}
assert!(
placed > 0,
"the sweep must reach holes the tail can actually use, or it asserts \
nothing about placement"
);
assert!(
declined > 0,
"the sweep must reach holes too small for the tail, or the rule about \
handing back a failed attempt is never exercised"
);
assert!(
with_slack > 0,
"the sweep must reach a hole the tail does not fill exactly, or the \
rule about spare bytes is never exercised"
);
}
#[test]
fn a_paged_tail_with_nowhere_to_go_opens_a_page_and_frees_the_rest() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
const PAGE: u64 = 4096;
let dir = tempdir().unwrap();
let path = dir.path().join("tail_appends.h5");
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..4000).collect::<Vec<i32>>())
.with_shape(&[4000]);
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(PAGE);
b.write(&path).unwrap();
let before = std::fs::metadata(&path).unwrap().len();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
{
let pg = s.paged.as_mut().expect("a paged file installs paged state");
pg.meta = FreeList::new();
pg.raw = FreeList::new();
pg.unclassified = FreeList::new();
}
s.create_group("g").unwrap();
s.commit().unwrap();
let after = std::fs::metadata(&path).unwrap().len();
assert!(
after > before && (after - before) % PAGE == 0,
"the commit opened whole pages ({before} -> {after})"
);
let pg = s.paged.as_ref().expect("still paged");
let (addr, len) = pg
.meta
.sections()
.into_iter()
.find(|&(addr, len)| addr + len == after)
.expect("the page the tail opened leaves a free remainder at end-of-file");
assert!(
len > 0 && len < PAGE && addr >= before,
"the tail's blocks take the front of the page it opened and the rest is \
free ({len} of {PAGE} at {addr}, file {before} -> {after})"
);
}
#[test]
fn deleted_chunk_index_is_freed_into_a_raw_manager() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("paged_chunk_index_free.h5");
let page = 4096u64;
let mut b = FileBuilder::new();
for name in ["drop", "keep"] {
b.create_dataset(name)
.with_i32_data(&(0..200).collect::<Vec<i32>>())
.with_shape(&[200])
.with_chunks(&[50]);
}
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(page);
b.write(&path).unwrap();
{
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.delete("/drop").unwrap();
s.commit().unwrap();
}
let live_raw_pages: Vec<u64> = {
let f = crate::reader::File::open(&path).unwrap();
let ds = f.dataset("keep").unwrap();
let mut pages: Vec<u64> = ds
.chunks()
.unwrap()
.iter()
.filter(|c| c.storage_size > 0)
.flat_map(|c| (c.address / page)..=((c.address + c.storage_size - 1) / page))
.collect();
pages.sort_unstable();
pages.dedup();
pages
};
assert!(!live_raw_pages.is_empty(), "expected live raw pages");
let s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let pg = s.paged.as_ref().expect("a paged file installs paged state");
for (addr, len) in pg.meta.sections() {
for p in (addr / page)..=((addr + len - 1) / page) {
assert!(
!live_raw_pages.contains(&p),
"metadata free section ({addr}, {len}) sits in page {p}, which still \
holds live raw chunk data"
);
}
}
let reclaimed: u64 = pg.reusable_sections().iter().map(|&(_, l)| l).sum();
assert!(reclaimed > 0, "the delete reclaimed nothing");
}
#[test]
fn failed_paged_commit_leaves_the_free_lists_untouched() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("paged_failed_commit.h5");
let mut b = FileBuilder::new();
b.create_dataset("keep")
.with_i32_data(&(0..200).collect::<Vec<i32>>())
.with_shape(&[200]);
b.create_dataset("drop")
.with_i32_data(&(0..200).collect::<Vec<i32>>())
.with_shape(&[200]);
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(4096);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let before = s.space_accounting().reusable_free_space;
let good_ext = s.superblock.superblock_extension_address;
s.superblock.superblock_extension_address = Some(0);
s.delete("/drop").unwrap();
assert!(
s.commit().is_err(),
"a commit with an unreadable extension must fail"
);
assert_eq!(
s.space_accounting().reusable_free_space,
before,
"a failed commit must not record still-live regions as free"
);
s.superblock.superblock_extension_address = good_ext;
s.delete("/drop").unwrap();
s.commit()
.expect("the session is usable after a failed commit");
drop(s);
let f = crate::reader::File::open(&path).unwrap();
let kept = f.dataset("keep").unwrap().read_i32().unwrap();
assert_eq!(kept, (0..200).collect::<Vec<i32>>(), "keep survives intact");
let freed: u64 = f.persisted_free_space().iter().map(|&(_, l)| l).sum();
let live_end = f.file_size();
assert!(
freed < live_end,
"the recorded free space cannot cover the whole file"
);
}
#[test]
fn a_failed_commit_rolls_back_the_heap_collection_provenance() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("failed_commit_vl_provenance.h5");
let mut b = FileBuilder::new();
b.create_dataset("labels")
.with_vlen_strings(&["seed-one", "seed-two"]);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 0);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.stage_dataset_write("/labels", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_vlen_strings(&["round-one-aaaa", "round-one-bbbb"]);
db
})
.unwrap();
s.commit().unwrap();
let recorded = s.vl_overwrite_heaps.clone();
assert!(!recorded.is_empty(), "the good round recorded nothing");
let good_ext = s.superblock.superblock_extension_address;
s.superblock.superblock_extension_address = Some(0);
s.stage_dataset_write("/labels", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_vlen_strings(&["round-two-aaaa", "round-two-bbbb"]);
db
})
.unwrap();
assert!(
s.commit().is_err(),
"a commit with an unreadable extension must fail"
);
assert_eq!(
s.vl_overwrite_heaps, recorded,
"a failed commit must not leave a record naming space it gave back"
);
s.superblock.superblock_extension_address = good_ext;
s.stage_dataset_write("/labels", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_vlen_strings(&["round-three-a", "round-three-b"]);
db
})
.unwrap();
s.commit()
.expect("the session is usable after a failed commit");
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("labels").unwrap().read_string().unwrap(),
vec!["round-three-a".to_string(), "round-three-b".to_string()]
);
}
#[test]
fn a_failed_commit_leaves_no_reusable_span_the_image_names() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("failed_commit_live_span.h5");
let mut b = FileBuilder::new();
b.create_dataset("labels")
.with_vlen_strings(&["seed-one", "seed-two"]);
b.create_dataset("big").with_u8_data(&[0x5A; 40960]);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 0);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.delete("/big").unwrap();
s.commit().unwrap();
let round_one = ["round-one-aaaa", "round-one-bbbb"];
s.stage_dataset_write("/labels", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_vlen_strings(&round_one);
db
})
.unwrap();
s.commit().unwrap();
let good_ext = s.superblock.superblock_extension_address;
s.superblock.superblock_extension_address = Some(0);
s.stage_dataset_write("/labels", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_vlen_strings(&["round-two-aaaa", "round-two-bbbb"]);
db
})
.unwrap();
assert!(
s.commit().is_err(),
"a commit with an unreadable extension must fail"
);
s.superblock.superblock_extension_address = good_ext;
for i in 0..4 {
s.stage_created_dataset(&format!("/filler{i}"), {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_vlen_strings(&["XXXXXXXXXXXXXXXXXXXX", "YYYYYYYYYYYYYYYYYYYY"]);
db
})
.unwrap();
s.commit().unwrap();
}
drop(s);
let f = crate::reader::File::open(&path).unwrap();
let got = f.dataset("labels").unwrap().read_string();
assert!(
got.as_ref()
.is_ok_and(|v| v.iter().map(String::as_str).eq(round_one)),
"/labels reads another dataset's heap objects: {got:?}"
);
}
#[test]
fn a_failed_commit_that_reused_free_space_leaves_the_file_intact() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("reuse_failed_commit.h5");
let victim: Vec<f64> = (0..4096).map(|i| (i % 13) as f64).collect();
let ceiling: Vec<i32> = (0..500).collect();
let mut b = FileBuilder::new();
b.create_dataset("keep").with_i32_data(&[1, 2, 3]);
b.create_dataset("victim")
.with_f64_data(&victim)
.with_shape(&[4096])
.with_chunks(&[512]);
b.create_dataset("ceiling")
.with_i32_data(&ceiling)
.with_shape(&[500]);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 1);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.delete("/victim").unwrap();
s.commit().unwrap();
let free_before = s.space_accounting().reusable_free_space;
let len_before = std::fs::metadata(&path).unwrap().len();
assert!(
free_before.iter().any(|&(_, l)| l > 4096 * 8 / 2),
"the deleted chunked dataset left a hole worth reusing: {free_before:?}"
);
s.superblock.superblock_extension_address = Some(0);
let mut db = crate::type_builders::DatasetBuilder::new("fresh");
db.with_f64_data(&vec![7.5f64; 4096])
.with_shape(&[4096])
.with_chunks(&[512]);
s.stage_created_dataset("/fresh", db).unwrap();
assert!(
s.commit().is_err(),
"a commit with an unreadable extension must fail"
);
assert_eq!(
s.space_accounting().reusable_free_space,
free_before,
"the failed commit gives back the region it drew from"
);
drop(s);
assert_eq!(std::fs::metadata(&path).unwrap().len(), len_before);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(f.file_size(), len_before);
assert_eq!(
f.dataset("keep").unwrap().read_i32().unwrap(),
vec![1, 2, 3]
);
assert_eq!(f.dataset("ceiling").unwrap().read_i32().unwrap(), ceiling);
assert!(f.dataset("victim").is_err());
assert!(f.dataset("fresh").is_err());
}
#[test]
fn a_failed_commit_puts_back_the_value_it_overwrote() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("inplace_partial.h5");
let mut b = FileBuilder::new();
b.create_dataset("nums").with_i32_data(&[1, 2, 3]);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 0);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let good_ext = s.superblock.superblock_extension_address;
s.superblock.superblock_extension_address = Some(0);
s.stage_dataset_write("/nums", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[9, 9, 9]);
db
})
.unwrap();
s.stage_created_dataset("/extra", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[42]);
db
})
.unwrap();
let refused = s.commit();
assert!(
matches!(refused, Err(Error::EditUnsupported(_))),
"the refusal, not a rollback failure: {refused:?}"
);
s.superblock.superblock_extension_address = good_ext;
s.stage_created_dataset("/later", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[7]);
db
})
.unwrap();
s.commit()
.expect("the session is usable after a failed commit");
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("nums").unwrap().read_i32().unwrap(),
vec![1, 2, 3],
"the refused batch's value overwrite is in the file"
);
assert!(
f.dataset("extra").is_err(),
"/extra was correctly discarded"
);
assert_eq!(f.dataset("later").unwrap().read_i32().unwrap(), vec![7]);
}
#[test]
fn a_failed_fast_path_commit_puts_back_the_value_it_overwrote() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("inplace_fast_path_torn.h5");
let mut b = FileBuilder::new();
b.create_dataset("kept").with_i32_data(&[1, 2, 3]);
b.create_dataset("victim").with_i32_data(&[4, 5, 6]);
b.write(&path).unwrap();
let victim_block = {
let f = crate::reader::File::open(&path).unwrap();
match f.dataset("victim").unwrap().layout().unwrap() {
crate::Layout::Contiguous {
address: Some(a),
size,
} => a..a + size,
other => panic!("expected a contiguous victim: {other:?}"),
}
};
let mut s = WriteEngine::open_torn_writes(&path, victim_block).unwrap();
for (name, data) in [("/kept", [9, 9, 9]), ("/victim", [8, 8, 8])] {
s.stage_dataset_write(name, {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&data);
db
})
.unwrap();
}
let refused = s.commit();
assert!(
matches!(refused, Err(Error::CommitPartiallyApplied { .. })),
"a rollback that could not run is not an ordinary refusal: {refused:?}"
);
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("kept").unwrap().read_i32().unwrap(),
vec![1, 2, 3],
"the overwrite the rollback could reach is put back"
);
assert_eq!(
f.dataset("victim").unwrap().read_i32().unwrap(),
vec![8, 8, 8],
"the overwrite it could not reach is the value the error is about"
);
}
#[test]
fn a_commit_that_fails_past_its_repoint_keeps_the_value_it_published() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("inplace_after_repoint.h5");
let mut b = FileBuilder::new();
b.create_dataset("nums").with_i32_data(&[1, 2, 3]);
b.create_dataset("refs").with_path_references(&[""]);
b.write(&path).unwrap();
let refs_block = {
let f = crate::reader::File::open(&path).unwrap();
match f.dataset("refs").unwrap().layout().unwrap() {
crate::Layout::Contiguous {
address: Some(a),
size,
} => a..a + size,
other => panic!("expected a contiguous reference dataset: {other:?}"),
}
};
let mut s = WriteEngine::open_torn_writes(&path, refs_block).unwrap();
s.stage_dataset_write("/nums", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[9, 9, 9]);
db
})
.unwrap();
s.stage_created_dataset("/extra", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[42]);
db
})
.unwrap();
let failed = s.commit();
assert!(
matches!(failed, Err(Error::Io(_))),
"the reference write is what failed, after the commit published: {failed:?}"
);
assert!(
s.publish_attempted,
"the failure has to be past the publish for this test to mean anything"
);
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("nums").unwrap().read_i32().unwrap(),
vec![9, 9, 9],
"the published overwrite must not be rolled back under it"
);
assert_eq!(f.dataset("extra").unwrap().read_i32().unwrap(), vec![42]);
}
#[test]
fn append_inplace_crash_consistency_paged_prefix() {
use crate::reader::File as PureFile;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
let (start, target) = (131_000i32, 132_000i32);
build_unit_chunked(&base, start);
for max_phase in 1u8..=4 {
let p = dir.path().join(format!("crash_paged_{max_phase}.h5"));
append_stopped_at(&base, &p, start..target, max_phase);
let expected_len = if max_phase == 4 { target } else { start };
let f = PureFile::from_bytes(std::fs::read(&p).unwrap()).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_i32().unwrap(),
(0..expected_len).collect::<Vec<_>>(),
"inconsistent paged view after crash at phase {max_phase}"
);
}
}
#[test]
#[cfg(all(not(target_pointer_width = "32"), target_endian = "little"))]
fn append_inplace_crash_consistency_c_library_reads_prefix() {
use tempfile::tempdir;
for (n, chunk, add) in [(6i32, 4u64, 5i32), (8, 2, 6), (50, 1, 200)] {
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
{
use crate::writer::FileBuilder;
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..n).collect::<Vec<i32>>())
.with_shape(&[n as u64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[chunk]);
b.write(&base).unwrap();
}
for max_phase in 1u8..=4 {
let p = dir
.path()
.join(format!("crash_c_{n}_{chunk}_{max_phase}.h5"));
append_stopped_at(&base, &p, n..n + add, max_phase);
let expected_len = if max_phase == 4 { n + add } else { n };
let f = hdf5::File::open(&p).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_raw::<i32>().unwrap(),
(0..expected_len).collect::<Vec<_>>(),
"C library saw an inconsistent view after crash at phase {max_phase} \
(n={n}, chunk={chunk})"
);
f.close().unwrap();
}
}
}
#[test]
#[cfg(all(not(target_pointer_width = "32"), target_endian = "little"))]
fn append_inplace_recover_and_reappend_after_phase3_crash() {
use crate::reader::File as PureFile;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("phase3_recover.h5");
let n = 50i32;
build_unit_chunked(&path, n);
{
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.append_inplace_i32_phased("d", &(1000..1200).collect::<Vec<_>>(), 3)
.unwrap();
}
let committed: Vec<i32> = (0..n).collect();
let pf = PureFile::from_bytes(std::fs::read(&path).unwrap()).unwrap();
assert_eq!(
pf.dataset("d").unwrap().read_i32().unwrap(),
committed,
"phase-3 crash exposed uncommitted data to the pure reader"
);
{
let f = hdf5::File::open(&path).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_raw::<i32>().unwrap(),
committed,
"phase-3 crash exposed uncommitted data to the C library"
);
f.close().unwrap();
}
{
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.append_inplace_i32_phased("d", &(n..150).collect::<Vec<_>>(), 4)
.unwrap();
}
let expected: Vec<i32> = (0..150).collect();
let pf = PureFile::from_bytes(std::fs::read(&path).unwrap()).unwrap();
assert_eq!(
pf.dataset("d").unwrap().read_i32().unwrap(),
expected,
"recovery did not roll forward correctly (pure reader)"
);
let f = hdf5::File::open(&path).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_raw::<i32>().unwrap(),
expected,
"recovery did not roll forward correctly (C library)"
);
f.close().unwrap();
}
#[test]
fn raw_appendable_recurses_into_aggregates() {
use crate::datatype::{CompoundMember, DatatypeByteOrder};
let f64_with = |byte_order| Datatype::FloatingPoint {
size: 8,
byte_order,
bit_offset: 0,
bit_precision: 64,
exponent_location: 52,
exponent_size: 11,
mantissa_location: 0,
mantissa_size: 52,
exponent_bias: 1023,
};
let le_f64 = f64_with(DatatypeByteOrder::LittleEndian);
let be_f64 = f64_with(DatatypeByteOrder::BigEndian);
assert!(datatype_is_raw_appendable(&le_f64));
assert!(!datatype_is_raw_appendable(&be_f64));
let be_member = Datatype::Compound {
size: 8,
members: vec![CompoundMember {
name: "x".into(),
byte_offset: 0,
datatype: be_f64.clone(),
}],
};
assert!(!datatype_is_raw_appendable(&be_member));
let le_member = Datatype::Compound {
size: 8,
members: vec![CompoundMember {
name: "x".into(),
byte_offset: 0,
datatype: le_f64.clone(),
}],
};
assert!(datatype_is_raw_appendable(&le_member));
assert!(!datatype_is_raw_appendable(&Datatype::Array {
base_type: Box::new(be_f64.clone()),
dimensions: vec![4],
}));
assert!(!datatype_is_raw_appendable(&Datatype::VariableLength {
is_string: false,
padding: None,
charset: None,
base_type: Box::new(le_f64.clone()),
}));
assert!(!datatype_is_raw_appendable(&Datatype::Reference {
size: 8,
ref_type: crate::datatype::ReferenceType::Object,
}));
}
#[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 a_refused_open_never_builds_the_image() {
use crate::writer::FileBuilder;
let dir = tempfile::tempdir().unwrap();
let flagged = dir.path().join("flagged.h5");
let mut b = FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1, 2, 3]);
b.write(&flagged).unwrap();
let ancient = dir.path().join("ancient.h5");
std::fs::copy(&flagged, &ancient).unwrap();
for (path, version, flags) in [(&flagged, 3, SWMR_WRITE_FLAGS), (&ancient, 9, 0)] {
let mut data = std::fs::read(path).unwrap();
let off = signature::find_signature(&data).unwrap();
let mut sb = Superblock::parse(&data, off).unwrap();
sb.version = version;
sb.consistency_flags = flags;
let bytes = sb.serialize();
data[off..off + bytes.len()].copy_from_slice(&bytes);
std::fs::write(path, &data).unwrap();
}
for path in [&flagged, &ancient] {
let built = std::cell::Cell::new(false);
let err = match WriteEngine::open_imaged(path, Some(FileLocking::Enabled), |h, len| {
built.set(true);
Ok(Box::new(HandleImage::new(
h,
len,
MetadataCacheConfig::disabled(),
)))
}) {
Err(e) => e,
Ok(_) => panic!("{} must be refused", path.display()),
};
assert!(
!built.get(),
"{} was refused with {err:?}, but the image was built first",
path.display()
);
}
}
#[test]
fn a_stale_consistency_flag_is_refused_then_cleared_by_a_commit() {
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
);
}
match WriteEngine::open_with_locking(&path, FileLocking::Enabled) {
Err(Error::FileMarkedInUse(_)) => {}
Err(e) => panic!("expected the flag refusal, got {e:?}"),
Ok(_) => panic!("a flagged file must not be edited in place"),
}
{
let mut data = std::fs::read(&path).unwrap();
let off = signature::find_signature(&data).unwrap();
let mut sb = Superblock::parse(&data, off).unwrap();
sb.version = 2;
sb.consistency_flags = crate::file_lock::WRITE_ACCESS;
let bytes = sb.serialize();
data[off..off + bytes.len()].copy_from_slice(&bytes);
std::fs::write(&path, &data).unwrap();
}
{
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled)
.expect("the gate skips a v2 superblock, so this opens");
let mut b = DatasetBuilder::new("e");
b.with_i32_data(&[4, 5]);
s.stage_created_dataset("e", b).unwrap();
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"
);
}
#[test]
fn add_vlen_string_dataset_with_null_elements_via_edit_session() {
use crate::type_builders::VlStringElement;
use crate::writer::FileBuilder;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("vlen_null.h5");
let mut b = FileBuilder::new();
b.create_dataset("seed").with_i32_data(&[0]);
b.write(&path).unwrap();
let datatype =
crate::type_builders::make_vlen_string_type(crate::datatype::CharacterSet::Utf8);
let elements = vec![
VlStringElement::Bytes(b"alpha".to_vec()),
VlStringElement::Null,
VlStringElement::Bytes(b"gamma".to_vec()),
];
{
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let mut b = DatasetBuilder::new("labels");
b.with_vlen_string_elements(datatype, &elements).unwrap();
s.stage_created_dataset("labels", b).unwrap();
s.commit().unwrap();
}
let file = crate::reader::File::open(&path).unwrap();
let ds = file.dataset("labels").unwrap();
assert_eq!(
ds.read_string().unwrap(),
vec!["alpha".to_string(), String::new(), "gamma".to_string()]
);
}
#[test]
fn edit_session_root_group_base_address_overflow_is_rejected() {
use crate::writer::FileBuilder;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("edit_root_overflow.h5");
const UB: u64 = 512;
let mut b = FileBuilder::new();
b.with_userblock(UB);
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_eq!(
sb.base_address,
BaseAddress::new(UB),
"userblock file must have base == UB"
);
sb.root_group_address = u64::MAX;
let bytes = sb.serialize();
data[off..off + bytes.len()].copy_from_slice(&bytes);
std::fs::write(&path, &data).unwrap();
let err = WriteEngine::open_with_locking(&path, FileLocking::Enabled)
.err()
.expect("open must fail");
match err {
Error::Format(FormatError::OffsetOverflow { offset, length }) => {
assert_eq!(offset, u64::MAX);
assert_eq!(length, UB);
}
other => panic!("expected root-group address overflow, got {other:?}"),
}
}
use tempfile::tempdir;
fn build_appendable(path: &Path, n: i32, chunk: u64) {
let data: Vec<i32> = (0..n).collect();
let mut b = crate::writer::FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&data)
.with_shape(&[n as u64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[chunk]);
b.write(path).unwrap();
}
fn open_bounded_session(path: &Path) -> WriteEngine {
WriteEngine::open_rw_with_strategy(
path,
crate::source::MetadataCacheConfig::disabled(),
FileLocking::Enabled,
MemoryStrategy::Bounded,
)
.unwrap()
}
fn dataset_addr(engine: &WriteEngine) -> u64 {
crate::group_v2::resolve_path_any_from_source(&engine.image(), engine.superblock(), "d")
.unwrap()
}
#[test]
fn bounded_append_crash_consistency_partial_tail_prefix() {
let dir = tempdir().unwrap();
for (case, (n, chunk, add)) in [(0usize, (6i32, 4u64, 5i32)), (1, (8, 2, 6))] {
let base = dir.path().join(std::format!("base_{case}.h5"));
build_appendable(&base, n, chunk);
for max_phase in 1u8..=4 {
let p = dir.path().join(std::format!("crash_{case}_{max_phase}.h5"));
std::fs::copy(&base, &p).unwrap();
{
let mut engine = open_bounded_session(&p);
let addr = dataset_addr(&engine);
let mut b = AppendBuilder::new();
b.append_i32(&(n..n + add).collect::<Vec<_>>());
engine
.append_inplace_gathered(AppendTarget::Header(addr), &b, max_phase)
.unwrap();
}
let expected_len = if max_phase == 4 { n + add } else { n };
let got = crate::File::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap();
assert_eq!(
got,
(0..expected_len).collect::<Vec<_>>(),
"case {case} phase {max_phase}"
);
}
}
}
#[test]
fn bounded_multi_batch_append_commits_every_batch() {
let dir = tempdir().unwrap();
let p = dir.path().join("multibatch.h5");
build_appendable(&p, 5, 512);
let total = 700_000i32;
{
let mut engine = open_bounded_session(&p);
let addr = dataset_addr(&engine);
let mut b = AppendBuilder::new();
b.append_i32(&(5..total).collect::<Vec<_>>());
engine
.append_inplace_gathered(AppendTarget::Header(addr), &b, 4)
.unwrap();
}
let got = crate::File::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap();
assert_eq!(got.len(), total as usize);
assert!(got.iter().enumerate().all(|(i, &v)| v == i as i32));
}
#[test]
fn only_a_bounded_session_batches_a_large_append() {
let dir = tempdir().unwrap();
let p = dir.path().join("batching.h5");
build_appendable(&p, 8, 4);
let bounded_batch = {
let mut engine = open_bounded_session(&p);
engine
.append_geometry(AppendTarget::Path("d"))
.unwrap()
.full_batch_elems
};
let mirror_batch = {
let mut engine = WriteEngine::open_with_locking(&p, FileLocking::Enabled).unwrap();
engine
.append_geometry(AppendTarget::Path("d"))
.unwrap()
.full_batch_elems
};
assert_eq!(
mirror_batch,
u64::MAX,
"a mirror session must take the whole append as one crash-atomic batch"
);
assert!(
bounded_batch < u64::MAX,
"a bounded session must cap a batch, got {bounded_batch}"
);
assert_eq!(
bounded_batch % 4,
0,
"a batch must be a whole number of chunks"
);
}
#[test]
fn bounded_persist_append_without_finalize_is_readable() {
let dir = tempdir().unwrap();
let p = dir.path().join("persist_crash.h5");
let mut b = crate::writer::FileBuilder::new();
b.with_file_space_strategy(crate::FileSpaceStrategy::FsmAggr, true, 1);
b.create_dataset("d")
.with_i32_data(&(0..6).collect::<Vec<i32>>())
.with_shape(&[6])
.with_maxshape(&[u64::MAX])
.with_chunks(&[4]);
b.write(&p).unwrap();
{
let mut engine = open_bounded_session(&p);
assert!(engine.persist.is_some(), "persist state is armed at open");
let addr = dataset_addr(&engine);
let mut ab = AppendBuilder::new();
ab.append_i32(&(6..20).collect::<Vec<_>>());
engine
.append_inplace_gathered(AppendTarget::Header(addr), &ab, 4)
.unwrap();
}
let got = crate::File::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap();
assert_eq!(got, (0..20).collect::<Vec<_>>());
}
#[test]
fn bounded_paged_reopen_after_crash_realigns_and_stays_readable() {
let dir = tempdir().unwrap();
let p = dir.path().join("paged_crash.h5");
let mut b = crate::writer::FileBuilder::new();
b.with_file_space_strategy(crate::FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(4096);
b.create_dataset("d")
.with_i32_data(&(0..64).collect::<Vec<i32>>())
.with_shape(&[64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
b.write(&p).unwrap();
{
let mut engine = open_bounded_session(&p);
let addr = dataset_addr(&engine);
let mut ab = AppendBuilder::new();
ab.append_i32(&(64..2000).collect::<Vec<_>>());
engine
.append_inplace_gathered(AppendTarget::Header(addr), &ab, 4)
.unwrap();
}
assert_ne!(
std::fs::metadata(&p).unwrap().len() % 4096,
0,
"a crashed (un-finalized) paged session leaves the file non-page-aligned"
);
{
let mut engine = open_bounded_session(&p);
let addr = dataset_addr(&engine);
let mut ab = AppendBuilder::new();
ab.append_i32(&(2000..2500).collect::<Vec<_>>());
engine
.append_inplace_gathered(AppendTarget::Header(addr), &ab, 4)
.unwrap();
engine.finalize_persist().unwrap();
engine.barrier().unwrap();
}
assert_eq!(
std::fs::metadata(&p).unwrap().len() % 4096,
0,
"reopen + append + finalize re-aligns the paged file"
);
let got = crate::File::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap();
assert_eq!(got, (0..2500).collect::<Vec<_>>());
}
#[test]
fn a_bounded_commit_reads_far_less_than_the_file() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
let dir = tempdir().unwrap();
let p = dir.path().join("bulk.h5");
let rows = 2_000_000i32;
build_appendable(&p, rows, 8192);
let file_len = std::fs::metadata(&p).unwrap().len();
assert!(file_len > 4 << 20, "file is only {file_len} bytes");
let read_bytes = Arc::new(AtomicU64::new(0));
{
let mut engine =
WriteEngine::open_bounded_counting(&p, Arc::clone(&read_bytes)).unwrap();
engine.create_group("g").unwrap();
engine.commit().unwrap();
}
let read = read_bytes.load(Ordering::Relaxed);
assert!(
read > 0,
"the commit read nothing, so the test proves nothing"
);
assert!(
read < 64 << 10,
"a bounded commit read {read} bytes of a {file_len}-byte file"
);
}
#[test]
fn a_commit_after_an_append_pads_the_raw_page_the_append_left() {
const PAGE: u64 = 4096;
let dir = tempdir().unwrap();
let p = dir.path().join("paged_interleave.h5");
let mut b = crate::writer::FileBuilder::new();
b.with_file_space_strategy(crate::FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(PAGE);
b.create_dataset("d")
.with_i32_data(&(0..64).collect::<Vec<i32>>())
.with_shape(&[64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
b.write(&p).unwrap();
let mut engine = WriteEngine::open_with_locking(&p, FileLocking::Enabled).unwrap();
let mut ab = AppendBuilder::new();
ab.append_i32(&(64..2000).collect::<Vec<_>>());
engine
.append_inplace_gathered(AppendTarget::Path("d"), &ab, 4)
.unwrap();
assert_eq!(
engine.paged.as_ref().unwrap().last,
Some(PageType::Raw),
"the append must record that the tail page now holds raw data"
);
assert_ne!(
engine.image.len() % PAGE,
0,
"the append must leave a partially-filled page for the commit to pad"
);
engine.create_group("g").unwrap();
engine.commit().unwrap();
let pg = engine.paged.as_ref().expect("the file is paged");
let raw_free = pg.raw.sections();
assert!(
!raw_free.is_empty(),
"the commit packed metadata into the raw page the append left open"
);
for (addr, len) in raw_free {
assert_eq!(
(addr + len) % PAGE,
0,
"padding {addr}+{len} does not reach a page boundary"
);
}
drop(engine);
assert_eq!(
crate::File::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap(),
(0..2000).collect::<Vec<_>>()
);
}
#[test]
fn an_inplace_append_to_a_paged_file_allocates_only_raw_pages() {
const PAGE: u64 = 4096;
let dir = tempdir().unwrap();
let p = dir.path().join("paged_raw.h5");
let mut b = crate::writer::FileBuilder::new();
b.with_file_space_strategy(crate::FileSpaceStrategy::Page, true, 0)
.with_file_space_page_size(PAGE);
b.create_dataset("d")
.with_i32_data(&(0..64).collect::<Vec<i32>>())
.with_shape(&[64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
b.write(&p).unwrap();
let mut engine = WriteEngine::open_with_locking(&p, FileLocking::Enabled).unwrap();
let before = engine.image().len();
for range in [64..2000, 2000..4000] {
let mut ab = AppendBuilder::new();
ab.append_i32(&range.collect::<Vec<_>>());
engine
.append_inplace_gathered(AppendTarget::Path("d"), &ab, 4)
.unwrap();
}
let pg = engine.paged.as_ref().expect("the file is paged");
assert_eq!(
pg.last,
Some(PageType::Raw),
"the append left the tail page holding something other than raw data"
);
assert!(
pg.meta_pad.is_empty() && pg.raw_pad.is_empty(),
"an in-place append switched page type: meta_pad={:?} raw_pad={:?}",
pg.meta_pad,
pg.raw_pad
);
let addr = crate::group_v2::resolve_path_any_from_source(
&engine.image(),
engine.superblock(),
"d",
)
.unwrap();
let spans = engine
.chunked_storage_spans(addr.to_usize().unwrap())
.expect("a chunked dataset has reclaimable spans");
let fresh = spans.iter().filter(|&&(a, _, _)| a >= before).count();
assert!(
fresh > 0,
"the append allocated nothing above {before}, so the assertion above proves nothing"
);
assert!(
spans
.iter()
.all(|&(_, _, class)| class == FreeClass::Page(PageType::Raw)),
"the reclaim tags every chunked span raw; a metadata or dead tag here would \
need the placement rule above to change with it"
);
}
fn gather_fixture(path: &std::path::Path, tables: usize, paged: bool) {
use crate::writer::FileBuilder;
let mut b = FileBuilder::new();
if paged {
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 1)
.with_file_space_page_size(16 * 1024);
}
for t in 0..tables {
b.create_dataset(&std::format!("t{t}"))
.with_i32_data(&(0..256).collect::<Vec<_>>())
.with_shape(&[256])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
}
b.write(path).unwrap();
}
fn gather_workload(session: &mut WriteEngine) -> (u64, u64) {
let before = session.image.issued_writes();
for round in 0..4 {
for t in 0..4 {
session
.append_inplace_i32_phased(&std::format!("t{t}"), &[round; 64], 4)
.unwrap();
}
}
for t in 0..4 {
let mut db = crate::type_builders::DatasetBuilder::new(&std::format!("n{t}"));
db.with_f64_data(&[2.5f64; 32]).with_shape(&[32]);
session
.stage_created_dataset(&std::format!("/n{t}"), db)
.unwrap();
}
let after_appends = session.image.issued_writes();
session.commit().unwrap();
(
after_appends - before,
session.image.issued_writes() - after_appends,
)
}
#[test]
fn gathering_writes_costs_fewer_of_them_and_changes_no_byte() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
for paged in [false, true] {
let straight = dir.path().join(std::format!("straight_{paged}.h5"));
let gathered = dir.path().join(std::format!("gathered_{paged}.h5"));
gather_fixture(&straight, 4, paged);
gather_fixture(&gathered, 4, paged);
let mut a = WriteEngine::open_with_locking(&straight, FileLocking::Enabled).unwrap();
a.set_sync_policy(SyncPolicy::OnClose);
a.image
.set_write_buffering(WriteBuffering::Unbuffered)
.unwrap();
let (straight_appends, straight_commit) = gather_workload(&mut a);
a.force_sync().unwrap();
drop(a);
let mut b = WriteEngine::open_with_locking(&gathered, FileLocking::Enabled).unwrap();
b.set_sync_policy(SyncPolicy::OnClose);
let (gathered_appends, gathered_commit) = gather_workload(&mut b);
b.force_sync().unwrap();
drop(b);
assert!(
gathered_commit * 3 < straight_commit,
"paged={paged}: gathering must cost meaningfully fewer writes for a \
commit, but cost {gathered_commit} against {straight_commit}"
);
assert!(
gathered_appends <= straight_appends,
"paged={paged}: gathering must not cost more writes for the appends, \
but cost {gathered_appends} against {straight_appends}"
);
assert_eq!(
std::fs::read(&straight).unwrap(),
std::fs::read(&gathered).unwrap(),
"paged={paged}: gathering changed the file it produced"
);
}
}
#[test]
fn the_merge_page_follows_the_file_rather_than_the_default() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let paged = dir.path().join("paged.h5");
let plain = dir.path().join("plain.h5");
gather_fixture(&paged, 1, true);
gather_fixture(&plain, 1, false);
let s = WriteEngine::open_with_locking(&paged, FileLocking::Enabled).unwrap();
assert_eq!(
s.gather_page_size(),
16 * 1024,
"a paged file merges within its own file-space page"
);
assert_ne!(
16 * 1024,
DEFAULT_GATHER_PAGE,
"the fixture must not be built at the default, or the assertion above \
holds for a session that ignores the file entirely"
);
drop(s);
let s = WriteEngine::open_with_locking(&plain, FileLocking::Enabled).unwrap();
assert_eq!(
s.gather_page_size(),
DEFAULT_GATHER_PAGE,
"an unpaged file has no page size of its own"
);
}
#[test]
fn a_status_flag_write_leaves_a_userblock_files_root_alone() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("userblock.h5");
let mut b = FileBuilder::new();
b.with_userblock(4096);
b.create_dataset("d")
.with_i32_data(&[1, 2, 3, 4])
.with_shape(&[4]);
b.write(&path).unwrap();
let before = std::fs::read(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
assert_eq!(
s.superblock.base_address,
BaseAddress::new(4096),
"the fixture must have a userblock, or this test holds for a file \
whose absolute and relative roots are the same number"
);
s.set_consistency_flags(file_lock::WRITE_ACCESS).unwrap();
s.set_consistency_flags(0).unwrap();
s.force_sync().unwrap();
drop(s);
assert_eq!(
std::fs::read(&path).unwrap(),
before,
"raising and clearing the flags must leave the file as it was"
);
assert_eq!(
crate::reader::File::open(&path)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap(),
vec![1, 2, 3, 4],
"and the file must still read"
);
}
#[test]
fn a_page_buffered_session_flushes_when_it_outruns_its_budget() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("outrun.h5");
gather_fixture(&path, 1, true);
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
s.set_page_buffer_size(WRITE_GATHER_BYTES).unwrap();
let before = s.image.issued_writes();
s.append_inplace_i32_phased("t0", &vec![7i32; 1 << 20], 4)
.unwrap();
let during = s.image.issued_writes() - before;
s.force_sync().unwrap();
s.release_status_flags().unwrap();
drop(s);
assert!(
during > 0,
"a session that wrote four times its budget issued nothing until it \
closed, so the budget it was given is not the one it asked for"
);
assert_eq!(
crate::reader::File::open(&path)
.unwrap()
.dataset("t0")
.unwrap()
.read_i32()
.unwrap()
.len(),
256 + (1 << 20),
"and the flushed append must still read back whole"
);
}
#[test]
fn a_smaller_budget_issues_more_writes() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let fixture = |path: &std::path::Path, tables: usize| {
use crate::writer::FileBuilder;
let mut b = FileBuilder::new();
b.with_file_space_strategy(FileSpaceStrategy::Page, true, 1)
.with_file_space_page_size(4096);
for t in 0..tables {
b.create_dataset(&std::format!("t{t}"))
.with_i32_data(&(0..256).collect::<Vec<_>>())
.with_shape(&[256])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
}
b.write(path).unwrap();
};
let run = |path: &std::path::Path, budget: usize, workload: &dyn Fn(&mut WriteEngine)| {
let mut s = WriteEngine::open_with_locking(path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
s.set_page_buffer_size(budget).unwrap();
let before = s.image.issued_writes();
workload(&mut s);
s.force_sync().unwrap();
s.release_status_flags().unwrap();
let issued = s.image.issued_writes() - before;
drop(s);
issued
};
let workload = |scattered: bool| {
move |s: &mut WriteEngine| {
if scattered {
for round in 0..4 {
for t in 0..8 {
s.append_inplace_i32_phased(&std::format!("t{t}"), &[round; 64], 4)
.unwrap();
}
}
} else {
s.append_inplace_i32_phased("t0", &vec![7i32; 1 << 20], 4)
.unwrap();
}
}
};
for (label, tables, scattered) in [("scattered", 8, true), ("one long run", 1, false)] {
let workload = workload(scattered);
let at = |budget: usize| {
let path = dir
.path()
.join(std::format!("{}_{budget}.h5", label.replace(' ', "_")));
fixture(&path, tables);
run(&path, budget, &workload)
};
let ample = at(WRITE_GATHER_BYTES);
let smallest = at(4096);
for smaller in [4096, 64 * 1024] {
let below = at(smaller);
assert!(
ample <= below,
"{label}: a {smaller}-byte budget issued {below} writes against \
{ample} at {WRITE_GATHER_BYTES}, so a smaller budget is not the \
trade the property documents"
);
}
assert!(
smallest > ample,
"{label}: the budget changed nothing — {smallest} writes at 4 KiB \
against {ample} at 1 MiB — so this workload cannot say which \
budget is better"
);
}
}
#[test]
fn a_page_buffer_marks_a_userblock_file_without_repointing_its_root() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("ub_buffered.h5");
let mut b = FileBuilder::new();
b.with_userblock(4096);
b.create_dataset("t0")
.with_i32_data(&(0..256).collect::<Vec<_>>())
.with_shape(&[256]);
b.write(&path).unwrap();
let userblock_before = std::fs::read(&path).unwrap()[..4096].to_vec();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
assert_eq!(
s.superblock.base_address,
BaseAddress::new(4096),
"the fixture must have a userblock, or this test says nothing about \
the conversion it exists for"
);
s.set_sync_policy(SyncPolicy::OnClose);
s.set_page_buffer_size(1 << 20)
.expect("an unpaged userblock file must accept a page buffer");
assert_eq!(
s.held_status_flags,
file_lock::WRITE_ACCESS,
"a page-buffered session must hold the crash mark"
);
for t in 0..3 {
let mut db = crate::type_builders::DatasetBuilder::new(&std::format!("n{t}"));
db.with_f64_data(&[2.5f64; 32]).with_shape(&[32]);
s.stage_created_dataset(&std::format!("/n{t}"), db).unwrap();
}
s.commit().unwrap();
s.force_sync().unwrap();
s.release_status_flags().unwrap();
drop(s);
assert_eq!(
&std::fs::read(&path).unwrap()[..4096],
&userblock_before[..],
"the page-buffered session rewrote the userblock"
);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("t0").unwrap().read_i32().unwrap(),
(0..256).collect::<Vec<_>>(),
"the commit must have left the original dataset alone"
);
for t in 0..3 {
assert_eq!(
f.dataset(&std::format!("n{t}"))
.unwrap()
.read_f64()
.unwrap(),
vec![2.5f64; 32],
"n{t}: the committed dataset must read back"
);
}
}
#[test]
fn a_page_buffer_holds_dirty_pages_across_operations() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let run = |path: &std::path::Path, page_buffer: usize| {
let mut s = WriteEngine::open_with_locking(path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
if page_buffer != 0 {
s.set_page_buffer_size(page_buffer).unwrap();
}
let (appends, commit) = gather_workload(&mut s);
s.force_sync().unwrap();
s.release_status_flags().unwrap();
drop(s);
appends + commit
};
for paged in [true, false] {
let label = if paged { "paged" } else { "unpaged" };
let per_op = dir.path().join(std::format!("{label}_per_op.h5"));
let buffered = dir.path().join(std::format!("{label}_buffered.h5"));
gather_fixture(&per_op, 4, paged);
gather_fixture(&buffered, 4, paged);
let per_op_writes = run(&per_op, 0);
let buffered_writes = run(&buffered, 1 << 20);
assert!(
buffered_writes * 4 < per_op_writes,
"{label}: a page buffer must cost meaningfully fewer writes than the \
per-operation default, but cost {buffered_writes} against {per_op_writes}"
);
assert_eq!(
std::fs::read(&per_op).unwrap(),
std::fs::read(&buffered).unwrap(),
"{label}: a page buffer changed the file it produced"
);
}
}
#[test]
fn a_page_buffer_below_the_gather_budget_writes_the_same_file() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let run = |path: &std::path::Path, budget: usize| -> Vec<i32> {
let file = crate::reader::File::create_with_options(
path,
crate::FileCreateProperties::new()
.with_file_space_strategy(FileSpaceStrategy::Page, true, 1)
.with_file_space_page_size(16 * 1024),
crate::FileAccessProperties::new()
.with_sync_policy(SyncPolicy::OnClose)
.with_page_buffer_size(budget),
)
.unwrap();
file.root()
.create_dataset("d", |b| {
b.with_i32_data(&(0..64).collect::<Vec<i32>>())
.with_shape(&[64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
})
.unwrap();
file.commit().unwrap();
for round in 0..8i32 {
let mut ds = file.dataset("d").unwrap();
ds.append(&vec![round; 64]).unwrap();
}
file.root()
.create_dataset("added", |b| {
b.with_f64_data(&[2.5f64; 32]).with_shape(&[32]);
})
.unwrap();
file.commit().unwrap();
file.close().unwrap();
let reopened = crate::reader::File::open(path).unwrap();
assert_eq!(
reopened.dataset("added").unwrap().read_f64().unwrap(),
vec![2.5f64; 32],
"the dataset committed through the buffer must read back"
);
reopened.dataset("d").unwrap().read_i32().unwrap()
};
let small = dir.path().join("small_budget.h5");
let ample = dir.path().join("ample_budget.h5");
let from_small = run(&small, 256 * 1024);
let from_ample = run(&le, 1 << 20);
let mut expected: Vec<i32> = (0..64).collect();
for round in 0..8i32 {
expected.extend(std::iter::repeat_n(round, 64));
}
assert_eq!(
from_small, expected,
"a 256 KiB page buffer did not append what it was given"
);
assert_eq!(
from_ample, expected,
"and neither did the 1 MiB one, so the file comparison below would be \
two arms agreeing on the wrong answer"
);
assert_eq!(
std::fs::read(&small).unwrap(),
std::fs::read(&le).unwrap(),
"a 256 KiB page buffer produced a different file from a 1 MiB one"
);
}
#[test]
fn a_barrier_orders_the_publish_point_last_under_every_policy() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
for policy in [SyncPolicy::Always, SyncPolicy::OnClose] {
let path = dir.path().join(std::format!("order_{policy:?}.h5"));
gather_fixture(&path, 1, false);
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.set_sync_policy(policy);
let mut db = crate::type_builders::DatasetBuilder::new("added");
db.with_f64_data(&[2.5f64; 64]).with_shape(&[64]);
s.stage_created_dataset("/added", db).unwrap();
let before = s.image.issued_write_order().len();
s.commit().unwrap();
let order = s.image.issued_write_order()[before..].to_vec();
let superblock = order
.iter()
.position(|&(at, _)| at == s.sb_sig_off as u64)
.unwrap_or_else(|| panic!("{policy:?}: the commit never wrote the superblock"));
let content = order
.iter()
.rposition(|&(at, _)| at != s.sb_sig_off as u64)
.unwrap_or_else(|| panic!("{policy:?}: the commit wrote nothing but a superblock"));
assert!(
superblock > content,
"{policy:?}: the superblock was issued at position {superblock} of \
{order:?}, ahead of content at {content} — a failure in that window \
leaves a root pointing at bytes that are not in the file"
);
let before = s.image.issued_write_order().len();
s.append_inplace_i32_phased("t0", &[7; 64], 4).unwrap();
let order = s.image.issued_write_order()[before..].to_vec();
let highest = order
.iter()
.map(|&(at, _)| at)
.max()
.expect("the append wrote");
assert!(
order.last().expect("the append wrote").0 < highest,
"{policy:?}: the append's last write is its highest-addressed one, so \
the whole append went out in address order and the dimension that \
publishes the new rows preceded the chunk bytes: {order:?}"
);
}
}
#[test]
fn a_page_buffer_changes_no_byte_through_the_bounded_backing() {
use crate::reader::File;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let run = |name: &str, page_buffer: usize| {
let path = dir.path().join(name);
gather_fixture(&path, 4, true);
{
let props = crate::FileAccessProperties::new()
.with_sync_policy(SyncPolicy::OnClose)
.with_memory_strategy(MemoryStrategy::Bounded)
.with_page_buffer_size(page_buffer);
let file = File::open_rw_with_options(&path, props).unwrap();
assert_eq!(
file.edit_backing(),
Some(crate::EditBacking::Bounded),
"{name}: this must exercise the mirrorless backing"
);
let root = file.root();
for round in 0..4u8 {
for t in 0..4 {
let mut ds = file.dataset(&std::format!("t{t}")).unwrap();
ds.append(&[i32::from(round); 64]).unwrap();
}
}
for t in 0..4 {
root.create_dataset(&std::format!("n{t}"), |b| {
b.with_f64_data(&[2.5f64; 32]).with_shape(&[32]);
})
.unwrap();
}
file.commit().unwrap();
root.set_attr("tag", crate::AttrValue::I32(1)).unwrap();
file.commit().unwrap();
file.close().unwrap();
}
std::fs::read(&path).unwrap()
};
assert_eq!(
run("bounded_plain.h5", 0),
run("bounded_buffered.h5", 1 << 20),
"a page buffer changed the file the bounded backing produced"
);
}
#[test]
fn a_finished_operation_has_nothing_left_to_write() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("finished_op.h5");
gather_fixture(&path, 1, false);
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
let before_append = s.image.issued_writes();
s.append_inplace_i32_phased("t0", &[7; 64], 4).unwrap();
let after_append = s.image.issued_writes();
assert!(
after_append > before_append,
"the append issued nothing at all, so the equality below holds for a \
session that did no work"
);
s.force_sync().unwrap();
assert_eq!(
s.image.issued_writes(),
after_append,
"a finished append left writes in this process's memory"
);
let mut db = crate::type_builders::DatasetBuilder::new("added");
db.with_f64_data(&[1.5f64; 8]).with_shape(&[8]);
s.stage_created_dataset("/added", db).unwrap();
s.commit().unwrap();
let after_commit = s.image.issued_writes();
assert!(
after_commit > after_append,
"the commit issued nothing at all"
);
s.force_sync().unwrap();
assert_eq!(
s.image.issued_writes(),
after_commit,
"a finished commit left writes in this process's memory"
);
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(f.dataset("t0").unwrap().read_i32().unwrap().len(), 320);
assert_eq!(
f.dataset("added").unwrap().read_f64().unwrap(),
vec![1.5f64; 8]
);
}
#[test]
fn overwriting_the_root_group_is_refused_at_staging() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("root_overwrite.h5");
let mut b = FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1]);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let mut db = crate::type_builders::DatasetBuilder::new("whatever");
db.with_i32_data(&[1]);
let err = s.stage_dataset_write("/", db).unwrap_err();
assert!(
matches!(&err, Error::EditUnsupported(m) if m.contains("root group")),
"unexpected error: {err:?}"
);
assert!(!s.has_staged_edits());
}
#[test]
fn staging_unallocated_storage_into_an_existing_file_is_refused() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("unallocated.h5");
let mut b = FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1]);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let mut db = crate::type_builders::DatasetBuilder::new("sparse");
db.with_unallocated_storage(make_i32_type(), &[1000]);
db.with_chunks(&[100]);
let err = s.stage_created_dataset("/sparse", db).unwrap_err();
assert!(
matches!(&err, Error::EditUnsupported(m) if m.contains("unallocated storage")),
"unexpected error: {err:?}"
);
assert!(!s.has_staged_edits());
}
#[test]
fn finalize_persist_has_nothing_left_to_write() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("persisting.h5");
let mut b = FileBuilder::new();
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 1);
b.create_dataset("t0")
.with_i32_data(&(0..256).collect::<Vec<_>>())
.with_shape(&[256])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
s.append_inplace_i32_phased("t0", &[7; 64], 4).unwrap();
s.finalize_persist().unwrap();
let after = s.image.issued_writes();
s.force_sync().unwrap();
assert_eq!(
s.image.issued_writes(),
after,
"finalize_persist returned with writes still gathered"
);
}
#[test]
fn the_append_reserve_is_reported_as_one_coalesced_free_list() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
const VICTIM: usize = (APPEND_RESERVE_BYTES as usize * 3) / 4;
const CHUNK: u64 = 16384;
let dir = tempdir().unwrap();
let path = dir.path().join("reserve_coalesced.h5");
let mut b = FileBuilder::new();
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 1);
b.create_dataset("t0")
.with_i32_data(&[0i32])
.with_shape(&[1])
.with_maxshape(&[u64::MAX])
.with_chunks(&[CHUNK]);
b.create_dataset("victim")
.with_i32_data(&vec![7i32; VICTIM]);
b.create_dataset("ceiling").with_i32_data(&[9i32, 9, 9]);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
s.delete("victim").unwrap();
s.commit().unwrap();
let batch: Vec<i32> = (0..CHUNK as i32).collect();
let mut reserved_ever = false;
for round in 0..16 {
s.append_inplace_i32_phased("t0", &batch, 4).unwrap();
reserved_ever |= !s.reserved.is_empty();
let acct = s.space_accounting();
let regions = &acct.reusable_free_space;
for w in regions.windows(2) {
let ((a_addr, a_len), (b_addr, _)) = (w[0], w[1]);
assert!(
a_addr + a_len < b_addr,
"round {round}: [{a_addr}, {}) and [{b_addr}, ..) touch or overlap, \
though reusable_free_space is documented as fully coalesced: {regions:?}",
a_addr + a_len
);
}
assert_eq!(
acct.reusable_free_bytes,
regions.iter().map(|&(_, len)| len).sum::<u64>(),
"round {round}: the total must be the summed length of the regions"
);
}
assert!(
reserved_ever,
"no reserve was ever held, so this measured the plain free list"
);
}
#[test]
fn an_unspent_append_reserve_goes_back_to_the_managers() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
const VICTIM: usize = (APPEND_RESERVE_BYTES as usize * 2) / 4;
let dir = tempdir().unwrap();
let path = dir.path().join("reserve_return.h5");
let mut b = FileBuilder::new();
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 1);
b.create_dataset("t0")
.with_i32_data(&(0..256).collect::<Vec<_>>())
.with_shape(&[256])
.with_maxshape(&[u64::MAX])
.with_chunks(&[64]);
b.create_dataset("victim")
.with_i32_data(&vec![7i32; VICTIM]);
b.create_dataset("ceiling").with_i32_data(&[9i32, 9, 9]);
b.write(&path).unwrap();
let persisted = |p: &std::path::Path| -> u64 {
crate::reader::File::open(p)
.unwrap()
.persisted_free_space()
.iter()
.map(|&(_, len)| len)
.sum()
};
{
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
s.delete("victim").unwrap();
s.commit().unwrap();
}
let before = persisted(&path);
assert!(
before > APPEND_RESERVE_BYTES,
"the fixture must leave more than one reserve batch on disk, not {before} bytes"
);
let appended: Vec<i32> = (0..1024).collect();
{
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.set_sync_policy(SyncPolicy::OnClose);
s.append_inplace_i32_phased("t0", &appended, 4).unwrap();
s.finalize_persist().unwrap();
}
let after = persisted(&path);
let spent = 64 * 1024;
assert!(
after + spent >= before,
"the session reserved {APPEND_RESERVE_BYTES} bytes and spent a few of them, \
so the managers should still describe nearly all of the {before} they did \
before — they describe {after}"
);
assert!(
after < before,
"the append placed bytes inside the hole, so the managers must describe \
less than the {before} they did (they describe {after})"
);
let f = crate::reader::File::open(&path).unwrap();
let mut want = (0..256).collect::<Vec<i32>>();
want.extend_from_slice(&appended);
assert_eq!(f.dataset("t0").unwrap().read_i32().unwrap(), want);
assert_eq!(f.dataset("ceiling").unwrap().read_i32().unwrap(), [9, 9, 9]);
}
#[test]
fn an_unbuffered_append_costs_a_small_constant_number_of_writes() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("swmr_cost.h5");
gather_fixture(&path, 1, false);
let mut s = WriteEngine::open_swmr_writer(&path, SyncPolicy::OnClose).unwrap();
let before = s.image.issued_writes();
s.append_inplace_i32_phased("t0", &[7; 64], 4).unwrap();
let cost = s.image.issued_writes() - before;
assert!(
cost > 0,
"the append issued nothing, so the ceiling below proves nothing"
);
assert!(
cost <= 10,
"an unbuffered append costs {cost} writes; the array header's six \
statistics belong in one write, not six, and each checksum belongs in \
the write that changed what it covers (measured at 8)"
);
}
#[test]
fn the_swmr_writer_holds_no_write_back() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("swmr.h5");
gather_fixture(&path, 1, false);
let mut s = WriteEngine::open_swmr_writer(&path, SyncPolicy::OnClose).unwrap();
let before = std::fs::read(&path).unwrap().len();
s.append_inplace_i32_phased("t0", &[7; 64], 1).unwrap();
assert!(
std::fs::read(&path).unwrap().len() > before,
"a SWMR reader must see the phase-1 chunk bytes as soon as they are written"
);
}
#[test]
fn sync_policy_governs_every_barrier() {
use crate::writer::FileBuilder;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tempfile::tempdir;
let dir = tempdir().unwrap();
let run = |name: &str, policy: SyncPolicy| -> [u64; 5] {
let path = dir.path().join(name);
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..8).collect::<Vec<_>>())
.with_shape(&[8])
.with_maxshape(&[u64::MAX])
.with_chunks(&[4]);
b.write(&path).unwrap();
let syncs = Arc::new(AtomicU64::new(0));
let mut s = WriteEngine::open_sync_counting(&path, policy, Arc::clone(&syncs)).unwrap();
s.append_inplace_i32_phased("d", &[8, 9, 10, 11], 4)
.unwrap();
let after_append = syncs.load(Ordering::Relaxed);
let mut db = crate::type_builders::DatasetBuilder::new("added");
db.with_f64_data(&[2.5f64; 8]).with_shape(&[8]);
s.stage_created_dataset("/added", db).unwrap();
s.commit().unwrap();
let after_commit = syncs.load(Ordering::Relaxed);
let mut ow = crate::type_builders::DatasetBuilder::new("added");
ow.with_f64_data(&[4.5f64; 8]).with_shape(&[8]);
s.stage_dataset_write("/added", ow).unwrap();
s.commit().unwrap();
let after_overwrite = syncs.load(Ordering::Relaxed);
s.delete("/added").unwrap();
s.commit().unwrap();
let after_truncate = syncs.load(Ordering::Relaxed);
s.force_sync().unwrap();
let after_forced = syncs.load(Ordering::Relaxed);
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_i32().unwrap(),
(0..12).collect::<Vec<_>>(),
"the append must land under {policy:?}"
);
assert!(
f.dataset("added").is_err(),
"the deleting commit must land under {policy:?}"
);
[
after_append,
after_commit,
after_overwrite,
after_truncate,
after_forced,
]
};
let always = run("always.h5", SyncPolicy::Always);
assert!(always[0] > 0, "an immediate append syncs under Always");
assert!(always[1] > always[0], "so does a commit");
assert!(
always[2] > always[1],
"so does the same-length-overwrite fast path"
);
assert!(
always[3] > always[2],
"so does a commit that truncates the file"
);
assert_eq!(
always[4],
always[3] + 1,
"and a forced sync is exactly one more"
);
let deferred = run("on_close.h5", SyncPolicy::OnClose);
assert_eq!(
&deferred[..4],
&[0, 0, 0, 0],
"OnClose must leave every one of those in-session barriers unissued"
);
assert_eq!(
deferred[4], 1,
"a forced sync is issued whatever the policy says — it is what the \
teardown path and File::sync both take"
);
}
#[test]
fn sync_policy_governs_the_persisting_and_flag_barriers() {
use crate::writer::FileBuilder;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tempfile::tempdir;
let dir = tempdir().unwrap();
let run = |name: &str, strategy: FileSpaceStrategy, policy: SyncPolicy| -> [u64; 3] {
let path = dir.path().join(name);
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&(0..8).collect::<Vec<_>>())
.with_shape(&[8])
.with_maxshape(&[u64::MAX])
.with_chunks(&[4]);
b.create_dataset("victim")
.with_f64_data(&[1.5f64; 64])
.with_shape(&[64]);
b.with_file_space_strategy(
strategy,
true,
if strategy == FileSpaceStrategy::Page {
0
} else {
1
},
);
b.write(&path).unwrap();
let syncs = Arc::new(AtomicU64::new(0));
let mut s = WriteEngine::open_sync_counting(&path, policy, Arc::clone(&syncs)).unwrap();
assert!(
s.persist.is_some(),
"the fixture must persist its free space, or this tests the wrong tail"
);
s.delete("/victim").unwrap();
s.commit().unwrap();
let after_commit = syncs.load(Ordering::Relaxed);
s.set_consistency_flags(0).unwrap();
let after_flags = syncs.load(Ordering::Relaxed);
let issued = s.image.issued_writes();
s.force_sync().unwrap();
assert_eq!(
s.image.issued_writes(),
issued,
"{policy:?}: the consistency-flag write was left gathered"
);
s.append_inplace_i32_phased("d", &[8, 9, 10, 11], 4)
.unwrap();
let before_close = syncs.load(Ordering::Relaxed);
s.finalize_persist().unwrap();
let after_close = syncs.load(Ordering::Relaxed) - before_close;
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert!(
f.dataset("victim").is_err(),
"the delete must land under {policy:?}"
);
assert_eq!(
f.dataset("d").unwrap().read_i32().unwrap(),
(0..12).collect::<Vec<_>>(),
"and so must the append under {policy:?}"
);
[after_commit, after_flags, after_close]
};
for (label, strategy) in [
("fsmaggr", FileSpaceStrategy::FsmAggr),
("paged", FileSpaceStrategy::Page),
] {
let always = run(&format!("{label}_always.h5"), strategy, SyncPolicy::Always);
assert!(
always[0] > 0,
"the {label} persisting commit tail syncs under Always"
);
assert!(
always[1] > always[0],
"so does the consistency-flag write a SWMR session makes ({label})"
);
assert!(
always[2] > 0,
"so does the manager re-homing close owes ({label})"
);
assert_eq!(
run(
&format!("{label}_on_close.h5"),
strategy,
SyncPolicy::OnClose
),
[0, 0, 0],
"OnClose must leave the {label} persisting tail, the flag write, and the \
close-time manager re-homing with no fsync at all"
);
}
}
#[test]
fn a_commit_whose_publish_write_fails_rolls_nothing_back() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("torn_publish.h5");
let mut b = FileBuilder::new();
b.create_dataset("nums").with_i32_data(&[1, 2, 3]);
b.write(&path).unwrap();
let mut s = WriteEngine::open_torn_writes(&path, 0..48).unwrap();
s.stage_dataset_write("/nums", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[9, 9, 9]);
db
})
.unwrap();
s.stage_created_dataset("/extra", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[42]);
db
})
.unwrap();
let failed = s.commit();
assert!(
matches!(failed, Err(Error::Io(_))),
"the publish write is what failed: {failed:?}"
);
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("extra").unwrap().read_i32().unwrap(),
vec![42],
"the publish landed despite reporting failure, so this commit is live"
);
assert_eq!(
f.dataset("nums").unwrap().read_i32().unwrap(),
vec![9, 9, 9],
"a rollback under a commit that did publish would tear it"
);
}
#[test]
#[cfg(all(not(target_pointer_width = "32"), target_endian = "little"))]
fn an_undo_replays_two_hard_links_to_one_block_newest_first() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("hard_link_undo.h5");
{
use hdf5::plist::file_create::FileSpaceStrategy as CStrategy;
let f = hdf5::FileBuilder::new()
.with_fapl(|fapl| fapl.libver_v110())
.with_fcpl(|fcpl| {
fcpl.file_space_strategy(CStrategy::FreeSpaceManager {
paged: false,
persist: true,
threshold: 1,
})
})
.create(&path)
.unwrap();
f.new_dataset::<i32>()
.shape((3,))
.create("aa")
.unwrap()
.write(&[1i32, 2, 3])
.unwrap();
f.link_hard("aa", "bb").unwrap();
f.close().unwrap();
}
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.superblock.superblock_extension_address = Some(0);
for (name, data) in [("/aa", [9, 9, 9]), ("/bb", [8, 8, 8])] {
s.stage_dataset_write(name, {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&data);
db
})
.unwrap();
}
s.stage_created_dataset("/extra", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[42]);
db
})
.unwrap();
assert!(s.commit().is_err());
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("aa").unwrap().read_i32().unwrap(),
vec![1, 2, 3],
"the block both links share must hold the value it held before"
);
assert_eq!(f.dataset("bb").unwrap().read_i32().unwrap(), vec![1, 2, 3]);
}
#[test]
fn a_failed_commit_puts_back_every_chunk_it_overwrote() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("chunked_undo.h5");
let original: Vec<i32> = (0..4096i32)
.map(|i| i.wrapping_mul(2_654_435_761u32 as i32) ^ i)
.collect();
let mut b = FileBuilder::new();
b.create_dataset("grid")
.with_i32_data(&original)
.with_shape(&[4096])
.with_chunks(&[512])
.with_deflate(6);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 0);
b.write(&path).unwrap();
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
s.superblock.superblock_extension_address = Some(0);
s.stage_dataset_write("/grid", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&vec![0i32; 4096]);
db
})
.unwrap();
s.stage_created_dataset("/extra", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[42]);
db
})
.unwrap();
assert!(s.commit().is_err());
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("grid").unwrap().read_i32().unwrap(),
original,
"every chunk, and the index sizing them, must read as it did before"
);
}
#[test]
fn a_commit_does_not_replay_a_journal_it_did_not_write() {
use crate::writer::FileBuilder;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("stale_journal.h5");
let mut b = FileBuilder::new();
b.create_dataset("nums").with_i32_data(&[1, 2, 3]);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 0);
b.write(&path).unwrap();
let nums_block = {
let f = crate::reader::File::open(&path).unwrap();
match f.dataset("nums").unwrap().layout().unwrap() {
crate::Layout::Contiguous {
address: Some(a), ..
} => a as usize,
other => panic!("expected a contiguous dataset: {other:?}"),
}
};
let mut s = WriteEngine::open_with_locking(&path, FileLocking::Enabled).unwrap();
let stale: Vec<u8> = [7i32, 7, 7].iter().flat_map(|v| v.to_le_bytes()).collect();
s.inplace_undo.push((nums_block, stale));
s.superblock.superblock_extension_address = Some(0);
s.stage_created_dataset("/extra", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[42]);
db
})
.unwrap();
assert!(s.commit().is_err());
drop(s);
let f = crate::reader::File::open(&path).unwrap();
assert_eq!(
f.dataset("nums").unwrap().read_i32().unwrap(),
vec![1, 2, 3],
"the commit replayed a journal entry that was not its own"
);
}
#[test]
fn a_rollback_orders_the_values_it_puts_back() {
use crate::writer::FileBuilder;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tempfile::tempdir;
let mut counted = Vec::new();
for with_overwrite in [false, true] {
let dir = tempdir().unwrap();
let path = dir.path().join("rollback_syncs.h5");
let mut b = FileBuilder::new();
b.create_dataset("nums").with_i32_data(&[1, 2, 3]);
b.with_file_space_strategy(FileSpaceStrategy::FsmAggr, true, 0);
b.write(&path).unwrap();
let syncs = Arc::new(AtomicU64::new(0));
let mut s =
WriteEngine::open_sync_counting(&path, SyncPolicy::Always, Arc::clone(&syncs))
.unwrap();
s.superblock.superblock_extension_address = Some(0);
if with_overwrite {
s.stage_dataset_write("/nums", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[9, 9, 9]);
db
})
.unwrap();
}
s.stage_created_dataset("/extra", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&[42]);
db
})
.unwrap();
let before = syncs.load(Ordering::Relaxed);
assert!(s.commit().is_err());
counted.push(syncs.load(Ordering::Relaxed) - before);
}
assert_eq!(
counted[0], 0,
"a refusal with nothing to put back reaches no barrier"
);
assert_eq!(
counted[1],
counted[0] + 1,
"a refusal that put values back must order them: {counted:?}"
);
}
#[test]
fn a_bounded_commit_reads_the_value_it_is_replacing() {
use crate::writer::FileBuilder;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tempfile::tempdir;
let dir = tempdir().unwrap();
let p = dir.path().join("journal_reads.h5");
let data: Vec<i32> = (0..250_000).collect();
let payload = (data.len() * 4) as u64;
let mut b = FileBuilder::new();
b.create_dataset("nums").with_i32_data(&data);
b.write(&p).unwrap();
let read_bytes = Arc::new(AtomicU64::new(0));
let mut engine = WriteEngine::open_bounded_counting(&p, Arc::clone(&read_bytes)).unwrap();
engine
.stage_dataset_write("/nums", {
let mut db = crate::type_builders::DatasetBuilder::new("");
db.with_i32_data(&data);
db
})
.unwrap();
let before = read_bytes.load(Ordering::Relaxed);
engine.commit().unwrap();
let read = read_bytes.load(Ordering::Relaxed) - before;
assert!(
read >= payload,
"the journal must read the whole value it replaces: {read} of {payload}"
);
assert!(
read < payload + (64 << 10),
"and not much more than it: {read} against {payload}"
);
}
}
#[cfg(test)]
mod staged_query_tests {
use super::*;
use crate::type_builders::DatasetBuilder;
use tempfile::tempdir;
fn open_session(path: &Path) -> WriteEngine {
let mut b = crate::writer::FileBuilder::new();
b.create_dataset("existing").with_i32_data(&[1, 2, 3]);
b.write(path).unwrap();
WriteEngine::open_rw_with_strategy(
path,
crate::source::MetadataCacheConfig::disabled(),
FileLocking::Enabled,
MemoryStrategy::Mirrored,
)
.unwrap()
}
fn i32_dataset(data: &[i32]) -> DatasetBuilder {
let mut b = DatasetBuilder::new("");
b.with_i32_data(data);
b
}
fn kind(e: &WriteEngine, path: &str) -> Option<StagedKind> {
e.staged_object(path).map(|o| o.kind)
}
fn child_names(e: &WriteEngine, parent: &str) -> Vec<(String, StagedKind)> {
e.staged_children(parent)
.into_iter()
.map(|c| (c.name, c.kind))
.collect()
}
#[test]
fn a_staged_creation_is_named_at_every_level_of_its_path() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.create_group("a").unwrap();
let mut col = DatasetBuilder::new("");
col.with_i32_data(&[1, 2])
.with_maxshape(&[u64::MAX])
.with_chunks(&[2]);
e.stage_created_dataset("a/b/col", col).unwrap();
assert_eq!(kind(&e, "a"), Some(StagedKind::Group));
assert_eq!(kind(&e, "a/b/col"), Some(StagedKind::Dataset));
assert_eq!(kind(&e, "a/b"), None);
assert_eq!(kind(&e, ""), None);
assert_eq!(kind(&e, "existing"), None);
assert_eq!(kind(&e, "a/missing"), None);
assert_eq!(
child_names(&e, ""),
vec![("a".to_string(), StagedKind::Group)]
);
assert_eq!(
child_names(&e, "a/b"),
vec![("col".to_string(), StagedKind::Dataset)]
);
assert!(child_names(&e, "a").is_empty());
assert!(child_names(&e, "existing").is_empty());
e.create_group("a/b").unwrap();
assert_eq!(kind(&e, "a/b"), Some(StagedKind::Group));
assert_eq!(
child_names(&e, "a"),
vec![("b".to_string(), StagedKind::Group)]
);
}
#[test]
fn a_staged_dataset_reports_what_its_builder_settled() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
let mut col = DatasetBuilder::new("");
col.with_i32_data(&[1, 2, 3, 4])
.with_maxshape(&[u64::MAX])
.with_chunks(&[2])
.with_deflate(4);
e.stage_created_dataset("col", col).unwrap();
e.stage_created_dataset("plain", i32_dataset(&[5])).unwrap();
let meta = e.staged_dataset_meta("col").unwrap();
assert_eq!(meta.dimensions, vec![4]);
assert_eq!(meta.maxshape, Some(vec![u64::MAX]));
assert_eq!(meta.datatype.type_size(), 4);
assert!(meta.chunked);
assert_eq!(meta.filters, vec![(1u16, false)]);
let plain = e.staged_dataset_meta("plain").unwrap();
assert_eq!(plain.dimensions, vec![1]);
assert_eq!(plain.maxshape, None);
assert!(!plain.chunked);
assert!(plain.filters.is_empty());
e.create_group("g").unwrap();
assert!(e.staged_dataset_meta("g").is_none());
assert!(e.staged_dataset_meta("existing").is_none());
}
#[test]
fn a_deletion_hides_nothing_until_a_creation_replaces_it() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.delete("existing").unwrap();
assert_eq!(kind(&e, "existing"), None);
assert!(e.staged_children("").is_empty());
e.stage_created_dataset("existing", i32_dataset(&[9]))
.unwrap();
let staged = e.staged_object("existing").unwrap();
assert_eq!(staged.kind, StagedKind::Dataset);
assert!(staged.replaces_link, "the same commit removes the link");
assert_eq!(
e.staged_dataset_meta("existing").unwrap().dimensions,
vec![1]
);
assert!(e.staged_children("")[0].replaces_link);
}
#[test]
fn a_creation_colliding_with_a_surviving_link_is_refused_where_it_is_staged() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
let err = e
.stage_created_dataset("existing", i32_dataset(&[9]))
.unwrap_err();
assert!(
matches!(&err, Error::EditUnsupported(m) if m.contains("already exists")),
"got: {err}"
);
assert!(e.create_group("existing").is_err());
assert_eq!(kind(&e, "existing"), None);
assert!(e.staged_dataset_meta("existing").is_none());
assert!(e.staged_children("").is_empty());
e.commit().unwrap();
}
#[test]
fn a_second_creation_at_a_staged_path_is_refused_where_it_is_staged() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.stage_created_dataset("fresh", i32_dataset(&[1, 2, 3]))
.unwrap();
let err = e
.stage_created_dataset("fresh", i32_dataset(&[9]))
.unwrap_err();
assert!(
matches!(&err, Error::EditUnsupported(m) if m.contains("already stages")),
"got: {err}"
);
assert_eq!(e.staged_dataset_meta("fresh").unwrap().dimensions, vec![3]);
assert_eq!(
child_names(&e, ""),
vec![("fresh".to_string(), StagedKind::Dataset)]
);
assert!(e.create_group("fresh").is_err());
e.create_group("g").unwrap();
assert!(e.stage_created_dataset("g", i32_dataset(&[9])).is_err());
assert_eq!(kind(&e, "g"), Some(StagedKind::Group));
e.commit().unwrap();
}
#[test]
fn a_replacement_is_staged_once_and_a_withdrawal_frees_the_name_again() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.delete("existing").unwrap();
e.stage_created_dataset("existing", i32_dataset(&[9]))
.unwrap();
assert!(
e.stage_created_dataset("existing", i32_dataset(&[8, 8]))
.is_err()
);
assert_eq!(
e.staged_dataset_meta("existing").unwrap().dimensions,
vec![1]
);
e.delete("existing").unwrap();
e.stage_created_dataset("existing", i32_dataset(&[8, 8]))
.unwrap();
assert_eq!(
e.staged_dataset_meta("existing").unwrap().dimensions,
vec![2]
);
e.commit().unwrap();
}
#[test]
fn a_group_staged_twice_is_one_group_and_stays_allowed() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.create_group("g").unwrap();
e.create_group("g").unwrap();
assert_eq!(kind(&e, "g"), Some(StagedKind::Group));
assert_eq!(
child_names(&e, ""),
vec![("g".to_string(), StagedKind::Group)]
);
e.stage_created_dataset("g/inner", i32_dataset(&[7]))
.unwrap();
e.commit().unwrap();
}
#[test]
fn a_creation_under_a_deleted_group_the_session_does_not_rebuild_shadows_nothing() {
let dir = tempdir().unwrap();
let path = dir.path().join("prefix.h5");
let mut b = crate::writer::FileBuilder::new();
let mut g = b.create_group("g");
g.create_dataset("inner").with_i32_data(&[1]);
b.add_group(g.finish());
b.write(&path).unwrap();
let mut e = WriteEngine::open_rw_with_strategy(
&path,
crate::source::MetadataCacheConfig::disabled(),
FileLocking::Enabled,
MemoryStrategy::Mirrored,
)
.unwrap();
e.delete("g").unwrap();
let err = e
.stage_created_dataset("g/inner", i32_dataset(&[9]))
.unwrap_err();
assert!(
matches!(&err, Error::EditUnsupported(m) if m.contains("already exists")),
"got: {err}"
);
assert_eq!(kind(&e, "g/inner"), None, "the file's own dataset");
e.stage_created_dataset("g/other", i32_dataset(&[9]))
.unwrap();
assert!(
!e.staged_children("g")[0].replaces_link,
"nothing rebuilds `g`, so its names are not handed over"
);
assert!(e.commit().is_err(), "a deletion overlapping an addition");
}
#[test]
fn a_deleted_group_rebuilt_in_the_same_commit_hands_its_names_over() {
let dir = tempdir().unwrap();
let path = dir.path().join("replaced.h5");
let mut b = crate::writer::FileBuilder::new();
let mut g = b.create_group("g");
g.create_dataset("inner").with_i32_data(&[1]);
b.add_group(g.finish());
b.write(&path).unwrap();
let mut e = WriteEngine::open_rw_with_strategy(
&path,
crate::source::MetadataCacheConfig::disabled(),
FileLocking::Enabled,
MemoryStrategy::Mirrored,
)
.unwrap();
e.delete("g").unwrap();
e.create_group("g").unwrap();
e.stage_created_dataset("g/inner", i32_dataset(&[9]))
.unwrap();
assert_eq!(kind(&e, "g/inner"), Some(StagedKind::Dataset));
assert!(e.staged_object("g/inner").unwrap().replaces_link);
assert!(e.staged_children("g")[0].replaces_link);
e.commit().unwrap();
}
#[test]
fn the_root_cannot_be_deleted() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
for path in ["", "/"] {
let err = e.delete(path).unwrap_err();
assert!(
matches!(&err, Error::EditUnsupported(m) if m.contains("root group")),
"got: {err}"
);
}
e.stage_created_dataset("fresh", i32_dataset(&[1])).unwrap();
assert!(
!e.staged_object("fresh").unwrap().replaces_link,
"no deletion was staged, so this replaces nothing"
);
}
#[test]
fn an_append_onto_a_staged_dataset_grows_the_pending_creation() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.stage_created_dataset("col", i32_dataset(&[1, 2]))
.unwrap();
let mut b = AppendBuilder::new();
b.append_i32(&[3, 4]);
e.stage_dataset_append_pending("col", b).unwrap();
assert_eq!(e.staged_dataset_meta("col").unwrap().dimensions, vec![4]);
assert!(e.staged.appends.is_empty());
e.commit().unwrap();
let addr = crate::group_v2::resolve_path_any_from_source(&e.image(), e.superblock(), "col")
.unwrap();
assert!(addr > 0);
}
#[test]
fn an_append_through_a_handle_onto_the_replaced_object_is_refused() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.delete("existing").unwrap();
e.stage_created_dataset("existing", i32_dataset(&[100]))
.unwrap();
let mut b = AppendBuilder::new();
b.append_i32(&[200]);
assert!(matches!(
e.stage_dataset_append("existing", b),
Err(Error::EditUnsupported(_))
));
assert_eq!(
e.staged_dataset_meta("existing").unwrap().dimensions,
vec![1],
"the refusal must leave the replacement alone"
);
assert!(e.staged.appends.is_empty());
}
#[test]
fn an_append_the_staged_dataset_cannot_carry_is_refused_without_changing_it() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.stage_created_dataset("col", i32_dataset(&[1, 2]))
.unwrap();
let mut wrong_type = AppendBuilder::new();
wrong_type.append_f64(&[1.0]);
assert!(matches!(
e.stage_dataset_append_pending("col", wrong_type),
Err(Error::AppendUnsupported(_))
));
let mut partial = AppendBuilder::new();
partial.append_raw(&[0u8, 1, 2]);
assert!(matches!(
e.stage_dataset_append_pending("col", partial),
Err(Error::AppendUnsupported(_))
));
let mut capped = DatasetBuilder::new("");
capped
.with_i32_data(&[1, 2])
.with_maxshape(&[3])
.with_chunks(&[2]);
e.stage_created_dataset("capped", capped).unwrap();
let mut over = AppendBuilder::new();
over.append_i32(&[3, 4]);
assert!(matches!(
e.stage_dataset_append_pending("capped", over),
Err(Error::AppendUnsupported(_))
));
assert_eq!(e.staged_dataset_meta("col").unwrap().dimensions, vec![2]);
assert_eq!(e.staged_dataset_meta("capped").unwrap().dimensions, vec![2]);
}
#[test]
fn an_append_onto_a_committed_dataset_still_stages_an_append() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
let mut b = AppendBuilder::new();
b.append_i32(&[4]);
e.stage_dataset_append("existing", b).unwrap();
assert_eq!(e.staged.appends.len(), 1);
let mut b = AppendBuilder::new();
b.append_i32(&[5]);
e.stage_dataset_append_pending("existing", b).unwrap();
assert_eq!(e.staged.appends.len(), 2);
}
#[test]
fn deleting_a_staged_creation_withdraws_it() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.stage_created_dataset("col", i32_dataset(&[1, 2]))
.unwrap();
e.delete("col").unwrap();
assert_eq!(kind(&e, "col"), None);
assert!(e.staged.deletes.is_empty());
assert!(!e.has_staged_edits());
e.commit().unwrap();
}
#[test]
fn deleting_a_staged_group_withdraws_its_staged_subtree() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.create_group("g").unwrap();
e.create_group("g/inner").unwrap();
e.stage_created_dataset("g/inner/col", i32_dataset(&[1]))
.unwrap();
e.set_group_attr("g", "kind", AttrValue::I64(1)).unwrap();
e.delete("g").unwrap();
assert_eq!(kind(&e, "g"), None);
assert_eq!(kind(&e, "g/inner"), None);
assert_eq!(kind(&e, "g/inner/col"), None);
assert!(
!e.has_staged_edits(),
"the attribute and the subtree go with the group"
);
e.commit().unwrap();
}
#[test]
fn deleting_a_staged_replacement_leaves_the_plain_deletion() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.delete("existing").unwrap();
e.stage_created_dataset("existing", i32_dataset(&[9]))
.unwrap();
e.delete("existing").unwrap();
assert_eq!(kind(&e, "existing"), None);
assert_eq!(e.staged.deletes.len(), 1);
e.commit().unwrap();
assert!(
crate::group_v2::resolve_path_any_from_source(&e.image(), e.superblock(), "existing",)
.is_err()
);
}
#[test]
fn a_batch_that_fails_leaves_the_staged_index_matching_the_staged_set() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.stage_created_dataset("kept", i32_dataset(&[1])).unwrap();
let refused: Result<(), Error> = e.stage_atomically(|s| {
s.create_group("gone")?;
s.stage_created_dataset("gone/col", i32_dataset(&[2]))?;
Err(Error::EditUnsupported("refused on purpose"))
});
assert!(refused.is_err());
assert_eq!(kind(&e, "gone"), None);
assert_eq!(kind(&e, "gone/col"), None);
assert_eq!(kind(&e, "kept"), Some(StagedKind::Dataset));
assert!(e.staged_children("gone").is_empty());
e.create_group("other").unwrap();
e.stage_created_dataset("other/col", i32_dataset(&[3]))
.unwrap();
e.create_group("gone").unwrap();
e.stage_created_dataset("gone/col", i32_dataset(&[2]))
.unwrap();
assert_eq!(kind(&e, "gone"), Some(StagedKind::Group));
assert_eq!(kind(&e, "gone/col"), Some(StagedKind::Dataset));
assert_eq!(
e.staged_dataset_meta("gone/col").unwrap().dimensions,
vec![1]
);
e.commit().unwrap();
}
#[test]
fn an_edit_that_changes_staged_work_is_refused_inside_a_batch() {
let dir = tempdir().unwrap();
let mut e = open_session(&dir.path().join("q.h5"));
e.stage_created_dataset("col", i32_dataset(&[1, 2]))
.unwrap();
let folded: Result<(), Error> = e.stage_atomically(|s| {
let mut b = AppendBuilder::new();
b.append_i32(&[3]);
s.stage_dataset_append_pending("col", b)
});
assert!(matches!(folded, Err(Error::EditUnsupported(_))));
let withdrawn: Result<(), Error> = e.stage_atomically(|s| s.delete("col"));
assert!(matches!(withdrawn, Err(Error::EditUnsupported(_))));
assert_eq!(e.staged_dataset_meta("col").unwrap().dimensions, vec![2]);
}
}
#[cfg(test)]
mod object_header_wrap_tests {
use super::*;
#[test]
fn an_unwalkable_region_is_refused_rather_than_wrapped() {
let mut region = vec![0x0Cu8]; region.extend_from_slice(&0xFFFFu16.to_le_bytes());
region.push(0); region.extend_from_slice(&[0u8; 4]);
let err = build_v2_object_header(®ion).unwrap_err();
assert!(
matches!(err, Error::EditUnsupported(_)),
"an unwalkable region gave {err:?}"
);
}
#[test]
fn a_walkable_region_still_gains_its_attribute_info() {
let body = [0u8; 8];
let mut region = vec![0x0Cu8]; region.extend_from_slice(&(body.len() as u16).to_le_bytes());
region.push(0); region.extend_from_slice(&body);
let oh = build_v2_object_header(®ion).unwrap();
assert_eq!(&oh[..4], b"OHDR");
assert!(
oh.len() > 8 + region.len() + 4,
"the wrapped header did not grow by an Attribute Info message"
);
}
}