use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::dataset::DatasetAccess;
use crate::format::btree_v1::{BTreeV1Config, ChunkBTreeV1Node, ChunkBTreeV1Tree, ChunkKey};
use crate::format::chunk_index::btree_v2::Bt2ChunkIndex;
use crate::format::chunk_index::extensible_array::{
compute_chunk_size_len, compute_ndblk_addrs, compute_nsblk_addrs, EaDblkPath, EaGeometry,
EaLoc, ExtensibleArrayDataBlock, ExtensibleArrayHeader, ExtensibleArrayIndexBlock,
ExtensibleArraySuperBlock, FilteredChunkEntry, FilteredDataBlock, FilteredIndexBlock,
EA_CLS_CHUNK, EA_CLS_FILT_CHUNK,
};
use crate::format::chunk_index::fixed_array::{
decode_filtered_page, decode_unfiltered_page, encode_filtered_page, encode_unfiltered_page,
FixedArrayDataBlock, FixedArrayFilteredChunkElement, FixedArrayHeader, FixedArrayPagedPrefix,
FA_CLIENT_FILT_CHUNK,
};
use crate::format::creation_order::CreationOrder;
use crate::format::dense_attr::build_dense_attributes;
use crate::format::dense_link::build_dense_links;
use crate::format::free_space::{
self, FreeSection, FreeSpaceClass, FreeSpaceHeader, FreeSpaceManager,
};
use crate::format::local_heap::{
local_heap_header_size, LocalHeapHeader, LocalHeapImage, LOCAL_HEAP_FREE_NULL,
};
use crate::format::messages::attr_info::{next_creation_index, AttributeInfoMessage};
use crate::format::messages::attribute::{
AttributeEntry, AttributeMessage, ATTR_FLAG_SPACE_SHARED, ATTR_FLAG_TYPE_SHARED,
};
use crate::format::messages::data_layout::{
DataLayoutMessage, EarrayParams, FixedArrayParams, LAYOUT_VERSION_DEFAULT,
};
use crate::format::messages::dataspace::{DataspaceClass, DataspaceMessage};
use crate::format::messages::datatype::{ByteOrder, DatatypeMessage, ReferenceKind};
use crate::format::messages::external_file_list::{ExternalFileListMessage, UNLIMITED};
use crate::format::messages::fill_value::{
FillValueMessage, FILL_TIME_ALLOC, FILL_TIME_IFSET, FILL_TIME_NEVER,
};
use crate::format::messages::filter::{self, FilterPipeline};
use crate::format::messages::group_info::GroupInfoMessage;
use crate::format::messages::link::{CharacterSet, LinkMessage, LinkTarget};
use crate::format::messages::link_info::LinkInfoMessage;
use crate::format::messages::mod_time::ModificationTime;
use crate::format::messages::superblock_ext::{
FileSpaceInfoMessage, FileSpaceStrategy, SharedMessageTableMessage,
DEFAULT_FILE_SPACE_PAGE_SIZE, FS_ADDR_COUNT_V1, PAGE_SIZE_MAX, PAGE_SIZE_MIN,
};
use crate::format::messages::virtual_mapping::{
parse_source_name, VirtualMapping, VirtualMappingList,
};
use crate::format::messages::*;
use crate::format::object_header::{ObjectHeader, ObjectTimes, MAX_MESSAGE_SIZE};
use crate::format::reference::{
encode_reference_element, encode_revised_blob, ReferenceElementImage, ReferenceTarget,
REVISED_BLOB_TOKEN_OFFSET,
};
use crate::format::selection::Selection;
use crate::format::sohm::{
type_flag, SharedMessagePointer, MAX_SOHM_INDEXES, SOHM_HEAP_ID_LEN, SOHM_POINTER_HEAP_ID_AT,
};
use crate::format::sohm_write::{
build_shared_messages, NestedShare, SharedMessage, SohmIndexContent, SohmIndexSpec,
};
use crate::format::superblock::*;
use crate::format::{FormatContext, LibverBound, ObjectFormat, UNDEF_ADDR};
use crate::format::selection::check_hyperslab;
use crate::io::allocator::{FileAllocator, FreeBlock};
use crate::io::file_handle::FileHandle;
use crate::io::hyperslab::{for_each_contiguous_run, for_each_dual_run};
use crate::io::symbol_table_io::{free_stab, write_stab, Stab, StabExtents, StabLink, StabTarget};
use crate::io::{FileMeta, IoResult};
fn fixed_array_dblk_disk_size(ctx: &FormatContext, hdr: &FixedArrayHeader) -> u64 {
let elem_size = hdr.element_size as u64;
let sa = ctx.sizeof_addr as u64;
let nelmts = hdr.num_elmts;
let meta_prefix = 4 + 1 + 1 + sa;
if hdr.is_paged() {
let npages = hdr.npages();
let bitmap_size = npages.div_ceil(8);
(meta_prefix + bitmap_size + 4) + nelmts * elem_size + npages * 4
} else {
meta_prefix + nelmts * elem_size + 4
}
}
struct Bt2Walk<'a> {
handle: &'a FileHandle,
ctx: &'a FormatContext,
record_size: u16,
node_size: u32,
geo: &'a crate::format::chunk_index::btree_v2::Bt2Geometry,
records: Vec<u8>,
node_addrs: Vec<u64>,
}
impl<'a> Bt2Walk<'a> {
fn new(
handle: &'a FileHandle,
ctx: &'a FormatContext,
record_size: u16,
node_size: u32,
geo: &'a crate::format::chunk_index::btree_v2::Bt2Geometry,
) -> Self {
Self {
handle,
ctx,
record_size,
node_size,
geo,
records: Vec::new(),
node_addrs: Vec::new(),
}
}
fn descend(&mut self, addr: u64, depth: u16, nrec: u16) -> IoResult<()> {
use crate::format::chunk_index::btree_v2::{Bt2InternalNode, Bt2LeafNode};
self.node_addrs.push(addr);
let buf = self.handle.read_at_most(addr, self.node_size as usize)?;
if depth == 0 {
let leaf = Bt2LeafNode::decode(&buf, nrec, self.record_size)?;
self.records.extend_from_slice(&leaf.record_data);
} else {
let node = Bt2InternalNode::decode(
&buf,
self.ctx,
depth,
nrec,
self.record_size,
self.geo.max_nrec_size,
self.geo.child_total_size(depth),
)?;
let children: Vec<(u64, u16)> = node
.child_addrs
.iter()
.zip(node.child_nrecords.iter())
.map(|(&a, &n)| (a, n))
.collect();
let rec = self.record_size as usize;
for (i, (child_addr, child_nrec)) in children.into_iter().enumerate() {
self.descend(child_addr, depth - 1, child_nrec)?;
if let Some(record) = node.record_data.get(i * rec..(i + 1) * rec) {
self.records.extend_from_slice(record);
}
}
}
Ok(())
}
}
struct BtreeV1Walk<'a> {
handle: &'a FileHandle,
ctx: &'a FormatContext,
config: &'a BTreeV1Config,
chunk_dims: &'a [u64],
file_size: u64,
records: Vec<BtreeV1ChunkRecord>,
node_addrs: Vec<u64>,
}
impl<'a> BtreeV1Walk<'a> {
fn new(
handle: &'a FileHandle,
ctx: &'a FormatContext,
config: &'a BTreeV1Config,
chunk_dims: &'a [u64],
file_size: u64,
) -> Self {
Self {
handle,
ctx,
config,
chunk_dims,
file_size,
records: Vec::new(),
node_addrs: Vec::new(),
}
}
fn descend(&mut self, addr: u64, depth: u32) -> IoResult<()> {
if depth > 256 {
return Err(crate::io::IoError::InvalidState(
"chunk B-tree v1 exceeds maximum depth".into(),
));
}
if addr == UNDEF_ADDR || addr >= self.file_size {
return Ok(());
}
let rank = self.chunk_dims.len();
let sa = self.ctx.sizeof_addr as usize;
let node_size = self.config.chunk_btree_node_size(sa, rank);
let buf = self.handle.read_at_most(addr, node_size)?;
let node = ChunkBTreeV1Node::decode(&buf, sa, rank, self.config.chunk_max_entries())?;
self.node_addrs.push(addr);
if node.level == 0 {
for (i, &child_addr) in node.children.iter().enumerate() {
let key = &node.keys[i];
let scaled: Vec<u64> = key.offsets[..rank]
.iter()
.zip(self.chunk_dims)
.map(|(&offset, &dim)| offset.checked_div(dim).unwrap_or(0))
.collect();
self.records.push(BtreeV1ChunkRecord {
scaled,
address: child_addr,
nbytes: key.chunk_size,
filter_mask: key.filter_mask,
});
}
} else {
for &child_addr in &node.children {
self.descend(child_addr, depth + 1)?;
}
}
Ok(())
}
}
fn encode_fixed_array_dblk(
ctx: &FormatContext,
hdr: &FixedArrayHeader,
dblk: &FixedArrayDataBlock,
) -> Vec<u8> {
let is_filtered = hdr.client_id == FA_CLIENT_FILT_CHUNK;
let sa = ctx.sizeof_addr as usize;
let chunk_size_len = (hdr.element_size as usize).saturating_sub(sa + 4);
if !hdr.is_paged() {
return if is_filtered {
dblk.encode_filtered(ctx, chunk_size_len)
} else {
dblk.encode_unfiltered(ctx)
};
}
let npages = hdr.npages() as usize;
let dblk_page_nelmts = hdr.dblk_page_nelmts() as usize;
let mut bitmap = vec![0u8; npages.div_ceil(8)];
let nelmts = if is_filtered {
dblk.filtered_elements.len()
} else {
dblk.elements.len()
};
for p in 0..npages {
let start = p * dblk_page_nelmts;
let end = ((p + 1) * dblk_page_nelmts).min(nelmts);
let initialized = if is_filtered {
dblk.filtered_elements[start..end]
.iter()
.any(|e| e.address != UNDEF_ADDR)
} else {
dblk.elements[start..end].iter().any(|&a| a != UNDEF_ADDR)
};
if initialized {
bitmap[p / 8] |= 0x80u8 >> (p % 8);
}
}
let prefix = FixedArrayPagedPrefix {
client_id: hdr.client_id,
header_addr: dblk.header_addr,
page_init_bitmap: bitmap,
prefix_size: 4 + 1 + 1 + sa + npages.div_ceil(8) + 4,
};
let mut buf = prefix.encode(ctx);
debug_assert_eq!(buf.len(), prefix.prefix_size);
for p in 0..npages {
let start = p * dblk_page_nelmts;
let end = ((p + 1) * dblk_page_nelmts).min(nelmts);
if is_filtered {
buf.extend_from_slice(&encode_filtered_page(
&dblk.filtered_elements[start..end],
ctx,
chunk_size_len,
));
} else {
buf.extend_from_slice(&encode_unfiltered_page(&dblk.elements[start..end], ctx));
}
}
buf
}
fn decode_fixed_array_dblk(
ctx: &FormatContext,
hdr: &FixedArrayHeader,
buf: &[u8],
chunk_size_len: usize,
) -> crate::format::FormatResult<FixedArrayDataBlock> {
let is_filtered = hdr.client_id == FA_CLIENT_FILT_CHUNK;
let num_elmts = hdr.num_elmts as usize;
if !hdr.is_paged() {
return if is_filtered {
FixedArrayDataBlock::decode_filtered(buf, ctx, num_elmts, chunk_size_len)
} else {
FixedArrayDataBlock::decode_unfiltered(buf, ctx, num_elmts)
};
}
let npages = hdr.npages() as usize;
let dblk_page_nelmts = hdr.dblk_page_nelmts() as usize;
let prefix = FixedArrayPagedPrefix::decode(buf, ctx, npages as u64)?;
let mut dblk = if is_filtered {
FixedArrayDataBlock::new_filtered(prefix.header_addr, num_elmts)
} else {
FixedArrayDataBlock::new_unfiltered(prefix.header_addr, num_elmts)
};
dblk.client_id = hdr.client_id;
let mut pos = prefix.prefix_size;
for p in 0..npages {
let start = p * dblk_page_nelmts;
let end = ((p + 1) * dblk_page_nelmts).min(num_elmts);
let nelmts = end - start;
if prefix.page_initialized(p) {
let page_buf = buf.get(pos..).unwrap_or(&[]);
if is_filtered {
let elems = decode_filtered_page(page_buf, ctx, nelmts, chunk_size_len)?;
dblk.filtered_elements[start..end].clone_from_slice(&elems);
} else {
let addrs = decode_unfiltered_page(page_buf, ctx, nelmts)?;
dblk.elements[start..end].copy_from_slice(&addrs);
}
}
pos += nelmts * hdr.element_size as usize + 4;
}
Ok(dblk)
}
#[cfg(not(feature = "threadsafe"))]
pub(crate) struct Slot<T>(std::cell::RefCell<T>);
#[cfg(not(feature = "threadsafe"))]
impl<T> Slot<T> {
pub(crate) fn new(value: T) -> Self {
Slot(std::cell::RefCell::new(value))
}
pub(crate) fn lock(&self) -> std::cell::RefMut<'_, T> {
self.0.borrow_mut()
}
}
#[cfg(feature = "threadsafe")]
pub(crate) struct Slot<T>(std::sync::Mutex<T>);
#[cfg(feature = "threadsafe")]
impl<T> Slot<T> {
pub(crate) fn new(value: T) -> Self {
Slot(std::sync::Mutex::new(value))
}
pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, T> {
self.0.lock().unwrap()
}
}
pub(crate) struct CreateGuard<'a> {
#[cfg(not(feature = "threadsafe"))]
_gate: std::cell::RefMut<'a, ()>,
#[cfg(feature = "threadsafe")]
_gate: std::sync::MutexGuard<'a, ()>,
pub(crate) name: String,
pub(crate) parent: Option<usize>,
}
#[cfg(not(feature = "threadsafe"))]
pub(crate) type Shared<T> = std::rc::Rc<T>;
#[cfg(feature = "threadsafe")]
pub(crate) type Shared<T> = std::sync::Arc<T>;
pub(crate) struct DatasetCell {
pub(crate) op: Slot<()>,
info: Slot<DatasetInfo>,
}
impl DatasetCell {
pub(crate) fn new(info: DatasetInfo) -> Self {
DatasetCell {
op: Slot::new(()),
info: Slot::new(info),
}
}
#[cfg(not(feature = "threadsafe"))]
pub(crate) fn lock(&self) -> std::cell::RefMut<'_, DatasetInfo> {
self.info.lock()
}
#[cfg(feature = "threadsafe")]
pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, DatasetInfo> {
self.info.lock()
}
}
pub(crate) type DatasetRef = Shared<DatasetCell>;
pub(crate) type GroupRef = Shared<Slot<GroupInfo>>;
pub struct AppendBuffer {
pub base: u64,
pub frames: u64,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalFile {
pub name: String,
pub name_offset: u64,
pub offset: u64,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct ExternalStorage {
pub heap_addr: u64,
pub files: Vec<ExternalFile>,
prefix: EfilePrefix,
}
#[derive(Debug, Clone, Default)]
struct EfilePrefix {
expanded: Option<PathBuf>,
open: std::sync::Weak<()>,
}
impl ExternalStorage {
fn message(&self) -> ExternalFileListMessage {
ExternalFileListMessage {
heap_addr: self.heap_addr,
slots: self
.files
.iter()
.map(
|f| crate::format::messages::external_file_list::ExternalFileSlot {
name_offset: f.name_offset,
offset: f.offset,
size: f.size,
},
)
.collect(),
}
}
fn total_size(&self) -> u64 {
self.files
.iter()
.fold(0u64, |acc, f| acc.saturating_add(f.size))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VirtualStorage {
pub heap_addr: u64,
pub heap_index: u32,
pub mappings: Vec<VirtualMapping>,
}
enum ContiguousTarget {
Local(u64),
External {
files: Vec<ExternalFile>,
prefix: Option<PathBuf>,
},
Virtual,
}
pub(crate) struct DatasetHandleParts {
pub(crate) shape: Vec<usize>,
pub(crate) element_size: usize,
pub(crate) chunk_index: Option<ChunkIndexKind>,
pub(crate) open: Option<crate::io::reader::DatasetOpenToken>,
}
impl ContiguousTarget {
fn is_storage(&self) -> bool {
!matches!(self, Self::Virtual)
}
}
fn check_virtual_mapping(dataset: &str, m: &VirtualMapping) -> IoResult<()> {
for (which, sel) in [
("virtual", &m.virtual_selection),
("source", &m.source_selection),
] {
if matches!(sel, Selection::Points(_)) {
return Err(crate::io::IoError::Unsupported(format!(
"virtual dataset '{dataset}' has a point {which} selection, which \
H5D_virtual_check_mapping_pre refuses for every virtual dataset mapping \
(\"point selections not currently supported with virtual datasets\")"
)));
}
}
let unlim_virtual = m.virtual_selection.unlim_dim().is_some();
let unlim_source = m.source_selection.unlim_dim().is_some();
if unlim_virtual && unlim_source {
if let (Some(v), Some(sr)) = (
regular_hyperslab(&m.virtual_selection),
regular_hyperslab(&m.source_selection),
) {
let (nv, ns) = (v.num_elem_non_unlim(), sr.num_elem_non_unlim());
if nv != ns {
return Err(crate::io::IoError::InvalidState(format!(
"virtual dataset '{dataset}' maps an unlimited source selection onto an \
unlimited virtual selection, but a slice of the non-unlimited \
dimensions holds {ns:?} source elements and {nv:?} virtual ones"
)));
}
}
}
let nsubs = parse_source_name(&m.source_file_name)
.and_then(|f| Ok(f.nsubs() + parse_source_name(&m.source_dset_name)?.nsubs()))
.map_err(|e| {
crate::io::IoError::InvalidState(format!(
"virtual dataset '{dataset}' source name: {e}"
))
})?;
if unlim_virtual && !unlim_source {
if nsubs == 0 {
return Err(crate::io::IoError::InvalidState(format!(
"virtual dataset '{dataset}' has an unlimited virtual selection, a limited \
source selection, and no printf specifiers in source names"
)));
}
if !matches!(m.virtual_selection, Selection::Hyperslab { .. }) {
return Err(crate::io::IoError::InvalidState(format!(
"virtual dataset '{dataset}' has a printf mapping whose virtual selection is \
not a hyperslab; the substitution runs over the blocks of that hyperslab"
)));
}
} else if nsubs > 0 {
return Err(crate::io::IoError::InvalidState(format!(
"virtual dataset '{dataset}' has printf specifier(s) in source name(s) without \
an unlimited virtual selection and limited source selection"
)));
}
Ok(())
}
fn regular_hyperslab(sel: &Selection) -> Option<&crate::format::selection::RegularHyperslab> {
match sel {
Selection::Hyperslab {
form: crate::format::selection::Hyperslab::Regular(r),
..
} => Some(r),
_ => None,
}
}
fn virtual_write_refused() -> crate::io::IoError {
crate::io::IoError::Unsupported(
"cannot write into a virtual dataset: its elements live in the source datasets \
its mappings name, and this writer does not write through to them — write the \
source datasets themselves"
.into(),
)
}
pub struct DatasetInfo {
pub name: String,
pub datatype: DatatypeMessage,
pub committed_type: Option<CommittedTypeRef>,
pub dataspace: DataspaceMessage,
pub read_format: Option<ObjectFormat>,
pub obj_header_addr: u64,
pub data_addr: u64,
pub data_size: u64,
pub compact: Option<Vec<u8>>,
pub external: Option<ExternalStorage>,
pub virtual_storage: Option<VirtualStorage>,
pub chunked: Option<ChunkedDatasetInfo>,
pub fixed_array: Option<FixedArrayDatasetInfo>,
pub btree_v2: Option<Bt2DatasetInfo>,
pub implicit: Option<ImplicitDatasetInfo>,
pub single_chunk: Option<SingleChunkDatasetInfo>,
pub btree_v1: Option<BtreeV1DatasetInfo>,
pub append: Option<AppendBuffer>,
pub attributes: Vec<AttributeEntry>,
pub obj_header_written_addr: Option<u64>,
pub obj_header_blocks: crate::io::object_header_io::HeaderBlocks,
pub filter_pipeline: Option<FilterPipeline>,
pub deleted: bool,
pub extent_dirty: bool,
pub header_dirty: bool,
pub nlink_written: u32,
pub creation_seq: u64,
pub track_attr_order: CreationOrder,
pub fill_value: Option<Vec<u8>>,
pub fill_time: u8,
pub layout_version: u8,
pub times: Option<ObjectTimes>,
}
impl DatasetInfo {
pub(crate) fn chunk_index_kind(&self) -> Option<ChunkIndexKind> {
if self.chunked.is_some() {
Some(ChunkIndexKind::ExtensibleArray)
} else if self.fixed_array.is_some() {
Some(ChunkIndexKind::FixedArray)
} else if self.btree_v2.is_some() {
Some(ChunkIndexKind::BtreeV2)
} else if self.implicit.is_some() {
Some(ChunkIndexKind::Implicit)
} else if self.single_chunk.is_some() {
Some(ChunkIndexKind::SingleChunk)
} else if self.btree_v1.is_some() {
Some(ChunkIndexKind::BtreeV1)
} else {
None
}
}
pub(crate) fn is_chunked(&self) -> bool {
self.chunk_index_kind().is_some()
}
fn contiguous_target(&self) -> Option<ContiguousTarget> {
if self.is_chunked() || self.compact.is_some() {
return None;
}
if self.virtual_storage.is_some() {
return Some(ContiguousTarget::Virtual);
}
match &self.external {
Some(ext) => Some(ContiguousTarget::External {
files: ext.files.clone(),
prefix: ext.prefix.expanded.clone(),
}),
None => {
(self.data_addr != UNDEF_ADDR).then_some(ContiguousTarget::Local(self.data_addr))
}
}
}
fn implicit_grid(&self) -> Option<(u64, u64)> {
let imp = self.implicit.as_ref()?;
(imp.data_addr != UNDEF_ADDR).then_some((imp.data_addr, imp.data_size))
}
fn allocated_storage_run(&self) -> Option<(ContiguousTarget, u64)> {
match self.implicit_grid() {
Some((addr, size)) => Some((ContiguousTarget::Local(addr), size)),
None => match self.contiguous_target() {
Some(t @ ContiguousTarget::Local(_)) => Some((t, self.data_size)),
_ => None,
},
}
}
fn storage_dirty(&self) -> bool {
self.chunked.as_ref().is_some_and(|c| c.chunks_written > 0)
|| self
.fixed_array
.as_ref()
.is_some_and(|f| f.chunks_written > 0)
|| self.btree_v2.as_ref().is_some_and(|b| b.chunks_written > 0)
|| self.btree_v1.as_ref().is_some_and(|b| b.chunks_written > 0)
|| self
.single_chunk
.as_ref()
.is_some_and(|s| s.chunks_written > 0)
|| self.extent_dirty
}
fn header_stale(&self) -> bool {
self.storage_dirty() || self.header_dirty
}
fn header_stale_with(&self, nlink: u32) -> bool {
self.header_stale() || nlink != self.nlink_written
}
fn header_written(&mut self, nlink: u32) {
self.nlink_written = nlink;
}
}
pub struct ChunkedDatasetInfo {
pub chunk_dims: Vec<u64>,
pub earray_params: EarrayParams,
pub ea_header_addr: u64,
pub ea_iblk_addr: u64,
pub ea_header: ExtensibleArrayHeader,
pub ea_iblk: ExtensibleArrayIndexBlock,
pub chunks_written: u64,
pub filt_iblk: Option<FilteredIndexBlock>,
pub chunk_size_len: u8,
}
enum DblkParent {
IndexBlock(usize),
SuperBlock {
sblk_addr: u64,
ndblks_in_sblk: usize,
local_dblk: usize,
},
}
#[derive(Clone, Copy)]
pub enum AttrTarget<'a> {
Root,
Group(&'a str),
Dataset(usize),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ChunkIndexKind {
ExtensibleArray,
FixedArray,
BtreeV2,
Implicit,
SingleChunk,
BtreeV1,
}
struct ChunkGeometry {
kind: ChunkIndexKind,
dims: Vec<u64>,
max_dims: Option<Vec<u64>>,
chunk_dims: Vec<u64>,
element_size: u64,
}
impl ChunkGeometry {
fn chunk_bytes(&self) -> u64 {
self.chunk_dims.iter().product::<u64>() * self.element_size
}
fn linear_index(&self, coords: &[u64]) -> IoResult<u64> {
crate::io::chunk_grid::linear_index(
&self.dims,
self.max_dims.as_deref(),
&self.chunk_dims,
coords,
)
}
}
fn swmr_attr_error(name: &str) -> crate::io::IoError {
crate::io::IoError::InvalidState(format!(
"cannot add or modify attribute '{name}' during SWMR streaming: object \
headers are frozen while readers stream, and a superseded variable-length \
value's heap storage could never be reclaimed; set attributes before \
start_swmr (libhdf5 forbids attribute changes during SWMR writes too)"
))
}
#[derive(Debug, Clone, Copy)]
enum AttrOrigin {
Created,
Rewritten(Option<u16>),
}
use AttrOrigin::{Created, Rewritten};
fn take_reopened_attributes(
attrs: crate::io::reader::ObjectAttributes,
owner: &str,
) -> IoResult<Vec<AttributeEntry>> {
let mut attrs = attrs.into_complete(owner)?;
attrs.sort_by_key(|a| a.creation_index());
Ok(attrs)
}
fn recover_track_order(
header: &crate::format::object_header::ObjectHeader,
ctx: &FormatContext,
) -> TrackOrder {
let links = header
.messages
.iter()
.find(|m| m.msg_type == crate::format::messages::MSG_LINK_INFO)
.and_then(|m| LinkInfoMessage::decode(&m.data, ctx).ok())
.map(|(info, _)| info.creation_order())
.unwrap_or_default();
TrackOrder {
links,
attrs: header.attribute_creation_order(),
}
}
fn touch_oh(
header: &mut ObjectHeader,
format: ObjectFormat,
times: Option<ObjectTimes>,
force: bool,
) {
let Some(times) = touched_times(times) else {
return;
};
match format {
ObjectFormat::Modern => header.times = Some(times),
ObjectFormat::Legacy if force => header.add_message(
crate::format::messages::MSG_MOD_TIME,
0x00,
ModificationTime(times.change).encode(),
),
ObjectFormat::Legacy => {}
}
}
fn touched_times(times: Option<ObjectTimes>) -> Option<ObjectTimes> {
times.map(|t| t.touched(now_seconds()))
}
fn now_seconds() -> u32 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| u32::try_from(d.as_secs()).unwrap_or(u32::MAX))
}
fn superseded_dense(
header: &crate::format::object_header::ObjectHeader,
ctx: &FormatContext,
) -> (Option<AttributeInfoMessage>, Option<LinkInfoMessage>) {
let decode = |msg_type: u8| {
header
.messages
.iter()
.find(|m| m.msg_type == msg_type)
.map(|m| m.data.as_slice())
};
let attrs = decode(crate::format::messages::MSG_ATTR_INFO)
.and_then(|d| AttributeInfoMessage::decode(d, ctx).ok())
.map(|(info, _)| info)
.filter(|info| info.is_dense());
let links = decode(crate::format::messages::MSG_LINK_INFO)
.and_then(|d| LinkInfoMessage::decode(d, ctx).ok())
.map(|(info, _)| info)
.filter(|info| info.is_dense());
(attrs, links)
}
struct CwfsEntry {
addr: u64,
size: usize,
free: usize,
}
const H5HG_NCWFS: usize = 16;
fn cwfs_note(cwfs: &mut Vec<CwfsEntry>, addr: u64, size: usize, free: usize) {
if let Some(p) = cwfs.iter().position(|e| e.addr == addr) {
cwfs[p].free = free;
return;
}
if cwfs.len() < H5HG_NCWFS {
cwfs.insert(0, CwfsEntry { addr, size, free });
return;
}
if let Some(p) = (0..cwfs.len()).min_by_key(|&p| cwfs[p].free) {
if free > cwfs[p].free {
cwfs[p] = CwfsEntry { addr, size, free };
}
}
}
fn swmr_delete_error(name: &str) -> crate::io::IoError {
crate::io::IoError::InvalidState(format!(
"cannot delete '{name}' during SWMR streaming: a reader may hold the \
object's header and storage addresses (libhdf5 forbids link deletion \
during SWMR writes too)"
))
}
fn chunk_outside_extent(coords: &[u64], chunk_dims: &[u64], extent: &[u64]) -> bool {
coords
.iter()
.zip(chunk_dims)
.zip(extent)
.any(|((&c, &cd), &e)| c.saturating_mul(cd) >= e)
}
fn chunk_straddles_extent(coords: &[u64], chunk_dims: &[u64], extent: &[u64]) -> bool {
!chunk_outside_extent(coords, chunk_dims, extent)
&& coords
.iter()
.zip(chunk_dims)
.zip(extent)
.any(|((&c, &cd), &e)| (c + 1).saturating_mul(cd) > e)
}
fn refill_chunk_beyond_extent(
data: &mut [u8],
fill: &[u8],
coords: &[u64],
chunk_dims: &[u64],
extent: &[u64],
element_size: usize,
) -> Vec<u8> {
let ndims = chunk_dims.len();
let keep: Vec<usize> = (0..ndims)
.map(|d| {
let origin = coords[d] * chunk_dims[d];
chunk_dims[d].min(extent[d].saturating_sub(origin)) as usize
})
.collect();
let row_elems = chunk_dims[ndims - 1] as usize;
let keep_last = keep[ndims - 1];
let nrows: u64 = chunk_dims[..ndims - 1].iter().product();
let mut replaced = Vec::new();
for r in 0..nrows {
let mut rem = r;
let mut in_keep = true;
for d in (0..ndims - 1).rev() {
let c = rem % chunk_dims[d];
rem /= chunk_dims[d];
if c as usize >= keep[d] {
in_keep = false;
}
}
let start = if in_keep { keep_last } else { 0 };
if start == row_elems {
continue;
}
let a = (r as usize * row_elems + start) * element_size;
let b = (r as usize + 1) * row_elems * element_size;
replaced.extend_from_slice(&data[a..b]);
data[a..b].copy_from_slice(&fill[a..b]);
}
replaced
}
fn validate_chunk_geometry(dims: &[u64], max_dims: &[u64], chunk_dims: &[u64]) -> IoResult<()> {
let ndims = dims.len();
if chunk_dims.len() != ndims {
return Err(crate::io::IoError::InvalidState(format!(
"chunk shape has {} dimensions but the dataspace has {}",
chunk_dims.len(),
ndims
)));
}
if max_dims.len() != ndims {
return Err(crate::io::IoError::InvalidState(format!(
"maximum shape has {} dimensions but the dataspace has {}",
max_dims.len(),
ndims
)));
}
for d in 0..ndims {
if chunk_dims[d] == 0 {
return Err(crate::io::IoError::InvalidState(format!(
"chunk dimension {d} is zero"
)));
}
if dims[d] != 0 && max_dims[d] != u64::MAX && max_dims[d] < chunk_dims[d] {
return Err(crate::io::IoError::InvalidState(format!(
"chunk dimension {} is {} but the maximum dimension size is {}",
d, chunk_dims[d], max_dims[d]
)));
}
}
Ok(())
}
fn ensure_at_most_one_unlimited(max_dims: &[u64]) -> IoResult<()> {
let unlimited: Vec<usize> = max_dims
.iter()
.enumerate()
.filter(|&(_, &m)| m == u64::MAX)
.map(|(d, _)| d)
.collect();
if unlimited.len() > 1 {
return Err(crate::io::IoError::InvalidState(format!(
"an extensible-array index supports at most one unlimited dimension, \
but dimensions {unlimited:?} are all unlimited; a v2 B-tree index \
handles two or more"
)));
}
Ok(())
}
fn ensure_vlen_charset(charset: u8, strings: &[&str]) -> IoResult<()> {
if charset == 0 {
if let Some((i, s)) = strings.iter().enumerate().find(|(_, s)| !s.is_ascii()) {
return Err(crate::io::IoError::InvalidState(format!(
"string {i} ({s:?}) is not ASCII, but the dataset's character set is"
)));
}
}
Ok(())
}
pub struct FixedArrayDatasetInfo {
pub chunk_dims: Vec<u64>,
pub fa_header_addr: u64,
pub fa_dblk_addr: u64,
pub fa_header: FixedArrayHeader,
pub fa_dblk: FixedArrayDataBlock,
pub chunks_written: u64,
}
pub struct ImplicitDatasetInfo {
pub chunk_dims: Vec<u64>,
pub data_addr: u64,
pub data_size: u64,
}
pub struct SingleChunkDatasetInfo {
pub chunk_dims: Vec<u64>,
pub data_addr: u64,
pub data_size: u64,
pub nbytes: u64,
pub filter_mask: u32,
pub chunks_written: u64,
pub early_alloc: bool,
}
pub struct BtreeV1ChunkRecord {
pub scaled: Vec<u64>,
pub address: u64,
pub nbytes: u32,
pub filter_mask: u32,
}
pub struct BtreeV1DatasetInfo {
pub chunk_dims: Vec<u64>,
pub max_dims: Vec<u64>,
pub config: BTreeV1Config,
pub records: Vec<BtreeV1ChunkRecord>,
pub node_addrs: Vec<u64>,
pub root_addr: u64,
pub chunks_written: u64,
}
impl BtreeV1DatasetInfo {
fn key_dims(&self, element_size: u64) -> Vec<u64> {
let mut dims = self.chunk_dims.clone();
dims.push(element_size);
dims
}
fn build_tree(&self, element_size: u64, sizeof_addr: usize) -> ChunkBTreeV1Tree {
let dims = self.key_dims(element_size);
let entries: Vec<(ChunkKey, u64)> = self
.records
.iter()
.map(|r| {
(
ChunkKey::for_chunk(&r.scaled, &dims, r.nbytes, r.filter_mask),
r.address,
)
})
.collect();
let last = self
.records
.last()
.map_or_else(|| vec![0; self.chunk_dims.len()], |r| r.scaled.clone());
ChunkBTreeV1Tree::build(
&entries,
ChunkKey::right_bound(&last, &dims),
&self.config,
sizeof_addr,
)
}
fn position(&self, scaled: &[u64]) -> Result<usize, usize> {
self.records
.binary_search_by(|r| r.scaled.as_slice().cmp(scaled))
}
}
pub struct Bt2DatasetInfo {
pub chunk_dims: Vec<u64>,
pub bt2_header_addr: u64,
pub node_addrs: Vec<u64>,
pub index: Bt2ChunkIndex,
pub chunks_written: u64,
}
pub struct GroupInfo {
pub name: String,
pub parent: Option<usize>,
pub child_datasets: Vec<usize>,
pub child_groups: Vec<usize>,
pub obj_header_addr: u64,
pub obj_header_written_addr: Option<u64>,
pub obj_header_blocks: crate::io::object_header_io::HeaderBlocks,
pub deleted: bool,
pub attributes: Vec<AttributeEntry>,
pub creation_seq: u64,
pub track_order: TrackOrder,
pub times: Option<ObjectTimes>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TrackOrder {
pub links: CreationOrder,
pub attrs: CreationOrder,
}
impl TrackOrder {
pub fn uniform(track: bool) -> Self {
let order = if track {
CreationOrder::Indexed
} else {
CreationOrder::Untracked
};
Self {
links: order,
attrs: order,
}
}
}
#[derive(Clone, Copy)]
pub enum HardLinkTarget {
Dataset(usize),
Group(usize),
}
#[derive(Clone)]
pub struct HardLink {
pub parent: Option<usize>,
pub name: String,
pub target: HardLinkTarget,
pub creation_seq: u64,
}
#[derive(Clone)]
pub struct SymbolicLink {
pub parent: Option<usize>,
pub name: String,
pub target: LinkTarget,
pub creation_seq: u64,
}
#[derive(Clone)]
pub struct CommittedDatatype {
pub name: String,
pub parent: Option<usize>,
pub datatype: DatatypeMessage,
pub creation_seq: u64,
pub times: Option<ObjectTimes>,
pub obj_header_addr: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommittedTypeRef {
Session(usize),
Preserved(u64),
}
#[derive(Clone)]
pub struct PreservedLink {
pub parent: Option<usize>,
pub name: String,
pub class: crate::io::reader::LinkClass,
pub encoded: Vec<u8>,
pub reason: Option<String>,
pub kind: PreservedKind,
}
#[derive(Default)]
struct CollectedLinks {
hard: Vec<(HardEntry, CollectedObject)>,
preserved: Vec<PreservedEntry>,
}
#[derive(Clone)]
struct HardEntry {
path: String,
address: u64,
encoded: Vec<u8>,
}
struct PreservedEntry {
path: String,
class: crate::io::reader::LinkClass,
encoded: Vec<u8>,
reason: Option<String>,
kind: PreservedKind,
}
enum ObjectPlan {
Dataset(Box<DatasetParts>),
Group(GroupParts),
Preserve { why: String, kind: PreservedKind },
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PreservedKind {
Unclassified,
NamedDatatype,
}
impl ObjectPlan {
fn preserve(why: impl Into<String>) -> Self {
ObjectPlan::Preserve {
why: why.into(),
kind: PreservedKind::Unclassified,
}
}
}
struct DatasetParts {
header_blocks: crate::io::object_header_io::HeaderBlocks,
datatype: DatatypeMessage,
committed_type: Option<u64>,
dataspace: crate::format::messages::dataspace::DataspaceMessage,
read_format: ObjectFormat,
layout: crate::format::messages::data_layout::DataLayoutMessage,
filter_pipeline: Option<FilterPipeline>,
fill_value: Option<Vec<u8>>,
fill_write_time: u8,
attributes: Vec<AttributeEntry>,
track_order: TrackOrder,
times: Option<ObjectTimes>,
dense: DenseCarry,
external: Option<ExternalStorage>,
}
struct GroupParts {
header_blocks: crate::io::object_header_io::HeaderBlocks,
attributes: Vec<AttributeEntry>,
links: Vec<(crate::format::messages::link::LinkMessage, Vec<u8>)>,
track_order: TrackOrder,
times: Option<ObjectTimes>,
dense: DenseCarry,
stab: Option<StabExtents>,
}
#[derive(Default)]
struct DenseCarry {
attrs: Option<AttributeInfoMessage>,
links: Option<LinkInfoMessage>,
}
enum CollectedObject {
Dataset(Box<DatasetParts>),
Group {
header_blocks: crate::io::object_header_io::HeaderBlocks,
attributes: Vec<AttributeEntry>,
track_order: TrackOrder,
times: Option<ObjectTimes>,
dense: DenseCarry,
stab: Option<StabExtents>,
},
}
struct ReopenWalk<'a> {
handle: &'a mut FileHandle,
meta: &'a crate::io::FileMeta,
out: CollectedLinks,
visited: std::collections::HashSet<u64>,
}
impl<'a> ReopenWalk<'a> {
fn new(handle: &'a mut FileHandle, meta: &'a crate::io::FileMeta) -> Self {
Self {
handle,
meta,
out: CollectedLinks::default(),
visited: std::collections::HashSet::new(),
}
}
fn finish(self) -> CollectedLinks {
self.out
}
fn plan(&mut self, addr: u64) -> IoResult<ObjectPlan> {
let (handle, meta) = (&mut *self.handle, self.meta);
let ctx = &meta.ctx;
use crate::format::messages::data_layout::DataLayoutMessage;
use crate::format::messages::dataspace::DataspaceMessage;
use crate::format::messages::link::{CharacterSet, LinkMessage};
use crate::format::messages::link_info::LinkInfoMessage;
use crate::format::messages::shared::MSG_FLAG_SHARED;
use crate::format::messages::{
MSG_ATTRIBUTE, MSG_DATASPACE, MSG_DATATYPE, MSG_DATA_LAYOUT, MSG_EXTERNAL_FILE_LIST,
MSG_FILL_VALUE, MSG_FILTER_PIPELINE, MSG_LINK, MSG_LINK_INFO, MSG_SYMBOL_TABLE,
};
let (header, header_blocks) =
match crate::io::object_header_io::read_object_header_with_blocks(handle, meta, addr) {
Ok(h) => h,
Err(e) => {
return Ok(ObjectPlan::preserve(format!(
"its object header chain does not read: {e}"
)))
}
};
let track_order = recover_track_order(&header, ctx);
let times = header.recorded_times();
let (dense_attrs, dense_links) = superseded_dense(&header, ctx);
let attributes = match take_reopened_attributes(
crate::io::reader::collect_object_attributes(handle, ctx, &header),
&format!("the object at {addr:#x}"),
) {
Ok(a) => a,
Err(e) => {
return Ok(ObjectPlan::preserve(format!(
"its attributes do not read back whole: {e}"
)))
}
};
let mut datatype = None;
let mut dataspace = None;
let mut layout = None;
let mut filter_pipeline = None;
let mut fill_value = None;
let mut fill_write_time: u8 = FILL_TIME_IFSET;
let mut external = None;
let mut links = Vec::new();
let mut stab = None;
let mut dataset_shaped = false;
for msg in &header.messages {
let consumed = matches!(
msg.msg_type,
MSG_DATATYPE
| MSG_DATASPACE
| MSG_DATA_LAYOUT
| MSG_FILTER_PIPELINE
| MSG_FILL_VALUE
| MSG_EXTERNAL_FILE_LIST
| MSG_ATTRIBUTE
| MSG_LINK
| MSG_LINK_INFO
| MSG_SYMBOL_TABLE
);
if consumed && msg.flags & MSG_FLAG_SHARED != 0 {
return Ok(ObjectPlan::preserve(format!(
"its message of type {:#04x} is a shared-message reference, which this \
writer does not resolve",
msg.msg_type
)));
}
macro_rules! consume {
($decode:expr, $what:literal) => {
match $decode {
Ok(v) => v,
Err(e) => {
return Ok(ObjectPlan::preserve(format!(
"its {} message does not decode: {e}",
$what
)))
}
}
};
}
match msg.msg_type {
crate::format::messages::MSG_MOD_TIME_OLD => {
return Ok(ObjectPlan::preserve(
"it carries a pre-1.6 modification time message, which this writer \
reads but does not write",
))
}
MSG_DATATYPE => {
dataset_shaped = true;
let (dt, _) = consume!(DatatypeMessage::decode(&msg.data, ctx), "datatype");
datatype = Some(dt);
}
MSG_DATASPACE => {
dataset_shaped = true;
let version = msg.data.first().copied().unwrap_or(1);
let (ds, _) = consume!(DataspaceMessage::decode(&msg.data, ctx), "dataspace");
dataspace = Some((ds, version));
}
MSG_DATA_LAYOUT => {
dataset_shaped = true;
let (dl, _) =
consume!(DataLayoutMessage::decode(&msg.data, ctx), "data layout");
layout = Some(dl);
}
MSG_FILTER_PIPELINE => {
let (p, _) = consume!(FilterPipeline::decode(&msg.data), "filter pipeline");
if !p.filters.is_empty() {
filter_pipeline = Some(p);
}
}
MSG_FILL_VALUE => {
let (fv, _) = consume!(FillValueMessage::decode(&msg.data), "fill value");
if fv.fill_defined == 2 {
fill_value = fv.fill_value;
}
fill_write_time = fv.fill_write_time;
}
MSG_EXTERNAL_FILE_LIST => {
dataset_shaped = true;
let (efl, _) = consume!(
ExternalFileListMessage::decode(&msg.data, ctx),
"external file list"
);
let resolved = match crate::io::reader::Hdf5Reader::resolve_external_file_slots(
handle, ctx, &efl,
) {
Ok(r) => r,
Err(e) => {
return Ok(ObjectPlan::preserve(format!(
"its external file list names do not read back: {e}"
)))
}
};
external = Some(ExternalStorage {
heap_addr: efl.heap_addr,
prefix: EfilePrefix::default(),
files: efl
.slots
.iter()
.zip(resolved)
.map(|(slot, seg)| ExternalFile {
name: seg.name,
name_offset: slot.name_offset,
offset: slot.offset,
size: slot.size,
})
.collect(),
});
}
MSG_LINK => {
let (l, _) = consume!(LinkMessage::decode(&msg.data, ctx), "link");
links.push((l, msg.data.clone()));
}
MSG_LINK_INFO => {
let (li, _) = consume!(LinkInfoMessage::decode(&msg.data, ctx), "link info");
if li.fractal_heap_address != UNDEF_ADDR {
let dense = match crate::io::reader::Hdf5Reader::read_dense_links(
handle,
ctx,
li.fractal_heap_address,
) {
Ok(l) => l,
Err(e) => {
return Ok(ObjectPlan::preserve(format!(
"its dense link storage does not read: {e}"
)))
}
};
links.extend(dense.into_iter().map(|l| {
let bytes = l.encode(ctx);
(l, bytes)
}));
}
}
MSG_SYMBOL_TABLE => {
let Some(s) = Stab::decode(&msg.data, ctx) else {
return Ok(ObjectPlan::preserve(
"its symbol table message is shorter than the two addresses it \
must carry",
));
};
let contents = match crate::io::symbol_table_io::read_stab(handle, meta, s) {
Ok(c) => c,
Err(e) => {
return Ok(ObjectPlan::preserve(format!(
"its symbol table does not read: {e}"
)))
}
};
stab = Some(contents.extents);
links.extend(contents.links.into_iter().map(|l| {
let msg = match l.target {
StabTarget::Hard { addr, .. } => LinkMessage::hard(&l.name, addr),
StabTarget::Soft { value } => LinkMessage::soft(&l.name, &value),
};
let msg = msg.with_cset(CharacterSet::Ascii);
let bytes = msg.encode(ctx);
(msg, bytes)
}));
}
_ => {}
}
}
if let (Some((ds, _)), Some(dt), Some(dl)) = (&dataspace, &datatype, &layout) {
if let Err(e) = dl.check_against_dataset(ds, dt, ctx) {
return Ok(ObjectPlan::preserve(format!(
"its layout doesn't fit its dataspace and datatype: {e}"
)));
}
}
match (datatype, dataspace, layout) {
(Some(_), Some(_), Some(layout)) if !layout_rebuilds(&layout) => {
Ok(ObjectPlan::preserve(format!(
"its data layout is {}, which this writer reads but does not build",
layout.describe()
)))
}
(Some(datatype), Some((dataspace, dataspace_version)), Some(layout)) => {
let committed_type = match crate::io::object_header_io::committed_datatype_address(
handle, meta, addr,
) {
Ok(c) => c,
Err(e) => {
return Ok(ObjectPlan::preserve(format!(
"its shared datatype pointer does not decode: {e}"
)))
}
};
Ok(ObjectPlan::Dataset(Box::new(DatasetParts {
header_blocks,
datatype,
committed_type,
dataspace,
read_format: if dataspace_version <= 1 {
ObjectFormat::Legacy
} else {
ObjectFormat::Modern
},
layout,
filter_pipeline,
fill_value,
fill_write_time,
attributes,
track_order,
times,
dense: DenseCarry {
attrs: dense_attrs,
links: dense_links,
},
external,
})))
}
_ if crate::io::reader::header_is_committed_datatype(&header) => {
Ok(ObjectPlan::Preserve {
why: "it is a committed (named) datatype, which this writer carries by \
its bytes rather than re-encoding"
.into(),
kind: PreservedKind::NamedDatatype,
})
}
_ if dataset_shaped => Ok(ObjectPlan::preserve(
"it carries a datatype, dataspace or layout message but not the three a \
dataset is built from; this writer models only groups and datasets",
)),
_ => Ok(ObjectPlan::Group(GroupParts {
header_blocks,
attributes,
links,
track_order,
times,
dense: DenseCarry {
attrs: dense_attrs,
links: dense_links,
},
stab,
})),
}
}
fn group(
&mut self,
links: &[(crate::format::messages::link::LinkMessage, Vec<u8>)],
prefix: &str,
depth: usize,
) -> IoResult<()> {
if depth > 256 {
return Ok(());
}
use crate::format::messages::link::LinkTarget;
for (link, encoded) in links {
let full_name = if prefix.is_empty() {
link.name.clone()
} else {
format!("{}/{}", prefix, link.name)
};
let LinkTarget::Hard { address } = &link.target else {
self.out.preserved.push(PreservedEntry {
path: full_name,
class: crate::io::reader::LinkClass::from_target(&link.target),
encoded: encoded.clone(),
reason: None,
kind: PreservedKind::Unclassified,
});
continue;
};
let entry = HardEntry {
path: full_name.clone(),
address: *address,
encoded: encoded.clone(),
};
match self.plan(*address)? {
ObjectPlan::Preserve { why, kind } => self.out.preserved.push(PreservedEntry {
path: full_name,
class: crate::io::reader::LinkClass::Hard,
encoded: entry.encoded,
reason: Some(why),
kind,
}),
ObjectPlan::Dataset(parts) => {
self.out.hard.push((entry, CollectedObject::Dataset(parts)));
}
ObjectPlan::Group(parts) => {
self.out.hard.push((
entry,
CollectedObject::Group {
header_blocks: parts.header_blocks,
attributes: parts.attributes,
track_order: parts.track_order,
times: parts.times,
dense: parts.dense,
stab: parts.stab,
},
));
if self.visited.insert(*address) {
self.group(&parts.links, &full_name, depth + 1)?;
}
}
}
}
Ok(())
}
}
fn rebuild_dataset(
handle: &mut FileHandle,
meta: &FileMeta,
file_size: u64,
name: String,
obj_addr: u64,
parts: DatasetParts,
) -> IoResult<DatasetInfo> {
let ctx = &meta.ctx;
let DatasetParts {
header_blocks,
datatype: dt,
committed_type,
dataspace: ds,
read_format,
layout: dl,
filter_pipeline: fp,
fill_value,
fill_write_time,
attributes: attrs,
track_order,
times,
dense: _,
external,
} = parts;
let mut info = DatasetInfo {
name,
datatype: dt,
committed_type: committed_type.map(CommittedTypeRef::Preserved),
read_format: Some(read_format),
external,
virtual_storage: None,
dataspace: ds,
obj_header_addr: obj_addr,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
chunked: None,
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
append: None,
attributes: attrs,
obj_header_written_addr: Some(obj_addr),
obj_header_blocks: header_blocks,
filter_pipeline: fp,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: 0,
track_attr_order: track_order.attrs,
fill_value,
fill_time: fill_write_time,
layout_version: match &dl {
DataLayoutMessage::ChunkedV4 { version, .. } => *version,
DataLayoutMessage::ChunkedV3 { .. } => LAYOUT_VERSION_DEFAULT,
_ => 4,
},
times,
};
debug_assert!(
layout_rebuilds(&dl),
"ReopenWalk::plan must preserve a layout this has no arm for"
);
match &dl {
DataLayoutMessage::Contiguous { address, size } => {
info.data_addr = *address;
info.data_size = *size;
}
DataLayoutMessage::Compact { data } => {
info.compact = Some(data.clone());
}
DataLayoutMessage::ChunkedV3 {
chunk_dims,
b_tree_address,
} => {
let real_chunk_dims: Vec<u64> = chunk_dims[..chunk_dims.len() - 1].to_vec();
let mut walk = BtreeV1Walk::new(handle, ctx, &meta.btree, &real_chunk_dims, file_size);
walk.descend(*b_tree_address, 0)?;
let BtreeV1Walk {
records,
node_addrs,
..
} = walk;
let max_dims = info
.dataspace
.max_dims
.clone()
.unwrap_or_else(|| info.dataspace.dims.clone());
info.btree_v1 = Some(BtreeV1DatasetInfo {
chunk_dims: real_chunk_dims,
max_dims,
config: meta.btree,
records,
node_addrs,
root_addr: *b_tree_address,
chunks_written: 0,
});
}
DataLayoutMessage::ChunkedV4 {
chunk_dims,
index_address,
index_type,
earray_params,
single_chunk_filter,
..
} => {
let real_chunk_dims: Vec<u64> = chunk_dims[..chunk_dims.len() - 1].to_vec();
if *index_type == crate::format::messages::data_layout::ChunkIndexType::ExtensibleArray
{
if let Some(params) = earray_params {
let ep = EarrayParams {
max_nelmts_bits: params.max_nelmts_bits,
idx_blk_elmts: params.idx_blk_elmts,
sup_blk_min_data_ptrs: params.sup_blk_min_data_ptrs,
data_blk_min_elmts: params.data_blk_min_elmts,
max_dblk_page_nelmts_bits: params.max_dblk_page_nelmts_bits,
};
let ndblk_addrs = compute_ndblk_addrs(ep.sup_blk_min_data_ptrs)?;
let nsblk_addrs = compute_nsblk_addrs(
ep.idx_blk_elmts,
ep.data_blk_min_elmts,
ep.sup_blk_min_data_ptrs,
ep.max_nelmts_bits,
)?;
let hdr_buf = handle.read_at_most(*index_address, 256)?;
let ea_header = ExtensibleArrayHeader::decode(&hdr_buf, ctx)?;
let is_filtered = ea_header.class_id
== crate::format::chunk_index::extensible_array::EA_CLS_FILT_CHUNK;
let chunk_size_len = if is_filtered {
ea_header.raw_elmt_size - ctx.sizeof_addr - 4
} else {
0
};
let ea_iblk_addr = ea_header.idx_blk_addr;
let (ea_iblk, filt_iblk) = if is_filtered {
let placeholder = ExtensibleArrayIndexBlock::new(
*index_address,
ep.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
);
let fib = if ea_iblk_addr != UNDEF_ADDR {
let iblk_buf = handle.read_at_most(ea_iblk_addr, 65536)?;
FilteredIndexBlock::decode(
&iblk_buf,
ctx,
ep.idx_blk_elmts as usize,
ndblk_addrs,
nsblk_addrs,
chunk_size_len,
)?
} else {
FilteredIndexBlock::new(
*index_address,
ep.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
)
};
(placeholder, Some(fib))
} else {
let eib = if ea_iblk_addr != UNDEF_ADDR {
let iblk_buf = handle.read_at_most(ea_iblk_addr, 65536)?;
ExtensibleArrayIndexBlock::decode(
&iblk_buf,
ctx,
ep.idx_blk_elmts as usize,
ndblk_addrs,
nsblk_addrs,
)?
} else {
ExtensibleArrayIndexBlock::new(
*index_address,
ep.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
)
};
(eib, None)
};
info.chunked = Some(ChunkedDatasetInfo {
chunk_dims: real_chunk_dims,
earray_params: ep,
ea_header_addr: *index_address,
ea_iblk_addr,
ea_header,
ea_iblk,
chunks_written: 0,
filt_iblk,
chunk_size_len,
});
}
} else if *index_type
== crate::format::messages::data_layout::ChunkIndexType::FixedArray
{
let hdr_buf = handle.read_at_most(*index_address, 256)?;
let fa_header = FixedArrayHeader::decode(&hdr_buf, ctx)?;
let is_filtered = fa_header.client_id == FA_CLIENT_FILT_CHUNK;
let chunk_size_len = if is_filtered {
(fa_header.element_size as usize)
.checked_sub(ctx.sizeof_addr as usize + 4)
.ok_or_else(|| {
crate::io::IoError::InvalidState(
"fixed array filtered element_size too small".into(),
)
})?
} else {
0
};
if fa_header.data_blk_addr != UNDEF_ADDR && chunk_size_len <= 8 {
let dblk_size = fixed_array_dblk_disk_size(ctx, &fa_header) as usize;
let dblk_buf = handle.read_at_most(fa_header.data_blk_addr, dblk_size)?;
let fa_dblk =
decode_fixed_array_dblk(ctx, &fa_header, &dblk_buf, chunk_size_len)?;
info.fixed_array = Some(FixedArrayDatasetInfo {
chunk_dims: real_chunk_dims,
fa_header_addr: *index_address,
fa_dblk_addr: fa_header.data_blk_addr,
fa_header,
fa_dblk,
chunks_written: 0,
});
}
} else if *index_type == crate::format::messages::data_layout::ChunkIndexType::BTreeV2 {
use crate::format::chunk_index::btree_v2::{
Bt2Geometry, Bt2Header, BT2_TYPE_CHUNK_FILT, BT2_TYPE_CHUNK_UNFILT,
};
let hdr_buf = handle.read_at_most(*index_address, 256)?;
let bt2_hdr = Bt2Header::decode(&hdr_buf, ctx)?;
let ndims = real_chunk_dims.len();
let is_filt = match bt2_hdr.record_type {
BT2_TYPE_CHUNK_UNFILT => Some(false),
BT2_TYPE_CHUNK_FILT => Some(true),
_ => None,
};
if let (Some(is_filt), true) = (
is_filt,
bt2_hdr.node_size as usize >= 10 + 3 * bt2_hdr.record_size as usize,
) {
let mut index = if is_filt {
let csl = (bt2_hdr.record_size as usize)
.checked_sub(ctx.sizeof_addr as usize + 4 + ndims * 8)
.filter(|&c| c <= 8)
.ok_or_else(|| {
crate::io::IoError::InvalidState(
"v2 B-tree filtered record size does not fit \
its rank and address width"
.into(),
)
})?;
Bt2ChunkIndex::new_filtered(ndims, csl as u8)
} else {
Bt2ChunkIndex::new_unfiltered(ndims)
};
index.node_size = bt2_hdr.node_size;
index.split_percent = bt2_hdr.split_percent;
index.merge_percent = bt2_hdr.merge_percent;
let mut node_addrs = Vec::new();
if bt2_hdr.root_node_addr != UNDEF_ADDR && bt2_hdr.total_num_records > 0 {
let geo = Bt2Geometry::new(
bt2_hdr.node_size,
bt2_hdr.record_size,
bt2_hdr.depth,
ctx.sizeof_addr,
);
let mut walk =
Bt2Walk::new(handle, ctx, bt2_hdr.record_size, bt2_hdr.node_size, &geo);
walk.descend(
bt2_hdr.root_node_addr,
bt2_hdr.depth,
bt2_hdr.num_records_in_root,
)?;
node_addrs = walk.node_addrs;
let record_bytes = walk.records;
let total = if bt2_hdr.record_size > 0 {
record_bytes.len() / bt2_hdr.record_size as usize
} else {
0
};
if is_filt {
for r in Bt2ChunkIndex::decode_filtered_records(
&record_bytes,
total,
ndims,
bt2_hdr.record_size,
ctx,
)? {
index.insert_filtered(
r.scaled_offsets,
r.chunk_address,
r.chunk_size,
r.filter_mask,
);
}
} else {
for r in Bt2ChunkIndex::decode_unfiltered_records(
&record_bytes,
total,
ndims,
ctx,
)? {
index.insert(r.scaled_offsets, r.chunk_address);
}
}
}
info.btree_v2 = Some(Bt2DatasetInfo {
chunk_dims: real_chunk_dims,
bt2_header_addr: *index_address,
node_addrs,
index,
chunks_written: 0,
});
}
} else if *index_type == crate::format::messages::data_layout::ChunkIndexType::Implicit
{
let mut nchunks: u64 = 1;
for g in crate::io::chunk_grid::index_grid(
&info.dataspace.dims,
info.dataspace.max_dims.as_deref(),
&real_chunk_dims,
)? {
nchunks = nchunks.checked_mul(g).ok_or_else(|| {
crate::io::IoError::InvalidState("chunk count overflows u64".into())
})?;
}
let data_size = nchunks
.checked_mul(chunk_dims.iter().product::<u64>())
.ok_or_else(|| {
crate::io::IoError::InvalidState(
"implicit chunk storage overflows u64".into(),
)
})?;
info.implicit = Some(ImplicitDatasetInfo {
chunk_dims: real_chunk_dims,
data_addr: *index_address,
data_size,
});
} else if *index_type
== crate::format::messages::data_layout::ChunkIndexType::SingleChunk
{
let data_size = chunk_dims.iter().product::<u64>();
let (nbytes, filter_mask) = match single_chunk_filter {
Some(scf) => (scf.nbytes, scf.filter_mask),
None => (data_size, 0),
};
info.single_chunk = Some(SingleChunkDatasetInfo {
chunk_dims: real_chunk_dims,
data_addr: *index_address,
data_size,
nbytes,
filter_mask,
chunks_written: 0,
early_alloc: false,
});
}
}
_ => {}
}
Ok(info)
}
fn write_external_file_bytes(
files: &[ExternalFile],
extfile_prefix: Option<&Path>,
mut skip: u64,
data: &[u8],
) -> IoResult<()> {
let mut slot_idx = 0usize;
while slot_idx < files.len() && skip >= files[slot_idx].size {
skip -= files[slot_idx].size;
slot_idx += 1;
}
let mut written = 0usize;
while written < data.len() {
let Some(slot) = files.get(slot_idx) else {
return Err(crate::io::IoError::InvalidState(
"write past the logical end of the external file list".into(),
));
};
let full_path = crate::io::reader::combine_prefixed_path(extfile_prefix, &slot.name);
let ext_handle = FileHandle::open_or_create_readwrite_with_locking(
&full_path,
crate::io::locking::FileLocking::Disabled,
)
.map_err(|e| {
crate::io::IoError::InvalidState(format!(
"unable to open external raw data file {} for writing: {e}",
full_path.display()
))
})?;
let this_write = (slot.size - skip).min((data.len() - written) as u64) as usize;
let at = slot.offset.checked_add(skip).ok_or_else(|| {
crate::io::IoError::InvalidState(format!(
"external file '{}' slot offset {} overflows {skip} bytes into the slot",
slot.name, slot.offset
))
})?;
ext_handle.write_at(at, &data[written..written + this_write])?;
ext_handle.flush()?;
written += this_write;
skip = 0;
slot_idx += 1;
}
Ok(())
}
fn source_dir_of(path: &Path) -> IoResult<PathBuf> {
let canonical = std::fs::canonicalize(path)?;
Ok(canonical
.parent()
.map(Path::to_path_buf)
.unwrap_or_default())
}
fn layout_rebuilds(layout: &DataLayoutMessage) -> bool {
matches!(
layout,
DataLayoutMessage::Contiguous { .. }
| DataLayoutMessage::Compact { .. }
| DataLayoutMessage::ChunkedV3 { .. }
| DataLayoutMessage::ChunkedV4 { .. }
)
}
fn encode_refcount(refcount: u32) -> Vec<u8> {
let mut v = Vec::with_capacity(5);
v.push(0u8);
v.extend_from_slice(&refcount.to_le_bytes());
v
}
struct SymbolTables {
found: HashSet<LinkScope>,
superseded: Slot<HashMap<LinkScope, StabExtents>>,
written: Slot<HashMap<LinkScope, Stab>>,
}
impl SymbolTables {
fn none_found() -> Self {
Self {
found: HashSet::new(),
superseded: Slot::new(HashMap::new()),
written: Slot::new(HashMap::new()),
}
}
}
struct LegacyFile {
superblock: SuperblockV0V1,
}
impl LegacyFile {
fn created(ctx: FormatContext, base_address: u64) -> Self {
let btree = BTreeV1Config::default();
Self {
superblock: SuperblockV0V1 {
version: SUPERBLOCK_V0,
sizeof_offsets: ctx.sizeof_addr,
sizeof_lengths: ctx.sizeof_size,
file_consistency_flags: 0,
sym_leaf_k: btree.sym_leaf_k,
btree_internal_k: btree.snode_internal_k,
indexed_storage_k: None,
base_address,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 0,
driver_info_address: UNDEF_ADDR,
root_symbol_table_entry: SymbolTableEntry {
name_offset: 0,
obj_header_addr: UNDEF_ADDR,
cache: SymbolTableCache::Nothing,
},
},
}
}
}
struct CarriedExtension {
superseded: crate::io::object_header_io::HeaderBlocks,
carried: Vec<crate::io::object_header_io::ExtensionMessage>,
addr: Slot<Option<u64>>,
}
struct ReopenedFreeSpace {
state: Option<Box<FileSpaceState>>,
sections: Vec<FreeBlock>,
}
struct FileSpaceState {
info: FileSpaceInfoMessage,
superseded: Vec<(u64, u64)>,
}
impl FileSpaceState {
fn records_free_space(&self) -> bool {
self.info.persist
&& matches!(
self.info.strategy,
FileSpaceStrategy::FsmAggr | FileSpaceStrategy::Page
)
}
}
struct PlacedManager {
manager: FreeSpaceManager,
hdr_addr: u64,
sect_addr: u64,
sect_size: u64,
sections: Vec<FreeSection>,
}
fn manager_header(sections: &[FreeSection]) -> FreeSpaceHeader {
FreeSpaceHeader {
client: free_space::CLIENT_FILE,
total_space: sections.iter().map(|s| s.len).sum(),
total_sections: sections.len() as u64,
serial_sections: sections.len() as u64,
ghost_sections: 0,
nclasses: free_space::FILE_SECT_CLASSES,
shrink_percent: free_space::SHRINK_PERCENT,
expand_percent: free_space::EXPAND_PERCENT,
max_sect_addr: free_space::SEC2_MAX_SECT_ADDR,
max_sect_size: free_space::SEC2_MAXADDR,
sect_addr: UNDEF_ADDR,
sect_size: 0,
alloc_sect_size: 0,
}
}
impl Default for CarriedExtension {
fn default() -> Self {
Self {
superseded: Vec::new(),
carried: Vec::new(),
addr: Slot::new(None),
}
}
}
#[derive(Debug, Clone, Copy)]
enum SuperblockVersion {
Chosen(u8),
Existing(u8),
}
impl SuperblockVersion {
fn libver_floor(self) -> LibverBound {
match self {
Self::Chosen(_) => LibverBound::Earliest,
Self::Existing(0..=1) => LibverBound::Earliest,
Self::Existing(2) => LibverBound::V18,
Self::Existing(_) => LibverBound::V110,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum NameHit {
Dataset(usize),
Group(usize),
Datatype(usize),
HardLink,
SymbolicLink,
PreservedLink,
}
struct NameIndex {
map: Option<HashMap<String, Vec<NameHit>>>,
epoch: u64,
}
impl NameIndex {
fn new() -> Self {
NameIndex {
map: None,
epoch: 0,
}
}
fn insert(&mut self, name: &str, hit: NameHit) {
match self.map.as_mut() {
None => self.epoch += 1,
Some(map) => {
let hits = map.entry(name.to_string()).or_default();
if !hits.contains(&hit) {
hits.push(hit);
}
}
}
}
fn forget(&mut self) {
self.map = None;
self.epoch += 1;
}
}
pub struct Hdf5Writer {
handle: FileHandle,
allocator: FileAllocator,
ctx: FormatContext,
pub(crate) datasets: Slot<Vec<DatasetRef>>,
pub(crate) groups: Slot<Vec<GroupRef>>,
pub(crate) hard_links: Slot<Vec<HardLink>>,
pub(crate) symbolic_links: Slot<Vec<SymbolicLink>>,
pub(crate) committed_datatypes: Slot<Vec<CommittedDatatype>>,
pub(crate) preserved_links: Slot<Vec<PreservedLink>>,
name_index: Slot<Box<NameIndex>>,
pub(crate) root_attributes: Slot<Vec<crate::format::messages::attribute::AttributeEntry>>,
pub(crate) create_lock: Slot<()>,
libver: Option<LibverBound>,
closed: bool,
swmr_active: bool,
cwfs: Slot<Vec<CwfsEntry>>,
root_group_addr: Option<u64>,
superseded_root_header: crate::io::object_header_io::HeaderBlocks,
superblock_version: SuperblockVersion,
dense_attributes: Slot<HashMap<AttrScope, AttributeInfoMessage>>,
dense_links: Slot<HashMap<LinkScope, LinkInfoMessage>>,
superseded_dense: Slot<Option<Box<SupersededDense>>>,
track_order: TrackOrder,
track_times: bool,
root_track_order: TrackOrder,
root_times: Option<ObjectTimes>,
next_creation_seq: Slot<u64>,
pending_object_references: Slot<Vec<PendingObjectReference>>,
pending_heap_references: Slot<Vec<PendingHeapReference>>,
attribute_references: Slot<Vec<AttributeReferenceValue>>,
legacy: Option<Box<LegacyFile>>,
symbol_tables: SymbolTables,
btree: BTreeV1Config,
extension: Box<CarriedExtension>,
free_space: Option<Box<FileSpaceState>>,
sohm: Option<Box<SohmState>>,
source_dir: PathBuf,
}
struct SohmState {
indexes: Vec<SohmIndexSpec>,
phase: Slot<SohmPhase>,
table_addr: Slot<Option<u64>>,
superseded: Slot<Vec<(u64, u64)>>,
}
enum SohmPhase {
Idle,
Predict(FirstCopies),
Collect(SohmCollector),
Resolve {
ids: HashMap<(u8, Vec<u8>), [u8; SOHM_HEAP_ID_LEN]>,
first: FirstCopies,
},
}
type FirstCopies = std::collections::HashSet<(u8, Vec<u8>)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ShareOwner {
Detached,
Header(u64),
}
impl SohmState {
fn new(indexes: Vec<SohmIndexSpec>, superseded: Vec<(u64, u64)>) -> Self {
Self {
indexes,
phase: Slot::new(SohmPhase::Idle),
table_addr: Slot::new(None),
superseded: Slot::new(superseded),
}
}
fn index_for(&self, msg_type: u8, body_len: usize) -> Option<usize> {
let flag = type_flag(msg_type)?;
let (at, spec) = self
.indexes
.iter()
.enumerate()
.find(|(_, spec)| spec.mesg_types & flag != 0)?;
(body_len as u64 >= u64::from(spec.min_mesg_size)).then_some(at)
}
fn shares_attributes(&self) -> bool {
let Some(flag) = type_flag(MSG_ATTRIBUTE) else {
return false;
};
self.indexes.iter().any(|spec| spec.mesg_types & flag != 0)
}
}
type CollectedKey = (u8, Vec<u8>, Vec<NestedShare>);
struct SohmCollector {
messages: Vec<Vec<SharedMessage>>,
seen: HashMap<CollectedKey, (usize, usize)>,
}
impl SohmCollector {
fn new(nindexes: usize) -> Self {
Self {
messages: vec![Vec::new(); nindexes],
seen: HashMap::new(),
}
}
fn record(
&mut self,
index: usize,
msg_type: u8,
body: &[u8],
nested: &[NestedShare],
ohdr: Option<u64>,
) -> bool {
let key = (msg_type, body.to_vec(), nested.to_vec());
match self.seen.get(&key) {
Some(&(at, pos)) => {
self.messages[at][pos].ref_count += 1;
false
}
None => {
let pos = self.messages[index].len();
self.messages[index].push(SharedMessage {
msg_type,
body: body.to_vec(),
nested: nested.to_vec(),
ref_count: 1,
ohdr_addr: ohdr,
});
self.seen.insert(key, (index, pos));
true
}
}
}
fn release(&mut self, msg_type: u8, body: &[u8]) {
if let Some(&(at, pos)) = self.seen.get(&(msg_type, body.to_vec(), Vec::new())) {
let count = &mut self.messages[at][pos].ref_count;
*count = count.saturating_sub(1);
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FileCreateOptions {
pub locking: crate::io::locking::FileLocking,
pub track_order: bool,
pub track_times: bool,
pub libver: Option<LibverBound>,
pub userblock: u64,
pub shared_messages: SharedMessageConfig,
pub file_space: FileSpaceConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileSpaceConfig {
pub strategy: FileSpaceStrategy,
pub persist: bool,
pub threshold: u64,
pub page_size: u64,
}
impl Default for FileSpaceConfig {
fn default() -> Self {
Self {
strategy: FileSpaceStrategy::FsmAggr,
persist: false,
threshold: 1,
page_size: DEFAULT_FILE_SPACE_PAGE_SIZE,
}
}
}
impl FileSpaceConfig {
pub fn new(strategy: FileSpaceStrategy, persist: bool, threshold: u64) -> Self {
let uses_managers = matches!(
strategy,
FileSpaceStrategy::FsmAggr | FileSpaceStrategy::Page
);
Self {
strategy,
persist: uses_managers && persist,
threshold: if uses_managers {
threshold
} else {
Self::default().threshold
},
..Self::default()
}
}
pub fn with_page_size(mut self, page_size: u64) -> Self {
self.page_size = page_size;
self
}
pub fn is_default(&self) -> bool {
*self == Self::default()
}
fn validate(&self) -> IoResult<()> {
if !(PAGE_SIZE_MIN..=PAGE_SIZE_MAX).contains(&self.page_size) {
return Err(crate::io::IoError::InvalidState(format!(
"a file-space page size is between {PAGE_SIZE_MIN} bytes and \
{PAGE_SIZE_MAX}, not {}",
self.page_size
)));
}
match self.strategy {
FileSpaceStrategy::FsmAggr
| FileSpaceStrategy::Aggr
| FileSpaceStrategy::None
| FileSpaceStrategy::Page => Ok(()),
FileSpaceStrategy::Unknown(b) => Err(crate::io::IoError::InvalidState(format!(
"invalid file-space strategy {b}"
))),
}
}
fn message(&self) -> FileSpaceInfoMessage {
FileSpaceInfoMessage {
version: 1,
strategy: self.strategy,
persist: self.persist,
threshold: self.threshold,
page_size: self.page_size,
pgend_meta_thres: 0,
eoa_pre_fsm_fsalloc: UNDEF_ADDR,
fs_addr: vec![UNDEF_ADDR; FS_ADDR_COUNT_V1],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SharedMessageConfig {
indexes: [SohmIndexSpec; MAX_SOHM_INDEXES],
count: usize,
}
impl Default for SharedMessageConfig {
fn default() -> Self {
Self {
indexes: [SohmIndexSpec {
mesg_types: 0,
min_mesg_size: 0,
list_max: DEFAULT_SOHM_LIST_MAX,
btree_min: DEFAULT_SOHM_BTREE_MIN,
}; MAX_SOHM_INDEXES],
count: 0,
}
}
}
impl SharedMessageConfig {
pub fn new(indexes: &[(u16, u32)], list_max: u16, btree_min: u16) -> Self {
let mut config = Self {
count: indexes.len(),
..Self::default()
};
for (slot, &(mesg_types, min_mesg_size)) in config.indexes.iter_mut().zip(indexes) {
*slot = SohmIndexSpec {
mesg_types,
min_mesg_size,
list_max,
btree_min,
};
}
config
}
pub(crate) fn specs(&self) -> &[SohmIndexSpec] {
&self.indexes[..self.count.min(MAX_SOHM_INDEXES)]
}
fn validate(&self) -> IoResult<()> {
if self.count > MAX_SOHM_INDEXES {
return Err(crate::io::IoError::InvalidState(format!(
"a file may declare at most {MAX_SOHM_INDEXES} shared-message \
indexes, not {}",
self.count
)));
}
for spec in self.specs() {
if u32::from(spec.btree_min) > u32::from(spec.list_max) + 1 {
return Err(crate::io::IoError::InvalidState(format!(
"shared-message phase change needs btree_min ({}) at most one \
past list_max ({}), or an index converts on every insert",
spec.btree_min, spec.list_max
)));
}
if spec.mesg_types == 0 {
return Err(crate::io::IoError::InvalidState(
"a shared-message index covering no message type would never \
be used; give it a type mask or drop it"
.into(),
));
}
}
Ok(())
}
}
pub(crate) struct PendingObjectReference {
dataset: usize,
element: u64,
target: String,
}
pub(crate) struct PendingHeapReference {
collection: u64,
index: u16,
token_offset: usize,
target: PendingHeapTarget,
}
#[derive(Debug, Clone)]
pub(crate) enum PendingHeapTarget {
Dataset(String),
Object(String),
}
pub(crate) struct AttributeReferenceValue {
scope: AttrScope,
name: String,
targets: Vec<String>,
stride: usize,
}
pub(crate) const DIMENSION_LIST: &str = "DIMENSION_LIST";
pub(crate) const REFERENCE_LIST: &str = "REFERENCE_LIST";
const DIMENSION_SCALE_CLASS: &str = "DIMENSION_SCALE";
enum ClassAttr {
Fixed {
size: u32,
null_terminated: bool,
text: String,
},
VarLen(String),
NotString,
}
fn c_string(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
fn check_header_size(
encoded: &[u8],
reserved: usize,
what: impl FnOnce() -> String,
) -> IoResult<()> {
if encoded.len() == reserved {
return Ok(());
}
Err(crate::io::IoError::InvalidState(format!(
"the object header of {} encodes to {} bytes but was measured at {}; \
a message in it changed length once the addresses it names were known",
what(),
encoded.len(),
reserved
)))
}
#[derive(Debug, Clone, Copy)]
struct HeaderPlacement {
addr: u64,
size: usize,
kept: bool,
continuation: Option<(u64, usize)>,
}
impl HeaderPlacement {
fn fresh(addr: u64, size: usize) -> Self {
Self {
addr,
size,
kept: false,
continuation: None,
}
}
fn blocks(&self) -> crate::io::object_header_io::HeaderBlocks {
std::iter::once((self.addr, self.size as u64))
.chain(self.continuation.map(|(a, s)| (a, s as u64)))
.collect()
}
fn over(blocks: &[(u64, u64)]) -> Option<Self> {
match blocks {
[(addr, size)] => Some(Self {
addr: *addr,
size: *size as usize,
kept: true,
continuation: None,
}),
[(addr, size), (cont, cont_size)] => Some(Self {
addr: *addr,
size: *size as usize,
kept: true,
continuation: Some((*cont, *cont_size as usize)),
}),
_ => None,
}
}
}
struct HeaderLayout {
datasets: Vec<(usize, HeaderPlacement)>,
groups: Vec<(usize, HeaderPlacement)>,
root: HeaderPlacement,
}
#[derive(Default)]
struct KeptChunks {
datasets: std::collections::HashMap<usize, (u64, u64)>,
groups: std::collections::HashMap<usize, (u64, u64)>,
root: Option<(u64, u64)>,
}
fn validate_region_selection(selection: &Selection, dims: &[u64], path: &str) -> IoResult<()> {
let boxes = selection.to_boxes(dims).map_err(|e| {
crate::io::IoError::InvalidState(format!("region reference over '{path}': {e}"))
})?;
for (start, count) in boxes {
for (d, (&s, &c)) in start.iter().zip(&count).enumerate() {
if s.checked_add(c).is_none_or(|end| end > dims[d]) {
return Err(crate::io::IoError::InvalidState(format!(
"region reference over '{path}' selects {s}..{} in dimension {d}, \
outside the dataset's extent of {}",
s.saturating_add(c),
dims[d]
)));
}
}
}
Ok(())
}
#[derive(Debug, Default)]
struct SupersededDense {
attrs: HashMap<AttrScope, AttributeInfoMessage>,
links: HashMap<LinkScope, LinkInfoMessage>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum AttrScope {
Root,
Group(usize),
Dataset(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum LinkScope {
Root,
Group(usize),
}
const MAX_COMPACT_ATTRS: usize = 8;
const EST_LINK_COUNT: usize = 4;
const EST_LINK_NAME_LEN: usize = 8;
const DEFAULT_SOHM_LIST_MAX: u16 = 50;
const DEFAULT_SOHM_BTREE_MIN: u16 = 40;
const MAX_COMPACT_LINKS: usize = 8;
pub const MAX_COMPACT_DATA: usize = MAX_MESSAGE_SIZE - 4;
pub const MIN_USERBLOCK: u64 = 512;
impl Hdf5Writer {
pub fn create(path: &Path) -> IoResult<Self> {
Self::create_with_locking(
path,
crate::io::locking::FileLocking::from_env_or(Default::default()),
)
}
pub fn create_with_locking(
path: &Path,
locking: crate::io::locking::FileLocking,
) -> IoResult<Self> {
Self::create_with_options(
path,
FileCreateOptions {
locking,
..Default::default()
},
)
}
pub fn create_with_options(path: &Path, options: FileCreateOptions) -> IoResult<Self> {
let FileCreateOptions {
locking,
track_order,
track_times,
libver,
userblock,
shared_messages,
file_space,
} = options;
shared_messages.validate()?;
file_space.validate()?;
if userblock != 0 && (userblock < MIN_USERBLOCK || !userblock.is_power_of_two()) {
return Err(crate::io::IoError::InvalidState(format!(
"a userblock is {MIN_USERBLOCK} bytes or a power of two above it, \
not {userblock}: a reader locates the superblock by doubling its \
search offset from {MIN_USERBLOCK}, so no other size can hold one"
)));
}
let policy = free_space::SpacePolicy::for_message(&file_space.message());
if let Some(page) = policy.page() {
if userblock != 0 && userblock % page != 0 {
return Err(crate::io::IoError::InvalidState(format!(
"a paged file's userblock is a multiple of its {page}-byte \
file-space page, not {userblock}"
)));
}
}
let mut handle = FileHandle::create_with_locking(path, locking)?;
if userblock != 0 {
handle.write_at(0, &vec![0u8; userblock as usize])?;
handle.set_base(userblock);
}
let ctx = FormatContext::default_v3();
let classic = libver == Some(LibverBound::Earliest);
let legacy = classic.then(|| Box::new(LegacyFile::created(ctx, userblock)));
let superblock_version = SuperblockVersion::Chosen(
if classic && shared_messages.specs().is_empty() && file_space.is_default() {
SUPERBLOCK_V0
} else {
SUPERBLOCK_V2
},
);
let superblock_size = match legacy.as_deref() {
Some(l) if matches!(superblock_version, SuperblockVersion::Chosen(v) if v < SUPERBLOCK_V2) => {
l.superblock.encoded_size()
}
_ => SuperblockV2V3::size_for(ctx.sizeof_addr),
};
let allocator = FileAllocator::with_policy(0, policy);
allocator.allocate(superblock_size as u64, FreeSpaceClass::Metadata);
Ok(Self {
handle,
allocator,
ctx,
datasets: Slot::new(Vec::new()),
groups: Slot::new(Vec::new()),
hard_links: Slot::new(Vec::new()),
symbolic_links: Slot::new(Vec::new()),
committed_datatypes: Slot::new(Vec::new()),
preserved_links: Slot::new(Vec::new()),
name_index: Slot::new(Box::new(NameIndex::new())),
root_attributes: Slot::new(Vec::new()),
create_lock: Slot::new(()),
libver,
closed: false,
swmr_active: false,
cwfs: Slot::new(Vec::new()),
root_group_addr: None,
superseded_root_header: Vec::new(),
superblock_version,
dense_attributes: Slot::new(HashMap::new()),
dense_links: Slot::new(HashMap::new()),
superseded_dense: Slot::new(None),
track_order: TrackOrder::uniform(track_order),
track_times,
root_track_order: TrackOrder::uniform(track_order),
root_times: track_times.then(|| ObjectTimes::created_at(now_seconds())),
next_creation_seq: Slot::new(0),
pending_object_references: Slot::new(Vec::new()),
pending_heap_references: Slot::new(Vec::new()),
attribute_references: Slot::new(Vec::new()),
legacy,
symbol_tables: SymbolTables::none_found(),
btree: BTreeV1Config::default(),
extension: Box::default(),
free_space: (!file_space.is_default()).then(|| {
Box::new(FileSpaceState {
info: file_space.message(),
superseded: Vec::new(),
})
}),
sohm: (!shared_messages.specs().is_empty())
.then(|| Box::new(SohmState::new(shared_messages.specs().to_vec(), Vec::new()))),
source_dir: source_dir_of(path)?,
})
}
pub fn set_libver_latest(&mut self, latest: bool) -> IoResult<()> {
self.set_libver_bound(if latest {
LibverBound::V200
} else {
LibverBound::Earliest
})
}
pub fn userblock_size(&self) -> u64 {
self.handle.base()
}
pub fn set_libver_bound(&mut self, libver: LibverBound) -> IoResult<()> {
if libver != LibverBound::Earliest && self.is_legacy() {
return Err(crate::io::IoError::Unsupported(format!(
"cannot set the library-version bound to {libver:?} on this file: it is in the classic (version-0/1 superblock) format, which libhdf5 writes only at H5F_LIBVER_EARLIEST"
)));
}
self.libver = Some(libver);
Ok(())
}
fn message_format(&self) -> ObjectFormat {
match self.legacy {
Some(_) => ObjectFormat::Legacy,
None => ObjectFormat::Modern,
}
}
fn header_format(&self, track: TrackOrder) -> ObjectFormat {
let attrs = self.header_attr_order(track.attrs);
if self.legacy.is_some() && !track.links.is_tracked() && !attrs.is_tracked() {
ObjectFormat::Legacy
} else {
ObjectFormat::Modern
}
}
fn header_attr_order(&self, requested: CreationOrder) -> CreationOrder {
if requested.is_tracked() || !self.tracks_message_creation_index() {
return requested;
}
CreationOrder::Tracked
}
fn rebuilds_shared_messages(&self) -> bool {
self.sohm
.as_deref()
.is_some_and(|s| s.table_addr.lock().is_none())
}
fn tracks_message_creation_index(&self) -> bool {
self.sohm
.as_deref()
.is_some_and(SohmState::shares_attributes)
}
fn uses_symbol_table(&self, scope: LinkScope, links: CreationOrder) -> bool {
(self.legacy.is_some() || self.symbol_tables.found.contains(&scope))
&& !links.is_tracked()
&& self.links_fit_symbol_table(scope, links)
}
fn links_fit_symbol_table(&self, scope: LinkScope, order: CreationOrder) -> bool {
self.group_links(scope, order)
.iter()
.all(LinkMessage::fits_symbol_table)
&& self.preserved_links_for(scope).iter().all(|encoded| {
LinkMessage::decode(encoded, &self.ctx)
.is_ok_and(|(link, _)| link.fits_symbol_table())
})
}
fn dataset_header_format(&self, index: usize) -> ObjectFormat {
let ds = self.ds(index);
let attrs = ds.lock().track_attr_order;
self.header_format(TrackOrder {
links: CreationOrder::default(),
attrs,
})
}
fn group_header_format(&self, index: usize) -> ObjectFormat {
let grp = self.grp(index);
let track = grp.lock().track_order;
self.header_format(track)
}
fn libver_floor(&self) -> LibverBound {
self.superblock_version.libver_floor()
}
fn session_libver(&self, create_default: LibverBound) -> LibverBound {
let floor = self.libver_floor();
let bound = match (self.libver, self.superblock_version) {
(Some(named), _) => named.max(floor),
(None, SuperblockVersion::Existing(_)) => floor,
(None, SuperblockVersion::Chosen(_)) => create_default,
};
match self.message_format() {
ObjectFormat::Legacy => bound.min(LibverBound::Earliest),
ObjectFormat::Modern => bound,
}
}
fn encoding_libver(&self) -> LibverBound {
self.session_libver(LibverBound::Earliest)
}
fn layout_version_bound(&self) -> u8 {
self.session_libver(LibverBound::V110).layout_version()
}
fn required_chunk_layout_version(chunk_bytes: u64) -> u8 {
if chunk_bytes > u32::MAX as u64 {
5
} else {
LAYOUT_VERSION_DEFAULT
}
}
pub(crate) fn uses_v110_chunk_indexing(&self, chunk_bytes: u64) -> bool {
self.layout_version_bound() >= 4 || Self::required_chunk_layout_version(chunk_bytes) >= 4
}
fn reject_swmr(&self) -> IoResult<()> {
let why = match self.superblock_version {
SuperblockVersion::Existing(version) if version >= SUPERBLOCK_V3 => return Ok(()),
SuperblockVersion::Existing(version) => format!(
"its superblock is version {version}, and reopening a file never \
rewrites that"
),
SuperblockVersion::Chosen(_) if self.is_legacy() => {
"it is in the classic (version-0/1 superblock) format that \
H5F_LIBVER_EARLIEST selects"
.to_string()
}
SuperblockVersion::Chosen(_) if self.libver.is_some_and(|b| b < LibverBound::V110) => {
"it was asked for at a library-version bound below H5F_LIBVER_V110, \
whose superblock row is version 2"
.to_string()
}
SuperblockVersion::Chosen(_) => return Ok(()),
};
Err(crate::io::IoError::Unsupported(format!(
"cannot start an SWMR session on this file: {why}, and SWMR needs a \
version-3 superblock to record that a writer is attached; create the \
file at H5F_LIBVER_V110 or newer"
)))
}
pub(crate) fn is_legacy(&self) -> bool {
self.legacy.is_some()
}
fn btree_v1_config(&self) -> BTreeV1Config {
self.btree
}
pub fn set_track_order(&mut self, track: bool) {
self.track_order = TrackOrder::uniform(track);
}
pub fn set_track_times(&mut self, track: bool) {
self.track_times = track;
}
fn created_object_times(&self) -> Option<ObjectTimes> {
self.track_times
.then(|| ObjectTimes::created_at(now_seconds()))
}
fn chunk_layout_version(&self, filtered: bool, chunk_bytes: u64) -> u8 {
let preferred = if filtered { 5 } else { 4 };
Self::required_chunk_layout_version(chunk_bytes)
.max(self.layout_version_bound().min(preferred))
.max(LAYOUT_VERSION_DEFAULT)
}
fn chunk_size_len_for(&self, layout_version: u8, chunk_bytes: u64) -> u8 {
if layout_version >= 5 {
self.ctx.sizeof_size
} else {
compute_chunk_size_len(chunk_bytes)
}
}
pub fn ctx(&self) -> &FormatContext {
&self.ctx
}
pub(crate) fn dataset_count(&self) -> usize {
self.datasets.lock().len()
}
pub(crate) fn ds(&self, index: usize) -> DatasetRef {
Shared::clone(&self.datasets.lock()[index])
}
pub(crate) fn group_count(&self) -> usize {
self.groups.lock().len()
}
pub(crate) fn grp(&self, index: usize) -> GroupRef {
Shared::clone(&self.groups.lock()[index])
}
pub(crate) fn begin_create(&self, name: &str) -> IoResult<CreateGuard<'_>> {
let gate = self.create_lock.lock();
let name = self.canonical_dataset_path(name);
self.reject_external_traversal(&name)?;
self.ensure_name_free(&name)?;
self.reject_preserved_object(&name)?;
let (parent, _leaf) = self.split_parent(&name)?;
Ok(CreateGuard {
_gate: gate,
name,
parent,
})
}
fn split_parent(&self, path: &str) -> IoResult<(Option<usize>, String)> {
let (parent_path, leaf) = path.rsplit_once('/').unwrap_or(("", path));
if leaf.is_empty() {
return Err(crate::io::IoError::InvalidState(format!(
"'{path}' does not end in a link name"
)));
}
if parent_path.is_empty() {
return Ok((None, leaf.to_string()));
}
let abs = format!("/{parent_path}");
let groups = self.group_refs();
let idx = groups
.iter()
.position(|g| {
let gg = g.lock();
gg.name == abs && !gg.deleted
})
.ok_or_else(|| {
crate::io::IoError::NotFound(format!(
"cannot create '{path}': group '{abs}' does not exist"
))
})?;
Ok((Some(idx), leaf.to_string()))
}
pub(crate) fn push_dataset(&self, create: &CreateGuard<'_>, info: DatasetInfo) -> usize {
let name = info.name.clone();
let idx = {
let mut reg = self.datasets.lock();
let idx = reg.len();
reg.push(Shared::new(DatasetCell::new(info)));
idx
};
self.register_name(&name, NameHit::Dataset(idx));
if let Some(pidx) = create.parent {
self.grp(pidx).lock().child_datasets.push(idx);
}
idx
}
pub(crate) fn push_group(&self, info: GroupInfo) -> usize {
let name = info.name.trim_start_matches('/').to_string();
let idx = {
let mut reg = self.groups.lock();
let idx = reg.len();
reg.push(Shared::new(Slot::new(info)));
idx
};
self.register_name(&name, NameHit::Group(idx));
idx
}
pub(crate) fn dataset_refs(&self) -> Vec<DatasetRef> {
self.datasets.lock().iter().map(Shared::clone).collect()
}
pub(crate) fn group_refs(&self) -> Vec<GroupRef> {
self.groups.lock().iter().map(Shared::clone).collect()
}
fn take_creation_seq(&self) -> u64 {
let mut next = self.next_creation_seq.lock();
let seq = *next;
*next += 1;
seq
}
pub(crate) fn hard_links_vec(&self) -> Vec<HardLink> {
self.hard_links.lock().clone()
}
pub(crate) fn symbolic_links_vec(&self) -> Vec<SymbolicLink> {
self.symbolic_links.lock().clone()
}
pub fn open_append(path: &Path) -> IoResult<Self> {
Self::open_append_with_locking(
path,
crate::io::locking::FileLocking::from_env_or(Default::default()),
)
}
fn reopen_free_space(
handle: &mut FileHandle,
meta: &crate::io::FileMeta,
ext: &crate::io::reader::SuperblockExtension,
) -> IoResult<ReopenedFreeSpace> {
let none = || ReopenedFreeSpace {
state: None,
sections: Vec::new(),
};
let Some(info) = ext.file_space_info.as_ref().filter(|i| i.persist) else {
return Ok(none());
};
if !matches!(
info.strategy,
FileSpaceStrategy::FsmAggr | FileSpaceStrategy::Page
) {
return Ok(none());
}
let found = crate::io::free_space_io::read_managers(handle, &meta.ctx, info)?;
Ok(ReopenedFreeSpace {
state: Some(Box::new(FileSpaceState {
info: info.clone(),
superseded: found.blocks,
})),
sections: found.sections,
})
}
fn reopen_shared_messages(
handle: &mut FileHandle,
meta: &crate::io::FileMeta,
ext: &crate::io::reader::SuperblockExtension,
) -> IoResult<Option<Box<SohmState>>> {
use crate::format::chunk_index::btree_v2::collect_btree_v2_extents;
use crate::format::fractal_heap::collect_heap_extents;
use crate::format::sohm::{list_size, SohmMasterTable, SOHM_INDEX_LIST};
let (Some(table), Some(smt)) = (
meta.sohm.as_ref().filter(|t| !t.indexes.is_empty()),
ext.shared_message_table.as_ref(),
) else {
return Ok(None);
};
let ctx = &meta.ctx;
let mut superseded = Vec::new();
superseded.push((
smt.table_address,
SohmMasterTable::encoded_size(ctx, smt.nindexes) as u64,
));
let mut specs = Vec::with_capacity(table.indexes.len());
for index in &table.indexes {
specs.push(SohmIndexSpec {
mesg_types: index.mesg_types,
min_mesg_size: index.min_mesg_size,
list_max: index.list_max,
btree_min: index.btree_min,
});
let mut reader = crate::io::reader::HandleBlockReader { handle };
if index.heap_addr != UNDEF_ADDR {
superseded.extend(collect_heap_extents(index.heap_addr, ctx, &mut reader)?);
}
if index.index_addr != UNDEF_ADDR {
if index.index_type == SOHM_INDEX_LIST {
superseded.push((index.index_addr, list_size(ctx, index.list_max) as u64));
} else {
superseded.extend(collect_btree_v2_extents(
index.index_addr,
ctx,
&mut reader,
)?);
}
}
}
Ok(Some(Box::new(SohmState::new(specs, superseded))))
}
pub fn open_append_with_locking(
path: &Path,
locking: crate::io::locking::FileLocking,
) -> IoResult<Self> {
let mut handle = FileHandle::open_readwrite_with_locking(path, locking)?;
let super_addr = handle
.locate_signature()?
.ok_or(crate::format::FormatError::InvalidSignature)?;
handle.set_base(super_addr);
let file_size = handle.file_size()?;
let sb_buf = handle.read_at_most(0, 256)?;
let version = crate::format::superblock::detect_superblock_version(&sb_buf)?;
let (ctx, sb_btree, root_addr, ext_addr, legacy) = if version <= 1 {
let sb = SuperblockV0V1::decode(&sb_buf)?;
let ctx = FormatContext {
sizeof_addr: sb.sizeof_offsets,
sizeof_size: sb.sizeof_lengths,
};
let btree = crate::format::btree_v1::BTreeV1Config {
sym_leaf_k: sb.sym_leaf_k,
snode_internal_k: sb.btree_internal_k,
chunk_internal_k: sb.indexed_storage_k.unwrap_or(32),
};
let root = sb.root_symbol_table_entry.obj_header_addr;
let ext = sb.superblock_extension_address;
(ctx, btree, root, ext, Some(sb))
} else {
let sb = SuperblockV2V3::decode(&sb_buf)?;
let ctx = FormatContext {
sizeof_addr: sb.sizeof_offsets,
sizeof_size: sb.sizeof_lengths,
};
(
ctx,
crate::format::btree_v1::BTreeV1Config::default(),
sb.root_group_object_header_address,
sb.superblock_extension_address,
None,
)
};
let (meta, ext) = crate::io::reader::Hdf5Reader::read_extension_and_meta(
&mut handle,
ctx,
sb_btree,
ext_addr,
)?;
let sohm = Self::reopen_shared_messages(&mut handle, &meta, &ext)?;
let extension = if ext_addr == UNDEF_ADDR || ext_addr == 0 {
Box::<CarriedExtension>::default()
} else {
let (carried, blocks) = crate::io::object_header_io::superblock_extension_messages(
&mut handle,
&meta,
ext_addr,
)?;
Box::new(CarriedExtension {
superseded: blocks,
carried,
addr: Slot::new(None),
})
};
let reopened_free_space = Self::reopen_free_space(&mut handle, &meta, &ext)?;
let mut walk = ReopenWalk::new(&mut handle, &meta);
let root = match walk.plan(root_addr)? {
ObjectPlan::Group(parts) => parts,
ObjectPlan::Dataset(_) => {
return Err(crate::io::IoError::InvalidState(
"cannot open this file for appending: its root object is a dataset, \
not a group"
.into(),
))
}
ObjectPlan::Preserve { why, .. } => {
return Err(crate::io::IoError::Unsupported(format!(
"cannot open this file for appending: {why}. Every append rewrites the \
root group's header, and this writer will not rewrite it from the part \
of it that it can read"
)));
}
};
let root_header_blocks = root.header_blocks;
let root_attributes = root.attributes;
let root_track_order = root.track_order;
let root_times = root.times;
let root_dense = root.dense;
let root_stab = root.stab;
walk.group(&root.links, "", 0)?;
let collected = walk.finish();
let mut link_entries = collected.hard;
let mut preserved = collected.preserved;
let mut unrebuilt: std::collections::HashMap<u64, String> = Default::default();
let mut seen_header_addrs = std::collections::HashSet::new();
let mut alias_entries: Vec<HardEntry> = Vec::new();
link_entries.retain(|(entry, _)| {
if seen_header_addrs.insert(entry.address) {
true
} else {
alias_entries.push(entry.clone());
false
}
});
let walk_order: Vec<String> = link_entries.iter().map(|(e, _)| e.path.clone()).collect();
let mut existing_datasets = Vec::new();
type GroupHeaderInfo = (
u64,
crate::io::object_header_io::HeaderBlocks,
Vec<AttributeEntry>,
TrackOrder,
Option<ObjectTimes>,
);
let mut group_headers: std::collections::HashMap<String, GroupHeaderInfo> =
Default::default();
let mut dataset_dense: Vec<(usize, AttributeInfoMessage)> = Vec::new();
let mut group_dense: Vec<(String, DenseCarry)> = Vec::new();
let mut group_stabs: Vec<(String, StabExtents)> = Vec::new();
for (entry, object) in link_entries {
let HardEntry {
path: name,
address: obj_addr,
encoded,
} = entry;
let parts = match object {
CollectedObject::Group {
header_blocks,
attributes,
track_order,
times,
dense,
stab,
} => {
group_dense.push((name.clone(), dense));
if let Some(stab) = stab {
group_stabs.push((name.clone(), stab));
}
group_headers.insert(
name,
(obj_addr, header_blocks, attributes, track_order, times),
);
continue;
}
CollectedObject::Dataset(parts) => *parts,
};
let dense_attrs = parts.dense.attrs.clone();
match rebuild_dataset(&mut handle, &meta, file_size, name.clone(), obj_addr, parts) {
Ok(info) => {
if let Some(ainfo) = dense_attrs {
dataset_dense.push((existing_datasets.len(), ainfo));
}
existing_datasets.push(info);
}
Err(e) => {
let why = format!("this writer could not rebuild its chunk index: {e}");
unrebuilt.insert(obj_addr, why.clone());
preserved.push(PreservedEntry {
path: name,
class: crate::io::reader::LinkClass::Hard,
encoded,
reason: Some(why),
kind: PreservedKind::Unclassified,
});
}
}
}
let mut groups: Vec<GroupInfo> = Vec::new();
let mut group_index_map: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
fn ensure_groups_for(
link_path: &str,
groups: &mut Vec<GroupInfo>,
group_index_map: &mut std::collections::HashMap<String, usize>,
group_headers: &mut std::collections::HashMap<String, GroupHeaderInfo>,
) {
let mut path = String::new();
for part in link_path.split('/') {
let parent_path = if path.is_empty() {
"/".to_string()
} else {
path.clone()
};
if path.is_empty() {
path = format!("/{}", part);
} else {
path = format!("{}/{}", path, part);
}
if group_index_map.contains_key(&path) {
continue;
}
let parent = if parent_path == "/" {
None
} else {
group_index_map.get(&parent_path).copied()
};
let gidx = groups.len();
let (obj_header_written_addr, obj_header_blocks, attributes, track_order, times) =
group_headers.remove(path.trim_start_matches('/')).map_or(
(None, Vec::new(), Vec::new(), TrackOrder::default(), None),
|(addr, blocks, attrs, track, times)| {
(Some(addr), blocks, attrs, track, times)
},
);
groups.push(GroupInfo {
name: path.clone(),
parent,
creation_seq: 0,
track_order,
times,
child_datasets: Vec::new(),
child_groups: Vec::new(),
obj_header_addr: 0,
obj_header_written_addr,
obj_header_blocks,
deleted: false,
attributes,
});
if let Some(pidx) = parent {
groups[pidx].child_groups.push(gidx);
}
group_index_map.insert(path.clone(), gidx);
}
}
for name in &walk_order {
if group_headers.contains_key(name.as_str()) {
ensure_groups_for(name, &mut groups, &mut group_index_map, &mut group_headers);
}
}
for (di, ds) in existing_datasets.iter().enumerate() {
let parts: Vec<&str> = ds.name.split('/').collect();
if parts.len() <= 1 {
continue; }
let parent_link_path = parts[..parts.len() - 1].join("/");
ensure_groups_for(
&parent_link_path,
&mut groups,
&mut group_index_map,
&mut group_headers,
);
let gidx = group_index_map[&format!("/{}", parent_link_path)];
groups[gidx].child_datasets.push(di);
}
alias_entries.retain(|entry| match unrebuilt.get(&entry.address) {
None => true,
Some(why) => {
preserved.push(PreservedEntry {
path: entry.path.clone(),
class: crate::io::reader::LinkClass::Hard,
encoded: entry.encoded.clone(),
reason: Some(why.clone()),
kind: PreservedKind::Unclassified,
});
false
}
});
if sohm.is_some() {
for entry in &preserved {
if !matches!(entry.class, crate::io::reader::LinkClass::Hard) {
continue;
}
let Ok((link, _)) = LinkMessage::decode(&entry.encoded, &meta.ctx) else {
continue;
};
let LinkTarget::Hard { address } = link.target else {
continue;
};
if let Some(blocks) = crate::io::object_header_io::blocks_shared_message_rebuild(
&mut handle,
&meta,
address,
)? {
let why = entry
.reason
.as_deref()
.unwrap_or("this writer cannot model it");
return Err(crate::io::IoError::Unsupported(format!(
"cannot open this file for appending: '{}' {blocks}, but {why}, so \
its header keeps the bytes it has while the append lays the \
shared-message table out afresh",
entry.path
)));
}
}
}
let mut hard_links: Vec<HardLink> = Vec::new();
for HardEntry {
path,
address: addr,
..
} in alias_entries
{
let target = if let Some(di) = existing_datasets
.iter()
.position(|d| d.obj_header_addr == addr)
{
HardLinkTarget::Dataset(di)
} else if let Some(gi) = groups
.iter()
.position(|g| g.obj_header_written_addr == Some(addr))
{
HardLinkTarget::Group(gi)
} else {
continue;
};
let (parent, link_name) = match path.rsplit_once('/') {
None => (None, path),
Some((dir, leaf)) => {
ensure_groups_for(dir, &mut groups, &mut group_index_map, &mut group_headers);
(
group_index_map.get(&format!("/{dir}")).copied(),
leaf.to_string(),
)
}
};
hard_links.push(HardLink {
parent,
name: link_name,
target,
creation_seq: 0,
});
}
let mut preserved_links: Vec<PreservedLink> = Vec::new();
for PreservedEntry {
path,
class,
encoded,
reason,
kind,
} in preserved
{
let (parent, link_name) = match path.rsplit_once('/') {
None => (None, path),
Some((dir, leaf)) => {
ensure_groups_for(dir, &mut groups, &mut group_index_map, &mut group_headers);
(
group_index_map.get(&format!("/{dir}")).copied(),
leaf.to_string(),
)
}
};
preserved_links.push(PreservedLink {
parent,
name: link_name,
class,
encoded,
reason,
kind,
});
}
let mut creation_seq = 0u64;
for d in &mut existing_datasets {
d.creation_seq = creation_seq;
creation_seq += 1;
}
for g in &mut groups {
g.creation_seq = creation_seq;
creation_seq += 1;
}
for l in &mut hard_links {
l.creation_seq = creation_seq;
creation_seq += 1;
}
let allocator = FileAllocator::with_policy(
file_size,
ext.file_space_info
.as_ref()
.map_or(free_space::SpacePolicy::Aggr, |info| {
free_space::SpacePolicy::for_message(info)
}),
);
allocator.reset_free_list(&reopened_free_space.sections);
let mut superseded = SupersededDense {
attrs: dataset_dense
.into_iter()
.map(|(di, ainfo)| (AttrScope::Dataset(di), ainfo))
.collect(),
links: HashMap::new(),
};
superseded
.attrs
.extend(root_dense.attrs.map(|a| (AttrScope::Root, a)));
superseded
.links
.extend(root_dense.links.map(|l| (LinkScope::Root, l)));
for (name, dense) in group_dense {
let Some(&gidx) = group_index_map.get(&format!("/{name}")) else {
continue;
};
superseded
.attrs
.extend(dense.attrs.map(|a| (AttrScope::Group(gidx), a)));
superseded
.links
.extend(dense.links.map(|l| (LinkScope::Group(gidx), l)));
}
let superseded = (!superseded.attrs.is_empty() || !superseded.links.is_empty())
.then(|| Box::new(superseded));
let mut stabs: HashMap<LinkScope, StabExtents> = HashMap::new();
stabs.extend(root_stab.map(|s| (LinkScope::Root, s)));
for (name, extents) in group_stabs {
if let Some(&gidx) = group_index_map.get(&format!("/{name}")) {
stabs.insert(LinkScope::Group(gidx), extents);
}
}
let symbol_tables = SymbolTables {
found: stabs.keys().copied().collect(),
superseded: Slot::new(stabs),
written: Slot::new(HashMap::new()),
};
let legacy = legacy.map(|superblock| Box::new(LegacyFile { superblock }));
let datasets = existing_datasets
.into_iter()
.map(|i| Shared::new(DatasetCell::new(i)))
.collect();
let groups = groups
.into_iter()
.map(|g| Shared::new(Slot::new(g)))
.collect();
let writer = Self {
handle,
allocator,
ctx,
datasets: Slot::new(datasets),
groups: Slot::new(groups),
hard_links: Slot::new(hard_links),
symbolic_links: Slot::new(Vec::new()),
committed_datatypes: Slot::new(Vec::new()),
preserved_links: Slot::new(preserved_links),
name_index: Slot::new(Box::new(NameIndex::new())),
root_attributes: Slot::new(root_attributes),
create_lock: Slot::new(()),
libver: None,
closed: false,
swmr_active: false,
cwfs: Slot::new(Vec::new()),
root_group_addr: None,
superseded_root_header: root_header_blocks,
superblock_version: SuperblockVersion::Existing(version),
root_track_order,
root_times,
dense_attributes: Slot::new(HashMap::new()),
dense_links: Slot::new(HashMap::new()),
superseded_dense: Slot::new(superseded),
track_order: root_track_order,
track_times: false,
next_creation_seq: Slot::new(creation_seq),
pending_object_references: Slot::new(Vec::new()),
pending_heap_references: Slot::new(Vec::new()),
attribute_references: Slot::new(Vec::new()),
legacy,
symbol_tables,
btree: meta.btree,
extension,
free_space: reopened_free_space.state,
sohm,
source_dir: source_dir_of(path)?,
};
for i in 0..writer.dataset_count() {
let nlink = writer.object_link_count(HardLinkTarget::Dataset(i));
writer.ds(i).lock().nlink_written = nlink;
}
Ok(writer)
}
pub fn dataset_names(&self) -> Vec<String> {
self.dataset_refs()
.iter()
.filter_map(|d| {
let g = d.lock();
(!g.deleted).then(|| g.name.clone())
})
.collect()
}
pub fn dataset_index(&self, name: &str) -> Option<usize> {
let name = self.canonical_dataset_path(name);
self.dataset_refs()
.iter()
.position(|d| {
let g = d.lock();
g.name == name && !g.deleted
})
.or_else(|| {
self.hard_links_vec().iter().find_map(|l| match l.target {
HardLinkTarget::Dataset(i)
if self.hard_link_emitted(l) && self.hard_link_full_path(l) == name =>
{
Some(i)
}
_ => None,
})
})
}
pub(crate) fn dataset_handle_parts(
&self,
index: usize,
access: &DatasetAccess,
) -> IoResult<DatasetHandleParts> {
let open = self.bind_efile_prefix(index, access)?;
let ds = self.ds(index);
let g = ds.lock();
Ok(DatasetHandleParts {
shape: g.dataspace.dims.iter().map(|&d| d as usize).collect(),
element_size: g.datatype.element_size() as usize,
chunk_index: g.chunk_index_kind(),
open,
})
}
pub(crate) fn bind_efile_prefix(
&self,
index: usize,
access: &DatasetAccess,
) -> IoResult<Option<crate::io::reader::DatasetOpenToken>> {
let ds = self.ds(index);
let mut g = ds.lock();
let source_dir = &self.source_dir;
let Some(ext) = g.external.as_mut() else {
return Ok(None);
};
let want =
crate::io::reader::resolve_extfile_prefix(access.efile_prefix_value(), source_dir);
if let Some(open) = ext.prefix.open.upgrade() {
if ext.prefix.expanded != want {
let name = g.name.clone();
return Err(crate::io::IoError::InvalidState(format!(
"dataset {name:?} is already open under a different external file prefix, and libhdf5 refuses to join an open that disagrees about one"
)));
}
return Ok(Some(open));
}
let token: crate::io::reader::DatasetOpenToken = std::sync::Arc::new(());
ext.prefix = EfilePrefix {
expanded: want,
open: std::sync::Arc::downgrade(&token),
};
Ok(Some(token))
}
fn ensure_name_free(&self, name: &str) -> IoResult<()> {
let holder = self.name_holder(name);
#[cfg(debug_assertions)]
assert_eq!(
holder,
self.scan_name_holder(name),
"the name index disagrees with the registries for '{name}'"
);
match holder {
None => Ok(()),
Some(kind) => Err(crate::io::IoError::InvalidState(format!(
"a {kind} named '{name}' already exists"
))),
}
}
fn name_holder(&self, name: &str) -> Option<&'static str> {
self.build_name_index();
let hits: Vec<NameHit> = {
let index = self.name_index.lock();
index.map.as_ref().and_then(|m| m.get(name))?.clone()
};
for hit in &hits {
if let NameHit::Dataset(i) = *hit {
let ds = self.ds(i);
let d = ds.lock();
if !d.deleted && d.name == name {
return Some("dataset");
}
}
}
for hit in &hits {
if let NameHit::Group(i) = *hit {
let grp = self.grp(i);
let g = grp.lock();
if !g.deleted && g.name.trim_start_matches('/') == name {
return Some("group");
}
}
}
for hit in &hits {
if let NameHit::Datatype(i) = *hit {
let (parent, held) = {
let reg = self.committed_datatypes.lock();
(reg[i].parent, reg[i].name == name)
};
if held && self.parent_alive(parent) {
return Some("committed datatype");
}
}
}
if hits.contains(&NameHit::HardLink)
&& self
.hard_links_vec()
.iter()
.any(|l| self.hard_link_emitted(l) && self.hard_link_full_path(l) == name)
{
return Some("hard link");
}
if hits.contains(&NameHit::SymbolicLink)
&& self
.symbolic_links_vec()
.iter()
.any(|l| self.symbolic_link_emitted(l) && self.symbolic_link_full_path(l) == name)
{
return Some("link");
}
if hits.contains(&NameHit::PreservedLink)
&& self.preserved_link_paths().iter().any(|(p, _)| *p == name)
{
return Some("link");
}
None
}
#[cfg(debug_assertions)]
fn scan_name_holder(&self, name: &str) -> Option<&'static str> {
if self.dataset_refs().iter().any(|d| {
let g = d.lock();
!g.deleted && g.name == name
}) {
return Some("dataset");
}
if self.group_refs().iter().any(|g| {
let gg = g.lock();
!gg.deleted && gg.name.trim_start_matches('/') == name
}) {
return Some("group");
}
if self
.committed_datatypes_vec()
.iter()
.any(|c| self.parent_alive(c.parent) && c.name == name)
{
return Some("committed datatype");
}
if self
.hard_links_vec()
.iter()
.any(|l| self.hard_link_emitted(l) && self.hard_link_full_path(l) == name)
{
return Some("hard link");
}
if self
.symbolic_links_vec()
.iter()
.any(|l| self.symbolic_link_emitted(l) && self.symbolic_link_full_path(l) == name)
{
return Some("link");
}
if self.preserved_link_paths().iter().any(|(p, _)| *p == name) {
return Some("link");
}
None
}
fn build_name_index(&self) {
let epoch = {
let index = self.name_index.lock();
if index.map.is_some() {
return;
}
index.epoch
};
let mut map: HashMap<String, Vec<NameHit>> = HashMap::new();
for (i, ds) in self.dataset_refs().iter().enumerate() {
let d = ds.lock();
if !d.deleted {
map.entry(d.name.clone())
.or_default()
.push(NameHit::Dataset(i));
}
}
for (i, grp) in self.group_refs().iter().enumerate() {
let g = grp.lock();
if !g.deleted {
map.entry(g.name.trim_start_matches('/').to_string())
.or_default()
.push(NameHit::Group(i));
}
}
for (i, c) in self.committed_datatypes_vec().iter().enumerate() {
map.entry(c.name.clone())
.or_default()
.push(NameHit::Datatype(i));
}
for l in self.hard_links_vec().iter() {
map.entry(self.hard_link_full_path(l))
.or_default()
.push(NameHit::HardLink);
}
for l in self.symbolic_links_vec().iter() {
map.entry(self.symbolic_link_full_path(l))
.or_default()
.push(NameHit::SymbolicLink);
}
for (path, _) in self.preserved_link_paths() {
map.entry(path).or_default().push(NameHit::PreservedLink);
}
let mut index = self.name_index.lock();
if index.map.is_none() && index.epoch == epoch {
index.map = Some(map);
}
}
fn register_name(&self, name: &str, hit: NameHit) {
self.name_index.lock().insert(name, hit);
}
fn forget_name_index(&self) {
self.name_index.lock().forget();
}
pub fn delete_dataset(&self, name: &str) -> IoResult<()> {
if self.swmr_active {
return Err(swmr_delete_error(name));
}
self.reject_external_traversal(name)?;
let _create = self.create_lock.lock();
let name = match name.rsplit_once('/') {
None => name.to_string(),
Some((dir, leaf)) => format!(
"{}/{leaf}",
self.canonical_group_path(&format!("/{dir}"))
.trim_start_matches('/')
),
};
let refs = self.dataset_refs();
let idx = match refs.iter().position(|d| {
let g = d.lock();
g.name == name && !g.deleted
}) {
Some(i) => i,
None => {
let link = self.hard_links_vec().iter().position(|l| {
self.hard_link_emitted(l)
&& matches!(l.target, HardLinkTarget::Dataset(_))
&& self.hard_link_full_path(l) == name
});
let Some(pos) = link else {
return Err(crate::io::IoError::NotFound(name));
};
self.hard_links.lock().remove(pos);
return Ok(());
}
};
let promote = self.hard_links_vec().iter().position(|l| {
self.hard_link_emitted(l) && matches!(l.target, HardLinkTarget::Dataset(i) if i == idx)
});
if let Some(pos) = promote {
self.promote_dataset_to_link(idx, pos);
return Ok(());
}
refs[idx].lock().deleted = true;
for grp in self.group_refs() {
grp.lock().child_datasets.retain(|&di| di != idx);
}
self.purge_dead_links();
let ds = self.ds(idx);
let _op = ds.op.lock();
self.release_dataset_storage(idx)
}
pub fn delete_group(&self, name: &str) -> IoResult<()> {
if self.swmr_active {
return Err(swmr_delete_error(name));
}
self.reject_external_traversal(name)?;
let _create = self.create_lock.lock();
let name = if name.starts_with('/') {
name.to_string()
} else {
format!("/{}", name)
};
let name = match name.rsplit_once('/') {
Some((dir, leaf)) if !dir.is_empty() => {
format!("{}/{leaf}", self.canonical_group_path(dir))
}
_ => name,
};
let groups = self.group_refs();
let gidx = match groups.iter().position(|g| {
let gg = g.lock();
gg.name == name && !gg.deleted
}) {
Some(i) => i,
None => {
let trimmed = name.trim_start_matches('/');
let link = self.hard_links_vec().iter().position(|l| {
self.hard_link_emitted(l)
&& matches!(l.target, HardLinkTarget::Group(_))
&& self.hard_link_full_path(l) == trimmed
});
let Some(pos) = link else {
return Err(crate::io::IoError::NotFound(name.clone()));
};
self.hard_links.lock().remove(pos);
return Ok(());
}
};
fn outside(parent: Option<usize>, doomed_gs: &[usize]) -> bool {
match parent {
None => true,
Some(pi) => !doomed_gs.contains(&pi),
}
}
let mut doomed_ds = Vec::new();
let mut doomed_gs = Vec::new();
loop {
doomed_ds.clear();
doomed_gs.clear();
self.collect_live_subtree(gidx, &mut doomed_ds, &mut doomed_gs);
let promote = self
.hard_links_vec()
.iter()
.enumerate()
.find_map(|(pos, l)| match l.target {
HardLinkTarget::Group(gi)
if self.hard_link_emitted(l)
&& outside(l.parent, &doomed_gs)
&& doomed_gs.contains(&gi) =>
{
Some((pos, gi))
}
_ => None,
});
let Some((pos, gi)) = promote else { break };
self.promote_group_to_link(gi, pos);
if gi == gidx {
return Ok(());
}
}
for di in doomed_ds {
let promote = self.hard_links_vec().iter().position(|l| {
self.hard_link_emitted(l)
&& outside(l.parent, &doomed_gs)
&& matches!(l.target, HardLinkTarget::Dataset(i) if i == di)
});
if let Some(pos) = promote {
self.promote_dataset_to_link(di, pos);
}
}
let mut ds_deleted = Vec::new();
let mut gs_deleted = Vec::new();
self.delete_group_recursive(gidx, &mut ds_deleted, &mut gs_deleted);
let parent = groups[gidx].lock().parent;
if let Some(pidx) = parent {
groups[pidx].lock().child_groups.retain(|&gi| gi != gidx);
}
self.purge_dead_links();
for di in ds_deleted {
let ds = self.ds(di);
let _op = ds.op.lock();
self.release_dataset_storage(di)?;
}
for gi in gs_deleted {
self.release_group_storage(gi)?;
}
Ok(())
}
fn collect_live_subtree(&self, gidx: usize, ds_out: &mut Vec<usize>, gs_out: &mut Vec<usize>) {
if gs_out.contains(&gidx) {
return;
}
let (child_ds, child_gs) = {
let grp = self.grp(gidx);
let g = grp.lock();
if g.deleted {
return;
}
(g.child_datasets.clone(), g.child_groups.clone())
};
gs_out.push(gidx);
for di in child_ds {
if !self.ds(di).lock().deleted && !ds_out.contains(&di) {
ds_out.push(di);
}
}
for gi in child_gs {
self.collect_live_subtree(gi, ds_out, gs_out);
}
}
fn promote_dataset_to_link(&self, idx: usize, pos: usize) {
let link = self.hard_links.lock().remove(pos);
let new_name = self.hard_link_full_path(&link);
for grp in self.group_refs() {
grp.lock().child_datasets.retain(|&di| di != idx);
}
if let Some(pi) = link.parent {
self.grp(pi).lock().child_datasets.push(idx);
}
self.ds(idx).lock().name = new_name.clone();
self.register_name(&new_name, NameHit::Dataset(idx));
}
fn promote_group_to_link(&self, gidx: usize, pos: usize) {
let link = self.hard_links.lock().remove(pos);
let new_name = format!("/{}", self.hard_link_full_path(&link));
let old_name = self.grp(gidx).lock().name.clone();
for grp in self.group_refs() {
grp.lock().child_groups.retain(|&g| g != gidx);
}
{
let grp = self.grp(gidx);
let mut g = grp.lock();
g.parent = link.parent;
g.name = new_name.clone();
}
if let Some(pi) = link.parent {
self.grp(pi).lock().child_groups.push(gidx);
}
let mut ds_in = Vec::new();
let mut gs_in = Vec::new();
self.collect_live_subtree(gidx, &mut ds_in, &mut gs_in);
let old_grp_prefix = format!("{old_name}/");
let new_grp_prefix = format!("{new_name}/");
let old_ds_prefix = old_grp_prefix.trim_start_matches('/').to_string();
let new_ds_prefix = new_grp_prefix.trim_start_matches('/').to_string();
for gi in gs_in {
if gi == gidx {
continue;
}
let grp = self.grp(gi);
let mut g = grp.lock();
let renamed = g
.name
.strip_prefix(&old_grp_prefix)
.map(|rest| format!("{new_grp_prefix}{rest}"));
if let Some(n) = renamed {
g.name = n;
}
}
for di in ds_in {
let ds = self.ds(di);
let mut d = ds.lock();
let renamed = d
.name
.strip_prefix(&old_ds_prefix)
.map(|rest| format!("{new_ds_prefix}{rest}"));
if let Some(n) = renamed {
d.name = n;
}
}
self.forget_name_index();
}
fn purge_dead_links(&self) {
let dead: Vec<usize> = self
.hard_links_vec()
.iter()
.enumerate()
.filter(|(_, l)| !self.hard_link_emitted(l))
.map(|(p, _)| p)
.collect();
let mut links = self.hard_links.lock();
for p in dead.into_iter().rev() {
links.remove(p);
}
drop(links);
let dead: Vec<usize> = self
.symbolic_links_vec()
.iter()
.enumerate()
.filter(|(_, l)| !self.symbolic_link_emitted(l))
.map(|(p, _)| p)
.collect();
let mut links = self.symbolic_links.lock();
for p in dead.into_iter().rev() {
links.remove(p);
}
}
fn delete_group_recursive(
&self,
gidx: usize,
ds_out: &mut Vec<usize>,
gs_out: &mut Vec<usize>,
) {
let (child_ds, child_gs) = {
let grp = self.grp(gidx);
let mut g = grp.lock();
if g.deleted {
return;
}
g.deleted = true;
(g.child_datasets.clone(), g.child_groups.clone())
};
gs_out.push(gidx);
for di in child_ds {
let ds = self.ds(di);
let mut d = ds.lock();
if !d.deleted {
d.deleted = true;
ds_out.push(di);
}
}
for gi in child_gs {
self.delete_group_recursive(gi, ds_out, gs_out);
}
}
fn release_dataset_storage(&self, index: usize) -> IoResult<()> {
use crate::format::messages::datatype::DatatypeMessage;
let (indexed, ndims, contiguous, is_vlen, attrs, header_blocks, mapping_list) = {
let ds = self.ds(index);
let mut m = ds.lock();
m.append = None;
let indexed = m.is_chunked();
let contiguous = (!indexed && m.data_addr != UNDEF_ADDR && m.data_size > 0)
.then_some((m.data_addr, m.data_size));
m.data_addr = UNDEF_ADDR;
m.data_size = 0;
m.external = None;
let mapping_list = m
.virtual_storage
.take()
.and_then(|v| u16::try_from(v.heap_index).ok().map(|i| (v.heap_addr, i)));
let is_vlen = matches!(
m.datatype,
DatatypeMessage::VarLenString { .. } | DatatypeMessage::VarLenSequence { .. }
);
let attrs = std::mem::take(&mut m.attributes);
m.obj_header_written_addr = None;
let header_blocks = std::mem::take(&mut m.obj_header_blocks);
(
indexed,
m.dataspace.dims.len(),
contiguous,
is_vlen,
attrs,
header_blocks,
mapping_list,
)
};
if let Some((addr, idx)) = mapping_list {
self.remove_heap_objects([(addr, vec![idx])].into_iter().collect())?;
}
if indexed {
self.prune_chunks_beyond(index, &vec![0; ndims])?;
self.free_chunk_index(index)?;
} else if let Some((addr, size)) = contiguous {
if is_vlen {
let data = self.handle.read_at(addr, size as usize)?;
self.release_vlen_references(&data)?;
}
self.allocator.free(addr, size, FreeSpaceClass::RawData);
}
for attr in &attrs {
self.release_attr_vlen(attr)?;
}
self.release_superseded_dense_attrs(AttrScope::Dataset(index))?;
for (addr, size) in header_blocks {
self.allocator.free(addr, size, FreeSpaceClass::Metadata);
}
Ok(())
}
fn release_group_storage(&self, gidx: usize) -> IoResult<()> {
let (attrs, header_blocks) = {
let grp = self.grp(gidx);
let mut g = grp.lock();
let attrs = std::mem::take(&mut g.attributes);
g.obj_header_written_addr = None;
(attrs, std::mem::take(&mut g.obj_header_blocks))
};
for attr in &attrs {
self.release_attr_vlen(attr)?;
}
self.release_superseded_dense_attrs(AttrScope::Group(gidx))?;
self.release_superseded_dense_links(LinkScope::Group(gidx))?;
for (addr, size) in header_blocks {
self.allocator.free(addr, size, FreeSpaceClass::Metadata);
}
Ok(())
}
fn release_superseded_dense_attrs(&self, scope: AttrScope) -> IoResult<()> {
if self.swmr_active {
return Ok(());
}
let taken = self
.superseded_dense
.lock()
.as_mut()
.and_then(|s| s.attrs.remove(&scope));
let Some(ainfo) = taken else {
return Ok(());
};
self.release_dense_storage(
ainfo.fractal_heap_address,
ainfo.name_btree_address,
ainfo.creation_order_btree_address,
)
}
fn release_superseded_dense_links(&self, scope: LinkScope) -> IoResult<()> {
if self.swmr_active {
return Ok(());
}
let taken = self
.superseded_dense
.lock()
.as_mut()
.and_then(|s| s.links.remove(&scope));
let Some(linfo) = taken else {
return Ok(());
};
self.release_dense_storage(
linfo.fractal_heap_address,
linfo.name_btree_address,
linfo.creation_order_btree_address,
)
}
fn release_dense_storage(
&self,
heap_addr: u64,
name_bt2_addr: u64,
corder_bt2_addr: Option<u64>,
) -> IoResult<()> {
use crate::format::chunk_index::btree_v2::collect_btree_v2_extents;
use crate::format::fractal_heap::collect_heap_extents;
let mut reader = crate::io::reader::HandleBlockReader {
handle: &self.handle,
};
let mut extents = Vec::new();
if heap_addr != UNDEF_ADDR {
extents.extend(collect_heap_extents(heap_addr, &self.ctx, &mut reader)?);
}
for addr in [Some(name_bt2_addr), corder_bt2_addr]
.into_iter()
.flatten()
.filter(|&a| a != UNDEF_ADDR)
{
extents.extend(collect_btree_v2_extents(addr, &self.ctx, &mut reader)?);
}
for (addr, len) in extents {
self.allocator.free(addr, len, FreeSpaceClass::Metadata);
}
Ok(())
}
fn free_chunk_index(&self, index: usize) -> IoResult<()> {
let ds = self.ds(index);
let mut m = ds.lock();
let is_filtered = m.filter_pipeline.is_some();
if let Some(c) = m.chunked.take() {
let p = &c.earray_params;
let bits = p.max_nelmts_bits;
let csl = c.chunk_size_len;
let geo = EaGeometry::new(
p.idx_blk_elmts,
p.data_blk_min_elmts,
p.sup_blk_min_data_ptrs,
bits,
p.max_dblk_page_nelmts_bits,
)?;
let dblk_size = |nelmts: u64| -> u64 {
if is_filtered {
FilteredDataBlock::new(c.ea_header_addr, 0, nelmts as usize)
.encode(&self.ctx, bits, csl)
.len() as u64
} else {
ExtensibleArrayDataBlock::new(c.ea_header_addr, 0, nelmts as usize)
.encoded_size(&self.ctx, bits) as u64
}
};
let (dblk_addrs, sblk_addrs, iblk_size) = if is_filtered {
let f = c.filt_iblk.as_ref().unwrap();
(
f.dblk_addrs.clone(),
f.sblk_addrs.clone(),
f.encode(&self.ctx, csl).len() as u64,
)
} else {
(
c.ea_iblk.dblk_addrs.clone(),
c.ea_iblk.sblk_addrs.clone(),
c.ea_iblk.encoded_size(&self.ctx) as u64,
)
};
let mut g = 0usize;
'direct: for s in geo.sblk.iter().take(geo.iblock_nsblks) {
for _ in 0..s.ndblks {
let Some(&a) = dblk_addrs.get(g) else {
break 'direct;
};
g += 1;
if a == UNDEF_ADDR {
continue;
}
if s.dblk_nelmts > geo.dblk_page_nelmts {
return Err(crate::io::IoError::InvalidState(
"cannot free a paged extensible-array data block, \
which is not yet supported"
.into(),
));
}
self.allocator
.free(a, dblk_size(s.dblk_nelmts), FreeSpaceClass::Metadata);
}
}
for (off, &sa) in sblk_addrs.iter().enumerate() {
if sa == UNDEF_ADDR {
continue;
}
let s = geo.sblk[geo.iblock_nsblks + off];
if s.dblk_nelmts > geo.dblk_page_nelmts {
return Err(crate::io::IoError::InvalidState(
"cannot free a paged extensible-array data block, \
which is not yet supported"
.into(),
));
}
let buf = self.handle.read_at_most(sa, 65536)?;
let sb =
ExtensibleArraySuperBlock::decode(&buf, &self.ctx, bits, s.ndblks as usize, 0)?;
for &da in &sb.dblk_addrs {
if da != UNDEF_ADDR {
self.allocator
.free(da, dblk_size(s.dblk_nelmts), FreeSpaceClass::Metadata);
}
}
self.allocator.free(
sa,
sb.encode(&self.ctx, bits).len() as u64,
FreeSpaceClass::Metadata,
);
}
self.allocator
.free(c.ea_iblk_addr, iblk_size, FreeSpaceClass::Metadata);
self.allocator.free(
c.ea_header_addr,
c.ea_header.encoded_size(&self.ctx) as u64,
FreeSpaceClass::Metadata,
);
return Ok(());
}
if let Some(fa) = m.fixed_array.take() {
self.allocator.free(
fa.fa_dblk_addr,
fixed_array_dblk_disk_size(&self.ctx, &fa.fa_header),
FreeSpaceClass::Metadata,
);
self.allocator.free(
fa.fa_header_addr,
fa.fa_header.encode(&self.ctx).len() as u64,
FreeSpaceClass::Metadata,
);
return Ok(());
}
if let Some(imp) = m.implicit.take() {
self.allocator
.free(imp.data_addr, imp.data_size, FreeSpaceClass::RawData);
return Ok(());
}
if let Some(sc) = m.single_chunk.take() {
if sc.data_addr != UNDEF_ADDR {
let len = if is_filtered { sc.nbytes } else { sc.data_size };
self.allocator
.free(sc.data_addr, len, FreeSpaceClass::RawData);
}
return Ok(());
}
if let Some(bt1) = m.btree_v1.take() {
let element_size = m.datatype.element_size() as u64;
let node_size = bt1
.build_tree(element_size, self.ctx.sizeof_addr as usize)
.node_size();
for &a in &bt1.node_addrs {
self.allocator
.free(a, node_size as u64, FreeSpaceClass::Metadata);
}
return Ok(());
}
if let Some(bt2) = m.btree_v2.take() {
let tree = bt2.index.build_tree(&self.ctx);
for &a in &bt2.node_addrs {
self.allocator
.free(a, tree.node_size as u64, FreeSpaceClass::Metadata);
}
self.allocator.free(
bt2.bt2_header_addr,
tree.header(UNDEF_ADDR).encode(&self.ctx).len() as u64,
FreeSpaceClass::Metadata,
);
}
Ok(())
}
pub fn dataset_chunk_dims(&self, index: usize) -> Option<Vec<u64>> {
let ds = self.ds(index);
let m = ds.lock();
m.chunk_index_kind().map(|kind| match kind {
ChunkIndexKind::ExtensibleArray => m.chunked.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::FixedArray => m.fixed_array.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::BtreeV2 => m.btree_v2.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::Implicit => m.implicit.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::SingleChunk => m.single_chunk.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::BtreeV1 => m.btree_v1.as_ref().unwrap().chunk_dims.clone(),
})
}
pub fn dataset_dims(&self, index: usize) -> Vec<u64> {
self.ds(index).lock().dataspace.dims.clone()
}
pub fn dataset_max_dims(&self, index: usize) -> Vec<u64> {
let ds = self.ds(index);
let m = ds.lock();
m.dataspace
.max_dims
.clone()
.unwrap_or_else(|| m.dataspace.dims.clone())
}
pub(crate) fn dataset_is_filtered(&self, index: usize) -> bool {
self.ds(index).lock().filter_pipeline.is_some()
}
pub fn dataset_datatype(&self, index: usize) -> DatatypeMessage {
self.ds(index).lock().datatype.clone()
}
pub fn create_group(&self, parent_path: &str, name: &str) -> IoResult<usize> {
let _create = self.create_lock.lock();
let parent_path = self.canonical_group_path(parent_path);
let parent_path = parent_path.as_str();
let full_name = if parent_path == "/" {
format!("/{}", name)
} else {
format!("{}/{}", parent_path, name)
};
self.reject_external_traversal(&full_name)?;
let (parent_idx, _leaf) = self.split_parent(full_name.trim_start_matches('/'))?;
self.ensure_name_free(full_name.trim_start_matches('/'))?;
let group_idx = self.push_group(GroupInfo {
name: full_name,
parent: parent_idx,
creation_seq: self.take_creation_seq(),
track_order: self.track_order,
times: self.created_object_times(),
child_datasets: Vec::new(),
child_groups: Vec::new(),
obj_header_addr: 0,
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
deleted: false,
attributes: Vec::new(),
});
if let Some(pidx) = parent_idx {
self.grp(pidx).lock().child_groups.push(group_idx);
}
Ok(group_idx)
}
pub fn assign_dataset_to_group(&self, group_path: &str, ds_index: usize) -> IoResult<()> {
let group_path = self.canonical_group_path(group_path);
let group_path = group_path.as_str();
let groups = self.group_refs();
let group_idx = groups
.iter()
.position(|g| {
let gg = g.lock();
gg.name == group_path && !gg.deleted
})
.ok_or_else(|| {
crate::io::IoError::NotFound(format!("group '{}' not found", group_path))
})?;
for g in &groups {
g.lock().child_datasets.retain(|&d| d != ds_index);
}
groups[group_idx].lock().child_datasets.push(ds_index);
Ok(())
}
pub fn create_hard_link(
&self,
parent_group_path: &str,
link_name: &str,
target_path: &str,
) -> IoResult<()> {
if link_name.is_empty() || link_name.contains('/') {
return Err(crate::io::IoError::InvalidState(format!(
"hard link name '{link_name}' must be a non-empty leaf name"
)));
}
self.reject_external_traversal(target_path)?;
self.reject_external_traversal(&format!(
"{}/{link_name}",
parent_group_path.trim_end_matches('/')
))?;
let _create = self.create_lock.lock();
let parent_group_path = self.canonical_group_path(parent_group_path);
let parent_group_path = parent_group_path.as_str();
let parent = if parent_group_path == "/" {
None
} else {
Some(
self.group_refs()
.iter()
.position(|g| {
let gg = g.lock();
gg.name == parent_group_path && !gg.deleted
})
.ok_or_else(|| {
crate::io::IoError::NotFound(format!(
"parent group '{parent_group_path}' not found"
))
})?,
)
};
let target_rel = self.canonical_dataset_path(target_path.trim_matches('/'));
let target_rel = target_rel.as_str();
if target_rel.is_empty() {
return Err(crate::io::IoError::InvalidState(
"cannot hard-link the root group".into(),
));
}
let target = self.resolve_object(target_rel).ok_or_else(|| {
crate::io::IoError::NotFound(format!("hard link target '{target_path}' not found"))
})?;
self.ensure_name_free(&self.link_full_path(parent, link_name))?;
self.hard_links.lock().push(HardLink {
parent,
name: link_name.to_string(),
target,
creation_seq: self.take_creation_seq(),
});
self.register_name(&self.link_full_path(parent, link_name), NameHit::HardLink);
Ok(())
}
fn hard_link_emitted(&self, link: &HardLink) -> bool {
let parent_ok = self.parent_alive(link.parent);
let target_ok = match link.target {
HardLinkTarget::Dataset(i) => !self.ds(i).lock().deleted,
HardLinkTarget::Group(i) => !self.grp(i).lock().deleted,
};
parent_ok && target_ok
}
fn link_full_path(&self, parent: Option<usize>, name: &str) -> String {
match parent {
None => name.to_string(),
Some(pi) => format!(
"{}/{name}",
self.grp(pi).lock().name.trim_start_matches('/')
),
}
}
fn hard_link_full_path(&self, link: &HardLink) -> String {
self.link_full_path(link.parent, &link.name)
}
fn symbolic_link_emitted(&self, link: &SymbolicLink) -> bool {
self.parent_alive(link.parent)
}
fn parent_alive(&self, parent: Option<usize>) -> bool {
match parent {
None => true,
Some(pi) => !self.grp(pi).lock().deleted,
}
}
fn symbolic_link_full_path(&self, link: &SymbolicLink) -> String {
self.link_full_path(link.parent, &link.name)
}
pub fn create_symbolic_link(
&self,
parent_group_path: &str,
link_name: &str,
target: LinkTarget,
) -> IoResult<()> {
if link_name.is_empty() || link_name.contains('/') {
return Err(crate::io::IoError::InvalidState(format!(
"link name '{link_name}' must be a non-empty leaf name"
)));
}
let target = match target {
LinkTarget::External { file, path } => {
if file.is_empty() || path.is_empty() {
return Err(crate::io::IoError::InvalidState(
"an external link needs both a file name and an object path".into(),
));
}
LinkTarget::External {
file,
path: crate::format::messages::link::normalize_object_path(&path),
}
}
other => other,
};
self.reject_external_traversal(&format!(
"{}/{link_name}",
parent_group_path.trim_end_matches('/')
))?;
let _create = self.create_lock.lock();
let parent_group_path = self.canonical_group_path(parent_group_path);
let parent_group_path = parent_group_path.as_str();
let parent = if parent_group_path == "/" {
None
} else {
Some(
self.group_refs()
.iter()
.position(|g| {
let gg = g.lock();
gg.name == parent_group_path && !gg.deleted
})
.ok_or_else(|| {
crate::io::IoError::NotFound(format!(
"parent group '{parent_group_path}' not found"
))
})?,
)
};
self.ensure_name_free(&self.link_full_path(parent, link_name))?;
self.symbolic_links.lock().push(SymbolicLink {
parent,
name: link_name.to_string(),
target,
creation_seq: self.take_creation_seq(),
});
self.register_name(
&self.link_full_path(parent, link_name),
NameHit::SymbolicLink,
);
Ok(())
}
pub(crate) fn committed_datatypes_vec(&self) -> Vec<CommittedDatatype> {
self.committed_datatypes.lock().clone()
}
pub(crate) fn committed_datatype_names(&self) -> Vec<String> {
let mut out: Vec<String> = self
.committed_datatypes_vec()
.iter()
.filter(|c| self.parent_alive(c.parent))
.map(|c| c.name.clone())
.collect();
out.extend(
self.preserved_links
.lock()
.iter()
.filter(|l| l.kind == PreservedKind::NamedDatatype)
.map(|l| self.preserved_link_full_path(l)),
);
out
}
pub fn commit_datatype(&self, name: &str, datatype: DatatypeMessage) -> IoResult<usize> {
let create = self.begin_create(name.trim_start_matches('/'))?;
let entry = CommittedDatatype {
name: create.name.clone(),
parent: create.parent,
datatype,
creation_seq: self.take_creation_seq(),
times: self.created_object_times(),
obj_header_addr: 0,
};
let name = entry.name.clone();
let idx = {
let mut reg = self.committed_datatypes.lock();
let idx = reg.len();
reg.push(entry);
idx
};
self.register_name(&name, NameHit::Datatype(idx));
Ok(idx)
}
pub(crate) fn committed_datatype_for_share(
&self,
name: &str,
) -> IoResult<(usize, DatatypeMessage)> {
let name = self.canonical_dataset_path(name.trim_start_matches('/'));
let all = self.committed_datatypes_vec();
all.iter()
.position(|c| self.parent_alive(c.parent) && c.name == name)
.map(|i| (i, all[i].datatype.clone()))
.ok_or_else(|| {
crate::io::IoError::NotFound(format!("no committed datatype named '{name}'"))
})
}
pub(crate) fn share_committed_type(&self, dataset: usize, committed: usize) {
debug_assert!(committed < self.committed_datatypes.lock().len());
self.ds(dataset).lock().committed_type = Some(CommittedTypeRef::Session(committed));
}
fn committed_datatype_refcount(&self, index: usize) -> u32 {
let linked = {
let parent = self.committed_datatypes.lock()[index].parent;
u32::from(self.parent_alive(parent))
};
let shares = self
.dataset_refs()
.iter()
.filter(|d| {
let m = d.lock();
!m.deleted && m.committed_type == Some(CommittedTypeRef::Session(index))
})
.count() as u32;
linked + shares
}
fn push_committed_datatypes(&self, links: &mut Vec<(u64, LinkMessage)>, parent: Option<usize>) {
for cd in self.committed_datatypes_vec() {
if cd.parent != parent {
continue;
}
let leaf = cd.name.rsplit('/').next().unwrap_or(&cd.name);
links.push((cd.creation_seq, LinkMessage::hard(leaf, cd.obj_header_addr)));
}
}
pub(crate) fn canonical_group_path(&self, path: &str) -> String {
let mut path = path.to_string();
for _ in 0..64 {
let mut best: Option<(usize, usize)> = None; for l in self.hard_links_vec() {
let HardLinkTarget::Group(gi) = l.target else {
continue;
};
if !self.hard_link_emitted(&l) {
continue;
}
let lp = format!("/{}", self.hard_link_full_path(&l));
let covers = path == lp || path.starts_with(&format!("{lp}/"));
if covers && best.is_none_or(|(len, _)| lp.len() > len) {
best = Some((lp.len(), gi));
}
}
let Some((len, gi)) = best else { break };
let target_name = self.grp(gi).lock().name.clone();
path = format!("{}{}", target_name, &path[len..]);
}
path
}
fn canonical_dataset_path(&self, name: &str) -> String {
self.canonical_group_path(&format!("/{name}"))
.trim_start_matches('/')
.to_string()
}
fn object_link_count(&self, target: HardLinkTarget) -> u32 {
let same = |a: HardLinkTarget, b: HardLinkTarget| -> bool {
matches!(
(a, b),
(HardLinkTarget::Dataset(x), HardLinkTarget::Dataset(y))
| (HardLinkTarget::Group(x), HardLinkTarget::Group(y))
if x == y
)
};
1 + self
.hard_links_vec()
.iter()
.filter(|l| self.hard_link_emitted(l) && same(l.target, target))
.count() as u32
}
pub(crate) fn resolve_object(&self, path: &str) -> Option<HardLinkTarget> {
if let Some(idx) = self.dataset_refs().iter().position(|d| {
let g = d.lock();
!g.deleted && g.name.trim_start_matches('/') == path
}) {
return Some(HardLinkTarget::Dataset(idx));
}
if let Some(idx) = self.group_refs().iter().position(|g| {
let gg = g.lock();
!gg.deleted && gg.name.trim_start_matches('/') == path
}) {
return Some(HardLinkTarget::Group(idx));
}
self.hard_links_vec().iter().find_map(|l| {
(self.hard_link_emitted(l) && self.hard_link_full_path(l) == path).then_some(l.target)
})
}
fn local_element_block(&self, index: usize, what: &str) -> IoResult<u64> {
let ds = self.ds(index);
let m = ds.lock();
match m.contiguous_target() {
Some(ContiguousTarget::Local(addr)) => Ok(addr),
Some(ContiguousTarget::External { .. }) => {
Err(crate::io::IoError::InvalidState(format!(
"{what} are stamped into the dataset's own contiguous block, and \
dataset '{}' has none: its raw data lives in external files",
m.name
)))
}
Some(ContiguousTarget::Virtual) => Err(crate::io::IoError::InvalidState(format!(
"{what} are stamped into the dataset's own contiguous block, and \
dataset '{}' has none: it is virtual, and its elements come from \
the source datasets its mappings name",
m.name
))),
None => Err(crate::io::IoError::InvalidState(format!(
"{what} are stamped into contiguous storage; create the dataset \
without chunking"
))),
}
}
pub fn write_object_references(
&self,
index: usize,
start: u64,
paths: &[&str],
) -> IoResult<()> {
let elements = {
let ds = self.ds(index);
let m = ds.lock();
match &m.datatype {
DatatypeMessage::Reference {
kind: ReferenceKind::Object1 | ReferenceKind::Object2,
..
} => {}
other => {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{}' has datatype {other}, not an object reference",
m.name
)))
}
}
m.dataspace
.dims
.iter()
.fold(1u64, |a, &d| a.saturating_mul(d))
};
self.local_element_block(index, "object references")?;
let end = start.saturating_add(paths.len() as u64);
if end > elements {
return Err(crate::io::IoError::InvalidState(format!(
"elements {start}..{end} are outside the dataset's {elements}"
)));
}
for path in paths {
self.object_reference_target(path)?;
}
let mut pending = self.pending_object_references.lock();
for (i, path) in paths.iter().enumerate() {
pending.push(PendingObjectReference {
dataset: index,
element: start + i as u64,
target: (*path).to_string(),
});
}
Ok(())
}
fn emit_refcount(&self, header: &mut ObjectHeader, rc: u32, format: ObjectFormat) {
if rc > 1 && format == ObjectFormat::Modern {
header.add_message(MSG_OBJ_REF_COUNT, MSG_FLAG_DONTSHARE, encode_refcount(rc));
}
}
fn encode_header_in(
&self,
header: &ObjectHeader,
rc: u32,
format: ObjectFormat,
placement: &HeaderPlacement,
) -> IoResult<Vec<(u64, Vec<u8>)>> {
let plan = if placement.kept {
header.plan_chunks_in(format, placement.size, &self.ctx)?
} else {
header.plan_chunks(format, self.chunk0_capacity(header, format), &self.ctx)?
};
let continuation_addr = match placement.continuation {
Some((addr, _)) => addr,
None => placement.addr + plan.chunk0_size as u64,
};
let (mut chunk0, continuation) =
header.encode_chunked(&plan, format, &self.ctx, continuation_addr, rc)?;
match (placement.continuation, continuation) {
(Some((addr, _)), Some(image)) => Ok(vec![(placement.addr, chunk0), (addr, image)]),
(None, Some(image)) => {
chunk0.extend_from_slice(&image);
Ok(vec![(placement.addr, chunk0)])
}
(None, None) => Ok(vec![(placement.addr, chunk0)]),
(Some((addr, size)), None) => Err(crate::io::IoError::InvalidState(format!(
"an object header was placed with a {size}-byte continuation block at \
{addr:#x} that it no longer needs; a message in it changed length \
once the addresses it names were known"
))),
}
}
fn place_header(
&mut self,
header: &ObjectHeader,
format: ObjectFormat,
kept: Option<(u64, u64)>,
) -> IoResult<HeaderPlacement> {
if let Some((addr, len)) = kept {
let plan = usize::try_from(len)
.ok()
.and_then(|len| header.plan_chunks_in(format, len, &self.ctx).ok());
match plan {
Some(plan) => {
let continuation = (plan.continuation_size > 0).then(|| {
let size = plan.continuation_size;
let addr = self
.allocator
.allocate(size as u64, FreeSpaceClass::Metadata);
(addr, size)
});
return Ok(HeaderPlacement {
addr,
size: len as usize,
kept: true,
continuation,
});
}
None if !self.swmr_active => {
self.allocator.free(addr, len, FreeSpaceClass::Metadata);
}
None => {}
}
}
let plan = header.plan_chunks(format, self.chunk0_capacity(header, format), &self.ctx)?;
let size = plan.chunk0_size + plan.continuation_size;
let addr = self
.allocator
.allocate(size as u64, FreeSpaceClass::Metadata);
Ok(HeaderPlacement::fresh(addr, size))
}
fn chunk0_capacity(&self, header: &ObjectHeader, format: ObjectFormat) -> usize {
if format == ObjectFormat::Legacy {
return usize::MAX;
}
let envelope = header.message_envelope_size();
let sized = |msg_type: u8| {
header
.messages
.iter()
.find(|m| m.msg_type == msg_type)
.map(|m| envelope + m.data.len())
};
let Some(link_info) = sized(MSG_LINK_INFO) else {
return usize::MAX;
};
let link = envelope + 1 + 1 + 1 + EST_LINK_NAME_LEN + self.ctx.sizeof_addr as usize;
link_info + sized(MSG_GROUP_INFO).unwrap_or(0) + EST_LINK_COUNT * link
}
fn object_reference_target(&self, path: &str) -> IoResult<Option<HardLinkTarget>> {
let rel = self.canonical_dataset_path(path.trim_matches('/'));
if rel.is_empty() {
return Ok(None);
}
self.resolve_object(&rel)
.map(Some)
.ok_or_else(|| crate::io::IoError::NotFound(format!("reference target '{path}'")))
}
fn object_reference_address(&self, path: &str) -> IoResult<u64> {
Ok(match self.object_reference_target(path)? {
Some(HardLinkTarget::Dataset(i)) => self.ds(i).lock().obj_header_addr,
Some(HardLinkTarget::Group(i)) => self.grp(i).lock().obj_header_addr,
None => self.root_group_addr.unwrap_or(0),
})
}
fn object_attributes(&self, scope: AttrScope) -> IoResult<Vec<AttributeEntry>> {
let mut attrs = match scope {
AttrScope::Root => self.root_attributes.lock().clone(),
AttrScope::Group(gi) => self.grp(gi).lock().attributes.clone(),
AttrScope::Dataset(i) => self.ds(i).lock().attributes.clone(),
};
let values: Vec<(String, Vec<String>, usize)> = self
.attribute_references
.lock()
.iter()
.filter(|r| r.scope == scope)
.map(|r| (r.name.clone(), r.targets.clone(), r.stride))
.collect();
let width = self.ctx.sizeof_addr as usize;
for (name, targets, stride) in values {
let Some(pos) = attrs.iter().position(|a| a.name() == name) else {
continue;
};
let Some(msg) = attrs[pos].readable() else {
continue;
};
let mut msg = msg.clone();
for (i, target) in targets.iter().enumerate() {
let at = i * stride;
let held = msg.data.len();
let slot = msg.data.get_mut(at..at + width).ok_or_else(|| {
crate::io::IoError::InvalidState(format!(
"attribute '{name}' holds {held} bytes, too few for reference {i} at {at}"
))
})?;
slot.copy_from_slice(
&self.object_reference_address(target)?.to_le_bytes()[..width],
);
}
attrs[pos] = AttributeEntry::from(msg).with_creation_index(attrs[pos].creation_index());
}
Ok(attrs)
}
fn attr_scope(&self, target: AttrTarget<'_>) -> IoResult<AttrScope> {
match target {
AttrTarget::Root => Ok(AttrScope::Root),
AttrTarget::Group(path) => {
let path = self.canonical_group_path(path);
self.group_refs()
.iter()
.position(|g| {
let gg = g.lock();
gg.name == path && !gg.deleted
})
.map(AttrScope::Group)
.ok_or_else(|| {
crate::io::IoError::NotFound(format!("group '{path}' not found"))
})
}
AttrTarget::Dataset(index) => {
let count = self.dataset_count();
if index >= count {
return Err(crate::io::IoError::InvalidState(format!(
"dataset index {index} out of range (have {count})"
)));
}
Ok(AttrScope::Dataset(index))
}
}
}
fn forget_attribute_reference(&self, scope: AttrScope, name: &str) {
self.attribute_references
.lock()
.retain(|r| !(r.scope == scope && r.name == name));
}
fn write_object_reference_values(&mut self) -> IoResult<()> {
let pending: Vec<(usize, u64, String)> = self
.pending_object_references
.lock()
.iter()
.map(|p| (p.dataset, p.element, p.target.clone()))
.collect();
for (dataset, element, target) in &pending {
let addr = match self.object_reference_target(target)? {
Some(HardLinkTarget::Dataset(i)) => self.ds(i).lock().obj_header_addr,
Some(HardLinkTarget::Group(i)) => self.grp(i).lock().obj_header_addr,
None => self.root_group_addr.ok_or_else(|| {
crate::io::IoError::InvalidState(
"root group header address is not assigned yet".into(),
)
})?,
};
let (kind, width) = {
let ds = self.ds(*dataset);
let m = ds.lock();
let DatatypeMessage::Reference { kind, size } = &m.datatype else {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{}' is no longer a reference dataset",
m.name
)));
};
(*kind, *size as usize)
};
let image = match kind {
ReferenceKind::Object1 => ReferenceElementImage::Legacy(addr),
ReferenceKind::Object2 => ReferenceElementImage::Inline(addr),
other => {
return Err(crate::io::IoError::InvalidState(format!(
"dataset {dataset} now holds {other:?} elements, not object references"
)))
}
};
let image = encode_reference_element(&image, width, &self.ctx)?;
let data_addr = self.local_element_block(*dataset, "object references")?;
let at = data_addr + element * width as u64;
self.handle.write_at(at, &image)?;
}
Ok(())
}
pub fn write_region_references(
&self,
index: usize,
start: u64,
targets: &[(&str, Selection)],
) -> IoResult<()> {
let elements = {
let ds = self.ds(index);
let m = ds.lock();
match &m.datatype {
DatatypeMessage::Reference {
kind: ReferenceKind::DatasetRegion1,
..
} => {}
other => {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{}' has datatype {other}, not a region reference",
m.name
)))
}
}
m.dataspace
.dims
.iter()
.fold(1u64, |a, &d| a.saturating_mul(d))
};
let data_addr = self.local_element_block(index, "region references")?;
let end = start.saturating_add(targets.len() as u64);
if end > elements {
return Err(crate::io::IoError::InvalidState(format!(
"elements {start}..{end} are outside the dataset's {elements}"
)));
}
let sa = self.ctx.sizeof_addr as usize;
let mut blobs = Vec::with_capacity(targets.len());
for (path, selection) in targets {
let target = self.region_reference_target(path)?;
let dims = self.ds(target).lock().dataspace.dims.clone();
validate_region_selection(selection, &dims, path)?;
let mut blob = vec![0u8; sa];
blob.extend_from_slice(&selection.encode()?);
blobs.push(blob);
}
let items: Vec<&[u8]> = blobs.iter().map(Vec::as_slice).collect();
let placements = self.insert_vlen_objects(&items)?;
let width = (sa + 4) as u64;
let mut pending = self.pending_heap_references.lock();
for (i, &(collection, obj_index)) in placements.iter().enumerate() {
let mut elem = Vec::with_capacity(width as usize);
elem.extend_from_slice(&collection.to_le_bytes()[..sa]);
elem.extend_from_slice(&u32::from(obj_index).to_le_bytes());
self.handle
.write_at(data_addr + (start + i as u64) * width, &elem)?;
pending.push(PendingHeapReference {
collection,
index: obj_index,
token_offset: 0,
target: PendingHeapTarget::Dataset(targets[i].0.to_string()),
});
}
Ok(())
}
pub fn write_revised_references(
&self,
index: usize,
start: u64,
targets: &[(&str, ReferenceTarget)],
) -> IoResult<()> {
let (width, elements) = {
let ds = self.ds(index);
let m = ds.lock();
match &m.datatype {
DatatypeMessage::Reference {
kind: ReferenceKind::Object2,
size,
} => (
*size as u64,
m.dataspace
.dims
.iter()
.fold(1u64, |a, &d| a.saturating_mul(d)),
),
other => {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{}' has datatype {other}, not the 1.12 H5T_STD_REF",
m.name
)))
}
}
};
let data_addr = self.local_element_block(index, "references")?;
let end = start.saturating_add(targets.len() as u64);
if end > elements {
return Err(crate::io::IoError::InvalidState(format!(
"elements {start}..{end} are outside the dataset's {elements}"
)));
}
let mut blobs: Vec<(u64, ReferenceKind, PendingHeapTarget, Vec<u8>)> = Vec::new();
let mut inline: Vec<(u64, String)> = Vec::new();
for (i, (path, target)) in targets.iter().enumerate() {
let element = start + i as u64;
let mut extent_rank = 0;
let (kind, pending) = match target {
ReferenceTarget::Object => {
self.object_reference_target(path)?;
inline.push((element, (*path).to_string()));
continue;
}
ReferenceTarget::Region(selection) => {
let ds = self.region_reference_target(path)?;
let dims = self.ds(ds).lock().dataspace.dims.clone();
validate_region_selection(selection, &dims, path)?;
extent_rank = dims.len();
(
ReferenceKind::DatasetRegion2,
PendingHeapTarget::Dataset((*path).to_string()),
)
}
ReferenceTarget::Attribute(name) => {
let scope = match self.object_reference_target(path)? {
Some(HardLinkTarget::Dataset(i)) => AttrScope::Dataset(i),
Some(HardLinkTarget::Group(i)) => AttrScope::Group(i),
None => AttrScope::Root,
};
if !self
.object_attributes(scope)?
.iter()
.any(|a| a.name() == name)
{
return Err(crate::io::IoError::NotFound(format!(
"attribute '{name}' of reference target '{path}'"
)));
}
(
ReferenceKind::Attr,
PendingHeapTarget::Object((*path).to_string()),
)
}
};
blobs.push((
element,
kind,
pending,
encode_revised_blob(0, target, extent_rank, &self.ctx)?,
));
}
let items: Vec<&[u8]> = blobs.iter().map(|(_, _, _, b)| b.as_slice()).collect();
let placements = self.insert_vlen_objects(&items)?;
let mut pending = self.pending_heap_references.lock();
for ((element, kind, target, blob), &(collection, obj_index)) in
blobs.iter().zip(&placements)
{
let image = encode_reference_element(
&ReferenceElementImage::Blob {
kind: *kind,
size: blob.len() as u32,
collection,
index: u32::from(obj_index),
},
width as usize,
&self.ctx,
)?;
self.handle.write_at(data_addr + element * width, &image)?;
pending.push(PendingHeapReference {
collection,
index: obj_index,
token_offset: REVISED_BLOB_TOKEN_OFFSET,
target: target.clone(),
});
}
drop(pending);
let mut pending = self.pending_object_references.lock();
for (element, path) in inline {
pending.push(PendingObjectReference {
dataset: index,
element,
target: path,
});
}
Ok(())
}
fn region_reference_target(&self, path: &str) -> IoResult<usize> {
match self.object_reference_target(path)? {
Some(HardLinkTarget::Dataset(i)) => Ok(i),
_ => Err(crate::io::IoError::InvalidState(format!(
"region reference target '{path}' is not a dataset"
))),
}
}
fn write_heap_reference_values(&mut self) -> IoResult<()> {
use crate::format::global_heap::GlobalHeapCollection;
let pending: Vec<(u64, u16, usize, PendingHeapTarget)> = self
.pending_heap_references
.lock()
.iter()
.map(|p| (p.collection, p.index, p.token_offset, p.target.clone()))
.collect();
if pending.is_empty() {
return Ok(());
}
let sa = self.ctx.sizeof_addr as usize;
let mut per_collection: std::collections::BTreeMap<u64, Vec<(u16, usize, u64)>> =
Default::default();
for (collection, index, token_offset, target) in &pending {
let addr = match target {
PendingHeapTarget::Dataset(path) => {
let ds = self.region_reference_target(path)?;
self.ds(ds).lock().obj_header_addr
}
PendingHeapTarget::Object(path) => self.object_reference_address(path)?,
};
per_collection
.entry(*collection)
.or_default()
.push((*index, *token_offset, addr));
}
for (collection, patches) in per_collection {
let mut image = self.handle.read_at_most(collection, 4096)?;
let declared = GlobalHeapCollection::decode_size(&image, &self.ctx)?;
if declared > image.len() {
image = self.handle.read_at(collection, declared)?;
}
let (mut gcol, _) = GlobalHeapCollection::decode(&image[..declared], &self.ctx)?;
for (index, token_offset, addr) in patches {
let token = gcol
.objects
.iter_mut()
.find(|o| o.index == index)
.and_then(|o| o.data.get_mut(token_offset..token_offset + sa))
.ok_or_else(|| {
crate::io::IoError::InvalidState(format!(
"object {index} of global heap collection {collection:#x} is no \
longer the reference written into it"
))
})?;
token.copy_from_slice(&addr.to_le_bytes()[..sa]);
}
let rewritten = gcol.encode_at_size(&self.ctx, declared)?;
self.handle.write_at(collection, &rewritten)?;
}
Ok(())
}
fn write_reference_values(&mut self) -> IoResult<()> {
self.write_object_reference_values()?;
self.write_heap_reference_values()
}
fn push_hard_links(&self, links: &mut Vec<(u64, LinkMessage)>, parent: Option<usize>) {
for link in self.hard_links_vec() {
if link.parent != parent || !self.hard_link_emitted(&link) {
continue;
}
let addr = match link.target {
HardLinkTarget::Dataset(i) => self.ds(i).lock().obj_header_addr,
HardLinkTarget::Group(i) => self.grp(i).lock().obj_header_addr,
};
links.push((link.creation_seq, LinkMessage::hard(&link.name, addr)));
}
}
fn push_symbolic_links(&self, links: &mut Vec<(u64, LinkMessage)>, parent: Option<usize>) {
for link in self.symbolic_links_vec() {
if link.parent != parent || !self.symbolic_link_emitted(&link) {
continue;
}
links.push((
link.creation_seq,
LinkMessage {
name: link.name.clone(),
target: link.target.clone(),
creation_order: None,
cset: CharacterSet::for_name(&link.name),
},
));
}
}
pub(crate) fn reject_external_traversal(&self, path: &str) -> IoResult<()> {
let path = path.trim_start_matches('/');
let crossing = self.preserved_link_paths().into_iter().find(|(p, class)| {
matches!(class, crate::io::reader::LinkClass::External { .. })
&& (path == p || path.starts_with(&format!("{p}/")))
});
match crossing {
None => Ok(()),
Some((link, crate::io::reader::LinkClass::External { file, path: target })) => {
Err(crate::io::IoError::Unsupported(format!(
"'{path}' resolves through the external link '{link}' to '{target}' in \
'{file}'; this writer carries external links through a rewrite but does \
not open the file they name"
)))
}
Some(_) => Ok(()),
}
}
pub(crate) fn open_dataset_index(&self, name: &str) -> IoResult<usize> {
self.reject_external_traversal(name)?;
self.reject_preserved_object(name)?;
self.dataset_index(name)
.ok_or_else(|| crate::io::IoError::NotFound(name.to_string()))
}
pub(crate) fn reject_preserved_object(&self, path: &str) -> IoResult<()> {
let path = path.trim_start_matches('/');
let objects: Vec<(String, String)> = {
let preserved = self.preserved_links.lock();
preserved
.iter()
.filter_map(|l| {
l.reason
.as_ref()
.map(|why| (self.preserved_link_full_path(l), why.clone()))
})
.collect()
};
match objects
.into_iter()
.find(|(full, _)| path == full || path.starts_with(&format!("{full}/")))
{
None => Ok(()),
Some((link, why)) => Err(crate::io::IoError::Unsupported(format!(
"'{path}' is, or is inside, the object '{link}', which this file's reopen \
kept exactly as it found it because {why}"
))),
}
}
pub(crate) fn path_link_classes(&self) -> Vec<(String, crate::io::reader::LinkClass)> {
let mut out: Vec<(String, crate::io::reader::LinkClass)> = self
.symbolic_links_vec()
.iter()
.filter(|l| self.symbolic_link_emitted(l))
.map(|l| {
(
self.symbolic_link_full_path(l),
crate::io::reader::LinkClass::from_target(&l.target),
)
})
.collect();
out.extend(self.preserved_link_paths());
out
}
pub(crate) fn preserved_link_paths(&self) -> Vec<(String, crate::io::reader::LinkClass)> {
self.preserved_links
.lock()
.iter()
.map(|l| (self.preserved_link_full_path(l), l.class.clone()))
.collect()
}
fn preserved_link_full_path(&self, link: &PreservedLink) -> String {
match link.parent {
None => link.name.clone(),
Some(gi) => {
let group = self.grp(gi).lock().name.clone();
format!("{}/{}", group.trim_start_matches('/'), link.name)
}
}
}
fn group_links(&self, scope: LinkScope, order: CreationOrder) -> Vec<LinkMessage> {
let mut links: Vec<(u64, LinkMessage)> = Vec::new();
match scope {
LinkScope::Root => {
let mut datasets_in_subgroups: std::collections::HashSet<usize> =
std::collections::HashSet::new();
for grp in self.group_refs() {
let g = grp.lock();
if g.deleted {
continue;
}
datasets_in_subgroups.extend(g.child_datasets.iter().copied());
}
for (i, ds) in self.dataset_refs().into_iter().enumerate() {
let m = ds.lock();
if m.deleted || datasets_in_subgroups.contains(&i) {
continue;
}
let leaf_name = m.name.rsplit('/').next().unwrap_or(&m.name);
links.push((
m.creation_seq,
LinkMessage::hard(leaf_name, m.obj_header_addr),
));
}
for grp in self.group_refs() {
let g = grp.lock();
if g.deleted || g.parent.is_some() {
continue;
}
let leaf_name = g.name.rsplit('/').next().unwrap_or(&g.name);
links.push((
g.creation_seq,
LinkMessage::hard(leaf_name, g.obj_header_addr),
));
}
self.push_hard_links(&mut links, None);
self.push_symbolic_links(&mut links, None);
self.push_committed_datatypes(&mut links, None);
}
LinkScope::Group(group_idx) => {
let (child_datasets, child_groups) = {
let grp = self.grp(group_idx);
let g = grp.lock();
(g.child_datasets.clone(), g.child_groups.clone())
};
for ds_idx in child_datasets {
let ds = self.ds(ds_idx);
let m = ds.lock();
if m.deleted {
continue;
}
let leaf_name = m.name.rsplit('/').next().unwrap_or(&m.name);
links.push((
m.creation_seq,
LinkMessage::hard(leaf_name, m.obj_header_addr),
));
}
for child_idx in child_groups {
let child_grp = self.grp(child_idx);
let g = child_grp.lock();
if g.deleted {
continue;
}
let leaf_name = g.name.rsplit('/').next().unwrap_or(&g.name);
links.push((
g.creation_seq,
LinkMessage::hard(leaf_name, g.obj_header_addr),
));
}
self.push_hard_links(&mut links, Some(group_idx));
self.push_symbolic_links(&mut links, Some(group_idx));
self.push_committed_datatypes(&mut links, Some(group_idx));
}
}
links.sort_by_key(|(seq, _)| *seq);
links
.into_iter()
.enumerate()
.map(|(rank, (_, link))| {
if order.is_tracked() {
link.with_creation_order(rank as i64)
} else {
link
}
})
.collect()
}
fn links_need_dense(&self, links: &[LinkMessage]) -> bool {
links.len() > MAX_COMPACT_LINKS
|| links
.iter()
.any(|l| l.encode(&self.ctx).len() > MAX_MESSAGE_SIZE)
}
fn emit_links(
&self,
header: &mut ObjectHeader,
scope: LinkScope,
links: &[LinkMessage],
order: CreationOrder,
) {
if self.uses_symbol_table(scope, order) {
let stab = self
.symbol_tables
.written
.lock()
.get(&scope)
.copied()
.unwrap_or(Stab {
btree_addr: UNDEF_ADDR,
heap_addr: UNDEF_ADDR,
});
header.add_message(MSG_SYMBOL_TABLE, 0x00, stab.encode(&self.ctx));
return;
}
let preserved = self.preserved_links_for(scope);
let dense = preserved.is_empty() && self.links_need_dense(links);
let link_info = self.dense_links.lock().get(&scope).cloned();
let link_info = link_info.unwrap_or_else(|| {
let mut info = LinkInfoMessage::compact();
if order.is_tracked() {
info.max_creation_order = Some(links.len() as u64);
}
if order.is_indexed() {
info.creation_order_btree_address = Some(UNDEF_ADDR);
}
info
});
header.add_message(MSG_LINK_INFO, 0x00, link_info.encode(&self.ctx));
header.add_message(
MSG_GROUP_INFO,
MSG_FLAG_CONSTANT,
GroupInfoMessage::default().encode(),
);
if dense {
return;
}
for link in links {
header.add_message(MSG_LINK, 0x00, link.encode(&self.ctx));
}
for encoded in preserved {
header.add_message(MSG_LINK, 0x00, encoded);
}
}
fn preserved_links_for(&self, scope: LinkScope) -> Vec<Vec<u8>> {
let parent = match scope {
LinkScope::Root => None,
LinkScope::Group(i) => Some(i),
};
self.preserved_links
.lock()
.iter()
.filter(|l| l.parent == parent)
.map(|l| l.encoded.clone())
.collect()
}
fn prepare_dense_links(&self) -> IoResult<()> {
let mut scopes: Vec<(LinkScope, Vec<LinkMessage>, CreationOrder)> = Vec::new();
for gi in 0..self.group_count() {
let (deleted, order) = {
let grp = self.grp(gi);
let g = grp.lock();
(g.deleted, g.track_order.links)
};
if deleted || self.uses_symbol_table(LinkScope::Group(gi), order) {
continue;
}
self.release_superseded_dense_links(LinkScope::Group(gi))?;
let links = self.group_links(LinkScope::Group(gi), order);
if self.links_need_dense(&links) {
scopes.push((LinkScope::Group(gi), links, order));
}
}
let root_order = self.root_track_order.links;
if !self.uses_symbol_table(LinkScope::Root, root_order) {
self.release_superseded_dense_links(LinkScope::Root)?;
let root_links = self.group_links(LinkScope::Root, root_order);
if self.links_need_dense(&root_links) {
scopes.push((LinkScope::Root, root_links, root_order));
}
}
for (scope, links, order) in scopes {
if self.dense_links.lock().contains_key(&scope) {
continue;
}
let dense = build_dense_links(&links, &self.ctx, order, &mut |len| {
self.allocator.allocate(len, FreeSpaceClass::Metadata)
})?;
for block in &dense.blocks {
self.handle.write_at(block.addr, &block.image)?;
}
self.dense_links.lock().insert(scope, dense.linfo);
}
Ok(())
}
fn prepare_link_storage(&self) -> IoResult<()> {
self.prepare_dense_links()?;
self.prepare_symbol_tables()
}
fn prepare_symbol_tables(&self) -> IoResult<()> {
let mut scopes: Vec<(usize, LinkScope, CreationOrder)> = Vec::new();
for gi in 0..self.group_count() {
let (deleted, order, mut parent) = {
let grp = self.grp(gi);
let g = grp.lock();
(g.deleted, g.track_order.links, g.parent)
};
if deleted || !self.uses_symbol_table(LinkScope::Group(gi), order) {
continue;
}
let mut depth = 1usize;
while let Some(p) = parent {
depth += 1;
parent = self.grp(p).lock().parent;
}
scopes.push((depth, LinkScope::Group(gi), order));
}
scopes.sort_by_key(|&(depth, ..)| std::cmp::Reverse(depth));
let root_order = self.root_track_order.links;
if self.uses_symbol_table(LinkScope::Root, root_order) {
scopes.push((0, LinkScope::Root, root_order));
}
let meta = self.stab_meta();
for (_, scope, order) in scopes {
let superseded = self.symbol_tables.superseded.lock().remove(&scope);
if let Some(extents) = superseded {
free_stab(&self.allocator, &extents);
}
let links = self.stab_links_for(scope, order)?;
let stab = write_stab(&self.handle, &self.allocator, &meta, &links)?;
self.symbol_tables.written.lock().insert(scope, stab);
}
Ok(())
}
fn stab_meta(&self) -> FileMeta {
FileMeta {
ctx: self.ctx,
btree: self.btree_v1_config(),
sohm: None,
}
}
fn stab_links_for(&self, scope: LinkScope, order: CreationOrder) -> IoResult<Vec<StabLink>> {
let groups = self.group_header_scopes();
let mut out = Vec::new();
for link in self.group_links(scope, order) {
out.push(self.stab_link(&link, &groups)?);
}
for encoded in self.preserved_links_for(scope) {
let (link, _) = LinkMessage::decode(&encoded, &self.ctx)?;
out.push(self.stab_link(&link, &groups)?);
}
Ok(out)
}
fn group_header_scopes(&self) -> HashMap<u64, LinkScope> {
let mut map = HashMap::new();
for gi in 0..self.group_count() {
let grp = self.grp(gi);
let g = grp.lock();
if !g.deleted {
map.insert(g.obj_header_addr, LinkScope::Group(gi));
}
}
map
}
fn stab_link(
&self,
link: &LinkMessage,
groups: &HashMap<u64, LinkScope>,
) -> IoResult<StabLink> {
let target = match &link.target {
LinkTarget::Hard { address } => {
let cached = groups
.get(address)
.and_then(|scope| self.symbol_tables.written.lock().get(scope).copied());
StabTarget::Hard {
addr: *address,
cached,
}
}
LinkTarget::Soft { target } => StabTarget::Soft {
value: target.clone(),
},
LinkTarget::External { .. } | LinkTarget::UserDefined { .. } => {
return Err(crate::io::IoError::InvalidState(format!(
"cannot store the link {:?} in a symbol table: it holds only \
hard and soft links, and this group was not converted to link \
messages the way `H5G_obj_insert` converts it",
link.name
)))
}
};
Ok(StabLink {
name: link.name.clone(),
target,
})
}
fn emit_attributes(
&self,
header: &mut ObjectHeader,
scope: AttrScope,
attributes: &[AttributeEntry],
order: CreationOrder,
format: ObjectFormat,
owner: ShareOwner,
) {
let order = self.header_attr_order(order);
header.set_attribute_creation_order(order);
if attributes.is_empty() {
return;
}
if format == ObjectFormat::Legacy {
for attr in attributes {
header.add_message(MSG_ATTRIBUTE, 0x00, self.encode_attribute(attr));
}
return;
}
let dense = self.attributes_need_dense(attributes, format);
let stored = self.dense_attributes.lock().get(&scope).cloned();
let ainfo = stored.unwrap_or_else(|| {
let mut ainfo = AttributeInfoMessage::compact();
if order.is_tracked() {
ainfo.max_creation_index = Some(next_creation_index(attributes));
}
if order.is_indexed() {
ainfo.creation_order_btree_address = Some(UNDEF_ADDR);
}
ainfo
});
header.add_message(MSG_ATTR_INFO, MSG_FLAG_DONTSHARE, ainfo.encode(&self.ctx));
if dense {
return;
}
for attr in attributes {
let (flags, body) = self.share_attribute(attr, format, owner);
header.add_message_indexed(
MSG_ATTRIBUTE,
flags,
body,
attr.creation_index().unwrap_or(0),
);
}
}
fn encode_attribute(&self, attr: &AttributeEntry) -> Vec<u8> {
attr.encode_for(&self.ctx, self.encoding_libver(), self.message_format())
}
fn share_attribute(
&self,
attr: &AttributeEntry,
format: ObjectFormat,
owner: ShareOwner,
) -> (u8, Vec<u8>) {
let libver = self.encoding_libver();
let Some(message) = attr.readable().filter(|_| format.attribute_version() >= 2) else {
return self.share_message(
owner,
MSG_ATTRIBUTE,
0x00,
attr.encode_for(&self.ctx, libver, format),
);
};
let datatype = message.datatype.encode_at(&self.ctx, libver);
let dataspace = message.dataspace.encode_for(&self.ctx, format);
let (dt_flags, dt_field) =
self.share_message(ShareOwner::Detached, MSG_DATATYPE, 0x00, datatype.clone());
let (ds_flags, ds_field) =
self.share_message(ShareOwner::Detached, MSG_DATASPACE, 0x00, dataspace.clone());
let mut attr_flags = 0u8;
if dt_flags & MSG_FLAG_SHARED != 0 {
attr_flags |= ATTR_FLAG_TYPE_SHARED;
}
if ds_flags & MSG_FLAG_SHARED != 0 {
attr_flags |= ATTR_FLAG_SPACE_SHARED;
}
let encoded = message.encode_with_fields(attr_flags, &dt_field, &ds_field);
let mut nested = Vec::new();
if attr_flags & ATTR_FLAG_TYPE_SHARED != 0 {
nested.push(NestedShare {
heap_id_at: encoded.datatype_at + SOHM_POINTER_HEAP_ID_AT,
target: (MSG_DATATYPE, datatype),
});
}
if attr_flags & ATTR_FLAG_SPACE_SHARED != 0 {
nested.push(NestedShare {
heap_id_at: encoded.dataspace_at + SOHM_POINTER_HEAP_ID_AT,
target: (MSG_DATASPACE, dataspace),
});
}
self.share_nesting_message(owner, MSG_ATTRIBUTE, 0x00, encoded.body, nested)
}
fn attributes_need_dense(&self, attributes: &[AttributeEntry], format: ObjectFormat) -> bool {
if format == ObjectFormat::Legacy {
return false;
}
attributes.len() > MAX_COMPACT_ATTRS
|| attributes
.iter()
.any(|a| self.encode_attribute(a).len() > MAX_MESSAGE_SIZE)
}
fn attribute_scopes(&self, datasets: &[usize]) -> Vec<(AttrScope, CreationOrder)> {
let order_of = |requested| self.header_attr_order(requested);
let mut scopes = vec![(AttrScope::Root, order_of(self.root_track_order.attrs))];
for gi in 0..self.group_count() {
if self.grp(gi).lock().deleted {
continue;
}
let order = self.grp(gi).lock().track_order.attrs;
scopes.push((AttrScope::Group(gi), order_of(order)));
}
for &i in datasets {
let order = self.ds(i).lock().track_attr_order;
scopes.push((AttrScope::Dataset(i), order_of(order)));
}
scopes
}
fn prepare_dense_attributes(&self, datasets: &[usize]) -> IoResult<()> {
for (scope, order) in self.attribute_scopes(datasets) {
self.release_superseded_dense_attrs(scope)?;
if self.dense_attributes.lock().contains_key(&scope) {
continue;
}
let attributes = self.object_attributes(scope)?;
if !self.attributes_need_dense(&attributes, self.attr_scope_format(scope)) {
continue;
}
let dense = build_dense_attributes(&attributes, &self.ctx, order, &mut |len| {
self.allocator.allocate(len, FreeSpaceClass::Metadata)
})?;
for block in &dense.blocks {
self.handle.write_at(block.addr, &block.image)?;
}
self.dense_attributes.lock().insert(scope, dense.ainfo);
}
Ok(())
}
fn attr_scope_format(&self, scope: AttrScope) -> ObjectFormat {
match scope {
AttrScope::Root => self.header_format(self.root_track_order),
AttrScope::Group(gi) => self.group_header_format(gi),
AttrScope::Dataset(i) => self.dataset_header_format(i),
}
}
fn dataset_datatype_shareable(&self, datatype: &DatatypeMessage, libver: LibverBound) -> bool {
!datatype.is_predefined() || datatype.is_relocatable() || libver >= LibverBound::V18
}
const fn shares_in_ohdr(msg_type: u8) -> bool {
matches!(
msg_type,
MSG_DATASPACE
| MSG_DATATYPE
| MSG_FILL_VALUE
| MSG_FILL_VALUE_OLD
| MSG_FILTER_PIPELINE
)
}
fn share_message(
&self,
owner: ShareOwner,
msg_type: u8,
flags: u8,
body: Vec<u8>,
) -> (u8, Vec<u8>) {
self.share_nesting_message(owner, msg_type, flags, body, Vec::new())
}
fn share_nesting_message(
&self,
owner: ShareOwner,
msg_type: u8,
flags: u8,
body: Vec<u8>,
nested: Vec<NestedShare>,
) -> (u8, Vec<u8>) {
let Some(sohm) = self.sohm.as_deref() else {
return (flags, body);
};
if flags & (MSG_FLAG_SHARED | MSG_FLAG_DONTSHARE) != 0 {
return (flags, body);
}
let Some(index) = sohm.index_for(msg_type, body.len()) else {
return (flags, body);
};
let ohdr = match owner {
ShareOwner::Header(addr) if Self::shares_in_ohdr(msg_type) => Some(addr),
_ => None,
};
let pointer = |id| {
(
flags | MSG_FLAG_SHARED,
SharedMessagePointer::encode_sohm(id),
)
};
match &mut *sohm.phase.lock() {
SohmPhase::Idle => (flags, body),
SohmPhase::Predict(first) => {
if ohdr.is_some() && first.insert((msg_type, body.clone())) {
return (flags | MSG_FLAG_SHAREABLE, body);
}
pointer([0u8; SOHM_HEAP_ID_LEN])
}
SohmPhase::Collect(collector) => {
let first = collector.record(index, msg_type, &body, &nested, ohdr);
if !first && !nested.is_empty() {
for share in &nested {
collector.release(share.target.0, &share.target.1);
}
}
if ohdr.is_some() && first {
return (flags | MSG_FLAG_SHAREABLE, body);
}
pointer([0u8; SOHM_HEAP_ID_LEN])
}
SohmPhase::Resolve { ids, first } => {
if ohdr.is_some() && first.insert((msg_type, body.clone())) {
return (flags | MSG_FLAG_SHAREABLE, body);
}
let key = (msg_type, body);
match ids.get(&key) {
Some(&id) => pointer(id),
None => (flags, key.1),
}
}
}
}
fn begin_shared_message_layout(&self) {
let Some(sohm) = self.sohm.as_deref() else {
return;
};
let mut phase = sohm.phase.lock();
if matches!(*phase, SohmPhase::Idle) && sohm.table_addr.lock().is_none() {
*phase = SohmPhase::Predict(FirstCopies::default());
}
}
fn prepare_shared_messages(&self, datasets: &[usize]) -> IoResult<()> {
let Some(sohm) = self.sohm.as_deref() else {
return Ok(());
};
if sohm.table_addr.lock().is_some() {
return Ok(());
}
*sohm.phase.lock() = SohmPhase::Collect(SohmCollector::new(sohm.indexes.len()));
for &i in datasets {
self.build_dataset_header(i)?;
}
for gi in 0..self.group_count() {
if self.grp(gi).lock().deleted {
continue;
}
self.build_group_header(gi)?;
}
self.build_root_group_header()?;
let SohmPhase::Collect(collector) =
std::mem::replace(&mut *sohm.phase.lock(), SohmPhase::Idle)
else {
return Err(crate::io::IoError::InvalidState(
"the shared-message collect pass did not finish in the collect phase".into(),
));
};
let indexes: Vec<SohmIndexContent> = sohm
.indexes
.iter()
.zip(collector.messages)
.map(|(&spec, messages)| SohmIndexContent { spec, messages })
.collect();
for (addr, len) in std::mem::take(&mut *sohm.superseded.lock()) {
self.allocator.free(addr, len, FreeSpaceClass::Metadata);
}
let built = build_shared_messages(&indexes, &self.ctx, &mut |len| {
self.allocator.allocate(len, FreeSpaceClass::Metadata)
})?;
for block in &built.blocks {
self.handle.write_at(block.addr, &block.image)?;
}
*sohm.phase.lock() = SohmPhase::Resolve {
ids: built.heap_ids,
first: FirstCopies::default(),
};
*sohm.table_addr.lock() = Some(built.table_addr);
Ok(())
}
fn write_free_space_managers(&self) -> IoResult<Option<Vec<u8>>> {
let Some(fs) = self.free_space.as_deref() else {
return Ok(None);
};
if !fs.records_free_space() {
return Ok(Some(fs.info.encode(&self.ctx)?));
}
for &(addr, len) in &fs.superseded {
self.allocator.free(addr, len, FreeSpaceClass::Metadata);
}
let hdr_size = FreeSpaceHeader::encoded_size(&self.ctx) as u64;
let settled = self.settle_free_space_managers(hdr_size, fs.info.threshold)?;
let mut info = fs.info.clone();
info.fs_addr = vec![UNDEF_ADDR; info.fs_addr.len()];
for placed in &settled {
let mut header = manager_header(&placed.sections);
let needed = free_space::sinfo_encoded_size(&header, &placed.sections, &self.ctx);
if needed > placed.sect_size {
return Err(crate::io::IoError::InvalidState(format!(
"the free-space sections need {needed} bytes, not the {} laid out",
placed.sect_size
)));
}
header.sect_addr = placed.sect_addr;
header.sect_size = placed.sect_size;
header.alloc_sect_size = placed.sect_size;
self.handle.write_at(
placed.sect_addr,
&free_space::encode_sections(
&header,
placed.hdr_addr,
&placed.sections,
placed.sect_size as usize,
&self.ctx,
),
)?;
self.handle
.write_at(placed.hdr_addr, &header.encode(&self.ctx))?;
info.fs_addr[placed.manager.message_slot()] = placed.hdr_addr;
}
info.eoa_pre_fsm_fsalloc = self.allocator.eof();
Ok(Some(info.encode(&self.ctx)?))
}
fn free_sections(&self, threshold: u64) -> IoResult<Vec<(FreeSpaceManager, Vec<FreeSection>)>> {
let policy = self.allocator.policy();
let extents = self.allocator.free_extents();
let mut sets = Vec::new();
for manager in FreeSpaceManager::ALL {
let mut sections: Vec<FreeSection> = extents
.iter()
.filter(|b| b.manager == manager)
.filter(|b| b.len >= threshold)
.map(|b| FreeSection {
addr: b.addr,
len: b.len,
class: policy.section_class(manager),
})
.collect();
sections.sort_unstable_by_key(|s| s.addr);
if let Some(bad) = sections
.windows(2)
.find(|w| w[0].addr + w[0].len > w[1].addr)
{
return Err(crate::io::IoError::InvalidState(format!(
"this session freed overlapping blocks: {:#x}+{} overlaps {:#x}",
bad[0].addr, bad[0].len, bad[1].addr
)));
}
sets.push((manager, sections));
}
Ok(sets)
}
fn settle_free_space_managers(
&self,
hdr_size: u64,
threshold: u64,
) -> IoResult<Vec<PlacedManager>> {
const ROUNDS: usize = 16;
const ORDER: [FreeSpaceManager; 3] = [
FreeSpaceManager::RawData,
FreeSpaceManager::Large,
FreeSpaceManager::Metadata,
];
let size_of = |sections: &[FreeSection]| {
let ordered = free_space::serialization_order(sections);
free_space::sinfo_encoded_size(&manager_header(&ordered), &ordered, &self.ctx)
};
let mut placed: Vec<PlacedManager> = Vec::new();
for _ in 0..ROUNDS {
let sets = self.free_sections(threshold)?;
let sections_of = |manager: FreeSpaceManager| {
sets.iter()
.find(|(m, _)| *m == manager)
.map(|(_, s)| s.as_slice())
.unwrap_or_default()
};
let mut changed = false;
for manager in ORDER {
let sections = sections_of(manager);
if sections.is_empty() || placed.iter().any(|p| p.manager == manager) {
continue;
}
let sect_size = size_of(sections);
let hdr_addr = self.allocator.allocate(hdr_size, FreeSpaceClass::Metadata);
let sect_addr = self.allocator.allocate(sect_size, FreeSpaceClass::Metadata);
placed.push(PlacedManager {
manager,
hdr_addr,
sect_addr,
sect_size,
sections: Vec::new(),
});
changed = true;
}
if !changed {
for p in &mut placed {
let needed = size_of(sections_of(p.manager));
if needed > p.sect_size {
self.allocator
.free(p.sect_addr, p.sect_size, FreeSpaceClass::Metadata);
p.sect_size = needed;
p.sect_addr = self.allocator.allocate(needed, FreeSpaceClass::Metadata);
changed = true;
}
}
}
if !changed {
for p in &mut placed {
p.sections = free_space::serialization_order(sections_of(p.manager));
}
return Ok(placed);
}
}
Err(crate::io::IoError::InvalidState(format!(
"the free-space managers did not settle in {ROUNDS} rounds"
)))
}
fn write_superblock_extension(&self) -> IoResult<()> {
if self.extension.addr.lock().is_some() {
return Ok(());
}
let table = self.sohm.as_deref().and_then(|sohm| {
sohm.table_addr
.lock()
.map(|addr| (sohm.indexes.len(), addr))
});
if self.extension.carried.is_empty() && table.is_none() && self.free_space.is_none() {
return Ok(());
}
let mut messages: Vec<crate::io::object_header_io::ExtensionMessage> =
self.extension.carried.clone();
if let Some(fs) = self.free_space.as_deref() {
let declared = fs.info.encode(&self.ctx)?;
match messages
.iter_mut()
.find(|m| m.msg_type == MSG_FILE_SPACE_INFO)
{
Some(msg) => msg.body = declared,
None => messages.push(crate::io::object_header_io::ExtensionMessage {
msg_type: MSG_FILE_SPACE_INFO,
flags: MSG_FLAG_DONTSHARE | MSG_FLAG_MARK_IF_UNKNOWN,
body: declared,
}),
}
}
if let Some((nindexes, table_addr)) = table {
let nindexes = u8::try_from(nindexes).map_err(|_| {
crate::io::IoError::InvalidState(format!("{nindexes} shared-message indexes"))
})?;
messages.push(crate::io::object_header_io::ExtensionMessage {
msg_type: MSG_SHARED_MESSAGE_TABLE,
flags: MSG_FLAG_CONSTANT | MSG_FLAG_DONTSHARE,
body: SharedMessageTableMessage {
version: 0,
table_address: table_addr,
nindexes,
}
.encode(&self.ctx),
});
}
let encode = |messages: &[crate::io::object_header_io::ExtensionMessage]| {
let mut extension = ObjectHeader::new();
for msg in messages {
extension.add_message(msg.msg_type, msg.flags, msg.body.clone());
}
extension.encode_v1(1)
};
let image = encode(&messages)?;
for &(addr, len) in &self.extension.superseded {
self.allocator.free(addr, len, FreeSpaceClass::Metadata);
}
let addr = self
.allocator
.allocate(image.len() as u64, FreeSpaceClass::Metadata);
let image = match self.write_free_space_managers()? {
None => image,
Some(body) => {
let msg = messages
.iter_mut()
.find(|m| m.msg_type == MSG_FILE_SPACE_INFO)
.ok_or_else(|| {
crate::io::IoError::InvalidState(
"a persisting file lost its file-space info message".into(),
)
})?;
if body.len() != msg.body.len() {
return Err(crate::io::IoError::InvalidState(format!(
"the file-space info message was laid out at {} bytes and \
written back at {}",
msg.body.len(),
body.len()
)));
}
msg.body = body;
encode(&messages)?
}
};
self.handle.write_at(addr, &image)?;
*self.extension.addr.lock() = Some(addr);
Ok(())
}
pub fn create_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
let total_elements: u64 = if dims.is_empty() {
1
} else {
dims.iter().product()
};
let element_size = datatype.element_size() as u64;
let data_size = total_elements * element_size;
let data_addr = if data_size > 0 {
self.allocator.allocate(data_size, FreeSpaceClass::RawData)
} else {
UNDEF_ADDR
};
let dataspace = if dims.is_empty() {
DataspaceMessage::scalar()
} else {
DataspaceMessage::simple(dims)
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0, data_addr,
data_size,
compact: None,
chunked: None,
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
append: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: 4,
times: self.created_object_times(),
},
);
Ok(idx)
}
pub fn create_external_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: Option<&[u64]>,
files: &[(&str, u64, u64)],
) -> IoResult<usize> {
if files.is_empty() {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' names no files; external storage is defined by \
the files it lives in, so at least one is required"
)));
}
let create = self.begin_create(name)?;
let name = create.name.as_str();
let total_elements: u64 = if dims.is_empty() {
1
} else {
dims.iter().product()
};
let data_size = total_elements * datatype.element_size() as u64;
let mut heap = LocalHeapImage::with_empty_string();
let mut entries = Vec::with_capacity(files.len());
for (i, &(file_name, offset, size)) in files.iter().enumerate() {
if file_name.is_empty() {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' has a slot with an empty file name"
)));
}
if size == UNLIMITED && i + 1 != files.len() {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' gives slot {i} ('{file_name}') the unlimited \
size H5O_EFL_UNLIMITED with {} slot(s) behind it; an unlimited slot \
absorbs the rest of the dataset, so it can only be the last",
files.len() - i - 1
)));
}
if offset.checked_add(size).is_none() {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' slot '{file_name}' spans offset {offset} \
plus {size} bytes, past the end of the 64-bit address space"
)));
}
entries.push(ExternalFile {
name: file_name.to_string(),
name_offset: heap.insert_str(file_name),
offset,
size,
});
}
let external = ExternalStorage {
heap_addr: UNDEF_ADDR,
files: entries,
prefix: EfilePrefix::default(),
};
let max_dims = max_dims.unwrap_or(dims);
if max_dims.len() != dims.len() {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' has {} dimensions but {} maximum ones",
dims.len(),
max_dims.len()
)));
}
for (d, (&max, &cur)) in max_dims.iter().zip(dims).enumerate().skip(1) {
if max > cur {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' makes dimension {d} extendible ({cur} of \
{max}); only the first dimension can be extendible for external storage"
)));
}
}
let reserved = external.total_size();
if max_dims.contains(&u64::MAX) {
if reserved != UNLIMITED {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' has an unlimited dataspace but its files \
reserve only {reserved} bytes; the last slot must take the unlimited \
size H5O_EFL_UNLIMITED"
)));
}
} else {
let max_bytes = max_dims
.iter()
.try_fold(datatype.element_size() as u64, |acc, &d| acc.checked_mul(d))
.ok_or_else(|| {
crate::io::IoError::InvalidState(format!(
"external dataset '{name}' maximum extent times its element size \
overflows 64 bits"
))
})?;
if reserved < max_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"external dataset '{name}' needs {max_bytes} bytes but its files reserve \
only {reserved}"
)));
}
}
let sa = self.ctx.sizeof_addr as usize;
let ss = self.ctx.sizeof_size as usize;
let heap_bytes = heap.as_bytes().to_vec();
let heap_addr = self.allocator.allocate(
local_heap_header_size(sa, ss) as u64,
FreeSpaceClass::Metadata,
);
let heap_data_addr = self
.allocator
.allocate(heap_bytes.len() as u64, FreeSpaceClass::Metadata);
let heap_hdr = LocalHeapHeader {
data_size: heap_bytes.len() as u64,
free_list_offset: LOCAL_HEAP_FREE_NULL,
data_addr: heap_data_addr,
};
self.handle.write_at(heap_addr, &heap_hdr.encode(sa, ss))?;
self.handle.write_at(heap_data_addr, &heap_bytes)?;
let external = ExternalStorage {
heap_addr,
..external
};
let dataspace = if dims.is_empty() {
DataspaceMessage::scalar()
} else {
let mut ds = DataspaceMessage::simple(dims);
if max_dims != dims {
ds.max_dims = Some(max_dims.to_vec());
}
ds
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: Some(external),
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0, data_addr: UNDEF_ADDR,
data_size,
compact: None,
chunked: None,
fixed_array: None,
btree_v2: None,
implicit: None,
single_chunk: None,
btree_v1: None,
append: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: 4,
times: self.created_object_times(),
},
);
Ok(idx)
}
pub fn create_virtual_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: Option<&[u64]>,
mappings: &[VirtualMapping],
) -> IoResult<usize> {
if mappings.is_empty() {
return Err(crate::io::IoError::InvalidState(format!(
"virtual dataset '{name}' names no mappings; a virtual dataset is defined \
by the source datasets it maps, so at least one is required"
)));
}
for m in mappings {
check_virtual_mapping(name, m)?;
}
let create = self.begin_create(name)?;
let name = create.name.as_str();
let block = VirtualMappingList {
mappings: mappings.to_vec(),
}
.encode(&self.ctx)?;
let (heap_addr, heap_index) = self.insert_vlen_objects(&[&block])?[0];
let dataspace = if dims.is_empty() {
DataspaceMessage::scalar()
} else {
let mut ds = DataspaceMessage::simple(dims);
if let Some(max) = max_dims {
ds.max_dims = Some(max.to_vec());
}
ds
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: Some(VirtualStorage {
heap_addr,
heap_index: heap_index as u32,
mappings: mappings.to_vec(),
}),
dataspace,
read_format: None,
obj_header_addr: 0, data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
chunked: None,
fixed_array: None,
btree_v2: None,
implicit: None,
single_chunk: None,
btree_v1: None,
append: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: 4,
times: self.created_object_times(),
},
);
Ok(idx)
}
pub fn create_compact_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
) -> IoResult<usize> {
let total_elements: u64 = if dims.is_empty() {
1
} else {
dims.iter().product()
};
let data_size = total_elements * datatype.element_size() as u64;
if data_size > MAX_COMPACT_DATA as u64 {
return Err(crate::io::IoError::InvalidState(format!(
"compact dataset '{name}' needs {data_size} bytes, above the \
{MAX_COMPACT_DATA}-byte ceiling a data layout message can hold; \
use contiguous or chunked storage"
)));
}
let create = self.begin_create(name)?;
let name = create.name.as_str();
let dataspace = if dims.is_empty() {
DataspaceMessage::scalar()
} else {
DataspaceMessage::simple(dims)
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0, data_addr: UNDEF_ADDR,
data_size: 0,
compact: Some(vec![0u8; data_size as usize]),
chunked: None,
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
append: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: 4,
times: self.created_object_times(),
},
);
Ok(idx)
}
pub fn create_null_dataset(&self, name: &str, datatype: DatatypeMessage) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace: DataspaceMessage::null(),
read_format: None,
obj_header_addr: 0, data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
chunked: None,
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
append: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: 4,
times: self.created_object_times(),
},
);
Ok(idx)
}
pub fn create_chunked_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: &[u64],
chunk_dims: &[u64],
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, max_dims, chunk_dims)?;
ensure_at_most_one_unlimited(max_dims)?;
let chunk_bytes = chunk_dims.iter().product::<u64>() * datatype.element_size() as u64;
let layout_version = self.chunk_layout_version(false, chunk_bytes);
let earray_params = EarrayParams::default_params();
let ndblk_addrs = compute_ndblk_addrs(earray_params.sup_blk_min_data_ptrs)?;
let nsblk_addrs = compute_nsblk_addrs(
earray_params.idx_blk_elmts,
earray_params.data_blk_min_elmts,
earray_params.sup_blk_min_data_ptrs,
earray_params.max_nelmts_bits,
)?;
let mut ea_header = ExtensibleArrayHeader::new_for_chunks(&self.ctx);
ea_header.max_nelmts_bits = earray_params.max_nelmts_bits;
ea_header.idx_blk_elmts = earray_params.idx_blk_elmts;
ea_header.data_blk_min_elmts = earray_params.data_blk_min_elmts;
ea_header.sup_blk_min_data_ptrs = earray_params.sup_blk_min_data_ptrs;
ea_header.max_dblk_page_nelmts_bits = earray_params.max_dblk_page_nelmts_bits;
let hdr_encoded = ea_header.encode(&self.ctx);
let ea_header_addr = self
.allocator
.allocate(hdr_encoded.len() as u64, FreeSpaceClass::Metadata);
let ea_iblk = ExtensibleArrayIndexBlock::new(
ea_header_addr,
earray_params.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
);
let iblk_encoded = ea_iblk.encode(&self.ctx);
let ea_iblk_addr = self
.allocator
.allocate(iblk_encoded.len() as u64, FreeSpaceClass::Metadata);
ea_header.idx_blk_addr = ea_iblk_addr;
let hdr_encoded = ea_header.encode(&self.ctx);
self.handle.write_at(ea_header_addr, &hdr_encoded)?;
self.handle.write_at(ea_iblk_addr, &iblk_encoded)?;
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(max_dims.to_vec()),
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
chunked: Some(ChunkedDatasetInfo {
chunk_dims: chunk_dims.to_vec(),
earray_params,
ea_header_addr,
ea_iblk_addr,
ea_header,
ea_iblk,
chunks_written: 0,
filt_iblk: None,
chunk_size_len: 0,
}),
append: None,
},
);
Ok(idx)
}
fn write_contiguous_bytes(
&self,
target: &ContiguousTarget,
off: u64,
data: &[u8],
) -> IoResult<()> {
match target {
ContiguousTarget::Local(addr) => Ok(self.handle.write_at(addr + off, data)?),
ContiguousTarget::External { files, prefix } => {
write_external_file_bytes(files, prefix.as_deref(), off, data)
}
ContiguousTarget::Virtual => Err(virtual_write_refused()),
}
}
pub fn write_dataset_raw(&self, index: usize, data: &[u8]) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
let target = {
let mut g = ds.lock();
if g.is_chunked() {
return Err(crate::io::IoError::InvalidState(
"use write_chunk for chunked datasets".into(),
));
}
if let Some(image) = g.compact.as_mut() {
if data.len() != image.len() {
return Err(crate::io::IoError::InvalidState(format!(
"data size mismatch: expected {} bytes, got {}",
image.len(),
data.len()
)));
}
image.copy_from_slice(data);
g.header_dirty = true;
return Ok(());
}
let Some(target) = g.contiguous_target() else {
return Err(crate::io::IoError::InvalidState(
"dataset has no data allocated".into(),
));
};
if target.is_storage() && data.len() as u64 != g.data_size {
return Err(crate::io::IoError::InvalidState(format!(
"data size mismatch: expected {} bytes, got {}",
g.data_size,
data.len()
)));
}
target
};
self.write_contiguous_bytes(&target, 0, data)
}
pub fn write_chunk(&self, index: usize, chunk_idx: u64, data: &[u8]) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
self.write_chunk_inner(index, chunk_idx, data)
}
pub(crate) fn write_chunk_inner(
&self,
index: usize,
chunk_idx: u64,
data: &[u8],
) -> IoResult<()> {
let ds = self.ds(index);
let (chunk_bytes, pipeline) = {
let g = ds.lock();
let element_size = g.datatype.element_size() as u64;
let chunked = g
.chunked
.as_ref()
.ok_or_else(|| crate::io::IoError::InvalidState("not a chunked dataset".into()))?;
(
chunked.chunk_dims.iter().product::<u64>() * element_size,
g.filter_pipeline.clone(),
)
};
if data.len() as u64 != chunk_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"chunk data size mismatch: expected {} bytes, got {}",
chunk_bytes,
data.len()
)));
}
let compressed;
let write_data = if let Some(ref pipeline) = pipeline {
compressed = filter::apply_filters(pipeline, data)?;
&compressed
} else {
data
};
self.record_ea_chunk(index, chunk_idx, write_data, 0)
}
fn place_chunk(&self, old: Option<(u64, u64)>, new_len: u64) -> u64 {
match old {
Some((addr, len)) if addr != UNDEF_ADDR && len == new_len => addr,
Some((addr, len)) if addr != UNDEF_ADDR => {
if !self.swmr_active {
self.allocator.free(addr, len, FreeSpaceClass::RawData);
}
self.allocator.allocate(new_len, FreeSpaceClass::RawData)
}
_ => self.allocator.allocate(new_len, FreeSpaceClass::RawData),
}
}
fn record_ea_chunk(
&self,
index: usize,
chunk_idx: u64,
final_bytes: &[u8],
filter_mask: u32,
) -> IoResult<()> {
let compressed_size = final_bytes.len() as u64;
let ds = self.ds(index);
let mut m = ds.lock();
let is_filtered = m.filter_pipeline.is_some();
if is_filtered {
let chunk_size_len = m.chunked.as_ref().unwrap().chunk_size_len as usize;
if chunk_size_len < 8 && compressed_size >= (1u64 << (chunk_size_len * 8)) {
return Err(crate::io::IoError::InvalidState(format!(
"filtered chunk size {compressed_size} does not fit in the \
{chunk_size_len}-byte extensible-array chunk-size field"
)));
}
}
let idx_blk_elmts = {
let c = m.chunked.as_ref().unwrap();
c.earray_params.idx_blk_elmts as u64
};
if chunk_idx < idx_blk_elmts {
let chunked = m.chunked.as_mut().unwrap();
if is_filtered {
if let Some(ref mut fiblk) = chunked.filt_iblk {
let old = fiblk.elements[chunk_idx as usize];
let chunk_addr =
self.place_chunk(Some((old.addr, old.nbytes)), compressed_size);
self.handle.write_at(chunk_addr, final_bytes)?;
fiblk.elements[chunk_idx as usize] = FilteredChunkEntry {
addr: chunk_addr,
nbytes: compressed_size,
filter_mask,
};
}
} else {
let old = chunked.ea_iblk.elements[chunk_idx as usize];
let chunk_addr = self.place_chunk(Some((old, compressed_size)), compressed_size);
self.handle.write_at(chunk_addr, final_bytes)?;
chunked.ea_iblk.elements[chunk_idx as usize] = chunk_addr;
}
chunked.chunks_written += 1;
if chunk_idx + 1 > chunked.ea_header.max_idx_set {
chunked.ea_header.max_idx_set = chunk_idx + 1;
}
if chunked.ea_header.num_elmts_realized < idx_blk_elmts {
chunked.ea_header.num_elmts_realized = idx_blk_elmts;
}
} else {
let (geo, max_nelmts_bits, chunk_size_len, ea_header_addr) = {
let c = m.chunked.as_ref().unwrap();
let p = &c.earray_params;
(
EaGeometry::new(
p.idx_blk_elmts,
p.data_blk_min_elmts,
p.sup_blk_min_data_ptrs,
p.max_nelmts_bits,
p.max_dblk_page_nelmts_bits,
)?,
p.max_nelmts_bits,
c.chunk_size_len,
c.ea_header_addr,
)
};
let loc = match geo.locate(chunk_idx)? {
EaLoc::Dblk(l) => l,
EaLoc::Index { .. } => unreachable!("chunk_idx >= idx_blk_elmts"),
};
if loc.paged {
return Err(crate::io::IoError::InvalidState(format!(
"chunk index {} needs a paged extensible-array data block, \
which is not yet supported",
chunk_idx
)));
}
let class_id = if is_filtered {
EA_CLS_FILT_CHUNK
} else {
EA_CLS_CHUNK
};
let dblk_nelmts = loc.dblk_nelmts as usize;
let parent: DblkParent;
let mut dblk_addr: u64;
match loc.path {
EaDblkPath::Direct { idx: di } => {
let c = m.chunked.as_ref().unwrap();
dblk_addr = if is_filtered {
c.filt_iblk.as_ref().unwrap().dblk_addrs[di]
} else {
c.ea_iblk.dblk_addrs[di]
};
parent = DblkParent::IndexBlock(di);
}
EaDblkPath::ViaSblk {
sblk_off,
local_dblk,
ndblks_in_sblk,
sblk_block_offset,
} => {
let mut sblk_addr = {
let c = m.chunked.as_ref().unwrap();
if is_filtered {
c.filt_iblk.as_ref().unwrap().sblk_addrs[sblk_off]
} else {
c.ea_iblk.sblk_addrs[sblk_off]
}
};
if sblk_addr == UNDEF_ADDR {
let sb = ExtensibleArraySuperBlock::new(
class_id,
ea_header_addr,
sblk_block_offset,
ndblks_in_sblk,
);
let enc = sb.encode(&self.ctx, max_nelmts_bits);
sblk_addr = self
.allocator
.allocate(enc.len() as u64, FreeSpaceClass::Metadata);
self.handle.write_at(sblk_addr, &enc)?;
let c = m.chunked.as_mut().unwrap();
if is_filtered {
c.filt_iblk.as_mut().unwrap().sblk_addrs[sblk_off] = sblk_addr;
} else {
c.ea_iblk.sblk_addrs[sblk_off] = sblk_addr;
}
c.ea_header.num_sblks_created += 1;
c.ea_header.size_sblks_created += enc.len() as u64;
}
let sb_buf = self.handle.read_at_most(sblk_addr, 65536)?;
let sb = ExtensibleArraySuperBlock::decode(
&sb_buf,
&self.ctx,
max_nelmts_bits,
ndblks_in_sblk,
0,
)?;
dblk_addr = sb.dblk_addrs[local_dblk];
parent = DblkParent::SuperBlock {
sblk_addr,
ndblks_in_sblk,
local_dblk,
};
}
}
let created = dblk_addr == UNDEF_ADDR;
if is_filtered {
let mut dblk = if created {
FilteredDataBlock::new(ea_header_addr, loc.dblk_block_offset, dblk_nelmts)
} else {
let buf = self.handle.read_at_most(dblk_addr, 65536)?;
FilteredDataBlock::decode(
&buf,
&self.ctx,
max_nelmts_bits,
dblk_nelmts,
chunk_size_len,
)?
};
let old = dblk.elements[loc.offset_in_dblk as usize];
let chunk_addr = self.place_chunk(Some((old.addr, old.nbytes)), compressed_size);
self.handle.write_at(chunk_addr, final_bytes)?;
let entry = FilteredChunkEntry {
addr: chunk_addr,
nbytes: compressed_size,
filter_mask,
};
dblk.elements[loc.offset_in_dblk as usize] = entry;
let enc = dblk.encode(&self.ctx, max_nelmts_bits, chunk_size_len);
if created {
dblk_addr = self
.allocator
.allocate(enc.len() as u64, FreeSpaceClass::Metadata);
}
self.handle.write_at(dblk_addr, &enc)?;
if created {
let c = m.chunked.as_mut().unwrap();
c.ea_header.num_dblks_created += 1;
c.ea_header.size_dblks_created += enc.len() as u64;
}
} else {
let mut dblk = if created {
ExtensibleArrayDataBlock::new(
ea_header_addr,
loc.dblk_block_offset,
dblk_nelmts,
)
} else {
let buf = self.handle.read_at_most(dblk_addr, 65536)?;
ExtensibleArrayDataBlock::decode(&buf, &self.ctx, max_nelmts_bits, dblk_nelmts)?
};
let old = dblk.elements[loc.offset_in_dblk as usize];
let chunk_addr = self.place_chunk(Some((old, compressed_size)), compressed_size);
self.handle.write_at(chunk_addr, final_bytes)?;
dblk.elements[loc.offset_in_dblk as usize] = chunk_addr;
let enc = dblk.encode(&self.ctx, max_nelmts_bits);
if created {
dblk_addr = self
.allocator
.allocate(enc.len() as u64, FreeSpaceClass::Metadata);
}
self.handle.write_at(dblk_addr, &enc)?;
if created {
let c = m.chunked.as_mut().unwrap();
c.ea_header.num_dblks_created += 1;
c.ea_header.size_dblks_created += enc.len() as u64;
}
}
if created {
match parent {
DblkParent::IndexBlock(di) => {
let c = m.chunked.as_mut().unwrap();
if is_filtered {
c.filt_iblk.as_mut().unwrap().dblk_addrs[di] = dblk_addr;
} else {
c.ea_iblk.dblk_addrs[di] = dblk_addr;
}
}
DblkParent::SuperBlock {
sblk_addr,
ndblks_in_sblk,
local_dblk,
} => {
let buf = self.handle.read_at_most(sblk_addr, 65536)?;
let mut sb = ExtensibleArraySuperBlock::decode(
&buf,
&self.ctx,
max_nelmts_bits,
ndblks_in_sblk,
0,
)?;
sb.dblk_addrs[local_dblk] = dblk_addr;
let enc = sb.encode(&self.ctx, max_nelmts_bits);
self.handle.write_at(sblk_addr, &enc)?;
}
}
}
let c = m.chunked.as_mut().unwrap();
c.chunks_written += 1;
if chunk_idx + 1 > c.ea_header.max_idx_set {
c.ea_header.max_idx_set = chunk_idx + 1;
}
if created {
c.ea_header.num_elmts_realized += loc.dblk_nelmts;
}
}
Ok(())
}
pub fn write_slice(
&self,
index: usize,
starts: &[u64],
counts: &[u64],
data: &[u8],
) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
self.write_slice_inner(index, starts, counts, data)
}
pub(crate) fn write_slice_inner(
&self,
index: usize,
starts: &[u64],
counts: &[u64],
data: &[u8],
) -> IoResult<()> {
let ds_ref = self.ds(index);
let ds = ds_ref.lock();
let is_chunked = ds.is_chunked();
let dims = &ds.dataspace.dims;
let element_size = ds.datatype.element_size() as u64;
let ndims = dims.len();
check_hyperslab(dims, starts, counts)?;
if ndims == 0 {
return Err(crate::io::IoError::InvalidState(
"write_slice does not support scalar datasets; use write_dataset_raw".into(),
));
}
let out_elems: u64 = counts.iter().product();
if data.len() as u64 != out_elems * element_size {
return Err(crate::io::IoError::InvalidState(format!(
"data size mismatch: expected {} bytes, got {}",
out_elems * element_size,
data.len()
)));
}
let dims = dims.clone();
let target = ds.contiguous_target();
drop(ds);
if is_chunked {
self.flush_append_buffer_if_intersecting(index, starts[0], starts[0] + counts[0])?;
return self.write_slice_chunked(index, starts, counts, data);
}
let Some(target) = target else {
return Err(crate::io::IoError::InvalidState(
"dataset has no data allocated".into(),
));
};
for_each_contiguous_run(
&dims,
starts,
counts,
element_size,
|dst_off, src_off, len| {
self.write_contiguous_bytes(&target, dst_off, &data[src_off..src_off + len])
},
)?;
Ok(())
}
fn write_slice_chunked(
&self,
index: usize,
starts: &[u64],
counts: &[u64],
data: &[u8],
) -> IoResult<()> {
if counts.contains(&0) {
return Ok(());
}
let geo = self.chunk_geometry(index)?;
let ndims = geo.dims.len();
if geo.chunk_dims.len() != ndims {
return Err(crate::io::IoError::InvalidState(format!(
"dataset chunk shape has {} dimensions but the dataspace has {}",
geo.chunk_dims.len(),
ndims
)));
}
if geo.chunk_dims.contains(&0) {
return Err(crate::io::IoError::InvalidState(
"chunk shape has a zero-length dimension".into(),
));
}
let chunk_bytes = geo.chunk_bytes() as usize;
let first: Vec<u64> = (0..ndims).map(|d| starts[d] / geo.chunk_dims[d]).collect();
let last: Vec<u64> = (0..ndims)
.map(|d| (starts[d] + counts[d] - 1) / geo.chunk_dims[d])
.collect();
let mut coords = first.clone();
loop {
let mut in_chunk = vec![0u64; ndims];
let mut in_data = vec![0u64; ndims];
let mut extent = vec![0u64; ndims];
let mut covers_whole_chunk = true;
for d in 0..ndims {
let chunk_origin = coords[d] * geo.chunk_dims[d];
let lo = starts[d].max(chunk_origin);
let hi = (starts[d] + counts[d]).min(chunk_origin + geo.chunk_dims[d]);
in_chunk[d] = lo - chunk_origin;
in_data[d] = lo - starts[d];
extent[d] = hi - lo;
if in_chunk[d] != 0 || extent[d] != geo.chunk_dims[d] {
covers_whole_chunk = false;
}
}
let mut buf = if covers_whole_chunk {
vec![0u8; chunk_bytes]
} else {
match self.read_chunk_at_coords(index, &coords)? {
Some(existing) => {
if existing.len() != chunk_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"stored chunk at {coords:?} is {} bytes but the chunk shape \
needs {chunk_bytes}",
existing.len()
)));
}
existing
}
None => self.new_write_chunk_buffer(index, chunk_bytes),
}
};
for_each_dual_run(
&geo.chunk_dims,
&in_chunk,
counts,
&in_data,
&extent,
geo.element_size,
|dst_off, src_off, len| {
let dst = dst_off as usize;
let src = src_off as usize;
buf[dst..dst + len].copy_from_slice(&data[src..src + len]);
Ok(())
},
)?;
self.write_chunk_at_coords(index, &coords, &buf)?;
let mut d = ndims;
loop {
if d == 0 {
return Ok(());
}
d -= 1;
if coords[d] < last[d] {
coords[d] += 1;
break;
}
coords[d] = first[d];
}
}
}
pub fn add_root_attribute(&self, attr: AttributeMessage) -> IoResult<()> {
self.set_attribute(AttrTarget::Root, attr)
}
pub fn set_attribute(&self, target: AttrTarget<'_>, attr: AttributeMessage) -> IoResult<()> {
self.insert_attribute(target, attr, Created)
}
fn insert_attribute(
&self,
target: AttrTarget<'_>,
attr: AttributeMessage,
origin: AttrOrigin,
) -> IoResult<()> {
if self.swmr_active {
return Err(swmr_attr_error(&attr.name));
}
self.forget_attribute_reference(self.attr_scope(target)?, &attr.name);
let mut entry = AttributeEntry::from(attr);
let old = self.with_attr_list(target, |attrs| {
if let Some(pos) = attrs.iter().position(|a| a.name() == entry.name()) {
entry.set_creation_index(attrs[pos].creation_index());
Some(std::mem::replace(&mut attrs[pos], entry))
} else {
entry.set_creation_index(match origin {
Created => Some(next_creation_index(attrs)),
Rewritten(kept) => kept,
});
attrs.push(entry);
None
}
})?;
match old {
Some(old) => self.release_attr_vlen(&old),
None => Ok(()),
}
}
pub fn set_vlen_string_attribute(
&self,
target: AttrTarget<'_>,
name: &str,
value: &str,
) -> IoResult<()> {
let origin = self.evict_attr(target, name)?;
let attr = self.vlen_string_attribute(name, value)?;
self.insert_attribute(target, attr, origin)
}
pub fn set_vlen_string_array_attribute(
&self,
target: AttrTarget<'_>,
name: &str,
values: &[&str],
dims: &[u64],
) -> IoResult<()> {
let origin = self.evict_attr(target, name)?;
let attr = self.vlen_string_array_attribute(name, values, dims)?;
self.insert_attribute(target, attr, origin)
}
pub fn set_object_reference_attribute(
&self,
target: AttrTarget<'_>,
name: &str,
paths: &[&str],
dims: &[u64],
) -> IoResult<()> {
let scope = self.attr_scope(target)?;
let elements: u64 = dims.iter().product();
if elements != paths.len() as u64 {
return Err(crate::io::IoError::InvalidState(format!(
"attribute '{name}' shape {dims:?} needs {elements} references, got {}",
paths.len()
)));
}
for path in paths {
self.object_reference_target(path)?;
}
let datatype = DatatypeMessage::object_reference(&self.ctx);
let image = vec![0u8; paths.len() * datatype.element_size() as usize];
let attr = if dims.is_empty() {
AttributeMessage::scalar_numeric(name, datatype, image)
} else {
AttributeMessage::array_numeric(name, datatype, dims, image)
};
self.set_attribute(target, attr)?;
self.attribute_references
.lock()
.push(AttributeReferenceValue {
scope,
name: name.to_string(),
targets: paths.iter().map(|p| (*p).to_string()).collect(),
stride: self.ctx.sizeof_addr as usize,
});
Ok(())
}
pub fn set_dimension_scale(&self, dsid: usize, name: Option<&str>) -> IoResult<()> {
let scale_path = self.dataset_name(dsid)?;
if self.dataset_attribute(dsid, DIMENSION_LIST)?.is_some() {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{scale_path}' has dimension scales attached and cannot become one"
)));
}
self.set_fixed_string_attribute(dsid, "CLASS", DIMENSION_SCALE_CLASS)?;
if let Some(name) = name {
self.set_fixed_string_attribute(dsid, "NAME", name)?;
}
Ok(())
}
pub fn attach_dimension_scale(&self, did: usize, dsid: usize, idx: usize) -> IoResult<()> {
let data_path = self.dataset_name(did)?;
let scale_path = self.dataset_name(dsid)?;
if did == dsid {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{data_path}' cannot be its own dimension scale"
)));
}
if self.is_dimension_scale(did)? {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{data_path}' is a dimension scale and cannot have scales attached"
)));
}
if self.dataset_attribute(dsid, DIMENSION_LIST)?.is_some() {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{scale_path}' has dimension scales attached and cannot be one"
)));
}
if self.has_reserved_class(did)? {
return Err(crate::io::IoError::InvalidState(format!(
"dataset '{data_path}' holds an image, palette or table and cannot have \
dimension scales"
)));
}
let rank = self.ds(did).lock().dataspace.dims.len().max(1);
if idx >= rank {
return Err(crate::io::IoError::InvalidState(format!(
"axis {idx} is out of range for the rank-{rank} dataset '{data_path}'"
)));
}
let mut lists = match self.dimension_list(did)? {
Some(lists) => lists,
None => vec![Vec::new(); rank],
};
if lists.len() != rank {
return Err(crate::io::IoError::InvalidState(format!(
"DIMENSION_LIST of '{data_path}' has {} entries for a rank-{rank} dataset",
lists.len()
)));
}
if lists[idx].contains(&scale_path) {
return Ok(());
}
lists[idx].push(scale_path);
self.write_dimension_list(did, &lists)?;
let mut entries = self.reference_list(dsid)?;
entries.push((data_path, idx as u32));
self.write_reference_list(dsid, &entries)?;
if !self.is_dimension_scale(dsid)? {
self.set_dimension_scale(dsid, None)?;
}
Ok(())
}
fn dataset_name(&self, index: usize) -> IoResult<String> {
let count = self.dataset_count();
if index >= count {
return Err(crate::io::IoError::InvalidState(format!(
"dataset index {index} out of range (have {count})"
)));
}
let ds = self.ds(index);
let m = ds.lock();
if m.deleted {
return Err(crate::io::IoError::NotFound(format!(
"dataset '{}' has been deleted",
m.name
)));
}
Ok(m.name.clone())
}
fn dataset_attribute(&self, index: usize, name: &str) -> IoResult<Option<AttributeEntry>> {
self.dataset_name(index)?;
Ok(self
.ds(index)
.lock()
.attributes
.iter()
.find(|a| a.name() == name)
.cloned())
}
fn set_fixed_string_attribute(&self, index: usize, name: &str, value: &str) -> IoResult<()> {
if value.as_bytes().contains(&0) {
return Err(crate::io::IoError::InvalidState(format!(
"attribute '{name}' value holds an interior NUL"
)));
}
let size = u32::try_from(value.len() + 1).map_err(|_| {
crate::io::IoError::InvalidState(format!(
"attribute '{name}' value of {} bytes exceeds the fixed-string width field",
value.len()
))
})?;
let mut data = value.as_bytes().to_vec();
data.push(0);
let attr =
AttributeMessage::scalar_numeric(name, DatatypeMessage::fixed_string(size), data);
let target = AttrTarget::Dataset(index);
self.evict_attr(target, name)?;
self.insert_attribute(target, attr, Created)
}
fn class_attribute(&self, index: usize) -> IoResult<Option<ClassAttr>> {
use crate::format::global_heap::decode_vlen_reference;
let Some(entry) = self.dataset_attribute(index, "CLASS")? else {
return Ok(None);
};
let msg = entry.decoded().map_err(|reason| {
crate::io::IoError::InvalidState(format!(
"CLASS attribute of '{}' cannot be decoded: {reason}",
self.ds(index).lock().name
))
})?;
Ok(Some(match &msg.datatype {
DatatypeMessage::FixedString { size, padding, .. } => {
let avail = (*size as usize).min(msg.data.len());
ClassAttr::Fixed {
size: *size,
null_terminated: *padding == 0,
text: c_string(&msg.data[..avail]),
}
}
DatatypeMessage::VarLenString { .. } => {
let (_, addr, obj_idx) = decode_vlen_reference(&msg.data, &self.ctx)?;
let bytes = if addr == 0 || addr == UNDEF_ADDR {
Vec::new()
} else {
let obj_idx = u16::try_from(obj_idx).map_err(|_| {
crate::io::IoError::InvalidState(format!(
"global heap object index {obj_idx} does not fit the 16-bit on-disk \
field"
))
})?;
self.read_heap_object(addr, obj_idx)?
};
ClassAttr::VarLen(c_string(&bytes))
}
_ => ClassAttr::NotString,
}))
}
fn is_dimension_scale(&self, index: usize) -> IoResult<bool> {
Ok(match self.class_attribute(index)? {
None | Some(ClassAttr::NotString) => false,
Some(ClassAttr::Fixed {
size,
null_terminated,
text,
}) => null_terminated && size == 16 && text == DIMENSION_SCALE_CLASS,
Some(ClassAttr::VarLen(text)) => text == DIMENSION_SCALE_CLASS,
})
}
fn has_reserved_class(&self, index: usize) -> IoResult<bool> {
Ok(match self.class_attribute(index)? {
None => false,
Some(ClassAttr::NotString) => {
return Err(crate::io::IoError::InvalidState(format!(
"CLASS attribute of '{}' is not a string",
self.ds(index).lock().name
)))
}
Some(ClassAttr::Fixed { text, .. }) | Some(ClassAttr::VarLen(text)) => {
matches!(text.as_str(), "IMAGE" | "PALETTE" | "TABLE")
}
})
}
fn read_heap_object(&self, collection: u64, index: u16) -> IoResult<Vec<u8>> {
use crate::format::global_heap::GlobalHeapCollection;
let mut image = self.handle.read_at_most(collection, 4096)?;
let declared = GlobalHeapCollection::decode_size(&image, &self.ctx)?;
if declared > image.len() {
image = self.handle.read_at(collection, declared)?;
}
let (gcol, _) = GlobalHeapCollection::decode(&image[..declared], &self.ctx)?;
gcol.get_object(index).map(<[u8]>::to_vec).ok_or_else(|| {
crate::io::IoError::InvalidState(format!(
"global heap collection {collection:#x} has no object {index}"
))
})
}
fn path_of_header_address(&self, addr: u64) -> IoResult<String> {
for ds in self.dataset_refs() {
let m = ds.lock();
if !m.deleted && m.obj_header_written_addr == Some(addr) {
return Ok(m.name.clone());
}
}
for grp in self.group_refs() {
let g = grp.lock();
if !g.deleted && g.obj_header_written_addr == Some(addr) {
return Ok(g.name.clone());
}
}
Err(crate::io::IoError::InvalidState(format!(
"object reference to header {addr:#x} names no dataset or group of this file"
)))
}
fn heap_reference_path(
&self,
collection: u64,
index: u16,
token_offset: usize,
on_disk: &[u8],
) -> IoResult<String> {
let registered = self
.pending_heap_references
.lock()
.iter()
.find(|p| {
p.collection == collection && p.index == index && p.token_offset == token_offset
})
.map(|p| match &p.target {
PendingHeapTarget::Dataset(path) | PendingHeapTarget::Object(path) => path.clone(),
});
if let Some(path) = registered {
return Ok(path);
}
let mut raw = [0u8; 8];
raw[..on_disk.len()].copy_from_slice(on_disk);
self.path_of_header_address(u64::from_le_bytes(raw))
}
fn dimension_list(&self, did: usize) -> IoResult<Option<Vec<Vec<String>>>> {
use crate::format::global_heap::{decode_vlen_reference, vlen_reference_size};
let Some(entry) = self.dataset_attribute(did, DIMENSION_LIST)? else {
return Ok(None);
};
let name = || self.ds(did).lock().name.clone();
let msg = entry.decoded().map_err(|reason| {
crate::io::IoError::InvalidState(format!(
"DIMENSION_LIST of '{}' cannot be decoded: {reason}",
name()
))
})?;
match &msg.datatype {
DatatypeMessage::VarLenSequence { base }
if matches!(
**base,
DatatypeMessage::Reference {
kind: ReferenceKind::Object1,
..
}
) => {}
other => {
return Err(crate::io::IoError::InvalidState(format!(
"DIMENSION_LIST of '{}' is {other}; only a sequence of H5T_STD_REF_OBJ \
references is supported",
name()
)))
}
}
let sa = self.ctx.sizeof_addr as usize;
let ref_size = vlen_reference_size(&self.ctx);
let mut lists = Vec::new();
for elem in msg.data.chunks_exact(ref_size) {
let (seq_len, addr, obj_idx) = decode_vlen_reference(elem, &self.ctx)?;
let seq_len = seq_len as usize;
let mut paths = Vec::with_capacity(seq_len);
if seq_len > 0 {
let index = u16::try_from(obj_idx).map_err(|_| {
crate::io::IoError::InvalidState(format!(
"global heap object index {obj_idx} does not fit the 16-bit on-disk field"
))
})?;
let bytes = self.read_heap_object(addr, index)?;
if bytes.len() < seq_len * sa {
return Err(crate::io::IoError::InvalidState(format!(
"DIMENSION_LIST of '{}' names {seq_len} scales in a {}-byte heap object",
name(),
bytes.len()
)));
}
for k in 0..seq_len {
paths.push(self.heap_reference_path(
addr,
index,
k * sa,
&bytes[k * sa..(k + 1) * sa],
)?);
}
}
lists.push(paths);
}
Ok(Some(lists))
}
fn write_dimension_list(&self, did: usize, lists: &[Vec<String>]) -> IoResult<()> {
use crate::format::global_heap::{
encode_vlen_reference, vlen_reference_size, vlen_seq_len,
};
let target = AttrTarget::Dataset(did);
let origin = self.evict_attr(target, DIMENSION_LIST)?;
let sa = self.ctx.sizeof_addr as usize;
let blobs: Vec<Vec<u8>> = lists.iter().map(|l| vec![0u8; l.len() * sa]).collect();
let items: Vec<&[u8]> = blobs.iter().map(Vec::as_slice).collect();
let placements = self.insert_vlen_objects(&items)?;
let mut data = Vec::with_capacity(lists.len() * vlen_reference_size(&self.ctx));
let mut pending = self.pending_heap_references.lock();
for (axis, &(collection, index)) in placements.iter().enumerate() {
for (k, path) in lists[axis].iter().enumerate() {
pending.push(PendingHeapReference {
collection,
index,
token_offset: k * sa,
target: PendingHeapTarget::Object(path.clone()),
});
}
data.extend_from_slice(&encode_vlen_reference(
vlen_seq_len(lists[axis].len())?,
collection,
u32::from(index),
&self.ctx,
));
}
drop(pending);
let attr = AttributeMessage {
name: DIMENSION_LIST.to_string(),
datatype: DatatypeMessage::VarLenSequence {
base: Box::new(DatatypeMessage::object_reference(&self.ctx)),
},
dataspace: DataspaceMessage::simple(&[lists.len() as u64]),
data,
};
self.insert_attribute(target, attr, origin)
}
fn reference_list(&self, dsid: usize) -> IoResult<Vec<(String, u32)>> {
let Some(entry) = self.dataset_attribute(dsid, REFERENCE_LIST)? else {
return Ok(Vec::new());
};
let name = || self.ds(dsid).lock().name.clone();
let msg = entry.decoded().map_err(|reason| {
crate::io::IoError::InvalidState(format!(
"REFERENCE_LIST of '{}' cannot be decoded: {reason}",
name()
))
})?;
let unsupported = |why: String| {
crate::io::IoError::InvalidState(format!(
"REFERENCE_LIST of '{}' is {}; {why}",
name(),
msg.datatype
))
};
let DatatypeMessage::Compound { size, members } = &msg.datatype else {
return Err(unsupported("a compound is required".into()));
};
let member = |m: &str| {
members
.iter()
.find(|c| c.name == m)
.ok_or_else(|| unsupported(format!("member '{m}' is missing")))
};
let dataset = member("dataset")?;
let dimension = member("dimension")?;
let sa = self.ctx.sizeof_addr as usize;
if !matches!(
dataset.datatype,
DatatypeMessage::Reference {
kind: ReferenceKind::Object1,
..
}
) {
return Err(unsupported(
"only an H5T_STD_REF_OBJ 'dataset' member is supported".into(),
));
}
let DatatypeMessage::FixedPoint {
size: 4,
byte_order,
..
} = dimension.datatype
else {
return Err(unsupported(
"a 4-byte integer 'dimension' member is required".into(),
));
};
let stride = *size as usize;
let registered: Option<Vec<String>> = self
.attribute_references
.lock()
.iter()
.find(|r| r.scope == AttrScope::Dataset(dsid) && r.name == REFERENCE_LIST)
.map(|r| r.targets.clone());
let mut entries = Vec::with_capacity(msg.data.len() / stride);
for (i, elem) in msg.data.chunks_exact(stride).enumerate() {
let at = |offset: u32, len: usize| {
elem.get(offset as usize..offset as usize + len)
.ok_or_else(|| unsupported(format!("element {i} is too short for its members")))
};
let path = match ®istered {
Some(targets) => targets.get(i).cloned().ok_or_else(|| {
crate::io::IoError::InvalidState(format!(
"REFERENCE_LIST of '{}' entry {i} has no registered target",
name()
))
})?,
None => {
let mut raw = [0u8; 8];
raw[..sa].copy_from_slice(at(dataset.offset, sa)?);
self.path_of_header_address(u64::from_le_bytes(raw))?
}
};
let dim: [u8; 4] = at(dimension.offset, 4)?.try_into().expect("4 bytes");
let dim = match byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes(dim),
ByteOrder::BigEndian => u32::from_be_bytes(dim),
};
entries.push((path, dim));
}
Ok(entries)
}
fn write_reference_list(&self, dsid: usize, entries: &[(String, u32)]) -> IoResult<()> {
use crate::format::messages::datatype::CompoundMember;
let target = AttrTarget::Dataset(dsid);
self.evict_attr(target, REFERENCE_LIST)?;
let sa = self.ctx.sizeof_addr as usize;
let stride = sa + 8;
let datatype = DatatypeMessage::compound(
stride as u32,
vec![
CompoundMember {
name: "dataset".to_string(),
offset: 0,
datatype: DatatypeMessage::object_reference(&self.ctx),
},
CompoundMember {
name: "dimension".to_string(),
offset: sa as u32,
datatype: DatatypeMessage::u32_type(),
},
],
);
let mut data = vec![0u8; entries.len() * stride];
for (i, (_, dim)) in entries.iter().enumerate() {
data[i * stride + sa..i * stride + sa + 4].copy_from_slice(&dim.to_le_bytes());
}
let attr = AttributeMessage::array_numeric(
REFERENCE_LIST,
datatype,
&[entries.len() as u64],
data,
);
self.insert_attribute(target, attr, Created)?;
self.attribute_references
.lock()
.push(AttributeReferenceValue {
scope: AttrScope::Dataset(dsid),
name: REFERENCE_LIST.to_string(),
targets: entries.iter().map(|(p, _)| p.clone()).collect(),
stride,
});
Ok(())
}
fn evict_attr(&self, target: AttrTarget<'_>, name: &str) -> IoResult<AttrOrigin> {
if self.swmr_active {
return Err(swmr_attr_error(name));
}
self.forget_attribute_reference(self.attr_scope(target)?, name);
let old = self.with_attr_list(target, |attrs| {
attrs
.iter()
.position(|a| a.name() == name)
.map(|pos| attrs.remove(pos))
})?;
match old {
Some(old) => {
let origin = Rewritten(old.creation_index());
self.release_attr_vlen(&old)?;
Ok(origin)
}
None => Ok(Created),
}
}
fn release_attr_vlen(&self, old: &AttributeEntry) -> IoResult<()> {
use crate::format::messages::datatype::DatatypeMessage;
let Some(old) = old.readable() else {
return Ok(());
};
if matches!(
old.datatype,
DatatypeMessage::VarLenString { .. } | DatatypeMessage::VarLenSequence { .. }
) {
self.release_vlen_references(&old.data)?;
}
Ok(())
}
fn with_attr_list<R>(
&self,
target: AttrTarget<'_>,
f: impl FnOnce(&mut Vec<AttributeEntry>) -> R,
) -> IoResult<R> {
match target {
AttrTarget::Root => Ok(f(&mut self.root_attributes.lock())),
AttrTarget::Group(path) => {
let path = self.canonical_group_path(path);
for grp in self.group_refs() {
let mut g = grp.lock();
if g.name == path && !g.deleted {
return Ok(f(&mut g.attributes));
}
}
Err(crate::io::IoError::NotFound(format!(
"group '{path}' not found"
)))
}
AttrTarget::Dataset(index) => {
let count = self.dataset_count();
if index >= count {
return Err(crate::io::IoError::InvalidState(format!(
"dataset index {index} out of range (have {count})"
)));
}
let ds = self.ds(index);
let mut m = ds.lock();
m.header_dirty = true;
Ok(f(&mut m.attributes))
}
}
}
fn insert_vlen_objects(&self, items: &[&[u8]]) -> IoResult<Vec<(u64, u16)>> {
use crate::format::global_heap::{GlobalHeapCollection, GlobalHeapObject};
if items.is_empty() {
return Ok(Vec::new());
}
let objhdr = GlobalHeapCollection::object_disk_size(&self.ctx, 0);
let mut placements = Vec::with_capacity(items.len());
let mut i = 0;
if !self.swmr_active {
let mut cwfs = self.cwfs.lock();
while i < items.len() {
let need = GlobalHeapCollection::object_disk_size(&self.ctx, items[i].len());
let Some(pos) = cwfs.iter().position(|e| e.free >= need + objhdr) else {
if self.extend_listed_collection(&mut cwfs, need + objhdr)? {
continue;
}
break;
};
let (addr, size) = (cwfs[pos].addr, cwfs[pos].size);
let image = self.handle.read_at(addr, size)?;
let (mut gcol, _) = GlobalHeapCollection::decode(&image[..size], &self.ctx)?;
let Some(mut free) = gcol.free_space_at(&self.ctx, size) else {
cwfs.remove(pos);
continue;
};
let mut next_idx = gcol.max_index();
let mut took = false;
while i < items.len() && next_idx < u16::MAX {
let need = GlobalHeapCollection::object_disk_size(&self.ctx, items[i].len());
if free < need + objhdr {
break;
}
next_idx += 1;
gcol.objects.push(GlobalHeapObject {
index: next_idx,
ref_count: 0,
data: items[i].to_vec(),
});
placements.push((addr, next_idx));
free -= need;
took = true;
i += 1;
}
if took {
let rewritten = gcol.encode_at_size(&self.ctx, size)?;
self.handle.write_at(addr, &rewritten)?;
let mut e = cwfs.remove(pos);
e.free = free;
cwfs.insert(0, e);
} else if next_idx == u16::MAX {
cwfs.remove(pos);
} else {
cwfs[pos].free = free;
}
}
}
while i < items.len() {
let mut gcol = GlobalHeapCollection::new();
let mut next_idx: u16 = 0;
while i < items.len() && next_idx < u16::MAX {
next_idx += 1;
gcol.objects.push(GlobalHeapObject {
index: next_idx,
ref_count: 0,
data: items[i].to_vec(),
});
i += 1;
}
let encoded = gcol.encode(&self.ctx);
let addr = self
.allocator
.allocate(encoded.len() as u64, FreeSpaceClass::RawData);
self.handle.write_at(addr, &encoded)?;
for idx in 1..=next_idx {
placements.push((addr, idx));
}
if !self.swmr_active {
if let Some(free) = gcol.free_space_at(&self.ctx, encoded.len()) {
if free >= 2 * objhdr {
cwfs_note(&mut self.cwfs.lock(), addr, encoded.len(), free);
}
}
}
}
Ok(placements)
}
fn extend_listed_collection(&self, cwfs: &mut Vec<CwfsEntry>, want: usize) -> IoResult<bool> {
use crate::format::global_heap::{GlobalHeapCollection, GCOL_MAX_SIZE};
let mut pos = 0;
while pos < cwfs.len() {
let (addr, size) = (cwfs[pos].addr, cwfs[pos].size);
let image = self.handle.read_at(addr, size)?;
let (gcol, _) = GlobalHeapCollection::decode(&image[..size], &self.ctx)?;
let Some(free) = gcol.free_space_at(&self.ctx, size) else {
cwfs.remove(pos);
continue;
};
if free >= want {
cwfs[pos].free = free;
return Ok(true);
}
let new_need = size.max(want.saturating_sub(free));
if size + new_need > GCOL_MAX_SIZE
|| !self.allocator.try_extend(
addr,
size as u64,
new_need as u64,
FreeSpaceClass::RawData,
)
{
pos += 1;
continue;
}
let new_size = size + new_need;
let rewritten = gcol.encode_at_size(&self.ctx, new_size)?;
self.handle.write_at(addr, &rewritten)?;
let mut e = cwfs.remove(pos);
e.size = new_size;
e.free = free + new_need;
cwfs.insert(0, e);
return Ok(true);
}
Ok(false)
}
pub fn create_vlen_string_dataset(
&self,
name: &str,
strings: &[&str],
charset: u8,
) -> IoResult<usize> {
use crate::format::global_heap::encode_vlen_reference;
use crate::format::messages::datatype::DatatypeMessage;
ensure_vlen_charset(charset, strings)?;
let create = self.begin_create(name)?;
let name = create.name.as_str();
let num_strings = strings.len() as u64;
let items: Vec<&[u8]> = strings.iter().map(|s| s.as_bytes()).collect();
let placements = self.insert_vlen_objects(&items)?;
let ref_size = crate::format::global_heap::vlen_reference_size(&self.ctx);
let data_size = (num_strings as usize) * ref_size;
let mut raw_data = Vec::with_capacity(data_size);
for (i, &(gcol_addr, obj_idx)) in placements.iter().enumerate() {
let seq_len = crate::format::global_heap::vlen_seq_len(strings[i].len())?;
raw_data.extend_from_slice(&encode_vlen_reference(
seq_len,
gcol_addr,
obj_idx as u32,
&self.ctx,
));
}
let data_addr = self
.allocator
.allocate(data_size as u64, FreeSpaceClass::RawData);
self.handle.write_at(data_addr, &raw_data)?;
let datatype = DatatypeMessage::VarLenString {
padding: 0,
charset,
};
let dataspace =
crate::format::messages::dataspace::DataspaceMessage::simple(&[num_strings]);
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr,
data_size: data_size as u64,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: 4,
times: self.created_object_times(),
chunked: None,
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
append: None,
},
);
Ok(idx)
}
#[cfg(test)]
pub fn create_vlen_bytes_dataset(&self, name: &str, items: &[&[u8]]) -> IoResult<usize> {
use crate::format::messages::datatype::DatatypeMessage;
self.create_vlen_sequence_dataset(name, DatatypeMessage::u8_type(), items)
}
pub fn create_vlen_sequence_dataset(
&self,
name: &str,
base: DatatypeMessage,
items: &[&[u8]],
) -> IoResult<usize> {
use crate::format::global_heap::encode_vlen_reference;
use crate::format::messages::datatype::DatatypeMessage;
let elem_size = base.element_size() as usize;
if elem_size == 0 {
return Err(crate::io::IoError::InvalidState(format!(
"vlen base datatype {base} has no element size"
)));
}
for (i, item) in items.iter().enumerate() {
if !item.len().is_multiple_of(elem_size) {
return Err(crate::io::IoError::InvalidState(format!(
"sequence {i} is {} bytes, not a whole number of {elem_size}-byte elements",
item.len()
)));
}
}
let create = self.begin_create(name)?;
let name = create.name.as_str();
let num_items = items.len() as u64;
let placements = self.insert_vlen_objects(items)?;
let ref_size = crate::format::global_heap::vlen_reference_size(&self.ctx);
let data_size = (num_items as usize) * ref_size;
let mut raw_data = Vec::with_capacity(data_size);
for (i, &(gcol_addr, obj_idx)) in placements.iter().enumerate() {
let seq_len = crate::format::global_heap::vlen_seq_len(items[i].len() / elem_size)?;
raw_data.extend_from_slice(&encode_vlen_reference(
seq_len,
gcol_addr,
obj_idx as u32,
&self.ctx,
));
}
let data_addr = self
.allocator
.allocate(data_size as u64, FreeSpaceClass::RawData);
self.handle.write_at(data_addr, &raw_data)?;
let datatype = DatatypeMessage::VarLenSequence {
base: Box::new(base),
};
let dataspace = crate::format::messages::dataspace::DataspaceMessage::simple(&[num_items]);
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr,
data_size: data_size as u64,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: 4,
times: self.created_object_times(),
chunked: None,
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
append: None,
},
);
Ok(idx)
}
pub fn create_vlen_string_dataset_compressed(
&self,
name: &str,
strings: &[&str],
chunk_size: usize,
pipeline: FilterPipeline,
) -> IoResult<usize> {
use crate::format::global_heap::encode_vlen_reference;
use crate::format::messages::datatype::DatatypeMessage;
let create = self.begin_create(name)?;
let name = create.name.as_str();
let num_strings = strings.len() as u64;
validate_chunk_geometry(&[num_strings], &[num_strings], &[chunk_size as u64])?;
let items: Vec<&[u8]> = strings.iter().map(|s| s.as_bytes()).collect();
let placements = self.insert_vlen_objects(&items)?;
let ref_size = crate::format::global_heap::vlen_reference_size(&self.ctx);
let data_size = (num_strings as usize) * ref_size;
let mut raw_data = Vec::with_capacity(data_size);
for (i, &(gcol_addr, obj_idx)) in placements.iter().enumerate() {
let seq_len = crate::format::global_heap::vlen_seq_len(strings[i].len())?;
raw_data.extend_from_slice(&encode_vlen_reference(
seq_len,
gcol_addr,
obj_idx as u32,
&self.ctx,
));
}
let datatype = DatatypeMessage::vlen_string_utf8();
let element_size = datatype.element_size_ctx(&self.ctx) as u64;
let chunk_dims: Vec<u64> = vec![chunk_size as u64];
let dims: Vec<u64> = vec![num_strings];
let max_dims: Vec<u64> = vec![num_strings];
let chunk_bytes = chunk_size as u64 * element_size;
let layout_version = self.chunk_layout_version(true, chunk_bytes);
let chunk_size_len = self.chunk_size_len_for(layout_version, chunk_bytes);
let earray_params = EarrayParams::default_params();
let ndblk_addrs = compute_ndblk_addrs(earray_params.sup_blk_min_data_ptrs)?;
let nsblk_addrs = compute_nsblk_addrs(
earray_params.idx_blk_elmts,
earray_params.data_blk_min_elmts,
earray_params.sup_blk_min_data_ptrs,
earray_params.max_nelmts_bits,
)?;
let mut ea_header =
ExtensibleArrayHeader::new_for_filtered_chunks(&self.ctx, chunk_size_len);
ea_header.max_nelmts_bits = earray_params.max_nelmts_bits;
ea_header.idx_blk_elmts = earray_params.idx_blk_elmts;
ea_header.data_blk_min_elmts = earray_params.data_blk_min_elmts;
ea_header.sup_blk_min_data_ptrs = earray_params.sup_blk_min_data_ptrs;
ea_header.max_dblk_page_nelmts_bits = earray_params.max_dblk_page_nelmts_bits;
let hdr_encoded = ea_header.encode(&self.ctx);
let ea_header_addr = self
.allocator
.allocate(hdr_encoded.len() as u64, FreeSpaceClass::Metadata);
let filt_iblk = FilteredIndexBlock::new(
ea_header_addr,
earray_params.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
);
let iblk_encoded = filt_iblk.encode(&self.ctx, chunk_size_len);
let ea_iblk_addr = self
.allocator
.allocate(iblk_encoded.len() as u64, FreeSpaceClass::Metadata);
ea_header.idx_blk_addr = ea_iblk_addr;
let hdr_encoded = ea_header.encode(&self.ctx);
self.handle.write_at(ea_header_addr, &hdr_encoded)?;
self.handle.write_at(ea_iblk_addr, &iblk_encoded)?;
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(max_dims.to_vec()),
};
let ea_iblk = ExtensibleArrayIndexBlock::new(
ea_header_addr,
earray_params.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
);
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: Some(pipeline),
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
chunked: Some(ChunkedDatasetInfo {
chunk_dims: chunk_dims.clone(),
earray_params,
ea_header_addr,
ea_iblk_addr,
ea_header,
ea_iblk,
chunks_written: 0,
filt_iblk: Some(filt_iblk),
chunk_size_len,
}),
append: None,
},
);
let chunk_byte_size = chunk_bytes as usize;
let num_chunks = raw_data.len().div_ceil(chunk_byte_size);
for chunk_i in 0..num_chunks {
let start = chunk_i * chunk_byte_size;
let end = (start + chunk_byte_size).min(raw_data.len());
let chunk_data = if end - start < chunk_byte_size {
let mut padded = self.new_chunk_buffer(idx, chunk_byte_size);
padded[..end - start].copy_from_slice(&raw_data[start..end]);
padded
} else {
raw_data[start..end].to_vec()
};
self.write_chunk(idx, chunk_i as u64, &chunk_data)?;
}
Ok(idx)
}
pub fn create_appendable_vlen_string_dataset(
&self,
name: &str,
chunk_size: usize,
pipeline: Option<FilterPipeline>,
) -> IoResult<usize> {
let datatype = DatatypeMessage::vlen_string_utf8();
let chunk_dims: Vec<u64> = vec![chunk_size as u64];
let dims: Vec<u64> = vec![0];
let max_dims: Vec<u64> = vec![u64::MAX];
if let Some(ref pl) = pipeline {
self.create_chunked_dataset_with_pipeline(
name,
datatype,
&dims,
&max_dims,
&chunk_dims,
pl.clone(),
)
} else {
self.create_chunked_dataset(name, datatype, &dims, &max_dims, &chunk_dims)
}
}
pub fn append_vlen_strings(&self, ds_index: usize, strings: &[&str]) -> IoResult<()> {
use crate::format::global_heap::encode_vlen_reference;
use crate::format::messages::datatype::DatatypeMessage;
if strings.is_empty() {
return Ok(());
}
let cell = self.ds(ds_index);
let _op = cell.op.lock();
let charset = {
let ds = self.ds(ds_index);
let m = ds.lock();
match m.datatype {
DatatypeMessage::VarLenString { charset, .. } => charset,
_ => {
return Err(crate::io::IoError::InvalidState(
"append_vlen_strings is only for variable-length string datasets".into(),
))
}
}
};
ensure_vlen_charset(charset, strings)?;
let chunk_dims = self
.dataset_chunk_dims(ds_index)
.ok_or_else(|| crate::io::IoError::InvalidState("not a chunked dataset".into()))?
.to_vec();
let dims = self.dataset_dims(ds_index).to_vec();
let items: Vec<&[u8]> = strings.iter().map(|s| s.as_bytes()).collect();
let placements = self.insert_vlen_objects(&items)?;
let ref_size = crate::format::global_heap::vlen_reference_size(&self.ctx);
let mut raw = Vec::with_capacity(strings.len() * ref_size);
for (i, &(gcol_addr, obj_idx)) in placements.iter().enumerate() {
let seq_len = crate::format::global_heap::vlen_seq_len(strings[i].len())?;
raw.extend_from_slice(&encode_vlen_reference(
seq_len,
gcol_addr,
obj_idx as u32,
&self.ctx,
));
}
let n_new_frames = strings.len();
let current_dim0 = dims[0] as usize;
let chunk_dim0 = chunk_dims[0] as usize;
let frame_bytes = ref_size;
let taken = { self.ds(ds_index).lock().append.take() };
let (base_dim0, buffered_frames, mut combined) = match taken {
Some(b) if b.base + b.frames == current_dim0 as u64 => {
(b.base as usize, b.frames as usize, b.bytes)
}
Some(b) => {
self.write_append_frames(ds_index, b.base, b.frames, &b.bytes)?;
(current_dim0, 0, Vec::new())
}
None => (current_dim0, 0, Vec::new()),
};
combined.extend_from_slice(&raw);
let total_frames = buffered_frames + n_new_frames;
let last_boundary = ((base_dim0 + total_frames) / chunk_dim0) * chunk_dim0;
let write_frames = last_boundary.saturating_sub(base_dim0);
let tail_frames = total_frames - write_frames;
if write_frames > 0 {
self.write_append_frames(
ds_index,
base_dim0 as u64,
write_frames as u64,
&combined[..write_frames * frame_bytes],
)?;
}
if tail_frames > 0 {
let ds = self.ds(ds_index);
let mut m = ds.lock();
m.append = Some(AppendBuffer {
base: (base_dim0 + write_frames) as u64,
frames: tail_frames as u64,
bytes: combined[write_frames * frame_bytes..].to_vec(),
});
}
let logical_dim0 = base_dim0 + total_frames;
let mut new_dims = dims;
new_dims[0] = logical_dim0 as u64;
self.extend_dataset_inner(ds_index, &new_dims)?;
Ok(())
}
pub fn write_vlen_strings_slice(
&self,
ds_index: usize,
start: u64,
strings: &[&str],
) -> IoResult<()> {
use crate::format::global_heap::{encode_vlen_reference, vlen_reference_size};
use crate::format::messages::datatype::DatatypeMessage;
if strings.is_empty() {
return Ok(());
}
let cell = self.ds(ds_index);
let _op = cell.op.lock();
let (charset, dims, writable) = {
let ds = self.ds(ds_index);
let m = ds.lock();
let charset = match m.datatype {
DatatypeMessage::VarLenString { charset, .. } => charset,
_ => {
return Err(crate::io::IoError::InvalidState(
"write_vlen_strings_slice is only for variable-length string datasets"
.into(),
))
}
};
let writable = if m.is_chunked() {
Ok(())
} else {
match m.contiguous_target() {
Some(ContiguousTarget::Virtual) => Err(virtual_write_refused()),
Some(_) => Ok(()),
None => Err(crate::io::IoError::InvalidState(
"dataset has no data allocated".into(),
)),
}
};
(charset, m.dataspace.dims.clone(), writable)
};
writable?;
if dims.len() != 1 {
return Err(crate::io::IoError::InvalidState(format!(
"write_vlen_strings_slice is only for 1-dimension datasets, this one has {}",
dims.len()
)));
}
let end = start + strings.len() as u64;
if end > dims[0] {
return Err(crate::io::IoError::InvalidState(format!(
"elements {start}..{end} are outside the dataset's {} elements",
dims[0]
)));
}
ensure_vlen_charset(charset, strings)?;
let ref_size = vlen_reference_size(&self.ctx);
self.flush_append_buffer_if_intersecting(ds_index, start, end)?;
let superseded = self.current_element_bytes(ds_index, start, end - start, ref_size)?;
self.release_vlen_references(&superseded)?;
let items: Vec<&[u8]> = strings.iter().map(|s| s.as_bytes()).collect();
let placements = self.insert_vlen_objects(&items)?;
let mut refs = Vec::with_capacity(strings.len() * ref_size);
for (i, &(gcol_addr, obj_idx)) in placements.iter().enumerate() {
refs.extend_from_slice(&encode_vlen_reference(
crate::format::global_heap::vlen_seq_len(strings[i].len())?,
gcol_addr,
obj_idx as u32,
&self.ctx,
));
}
self.write_slice_inner(ds_index, &[start], &[strings.len() as u64], &refs)?;
Ok(())
}
fn current_element_bytes(
&self,
ds_index: usize,
start: u64,
count: u64,
element_size: usize,
) -> IoResult<Vec<u8>> {
let mut out = vec![0u8; count as usize * element_size];
if count == 0 {
return Ok(out);
}
let (is_chunked, data_addr) = {
let ds = self.ds(ds_index);
let m = ds.lock();
(m.is_chunked(), m.data_addr)
};
if !is_chunked {
if data_addr != UNDEF_ADDR {
let at = data_addr + start * element_size as u64;
let got = self.handle.read_at_most(at, out.len())?;
out[..got.len()].copy_from_slice(&got);
}
return Ok(out);
}
let geo = self.chunk_geometry(ds_index)?;
let per_chunk = geo.chunk_dims[0];
if per_chunk == 0 {
return Err(crate::io::IoError::InvalidState(
"chunk shape has a zero-length dimension".into(),
));
}
let end = start + count;
for c in (start / per_chunk)..=((end - 1) / per_chunk) {
let origin = c * per_chunk;
let lo = start.max(origin);
let hi = end.min(origin + per_chunk);
let Some(chunk) = self.read_chunk_at_coords(ds_index, &[c])? else {
continue;
};
let src = ((lo - origin) as usize) * element_size;
let dst = ((lo - start) as usize) * element_size;
let len = ((hi - lo) as usize) * element_size;
if src + len > chunk.len() {
return Err(crate::io::IoError::InvalidState(format!(
"chunk {c} is {} bytes, too short for elements {lo}..{hi}",
chunk.len()
)));
}
out[dst..dst + len].copy_from_slice(&chunk[src..src + len]);
}
Ok(out)
}
fn release_vlen_references(&self, refs: &[u8]) -> IoResult<()> {
use crate::format::global_heap::{decode_vlen_reference, vlen_reference_size};
let ref_size = vlen_reference_size(&self.ctx);
if ref_size == 0 || refs.len() < ref_size {
return Ok(());
}
let mut per_collection: std::collections::BTreeMap<u64, Vec<u16>> = Default::default();
for r in refs.chunks_exact(ref_size) {
let (_seq_len, addr, obj_idx) = decode_vlen_reference(r, &self.ctx)?;
if addr == 0 || addr == UNDEF_ADDR {
continue;
}
let Ok(idx) = u16::try_from(obj_idx) else {
return Err(crate::io::IoError::InvalidState(format!(
"global heap object index {obj_idx} does not fit the 16-bit on-disk field"
)));
};
per_collection.entry(addr).or_default().push(idx);
}
self.remove_heap_objects(per_collection)
}
fn remove_heap_objects(
&self,
per_collection: std::collections::BTreeMap<u64, Vec<u16>>,
) -> IoResult<()> {
use crate::format::global_heap::GlobalHeapCollection;
if self.swmr_active {
return Ok(());
}
self.pending_heap_references.lock().retain(|p| {
!per_collection
.get(&p.collection)
.is_some_and(|indices| indices.contains(&p.index))
});
let objhdr = GlobalHeapCollection::object_disk_size(&self.ctx, 0);
let mut cwfs = self.cwfs.lock();
for (addr, indices) in per_collection {
let mut image = self.handle.read_at_most(addr, 4096)?;
let declared = GlobalHeapCollection::decode_size(&image, &self.ctx)?;
if declared > image.len() {
image = self.handle.read_at(addr, declared)?;
}
let (mut gcol, _) = GlobalHeapCollection::decode(&image[..declared], &self.ctx)?;
let mut removed_any = false;
for idx in indices {
removed_any |= gcol.remove_object(idx);
}
if !removed_any {
continue;
}
if gcol.is_empty() {
self.allocator
.free(addr, declared as u64, FreeSpaceClass::RawData);
cwfs.retain(|e| e.addr != addr);
} else {
let rewritten = gcol.encode_at_size(&self.ctx, declared)?;
self.handle.write_at(addr, &rewritten)?;
if let Some(free) = gcol.free_space_at(&self.ctx, declared) {
if free >= 2 * objhdr {
cwfs_note(&mut cwfs, addr, declared, free);
}
}
}
}
Ok(())
}
pub fn add_dataset_attribute(&self, ds_index: usize, attr: AttributeMessage) -> IoResult<()> {
self.set_attribute(AttrTarget::Dataset(ds_index), attr)
}
fn vlen_string_attribute(&self, name: &str, value: &str) -> IoResult<AttributeMessage> {
use crate::format::global_heap::encode_vlen_reference;
use crate::format::messages::dataspace::DataspaceMessage;
use crate::format::messages::datatype::DatatypeMessage;
let (gcol_addr, obj_idx) = self.insert_vlen_objects(&[value.as_bytes()])?[0];
let seq_len = crate::format::global_heap::vlen_seq_len(value.len())?;
let data = encode_vlen_reference(seq_len, gcol_addr, obj_idx as u32, &self.ctx);
Ok(AttributeMessage {
name: name.to_string(),
datatype: DatatypeMessage::vlen_string_utf8(),
dataspace: DataspaceMessage::scalar(),
data,
})
}
fn vlen_string_array_attribute(
&self,
name: &str,
values: &[&str],
shape: &[u64],
) -> IoResult<AttributeMessage> {
use crate::format::global_heap::encode_vlen_reference;
use crate::format::messages::dataspace::DataspaceMessage;
use crate::format::messages::datatype::DatatypeMessage;
debug_assert_eq!(
values.len() as u64,
shape.iter().product::<u64>(),
"vlen_string_array_attribute values.len() must equal product(shape)"
);
let items: Vec<&[u8]> = values.iter().map(|v| v.as_bytes()).collect();
let placements = self.insert_vlen_objects(&items)?;
let mut data = Vec::with_capacity(values.len() * 16);
for (i, &(gcol_addr, obj_idx)) in placements.iter().enumerate() {
data.extend_from_slice(&encode_vlen_reference(
crate::format::global_heap::vlen_seq_len(values[i].len())?,
gcol_addr,
obj_idx as u32,
&self.ctx,
));
}
Ok(AttributeMessage {
name: name.to_string(),
datatype: DatatypeMessage::vlen_string_utf8(),
dataspace: DataspaceMessage::simple(shape),
data,
})
}
pub fn set_dataset_fill_value(&self, ds_index: usize, bytes: Vec<u8>) -> IoResult<()> {
let count = self.dataset_count();
if ds_index >= count {
return Err(crate::io::IoError::InvalidState(format!(
"dataset index {} out of range",
ds_index
)));
}
let ds_ref = self.ds(ds_index);
let mut ds = ds_ref.lock();
let es = ds.datatype.element_size() as usize;
if bytes.len() != es {
return Err(crate::io::IoError::InvalidState(format!(
"fill value is {} bytes but dataset element size is {}",
bytes.len(),
es
)));
}
let fills_per_chunk = ds
.chunk_index_kind()
.is_some_and(|k| k != ChunkIndexKind::Implicit);
if !fills_per_chunk && ds.fill_time != FILL_TIME_NEVER {
if let Some(len) = ds.compact.as_ref().map(Vec::len) {
ds.compact = Some(crate::format::messages::fill_value::tiled_fill(
len,
Some(&bytes),
));
} else {
let run = ds.allocated_storage_run();
if let Some((target, data_size)) = run.filter(|&(_, size)| size > 0) {
let filled = crate::format::messages::fill_value::tiled_fill(
data_size as usize,
Some(&bytes),
);
self.write_contiguous_bytes(&target, 0, &filled)?;
}
}
}
ds.fill_value = Some(bytes);
ds.header_dirty = true;
Ok(())
}
pub fn set_dataset_fill_time(&self, ds_index: usize, time: u8) -> IoResult<()> {
if !matches!(time, FILL_TIME_ALLOC | FILL_TIME_NEVER | FILL_TIME_IFSET) {
return Err(crate::io::IoError::InvalidState(format!(
"invalid fill time {time}; must be {FILL_TIME_ALLOC} (alloc), \
{FILL_TIME_NEVER} (never) or {FILL_TIME_IFSET} (if-set)"
)));
}
let count = self.dataset_count();
if ds_index >= count {
return Err(crate::io::IoError::InvalidState(format!(
"dataset index {} out of range",
ds_index
)));
}
let ds_ref = self.ds(ds_index);
let mut ds = ds_ref.lock();
ds.fill_time = time;
ds.header_dirty = true;
Ok(())
}
pub(crate) fn new_chunk_buffer(&self, ds_index: usize, chunk_bytes: usize) -> Vec<u8> {
let ds = self.ds(ds_index);
let m = ds.lock();
let fv = m.fill_value.as_deref();
crate::format::messages::fill_value::tiled_fill(chunk_bytes, fv)
}
fn new_write_chunk_buffer(&self, ds_index: usize, chunk_bytes: usize) -> Vec<u8> {
let never = {
let ds = self.ds(ds_index);
let m = ds.lock();
m.fill_time == FILL_TIME_NEVER
};
if never {
vec![0u8; chunk_bytes]
} else {
self.new_chunk_buffer(ds_index, chunk_bytes)
}
}
pub(crate) fn write_append_frames(
&self,
ds_index: usize,
base_frame: u64,
n_frames: u64,
frames: &[u8],
) -> IoResult<()> {
if n_frames == 0 {
return Ok(());
}
let geo = self.chunk_geometry(ds_index)?;
let mut starts = vec![0u64; geo.dims.len()];
starts[0] = base_frame;
let mut counts = geo.dims.clone();
counts[0] = n_frames;
let expected = counts.iter().product::<u64>() * geo.element_size;
if frames.len() as u64 != expected {
return Err(crate::io::IoError::InvalidState(format!(
"{n_frames} frames at rows {base_frame}.. need {expected} bytes, got {}",
frames.len()
)));
}
self.write_slice_chunked(ds_index, &starts, &counts, frames)
}
pub(crate) fn flush_append_buffer(&self, ds_index: usize) -> IoResult<()> {
let taken = { self.ds(ds_index).lock().append.take() };
match taken {
Some(b) => self.write_append_frames(ds_index, b.base, b.frames, &b.bytes),
None => Ok(()),
}
}
pub(crate) fn flush_append_buffer_if_intersecting(
&self,
ds_index: usize,
start_row: u64,
end_row: u64,
) -> IoResult<()> {
let intersects = {
let ds = self.ds(ds_index);
let m = ds.lock();
m.append
.as_ref()
.is_some_and(|b| start_row < b.base + b.frames && end_row > b.base)
};
if intersects {
self.flush_append_buffer(ds_index)
} else {
Ok(())
}
}
pub(crate) fn read_chunk_if_present(
&self,
ds_index: usize,
chunk_idx: u64,
) -> IoResult<Option<Vec<u8>>> {
let ds = self.ds(ds_index);
let m = ds.lock();
let element_size = m.datatype.element_size() as u64;
let pipeline = m.filter_pipeline.clone();
let Some(chunked) = m.chunked.as_ref() else {
return Ok(None);
};
let chunk_bytes = chunked.chunk_dims.iter().product::<u64>() * element_size;
let max_nelmts_bits = chunked.earray_params.max_nelmts_bits;
let chunk_size_len = chunked.chunk_size_len;
let is_filtered = chunked.filt_iblk.is_some();
enum Loc {
Direct(u64, u64, u32),
DataBlock {
dblk_addr: u64,
offset: usize,
nelmts: usize,
},
}
let ea_loc = {
let p = &chunked.earray_params;
EaGeometry::new(
p.idx_blk_elmts,
p.data_blk_min_elmts,
p.sup_blk_min_data_ptrs,
p.max_nelmts_bits,
p.max_dblk_page_nelmts_bits,
)?
.locate(chunk_idx)?
};
let loc = match ea_loc {
EaLoc::Index { elem } => {
if is_filtered {
let e = &chunked.filt_iblk.as_ref().unwrap().elements[elem];
Loc::Direct(e.addr, e.nbytes, e.filter_mask)
} else {
Loc::Direct(chunked.ea_iblk.elements[elem], chunk_bytes, 0)
}
}
EaLoc::Dblk(l) => {
if l.paged {
return Err(crate::io::IoError::InvalidState(format!(
"chunk index {} lives in a paged extensible-array data \
block, which is not yet supported for read-modify-write",
chunk_idx
)));
}
let dblk_addr = match l.path {
EaDblkPath::Direct { idx } => {
if is_filtered {
chunked.filt_iblk.as_ref().unwrap().dblk_addrs[idx]
} else {
chunked.ea_iblk.dblk_addrs[idx]
}
}
EaDblkPath::ViaSblk {
sblk_off,
local_dblk,
ndblks_in_sblk,
..
} => {
let sblk_addr = if is_filtered {
chunked.filt_iblk.as_ref().unwrap().sblk_addrs[sblk_off]
} else {
chunked.ea_iblk.sblk_addrs[sblk_off]
};
if sblk_addr == UNDEF_ADDR {
return Ok(None);
}
let sb_buf = self.handle.read_at_most(sblk_addr, 65536)?;
let sb = ExtensibleArraySuperBlock::decode(
&sb_buf,
&self.ctx,
max_nelmts_bits,
ndblks_in_sblk,
0,
)?;
sb.dblk_addrs[local_dblk]
}
};
if dblk_addr == UNDEF_ADDR {
return Ok(None);
}
Loc::DataBlock {
dblk_addr,
offset: l.offset_in_dblk as usize,
nelmts: l.dblk_nelmts as usize,
}
}
};
let (addr, nbytes, mask) = match loc {
Loc::Direct(a, n, m) => (a, n, m),
Loc::DataBlock {
dblk_addr,
offset,
nelmts,
} => {
let buf = self.handle.read_at_most(dblk_addr, 65536)?;
if is_filtered {
let dblk = FilteredDataBlock::decode(
&buf,
&self.ctx,
max_nelmts_bits,
nelmts,
chunk_size_len,
)?;
let e = &dblk.elements[offset];
(e.addr, e.nbytes, e.filter_mask)
} else {
let dblk =
ExtensibleArrayDataBlock::decode(&buf, &self.ctx, max_nelmts_bits, nelmts)?;
(dblk.elements[offset], chunk_bytes, 0)
}
}
};
self.read_chunk_block(pipeline.as_ref(), addr, nbytes, mask)
}
fn read_chunk_block(
&self,
pipeline: Option<&FilterPipeline>,
addr: u64,
nbytes: u64,
mask: u32,
) -> IoResult<Option<Vec<u8>>> {
if addr == UNDEF_ADDR || nbytes == 0 {
return Ok(None);
}
let raw = self.handle.read_at(addr, nbytes as usize)?;
match pipeline {
Some(pl) => Ok(Some(filter::reverse_filters_masked(pl, &raw, mask)?)),
None => Ok(Some(raw)),
}
}
pub(crate) fn read_chunk_at_coords(
&self,
ds_index: usize,
chunk_coords: &[u64],
) -> IoResult<Option<Vec<u8>>> {
let geo = self.chunk_geometry(ds_index)?;
match geo.kind {
ChunkIndexKind::ExtensibleArray => {
let linear = geo.linear_index(chunk_coords)?;
self.read_chunk_if_present(ds_index, linear)
}
ChunkIndexKind::FixedArray => {
let linear = geo.linear_index(chunk_coords)?;
let ds = self.ds(ds_index);
let m = ds.lock();
let pipeline = m.filter_pipeline.clone();
let fa = m.fixed_array.as_ref().unwrap();
let lidx = linear as usize;
let (addr, nbytes, mask) = if pipeline.is_some() {
match fa.fa_dblk.filtered_elements.get(lidx) {
Some(e) => (e.address, e.chunk_size, e.filter_mask),
None => return Ok(None),
}
} else {
match fa.fa_dblk.elements.get(lidx) {
Some(&a) => (a, geo.chunk_bytes(), 0),
None => return Ok(None),
}
};
drop(m);
self.read_chunk_block(pipeline.as_ref(), addr, nbytes, mask)
}
ChunkIndexKind::BtreeV2 => {
let ds = self.ds(ds_index);
let m = ds.lock();
let pipeline = m.filter_pipeline.clone();
let bt2 = m.btree_v2.as_ref().unwrap();
let found = if bt2.index.filtered {
bt2.index
.lookup_filtered(chunk_coords)
.map(|r| (r.chunk_address, r.chunk_size, r.filter_mask))
} else {
bt2.index
.lookup(chunk_coords)
.map(|r| (r.chunk_address, geo.chunk_bytes(), 0))
};
drop(m);
match found {
Some((addr, nbytes, mask)) => {
self.read_chunk_block(pipeline.as_ref(), addr, nbytes, mask)
}
None => Ok(None),
}
}
ChunkIndexKind::Implicit => {
let (grid, offset) = self.implicit_chunk_slot(ds_index, &geo, chunk_coords)?;
self.read_chunk_block(None, grid + offset, geo.chunk_bytes(), 0)
}
ChunkIndexKind::SingleChunk => {
let ds = self.ds(ds_index);
let m = ds.lock();
let pipeline = m.filter_pipeline.clone();
let sc = m.single_chunk.as_ref().unwrap();
if sc.data_addr == UNDEF_ADDR {
return Ok(None);
}
let (addr, nbytes, mask) = if pipeline.is_some() {
(sc.data_addr, sc.nbytes, sc.filter_mask)
} else {
(sc.data_addr, geo.chunk_bytes(), 0)
};
drop(m);
self.read_chunk_block(pipeline.as_ref(), addr, nbytes, mask)
}
ChunkIndexKind::BtreeV1 => {
let ds = self.ds(ds_index);
let m = ds.lock();
let pipeline = m.filter_pipeline.clone();
let bt1 = m.btree_v1.as_ref().unwrap();
let found = bt1
.position(chunk_coords)
.ok()
.map(|i| &bt1.records[i])
.map(|r| (r.address, r.nbytes as u64, r.filter_mask));
drop(m);
match found {
Some((addr, nbytes, mask)) => {
self.read_chunk_block(pipeline.as_ref(), addr, nbytes, mask)
}
None => Ok(None),
}
}
}
}
fn implicit_chunk_slot(
&self,
ds_index: usize,
geo: &ChunkGeometry,
chunk_coords: &[u64],
) -> IoResult<(u64, u64)> {
let linear = geo.linear_index(chunk_coords)?;
let ds = self.ds(ds_index);
let m = ds.lock();
let (grid, grid_size) = m.implicit_grid().ok_or_else(|| {
crate::io::IoError::InvalidState("no implicitly indexed chunk grid".into())
})?;
let offset = linear.checked_mul(geo.chunk_bytes()).ok_or_else(|| {
crate::io::IoError::InvalidState("implicit chunk offset overflows u64".into())
})?;
if offset + geo.chunk_bytes() > grid_size {
return Err(crate::io::IoError::InvalidState(format!(
"chunk {chunk_coords:?} lies outside the {grid_size} bytes of chunk space \
this implicitly indexed dataset was created with"
)));
}
Ok((grid, offset))
}
pub(crate) fn write_chunk_at_coords(
&self,
ds_index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let geo = self.chunk_geometry(ds_index)?;
match geo.kind {
ChunkIndexKind::ExtensibleArray => {
let linear = geo.linear_index(chunk_coords)?;
self.write_chunk_inner(ds_index, linear, data)
}
ChunkIndexKind::FixedArray => {
self.write_chunk_fixed_array_inner(ds_index, chunk_coords, data)
}
ChunkIndexKind::BtreeV2 => {
self.write_chunk_btree_v2_inner(ds_index, chunk_coords, data)
}
ChunkIndexKind::Implicit => {
self.write_chunk_implicit_inner(ds_index, chunk_coords, data)
}
ChunkIndexKind::SingleChunk => {
self.write_chunk_single_chunk_inner(ds_index, chunk_coords, data)
}
ChunkIndexKind::BtreeV1 => {
self.write_chunk_btree_v1_inner(ds_index, chunk_coords, data)
}
}
}
pub(crate) fn write_chunk_btree_v1_inner(
&self,
ds_index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let ds = self.ds(ds_index);
let (chunk_bytes, pipeline) = {
let m = ds.lock();
let element_size = m.datatype.element_size() as u64;
let bt1 = m.btree_v1.as_ref().ok_or_else(|| {
crate::io::IoError::InvalidState("not a version-1 B-tree dataset".into())
})?;
(
bt1.chunk_dims.iter().product::<u64>() * element_size,
m.filter_pipeline.clone(),
)
};
if data.len() as u64 != chunk_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"chunk data size mismatch: expected {} bytes, got {}",
chunk_bytes,
data.len()
)));
}
let filtered;
let stored = match pipeline {
Some(ref pl) => {
filtered = filter::apply_filters(pl, data)?;
&filtered[..]
}
None => data,
};
self.record_btree_v1_chunk(ds_index, chunk_coords, stored, 0)
}
pub(crate) fn write_compressed_chunk_btree_v1_inner(
&self,
ds_index: usize,
chunk_coords: &[u64],
data: &[u8],
filter_mask: u32,
) -> IoResult<()> {
if self.ds(ds_index).lock().filter_pipeline.is_none() {
return Err(crate::io::IoError::InvalidState(
"write_chunk_raw requires a filtered dataset (an unfiltered chunk \
is stored at its full size, so there is nothing for a stored size \
or a filter mask to say)"
.into(),
));
}
self.record_btree_v1_chunk(ds_index, chunk_coords, data, filter_mask)
}
fn record_btree_v1_chunk(
&self,
ds_index: usize,
chunk_coords: &[u64],
final_bytes: &[u8],
filter_mask: u32,
) -> IoResult<()> {
let stored_len = final_bytes.len() as u64;
let Ok(nbytes) = u32::try_from(stored_len) else {
return Err(crate::io::IoError::InvalidState(format!(
"stored chunk size {stored_len} does not fit in the 32-bit size \
field of a version-1 B-tree chunk key"
)));
};
let ds = self.ds(ds_index);
let mut m = ds.lock();
let bt1 = m.btree_v1.as_ref().ok_or_else(|| {
crate::io::IoError::InvalidState("not a version-1 B-tree dataset".into())
})?;
if chunk_coords.len() != bt1.chunk_dims.len() {
return Err(crate::io::IoError::InvalidState(format!(
"chunk_coords has {} entries but the dataset has {} dimensions",
chunk_coords.len(),
bt1.chunk_dims.len()
)));
}
for (d, ((&c, &cd), &max)) in chunk_coords
.iter()
.zip(&bt1.chunk_dims)
.zip(&bt1.max_dims)
.enumerate()
{
if max != u64::MAX && c.saturating_mul(cd) >= max {
return Err(crate::io::IoError::InvalidState(format!(
"chunk coordinate {c} in dimension {d} is outside the maximum \
extent {max}"
)));
}
}
let slot = bt1.position(chunk_coords);
let old = slot.ok().map(|i| {
let r = &bt1.records[i];
(r.address, r.nbytes as u64)
});
let address = self.place_chunk(old, stored_len);
self.handle.write_at(address, final_bytes)?;
let bt1 = m.btree_v1.as_mut().unwrap();
let record = BtreeV1ChunkRecord {
scaled: chunk_coords.to_vec(),
address,
nbytes,
filter_mask,
};
match slot {
Ok(i) => bt1.records[i] = record,
Err(i) => bt1.records.insert(i, record),
}
bt1.chunks_written += 1;
Ok(())
}
pub(crate) fn write_chunk_implicit_inner(
&self,
ds_index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let geo = self.chunk_geometry(ds_index)?;
let chunk_bytes = geo.chunk_bytes();
if data.len() as u64 != chunk_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"chunk data size mismatch: expected {} bytes, got {}",
chunk_bytes,
data.len()
)));
}
let (grid, offset) = self.implicit_chunk_slot(ds_index, &geo, chunk_coords)?;
self.write_contiguous_bytes(&ContiguousTarget::Local(grid), offset, data)
}
fn chunk_geometry(&self, ds_index: usize) -> IoResult<ChunkGeometry> {
let ds = self.ds(ds_index);
let m = ds.lock();
let Some(kind) = m.chunk_index_kind() else {
return Err(crate::io::IoError::InvalidState(
"not a chunked dataset".into(),
));
};
let chunk_dims = match kind {
ChunkIndexKind::ExtensibleArray => m.chunked.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::FixedArray => m.fixed_array.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::BtreeV2 => m.btree_v2.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::Implicit => m.implicit.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::SingleChunk => m.single_chunk.as_ref().unwrap().chunk_dims.clone(),
ChunkIndexKind::BtreeV1 => m.btree_v1.as_ref().unwrap().chunk_dims.clone(),
};
Ok(ChunkGeometry {
kind,
dims: m.dataspace.dims.clone(),
max_dims: m.dataspace.max_dims.clone(),
chunk_dims,
element_size: m.datatype.element_size() as u64,
})
}
pub(crate) fn chunk_slot(&self, ds_index: usize, coords: &[u64]) -> IoResult<u64> {
self.chunk_geometry(ds_index)?.linear_index(coords)
}
pub(crate) fn chunk_coords_from_slot(
&self,
ds_index: usize,
linear: u64,
) -> IoResult<Vec<u64>> {
let geo = self.chunk_geometry(ds_index)?;
crate::io::chunk_grid::coords_of(
&geo.dims,
geo.max_dims.as_deref(),
&geo.chunk_dims,
linear,
)
}
pub fn create_fixed_array_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
chunk_dims: &[u64],
) -> IoResult<usize> {
self.create_fixed_array_dataset_with_max(name, datatype, dims, dims, chunk_dims, None)
}
#[cfg(all(test, feature = "deflate"))]
pub fn create_fixed_array_dataset_with_pipeline(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
chunk_dims: &[u64],
pipeline: FilterPipeline,
) -> IoResult<usize> {
self.create_fixed_array_dataset_with_max(
name,
datatype,
dims,
dims,
chunk_dims,
Some(pipeline),
)
}
pub fn create_fixed_array_dataset_with_max(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: &[u64],
chunk_dims: &[u64],
pipeline: Option<FilterPipeline>,
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, max_dims, chunk_dims)?;
if max_dims.contains(&u64::MAX) {
return Err(crate::io::IoError::InvalidState(
"a fixed-array index requires a fixed maximum shape (no unlimited dimension)"
.into(),
));
}
let mut num_chunks: u64 = 1;
for g in crate::io::chunk_grid::index_grid(dims, Some(max_dims), chunk_dims)? {
num_chunks = num_chunks.checked_mul(g).ok_or_else(|| {
crate::io::IoError::InvalidState("chunk count overflows u64".into())
})?;
}
let chunk_bytes: u64 = chunk_dims.iter().product::<u64>() * datatype.element_size() as u64;
let layout_version = self.chunk_layout_version(pipeline.is_some(), chunk_bytes);
let mut fa_header = if pipeline.is_some() {
let chunk_size_len = self.chunk_size_len_for(layout_version, chunk_bytes);
FixedArrayHeader::new_for_filtered_chunks(&self.ctx, num_chunks, chunk_size_len)
} else {
FixedArrayHeader::new_for_chunks(&self.ctx, num_chunks)
};
let hdr_encoded = fa_header.encode(&self.ctx);
let fa_header_addr = self
.allocator
.allocate(hdr_encoded.len() as u64, FreeSpaceClass::Metadata);
let fa_dblk = if pipeline.is_some() {
FixedArrayDataBlock::new_filtered(fa_header_addr, num_chunks as usize)
} else {
FixedArrayDataBlock::new_unfiltered(fa_header_addr, num_chunks as usize)
};
let dblk_size = fixed_array_dblk_disk_size(&self.ctx, &fa_header);
let fa_dblk_addr = self.allocator.allocate(dblk_size, FreeSpaceClass::Metadata);
fa_header.data_blk_addr = fa_dblk_addr;
let hdr_encoded = fa_header.encode(&self.ctx);
self.handle.write_at(fa_header_addr, &hdr_encoded)?;
let dblk_encoded = encode_fixed_array_dblk(&self.ctx, &fa_header, &fa_dblk);
debug_assert_eq!(dblk_encoded.len() as u64, dblk_size);
self.handle.write_at(fa_dblk_addr, &dblk_encoded)?;
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(max_dims.to_vec()),
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: pipeline,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
chunked: None,
btree_v2: None,
implicit: None,
single_chunk: None,
btree_v1: None,
fixed_array: Some(FixedArrayDatasetInfo {
chunk_dims: chunk_dims.to_vec(),
fa_header_addr,
fa_dblk_addr,
fa_header,
fa_dblk,
chunks_written: 0,
}),
append: None,
},
);
Ok(idx)
}
pub fn create_implicit_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
chunk_dims: &[u64],
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, dims, chunk_dims)?;
let mut num_chunks: u64 = 1;
for g in crate::io::chunk_grid::index_grid(dims, None, chunk_dims)? {
num_chunks = num_chunks.checked_mul(g).ok_or_else(|| {
crate::io::IoError::InvalidState("chunk count overflows u64".into())
})?;
}
let chunk_bytes: u64 = chunk_dims.iter().product::<u64>() * datatype.element_size() as u64;
let data_size = num_chunks.checked_mul(chunk_bytes).ok_or_else(|| {
crate::io::IoError::InvalidState("implicit chunk storage overflows u64".into())
})?;
let layout_version = self.chunk_layout_version(false, chunk_bytes);
let data_addr = self.allocator.allocate(data_size, FreeSpaceClass::RawData);
self.handle.write_at(
data_addr,
&crate::format::messages::fill_value::tiled_fill(data_size as usize, None),
)?;
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(dims.to_vec()),
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
chunked: None,
btree_v2: None,
fixed_array: None,
implicit: Some(ImplicitDatasetInfo {
chunk_dims: chunk_dims.to_vec(),
data_addr,
data_size,
}),
single_chunk: None,
btree_v1: None,
append: None,
},
);
Ok(idx)
}
pub fn create_single_chunk_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
chunk_dims: &[u64],
early_alloc: bool,
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, dims, chunk_dims)?;
let data_size = chunk_dims.iter().product::<u64>() * datatype.element_size() as u64;
let layout_version = self.chunk_layout_version(false, data_size);
let data_addr = if early_alloc {
let addr = self.allocator.allocate(data_size, FreeSpaceClass::RawData);
self.handle.write_at(
addr,
&crate::format::messages::fill_value::tiled_fill(data_size as usize, None),
)?;
addr
} else {
UNDEF_ADDR
};
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(dims.to_vec()),
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: None,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
chunked: None,
btree_v2: None,
fixed_array: None,
implicit: None,
single_chunk: Some(SingleChunkDatasetInfo {
chunk_dims: chunk_dims.to_vec(),
data_addr,
data_size,
nbytes: if early_alloc { data_size } else { 0 },
filter_mask: 0,
chunks_written: 0,
early_alloc,
}),
btree_v1: None,
append: None,
},
);
Ok(idx)
}
pub fn create_single_chunk_dataset_with_pipeline(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
chunk_dims: &[u64],
pipeline: FilterPipeline,
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, dims, chunk_dims)?;
let data_size = chunk_dims.iter().product::<u64>() * datatype.element_size() as u64;
let layout_version = self.chunk_layout_version(true, data_size);
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(dims.to_vec()),
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: Some(pipeline),
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
chunked: None,
btree_v2: None,
fixed_array: None,
implicit: None,
single_chunk: Some(SingleChunkDatasetInfo {
chunk_dims: chunk_dims.to_vec(),
data_addr: UNDEF_ADDR,
data_size,
nbytes: 0,
filter_mask: 0,
chunks_written: 0,
early_alloc: false,
}),
btree_v1: None,
append: None,
},
);
Ok(idx)
}
pub fn create_btree_v1_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: &[u64],
chunk_dims: &[u64],
pipeline: Option<FilterPipeline>,
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, max_dims, chunk_dims)?;
let chunk_bytes: u64 = chunk_dims.iter().product::<u64>() * datatype.element_size() as u64;
if chunk_bytes > u32::MAX as u64 {
return Err(crate::io::IoError::InvalidState(format!(
"a {chunk_bytes}-byte chunk does not fit the 32-bit size field of a \
version-1 B-tree chunk key"
)));
}
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(max_dims.to_vec()),
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: pipeline,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version: LAYOUT_VERSION_DEFAULT,
times: self.created_object_times(),
chunked: None,
fixed_array: None,
btree_v2: None,
implicit: None,
single_chunk: None,
btree_v1: Some(BtreeV1DatasetInfo {
chunk_dims: chunk_dims.to_vec(),
max_dims: max_dims.to_vec(),
config: self.btree_v1_config(),
records: Vec::new(),
node_addrs: Vec::new(),
root_addr: UNDEF_ADDR,
chunks_written: 0,
}),
append: None,
},
);
Ok(idx)
}
pub fn create_btree_v2_dataset(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: &[u64],
chunk_dims: &[u64],
) -> IoResult<usize> {
self.create_btree_v2_dataset_inner(name, datatype, dims, max_dims, chunk_dims, None)
}
pub fn create_btree_v2_dataset_with_pipeline(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: &[u64],
chunk_dims: &[u64],
pipeline: FilterPipeline,
) -> IoResult<usize> {
self.create_btree_v2_dataset_inner(
name,
datatype,
dims,
max_dims,
chunk_dims,
Some(pipeline),
)
}
fn create_btree_v2_dataset_inner(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: &[u64],
chunk_dims: &[u64],
pipeline: Option<FilterPipeline>,
) -> IoResult<usize> {
use crate::format::chunk_index::btree_v2::Bt2Header;
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, max_dims, chunk_dims)?;
let ndims = dims.len();
let chunk_bytes: u64 = chunk_dims.iter().product::<u64>() * datatype.element_size() as u64;
let layout_version = self.chunk_layout_version(pipeline.is_some(), chunk_bytes);
let bt2_index = match pipeline {
Some(_) => {
let len = self.chunk_size_len_for(layout_version, chunk_bytes);
Bt2ChunkIndex::new_filtered(ndims, len)
}
None => Bt2ChunkIndex::new_unfiltered(ndims),
};
let record_size = bt2_index.record_size(&self.ctx) as usize;
let node_size = bt2_index.node_size as usize;
if node_size < 10 + 3 * record_size {
return Err(crate::io::IoError::InvalidState(format!(
"a {ndims}-dimension v2 B-tree record is {record_size} bytes, too wide \
for a {node_size}-byte node"
)));
}
let hdr = if bt2_index.filtered {
Bt2Header::new_for_filtered_chunks(&self.ctx, ndims, bt2_index.chunk_size_len)
} else {
Bt2Header::new_for_chunks(&self.ctx, ndims)
};
let hdr_encoded = hdr.encode(&self.ctx);
let bt2_header_addr = self
.allocator
.allocate(hdr_encoded.len() as u64, FreeSpaceClass::Metadata);
self.handle.write_at(bt2_header_addr, &hdr_encoded)?;
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(max_dims.to_vec()),
};
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: pipeline,
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
chunked: None,
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: Some(Bt2DatasetInfo {
chunk_dims: chunk_dims.to_vec(),
bt2_header_addr,
node_addrs: Vec::new(),
index: bt2_index,
chunks_written: 0,
}),
append: None,
},
);
Ok(idx)
}
pub fn create_chunked_dataset_with_pipeline(
&self,
name: &str,
datatype: DatatypeMessage,
dims: &[u64],
max_dims: &[u64],
chunk_dims: &[u64],
pipeline: FilterPipeline,
) -> IoResult<usize> {
let create = self.begin_create(name)?;
let name = create.name.as_str();
validate_chunk_geometry(dims, max_dims, chunk_dims)?;
ensure_at_most_one_unlimited(max_dims)?;
let element_size = datatype.element_size() as u64;
let chunk_bytes: u64 = chunk_dims.iter().product::<u64>() * element_size;
let layout_version = self.chunk_layout_version(true, chunk_bytes);
let chunk_size_len = self.chunk_size_len_for(layout_version, chunk_bytes);
let earray_params = EarrayParams::default_params();
let ndblk_addrs = compute_ndblk_addrs(earray_params.sup_blk_min_data_ptrs)?;
let nsblk_addrs = compute_nsblk_addrs(
earray_params.idx_blk_elmts,
earray_params.data_blk_min_elmts,
earray_params.sup_blk_min_data_ptrs,
earray_params.max_nelmts_bits,
)?;
let mut ea_header =
ExtensibleArrayHeader::new_for_filtered_chunks(&self.ctx, chunk_size_len);
ea_header.max_nelmts_bits = earray_params.max_nelmts_bits;
ea_header.idx_blk_elmts = earray_params.idx_blk_elmts;
ea_header.data_blk_min_elmts = earray_params.data_blk_min_elmts;
ea_header.sup_blk_min_data_ptrs = earray_params.sup_blk_min_data_ptrs;
ea_header.max_dblk_page_nelmts_bits = earray_params.max_dblk_page_nelmts_bits;
let hdr_encoded = ea_header.encode(&self.ctx);
let ea_header_addr = self
.allocator
.allocate(hdr_encoded.len() as u64, FreeSpaceClass::Metadata);
let filt_iblk = FilteredIndexBlock::new(
ea_header_addr,
earray_params.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
);
let iblk_encoded = filt_iblk.encode(&self.ctx, chunk_size_len);
let ea_iblk_addr = self
.allocator
.allocate(iblk_encoded.len() as u64, FreeSpaceClass::Metadata);
ea_header.idx_blk_addr = ea_iblk_addr;
let hdr_encoded = ea_header.encode(&self.ctx);
self.handle.write_at(ea_header_addr, &hdr_encoded)?;
self.handle.write_at(ea_iblk_addr, &iblk_encoded)?;
let dataspace = DataspaceMessage {
class: DataspaceClass::Simple,
dims: dims.to_vec(),
max_dims: Some(max_dims.to_vec()),
};
let ea_iblk = ExtensibleArrayIndexBlock::new(
ea_header_addr,
earray_params.idx_blk_elmts,
ndblk_addrs,
nsblk_addrs,
);
let idx = self.push_dataset(
&create,
DatasetInfo {
name: name.to_string(),
datatype,
committed_type: None,
external: None,
virtual_storage: None,
dataspace,
read_format: None,
obj_header_addr: 0,
data_addr: UNDEF_ADDR,
data_size: 0,
compact: None,
attributes: Vec::new(),
obj_header_written_addr: None,
obj_header_blocks: Vec::new(),
filter_pipeline: Some(pipeline),
deleted: false,
extent_dirty: false,
header_dirty: false,
nlink_written: 1,
creation_seq: self.take_creation_seq(),
track_attr_order: self.track_order.attrs,
fill_value: None,
fill_time: FILL_TIME_IFSET,
layout_version,
times: self.created_object_times(),
fixed_array: None,
implicit: None,
single_chunk: None,
btree_v1: None,
btree_v2: None,
chunked: Some(ChunkedDatasetInfo {
chunk_dims: chunk_dims.to_vec(),
earray_params,
ea_header_addr,
ea_iblk_addr,
ea_header,
ea_iblk,
chunks_written: 0,
filt_iblk: Some(filt_iblk),
chunk_size_len,
}),
append: None,
},
);
Ok(idx)
}
pub fn write_chunk_fixed_array(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
self.write_chunk_fixed_array_inner(index, chunk_coords, data)
}
pub(crate) fn write_chunk_fixed_array_inner(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let ds = self.ds(index);
let (chunk_bytes, pipeline) = {
let m = ds.lock();
let element_size = m.datatype.element_size() as u64;
let fa = m.fixed_array.as_ref().ok_or_else(|| {
crate::io::IoError::InvalidState("not a fixed-array dataset".into())
})?;
(
fa.chunk_dims.iter().product::<u64>() * element_size,
m.filter_pipeline.clone(),
)
};
if data.len() as u64 != chunk_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"chunk data size mismatch: expected {} bytes, got {}",
chunk_bytes,
data.len()
)));
}
let write_data;
let data_to_write = if let Some(ref pipeline) = pipeline {
write_data = filter::apply_filters(pipeline, data)?;
&write_data[..]
} else {
data
};
self.record_fixed_array_chunk(index, chunk_coords, data_to_write, 0)
}
pub(crate) fn write_compressed_chunk_fixed_array_inner(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
filter_mask: u32,
) -> IoResult<()> {
if self.ds(index).lock().filter_pipeline.is_none() {
return Err(crate::io::IoError::InvalidState(
"write_compressed_chunk_fixed_array requires a filtered dataset \
(no slot for a compressed size or filter mask on an unfiltered \
chunk index)"
.into(),
));
}
self.record_fixed_array_chunk(index, chunk_coords, data, filter_mask)
}
fn record_fixed_array_chunk(
&self,
index: usize,
chunk_coords: &[u64],
final_bytes: &[u8],
filter_mask: u32,
) -> IoResult<()> {
let ds = self.ds(index);
let mut m = ds.lock();
let is_filtered = m.filter_pipeline.is_some();
let fa = m
.fixed_array
.as_ref()
.ok_or_else(|| crate::io::IoError::InvalidState("not a fixed-array dataset".into()))?;
let linear_idx = crate::io::chunk_grid::linear_index(
&m.dataspace.dims,
m.dataspace.max_dims.as_deref(),
&fa.chunk_dims,
chunk_coords,
)?;
let fa = m.fixed_array.as_mut().unwrap();
let lidx = linear_idx as usize;
if is_filtered {
let stored_size = final_bytes.len();
let chunk_size_len = (fa.fa_header.element_size as usize)
.checked_sub(self.ctx.sizeof_addr as usize + 4)
.ok_or_else(|| {
crate::io::IoError::InvalidState(
"filtered fixed-array element size is too small".into(),
)
})?;
if chunk_size_len < 8 && stored_size >= (1usize << (chunk_size_len * 8)) {
return Err(crate::io::IoError::InvalidState(format!(
"compressed chunk size {stored_size} does not fit in the \
{chunk_size_len}-byte fixed-array chunk-size field"
)));
}
if lidx < fa.fa_dblk.filtered_elements.len() {
let old = &fa.fa_dblk.filtered_elements[lidx];
let chunk_addr =
self.place_chunk(Some((old.address, old.chunk_size)), stored_size as u64);
self.handle.write_at(chunk_addr, final_bytes)?;
fa.fa_dblk.filtered_elements[lidx] = FixedArrayFilteredChunkElement {
address: chunk_addr,
chunk_size: stored_size as u64,
filter_mask,
};
fa.chunks_written += 1;
} else {
return Err(crate::io::IoError::InvalidState(format!(
"chunk index {} out of range (max {})",
linear_idx,
fa.fa_dblk.filtered_elements.len()
)));
}
} else {
if filter_mask != 0 {
return Err(crate::io::IoError::InvalidState(
"filter_mask is non-zero but the dataset is unfiltered".into(),
));
}
if lidx < fa.fa_dblk.elements.len() {
let old = fa.fa_dblk.elements[lidx];
let len = final_bytes.len() as u64;
let chunk_addr = self.place_chunk(Some((old, len)), len);
self.handle.write_at(chunk_addr, final_bytes)?;
fa.fa_dblk.elements[lidx] = chunk_addr;
fa.chunks_written += 1;
} else {
return Err(crate::io::IoError::InvalidState(format!(
"chunk index {} out of range (max {})",
linear_idx,
fa.fa_dblk.elements.len()
)));
}
}
Ok(())
}
pub(crate) fn write_chunk_single_chunk_inner(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let geo = self.chunk_geometry(index)?;
geo.linear_index(chunk_coords)?;
let chunk_bytes = geo.chunk_bytes();
if data.len() as u64 != chunk_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"chunk data size mismatch: expected {} bytes, got {}",
chunk_bytes,
data.len()
)));
}
let pipeline = self.ds(index).lock().filter_pipeline.clone();
let write_data;
let data_to_write = if let Some(ref pipeline) = pipeline {
write_data = filter::apply_filters(pipeline, data)?;
&write_data[..]
} else {
data
};
self.record_single_chunk(index, data_to_write, 0)
}
pub(crate) fn write_compressed_chunk_single_chunk_inner(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
filter_mask: u32,
) -> IoResult<()> {
if self.ds(index).lock().filter_pipeline.is_none() {
return Err(crate::io::IoError::InvalidState(
"write_compressed_chunk_single_chunk requires a filtered dataset \
(no slot for a compressed size or filter mask on an unfiltered \
chunk index)"
.into(),
));
}
let geo = self.chunk_geometry(index)?;
geo.linear_index(chunk_coords)?;
self.record_single_chunk(index, data, filter_mask)
}
fn record_single_chunk(
&self,
index: usize,
final_bytes: &[u8],
filter_mask: u32,
) -> IoResult<()> {
let ds = self.ds(index);
let mut m = ds.lock();
let is_filtered = m.filter_pipeline.is_some();
if !is_filtered && filter_mask != 0 {
return Err(crate::io::IoError::InvalidState(
"filter_mask is non-zero but the dataset is unfiltered".into(),
));
}
let sc = m
.single_chunk
.as_ref()
.ok_or_else(|| crate::io::IoError::InvalidState("not a single-chunk dataset".into()))?;
let old = if sc.data_addr == UNDEF_ADDR {
None
} else {
Some((
sc.data_addr,
if is_filtered { sc.nbytes } else { sc.data_size },
))
};
let stored_size = final_bytes.len() as u64;
let addr = self.place_chunk(old, stored_size);
self.handle.write_at(addr, final_bytes)?;
let sc = m.single_chunk.as_mut().unwrap();
sc.data_addr = addr;
sc.nbytes = stored_size;
sc.filter_mask = filter_mask;
sc.chunks_written = 1;
Ok(())
}
#[cfg(test)]
pub fn write_chunk_btree_v2(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
self.write_chunk_btree_v2_inner(index, chunk_coords, data)
}
pub(crate) fn write_chunk_btree_v2_inner(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
) -> IoResult<()> {
let ds = self.ds(index);
let (chunk_bytes, pipeline) = {
let m = ds.lock();
let element_size = m.datatype.element_size() as u64;
let bt2 = m.btree_v2.as_ref().ok_or_else(|| {
crate::io::IoError::InvalidState("not a B-tree v2 dataset".into())
})?;
(
bt2.chunk_dims.iter().product::<u64>() * element_size,
m.filter_pipeline.clone(),
)
};
if data.len() as u64 != chunk_bytes {
return Err(crate::io::IoError::InvalidState(format!(
"chunk data size mismatch: expected {} bytes, got {}",
chunk_bytes,
data.len()
)));
}
let filtered;
let stored = match pipeline {
Some(ref pl) => {
filtered = filter::apply_filters(pl, data)?;
&filtered[..]
}
None => data,
};
self.record_btree_v2_chunk(index, chunk_coords, stored, 0)
}
pub(crate) fn write_compressed_chunk_btree_v2_inner(
&self,
index: usize,
chunk_coords: &[u64],
data: &[u8],
filter_mask: u32,
) -> IoResult<()> {
if self.ds(index).lock().filter_pipeline.is_none() {
return Err(crate::io::IoError::InvalidState(
"write_compressed_chunk_btree_v2 requires a filtered dataset (no \
slot for a compressed size or filter mask on an unfiltered chunk \
index)"
.into(),
));
}
self.record_btree_v2_chunk(index, chunk_coords, data, filter_mask)
}
fn record_btree_v2_chunk(
&self,
index: usize,
chunk_coords: &[u64],
final_bytes: &[u8],
filter_mask: u32,
) -> IoResult<()> {
let stored_len = final_bytes.len() as u64;
let ds = self.ds(index);
let mut m = ds.lock();
let element_size = m.datatype.element_size() as u64;
let bt2 = m
.btree_v2
.as_ref()
.ok_or_else(|| crate::io::IoError::InvalidState("not a B-tree v2 dataset".into()))?;
let chunk_bytes = bt2.chunk_dims.iter().product::<u64>() * element_size;
if bt2.index.filtered {
let chunk_size_len = bt2.index.chunk_size_len as usize;
if chunk_size_len < 8 && stored_len >= (1u64 << (chunk_size_len * 8)) {
return Err(crate::io::IoError::InvalidState(format!(
"filtered chunk size {stored_len} does not fit in the \
{chunk_size_len}-byte v2 B-tree chunk-size field"
)));
}
}
let old = if bt2.index.filtered {
bt2.index
.lookup_filtered(chunk_coords)
.map(|r| (r.chunk_address, r.chunk_size))
} else {
bt2.index
.lookup(chunk_coords)
.map(|r| (r.chunk_address, chunk_bytes))
};
let chunk_addr = self.place_chunk(old, stored_len);
self.handle.write_at(chunk_addr, final_bytes)?;
let bt2 = m.btree_v2.as_mut().unwrap();
if bt2.index.filtered {
bt2.index
.insert_filtered(chunk_coords.to_vec(), chunk_addr, stored_len, filter_mask);
} else {
bt2.index.insert(chunk_coords.to_vec(), chunk_addr);
}
bt2.chunks_written += 1;
Ok(())
}
pub fn write_chunks_batch(&self, ds_index: usize, chunks: &[(u64, &[u8])]) -> IoResult<()> {
let ds = self.ds(ds_index);
let _op = ds.op.lock();
self.write_chunks_batch_inner(ds_index, chunks)
}
pub(crate) fn write_chunks_batch_inner(
&self,
ds_index: usize,
chunks: &[(u64, &[u8])],
) -> IoResult<()> {
#[cfg(feature = "parallel")]
{
let pipeline = self.ds(ds_index).lock().filter_pipeline.clone();
if let Some(ref pipeline) = pipeline {
let chunk_data: Vec<&[u8]> = chunks.iter().map(|&(_, d)| d).collect();
let compressed = filter::apply_filters_parallel(pipeline, &chunk_data)?;
for ((idx, _), compressed_data) in chunks.iter().zip(compressed.iter()) {
self.write_compressed_chunk_inner(ds_index, *idx, compressed_data, 0)?;
}
return Ok(());
}
}
for (idx, data) in chunks {
self.write_chunk_inner(ds_index, *idx, data)?;
}
Ok(())
}
pub(crate) fn write_chunks_fixed_array_batch_inner(
&self,
ds_index: usize,
chunks: &[(&[u64], &[u8])],
) -> IoResult<()> {
#[cfg(feature = "parallel")]
{
let pipeline = self.ds(ds_index).lock().filter_pipeline.clone();
if let Some(ref pipeline) = pipeline {
let chunk_data: Vec<&[u8]> = chunks.iter().map(|&(_, d)| d).collect();
let compressed = filter::apply_filters_parallel(pipeline, &chunk_data)?;
for ((coords, _), compressed_data) in chunks.iter().zip(compressed.iter()) {
self.record_fixed_array_chunk(ds_index, coords, compressed_data, 0)?;
}
return Ok(());
}
}
for (coords, data) in chunks {
self.write_chunk_fixed_array_inner(ds_index, coords, data)?;
}
Ok(())
}
pub(crate) fn write_compressed_chunk_inner(
&self,
index: usize,
chunk_idx: u64,
compressed_data: &[u8],
filter_mask: u32,
) -> IoResult<()> {
if self.ds(index).lock().filter_pipeline.is_none() {
return Err(crate::io::IoError::InvalidState(
"write_compressed_chunk requires a filtered dataset (no slot for \
a compressed size or filter mask on an unfiltered chunk index)"
.into(),
));
}
self.record_ea_chunk(index, chunk_idx, compressed_data, filter_mask)
}
pub fn extend_dataset(&self, index: usize, new_dims: &[u64]) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
self.extend_dataset_inner(index, new_dims)
}
pub(crate) fn extend_dataset_inner(&self, index: usize, new_dims: &[u64]) -> IoResult<()> {
let ds = self.ds(index);
let mut m = ds.lock();
if !m.is_chunked() {
return Err(crate::io::IoError::InvalidState(
"can only extend chunked datasets".into(),
));
}
if new_dims.len() != m.dataspace.dims.len() {
return Err(crate::io::IoError::InvalidState(format!(
"extend_dataset rank mismatch: dataset has {} dimensions, got {}",
m.dataspace.dims.len(),
new_dims.len()
)));
}
for (d, (&new, &cur)) in new_dims.iter().zip(&m.dataspace.dims).enumerate() {
if new < cur {
return Err(crate::io::IoError::InvalidState(format!(
"extend_dataset cannot shrink dimension {d} from {cur} to {new}"
)));
}
match m.dataspace.max_dims {
Some(ref max) if new > max[d] => {
return Err(crate::io::IoError::InvalidState(format!(
"extend_dataset dimension {d} ({new}) exceeds the maximum {}",
max[d]
)));
}
None if new > cur => {
return Err(crate::io::IoError::InvalidState(format!(
"extend_dataset dimension {d} ({new}) exceeds the maximum {cur}: \
a dataset without a stored maximum shape is fixed at its extent"
)));
}
_ => {}
}
}
if m.dataspace.dims != new_dims {
m.dataspace.dims = new_dims.to_vec();
m.extent_dirty = true;
}
Ok(())
}
pub fn set_dataset_extent(&self, index: usize, new_dims: &[u64]) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
let old_dims = {
let m = ds.lock();
if !m.is_chunked() {
return Err(crate::io::IoError::InvalidState(
"can only set the extent of chunked datasets".into(),
));
}
if new_dims.len() != m.dataspace.dims.len() {
return Err(crate::io::IoError::InvalidState(format!(
"set_extent rank mismatch: dataset has {} dimensions, got {}",
m.dataspace.dims.len(),
new_dims.len()
)));
}
if m.append.is_some() {
return Err(crate::io::IoError::InvalidState(
"set_extent cannot run while the dataset has buffered appends; \
flush them first"
.into(),
));
}
match m.dataspace.max_dims {
Some(ref max) => {
for (d, (&new, &mx)) in new_dims.iter().zip(max).enumerate() {
if new > mx {
return Err(crate::io::IoError::InvalidState(format!(
"set_extent dimension {d} ({new}) exceeds the maximum {mx}"
)));
}
}
}
None => {
for (d, (&new, &cur)) in new_dims.iter().zip(&m.dataspace.dims).enumerate() {
if new > cur {
return Err(crate::io::IoError::InvalidState(format!(
"set_extent dimension {d} ({new}) exceeds the maximum {cur}: \
a dataset without a stored maximum shape is fixed at its extent"
)));
}
}
}
}
m.dataspace.dims.clone()
};
if new_dims.iter().zip(&old_dims).any(|(&n, &o)| n < o) {
self.prune_chunks_beyond(index, new_dims)?;
}
let mut m = ds.lock();
if m.dataspace.dims != new_dims {
m.dataspace.dims = new_dims.to_vec();
m.extent_dirty = true;
}
Ok(())
}
fn prune_chunks_beyond(&self, index: usize, new_dims: &[u64]) -> IoResult<()> {
let geo = self.chunk_geometry(index)?;
let collect_refs = !self.swmr_active && {
let ds = self.ds(index);
let m = ds.lock();
matches!(
m.datatype,
DatatypeMessage::VarLenString { .. } | DatatypeMessage::VarLenSequence { .. }
)
};
let (straddlers, dead_refs) = match geo.kind {
ChunkIndexKind::ExtensibleArray => {
self.prune_ea_chunks(index, &geo, new_dims, collect_refs)?
}
ChunkIndexKind::FixedArray => {
self.prune_fa_chunks(index, &geo, new_dims, collect_refs)?
}
ChunkIndexKind::BtreeV2 => {
self.prune_bt2_chunks(index, &geo, new_dims, collect_refs)?
}
ChunkIndexKind::Implicit => (self.implicit_straddlers(&geo, new_dims)?, Vec::new()),
ChunkIndexKind::SingleChunk => (self.implicit_straddlers(&geo, new_dims)?, Vec::new()),
ChunkIndexKind::BtreeV1 => {
self.prune_btree_v1_chunks(index, &geo, new_dims, collect_refs)?
}
};
if !dead_refs.is_empty() {
self.release_vlen_references(&dead_refs)?;
}
let chunk_bytes = geo.chunk_bytes() as usize;
for coords in straddlers {
let Some(mut data) = self.read_chunk_at_coords(index, &coords)? else {
continue;
};
let fill = self.new_chunk_buffer(index, chunk_bytes);
let replaced = refill_chunk_beyond_extent(
&mut data,
&fill,
&coords,
&geo.chunk_dims,
new_dims,
geo.element_size as usize,
);
if collect_refs && !replaced.is_empty() {
self.release_vlen_references(&replaced)?;
}
self.write_chunk_at_coords(index, &coords, &data)?;
}
Ok(())
}
fn prune_ea_chunks(
&self,
index: usize,
geo: &ChunkGeometry,
new_dims: &[u64],
collect_refs: bool,
) -> IoResult<(Vec<Vec<u64>>, Vec<u8>)> {
let ds = self.ds(index);
let mut m = ds.lock();
let is_filtered = m.filter_pipeline.is_some();
let pipeline = m.filter_pipeline.clone();
let chunk_bytes = geo.chunk_bytes();
let (ea_geo, max_nelmts_bits, chunk_size_len, max_idx) = {
let c = m.chunked.as_ref().unwrap();
let p = &c.earray_params;
(
EaGeometry::new(
p.idx_blk_elmts,
p.data_blk_min_elmts,
p.sup_blk_min_data_ptrs,
p.max_nelmts_bits,
p.max_dblk_page_nelmts_bits,
)?,
p.max_nelmts_bits,
c.chunk_size_len,
c.ea_header.max_idx_set,
)
};
let mut straddlers = Vec::new();
let mut dead_refs = Vec::new();
enum Dblk {
Unfiltered(ExtensibleArrayDataBlock),
Filtered(FilteredDataBlock),
}
let mut cache: Option<(u64, Dblk, bool)> = None;
let flush = |cache: &mut Option<(u64, Dblk, bool)>| -> IoResult<()> {
if let Some((addr, blk, dirty)) = cache.take() {
if dirty {
let enc = match &blk {
Dblk::Unfiltered(d) => d.encode(&self.ctx, max_nelmts_bits),
Dblk::Filtered(d) => d.encode(&self.ctx, max_nelmts_bits, chunk_size_len),
};
self.handle.write_at(addr, &enc)?;
}
}
Ok(())
};
let mut sblk_cache: Option<(usize, ExtensibleArraySuperBlock)> = None;
let mut slot = 0u64;
while slot < max_idx {
let coords = crate::io::chunk_grid::coords_of(
&geo.dims,
geo.max_dims.as_deref(),
&geo.chunk_dims,
slot,
)?;
if !chunk_outside_extent(&coords, &geo.chunk_dims, new_dims) {
if chunk_straddles_extent(&coords, &geo.chunk_dims, new_dims) {
straddlers.push(coords);
}
slot += 1;
continue;
}
match ea_geo.locate(slot)? {
EaLoc::Index { elem } => {
let c = m.chunked.as_mut().unwrap();
if is_filtered {
let fiblk = c.filt_iblk.as_mut().unwrap();
let e = fiblk.elements[elem];
if e.addr != UNDEF_ADDR {
if collect_refs {
if let Some(bytes) = self.read_chunk_block(
pipeline.as_ref(),
e.addr,
e.nbytes,
e.filter_mask,
)? {
dead_refs.extend_from_slice(&bytes);
}
}
if !self.swmr_active {
self.allocator
.free(e.addr, e.nbytes, FreeSpaceClass::RawData);
}
fiblk.elements[elem] = FilteredChunkEntry {
addr: UNDEF_ADDR,
nbytes: 0,
filter_mask: 0,
};
}
} else {
let a = c.ea_iblk.elements[elem];
if a != UNDEF_ADDR {
if collect_refs {
if let Some(bytes) =
self.read_chunk_block(pipeline.as_ref(), a, chunk_bytes, 0)?
{
dead_refs.extend_from_slice(&bytes);
}
}
if !self.swmr_active {
self.allocator.free(a, chunk_bytes, FreeSpaceClass::RawData);
}
c.ea_iblk.elements[elem] = UNDEF_ADDR;
}
}
slot += 1;
}
EaLoc::Dblk(l) => {
if l.paged {
return Err(crate::io::IoError::InvalidState(format!(
"chunk index {slot} lives in a paged extensible-array \
data block, which is not yet supported"
)));
}
let dblk_start = slot - l.offset_in_dblk;
let dblk_end = dblk_start + l.dblk_nelmts;
let dblk_addr = {
let c = m.chunked.as_ref().unwrap();
match l.path {
EaDblkPath::Direct { idx } => {
if is_filtered {
c.filt_iblk.as_ref().unwrap().dblk_addrs[idx]
} else {
c.ea_iblk.dblk_addrs[idx]
}
}
EaDblkPath::ViaSblk {
sblk_off,
local_dblk,
ndblks_in_sblk,
..
} => {
let sblk_addr = if is_filtered {
c.filt_iblk.as_ref().unwrap().sblk_addrs[sblk_off]
} else {
c.ea_iblk.sblk_addrs[sblk_off]
};
if sblk_addr == UNDEF_ADDR {
UNDEF_ADDR
} else {
if sblk_cache.as_ref().map(|&(o, _)| o) != Some(sblk_off) {
let buf = self.handle.read_at_most(sblk_addr, 65536)?;
let sb = ExtensibleArraySuperBlock::decode(
&buf,
&self.ctx,
max_nelmts_bits,
ndblks_in_sblk,
0,
)?;
sblk_cache = Some((sblk_off, sb));
}
sblk_cache.as_ref().unwrap().1.dblk_addrs[local_dblk]
}
}
}
};
if dblk_addr == UNDEF_ADDR {
slot = dblk_end;
continue;
}
if cache.as_ref().map(|&(a, _, _)| a) != Some(dblk_addr) {
flush(&mut cache)?;
let buf = self.handle.read_at_most(dblk_addr, 65536)?;
let blk = if is_filtered {
Dblk::Filtered(FilteredDataBlock::decode(
&buf,
&self.ctx,
max_nelmts_bits,
l.dblk_nelmts as usize,
chunk_size_len,
)?)
} else {
Dblk::Unfiltered(ExtensibleArrayDataBlock::decode(
&buf,
&self.ctx,
max_nelmts_bits,
l.dblk_nelmts as usize,
)?)
};
cache = Some((dblk_addr, blk, false));
}
let (_, blk, dirty) = cache.as_mut().unwrap();
match blk {
Dblk::Filtered(d) => {
let e = d.elements[l.offset_in_dblk as usize];
if e.addr != UNDEF_ADDR {
if collect_refs {
if let Some(bytes) = self.read_chunk_block(
pipeline.as_ref(),
e.addr,
e.nbytes,
e.filter_mask,
)? {
dead_refs.extend_from_slice(&bytes);
}
}
if !self.swmr_active {
self.allocator
.free(e.addr, e.nbytes, FreeSpaceClass::RawData);
}
d.elements[l.offset_in_dblk as usize] = FilteredChunkEntry {
addr: UNDEF_ADDR,
nbytes: 0,
filter_mask: 0,
};
*dirty = true;
}
}
Dblk::Unfiltered(d) => {
let a = d.elements[l.offset_in_dblk as usize];
if a != UNDEF_ADDR {
if collect_refs {
if let Some(bytes) =
self.read_chunk_block(pipeline.as_ref(), a, chunk_bytes, 0)?
{
dead_refs.extend_from_slice(&bytes);
}
}
if !self.swmr_active {
self.allocator.free(a, chunk_bytes, FreeSpaceClass::RawData);
}
d.elements[l.offset_in_dblk as usize] = UNDEF_ADDR;
*dirty = true;
}
}
}
slot += 1;
}
}
}
flush(&mut cache)?;
Ok((straddlers, dead_refs))
}
fn prune_fa_chunks(
&self,
index: usize,
geo: &ChunkGeometry,
new_dims: &[u64],
collect_refs: bool,
) -> IoResult<(Vec<Vec<u64>>, Vec<u8>)> {
let ds = self.ds(index);
let mut m = ds.lock();
let is_filtered = m.filter_pipeline.is_some();
let pipeline = m.filter_pipeline.clone();
let chunk_bytes = geo.chunk_bytes();
let mut straddlers = Vec::new();
let mut dead_refs = Vec::new();
let fa = m.fixed_array.as_mut().unwrap();
let nslots = if is_filtered {
fa.fa_dblk.filtered_elements.len()
} else {
fa.fa_dblk.elements.len()
};
for lidx in 0..nslots {
let (addr, stored, mask) = if is_filtered {
let e = &fa.fa_dblk.filtered_elements[lidx];
(e.address, e.chunk_size, e.filter_mask)
} else {
(fa.fa_dblk.elements[lidx], chunk_bytes, 0)
};
if addr == UNDEF_ADDR {
continue;
}
let coords = crate::io::chunk_grid::coords_of(
&geo.dims,
geo.max_dims.as_deref(),
&geo.chunk_dims,
lidx as u64,
)?;
if chunk_outside_extent(&coords, &geo.chunk_dims, new_dims) {
if collect_refs {
if let Some(bytes) =
self.read_chunk_block(pipeline.as_ref(), addr, stored, mask)?
{
dead_refs.extend_from_slice(&bytes);
}
}
if !self.swmr_active {
self.allocator.free(addr, stored, FreeSpaceClass::RawData);
}
if is_filtered {
fa.fa_dblk.filtered_elements[lidx] = FixedArrayFilteredChunkElement {
address: UNDEF_ADDR,
chunk_size: 0,
filter_mask: 0,
};
} else {
fa.fa_dblk.elements[lidx] = UNDEF_ADDR;
}
} else if chunk_straddles_extent(&coords, &geo.chunk_dims, new_dims) {
straddlers.push(coords);
}
}
Ok((straddlers, dead_refs))
}
fn implicit_straddlers(
&self,
geo: &ChunkGeometry,
new_dims: &[u64],
) -> IoResult<Vec<Vec<u64>>> {
let mut nchunks: u64 = 1;
for g in
crate::io::chunk_grid::index_grid(&geo.dims, geo.max_dims.as_deref(), &geo.chunk_dims)?
{
nchunks = nchunks.checked_mul(g).ok_or_else(|| {
crate::io::IoError::InvalidState("chunk count overflows u64".into())
})?;
}
let mut straddlers = Vec::new();
for lidx in 0..nchunks {
let coords = crate::io::chunk_grid::coords_of(
&geo.dims,
geo.max_dims.as_deref(),
&geo.chunk_dims,
lidx,
)?;
if chunk_straddles_extent(&coords, &geo.chunk_dims, new_dims) {
straddlers.push(coords);
}
}
Ok(straddlers)
}
fn prune_bt2_chunks(
&self,
index: usize,
geo: &ChunkGeometry,
new_dims: &[u64],
collect_refs: bool,
) -> IoResult<(Vec<Vec<u64>>, Vec<u8>)> {
let ds = self.ds(index);
let mut m = ds.lock();
let pipeline = m.filter_pipeline.clone();
let chunk_bytes = geo.chunk_bytes();
let swmr = self.swmr_active;
let mut straddlers = Vec::new();
let mut dead_refs = Vec::new();
let bt2 = m.btree_v2.as_mut().unwrap();
if bt2.index.filtered {
let records = std::mem::take(&mut bt2.index.filtered_records);
let mut kept = Vec::with_capacity(records.len());
for r in records {
if chunk_outside_extent(&r.scaled_offsets, &geo.chunk_dims, new_dims) {
if collect_refs {
if let Some(bytes) = self.read_chunk_block(
pipeline.as_ref(),
r.chunk_address,
r.chunk_size,
r.filter_mask,
)? {
dead_refs.extend_from_slice(&bytes);
}
}
if !swmr {
self.allocator
.free(r.chunk_address, r.chunk_size, FreeSpaceClass::RawData);
}
} else {
if chunk_straddles_extent(&r.scaled_offsets, &geo.chunk_dims, new_dims) {
straddlers.push(r.scaled_offsets.clone());
}
kept.push(r);
}
}
bt2.index.filtered_records = kept;
} else {
let records = std::mem::take(&mut bt2.index.records);
let mut kept = Vec::with_capacity(records.len());
for r in records {
if chunk_outside_extent(&r.scaled_offsets, &geo.chunk_dims, new_dims) {
if collect_refs {
if let Some(bytes) = self.read_chunk_block(
pipeline.as_ref(),
r.chunk_address,
chunk_bytes,
0,
)? {
dead_refs.extend_from_slice(&bytes);
}
}
if !swmr {
self.allocator
.free(r.chunk_address, chunk_bytes, FreeSpaceClass::RawData);
}
} else {
if chunk_straddles_extent(&r.scaled_offsets, &geo.chunk_dims, new_dims) {
straddlers.push(r.scaled_offsets.clone());
}
kept.push(r);
}
}
bt2.index.records = kept;
}
Ok((straddlers, dead_refs))
}
fn prune_btree_v1_chunks(
&self,
index: usize,
geo: &ChunkGeometry,
new_dims: &[u64],
collect_refs: bool,
) -> IoResult<(Vec<Vec<u64>>, Vec<u8>)> {
let ds = self.ds(index);
let mut m = ds.lock();
let pipeline = m.filter_pipeline.clone();
let swmr = self.swmr_active;
let mut straddlers = Vec::new();
let mut dead_refs = Vec::new();
let bt1 = m.btree_v1.as_mut().unwrap();
let records = std::mem::take(&mut bt1.records);
let mut kept = Vec::with_capacity(records.len());
for r in records {
if chunk_outside_extent(&r.scaled, &geo.chunk_dims, new_dims) {
if collect_refs {
if let Some(bytes) = self.read_chunk_block(
pipeline.as_ref(),
r.address,
r.nbytes as u64,
r.filter_mask,
)? {
dead_refs.extend_from_slice(&bytes);
}
}
if !swmr {
self.allocator
.free(r.address, r.nbytes as u64, FreeSpaceClass::RawData);
}
} else {
if chunk_straddles_extent(&r.scaled, &geo.chunk_dims, new_dims) {
straddlers.push(r.scaled.clone());
}
kept.push(r);
}
}
m.btree_v1.as_mut().unwrap().records = kept;
Ok((straddlers, dead_refs))
}
pub fn flush_dataset(&self, index: usize) -> IoResult<()> {
let ds = self.ds(index);
let _op = ds.op.lock();
self.flush_dataset_synced(index, true)
}
fn flush_dataset_synced(&self, index: usize, sync: bool) -> IoResult<()> {
let ds = self.ds(index);
let mut m = ds.lock();
if let Some(ref chunked) = m.chunked {
if let Some(ref fiblk) = chunked.filt_iblk {
let iblk_encoded = fiblk.encode(&self.ctx, chunked.chunk_size_len);
self.handle.write_at(chunked.ea_iblk_addr, &iblk_encoded)?;
} else {
let iblk_encoded = chunked.ea_iblk.encode(&self.ctx);
self.handle.write_at(chunked.ea_iblk_addr, &iblk_encoded)?;
}
let hdr_encoded = chunked.ea_header.encode(&self.ctx);
self.handle.write_at(chunked.ea_header_addr, &hdr_encoded)?;
if sync {
self.handle.sync_data()?;
}
return Ok(());
}
if let Some(ref fa) = m.fixed_array {
let dblk_encoded = encode_fixed_array_dblk(&self.ctx, &fa.fa_header, &fa.fa_dblk);
self.handle.write_at(fa.fa_dblk_addr, &dblk_encoded)?;
let hdr_encoded = fa.fa_header.encode(&self.ctx);
self.handle.write_at(fa.fa_header_addr, &hdr_encoded)?;
if sync {
self.handle.sync_data()?;
}
return Ok(());
}
if let Some(ref bt2) = m.btree_v2 {
let tree = bt2.index.build_tree(&self.ctx);
let mut node_addrs = bt2.node_addrs.clone();
while node_addrs.len() < tree.nodes.len() {
node_addrs.push(
self.allocator
.allocate(tree.node_size as u64, FreeSpaceClass::Metadata),
);
}
for addr in node_addrs.split_off(tree.nodes.len()) {
if !self.swmr_active {
self.allocator
.free(addr, tree.node_size as u64, FreeSpaceClass::Metadata);
}
}
for (image, &addr) in tree.encode(&self.ctx, &node_addrs).iter().zip(&node_addrs) {
self.handle.write_at(addr, image)?;
}
let root_addr = match tree.nodes.len() {
0 => UNDEF_ADDR,
n => node_addrs[n - 1],
};
let hdr_encoded = tree.header(root_addr).encode(&self.ctx);
self.handle.write_at(bt2.bt2_header_addr, &hdr_encoded)?;
m.btree_v2.as_mut().unwrap().node_addrs = node_addrs;
if sync {
self.handle.sync_data()?;
}
return Ok(());
}
if let Some(ref bt1) = m.btree_v1 {
let element_size = m.datatype.element_size() as u64;
let tree = bt1.build_tree(element_size, self.ctx.sizeof_addr as usize);
let node_size = tree.node_size() as u64;
let mut node_addrs = bt1.node_addrs.clone();
while node_addrs.len() < tree.node_count() {
node_addrs.push(self.allocator.allocate(node_size, FreeSpaceClass::Metadata));
}
for addr in node_addrs.split_off(tree.node_count()) {
self.allocator
.free(addr, node_size, FreeSpaceClass::Metadata);
}
for (image, &addr) in tree.encode(&node_addrs)?.iter().zip(&node_addrs) {
self.handle.write_at(addr, image)?;
}
let root_addr = tree.root_address(&node_addrs);
let bt1 = m.btree_v1.as_mut().unwrap();
bt1.node_addrs = node_addrs;
bt1.root_addr = root_addr;
if sync {
self.handle.sync_data()?;
}
return Ok(());
}
Ok(())
}
pub fn close(mut self) -> IoResult<()> {
self.close_in_place()
}
pub(crate) fn close_in_place(&mut self) -> IoResult<()> {
self.closed = true;
self.finalize(true)
}
pub fn close_no_sync(mut self) -> IoResult<()> {
self.closed = true;
self.finalize(false)
}
pub fn handle(&mut self) -> &mut FileHandle {
&mut self.handle
}
fn superblock_version_for(&self, flags: u8) -> u8 {
let chosen = match self.superblock_version {
SuperblockVersion::Existing(version) => return version,
SuperblockVersion::Chosen(version) => version,
};
if self.is_legacy() {
return chosen;
}
let mut version = chosen
.max(SUPERBLOCK_V2)
.max(self.effective_libver().superblock_version());
if self.swmr_active || flags & FLAG_SWMR_WRITE != 0 {
version = version.max(SUPERBLOCK_V3);
}
version
}
fn effective_libver(&self) -> LibverBound {
self.libver.unwrap_or_else(|| {
if self.has_v110_chunk_index() {
LibverBound::V110
} else {
LibverBound::V18
}
})
}
fn has_v110_chunk_index(&self) -> bool {
self.dataset_refs().iter().any(|d| {
let m = d.lock();
!m.deleted
&& m.chunk_index_kind()
.is_some_and(|k| k != ChunkIndexKind::BtreeV1)
})
}
pub fn write_superblock(&mut self, flags: u8) -> IoResult<()> {
let root_addr = self
.root_group_addr
.ok_or_else(|| crate::io::IoError::InvalidState("root group not yet written".into()))?;
let base = self.handle.base();
let eof = self.allocator.eof() + base;
let version = self.superblock_version_for(flags);
if let Some(legacy) = self.legacy.as_deref().filter(|_| version < SUPERBLOCK_V2) {
let root_stab = self
.symbol_tables
.written
.lock()
.get(&LinkScope::Root)
.copied();
let mut sb = legacy.superblock.clone();
sb.version = version;
sb.file_consistency_flags = flags as u32;
sb.end_of_file_address = eof;
sb.root_symbol_table_entry.obj_header_addr = root_addr;
sb.root_symbol_table_entry.cache = match root_stab {
Some(s) => SymbolTableCache::SymbolTable {
btree_addr: s.btree_addr,
heap_addr: s.heap_addr,
},
None => SymbolTableCache::Nothing,
};
self.handle.write_at(0, &sb.encode())?;
return Ok(());
}
let sb = SuperblockV2V3 {
version,
sizeof_offsets: self.ctx.sizeof_addr,
sizeof_lengths: self.ctx.sizeof_size,
file_consistency_flags: flags,
base_address: base,
superblock_extension_address: self.extension.addr.lock().unwrap_or(UNDEF_ADDR),
end_of_file_address: eof,
root_group_object_header_address: root_addr,
};
self.handle.write_at(0, &sb.encode())?;
Ok(())
}
pub fn write_dataset_header_inplace(&mut self, index: usize) -> IoResult<()> {
let placement = {
let ds = self.ds(index);
let m = ds.lock();
HeaderPlacement::over(&m.obj_header_blocks).ok_or_else(|| {
crate::io::IoError::InvalidState("dataset header not yet written".into())
})?
};
let header = self.build_dataset_header(index)?;
let nlink = self.object_link_count(HardLinkTarget::Dataset(index));
let format = self.dataset_header_format(index);
let images = self.encode_header_in(&header, nlink, format, &placement)?;
let reserved = placement.blocks();
let fits = images.len() == reserved.len()
&& images
.iter()
.zip(&reserved)
.all(|((_, image), &(_, size))| image.len() as u64 == size);
if !fits {
return Err(crate::io::IoError::InvalidState(format!(
"dataset header grew from {} to {} bytes; cannot rewrite in place",
reserved.iter().map(|&(_, size)| size).sum::<u64>(),
images.iter().map(|(_, image)| image.len()).sum::<usize>()
)));
}
for (addr, image) in &images {
self.handle.write_at(*addr, image)?;
}
self.ds(index).lock().header_written(nlink);
Ok(())
}
pub fn finalize_for_swmr(&mut self) -> IoResult<()> {
self.reject_swmr()?;
for i in 0..self.dataset_count() {
let is_indexed = {
let ds = self.ds(i);
let m = ds.lock();
!m.deleted && m.is_chunked()
};
if is_indexed {
self.flush_dataset(i)?;
}
}
let live: Vec<usize> = (0..self.dataset_count())
.filter(|&i| !self.ds(i).lock().deleted)
.collect();
let kept = self.supersede_headers(&live);
self.write_committed_datatype_headers()?;
let layout = self.allocate_object_headers(&live, &kept)?;
self.prepare_dense_attributes(&live)?;
self.prepare_link_storage()?;
self.write_reference_values()?;
self.write_object_headers(&layout)?;
for &(i, placement) in &layout.datasets {
let ds = self.ds(i);
let mut m = ds.lock();
m.obj_header_written_addr = Some(placement.addr);
m.obj_header_blocks = placement.blocks();
}
for &(gi, placement) in &layout.groups {
let grp = self.grp(gi);
let mut g = grp.lock();
g.obj_header_written_addr = Some(placement.addr);
g.obj_header_blocks = placement.blocks();
}
self.superseded_root_header = layout.root.blocks();
self.write_superblock(FLAG_WRITE_ACCESS | FLAG_SWMR_WRITE)?;
self.handle.set_eof(self.allocator.eof())?;
self.handle.sync_all()?;
self.swmr_active = true;
Ok(())
}
fn flush_append_buffers(&mut self) -> IoResult<()> {
for i in 0..self.dataset_count() {
if self.ds(i).lock().deleted {
continue;
}
self.flush_append_buffer(i)?;
}
Ok(())
}
fn finalize(&mut self, sync: bool) -> IoResult<()> {
self.flush_append_buffers()?;
for i in 0..self.dataset_count() {
let ds = self.ds(i);
{
let m = ds.lock();
if m.deleted {
continue;
}
if m.obj_header_written_addr.is_some() && !m.storage_dirty() {
continue;
}
let is_indexed = m.is_chunked();
if !is_indexed {
continue;
}
}
self.flush_dataset_synced(i, sync)?;
}
let mut rewritten: Vec<usize> = Vec::new();
let table_replaced = self.rebuilds_shared_messages();
for i in 0..self.dataset_count() {
let nlink = self.object_link_count(HardLinkTarget::Dataset(i));
let ds = self.ds(i);
let mut m = ds.lock();
if m.deleted {
continue;
}
if let Some(written) = m.obj_header_written_addr {
if !table_replaced && !m.header_stale_with(nlink) {
m.obj_header_addr = written;
continue;
}
}
rewritten.push(i);
}
let kept = self.supersede_headers(&rewritten);
self.write_committed_datatype_headers()?;
self.begin_shared_message_layout();
let layout = self.allocate_object_headers(&rewritten, &kept)?;
self.prepare_dense_attributes(&rewritten)?;
self.prepare_link_storage()?;
self.write_reference_values()?;
self.prepare_shared_messages(&rewritten)?;
self.write_superblock_extension()?;
self.write_object_headers(&layout)?;
self.write_superblock(0)?;
self.handle.set_eof(self.allocator.eof())?;
if sync {
self.handle.sync_all()?;
}
Ok(())
}
fn supersede_headers(&mut self, datasets: &[usize]) -> KeptChunks {
let mut kept = KeptChunks::default();
let mut taken = std::collections::HashSet::new();
for &i in datasets {
let ds = self.ds(i);
let mut m = ds.lock();
let Some(old) = m.obj_header_written_addr.take() else {
continue;
};
let blocks = std::mem::take(&mut m.obj_header_blocks);
if !blocks.is_empty() && taken.insert(old) {
kept.datasets.insert(i, self.keep_chunk0(blocks));
}
}
for gi in 0..self.group_count() {
let grp = self.grp(gi);
let mut g = grp.lock();
let Some(old) = g.obj_header_written_addr.take() else {
continue;
};
let blocks = std::mem::take(&mut g.obj_header_blocks);
if !blocks.is_empty() && taken.insert(old) {
kept.groups.insert(gi, self.keep_chunk0(blocks));
}
}
let root_blocks = std::mem::take(&mut self.superseded_root_header);
if root_blocks
.first()
.is_some_and(|&(addr, _)| taken.insert(addr))
{
kept.root = Some(self.keep_chunk0(root_blocks));
}
kept
}
fn keep_chunk0(&self, blocks: crate::io::object_header_io::HeaderBlocks) -> (u64, u64) {
let mut blocks = blocks.into_iter();
let chunk0 = blocks.next().expect("a written header has a chunk 0");
if !self.swmr_active {
for (addr, len) in blocks {
self.allocator.free(addr, len, FreeSpaceClass::Metadata);
}
}
chunk0
}
fn allocate_object_headers(
&mut self,
datasets: &[usize],
kept: &KeptChunks,
) -> IoResult<HeaderLayout> {
let mut layout = HeaderLayout {
datasets: Vec::with_capacity(datasets.len()),
groups: Vec::new(),
root: HeaderPlacement::fresh(0, 0),
};
for &i in datasets {
let header = self.build_dataset_header(i)?;
let format = self.dataset_header_format(i);
let placement = self.place_header(&header, format, kept.datasets.get(&i).copied())?;
self.ds(i).lock().obj_header_addr = placement.addr;
layout.datasets.push((i, placement));
}
for gi in 0..self.group_count() {
if self.grp(gi).lock().deleted {
continue;
}
let header = self.build_group_header(gi)?;
let format = self.group_header_format(gi);
let placement = self.place_header(&header, format, kept.groups.get(&gi).copied())?;
self.grp(gi).lock().obj_header_addr = placement.addr;
layout.groups.push((gi, placement));
}
let header = self.build_root_group_header()?;
let format = self.header_format(self.root_track_order);
let placement = self.place_header(&header, format, kept.root)?;
self.root_group_addr = Some(placement.addr);
layout.root = placement;
Ok(layout)
}
fn write_object_headers(&mut self, layout: &HeaderLayout) -> IoResult<()> {
for &(i, placement) in &layout.datasets {
let rc = self.object_link_count(HardLinkTarget::Dataset(i));
let header = self.build_dataset_header(i)?;
let format = self.dataset_header_format(i);
let what = format!("dataset '{}'", self.ds(i).lock().name);
self.write_header_in(&header, rc, format, &placement, &what)?;
self.ds(i).lock().header_written(rc);
}
for &(gi, placement) in &layout.groups {
let rc = self.object_link_count(HardLinkTarget::Group(gi));
let header = self.build_group_header(gi)?;
let format = self.group_header_format(gi);
let what = format!("group '{}'", self.grp(gi).lock().name);
self.write_header_in(&header, rc, format, &placement, &what)?;
}
let header = self.build_root_group_header()?;
let format = self.header_format(self.root_track_order);
self.write_header_in(&header, 1, format, &layout.root, "the root group")
}
fn write_header_in(
&mut self,
header: &ObjectHeader,
rc: u32,
format: ObjectFormat,
placement: &HeaderPlacement,
what: &str,
) -> IoResult<()> {
let images = self.encode_header_in(header, rc, format, placement)?;
let reserved =
std::iter::once(placement.size).chain(placement.continuation.map(|(_, s)| s));
for ((addr, image), size) in images.iter().zip(reserved) {
check_header_size(image, size, || what.to_string())?;
self.handle.write_at(*addr, image)?;
}
Ok(())
}
fn build_dataset_header(&self, index: usize) -> IoResult<ObjectHeader> {
let rc = self.object_link_count(HardLinkTarget::Dataset(index));
let committed = self.ds(index).lock().committed_type;
let committed_addr = committed.map(|r| match r {
CommittedTypeRef::Session(ci) => self.committed_datatypes.lock()[ci].obj_header_addr,
CommittedTypeRef::Preserved(addr) => addr,
});
let attributes = self.object_attributes(AttrScope::Dataset(index))?;
let ds = self.ds(index);
let m = ds.lock();
let mut header = ObjectHeader::new();
let format = m.read_format.unwrap_or_else(|| self.message_format());
let libver = match format {
ObjectFormat::Legacy => LibverBound::Earliest,
ObjectFormat::Modern => self.encoding_libver(),
};
let ds_msg = m.dataspace.encode_for(&self.ctx, format);
let owner = ShareOwner::Header(m.obj_header_addr);
let (flags, ds_msg) = self.share_message(owner, MSG_DATASPACE, 0x00, ds_msg);
header.add_message(MSG_DATASPACE, flags, ds_msg);
match committed_addr {
Some(addr) => header.add_message(
MSG_DATATYPE,
MSG_FLAG_CONSTANT | MSG_FLAG_SHARED,
SharedMessagePointer::encode_committed(addr, &self.ctx),
),
None => {
let body = m.datatype.encode_at(&self.ctx, libver);
let (flags, body) = if self.dataset_datatype_shareable(&m.datatype, libver) {
self.share_message(owner, MSG_DATATYPE, MSG_FLAG_CONSTANT, body)
} else {
(MSG_FLAG_CONSTANT, body)
};
header.add_message(MSG_DATATYPE, flags, body)
}
}
let is_chunked = m.is_chunked();
let alloc_time = if m.compact.is_some()
|| m.implicit.is_some()
|| m.single_chunk.as_ref().is_some_and(|s| s.early_alloc)
{
1 } else if is_chunked || m.virtual_storage.is_some() {
3 } else {
2 };
let is_vlen = matches!(
m.datatype,
DatatypeMessage::VarLenString { .. } | DatatypeMessage::VarLenSequence { .. }
);
let fill_write_time = if is_vlen && m.fill_value.is_none() && m.fill_time == FILL_TIME_IFSET
{
FILL_TIME_ALLOC
} else {
m.fill_time
};
let fv = if let Some(ref bytes) = m.fill_value {
FillValueMessage {
alloc_time,
fill_write_time,
fill_defined: 2,
fill_value: Some(bytes.clone()),
}
} else {
FillValueMessage {
alloc_time,
fill_write_time,
fill_defined: 1, fill_value: None,
}
};
let fv_msg = fv.encode_for(format);
let (flags, fv_msg) = self.share_message(owner, MSG_FILL_VALUE, MSG_FLAG_CONSTANT, fv_msg);
header.add_message(MSG_FILL_VALUE, flags, fv_msg);
if matches!(format, ObjectFormat::Legacy) {
if let Some(ref bytes) = m.fill_value {
let mut old = Vec::with_capacity(4 + bytes.len());
old.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
old.extend_from_slice(bytes);
let (flags, old) =
self.share_message(owner, MSG_FILL_VALUE_OLD, MSG_FLAG_CONSTANT, old);
header.add_message(MSG_FILL_VALUE_OLD, flags, old);
}
}
if let Some(ref ext) = m.external {
header.add_message(
MSG_EXTERNAL_FILE_LIST,
MSG_FLAG_CONSTANT,
ext.message().encode(&self.ctx),
);
}
let layout = if let Some(ref chunked) = m.chunked {
let mut layout_dims = chunked.chunk_dims.clone();
layout_dims.push(m.datatype.element_size() as u64);
DataLayoutMessage::chunked_v4_earray(
m.layout_version,
layout_dims,
chunked.earray_params.clone(),
chunked.ea_header_addr,
)
} else if let Some(ref fa) = m.fixed_array {
let mut layout_dims = fa.chunk_dims.clone();
layout_dims.push(m.datatype.element_size() as u64);
DataLayoutMessage::chunked_v4_farray(
m.layout_version,
layout_dims,
FixedArrayParams::default_params(),
fa.fa_header_addr,
)
} else if let Some(ref bt2) = m.btree_v2 {
let mut layout_dims = bt2.chunk_dims.clone();
layout_dims.push(m.datatype.element_size() as u64);
DataLayoutMessage::chunked_v4_btree_v2(
m.layout_version,
layout_dims,
crate::format::messages::data_layout::Bt2Params {
node_size: bt2.index.node_size,
split_percent: bt2.index.split_percent,
merge_percent: bt2.index.merge_percent,
},
bt2.bt2_header_addr,
)
} else if let Some(ref imp) = m.implicit {
let mut layout_dims = imp.chunk_dims.clone();
layout_dims.push(m.datatype.element_size() as u64);
DataLayoutMessage::chunked_v4_implicit(m.layout_version, layout_dims, imp.data_addr)
} else if let Some(ref sc) = m.single_chunk {
let mut layout_dims = sc.chunk_dims.clone();
layout_dims.push(m.datatype.element_size() as u64);
if m.filter_pipeline.is_some() {
DataLayoutMessage::chunked_v4_single_filtered(
layout_dims,
sc.data_addr,
sc.nbytes,
sc.filter_mask,
)
} else {
DataLayoutMessage::chunked_v4_single(layout_dims, sc.data_addr)
}
} else if let Some(ref bt1) = m.btree_v1 {
let mut layout_dims = bt1.chunk_dims.clone();
layout_dims.push(m.datatype.element_size() as u64);
DataLayoutMessage::chunked_v3_btree_v1(layout_dims, bt1.root_addr)
} else if let Some(ref image) = m.compact {
DataLayoutMessage::compact(image.clone())
} else if let Some(ref virt) = m.virtual_storage {
DataLayoutMessage::virtual_layout(4, virt.heap_addr, virt.heap_index)
} else {
DataLayoutMessage::contiguous(m.data_addr, m.data_size)
};
let npoints: u64 = if m.dataspace.is_null() {
0
} else {
m.dataspace.dims.iter().product()
};
let filtered = m
.filter_pipeline
.as_ref()
.is_some_and(|p| !p.filters.is_empty());
let layout_flags = if alloc_time == 1 && m.compact.is_none() && !filtered && npoints != 0 {
MSG_FLAG_CONSTANT
} else {
0x00
};
let layout_msg = layout.encode(&self.ctx);
header.add_message(MSG_DATA_LAYOUT, layout_flags, layout_msg);
if let Some(ref pipeline) = m.filter_pipeline {
if !pipeline.filters.is_empty() {
let (flags, filter_msg) = self.share_message(
owner,
MSG_FILTER_PIPELINE,
MSG_FLAG_CONSTANT,
pipeline.encode_for(format),
);
header.add_message(MSG_FILTER_PIPELINE, flags, filter_msg);
}
}
let format = self.header_format(TrackOrder {
links: CreationOrder::default(),
attrs: m.track_attr_order,
});
touch_oh(&mut header, format, m.times, true);
self.emit_attributes(
&mut header,
AttrScope::Dataset(index),
&attributes,
m.track_attr_order,
format,
owner,
);
self.emit_refcount(&mut header, rc, format);
Ok(header)
}
fn write_committed_datatype_headers(&mut self) -> IoResult<()> {
let count = self.committed_datatypes.lock().len();
for i in 0..count {
let rc = self.committed_datatype_refcount(i);
if rc == 0 {
continue;
}
let format = self.committed_datatype_header_format();
let encoded = self
.build_committed_datatype_header(i, rc, format)
.encode_for(format, rc)?;
let addr = self
.allocator
.allocate(encoded.len() as u64, FreeSpaceClass::Metadata);
self.handle.write_at(addr, &encoded)?;
self.committed_datatypes.lock()[i].obj_header_addr = addr;
}
Ok(())
}
fn committed_datatype_header_format(&self) -> ObjectFormat {
self.header_format(TrackOrder::default())
}
fn build_committed_datatype_header(
&self,
index: usize,
rc: u32,
format: ObjectFormat,
) -> ObjectHeader {
let (datatype, times) = {
let reg = self.committed_datatypes.lock();
(reg[index].datatype.clone(), reg[index].times)
};
let mut header = ObjectHeader::new();
header.set_attribute_creation_order(self.header_attr_order(CreationOrder::default()));
header.add_message(
MSG_DATATYPE,
MSG_FLAG_CONSTANT | MSG_FLAG_DONTSHARE,
datatype.encode_at(&self.ctx, self.encoding_libver()),
);
touch_oh(&mut header, format, times, false);
self.emit_refcount(&mut header, rc, format);
header
}
fn build_group_header(&self, group_idx: usize) -> IoResult<ObjectHeader> {
let mut header = ObjectHeader::new();
let (track_order, times, owner) = {
let grp = self.grp(group_idx);
let g = grp.lock();
(
g.track_order,
g.times,
ShareOwner::Header(g.obj_header_addr),
)
};
let attributes = self.object_attributes(AttrScope::Group(group_idx))?;
touch_oh(&mut header, self.header_format(track_order), times, false);
let links = self.group_links(LinkScope::Group(group_idx), track_order.links);
self.emit_links(
&mut header,
LinkScope::Group(group_idx),
&links,
track_order.links,
);
let format = self.header_format(track_order);
self.emit_attributes(
&mut header,
AttrScope::Group(group_idx),
&attributes,
track_order.attrs,
format,
owner,
);
self.emit_refcount(
&mut header,
self.object_link_count(HardLinkTarget::Group(group_idx)),
format,
);
Ok(header)
}
fn build_root_group_header(&self) -> IoResult<ObjectHeader> {
let mut header = ObjectHeader::new();
touch_oh(
&mut header,
self.header_format(self.root_track_order),
self.root_times,
false,
);
let links = self.group_links(LinkScope::Root, self.root_track_order.links);
self.emit_links(
&mut header,
LinkScope::Root,
&links,
self.root_track_order.links,
);
let root_attributes = self.object_attributes(AttrScope::Root)?;
self.emit_attributes(
&mut header,
AttrScope::Root,
&root_attributes,
self.root_track_order.attrs,
self.header_format(self.root_track_order),
ShareOwner::Header(self.root_group_addr.unwrap_or(0)),
);
Ok(header)
}
}
impl Drop for Hdf5Writer {
fn drop(&mut self) {
if !self.closed {
if let Err(e) = self.finalize(true) {
eprintln!(
"rust-hdf5: failed to finalize HDF5 file on drop: {e}. \
The file may be incomplete or corrupt; call \
H5File::close() to handle this error explicitly."
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::messages::datatype::DatatypeMessage;
use crate::io::reader::Hdf5Reader;
fn fixture(name: &str) -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn fixture_copy(name: &str, tag: &str) -> std::path::PathBuf {
let path = temp_path(tag);
std::fs::copy(fixture(name), &path).unwrap();
path
}
fn temp_path(tag: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"rust_hdf5_w_{}_{}_{}.h5",
std::process::id(),
tag,
n
))
}
#[test]
fn a_reopen_frees_the_dense_link_storage_its_rewrite_supersedes() {
let path = temp_path("dense_link_reclaim");
let writer = Hdf5Writer::create(&path).unwrap();
writer.create_group("/", "run").unwrap();
for i in 0..12 {
writer
.create_dataset(&format!("run/d{i:02}"), DatatypeMessage::i32_type(), &[2])
.unwrap();
}
writer.close().unwrap();
let writer = Hdf5Writer::open_append(&path).unwrap();
let gidx = (0..writer.group_count())
.find(|&g| writer.grp(g).lock().name == "/run")
.expect("the reopen registered the group");
let linfo = writer
.superseded_dense
.lock()
.as_ref()
.and_then(|s| s.links.get(&LinkScope::Group(gidx)).cloned())
.expect("the reopen recorded the group's dense link storage");
assert_ne!(linfo.fractal_heap_address, UNDEF_ADDR);
assert_ne!(linfo.name_btree_address, UNDEF_ADDR);
writer
.release_superseded_dense_links(LinkScope::Group(gidx))
.unwrap();
let freed = writer.allocator.free_blocks();
let covers = |addr: u64| {
freed
.iter()
.any(|&(a, len)| addr >= a && addr < a.saturating_add(len))
};
assert!(covers(linfo.fractal_heap_address), "heap header: {freed:?}");
assert!(covers(linfo.name_btree_address), "name index: {freed:?}");
assert!(writer
.superseded_dense
.lock()
.as_ref()
.is_none_or(|s| s.links.is_empty()));
writer
.release_superseded_dense_links(LinkScope::Group(gidx))
.unwrap();
assert_eq!(writer.allocator.free_blocks(), freed);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn a_rewrite_that_drops_out_of_dense_storage_still_frees_it() {
let path = temp_path("dense_attr_to_compact");
let numeric = |name: &str| {
AttributeMessage::scalar_numeric(
name,
DatatypeMessage::i32_type(),
7i32.to_le_bytes().to_vec(),
)
};
let writer = Hdf5Writer::create(&path).unwrap();
for i in 0..12 {
writer
.add_root_attribute(numeric(&format!("a{i:02}")))
.unwrap();
}
writer.close().unwrap();
let writer = Hdf5Writer::open_append(&path).unwrap();
let ainfo = writer
.superseded_dense
.lock()
.as_ref()
.and_then(|s| s.attrs.get(&AttrScope::Root).cloned())
.expect("the reopen recorded the root's dense attribute storage");
for i in 0..10 {
writer
.evict_attr(AttrTarget::Root, &format!("a{i:02}"))
.unwrap();
}
assert!(!writer.attributes_need_dense(&writer.root_attributes.lock(), ObjectFormat::Modern));
writer.prepare_dense_attributes(&[]).unwrap();
let freed = writer.allocator.free_blocks();
let covers = |addr: u64| {
freed
.iter()
.any(|&(a, len)| addr >= a && addr < a.saturating_add(len))
};
assert!(covers(ainfo.fractal_heap_address), "heap header: {freed:?}");
assert!(covers(ainfo.name_btree_address), "name index: {freed:?}");
assert!(writer
.superseded_dense
.lock()
.as_ref()
.is_none_or(|s| s.attrs.is_empty()));
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn deleting_a_reopened_group_frees_its_dense_attribute_storage() {
let path = temp_path("dense_attr_delete");
let numeric = |name: &str| {
AttributeMessage::scalar_numeric(
name,
DatatypeMessage::i32_type(),
7i32.to_le_bytes().to_vec(),
)
};
let writer = Hdf5Writer::create(&path).unwrap();
writer.create_group("/", "run").unwrap();
for i in 0..12 {
writer
.set_attribute(AttrTarget::Group("/run"), numeric(&format!("a{i:02}")))
.unwrap();
}
writer.close().unwrap();
let writer = Hdf5Writer::open_append(&path).unwrap();
let gidx = (0..writer.group_count())
.find(|&g| writer.grp(g).lock().name == "/run")
.expect("the reopen registered the group");
let ainfo = writer
.superseded_dense
.lock()
.as_ref()
.and_then(|s| s.attrs.get(&AttrScope::Group(gidx)).cloned())
.expect("the reopen recorded the group's dense attribute storage");
writer.delete_group("/run").unwrap();
let freed = writer.allocator.free_blocks();
let covers = |addr: u64| {
freed
.iter()
.any(|&(a, len)| addr >= a && addr < a.saturating_add(len))
};
assert!(covers(ainfo.fractal_heap_address), "heap header: {freed:?}");
assert!(covers(ainfo.name_btree_address), "name index: {freed:?}");
assert!(writer
.superseded_dense
.lock()
.as_ref()
.is_none_or(|s| s.attrs.is_empty()));
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn append_vlen_strings_checks_the_datatype_and_charset() {
let path = temp_path("append_vlen_charset");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_appendable_vlen_string_dataset("d", 4, None)
.unwrap();
writer.ds(idx).lock().datatype = DatatypeMessage::vlen_string_ascii();
let err = writer
.append_vlen_strings(idx, &["ok", "안녕"])
.unwrap_err();
assert!(
err.to_string().contains("is not ASCII"),
"unexpected error: {err}"
);
writer.append_vlen_strings(idx, &["ok", "fine"]).unwrap();
let nums = writer
.create_chunked_dataset("n", DatatypeMessage::i32_type(), &[0], &[u64::MAX], &[4])
.unwrap();
let err = writer.append_vlen_strings(nums, &["x"]).unwrap_err();
assert!(
err.to_string()
.contains("only for variable-length string datasets"),
"unexpected error: {err}"
);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn create_chunked_dataset_rejects_two_unlimited_dimensions() {
let path = temp_path("earray_two_unlimited");
let writer = Hdf5Writer::create(&path).unwrap();
let err = writer
.create_chunked_dataset(
"d",
DatatypeMessage::i32_type(),
&[4, 4],
&[u64::MAX, u64::MAX],
&[2, 2],
)
.unwrap_err();
assert!(err.to_string().contains("at most one unlimited"), "{err}");
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn every_creator_rejects_an_existing_dataset_name() {
let path = temp_path("create_gate");
let writer = Hdf5Writer::create(&path).unwrap();
writer
.create_dataset("d", DatatypeMessage::i32_type(), &[2])
.unwrap();
let attempts: [(&str, IoResult<usize>); 4] = [
(
"vlen_string",
writer.create_vlen_string_dataset("d", &["x"], 1),
),
("vlen_bytes", writer.create_vlen_bytes_dataset("d", &[b"x"])),
(
"vlen_string_compressed",
writer.create_vlen_string_dataset_compressed(
"d",
&["x"],
1,
FilterPipeline::deflate(6),
),
),
(
"chunked_with_pipeline",
writer.create_chunked_dataset_with_pipeline(
"d",
DatatypeMessage::i32_type(),
&[0],
&[u64::MAX],
&[4],
FilterPipeline::deflate(6),
),
),
];
for (which, res) in attempts {
match res {
Ok(_) => panic!("{which} accepted a duplicate name"),
Err(e) => assert!(
e.to_string().contains("already exists"),
"{which}: unexpected error: {e}"
),
}
}
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn every_creator_refuses_every_kind_of_taken_name() {
let path = temp_path("create_gate_matrix");
let writer = Hdf5Writer::create(&path).unwrap();
let i32t = || DatatypeMessage::i32_type();
writer.create_dataset("d", i32t(), &[2]).unwrap();
writer.create_compact_dataset("c", i32t(), &[2]).unwrap();
writer.create_group("/", "g").unwrap();
writer.commit_datatype("t", i32t()).unwrap();
writer.create_hard_link("/", "h", "d").unwrap();
writer
.create_symbolic_link(
"/",
"s",
LinkTarget::Soft {
target: "/d".into(),
},
)
.unwrap();
writer
.create_symbolic_link(
"/",
"e",
LinkTarget::External {
file: "other.h5".into(),
path: "/x".into(),
},
)
.unwrap();
for taken in ["d", "c", "g", "t", "h", "s", "e"] {
let attempts: [(&str, IoResult<()>); 8] = [
(
"dataset",
writer.create_dataset(taken, i32t(), &[2]).map(|_| ()),
),
(
"compact",
writer
.create_compact_dataset(taken, i32t(), &[2])
.map(|_| ()),
),
(
"chunked",
writer
.create_chunked_dataset(taken, i32t(), &[0], &[u64::MAX], &[4])
.map(|_| ()),
),
(
"vlen_string",
writer
.create_vlen_string_dataset(taken, &["x"], 1)
.map(|_| ()),
),
(
"committed datatype",
writer.commit_datatype(taken, i32t()).map(|_| ()),
),
("group", writer.create_group("/", taken).map(|_| ())),
("hard link", writer.create_hard_link("/", taken, "d")),
(
"soft link",
writer.create_symbolic_link(
"/",
taken,
LinkTarget::Soft {
target: "/d".into(),
},
),
),
];
for (which, res) in attempts {
match res {
Ok(()) => panic!("{which} accepted the taken name '{taken}'"),
Err(e) => assert!(
e.to_string().contains("already exists"),
"{which} on '{taken}': unexpected error: {e}"
),
}
}
}
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn vlen_sequence_refuses_a_partial_element() {
let path = temp_path("vlen_partial_element");
let writer = Hdf5Writer::create(&path).unwrap();
let err = writer
.create_vlen_sequence_dataset("d", DatatypeMessage::i32_type(), &[&[1u8, 2, 3, 4, 5]])
.unwrap_err()
.to_string();
assert!(err.contains("5 bytes"), "unexpected error: {err}");
assert!(err.contains("4-byte elements"), "unexpected error: {err}");
writer
.create_vlen_sequence_dataset(
"d",
DatatypeMessage::i32_type(),
&[&[1u8, 2, 3, 4], &[][..]],
)
.unwrap();
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn vlen_slice_rejects_a_zero_chunk_dimension() {
let path = temp_path("vlen_slice_zero_chunk");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_appendable_vlen_string_dataset("d", 2, None)
.unwrap();
writer.append_vlen_strings(idx, &["a", "b"]).unwrap();
writer.ds(idx).lock().chunked.as_mut().unwrap().chunk_dims[0] = 0;
let err = writer.write_vlen_strings_slice(idx, 0, &["x"]).unwrap_err();
assert!(
err.to_string().contains("zero-length dimension"),
"unexpected error: {err}"
);
writer.ds(idx).lock().chunked.as_mut().unwrap().chunk_dims[0] = 2;
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn release_leaves_a_full_collection_it_removed_nothing_from() {
use crate::format::global_heap::encode_vlen_reference;
let path = temp_path("release_full_gcol");
let writer = Hdf5Writer::create(&path).unwrap();
let mut img = Vec::new();
img.extend_from_slice(b"GCOL");
img.push(1);
img.extend_from_slice(&[0u8; 3]);
img.extend_from_slice(&40u64.to_le_bytes());
img.extend_from_slice(&1u16.to_le_bytes()); img.extend_from_slice(&1u16.to_le_bytes()); img.extend_from_slice(&0u32.to_le_bytes()); img.extend_from_slice(&8u64.to_le_bytes()); img.extend_from_slice(b"deadbeef");
assert_eq!(img.len(), 40);
let addr = writer
.allocator
.allocate(img.len() as u64, FreeSpaceClass::RawData);
writer.handle.write_at(addr, &img).unwrap();
let refs = encode_vlen_reference(3, addr, 2, &writer.ctx);
writer.release_vlen_references(&refs).unwrap();
assert_eq!(writer.handle.read_at(addr, 40).unwrap(), img);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn an_oversized_vlen_insert_extends_the_listed_collection() {
use crate::format::global_heap::GlobalHeapCollection;
let path = temp_path("cwfs_extend_tail");
let writer = Hdf5Writer::create(&path).unwrap();
let p1 = writer.insert_vlen_objects(&[b"hello".as_slice()]).unwrap();
let big = vec![0x41u8; 5000]; let p2 = writer.insert_vlen_objects(&[big.as_slice()]).unwrap();
assert_eq!(
p2[0].0, p1[0].0,
"the big object opened a second collection"
);
let img = writer.handle.read_at_most(p1[0].0, 65536).unwrap();
let (gcol, csize) = GlobalHeapCollection::decode(&img, &writer.ctx).unwrap();
assert!(csize > 4096, "declared size did not grow: {csize}");
assert_eq!(gcol.objects.len(), 2);
assert_eq!(gcol.objects[1].data, big);
writer.close().unwrap();
let bytes = std::fs::read(&path).unwrap();
assert_eq!(
bytes.windows(4).filter(|w| *w == b"GCOL").count(),
1,
"a second collection signature is in the file"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn extension_consumes_a_freed_block_after_the_collection() {
use crate::format::global_heap::GlobalHeapCollection;
let path = temp_path("cwfs_extend_freed");
let writer = Hdf5Writer::create(&path).unwrap();
let p1 = writer.insert_vlen_objects(&[b"hello".as_slice()]).unwrap();
let addr = p1[0].0;
let spacer = writer.allocator.allocate(8192, FreeSpaceClass::RawData);
assert_eq!(spacer, addr + 4096, "spacer not adjacent; layout changed");
writer.allocator.allocate(8, FreeSpaceClass::RawData);
writer.allocator.free(spacer, 8192, FreeSpaceClass::RawData);
let big = vec![0x42u8; 5000];
let p2 = writer.insert_vlen_objects(&[big.as_slice()]).unwrap();
assert_eq!(p2[0].0, addr, "the big object opened a second collection");
let img = writer.handle.read_at_most(addr, 65536).unwrap();
let (gcol, csize) = GlobalHeapCollection::decode(&img, &writer.ctx).unwrap();
assert_eq!(csize, 8192, "grew by max(size, shortfall) = 4096");
assert_eq!(gcol.objects.len(), 2);
assert_eq!(
writer.allocator.allocate(4096, FreeSpaceClass::RawData),
addr + 8192,
"the freed block's tail was lost"
);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn vlen_replace_across_reopen_keeps_the_file_flat() {
let path = temp_path("vlen_reopen_flat");
let payload_a = "a".repeat(64 * 1024);
let payload_b = "b".repeat(64 * 1024);
let writer = Hdf5Writer::create(&path).unwrap();
writer
.create_vlen_string_dataset("notes", &["initial"], 1)
.unwrap();
writer.close().unwrap();
let mut sizes = Vec::new();
for i in 0..8 {
let writer = Hdf5Writer::open_append(&path).unwrap();
let payload = if i % 2 == 0 { &payload_a } else { &payload_b };
writer
.write_vlen_strings_slice(0, 0, &[payload.as_str()])
.unwrap();
writer.close().unwrap();
sizes.push(std::fs::metadata(&path).unwrap().len());
}
assert_eq!(&sizes[1..], &vec![sizes[0]; 7][..], "sizes: {sizes:?}");
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(
reader.read_vlen_strings("notes").unwrap(),
vec![payload_b.clone()]
);
std::fs::remove_file(&path).ok();
}
#[test]
fn vlen_attr_replace_across_reopen_keeps_the_file_flat() {
let path = temp_path("vlen_attr_reopen_flat");
let payload_a = "a".repeat(8 * 1024);
let payload_b = "b".repeat(8 * 1024);
let writer = Hdf5Writer::create(&path).unwrap();
writer
.set_vlen_string_attribute(AttrTarget::Root, "note", &payload_a)
.unwrap();
writer.close().unwrap();
let mut sizes = Vec::new();
for i in 0..8 {
let writer = Hdf5Writer::open_append(&path).unwrap();
let payload = if i % 2 == 0 { &payload_b } else { &payload_a };
writer
.set_vlen_string_attribute(AttrTarget::Root, "note", payload)
.unwrap();
writer.close().unwrap();
sizes.push(std::fs::metadata(&path).unwrap().len());
}
assert_eq!(&sizes[1..], &vec![sizes[0]; 7][..], "sizes: {sizes:?}");
let reader = Hdf5Reader::open(&path).unwrap();
let attr = reader.root_attr("note").unwrap().clone();
let mut reader = reader;
assert_eq!(reader.attr_string_value(&attr).unwrap(), payload_a);
std::fs::remove_file(&path).ok();
}
#[test]
fn numeric_replacing_a_vlen_attr_releases_its_collection() {
let payload = "x".repeat(8 * 1024);
let numeric = || {
AttributeMessage::scalar_numeric(
"x",
DatatypeMessage::i32_type(),
7i32.to_le_bytes().to_vec(),
)
};
let path_a = temp_path("vlen_attr_cross_a");
let writer = Hdf5Writer::create(&path_a).unwrap();
writer
.set_vlen_string_attribute(AttrTarget::Root, "x", &payload)
.unwrap();
writer.add_root_attribute(numeric()).unwrap();
writer
.set_vlen_string_attribute(AttrTarget::Root, "y", &payload)
.unwrap();
writer.close().unwrap();
let path_b = temp_path("vlen_attr_cross_b");
let writer = Hdf5Writer::create(&path_b).unwrap();
writer.add_root_attribute(numeric()).unwrap();
writer
.set_vlen_string_attribute(AttrTarget::Root, "y", &payload)
.unwrap();
writer.close().unwrap();
assert_eq!(
std::fs::metadata(&path_a).unwrap().len(),
std::fs::metadata(&path_b).unwrap().len()
);
let reader = Hdf5Reader::open(&path_a).unwrap();
let y = reader.root_attr("y").unwrap().clone();
let mut reader = reader;
assert_eq!(reader.attr_string_value(&y).unwrap(), payload);
std::fs::remove_file(&path_a).ok();
std::fs::remove_file(&path_b).ok();
}
#[test]
fn reopen_cycles_reuse_superseded_header_blocks() {
let path = temp_path("header_reuse");
{
let writer = Hdf5Writer::create(&path).unwrap();
writer.create_group("/", "g").unwrap();
let idx = writer
.create_chunked_dataset(
"g/data",
DatatypeMessage::i32_type(),
&[4],
&[u64::MAX],
&[4],
)
.unwrap();
let seed: Vec<u8> = [1i32, 2, 3, 4]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk(idx, 0, &seed).unwrap();
writer.close().unwrap();
}
let mut sizes = Vec::new();
for i in 0..6i32 {
let writer = Hdf5Writer::open_append(&path).unwrap();
let data: Vec<u8> = [i; 4].iter().flat_map(|v| v.to_le_bytes()).collect();
writer.write_chunk(0, 0, &data).unwrap();
writer.close().unwrap();
sizes.push(std::fs::metadata(&path).unwrap().len());
}
assert_eq!(&sizes[1..], &vec![sizes[0]; 5][..], "sizes: {sizes:?}");
let mut reader = Hdf5Reader::open(&path).unwrap();
let raw = reader.read_dataset_raw("g/data").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values, vec![5, 5, 5, 5]);
std::fs::remove_file(&path).ok();
}
#[test]
fn create_empty_file() {
let path = temp_path("empty");
let writer = Hdf5Writer::create(&path).unwrap();
writer.close().unwrap();
let reader = Hdf5Reader::open(&path).unwrap();
assert!(reader.dataset_names().is_empty());
std::fs::remove_file(&path).ok();
}
#[test]
fn create_single_dataset() {
let path = temp_path("single");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_dataset("data", DatatypeMessage::f64_type(), &[4])
.unwrap();
let values: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
writer.write_dataset_raw(idx, &raw).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_names(), vec!["data"]);
assert_eq!(reader.dataset_shape("data").unwrap(), vec![4]);
let readback = reader.read_dataset_raw("data").unwrap();
assert_eq!(readback, raw);
std::fs::remove_file(&path).ok();
}
#[test]
fn create_multiple_datasets() {
let path = temp_path("multi");
let writer = Hdf5Writer::create(&path).unwrap();
let idx0 = writer
.create_dataset("ints", DatatypeMessage::i32_type(), &[3])
.unwrap();
let i_data: Vec<u8> = [10i32, 20, 30]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_dataset_raw(idx0, &i_data).unwrap();
let idx1 = writer
.create_dataset("floats", DatatypeMessage::f32_type(), &[2, 2])
.unwrap();
let f_data: Vec<u8> = [1.0f32, 2.0, 3.0, 4.0]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_dataset_raw(idx1, &f_data).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
let names = reader.dataset_names();
assert!(names.contains(&"ints"));
assert!(names.contains(&"floats"));
assert_eq!(reader.dataset_shape("ints").unwrap(), vec![3]);
assert_eq!(reader.dataset_shape("floats").unwrap(), vec![2, 2]);
assert_eq!(reader.read_dataset_raw("ints").unwrap(), i_data);
assert_eq!(reader.read_dataset_raw("floats").unwrap(), f_data);
std::fs::remove_file(&path).ok();
}
#[test]
fn data_size_mismatch() {
let path = temp_path("mismatch");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_dataset("x", DatatypeMessage::u8_type(), &[4])
.unwrap();
let err = writer.write_dataset_raw(idx, &[1, 2, 3]); assert!(err.is_err());
std::fs::remove_file(&path).ok();
}
#[test]
fn create_chunked_dataset_simple() {
let path = temp_path("chunked_simple");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_chunked_dataset(
"data",
DatatypeMessage::f64_type(),
&[0, 4], &[u64::MAX, 4], &[1, 4], )
.unwrap();
for frame in 0..3u64 {
let values: Vec<f64> = (0..4).map(|i| (frame * 4 + i) as f64).collect();
let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
writer.write_chunk(idx, frame, &raw).unwrap();
}
writer.extend_dataset(idx, &[3, 4]).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_names(), vec!["data"]);
assert_eq!(reader.dataset_shape("data").unwrap(), vec![3, 4]);
let raw = reader.read_dataset_raw("data").unwrap();
let values: Vec<f64> = raw
.chunks(8)
.map(|chunk| f64::from_le_bytes(chunk.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 12);
for (i, val) in values.iter().enumerate() {
assert_eq!(*val, i as f64);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn chunked_dataset_many_frames() {
let path = temp_path("chunked_many");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_chunked_dataset(
"frames",
DatatypeMessage::i32_type(),
&[0, 2],
&[u64::MAX, 2],
&[1, 2],
)
.unwrap();
let n_frames = 10u64;
for frame in 0..n_frames {
let values = [(frame * 2) as i32, (frame * 2 + 1) as i32];
let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
writer.write_chunk(idx, frame, &raw).unwrap();
}
writer.extend_dataset(idx, &[n_frames, 2]).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("frames").unwrap(), vec![10, 2]);
let raw = reader.read_dataset_raw("frames").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|chunk| i32::from_le_bytes(chunk.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 20);
for (i, val) in values.iter().enumerate() {
assert_eq!(*val, i as i32);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn create_fixed_array_dataset_roundtrip() {
let path = temp_path("fixed_array");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_fixed_array_dataset(
"grid",
DatatypeMessage::i32_type(),
&[4, 6], &[2, 3], )
.unwrap();
let c00: Vec<u8> = [0i32, 1, 2, 6, 7, 8]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[0, 0], &c00).unwrap();
let c01: Vec<u8> = [3i32, 4, 5, 9, 10, 11]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[0, 1], &c01).unwrap();
let c10: Vec<u8> = [12i32, 13, 14, 18, 19, 20]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[1, 0], &c10).unwrap();
let c11: Vec<u8> = [15i32, 16, 17, 21, 22, 23]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[1, 1], &c11).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_names(), vec!["grid"]);
assert_eq!(reader.dataset_shape("grid").unwrap(), vec![4, 6]);
let raw = reader.read_dataset_raw("grid").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|chunk| i32::from_le_bytes(chunk.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 24);
for (i, val) in values.iter().enumerate() {
assert_eq!(*val, i as i32);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn fixed_array_paged_dblk_disk_size() {
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let hdr = FixedArrayHeader::new_for_chunks(&ctx, 3000);
assert!(hdr.is_paged());
assert_eq!(hdr.npages(), 3);
assert_eq!(fixed_array_dblk_disk_size(&ctx, &hdr), 19 + 24000 + 12);
let small = FixedArrayHeader::new_for_chunks(&ctx, 1000);
assert!(!small.is_paged());
assert_eq!(fixed_array_dblk_disk_size(&ctx, &small), 14 + 8000 + 4);
}
#[test]
fn fixed_array_paged_encode_matches_reader_layout() {
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let mut hdr = FixedArrayHeader::new_for_chunks(&ctx, 2500);
hdr.data_blk_addr = 0x9000;
let npages = hdr.npages() as usize;
let mut dblk = FixedArrayDataBlock::new_unfiltered(0x1000, 2500);
for (i, e) in dblk.elements.iter_mut().enumerate() {
*e = 0x10000 + (i as u64) * 0x100;
}
let encoded = encode_fixed_array_dblk(&ctx, &hdr, &dblk);
assert_eq!(encoded.len() as u64, fixed_array_dblk_disk_size(&ctx, &hdr));
let prefix = FixedArrayPagedPrefix::decode(&encoded, &ctx, npages as u64).unwrap();
assert_eq!(prefix.header_addr, 0x1000);
for p in 0..npages {
assert!(prefix.page_initialized(p), "page {p} should be initialized");
}
let dblk_page_nelmts = hdr.dblk_page_nelmts() as usize;
let page_stride = dblk_page_nelmts * 8 + 4;
let mut recovered = Vec::new();
for p in 0..npages {
let page_nelmts = if p + 1 == npages {
2500 - p * dblk_page_nelmts
} else {
dblk_page_nelmts
};
let off = prefix.prefix_size + p * page_stride;
let page_buf = &encoded[off..];
let addrs = crate::format::chunk_index::fixed_array::decode_unfiltered_page(
page_buf,
&ctx,
page_nelmts,
)
.unwrap();
recovered.extend(addrs);
}
assert_eq!(recovered, dblk.elements);
}
#[test]
fn fixed_array_paged_decode_roundtrip_with_uninitialized_page() {
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let hdr = FixedArrayHeader::new_for_chunks(&ctx, 2500);
let npages = hdr.npages() as usize; let page = hdr.dblk_page_nelmts() as usize;
let mut dblk = FixedArrayDataBlock::new_unfiltered(0x1000, 2500);
for i in (0..page).chain(2 * page..2500) {
dblk.elements[i] = 0x10000 + (i as u64) * 0x100;
}
let mut encoded = encode_fixed_array_dblk(&ctx, &hdr, &dblk);
let prefix = FixedArrayPagedPrefix::decode(&encoded, &ctx, npages as u64).unwrap();
assert!(prefix.page_initialized(0));
assert!(!prefix.page_initialized(1));
assert!(prefix.page_initialized(2));
let page_stride = page * 8 + 4;
let p1 = prefix.prefix_size + page_stride;
for b in &mut encoded[p1..p1 + page_stride] {
*b = 0x5A;
}
let decoded = decode_fixed_array_dblk(&ctx, &hdr, &encoded, 0).unwrap();
assert_eq!(decoded.elements, dblk.elements);
assert_eq!(decoded.header_addr, 0x1000);
}
#[test]
fn fixed_array_paged_decode_filtered_roundtrip() {
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let chunk_size_len = 4usize;
let hdr = FixedArrayHeader::new_for_filtered_chunks(&ctx, 1500, chunk_size_len as u8);
assert!(hdr.is_paged());
let mut dblk = FixedArrayDataBlock::new_filtered(0x2000, 1500);
for (i, e) in dblk.filtered_elements.iter_mut().enumerate() {
e.address = 0x8000 + (i as u64) * 0x40;
e.chunk_size = 100 + i as u64;
e.filter_mask = (i % 3) as u32;
}
let encoded = encode_fixed_array_dblk(&ctx, &hdr, &dblk);
assert_eq!(encoded.len() as u64, fixed_array_dblk_disk_size(&ctx, &hdr));
let decoded = decode_fixed_array_dblk(&ctx, &hdr, &encoded, chunk_size_len).unwrap();
assert_eq!(decoded.filtered_elements, dblk.filtered_elements);
assert_eq!(decoded.client_id, FA_CLIENT_FILT_CHUNK);
}
#[test]
fn create_fixed_array_paged_dataset_roundtrip() {
let path = temp_path("fixed_array_paged");
let n: usize = 3000;
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_fixed_array_dataset("paged", DatatypeMessage::i32_type(), &[n as u64], &[1])
.unwrap();
for i in 0..n {
let v = (i as i32).to_le_bytes();
writer
.write_chunk_fixed_array(idx, &[i as u64], &v)
.unwrap();
}
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("paged").unwrap(), vec![n as u64]);
let raw = reader.read_dataset_raw("paged").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values.len(), n);
for (i, v) in values.iter().enumerate() {
assert_eq!(*v, i as i32, "element {i}");
}
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "deflate")]
#[test]
fn create_filtered_fixed_array_dataset_roundtrip() {
let path = temp_path("fixed_array_filt");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_fixed_array_dataset_with_pipeline(
"grid",
DatatypeMessage::i32_type(),
&[4, 6], &[2, 3], FilterPipeline::deflate(6),
)
.unwrap();
let c00: Vec<u8> = [0i32, 1, 2, 6, 7, 8]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[0, 0], &c00).unwrap();
let c01: Vec<u8> = [3i32, 4, 5, 9, 10, 11]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[0, 1], &c01).unwrap();
let c10: Vec<u8> = [12i32, 13, 14, 18, 19, 20]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[1, 0], &c10).unwrap();
let c11: Vec<u8> = [15i32, 16, 17, 21, 22, 23]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_fixed_array(idx, &[1, 1], &c11).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("grid").unwrap(), vec![4, 6]);
let raw = reader.read_dataset_raw("grid").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 24);
for (i, v) in values.iter().enumerate() {
assert_eq!(*v, i as i32, "element {i}");
}
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "deflate")]
#[test]
fn create_filtered_fixed_array_paged_dataset_roundtrip() {
let path = temp_path("fixed_array_filt_paged");
let n: usize = 3000;
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_fixed_array_dataset_with_pipeline(
"paged",
DatatypeMessage::i32_type(),
&[n as u64],
&[1],
FilterPipeline::deflate(6),
)
.unwrap();
for i in 0..n {
let v = (i as i32).to_le_bytes();
writer
.write_chunk_fixed_array(idx, &[i as u64], &v)
.unwrap();
}
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("paged").unwrap(), vec![n as u64]);
let raw = reader.read_dataset_raw("paged").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values.len(), n);
for (i, v) in values.iter().enumerate() {
assert_eq!(*v, i as i32, "element {i}");
}
std::fs::remove_file(&path).ok();
}
#[test]
fn filtered_fixed_array_dblk_disk_size_and_encode() {
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let csl = 3u8; let elem_size = 8 + csl as usize + 4;
let mut flat = FixedArrayHeader::new_for_filtered_chunks(&ctx, 100, csl);
flat.data_blk_addr = 0x4000;
assert!(!flat.is_paged());
assert_eq!(
fixed_array_dblk_disk_size(&ctx, &flat),
(14 + 100 * elem_size + 4) as u64
);
let flat_dblk = FixedArrayDataBlock::new_filtered(0x1000, 100);
assert_eq!(
encode_fixed_array_dblk(&ctx, &flat, &flat_dblk).len() as u64,
fixed_array_dblk_disk_size(&ctx, &flat)
);
let mut paged = FixedArrayHeader::new_for_filtered_chunks(&ctx, 2500, csl);
paged.data_blk_addr = 0x9000;
assert!(paged.is_paged());
assert_eq!(paged.npages(), 3);
assert_eq!(
fixed_array_dblk_disk_size(&ctx, &paged),
(19 + 2500 * elem_size + 12) as u64
);
let mut paged_dblk = FixedArrayDataBlock::new_filtered(0x1000, 2500);
for (i, e) in paged_dblk.filtered_elements.iter_mut().enumerate() {
e.address = 0x10000 + (i as u64) * 0x100;
e.chunk_size = (i % 200) as u64;
}
let encoded = encode_fixed_array_dblk(&ctx, &paged, &paged_dblk);
assert_eq!(
encoded.len() as u64,
fixed_array_dblk_disk_size(&ctx, &paged)
);
let npages = paged.npages() as usize;
let prefix = FixedArrayPagedPrefix::decode(&encoded, &ctx, npages as u64).unwrap();
for p in 0..npages {
assert!(prefix.page_initialized(p), "page {p}");
}
let dblk_page_nelmts = paged.dblk_page_nelmts() as usize;
let page_stride = dblk_page_nelmts * elem_size + 4;
let mut recovered = Vec::new();
for p in 0..npages {
let page_nelmts = if p + 1 == npages {
2500 - p * dblk_page_nelmts
} else {
dblk_page_nelmts
};
let off = prefix.prefix_size + p * page_stride;
let elems = crate::format::chunk_index::fixed_array::decode_filtered_page(
&encoded[off..],
&ctx,
page_nelmts,
csl as usize,
)
.unwrap();
recovered.extend(elems);
}
assert_eq!(recovered, paged_dblk.filtered_elements);
}
#[test]
fn create_btree_v2_dataset_roundtrip() {
let path = temp_path("btree_v2");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_btree_v2_dataset(
"data",
DatatypeMessage::f64_type(),
&[0, 0], &[u64::MAX, u64::MAX], &[2, 3], )
.unwrap();
let c00: Vec<u8> = [0.0f64, 1.0, 2.0, 6.0, 7.0, 8.0]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_btree_v2(idx, &[0, 0], &c00).unwrap();
let c01: Vec<u8> = [3.0f64, 4.0, 5.0, 9.0, 10.0, 11.0]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_btree_v2(idx, &[0, 1], &c01).unwrap();
let c10: Vec<u8> = [12.0f64, 13.0, 14.0, 18.0, 19.0, 20.0]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_btree_v2(idx, &[1, 0], &c10).unwrap();
let c11: Vec<u8> = [15.0f64, 16.0, 17.0, 21.0, 22.0, 23.0]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_chunk_btree_v2(idx, &[1, 1], &c11).unwrap();
writer.extend_dataset(idx, &[4, 6]).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_names(), vec!["data"]);
assert_eq!(reader.dataset_shape("data").unwrap(), vec![4, 6]);
let raw = reader.read_dataset_raw("data").unwrap();
let values: Vec<f64> = raw
.chunks(8)
.map(|chunk| f64::from_le_bytes(chunk.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 24);
for (i, val) in values.iter().enumerate() {
assert_eq!(*val, i as f64);
}
std::fs::remove_file(&path).ok();
}
const BT2_PROBE_CHUNK: u64 = 8;
fn btree_v2_flush_probe(path: &std::path::Path, batches: &[u64]) -> Vec<(Vec<u64>, u64)> {
let writer = Hdf5Writer::create(path).unwrap();
let idx = writer
.create_btree_v2_dataset(
"data",
DatatypeMessage::f64_type(),
&[0, 0],
&[u64::MAX, u64::MAX],
&[1, 1],
)
.unwrap();
let mut written = 0u64;
let mut out = Vec::new();
for &upto in batches {
while written < upto {
writer
.write_chunk_btree_v2(idx, &[written, 0], &(written as f64).to_le_bytes())
.unwrap();
written += 1;
}
writer.flush_dataset(idx).unwrap();
let addrs = writer
.ds(idx)
.lock()
.btree_v2
.as_ref()
.unwrap()
.node_addrs
.clone();
out.push((addrs, std::fs::metadata(path).unwrap().len()));
}
writer.extend_dataset(idx, &[written.max(1), 1]).unwrap();
writer.close().unwrap();
out
}
#[test]
fn a_btree_v2_flush_frees_the_node_blocks_its_tree_gave_up() {
use crate::format::chunk_index::btree_v2::BT2_NODE_SIZE;
let path = temp_path("bt2_node_shrink");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_btree_v2_dataset(
"data",
DatatypeMessage::f64_type(),
&[0, 0],
&[u64::MAX, u64::MAX],
&[1, 1],
)
.unwrap();
for i in 0..85u64 {
writer
.write_chunk_btree_v2(idx, &[i, 0], &(i as f64).to_le_bytes())
.unwrap();
}
writer.flush_dataset(idx).unwrap();
let grown = writer
.ds(idx)
.lock()
.btree_v2
.as_ref()
.unwrap()
.node_addrs
.clone();
assert_eq!(grown.len(), 3, "expected two leaves and a root");
writer
.ds(idx)
.lock()
.btree_v2
.as_mut()
.unwrap()
.index
.records
.truncate(84);
writer.flush_dataset(idx).unwrap();
let shrunk = writer
.ds(idx)
.lock()
.btree_v2
.as_ref()
.unwrap()
.node_addrs
.clone();
assert_eq!(
shrunk,
grown[..1],
"the pool still records the surplus blocks"
);
let reused = writer
.allocator
.allocate(BT2_NODE_SIZE as u64, FreeSpaceClass::Metadata);
assert!(
(grown[1]..grown[1] + 2 * BT2_NODE_SIZE as u64).contains(&reused),
"a node block allocated at {reused:#x}, outside the freed \
[{:#x}, {:#x}) the flush gave up",
grown[1],
grown[1] + 2 * BT2_NODE_SIZE as u64
);
writer.extend_dataset(idx, &[85, 1]).unwrap();
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn a_btree_v2_with_a_foreign_node_size_reopens_and_grows() {
let path = temp_path("bt2_foreign_node_size");
{
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_btree_v2_dataset(
"data",
DatatypeMessage::f64_type(),
&[0, 0],
&[u64::MAX, u64::MAX],
&[1, 1],
)
.unwrap();
{
let ds = writer.ds(idx);
let mut m = ds.lock();
let index = &mut m.btree_v2.as_mut().unwrap().index;
index.node_size = 512;
index.split_percent = 90;
index.merge_percent = 30;
}
for i in 0..85u64 {
writer
.write_chunk_btree_v2(idx, &[i, 0], &(i as f64).to_le_bytes())
.unwrap();
}
writer.extend_dataset(idx, &[85, 1]).unwrap();
writer.close().unwrap();
}
{
let writer = Hdf5Writer::open_append(&path).unwrap();
let idx = writer.dataset_index("data").unwrap();
{
let ds = writer.ds(idx);
let m = ds.lock();
let index = &m.btree_v2.as_ref().unwrap().index;
assert_eq!(index.node_size, 512, "header node_size not adopted");
assert_eq!(index.split_percent, 90);
assert_eq!(index.merge_percent, 30);
assert_eq!(index.records.len(), 85, "records not walked back");
}
for i in 85..115u64 {
writer
.write_chunk_btree_v2(idx, &[i, 0], &(i as f64).to_le_bytes())
.unwrap();
}
writer.extend_dataset(idx, &[115, 1]).unwrap();
writer.close().unwrap();
}
let mut reader = Hdf5Reader::open(&path).unwrap();
let raw = reader.read_dataset_raw("data").unwrap();
let values: Vec<f64> = raw
.chunks(8)
.map(|c| f64::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 115);
for (i, v) in values.iter().enumerate() {
assert_eq!(*v, i as f64, "element {i}");
}
std::fs::remove_file(&path).ok();
}
#[test]
fn a_shrinking_btree_v2_node_leaves_no_stale_records_behind() {
use crate::format::chunk_index::btree_v2::{Bt2ChunkIndex, BT2_NODE_SIZE};
let path = temp_path("bt2_node_blocks");
let probe = btree_v2_flush_probe(&path, &[84, 85]);
let node0 = probe.last().unwrap().0[0];
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let mut index = Bt2ChunkIndex::new_unfiltered(2);
for i in 0..85u64 {
index.insert(vec![i, 0], 0);
}
let tree = index.build_tree(&ctx);
assert!(
tree.nodes[0].num_records < 84,
"this test needs the first leaf to shrink, got {}",
tree.nodes[0].num_records
);
let used = 10 + tree.nodes[0].num_records as usize * tree.record_size as usize;
let bytes = std::fs::read(&path).unwrap();
let block = &bytes[node0 as usize..node0 as usize + BT2_NODE_SIZE as usize];
assert!(
block[used..].iter().all(|&b| b == 0),
"leaf block at {node0:#x} still holds {} bytes of its previous, larger image",
block[used..].iter().rposition(|&b| b != 0).unwrap_or(0) + 1
);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_btree_v2_flush_allocates_only_the_node_blocks_it_adds() {
use crate::format::chunk_index::btree_v2::BT2_NODE_SIZE;
let path = temp_path("bt2_pool_growth");
let batches = [84u64, 84, 85, 200, 200];
let probe = btree_v2_flush_probe(&path, &batches);
for i in 1..probe.len() {
let (prev_addrs, prev_len) = &probe[i - 1];
let (addrs, len) = &probe[i];
assert!(
addrs.starts_with(prev_addrs),
"flush {i} moved a node block instead of reusing it"
);
let new_blocks = (addrs.len() - prev_addrs.len()) as u64 * BT2_NODE_SIZE as u64;
let new_chunks = (batches[i] - batches[i - 1]) * BT2_PROBE_CHUNK;
assert_eq!(
len - prev_len,
new_blocks + new_chunks,
"flush {i} grew the file by more than the blocks it added"
);
}
assert_eq!(probe[1].1, probe[0].1);
assert_eq!(probe[4].1, probe[3].1);
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "parallel")]
#[test]
fn parallel_batch_write_roundtrip() {
let path = temp_path("parallel_batch");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_chunked_dataset(
"data",
DatatypeMessage::i32_type(),
&[0, 4],
&[u64::MAX, 4],
&[1, 4],
)
.unwrap();
let chunks_data: Vec<(u64, Vec<u8>)> = (0..8u64)
.map(|frame| {
let values: Vec<i32> = (0..4).map(|i| (frame * 4 + i) as i32).collect();
let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
(frame, raw)
})
.collect();
let batch: Vec<(u64, &[u8])> = chunks_data
.iter()
.map(|(idx, data)| (*idx, data.as_slice()))
.collect();
writer.write_chunks_batch(idx, &batch).unwrap();
writer.extend_dataset(idx, &[8, 4]).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("data").unwrap(), vec![8, 4]);
let raw = reader.read_dataset_raw("data").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|chunk| i32::from_le_bytes(chunk.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 32);
for (i, val) in values.iter().enumerate() {
assert_eq!(*val, i as i32);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn swmr_writer_append_frames() {
use crate::io::swmr::SwmrWriter;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rust_hdf5_swmr_append_{}_{}.h5",
std::process::id(),
n
));
let mut swmr = SwmrWriter::create(&path).unwrap();
let idx = swmr
.create_streaming_dataset("detector", DatatypeMessage::u16_type(), &[4, 4])
.unwrap();
swmr.start_swmr().unwrap();
for frame in 0..5u16 {
let data: Vec<u16> = (0..16).map(|i| frame * 16 + i).collect();
let raw: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
swmr.append_frame(idx, &raw).unwrap();
}
swmr.flush().unwrap();
swmr.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("detector").unwrap(), vec![5, 4, 4]);
let raw = reader.read_dataset_raw("detector").unwrap();
let values: Vec<u16> = raw
.chunks(2)
.map(|chunk| u16::from_le_bytes(chunk.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 80); for (i, val) in values.iter().enumerate().take(16) {
assert_eq!(*val, i as u16);
}
for (i, val) in values[64..80].iter().enumerate() {
assert_eq!(*val, 4 * 16 + i as u16);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn swmr_writer_tiled_frames() {
use crate::io::swmr::SwmrWriter;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rust_hdf5_swmr_tiled_{}_{}.h5",
std::process::id(),
n
));
let mut swmr = SwmrWriter::create(&path).unwrap();
let idx = swmr
.create_streaming_dataset_tiled("det", DatatypeMessage::u16_type(), &[4, 4], &[2, 2])
.unwrap();
swmr.start_swmr().unwrap();
for frame in 0..3u16 {
let data: Vec<u16> = (0..16).map(|i| frame * 100 + i).collect();
let raw: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
swmr.append_frame(idx, &raw).unwrap();
}
swmr.flush().unwrap();
swmr.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("det").unwrap(), vec![3, 4, 4]);
let raw = reader.read_dataset_raw("det").unwrap();
let values: Vec<u16> = raw
.chunks(2)
.map(|c| u16::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 48);
for frame in 0..3u16 {
for i in 0..16usize {
assert_eq!(values[frame as usize * 16 + i], frame * 100 + i as u16);
}
}
std::fs::remove_file(&path).ok();
}
#[test]
fn swmr_writer_tiled_chunk_larger_than_frame_is_rejected() {
use crate::io::swmr::SwmrWriter;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rust_hdf5_swmr_bigchunk_{}_{}.h5",
std::process::id(),
n
));
let mut swmr = SwmrWriter::create(&path).unwrap();
let err = swmr
.create_streaming_dataset_tiled("det", DatatypeMessage::u16_type(), &[3, 3], &[8, 8])
.unwrap_err();
assert!(
err.to_string().contains("maximum dimension size"),
"unexpected error: {err}"
);
swmr.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn swmr_writer_multi_frame_chunks() {
use crate::io::swmr::SwmrWriter;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rust_hdf5_swmr_mfc_{}_{}.h5",
std::process::id(),
n
));
let mut swmr = SwmrWriter::create(&path).unwrap();
let idx = swmr
.create_streaming_dataset_chunked(
"det",
DatatypeMessage::u16_type(),
&[3, 3],
&[4, 3, 3],
)
.unwrap();
swmr.start_swmr().unwrap();
for frame in 0..10u16 {
let data: Vec<u16> = (0..9).map(|i| frame * 100 + i).collect();
let raw: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
swmr.append_frame(idx, &raw).unwrap();
}
swmr.flush().unwrap();
swmr.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("det").unwrap(), vec![10, 3, 3]);
let raw = reader.read_dataset_raw("det").unwrap();
let values: Vec<u16> = raw
.chunks(2)
.map(|c| u16::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 90);
for frame in 0..10u16 {
for i in 0..9usize {
assert_eq!(values[frame as usize * 9 + i], frame * 100 + i as u16);
}
}
std::fs::remove_file(&path).ok();
}
#[test]
fn swmr_writer_multi_frame_tiled_chunks() {
use crate::io::swmr::SwmrWriter;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rust_hdf5_swmr_mftc_{}_{}.h5",
std::process::id(),
n
));
let mut swmr = SwmrWriter::create(&path).unwrap();
let idx = swmr
.create_streaming_dataset_chunked(
"det",
DatatypeMessage::u16_type(),
&[4, 4],
&[2, 2, 2],
)
.unwrap();
swmr.start_swmr().unwrap();
for frame in 0..5u16 {
let data: Vec<u16> = (0..16).map(|i| frame * 100 + i).collect();
let raw: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
swmr.append_frame(idx, &raw).unwrap();
}
swmr.flush().unwrap();
swmr.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("det").unwrap(), vec![5, 4, 4]);
let raw = reader.read_dataset_raw("det").unwrap();
let values: Vec<u16> = raw
.chunks(2)
.map(|c| u16::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values.len(), 80);
for frame in 0..5u16 {
for i in 0..16usize {
assert_eq!(values[frame as usize * 16 + i], frame * 100 + i as u16);
}
}
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "deflate")]
#[test]
fn swmr_writer_compressed_frames() {
use crate::io::swmr::SwmrWriter;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rust_hdf5_swmr_comp_{}_{}.h5",
std::process::id(),
n
));
let mut swmr = SwmrWriter::create(&path).unwrap();
let pipeline = crate::format::messages::filter::FilterPipeline::deflate(4);
let idx = swmr
.create_streaming_dataset_compressed(
"detector",
DatatypeMessage::i32_type(),
&[8],
pipeline,
)
.unwrap();
swmr.start_swmr().unwrap();
for frame in 0..40i32 {
let raw: Vec<u8> = (0..8).flat_map(|i| (frame * 8 + i).to_le_bytes()).collect();
swmr.append_frame(idx, &raw).unwrap();
if frame % 7 == 0 {
swmr.flush().unwrap();
}
}
swmr.flush().unwrap();
swmr.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("detector").unwrap(), vec![40, 8]);
let raw = reader.read_dataset_raw("detector").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values, (0..320).collect::<Vec<i32>>());
std::fs::remove_file(&path).ok();
}
#[test]
fn group_hierarchy_writer_reader() {
let path = temp_path("group_hierarchy");
let writer = Hdf5Writer::create(&path).unwrap();
let g0 = writer.create_group("/", "group1").unwrap();
let g1 = writer.create_group("/group1", "sub").unwrap();
assert_eq!(g0, 0);
assert_eq!(g1, 1);
let ds_root = writer
.create_dataset("root_data", DatatypeMessage::f64_type(), &[2])
.unwrap();
let raw_root: Vec<u8> = [1.0f64, 2.0].iter().flat_map(|v| v.to_le_bytes()).collect();
writer.write_dataset_raw(ds_root, &raw_root).unwrap();
let ds_g0 = writer
.create_dataset("group1/data", DatatypeMessage::i32_type(), &[3])
.unwrap();
let raw_g0: Vec<u8> = [10i32, 20, 30]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_dataset_raw(ds_g0, &raw_g0).unwrap();
let ds_g1 = writer
.create_dataset("group1/sub/values", DatatypeMessage::u8_type(), &[4])
.unwrap();
writer.write_dataset_raw(ds_g1, &[1u8, 2, 3, 4]).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
let names = reader.dataset_names();
assert!(names.contains(&"root_data"), "names: {:?}", names);
assert!(names.contains(&"group1/data"), "names: {:?}", names);
assert!(names.contains(&"group1/sub/values"), "names: {:?}", names);
let raw = reader.read_dataset_raw("root_data").unwrap();
let vals: Vec<f64> = raw
.chunks(8)
.map(|c| f64::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(vals, vec![1.0, 2.0]);
let raw = reader.read_dataset_raw("group1/data").unwrap();
let vals: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(vals, vec![10, 20, 30]);
let raw = reader.read_dataset_raw("group1/sub/values").unwrap();
assert_eq!(raw, vec![1, 2, 3, 4]);
std::fs::remove_file(&path).ok();
}
#[test]
fn create_rejects_a_chunk_wider_than_a_fixed_max_dimension() {
let path = temp_path("chunk_wider_than_max");
let writer = Hdf5Writer::create(&path).unwrap();
let err = writer
.create_chunked_dataset(
"data",
DatatypeMessage::f64_type(),
&[0, 2],
&[u64::MAX, 2],
&[2, 4],
)
.unwrap_err();
assert!(
err.to_string().contains("maximum dimension size"),
"unexpected error: {err}"
);
let err = writer
.create_fixed_array_dataset("fa", DatatypeMessage::f64_type(), &[3], &[5])
.unwrap_err();
assert!(
err.to_string().contains("maximum dimension size"),
"unexpected error: {err}"
);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn create_mirrors_the_libhdf5_chunk_geometry_exemptions() {
let path = temp_path("chunk_geometry_exemptions");
let writer = Hdf5Writer::create(&path).unwrap();
writer
.create_chunked_dataset(
"exempt",
DatatypeMessage::f64_type(),
&[0, 0],
&[u64::MAX, 2],
&[2, 4],
)
.unwrap();
let err = writer
.create_chunked_dataset("zero", DatatypeMessage::f64_type(), &[0], &[u64::MAX], &[0])
.unwrap_err();
assert!(
err.to_string().contains("chunk dimension 0 is zero"),
"unexpected error: {err}"
);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn append_scatters_into_a_legacy_wider_than_row_chunk() {
let path = temp_path("legacy_wide_chunk_append");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_chunked_dataset(
"data",
DatatypeMessage::i32_type(),
&[0, 2],
&[u64::MAX, 2],
&[2, 2],
)
.unwrap();
writer.ds(idx).lock().chunked.as_mut().unwrap().chunk_dims = vec![2, 4];
let frames: Vec<u8> = [1i32, 2, 3, 4]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
writer.write_append_frames(idx, 0, 2, &frames).unwrap();
writer.extend_dataset(idx, &[2, 2]).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
assert_eq!(reader.dataset_shape("data").unwrap(), vec![2, 2]);
let raw = reader.read_dataset_raw("data").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values, vec![1, 2, 3, 4]);
std::fs::remove_file(&path).ok();
}
#[test]
#[cfg(feature = "deflate")]
fn compressed_vlen_create_validates_its_chunk_size() {
use crate::format::messages::filter::FilterPipeline;
let path = temp_path("vlen_compressed_chunk");
let writer = Hdf5Writer::create(&path).unwrap();
let err = writer
.create_vlen_string_dataset_compressed(
"texts",
&["a", "b", "c"],
100,
FilterPipeline::deflate(6),
)
.unwrap_err();
assert!(
err.to_string().contains("maximum dimension size"),
"unexpected error: {err}"
);
writer
.create_vlen_string_dataset_compressed("empty", &[], 16, FilterPipeline::deflate(6))
.unwrap();
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "deflate")]
#[test]
fn libver_latest_selects_v5_for_filtered_chunks_only() {
let path = temp_path("libver_v5_select");
let mut writer = Hdf5Writer::create(&path).unwrap();
let before = writer
.create_chunked_dataset_with_pipeline(
"d4",
DatatypeMessage::i32_type(),
&[0],
&[u64::MAX],
&[16],
FilterPipeline::deflate(4),
)
.unwrap();
writer.set_libver_latest(true).unwrap();
let ea5 = writer
.create_chunked_dataset_with_pipeline(
"ea5",
DatatypeMessage::i32_type(),
&[0],
&[u64::MAX],
&[16],
FilterPipeline::deflate(4),
)
.unwrap();
let plain = writer
.create_chunked_dataset(
"plain",
DatatypeMessage::i32_type(),
&[0],
&[u64::MAX],
&[16],
)
.unwrap();
let fa5 = writer
.create_fixed_array_dataset_with_pipeline(
"fa5",
DatatypeMessage::i32_type(),
&[4, 6],
&[2, 3],
FilterPipeline::deflate(6),
)
.unwrap();
let bt5 = writer
.create_btree_v2_dataset_with_pipeline(
"bt5",
DatatypeMessage::i32_type(),
&[0, 0],
&[u64::MAX, u64::MAX],
&[2, 3],
FilterPipeline::deflate(6),
)
.unwrap();
{
let d4 = writer.ds(before);
let d4 = d4.lock();
assert_eq!(d4.layout_version, 4);
assert_eq!(
d4.chunked.as_ref().unwrap().chunk_size_len,
compute_chunk_size_len(16 * 4)
);
let e5 = writer.ds(ea5);
let e5 = e5.lock();
assert_eq!(e5.layout_version, 5);
assert_eq!(e5.chunked.as_ref().unwrap().chunk_size_len, 8);
assert_eq!(writer.ds(plain).lock().layout_version, 4);
assert_eq!(writer.ds(fa5).lock().layout_version, 5);
assert_eq!(writer.ds(bt5).lock().layout_version, 5);
}
for (coords, vals) in [
([0u64, 0], [0i32, 1, 2, 6, 7, 8]),
([0, 1], [3, 4, 5, 9, 10, 11]),
([1, 0], [12, 13, 14, 18, 19, 20]),
([1, 1], [15, 16, 17, 21, 22, 23]),
] {
let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
writer
.write_chunk_fixed_array(fa5, &coords, &bytes)
.unwrap();
writer.write_chunk_btree_v2(bt5, &coords, &bytes).unwrap();
}
writer.extend_dataset(bt5, &[4, 6]).unwrap();
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
for name in ["fa5", "bt5"] {
let raw = reader.read_dataset_raw(name).unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values, (0..24).collect::<Vec<i32>>(), "dataset {name}");
}
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "deflate")]
#[test]
fn v5_layout_survives_reopen_and_append() {
let path = temp_path("libver_v5_reopen");
let chunk: usize = 8;
let mut writer = Hdf5Writer::create(&path).unwrap();
writer.set_libver_latest(true).unwrap();
let idx = writer
.create_chunked_dataset_with_pipeline(
"d",
DatatypeMessage::i32_type(),
&[0],
&[u64::MAX],
&[chunk as u64],
FilterPipeline::deflate(4),
)
.unwrap();
for c in 0..2u64 {
let data: Vec<u8> = (0..chunk as i32)
.flat_map(|i| (c as i32 * chunk as i32 + i).to_le_bytes())
.collect();
writer.write_chunk(idx, c, &data).unwrap();
}
writer.extend_dataset(idx, &[2 * chunk as u64]).unwrap();
writer.close().unwrap();
let writer = Hdf5Writer::open_append(&path).unwrap();
assert_eq!(writer.ds(0).lock().layout_version, 5);
for c in 2..4u64 {
let data: Vec<u8> = (0..chunk as i32)
.flat_map(|i| (c as i32 * chunk as i32 + i).to_le_bytes())
.collect();
writer.write_chunk(0, c, &data).unwrap();
}
writer.extend_dataset(0, &[4 * chunk as u64]).unwrap();
writer.close().unwrap();
let writer = Hdf5Writer::open_append(&path).unwrap();
assert_eq!(writer.ds(0).lock().layout_version, 5);
writer.close().unwrap();
let mut reader = Hdf5Reader::open(&path).unwrap();
let raw = reader.read_dataset_raw("d").unwrap();
let values: Vec<i32> = raw
.chunks(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()))
.collect();
assert_eq!(values, (0..4 * chunk as i32).collect::<Vec<i32>>());
std::fs::remove_file(&path).ok();
}
#[test]
fn oversized_chunk_forces_v5_without_opt_in() {
let path = temp_path("libver_4gib_force");
let writer = Hdf5Writer::create(&path).unwrap();
let at_limit = writer
.create_chunked_dataset_with_pipeline(
"at_limit",
DatatypeMessage::u8_type(),
&[0],
&[u64::MAX],
&[u32::MAX as u64],
FilterPipeline::deflate(4),
)
.unwrap();
let over = writer
.create_chunked_dataset_with_pipeline(
"over",
DatatypeMessage::u8_type(),
&[0],
&[u64::MAX],
&[u32::MAX as u64 + 1],
FilterPipeline::deflate(4),
)
.unwrap();
let over_unfiltered = writer
.create_chunked_dataset(
"over_plain",
DatatypeMessage::u8_type(),
&[0],
&[u64::MAX],
&[u32::MAX as u64 + 1],
)
.unwrap();
assert_eq!(writer.ds(at_limit).lock().layout_version, 4);
{
let ds = writer.ds(over);
let ds = ds.lock();
assert_eq!(ds.layout_version, 5);
assert_eq!(ds.chunked.as_ref().unwrap().chunk_size_len, 8);
}
assert_eq!(writer.ds(over_unfiltered).lock().layout_version, 5);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn swmr_reaches_version_3_with_no_chunked_dataset_in_the_file() {
let path = temp_path("swmr_superblock");
let mut writer = Hdf5Writer::create(&path).unwrap();
writer
.create_dataset("d", DatatypeMessage::i32_type(), &[2])
.unwrap();
assert_eq!(writer.superblock_version_for(0), SUPERBLOCK_V2);
writer.finalize_for_swmr().unwrap();
writer.handle().release_lock().unwrap();
assert_eq!(std::fs::read(&path).unwrap()[8], SUPERBLOCK_V3);
writer.close().unwrap();
assert_eq!(std::fs::read(&path).unwrap()[8], SUPERBLOCK_V3);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_named_bound_below_v110_refuses_an_swmr_session() {
for bound in [LibverBound::Earliest, LibverBound::V18] {
let path = temp_path(&format!("swmr_refused_{bound:?}"));
let mut writer = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
libver: Some(bound),
..Default::default()
},
)
.unwrap();
writer
.create_dataset("d", DatatypeMessage::i32_type(), &[2])
.unwrap();
let err = writer.finalize_for_swmr().unwrap_err().to_string();
assert!(err.contains("SWMR"), "{bound:?}: {err}");
assert!(err.contains("H5F_LIBVER_V110"), "{bound:?}: {err}");
writer.close().unwrap();
let version = std::fs::read(&path).unwrap()[8];
assert_eq!(version, bound.superblock_version(), "{bound:?}");
std::fs::remove_file(&path).ok();
}
}
#[test]
fn inplace_rewrite_goes_over_a_chained_header() {
let path = temp_path("inplace_rewrite_chained");
let writer = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
libver: Some(LibverBound::V110),
..Default::default()
},
)
.unwrap();
writer
.create_chunked_dataset("d", DatatypeMessage::i32_type(), &[0], &[u64::MAX], &[4])
.unwrap();
writer.close().unwrap();
let mut writer = Hdf5Writer::open_append(&path).unwrap();
let idx = 0;
let published = writer.ds(idx).lock().obj_header_written_addr.unwrap();
for i in 0..4 {
writer
.add_dataset_attribute(
idx,
AttributeMessage::array_numeric(
&format!("wide{i}"),
DatatypeMessage::f64_type(),
&[32],
vec![0u8; 256],
),
)
.unwrap();
}
writer.finalize_for_swmr().unwrap();
let blocks = writer.ds(idx).lock().obj_header_blocks.clone();
assert_eq!(blocks.len(), 2, "chunk 0 and a continuation: {blocks:?}");
assert_eq!(blocks[0].0, published, "chunk 0 stayed where it was");
writer.write_dataset_header_inplace(idx).unwrap();
assert_eq!(writer.ds(idx).lock().obj_header_blocks, blocks);
writer.close().unwrap();
let writer = Hdf5Writer::open_append(&path).unwrap();
assert_eq!(writer.ds(0).lock().obj_header_written_addr, Some(published));
assert_eq!(writer.ds(0).lock().attributes.len(), 4);
std::fs::remove_file(&path).ok();
}
#[test]
fn every_dataset_header_write_records_its_link_count() {
let path = temp_path("header_write_records_nlink");
let writer = Hdf5Writer::create(&path).unwrap();
let idx = writer
.create_chunked_dataset("d", DatatypeMessage::i32_type(), &[0], &[u64::MAX], &[4])
.unwrap();
let mut writer = writer;
writer.finalize_for_swmr().unwrap();
assert_eq!(
writer.ds(idx).lock().nlink_written,
1,
"the SWMR publish put one name in the header"
);
writer.write_dataset_header_inplace(idx).unwrap();
assert_eq!(writer.ds(idx).lock().nlink_written, 1);
writer.create_hard_link("/", "alias", "d").unwrap();
assert_eq!(writer.object_link_count(HardLinkTarget::Dataset(idx)), 2);
let grew = writer
.write_dataset_header_inplace(idx)
.unwrap_err()
.to_string();
assert!(
grew.contains("cannot rewrite in place"),
"a header that outgrew its block must be refused: {grew}"
);
assert_eq!(
writer.ds(idx).lock().nlink_written,
1,
"a refused rewrite leaves the registry describing the header the file holds"
);
writer.close().unwrap();
let writer = Hdf5Writer::open_append(&path).unwrap();
assert_eq!(
writer.ds(0).lock().nlink_written,
2,
"finalize wrote two names and the reopen reads two"
);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn required_chunk_layout_version_pins_5_past_4_gib() {
assert_eq!(
Hdf5Writer::required_chunk_layout_version(u32::MAX as u64),
LAYOUT_VERSION_DEFAULT
);
assert_eq!(
Hdf5Writer::required_chunk_layout_version(u32::MAX as u64 + 1),
5
);
}
#[test]
fn uses_v110_chunk_indexing_escapes_past_4_gib_at_every_bound() {
let over_4gib = u32::MAX as u64 + 1;
let small = 1024u64;
let path = temp_path("uses_v110_default");
let writer = Hdf5Writer::create(&path).unwrap();
assert!(writer.uses_v110_chunk_indexing(small));
assert!(writer.uses_v110_chunk_indexing(over_4gib));
writer.close().unwrap();
std::fs::remove_file(&path).ok();
let path = temp_path("uses_v110_v18");
let mut writer = Hdf5Writer::create(&path).unwrap();
writer.set_libver_bound(LibverBound::V18).unwrap();
assert!(
!writer.uses_v110_chunk_indexing(small),
"V18's layout row (3) stays below the v1.10 gate for an ordinary chunk"
);
assert!(
writer.uses_v110_chunk_indexing(over_4gib),
"the >4 GiB escape reaches v1.10 indexing despite V18's row"
);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
let path = temp_path("uses_v110_earliest");
let mut writer = Hdf5Writer::create(&path).unwrap();
writer.set_libver_bound(LibverBound::Earliest).unwrap();
assert!(
!writer.uses_v110_chunk_indexing(small),
"Earliest's layout row (1) stays below the v1.10 gate for an ordinary chunk"
);
assert!(
writer.uses_v110_chunk_indexing(over_4gib),
"the >4 GiB escape reaches v1.10 indexing despite Earliest's row"
);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn chunk_layout_version_pins_5_past_4_gib_at_every_bound() {
let over_4gib = u32::MAX as u64 + 1;
let small = 1024u64;
let path = temp_path("chunk_ver_default");
let writer = Hdf5Writer::create(&path).unwrap();
assert_eq!(writer.chunk_layout_version(false, small), 4);
assert_eq!(writer.chunk_layout_version(false, over_4gib), 5);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
let path = temp_path("chunk_ver_v18");
let mut writer = Hdf5Writer::create(&path).unwrap();
writer.set_libver_bound(LibverBound::V18).unwrap();
assert_eq!(writer.chunk_layout_version(false, small), 3);
assert_eq!(writer.chunk_layout_version(false, over_4gib), 5);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
let path = temp_path("chunk_ver_earliest");
let mut writer = Hdf5Writer::create(&path).unwrap();
writer.set_libver_bound(LibverBound::Earliest).unwrap();
assert_eq!(
writer.chunk_layout_version(false, small),
LAYOUT_VERSION_DEFAULT
);
assert_eq!(writer.chunk_layout_version(false, over_4gib), 5);
writer.close().unwrap();
std::fs::remove_file(&path).ok();
}
#[test]
fn a_persisting_file_reopens_with_its_free_sections() {
let path = fixture_copy("fsm_persist.h5", "fsm_read");
let writer = Hdf5Writer::open_append(&path).unwrap();
let fs = writer.free_space.as_deref().expect("managers were read");
assert!(fs.info.persist);
assert_eq!(fs.info.strategy, FileSpaceStrategy::FsmAggr);
assert_eq!(fs.info.threshold, 1);
let sections = writer.allocator.free_blocks();
assert_eq!(sections.iter().map(|s| s.1).sum::<u64>(), 1910);
for w in sections.windows(2) {
assert!(w[0].0 + w[0].1 < w[1].0, "{sections:?}");
}
assert_eq!(fs.superseded.len(), 4);
for &(addr, len) in &fs.superseded {
assert!(len > 0);
assert!(
!sections
.iter()
.any(|&(a, l)| addr < a + l && a < addr + len),
"manager block {addr:#x}+{len} sits in a free section"
);
}
drop(writer);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_created_file_declares_the_strategy_it_was_made_with() {
let path = temp_path("fsm_create");
{
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::FsmAggr, true, 1),
..Default::default()
},
)
.unwrap();
let i = w
.create_dataset("keep", DatatypeMessage::i32_type(), &[8])
.unwrap();
w.write_dataset_raw(i, &[0u8; 32]).unwrap();
w.close().unwrap();
}
let info = read_only_append(&path)
.free_space
.as_deref()
.expect("the created file declares a strategy")
.info
.clone();
assert_eq!(info.strategy, FileSpaceStrategy::FsmAggr);
assert!(info.persist);
assert_eq!(info.threshold, 1);
assert_eq!(info.page_size, 4096);
assert_ne!(info.fs_addr[0], UNDEF_ADDR);
assert!(info.fs_addr.iter().skip(1).all(|&a| a == UNDEF_ADDR));
append_one(&path, "added", false);
assert!(
tracked_free_space(&path) > 0,
"the append recorded no free space"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_strategy_without_managers_still_declares_itself() {
for (strategy, persist) in [
(FileSpaceStrategy::Aggr, true),
(FileSpaceStrategy::None, false),
] {
let path = temp_path("fsm_nomgr");
{
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(strategy, persist, 7),
..Default::default()
},
)
.unwrap();
w.create_dataset("d", DatatypeMessage::f64_type(), &[4])
.unwrap();
w.close().unwrap();
}
let info = declared_file_space(&path).expect("the strategy is declared");
assert_eq!(info.strategy, strategy);
assert!(!info.persist);
assert_eq!(info.threshold, 1);
let _ = std::fs::remove_file(&path);
}
}
#[test]
fn the_default_strategy_writes_no_message() {
let path = temp_path("fsm_default");
{
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::FsmAggr, false, 1),
..Default::default()
},
)
.unwrap();
w.create_dataset("d", DatatypeMessage::f64_type(), &[4])
.unwrap();
w.close().unwrap();
}
assert!(declared_file_space(&path).is_none());
let _ = std::fs::remove_file(&path);
}
fn declared_file_space(path: &std::path::Path) -> Option<FileSpaceInfoMessage> {
crate::io::reader::Hdf5Reader::open(path)
.unwrap()
.superblock_extension()
.file_space_info
.clone()
}
#[test]
fn a_created_paged_file_lays_its_pages_out() {
let path = temp_path("fsm_paged_created");
{
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::Page, true, 1),
..Default::default()
},
)
.unwrap();
let i = w
.create_dataset("keep", DatatypeMessage::i32_type(), &[8])
.unwrap();
w.write_dataset_raw(i, &[0u8; 32]).unwrap();
w.close().unwrap();
}
let info = read_only_append(&path)
.free_space
.as_deref()
.expect("the created file declares a strategy")
.info
.clone();
assert_eq!(info.strategy, FileSpaceStrategy::Page);
assert!(info.persist);
assert_eq!(info.page_size, 4096);
assert_eq!(
std::fs::metadata(&path).unwrap().len() % info.page_size,
0,
"a paged file ends on a page boundary"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_paged_file_refuses_a_userblock_smaller_than_its_page() {
let path = temp_path("fsm_paged_userblock");
let Err(err) = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::Page, true, 1),
userblock: 512,
..Default::default()
},
) else {
panic!("a 512-byte userblock was accepted on a 4096-byte page");
};
assert!(
format!("{err}").contains("multiple of its 4096-byte"),
"{err}"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_file_created_at_a_non_default_page_size_allocates_by_it() {
let path = temp_path("fsm_page_size_8k");
{
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::Page, true, 1)
.with_page_size(8192),
..Default::default()
},
)
.unwrap();
let i = w
.create_dataset("keep", DatatypeMessage::i32_type(), &[8])
.unwrap();
w.write_dataset_raw(i, &[0u8; 32]).unwrap();
w.close().unwrap();
}
let info = read_only_append(&path)
.free_space
.as_deref()
.expect("the created file declares a strategy")
.info
.clone();
assert_eq!(info.page_size, 8192);
assert_eq!(
std::fs::metadata(&path).unwrap().len() % 8192,
0,
"the file ends on one of the pages it was created with"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_non_default_page_size_alone_gives_the_file_a_message() {
let path = temp_path("fsm_page_size_only");
{
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::default().with_page_size(1024),
..Default::default()
},
)
.unwrap();
w.close().unwrap();
}
let info = declared_file_space(&path)
.expect("a file naming only a page size still carries the message");
assert_eq!(info.strategy, FileSpaceStrategy::FsmAggr);
assert!(!info.persist);
assert_eq!(info.page_size, 1024);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_page_size_outside_the_library_bounds_is_refused() {
for size in [0, 1, 511, PAGE_SIZE_MAX + 1] {
let path = temp_path(&format!("fsm_page_size_bad_{size}"));
let Err(err) = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::Page, true, 1)
.with_page_size(size),
..Default::default()
},
) else {
panic!("a {size}-byte file-space page was accepted");
};
assert!(
format!("{err}").contains("between 512 bytes and 1073741824"),
"{err}"
);
let _ = std::fs::remove_file(&path);
}
let path = temp_path("fsm_page_size_odd");
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::Page, true, 1)
.with_page_size(513),
..Default::default()
},
)
.expect("513 is inside the bounds, and no power of two is required");
w.close().unwrap();
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_paged_file_reports_the_managers_it_persists() {
let path = fixture_copy("fsm_persist_page.h5", "fsm_read_paged");
let writer = Hdf5Writer::open_append(&path).unwrap();
let fs = writer.free_space.as_deref().expect("no managers read");
assert_eq!(fs.info.strategy, FileSpaceStrategy::Page);
assert!(
!writer.allocator.free_extents().is_empty(),
"the sections the file records were not put back in circulation"
);
drop(writer);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_file_without_a_strategy_has_no_managers() {
let path = temp_path("fsm_none");
{
let w = Hdf5Writer::create(&path).unwrap();
w.create_dataset("d", DatatypeMessage::f64_type(), &[4])
.unwrap();
w.close().unwrap();
}
let writer = Hdf5Writer::open_append(&path).unwrap();
assert!(writer.free_space.is_none());
drop(writer);
let _ = std::fs::remove_file(&path);
}
fn tracked_free_space(path: &std::path::Path) -> u64 {
read_only_append(path)
.allocator
.free_blocks()
.iter()
.map(|b| b.1)
.sum()
}
fn read_only_append(path: &std::path::Path) -> Hdf5Writer {
let mut w = Hdf5Writer::open_append(path).unwrap();
w.closed = true;
w
}
fn append_one(path: &std::path::Path, name: &str, disable_managers: bool) {
let mut w = Hdf5Writer::open_append(path).unwrap();
if disable_managers {
w.free_space = None;
w.allocator.reset_free_list(&[]);
}
let i = w
.create_dataset(name, DatatypeMessage::i32_type(), &[8])
.unwrap();
w.write_dataset_raw(
i,
&(0..8i32).flat_map(|v| v.to_le_bytes()).collect::<Vec<u8>>(),
)
.unwrap();
w.close().unwrap();
}
#[test]
fn a_reopen_carries_every_chunk_of_the_superblock_extension() {
let path = fixture_copy("fsm_persist.h5", "fsm_ext_chunks");
let blocks = read_only_append(&path).extension.superseded.clone();
assert!(
blocks.len() > 1,
"the fixture's extension is one chunk, so this proves nothing: {blocks:?}"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn an_append_reuses_and_records_the_space_the_managers_track() {
let path = fixture_copy("fsm_persist.h5", "fsm_write");
let original = std::fs::metadata(&path).unwrap().len();
let before = tracked_free_space(&path);
assert_eq!(before, 1910, "the fixture's own managers");
append_one(&path, "added", false);
let size = std::fs::metadata(&path).unwrap().len();
let tracked = tracked_free_space(&path);
let control = fixture_copy("fsm_persist.h5", "fsm_write_control");
append_one(&control, "added", true);
let control_size = std::fs::metadata(&control).unwrap().len();
assert_eq!(
tracked_free_space(&control),
before,
"with the manager rewrite disabled the number must not move"
);
assert!(
size < control_size,
"the append took nothing from the {before} bytes free: \
{original} grew to {size}, the control to {control_size}"
);
assert!(
control_size > original,
"the control has to grow or it proves nothing"
);
assert!(
size - tracked < control_size - before,
"unaccounted space went from {} to {}",
control_size - before,
size - tracked
);
for p in [&path, &control] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn the_manager_records_the_free_list_the_close_ends_with() {
let path = fixture_copy("fsm_persist.h5", "fsm_roundtrip");
let internal = {
let mut w = Hdf5Writer::open_append(&path).unwrap();
let i = w
.create_dataset("added", DatatypeMessage::i32_type(), &[8])
.unwrap();
w.write_dataset_raw(i, &[0u8; 32]).unwrap();
w.finalize(true).unwrap();
let blocks = w.allocator.free_extents();
w.closed = true;
blocks
};
assert!(!internal.is_empty(), "the append freed nothing");
let reread = {
let w = read_only_append(&path);
assert!(w.free_space.is_some(), "managers were written");
w.allocator.free_extents()
};
assert_eq!(internal, reread);
let _ = std::fs::remove_file(&path);
}
#[test]
fn the_manager_records_the_free_list_a_paged_close_ends_with() {
let path = fixture_copy("fsm_persist_page.h5", "fsm_paged_roundtrip");
let internal = {
let mut w = Hdf5Writer::open_append(&path).unwrap();
let i = w
.create_dataset("added", DatatypeMessage::i32_type(), &[8])
.unwrap();
w.write_dataset_raw(i, &[0u8; 32]).unwrap();
w.finalize(true).unwrap();
let blocks = w.allocator.free_extents();
w.closed = true;
blocks
};
assert!(!internal.is_empty(), "the append freed nothing");
let reread = {
let w = read_only_append(&path);
assert!(w.free_space.is_some(), "managers were written");
w.allocator.free_extents()
};
assert_eq!(internal, reread);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_paged_append_records_nothing_without_the_manager_rewrite() {
let path = fixture_copy("fsm_persist_page.h5", "fsm_paged_measured");
let control = fixture_copy("fsm_persist_page.h5", "fsm_paged_control");
let before = tracked_free_space(&path);
let original = std::fs::metadata(&path).unwrap().len();
append_one(&path, "added", false);
append_one(&control, "added", true);
assert_eq!(
tracked_free_space(&control),
before,
"the control moved the number it is there to hold still"
);
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
original,
"the append grew a paged file with {before} bytes recorded free"
);
assert!(
std::fs::metadata(&control).unwrap().len() > original,
"the control has to grow or it proves nothing"
);
assert_ne!(
tracked_free_space(&path),
before,
"the managers came back holding what the fixture wrote"
);
for p in [&path, &control] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn a_released_raw_block_lands_in_the_raw_data_manager() {
let path = temp_path("fsm_dichotomy");
{
let w = Hdf5Writer::create_with_options(
&path,
FileCreateOptions {
file_space: FileSpaceConfig::new(FileSpaceStrategy::FsmAggr, true, 1),
..Default::default()
},
)
.unwrap();
let i = w
.create_dataset("bulk", DatatypeMessage::i32_type(), &[256])
.unwrap();
w.write_dataset_raw(i, &vec![0u8; 1024]).unwrap();
w.create_dataset("keep", DatatypeMessage::i32_type(), &[8])
.unwrap();
w.close().unwrap();
}
let (raw_addr, raw_len) = {
let w = read_only_append(&path);
let i = w.dataset_index("bulk").unwrap();
let ds = w.ds(i);
let m = ds.lock();
(m.data_addr, m.data_size)
};
assert!(raw_len >= 1024, "the raw block is {raw_len} bytes");
{
let w = Hdf5Writer::open_append(&path).unwrap();
w.delete_dataset("bulk").unwrap();
w.close().unwrap();
}
let mut w = read_only_append(&path);
let info = w
.free_space
.as_deref()
.expect("the file persists managers")
.info
.clone();
assert_ne!(info.fs_addr[0], UNDEF_ADDR, "no metadata manager");
assert_ne!(info.fs_addr[2], UNDEF_ADDR, "no raw-data manager");
for (slot, &addr) in info.fs_addr.iter().enumerate() {
if slot != 0 && slot != 2 {
assert_eq!(addr, UNDEF_ADDR, "slot {slot} names a manager");
}
}
let found = crate::io::free_space_io::read_managers(&mut w.handle, &w.ctx, &info).unwrap();
let inside = |b: &FreeBlock| b.addr >= raw_addr && b.addr + b.len <= raw_addr + raw_len;
let raw: Vec<&FreeBlock> = found
.sections
.iter()
.filter(|b| b.manager == FreeSpaceManager::RawData)
.collect();
assert!(
!raw.is_empty(),
"the deleted dataset's bytes were not recorded"
);
assert!(
raw.iter().all(|b| inside(b)),
"a raw-data section is outside the deleted dataset's block: {raw:?}"
);
assert!(
found
.sections
.iter()
.filter(|b| b.manager == FreeSpaceManager::Metadata)
.all(|b| !inside(b)),
"raw-data bytes were recorded by the metadata manager"
);
drop(w);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_paged_file_names_only_the_managers_sec2_can_reach() {
let path = fixture_copy("fsm_persist_page.h5", "fsm_write_paged");
assert!(
read_only_append(&path).free_space.is_some(),
"the paged fixture's managers were not read"
);
append_one(&path, "added", false);
let mut w = read_only_append(&path);
let info = w
.free_space
.as_deref()
.expect("the file persists managers")
.info
.clone();
assert_eq!(info.strategy, FileSpaceStrategy::Page);
for (slot, &addr) in info.fs_addr.iter().enumerate() {
if !matches!(slot, 0 | 2 | 6) {
assert_eq!(addr, UNDEF_ADDR, "slot {slot} names a manager");
}
}
assert!(
info.fs_addr.iter().any(|&a| a != UNDEF_ADDR),
"the rewritten file records nothing free"
);
crate::io::free_space_io::read_managers(&mut w.handle, &w.ctx, &info).unwrap();
drop(w);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_paged_files_small_sections_stay_inside_one_page_of_one_kind() {
let path = fixture_copy("fsm_persist_page.h5", "fsm_paged_pages");
append_one(&path, "added", false);
let mut w = read_only_append(&path);
let info = w
.free_space
.as_deref()
.expect("the file persists managers")
.info
.clone();
let page = info.page_size;
let found = crate::io::free_space_io::read_managers(&mut w.handle, &w.ctx, &info).unwrap();
let mut kind_of_page: std::collections::HashMap<u64, FreeSpaceManager> =
std::collections::HashMap::new();
for section in &found.sections {
if section.manager == FreeSpaceManager::Large {
continue;
}
assert_eq!(
section.addr / page,
(section.addr + section.len - 1) / page,
"the section at {:#x} crosses a page boundary",
section.addr
);
let owner = kind_of_page
.entry(section.addr / page)
.or_insert(section.manager);
assert_eq!(
*owner,
section.manager,
"page {} holds sections of two kinds",
section.addr / page
);
}
drop(w);
let _ = std::fs::remove_file(&path);
}
}