Merge branch 'feat/p2b-writer-btree-internal-nodes' into feat/p2b-scale
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -48,6 +48,47 @@
|
|||||||
with partial edge chunks, sparse datasets and fill values, and datasets
|
with partial edge chunks, sparse datasets and fill values, and datasets
|
||||||
larger than the chunk cache.
|
larger than the chunk cache.
|
||||||
|
|
||||||
|
### Writer: large dense indexes (2026-09-26)
|
||||||
|
- **`track_order` orders attributes too, as h5py's `track_order=True`
|
||||||
|
does.** It tracked link creation order only, so h5py listed a tracked
|
||||||
|
object's attributes by name. A tracking object's header now has the
|
||||||
|
attribute creation order tracked and indexed flags, an Attribute Info
|
||||||
|
message with the next creation order (also for inline attributes), and a
|
||||||
|
creation order on each inline attribute message; dense attribute storage
|
||||||
|
gets a creation-order index (B-tree type 9). `FileWriter::track_order` /
|
||||||
|
`FileBuilder::track_order` now apply to datasets' attributes as well, and
|
||||||
|
`DatasetBuilder::track_order` sets it per dataset. Groups and datasets
|
||||||
|
that track order are written differently from before; others are
|
||||||
|
unchanged. More than 65 535 attributes on a tracking object is an error
|
||||||
|
(libhdf5's creation order counter is 2 bytes). The reader
|
||||||
|
(`attribute::extract_attributes*`) lists a tracking object's attributes
|
||||||
|
in creation order. Test `track_order_lists_attributes_in_creation_order`
|
||||||
|
(h5py lists, reads and extends them in "r+" mode, including libhdf5's
|
||||||
|
move from inline to dense storage).
|
||||||
|
- **No more 65 535-record limit on the writer's v2 B-trees.** Dense link
|
||||||
|
storage (name index and creation-order index), dense attribute storage
|
||||||
|
and the chunk index of datasets with more than one unlimited dimension
|
||||||
|
were written as a single leaf, so a group with more than 65 535 links, an
|
||||||
|
object with more than 65 535 dense attributes, or such a dataset with more
|
||||||
|
than 65 535 chunks was an error. The writer now builds internal nodes to
|
||||||
|
any depth (`clawhdf5_format::btree_v2_write`), with node capacities and
|
||||||
|
child-pointer widths from the same arithmetic as libhdf5's
|
||||||
|
`H5B2__hdr_init` (shared with the reader, `btree_v2::node_info`) and
|
||||||
|
libhdf5's node sizes (512 bytes for dense storage, 2048 for chunks).
|
||||||
|
Indexes that fit the old one-leaf layout are written byte for byte as
|
||||||
|
before. New tests `crates/clawhdf5/tests/deep_btree_interop.rs` (100 000
|
||||||
|
links, 70 000 attributes, 200 000 chunks; h5py, h5dump, clawhdf5, and
|
||||||
|
h5py "r+" edits) and `check_files_with_deep_btrees` (`h5rs check`).
|
||||||
|
- **Links and attributes whose name hashes collide are found by name.** The
|
||||||
|
dense name indexes (a group's links: B-tree v2 type 5; an object's
|
||||||
|
attributes: type 8) are ordered by the name's lookup3 hash and, when two
|
||||||
|
hashes are equal, by the name itself, as libhdf5 compares them. The writer
|
||||||
|
broke ties by insertion order, so libhdf5 could not open one of two
|
||||||
|
colliding names (`"k69209"` and `"k155448"` share hash `0x3a0b13e6`;
|
||||||
|
collisions are likely from about 77 000 names). Regression test
|
||||||
|
`names_whose_hashes_collide_are_found_by_name` in
|
||||||
|
`crates/clawhdf5/tests/writer_groups_interop.rs`.
|
||||||
|
|
||||||
### Concurrent reads (2026-09-26)
|
### Concurrent reads (2026-09-26)
|
||||||
- **Full reads of chunked datasets scale with threads again when rayon's
|
- **Full reads of chunked datasets scale with threads again when rayon's
|
||||||
pool has one thread.** Each full read handed its chunks to rayon to
|
pool has one thread.** Each full read handed its chunks to rayon to
|
||||||
|
|||||||
@@ -450,6 +450,8 @@ fn extract_attributes_with(
|
|||||||
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
||||||
) -> Result<Vec<AttributeMessage>, FormatError> {
|
) -> Result<Vec<AttributeMessage>, FormatError> {
|
||||||
let mut attrs = Vec::new();
|
let mut attrs = Vec::new();
|
||||||
|
// Each attribute's creation order, where the file records one.
|
||||||
|
let mut orders: Vec<u32> = Vec::new();
|
||||||
|
|
||||||
// Collect compact attributes (inline in OH)
|
// Collect compact attributes (inline in OH)
|
||||||
for msg in &header.messages {
|
for msg in &header.messages {
|
||||||
@@ -479,7 +481,10 @@ fn extract_attributes_with(
|
|||||||
};
|
};
|
||||||
let attr = attr.and_then(|a| check_in_header(a, header));
|
let attr = attr.and_then(|a| check_in_header(a, header));
|
||||||
match attr {
|
match attr {
|
||||||
Ok(attr) => attrs.push(attr),
|
Ok(attr) => {
|
||||||
|
attrs.push(attr);
|
||||||
|
orders.push(msg.creation_order.map_or(0, u32::from));
|
||||||
|
}
|
||||||
Err(e) => on_error(e)?,
|
Err(e) => on_error(e)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,20 +492,30 @@ fn extract_attributes_with(
|
|||||||
|
|
||||||
// Check for dense attributes via AttributeInfo message
|
// Check for dense attributes via AttributeInfo message
|
||||||
let attr_info = find_attribute_info(header, offset_size)?;
|
let attr_info = find_attribute_info(header, offset_size)?;
|
||||||
if let Some(info) = attr_info
|
if let Some(info) = &attr_info
|
||||||
&& let Some(fh_addr) = info.fractal_heap_address
|
&& let Some(fh_addr) = info.fractal_heap_address
|
||||||
{
|
{
|
||||||
extract_dense_attributes(
|
extract_dense_attributes(
|
||||||
file_data,
|
file_data,
|
||||||
&info,
|
info,
|
||||||
fh_addr,
|
fh_addr,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
&mut attrs,
|
&mut attrs,
|
||||||
|
&mut orders,
|
||||||
on_error,
|
on_error,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An object that tracks attribute creation order lists its attributes
|
||||||
|
// in that order (h5py's `track_order=True`), as libhdf5 does; otherwise
|
||||||
|
// they come in storage order.
|
||||||
|
if attr_info.is_some_and(|i| i.max_creation_index.is_some()) {
|
||||||
|
let mut paired: Vec<(u32, AttributeMessage)> = orders.into_iter().zip(attrs).collect();
|
||||||
|
paired.sort_by_key(|(o, _)| *o);
|
||||||
|
attrs = paired.into_iter().map(|(_, a)| a).collect();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(attrs)
|
Ok(attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +533,9 @@ fn find_attribute_info(
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract attributes from dense storage (fractal heap + B-tree v2).
|
/// Extract attributes from dense storage (fractal heap + B-tree v2), and
|
||||||
|
/// each one's creation order into `orders`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn extract_dense_attributes(
|
fn extract_dense_attributes(
|
||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
attr_info: &AttributeInfoMessage,
|
attr_info: &AttributeInfoMessage,
|
||||||
@@ -526,6 +543,7 @@ fn extract_dense_attributes(
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
attrs: &mut Vec<AttributeMessage>,
|
attrs: &mut Vec<AttributeMessage>,
|
||||||
|
orders: &mut Vec<u32>,
|
||||||
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
// Parse fractal heap
|
// Parse fractal heap
|
||||||
@@ -561,7 +579,14 @@ fn extract_dense_attributes(
|
|||||||
AttributeMessage::parse_in_file(&attr_data, file_data, offset_size, length_size)
|
AttributeMessage::parse_in_file(&attr_data, file_data, offset_size, length_size)
|
||||||
});
|
});
|
||||||
match attr {
|
match attr {
|
||||||
Ok(attr) => attrs.push(attr),
|
Ok(attr) => {
|
||||||
|
attrs.push(attr);
|
||||||
|
let order = record
|
||||||
|
.data
|
||||||
|
.get(id_len + 1..id_len + 5)
|
||||||
|
.map_or(0, |b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||||
|
orders.push(order);
|
||||||
|
}
|
||||||
Err(e) => on_error(e)?,
|
Err(e) => on_error(e)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fn ensure_len(data: &[u8], pos: usize, needed: usize) -> Result<(), FormatError>
|
|||||||
|
|
||||||
/// Compute the number of bytes needed to represent a count, using variable-width encoding.
|
/// Compute the number of bytes needed to represent a count, using variable-width encoding.
|
||||||
/// B-tree v2 uses this for the number of records fields in internal nodes.
|
/// B-tree v2 uses this for the number of records fields in internal nodes.
|
||||||
fn bytes_for_max_records(max_nrec: u64) -> usize {
|
pub(crate) fn bytes_for_max_records(max_nrec: u64) -> usize {
|
||||||
if max_nrec == 0 {
|
if max_nrec == 0 {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ impl BTreeV2Header {
|
|||||||
/// Compute maximum records per node for a given depth level.
|
/// Compute maximum records per node for a given depth level.
|
||||||
/// leaf: (node_size - overhead) / record_size
|
/// leaf: (node_size - overhead) / record_size
|
||||||
/// internal: depends on pointers
|
/// internal: depends on pointers
|
||||||
fn max_records_leaf(node_size: u32, record_size: u16) -> u64 {
|
pub(crate) fn max_records_leaf(node_size: u32, record_size: u16) -> u64 {
|
||||||
// Leaf overhead: signature(4) + version(1) + type(1) + checksum(4) = 10
|
// Leaf overhead: signature(4) + version(1) + type(1) + checksum(4) = 10
|
||||||
let overhead = 10u32;
|
let overhead = 10u32;
|
||||||
if node_size <= overhead || record_size == 0 {
|
if node_size <= overhead || record_size == 0 {
|
||||||
@@ -418,10 +418,7 @@ fn collect_internal_records(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Most records a subtree whose root is at `depth` can hold (libhdf5's
|
/// Most records a subtree whose root is at `depth` can hold (libhdf5's
|
||||||
/// `cum_max_nrec`): a leaf holds `max_leaf_nrec`; an internal node at depth
|
/// `cum_max_nrec`). See [`node_info`].
|
||||||
/// `d` holds `max_nrec(d)` records and `max_nrec(d) + 1` subtrees of depth
|
|
||||||
/// `d - 1`, where `max_nrec(d)` is what fits in a node once each record is
|
|
||||||
/// paired with a child pointer of the width depth `d` needs.
|
|
||||||
fn cum_max_records(
|
fn cum_max_records(
|
||||||
node_size: u32,
|
node_size: u32,
|
||||||
record_size: u16,
|
record_size: u16,
|
||||||
@@ -429,24 +426,82 @@ fn cum_max_records(
|
|||||||
max_leaf_nrec: u64,
|
max_leaf_nrec: u64,
|
||||||
depth: u16,
|
depth: u16,
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
|
node_info_from_leaf(node_size, record_size, offset_size, max_leaf_nrec, depth)
|
||||||
|
.last()
|
||||||
|
.map_or(max_leaf_nrec, |n| n.cum_max_nrec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capacity of a B-tree v2 node at one depth, as libhdf5 computes it
|
||||||
|
/// (`H5B2__hdr_init`'s `node_info`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct NodeInfo {
|
||||||
|
/// Most records one node at this depth holds.
|
||||||
|
pub(crate) max_nrec: u64,
|
||||||
|
/// Most records a subtree rooted at this depth holds.
|
||||||
|
pub(crate) cum_max_nrec: u64,
|
||||||
|
/// Bytes a subtree's total record count takes in a pointer to a node
|
||||||
|
/// at this depth (0 for a leaf, whose count is its own).
|
||||||
|
pub(crate) cum_max_nrec_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Node capacities for depths `0..=depth` (entry `d` for depth `d`): a leaf
|
||||||
|
/// holds `max_nrec(0)` records; an internal node at depth `d` holds
|
||||||
|
/// `max_nrec(d)` records and `max_nrec(d) + 1` subtrees of depth `d - 1`,
|
||||||
|
/// where `max_nrec(d)` is what fits in a node once each record is paired
|
||||||
|
/// with a child pointer of the width depth `d` needs (address, the child's
|
||||||
|
/// record count in the width a *leaf's* maximum needs, and below the first
|
||||||
|
/// internal level the child subtree's total in the width its maximum
|
||||||
|
/// needs), with one pointer more than records.
|
||||||
|
pub(crate) fn node_info(
|
||||||
|
node_size: u32,
|
||||||
|
record_size: u16,
|
||||||
|
offset_size: u8,
|
||||||
|
depth: u16,
|
||||||
|
) -> Vec<NodeInfo> {
|
||||||
|
let max_leaf = max_records_leaf(node_size, record_size);
|
||||||
|
node_info_from_leaf(node_size, record_size, offset_size, max_leaf, depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_info_from_leaf(
|
||||||
|
node_size: u32,
|
||||||
|
record_size: u16,
|
||||||
|
offset_size: u8,
|
||||||
|
max_leaf_nrec: u64,
|
||||||
|
depth: u16,
|
||||||
|
) -> Vec<NodeInfo> {
|
||||||
// Internal node overhead: signature(4) + version(1) + type(1) + checksum(4).
|
// Internal node overhead: signature(4) + version(1) + type(1) + checksum(4).
|
||||||
const PREFIX: u64 = 10;
|
const PREFIX: u64 = 10;
|
||||||
let nrec_width = bytes_for_max_records(max_leaf_nrec) as u64;
|
let nrec_width = bytes_for_max_records(max_leaf_nrec) as u64;
|
||||||
let mut cum = max_leaf_nrec;
|
let mut info = Vec::with_capacity(usize::from(depth) + 1);
|
||||||
let mut cum_width = 0u64;
|
info.push(NodeInfo {
|
||||||
|
max_nrec: max_leaf_nrec,
|
||||||
|
cum_max_nrec: max_leaf_nrec,
|
||||||
|
cum_max_nrec_size: 0,
|
||||||
|
});
|
||||||
for d in 1..=depth {
|
for d in 1..=depth {
|
||||||
let ptr = u64::from(offset_size) + nrec_width + if d > 1 { cum_width } else { 0 };
|
let below = info[usize::from(d) - 1];
|
||||||
|
let ptr = u64::from(offset_size)
|
||||||
|
+ nrec_width
|
||||||
|
+ if d > 1 {
|
||||||
|
below.cum_max_nrec_size as u64
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
let max_nrec = u64::from(node_size)
|
let max_nrec = u64::from(node_size)
|
||||||
.saturating_sub(PREFIX)
|
.saturating_sub(PREFIX)
|
||||||
.saturating_sub(ptr)
|
.saturating_sub(ptr)
|
||||||
/ (u64::from(record_size) + ptr).max(1);
|
/ (u64::from(record_size) + ptr).max(1);
|
||||||
cum = max_nrec
|
let cum = max_nrec
|
||||||
.saturating_add(1)
|
.saturating_add(1)
|
||||||
.saturating_mul(cum)
|
.saturating_mul(below.cum_max_nrec)
|
||||||
.saturating_add(max_nrec);
|
.saturating_add(max_nrec);
|
||||||
cum_width = bytes_for_max_records(cum) as u64;
|
info.push(NodeInfo {
|
||||||
|
max_nrec,
|
||||||
|
cum_max_nrec: cum,
|
||||||
|
cum_max_nrec_size: bytes_for_max_records(cum),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cum
|
info
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
//! Writing version-2 B-trees: a header (`BTHD`) and its nodes, leaves
|
||||||
|
//! (`BTLF`) and, for more records than one leaf holds, internal nodes
|
||||||
|
//! (`BTIN`) to any depth.
|
||||||
|
//!
|
||||||
|
//! Node capacities come from [`crate::btree_v2::node_info`], the arithmetic
|
||||||
|
//! libhdf5 uses (`H5B2__hdr_init`) and the reader decodes pointers with, so
|
||||||
|
//! the pointer widths the writer encodes are the ones every reader expects.
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
|
use crate::btree_v2::{NodeInfo, bytes_for_max_records, node_info};
|
||||||
|
use crate::checksum::jenkins_lookup3;
|
||||||
|
use crate::error::FormatError;
|
||||||
|
|
||||||
|
/// How a B-tree is laid out: its record type and node geometry, as the
|
||||||
|
/// header records them.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct BTreeV2Params {
|
||||||
|
/// Record type (5: link names, 6: link creation order, 8: attribute
|
||||||
|
/// names, 9: attribute creation order, 10/11: chunks).
|
||||||
|
pub(crate) tree_type: u8,
|
||||||
|
/// Bytes per node.
|
||||||
|
pub(crate) node_size: u32,
|
||||||
|
/// Bytes per record.
|
||||||
|
pub(crate) record_size: u16,
|
||||||
|
/// Split and merge percentages. The writer fills nodes itself; these
|
||||||
|
/// only tell libhdf5 when to split and merge as it modifies the tree.
|
||||||
|
pub(crate) split_percent: u8,
|
||||||
|
pub(crate) merge_percent: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of a B-tree v2 header.
|
||||||
|
pub(crate) fn header_size(offset_size: u8, length_size: u8) -> usize {
|
||||||
|
4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + offset_size as usize + 2 + length_size as usize + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deepest tree the writer builds. Even at the smallest fan-out libhdf5's
|
||||||
|
/// arithmetic allows, a few levels hold more records than any file could.
|
||||||
|
const MAX_WRITE_DEPTH: u16 = 32;
|
||||||
|
|
||||||
|
/// Write a B-tree v2 holding `records` (`record_size` bytes each,
|
||||||
|
/// concatenated, already in the tree's key order) at `addr`: the header,
|
||||||
|
/// then its nodes, each `node_size` bytes. No records gives a header with
|
||||||
|
/// an undefined root.
|
||||||
|
///
|
||||||
|
/// The tree is as shallow as the node size allows: a single leaf when the
|
||||||
|
/// records fit one, otherwise internal nodes above leaves. Records are
|
||||||
|
/// spread evenly over each node's children, so every node but the root is
|
||||||
|
/// at least about half full (above libhdf5's merge threshold, which is below
|
||||||
|
/// half), and each node holds at most its depth's maximum.
|
||||||
|
pub(crate) fn build_btree_v2(
|
||||||
|
p: BTreeV2Params,
|
||||||
|
records: &[u8],
|
||||||
|
addr: u64,
|
||||||
|
offset_size: u8,
|
||||||
|
length_size: u8,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
let rs = usize::from(p.record_size);
|
||||||
|
if rs == 0 || !records.len().is_multiple_of(rs) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"B-tree v2 records are {} bytes, not a multiple of the record size {rs}",
|
||||||
|
records.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let n = (records.len() / rs) as u64;
|
||||||
|
let hdr_len = header_size(offset_size, length_size);
|
||||||
|
|
||||||
|
// The shallowest depth whose subtree can hold every record.
|
||||||
|
let mut info = node_info(p.node_size, p.record_size, offset_size, 0);
|
||||||
|
let max_leaf = info[0].max_nrec;
|
||||||
|
if max_leaf == 0 || max_leaf > u64::from(u16::MAX) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"a {}-byte B-tree v2 node holds {max_leaf} {}-byte records; \
|
||||||
|
a node holds 1 to 65535",
|
||||||
|
p.node_size, p.record_size
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mut depth = 0u16;
|
||||||
|
while info[usize::from(depth)].cum_max_nrec < n {
|
||||||
|
depth += 1;
|
||||||
|
if depth > MAX_WRITE_DEPTH {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"{n} records do not fit a B-tree v2 of {}-byte nodes",
|
||||||
|
p.node_size
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
info = node_info(p.node_size, p.record_size, offset_size, depth);
|
||||||
|
let max = info[usize::from(depth)].max_nrec;
|
||||||
|
if max == 0 || max > u64::from(u16::MAX) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"a {}-byte B-tree v2 internal node holds {max} records; \
|
||||||
|
a node holds 1 to 65535",
|
||||||
|
p.node_size
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut w = TreeWriter {
|
||||||
|
p,
|
||||||
|
records,
|
||||||
|
info: &info,
|
||||||
|
nrec_width: bytes_for_max_records(max_leaf),
|
||||||
|
offset_size,
|
||||||
|
first_node: addr + hdr_len as u64,
|
||||||
|
nodes: Vec::new(),
|
||||||
|
};
|
||||||
|
let root = (n > 0).then(|| w.node(depth, 0, n as usize)).transpose()?;
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(hdr_len + w.nodes.len() * p.node_size as usize);
|
||||||
|
out.extend_from_slice(b"BTHD");
|
||||||
|
out.push(0); // version
|
||||||
|
out.push(p.tree_type);
|
||||||
|
out.extend_from_slice(&p.node_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&p.record_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&depth.to_le_bytes());
|
||||||
|
out.push(p.split_percent);
|
||||||
|
out.push(p.merge_percent);
|
||||||
|
match root {
|
||||||
|
Some(r) => push_uint(&mut out, r.addr, offset_size as usize),
|
||||||
|
None => out.extend(core::iter::repeat_n(0xFF, offset_size as usize)),
|
||||||
|
}
|
||||||
|
let root_nrec = root.map_or(0, |r| r.nrec);
|
||||||
|
out.extend_from_slice(&(root_nrec as u16).to_le_bytes());
|
||||||
|
push_uint(&mut out, n, length_size as usize);
|
||||||
|
let sum = jenkins_lookup3(&out);
|
||||||
|
out.extend_from_slice(&sum.to_le_bytes());
|
||||||
|
debug_assert_eq!(out.len(), hdr_len);
|
||||||
|
for node in &w.nodes {
|
||||||
|
out.extend_from_slice(node);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A written node, as its parent points at it.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct NodeRef {
|
||||||
|
addr: u64,
|
||||||
|
/// Records in the node itself.
|
||||||
|
nrec: u64,
|
||||||
|
/// Records in the subtree it roots.
|
||||||
|
all_nrec: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TreeWriter<'a> {
|
||||||
|
p: BTreeV2Params,
|
||||||
|
records: &'a [u8],
|
||||||
|
info: &'a [NodeInfo],
|
||||||
|
/// Width of a child's record count: what a leaf's maximum needs.
|
||||||
|
nrec_width: usize,
|
||||||
|
offset_size: u8,
|
||||||
|
/// Address of the first node (right after the header).
|
||||||
|
first_node: u64,
|
||||||
|
/// Nodes in file order (children before their parent).
|
||||||
|
nodes: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeWriter<'_> {
|
||||||
|
fn record(&self, i: usize) -> &[u8] {
|
||||||
|
let rs = usize::from(self.p.record_size);
|
||||||
|
&self.records[i * rs..(i + 1) * rs]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_node(&mut self, mut node: Vec<u8>) -> u64 {
|
||||||
|
// The checksum covers the node up to it, not the padding after.
|
||||||
|
let sum = jenkins_lookup3(&node);
|
||||||
|
node.extend_from_slice(&sum.to_le_bytes());
|
||||||
|
debug_assert!(node.len() <= self.p.node_size as usize);
|
||||||
|
node.resize(self.p.node_size as usize, 0);
|
||||||
|
let addr = self.first_node + self.nodes.len() as u64 * u64::from(self.p.node_size);
|
||||||
|
self.nodes.push(node);
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the subtree of `depth` holding records `first..first + n`.
|
||||||
|
fn node(&mut self, depth: u16, first: usize, n: usize) -> Result<NodeRef, FormatError> {
|
||||||
|
let rs = usize::from(self.p.record_size);
|
||||||
|
let mut node = Vec::with_capacity(self.p.node_size as usize);
|
||||||
|
if depth == 0 {
|
||||||
|
debug_assert!(n as u64 <= self.info[0].max_nrec);
|
||||||
|
node.extend_from_slice(b"BTLF");
|
||||||
|
node.push(0); // version
|
||||||
|
node.push(self.p.tree_type);
|
||||||
|
node.extend_from_slice(&self.records[first * rs..(first + n) * rs]);
|
||||||
|
let addr = self.push_node(node);
|
||||||
|
return Ok(NodeRef {
|
||||||
|
addr,
|
||||||
|
nrec: n as u64,
|
||||||
|
all_nrec: n as u64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// As few children as hold the records, at least two, with the
|
||||||
|
// records spread evenly: `k` children and `k - 1` records between
|
||||||
|
// them.
|
||||||
|
let below = self.info[usize::from(depth) - 1].cum_max_nrec;
|
||||||
|
let k = (n as u64 + 1).div_ceil(below + 1).max(2);
|
||||||
|
let max = self.info[usize::from(depth)].max_nrec;
|
||||||
|
if k - 1 > max || (n as u64) < k - 1 + k {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"cannot spread {n} B-tree v2 records over {k} children at depth {depth}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let k = k as usize;
|
||||||
|
let in_children = n - (k - 1);
|
||||||
|
let (base, extra) = (in_children / k, in_children % k);
|
||||||
|
|
||||||
|
let mut children = Vec::with_capacity(k);
|
||||||
|
let mut separators = Vec::with_capacity(k - 1);
|
||||||
|
let mut next = first;
|
||||||
|
for c in 0..k {
|
||||||
|
let m = base + usize::from(c < extra);
|
||||||
|
children.push(self.node(depth - 1, next, m)?);
|
||||||
|
next += m;
|
||||||
|
if c + 1 < k {
|
||||||
|
separators.push(next);
|
||||||
|
next += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug_assert_eq!(next, first + n);
|
||||||
|
|
||||||
|
node.extend_from_slice(b"BTIN");
|
||||||
|
node.push(0); // version
|
||||||
|
node.push(self.p.tree_type);
|
||||||
|
for &s in &separators {
|
||||||
|
node.extend_from_slice(self.record(s));
|
||||||
|
}
|
||||||
|
let total_width = if depth > 1 {
|
||||||
|
self.info[usize::from(depth) - 1].cum_max_nrec_size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
for c in &children {
|
||||||
|
push_uint(&mut node, c.addr, self.offset_size as usize);
|
||||||
|
push_uint(&mut node, c.nrec, self.nrec_width);
|
||||||
|
if depth > 1 {
|
||||||
|
push_uint(&mut node, c.all_nrec, total_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let addr = self.push_node(node);
|
||||||
|
Ok(NodeRef {
|
||||||
|
addr,
|
||||||
|
nrec: (k - 1) as u64,
|
||||||
|
all_nrec: n as u64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append `v` as a `width`-byte little-endian integer.
|
||||||
|
fn push_uint(buf: &mut Vec<u8>, v: u64, width: usize) {
|
||||||
|
let bytes = v.to_le_bytes();
|
||||||
|
buf.extend_from_slice(&bytes[..width.min(8)]);
|
||||||
|
buf.extend(vec![0u8; width.saturating_sub(8)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
|
||||||
|
|
||||||
|
fn params(node_size: u32, record_size: u16) -> BTreeV2Params {
|
||||||
|
BTreeV2Params {
|
||||||
|
tree_type: 5,
|
||||||
|
node_size,
|
||||||
|
record_size,
|
||||||
|
split_percent: 100,
|
||||||
|
merge_percent: 40,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `n` 11-byte records: a big-endian counter, so byte order is key order.
|
||||||
|
fn records(n: usize, rs: usize) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(n * rs);
|
||||||
|
for i in 0..n {
|
||||||
|
let mut r = vec![0u8; rs];
|
||||||
|
r[..8].copy_from_slice(&(i as u64).to_be_bytes());
|
||||||
|
out.extend_from_slice(&r);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn roundtrip(node_size: u32, rs: u16, n: usize, os: u8, ls: u8) -> BTreeV2Header {
|
||||||
|
let recs = records(n, usize::from(rs));
|
||||||
|
let base = 4096u64;
|
||||||
|
let tree = build_btree_v2(params(node_size, rs), &recs, base, os, ls).unwrap();
|
||||||
|
let mut file = vec![0u8; base as usize];
|
||||||
|
file.extend_from_slice(&tree);
|
||||||
|
let hdr = BTreeV2Header::parse(&file, base as usize, os, ls).unwrap();
|
||||||
|
assert_eq!(hdr.total_records, n as u64);
|
||||||
|
let got = collect_btree_v2_records(&file, &hdr, os, ls).unwrap();
|
||||||
|
assert_eq!(got.len(), n);
|
||||||
|
let flat: Vec<u8> = got.into_iter().flat_map(|r| r.data).collect();
|
||||||
|
assert_eq!(flat, recs, "node {node_size} rs {rs} n {n}");
|
||||||
|
hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_leaf_then_deeper_trees_read_back_in_order() {
|
||||||
|
// 512-byte nodes of 11-byte records: 45 per leaf, 1149 at depth 1,
|
||||||
|
// 26 449 at depth 2.
|
||||||
|
let info = node_info(512, 11, 8, 3);
|
||||||
|
assert_eq!(
|
||||||
|
info.iter().map(|i| i.cum_max_nrec).collect::<Vec<_>>(),
|
||||||
|
[45, 1149, 26_449, 608_349]
|
||||||
|
);
|
||||||
|
for (n, depth) in [
|
||||||
|
(0, 0),
|
||||||
|
(1, 0),
|
||||||
|
(45, 0),
|
||||||
|
(46, 1),
|
||||||
|
(1149, 1),
|
||||||
|
(1150, 2),
|
||||||
|
(26_449, 2),
|
||||||
|
(26_450, 3),
|
||||||
|
(100_000, 3),
|
||||||
|
] {
|
||||||
|
let hdr = roundtrip(512, 11, n, 8, 8);
|
||||||
|
assert_eq!(hdr.depth, depth, "{n} records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_widths_follow_the_offset_and_length_sizes() {
|
||||||
|
for (os, ls) in [(4, 4), (8, 4), (4, 8), (2, 2)] {
|
||||||
|
roundtrip(512, 11, 5000, os, ls);
|
||||||
|
}
|
||||||
|
// Wide counts: a leaf of 2048 bytes / 9-byte records (226, one byte)
|
||||||
|
// and deeper subtree totals of three bytes.
|
||||||
|
roundtrip(2048, 9, 300_000, 8, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_node_is_within_its_capacity_and_above_the_merge_threshold() {
|
||||||
|
let rs = 17u16;
|
||||||
|
let n = 70_000usize;
|
||||||
|
let info = node_info(512, rs, 8, 3);
|
||||||
|
let recs = records(n, usize::from(rs));
|
||||||
|
let tree = build_btree_v2(params(512, rs), &recs, 0, 8, 8).unwrap();
|
||||||
|
let hdr_len = header_size(8, 8);
|
||||||
|
let nodes = (tree.len() - hdr_len) / 512;
|
||||||
|
for i in 0..nodes {
|
||||||
|
let node = &tree[hdr_len + i * 512..hdr_len + (i + 1) * 512];
|
||||||
|
let sig = &node[..4];
|
||||||
|
if sig == b"BTLF" {
|
||||||
|
continue; // counts checked through the parents below
|
||||||
|
}
|
||||||
|
assert_eq!(sig, b"BTIN");
|
||||||
|
}
|
||||||
|
// Walk from the header: each child's count within [40%, 100%].
|
||||||
|
let hdr = BTreeV2Header::parse(&tree, 0, 8, 8).unwrap();
|
||||||
|
assert_eq!(hdr.depth, 3);
|
||||||
|
assert!(u64::from(hdr.num_records_in_root) <= info[3].max_nrec);
|
||||||
|
fn walk(tree: &[u8], addr: usize, nrec: usize, depth: usize, info: &[NodeInfo], rs: usize) {
|
||||||
|
if depth == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let nrec_w = bytes_for_max_records(info[0].max_nrec);
|
||||||
|
let tot_w = if depth > 1 {
|
||||||
|
info[depth - 1].cum_max_nrec_size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let mut pos = addr + 6 + nrec * rs;
|
||||||
|
for _ in 0..=nrec {
|
||||||
|
let a = u64::from_le_bytes(tree[pos..pos + 8].try_into().unwrap()) as usize;
|
||||||
|
pos += 8;
|
||||||
|
let mut c = 0usize;
|
||||||
|
for b in 0..nrec_w {
|
||||||
|
c |= usize::from(tree[pos + b]) << (8 * b);
|
||||||
|
}
|
||||||
|
pos += nrec_w + tot_w;
|
||||||
|
let max = info[depth - 1].max_nrec as usize;
|
||||||
|
assert!(c <= max && c * 100 > max * 40, "{c} of {max}");
|
||||||
|
walk(tree, a, c, depth - 1, info, rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(
|
||||||
|
&tree,
|
||||||
|
hdr.root_node_address as usize,
|
||||||
|
usize::from(hdr.num_records_in_root),
|
||||||
|
3,
|
||||||
|
&info,
|
||||||
|
usize::from(rs),
|
||||||
|
);
|
||||||
|
assert!(nodes > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_node_too_small_or_too_big_is_an_error() {
|
||||||
|
assert!(build_btree_v2(params(16, 11), &records(1, 11), 0, 8, 8).is_err());
|
||||||
|
// A leaf with room for more than 65 535 records.
|
||||||
|
assert!(build_btree_v2(params(1 << 20, 11), &records(1, 11), 0, 8, 8).is_err());
|
||||||
|
// Records that are not whole.
|
||||||
|
assert!(build_btree_v2(params(512, 11), &[0u8; 12], 0, 8, 8).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ extern crate alloc;
|
|||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
|
use crate::btree_v2_write::{BTreeV2Params, build_btree_v2};
|
||||||
use crate::checksum::jenkins_lookup3;
|
use crate::checksum::jenkins_lookup3;
|
||||||
use crate::chunk_cache::{CACHE_LINE_SIZE, align_to_cache_line};
|
use crate::chunk_cache::{CACHE_LINE_SIZE, align_to_cache_line};
|
||||||
use crate::chunk_grid::ChunkGrid;
|
use crate::chunk_grid::ChunkGrid;
|
||||||
@@ -1105,11 +1106,12 @@ const BT2_CHUNK_FILTERED: u8 = 11;
|
|||||||
///
|
///
|
||||||
/// `records` are `(scaled coordinates, chunk)` in lexicographic order of the
|
/// `records` are `(scaled coordinates, chunk)` in lexicographic order of the
|
||||||
/// coordinates, which is the order the library's comparator
|
/// coordinates, which is the order the library's comparator
|
||||||
/// (`H5VM_vector_cmp_u`) keeps them in. The tree is a single leaf: the
|
/// (`H5VM_vector_cmp_u`) keeps them in. Up to 65 535 chunks go in a single
|
||||||
/// library's 2048-byte node when the records fit, otherwise a leaf node
|
/// leaf: the library's 2048-byte node when the records fit, otherwise a leaf
|
||||||
/// sized to hold them all (the root's record count is 16-bit, so at most
|
/// node sized to hold them all (the layout the writer has always used, kept
|
||||||
/// 65535 chunks). Returns the bytes and the node size the layout message
|
/// so those files do not change). More chunks get the library's 2048-byte
|
||||||
/// must record.
|
/// nodes with internal nodes above the leaves. Returns the bytes and the
|
||||||
|
/// node size the layout message must record.
|
||||||
fn build_btree_v2_chunk_index_at(
|
fn build_btree_v2_chunk_index_at(
|
||||||
rank: usize,
|
rank: usize,
|
||||||
records: &[(Vec<u64>, &WrittenChunk)],
|
records: &[(Vec<u64>, &WrittenChunk)],
|
||||||
@@ -1119,73 +1121,49 @@ fn build_btree_v2_chunk_index_at(
|
|||||||
base_address: u64,
|
base_address: u64,
|
||||||
) -> Result<(Vec<u8>, u32), FormatError> {
|
) -> Result<(Vec<u8>, u32), FormatError> {
|
||||||
let os = offset_size as usize;
|
let os = offset_size as usize;
|
||||||
let nrec = u16::try_from(records.len()).map_err(|_| {
|
|
||||||
FormatError::ChunkedReadError(
|
|
||||||
"more than 65535 chunks with more than one unlimited dimension: \
|
|
||||||
use larger chunks"
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let chunk_size_bytes = has_filters.then(|| {
|
let chunk_size_bytes = has_filters.then(|| {
|
||||||
let slots: Vec<Option<WrittenChunk>> =
|
let slots: Vec<Option<WrittenChunk>> =
|
||||||
records.iter().map(|(_, c)| Some((*c).clone())).collect();
|
records.iter().map(|(_, c)| Some((*c).clone())).collect();
|
||||||
filtered_chunk_size_len(&slots)
|
filtered_chunk_size_len(&slots)
|
||||||
});
|
});
|
||||||
let record_size = os + chunk_size_bytes.map_or(0, |n| n + 4) + 8 * rank;
|
let record_size = os + chunk_size_bytes.map_or(0, |n| n + 4) + 8 * rank;
|
||||||
// Leaf: signature, version, type, records, checksum.
|
let record_size_u16 = u16::try_from(record_size)
|
||||||
let leaf_len = 4 + 1 + 1 + records.len() * record_size + 4;
|
.map_err(|_| FormatError::Overflow("B-tree v2 record size".into()))?;
|
||||||
let node_size = u32::try_from(leaf_len)
|
let node_size = if records.len() <= usize::from(u16::MAX) {
|
||||||
.map_err(|_| FormatError::Overflow("B-tree v2 leaf size".into()))?
|
// Leaf: signature, version, type, records, checksum.
|
||||||
.max(BT2_NODE_SIZE);
|
let leaf_len = 4 + 1 + 1 + records.len() * record_size + 4;
|
||||||
|
u32::try_from(leaf_len)
|
||||||
|
.map_err(|_| FormatError::Overflow("B-tree v2 leaf size".into()))?
|
||||||
|
.max(BT2_NODE_SIZE)
|
||||||
|
} else {
|
||||||
|
BT2_NODE_SIZE
|
||||||
|
};
|
||||||
let tree_type = if has_filters {
|
let tree_type = if has_filters {
|
||||||
BT2_CHUNK_FILTERED
|
BT2_CHUNK_FILTERED
|
||||||
} else {
|
} else {
|
||||||
BT2_CHUNK_UNFILTERED
|
BT2_CHUNK_UNFILTERED
|
||||||
};
|
};
|
||||||
|
|
||||||
let hdr_len = 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + os + 2 + length_size as usize + 4;
|
let mut flat = Vec::with_capacity(records.len() * record_size);
|
||||||
let leaf_address = base_address + hdr_len as u64;
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(hdr_len + node_size as usize);
|
|
||||||
out.extend_from_slice(b"BTHD");
|
|
||||||
out.push(0); // version
|
|
||||||
out.push(tree_type);
|
|
||||||
out.extend_from_slice(&node_size.to_le_bytes());
|
|
||||||
out.extend_from_slice(&(record_size as u16).to_le_bytes());
|
|
||||||
out.extend_from_slice(&0u16.to_le_bytes()); // depth
|
|
||||||
out.push(BT2_SPLIT_PERCENT);
|
|
||||||
out.push(BT2_MERGE_PERCENT);
|
|
||||||
if records.is_empty() {
|
|
||||||
out.extend(core::iter::repeat_n(0xFF, os));
|
|
||||||
} else {
|
|
||||||
push_addr(&mut out, leaf_address, offset_size);
|
|
||||||
}
|
|
||||||
out.extend_from_slice(&nrec.to_le_bytes());
|
|
||||||
match length_size {
|
|
||||||
4 => out.extend_from_slice(&(records.len() as u32).to_le_bytes()),
|
|
||||||
_ => out.extend_from_slice(&(records.len() as u64).to_le_bytes()),
|
|
||||||
}
|
|
||||||
let sum = jenkins_lookup3(&out);
|
|
||||||
out.extend_from_slice(&sum.to_le_bytes());
|
|
||||||
debug_assert_eq!(out.len(), hdr_len);
|
|
||||||
if records.is_empty() {
|
|
||||||
return Ok((out, node_size));
|
|
||||||
}
|
|
||||||
|
|
||||||
let leaf_start = out.len();
|
|
||||||
out.extend_from_slice(b"BTLF");
|
|
||||||
out.push(0); // version
|
|
||||||
out.push(tree_type);
|
|
||||||
for (scaled, chunk) in records {
|
for (scaled, chunk) in records {
|
||||||
push_index_element(&mut out, Some(chunk), offset_size, chunk_size_bytes);
|
push_index_element(&mut flat, Some(chunk), offset_size, chunk_size_bytes);
|
||||||
for &c in scaled {
|
for &c in scaled {
|
||||||
out.extend_from_slice(&c.to_le_bytes());
|
flat.extend_from_slice(&c.to_le_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let sum = jenkins_lookup3(&out[leaf_start..]);
|
let out = build_btree_v2(
|
||||||
out.extend_from_slice(&sum.to_le_bytes());
|
BTreeV2Params {
|
||||||
// The library reads whole nodes; pad the leaf out to the node size.
|
tree_type,
|
||||||
out.resize(leaf_start + node_size as usize, 0);
|
node_size,
|
||||||
|
record_size: record_size_u16,
|
||||||
|
split_percent: BT2_SPLIT_PERCENT,
|
||||||
|
merge_percent: BT2_MERGE_PERCENT,
|
||||||
|
},
|
||||||
|
&flat,
|
||||||
|
base_address,
|
||||||
|
offset_size,
|
||||||
|
length_size,
|
||||||
|
)?;
|
||||||
Ok((out, node_size))
|
Ok((out, node_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
||||||
use crate::attribute::AttributeMessage;
|
use crate::attribute::AttributeMessage;
|
||||||
|
use crate::btree_v2_write::{BTreeV2Params, build_btree_v2};
|
||||||
use crate::chunked_write::{
|
use crate::chunked_write::{
|
||||||
ChunkOptions, PrecompressedChunks, build_chunked_data_from_precompressed, precompress_chunks,
|
ChunkOptions, PrecompressedChunks, build_chunked_data_from_precompressed, precompress_chunks,
|
||||||
};
|
};
|
||||||
@@ -74,14 +75,57 @@ const DENSE_LINK_THRESHOLD: usize = 8;
|
|||||||
|
|
||||||
// ---- OH builders ----
|
// ---- OH builders ----
|
||||||
|
|
||||||
|
/// An object's attributes as its header stores them: inline Attribute
|
||||||
|
/// messages, or (`dense`) the Attribute Info message of dense storage; with
|
||||||
|
/// `track_order`, their creation order tracked and indexed.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct AttrStorage<'a> {
|
||||||
|
pub(crate) attrs: &'a [AttributeMessage],
|
||||||
|
pub(crate) dense: Option<&'a DenseAttrBlob>,
|
||||||
|
pub(crate) track_order: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrStorage<'_> {
|
||||||
|
/// Add the attribute messages to the header being built. Tracking
|
||||||
|
/// creation order, as libhdf5 does it: the header's flags say so, an
|
||||||
|
/// Attribute Info message is written even for inline attributes (it
|
||||||
|
/// holds the next creation order), and each inline attribute's message
|
||||||
|
/// carries its creation order.
|
||||||
|
fn add_to(&self, w: &mut ObjectHeaderWriter) {
|
||||||
|
if self.track_order {
|
||||||
|
w.track_attr_order();
|
||||||
|
}
|
||||||
|
if let Some(blob) = self.dense {
|
||||||
|
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.track_order {
|
||||||
|
w.add_message(
|
||||||
|
MessageType::AttributeInfo,
|
||||||
|
serialize_attribute_info(
|
||||||
|
u64::MAX,
|
||||||
|
u64::MAX,
|
||||||
|
Some((self.attrs.len() as u16, u64::MAX)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (i, attr) in self.attrs.iter().enumerate() {
|
||||||
|
w.add_message_with_order(
|
||||||
|
MessageType::Attribute,
|
||||||
|
attr.serialize(LENGTH_SIZE),
|
||||||
|
i as u16,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn build_chunked_dataset_oh(
|
pub(crate) fn build_chunked_dataset_oh(
|
||||||
dt: &Datatype,
|
dt: &Datatype,
|
||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
layout_message: &[u8],
|
layout_message: &[u8],
|
||||||
pipeline_message: Option<&[u8]>,
|
pipeline_message: Option<&[u8]>,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -93,13 +137,7 @@ pub(crate) fn build_chunked_dataset_oh(
|
|||||||
if let Some(pm) = pipeline_message {
|
if let Some(pm) = pipeline_message {
|
||||||
w.add_message(MessageType::FilterPipeline, pm.to_vec());
|
w.add_message(MessageType::FilterPipeline, pm.to_vec());
|
||||||
}
|
}
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -110,8 +148,7 @@ pub(crate) fn build_dataset_oh(
|
|||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
data_addr: u64,
|
data_addr: u64,
|
||||||
data_size: u64,
|
data_size: u64,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -131,13 +168,7 @@ pub(crate) fn build_dataset_oh(
|
|||||||
dl.extend_from_slice(&data_addr.to_le_bytes());
|
dl.extend_from_slice(&data_addr.to_le_bytes());
|
||||||
dl.extend_from_slice(&data_size.to_le_bytes());
|
dl.extend_from_slice(&data_size.to_le_bytes());
|
||||||
w.add_message(MessageType::DataLayout, dl);
|
w.add_message(MessageType::DataLayout, dl);
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -147,8 +178,7 @@ pub(crate) fn build_compact_dataset_oh(
|
|||||||
dt: &Datatype,
|
dt: &Datatype,
|
||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -163,13 +193,7 @@ pub(crate) fn build_compact_dataset_oh(
|
|||||||
dl.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
dl.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
||||||
dl.extend_from_slice(data);
|
dl.extend_from_slice(data);
|
||||||
w.add_message(MessageType::DataLayout, dl);
|
w.add_message(MessageType::DataLayout, dl);
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -181,8 +205,7 @@ pub(crate) fn build_group_oh(
|
|||||||
links: &[LinkMessage],
|
links: &[LinkMessage],
|
||||||
link_info: &[u8],
|
link_info: &[u8],
|
||||||
dense_links: bool,
|
dense_links: bool,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let mut w = ObjectHeaderWriter::new();
|
let mut w = ObjectHeaderWriter::new();
|
||||||
@@ -197,13 +220,7 @@ pub(crate) fn build_group_oh(
|
|||||||
w.add_message(MessageType::Link, link.serialize(OFFSET_SIZE));
|
w.add_message(MessageType::Link, link.serialize(OFFSET_SIZE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -890,11 +907,31 @@ fn write_frhp(p: WriteFrhp) -> Vec<u8> {
|
|||||||
frhp
|
frhp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// libhdf5 numbers the attributes of an object that tracks their creation
|
||||||
|
/// order with a 2-byte counter.
|
||||||
|
fn check_tracked_attr_count(track_order: bool, n: usize) -> Result<(), FormatError> {
|
||||||
|
if track_order && n > usize::from(u16::MAX) {
|
||||||
|
return Err(FormatError::SerializationError(format!(
|
||||||
|
"{n} attributes on one object with creation order tracked: libhdf5 \
|
||||||
|
numbers at most {} (set fewer, or turn off track_order)",
|
||||||
|
u16::MAX
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Build dense attribute storage for a set of attributes.
|
/// Build dense attribute storage for a set of attributes.
|
||||||
|
///
|
||||||
|
/// With `track_order` the Attribute Info message tracks creation order (an
|
||||||
|
/// attribute's creation order is its position in `attrs`) and a type-9
|
||||||
|
/// creation-order index follows the name index, as libhdf5 writes for h5py's
|
||||||
|
/// `track_order=True`. libhdf5 numbers at most 65 535 attributes.
|
||||||
pub(crate) fn build_dense_attrs(
|
pub(crate) fn build_dense_attrs(
|
||||||
attrs: &[AttributeMessage],
|
attrs: &[AttributeMessage],
|
||||||
base_address: u64,
|
base_address: u64,
|
||||||
|
track_order: bool,
|
||||||
) -> Result<DenseAttrBlob, FormatError> {
|
) -> Result<DenseAttrBlob, FormatError> {
|
||||||
|
check_tracked_attr_count(track_order, attrs.len())?;
|
||||||
// Dense attrs use v3 attribute messages (adds character set encoding byte).
|
// Dense attrs use v3 attribute messages (adds character set encoding byte).
|
||||||
let serialized: Vec<Vec<u8>> = attrs.iter().map(|a| a.serialize_v3(LENGTH_SIZE)).collect();
|
let serialized: Vec<Vec<u8>> = attrs.iter().map(|a| a.serialize_v3(LENGTH_SIZE)).collect();
|
||||||
|
|
||||||
@@ -910,31 +947,55 @@ pub(crate) fn build_dense_attrs(
|
|||||||
let heap_id_length = heap.heap_id_length;
|
let heap_id_length = heap.heap_id_length;
|
||||||
let heap_ids = &heap.heap_ids;
|
let heap_ids = &heap.heap_ids;
|
||||||
|
|
||||||
// Build B-tree v2 type 8 records (17 bytes each)
|
// Build B-tree v2 type 8 records (17 bytes each), in the index's key
|
||||||
|
// order: libhdf5 compares the name hash, then — for names whose hashes
|
||||||
|
// collide — the names themselves (`strcmp`).
|
||||||
let record_size: u16 = heap_id_length + 1 + 4 + 4;
|
let record_size: u16 = heap_id_length + 1 + 4 + 4;
|
||||||
let mut records: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(attrs.len());
|
let mut order: Vec<usize> = (0..attrs.len()).collect();
|
||||||
for (i, heap_id) in heap_ids.iter().enumerate() {
|
order.sort_by(|&a, &b| {
|
||||||
let mut rec = Vec::with_capacity(record_size as usize);
|
name_hashes[a]
|
||||||
rec.extend_from_slice(heap_id);
|
.cmp(&name_hashes[b])
|
||||||
rec.push(0); // msg_flags
|
.then_with(|| attrs[a].name.as_bytes().cmp(attrs[b].name.as_bytes()))
|
||||||
rec.extend_from_slice(&(i as u32).to_le_bytes()); // creation_order
|
});
|
||||||
rec.extend_from_slice(&name_hashes[i].to_le_bytes()); // hash
|
let records: Vec<Vec<u8>> = order
|
||||||
records.push((name_hashes[i], i as u32, rec));
|
.into_iter()
|
||||||
}
|
.map(|i| {
|
||||||
records.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
|
let mut rec = Vec::with_capacity(record_size as usize);
|
||||||
|
rec.extend_from_slice(&heap_ids[i]);
|
||||||
let records: Vec<Vec<u8>> = records.into_iter().map(|(_, _, rec)| rec).collect();
|
rec.push(0); // msg_flags
|
||||||
|
rec.extend_from_slice(&(i as u32).to_le_bytes()); // creation_order
|
||||||
|
rec.extend_from_slice(&name_hashes[i].to_le_bytes()); // hash
|
||||||
|
rec
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let bthd_addr = btree_addr;
|
let bthd_addr = btree_addr;
|
||||||
let mut blob = heap.blob;
|
let mut blob = heap.blob;
|
||||||
blob.extend_from_slice(&single_leaf_v2_btree(
|
blob.extend_from_slice(&dense_v2_btree(8, record_size, &records, bthd_addr)?);
|
||||||
8,
|
|
||||||
record_size,
|
|
||||||
&records,
|
|
||||||
bthd_addr,
|
|
||||||
"attributes on one object",
|
|
||||||
)?);
|
|
||||||
|
|
||||||
let attr_info = serialize_attribute_info(frhp_addr, bthd_addr);
|
let order = if track_order {
|
||||||
|
// Type 9 records: heap ID, message flags, creation order (the key).
|
||||||
|
let records: Vec<Vec<u8>> = heap_ids
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, heap_id)| {
|
||||||
|
let mut rec = heap_id.clone();
|
||||||
|
rec.push(0); // msg_flags
|
||||||
|
rec.extend_from_slice(&(i as u32).to_le_bytes());
|
||||||
|
rec
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let corder_addr = base_address + blob.len() as u64;
|
||||||
|
blob.extend_from_slice(&dense_v2_btree(
|
||||||
|
9,
|
||||||
|
heap_id_length + 1 + 4,
|
||||||
|
&records,
|
||||||
|
corder_addr,
|
||||||
|
)?);
|
||||||
|
Some((attrs.len() as u16, corder_addr))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let attr_info = serialize_attribute_info(frhp_addr, bthd_addr, order);
|
||||||
|
|
||||||
Ok(DenseAttrBlob {
|
Ok(DenseAttrBlob {
|
||||||
attr_info_message: attr_info,
|
attr_info_message: attr_info,
|
||||||
@@ -953,69 +1014,57 @@ pub(crate) struct DenseLinkBlob {
|
|||||||
pub(crate) blob: Vec<u8>,
|
pub(crate) blob: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A v2 B-tree of `btree_type` holding `records` (already in key order) in a
|
/// libhdf5's node size for the dense link and attribute indexes it creates
|
||||||
/// single leaf, laid out at `addr`: the header, then the leaf. `what` names
|
/// (`H5G_NAME_BT2_NODE_SIZE`, `H5A_NAME_BT2_NODE_SIZE`, and the
|
||||||
/// the records in the error for too many ("links in one group").
|
/// creation-order indexes'), with their split and merge percentages.
|
||||||
fn single_leaf_v2_btree(
|
const DENSE_BT2_NODE_SIZE: u32 = 512;
|
||||||
|
const DENSE_BT2_SPLIT_PERCENT: u8 = 100;
|
||||||
|
const DENSE_BT2_MERGE_PERCENT: u8 = 40;
|
||||||
|
|
||||||
|
/// A dense-storage v2 B-tree of `btree_type` holding `records` (already in
|
||||||
|
/// key order), laid out at `addr`: the header, then its nodes.
|
||||||
|
///
|
||||||
|
/// Up to 65 535 records go in one leaf node sized to hold them (the layout
|
||||||
|
/// the writer has always used, kept so those files do not change). A leaf's
|
||||||
|
/// record count is a 2-byte field, and libhdf5 sizes a leaf's capacity from
|
||||||
|
/// the node size: a node with room for more than 65 535 records makes it
|
||||||
|
/// overflow that count when it adds one, so the node is capped at a full
|
||||||
|
/// leaf. More records get libhdf5's own 512-byte nodes, with internal nodes
|
||||||
|
/// above the leaves.
|
||||||
|
fn dense_v2_btree(
|
||||||
btree_type: u8,
|
btree_type: u8,
|
||||||
record_size: u16,
|
record_size: u16,
|
||||||
records: &[Vec<u8>],
|
records: &[Vec<u8>],
|
||||||
addr: u64,
|
addr: u64,
|
||||||
what: &str,
|
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
let os = OFFSET_SIZE as usize;
|
let rs = usize::from(record_size);
|
||||||
let ls = LENGTH_SIZE as usize;
|
let n = records.len();
|
||||||
// The root node's record count is a 2-byte field; more records need
|
let node_size = if n <= usize::from(u16::MAX) {
|
||||||
// internal nodes, which the writer does not build.
|
let btlf_size = 4 + 1 + 1 + n * rs + 4;
|
||||||
let num_records = u16::try_from(records.len()).map_err(|_| {
|
let max_node = 4 + 1 + 1 + usize::from(u16::MAX) * rs + 4;
|
||||||
FormatError::SerializationError(format!(
|
u32::try_from(btlf_size.next_power_of_two().max(512).min(max_node))
|
||||||
"{} {what}: at most {} can be written \
|
.map_err(|_| FormatError::Overflow("B-tree v2 node size".into()))?
|
||||||
(a deeper B-tree index is not implemented)",
|
} else {
|
||||||
records.len(),
|
DENSE_BT2_NODE_SIZE
|
||||||
u16::MAX
|
};
|
||||||
))
|
let flat: Vec<u8> = records
|
||||||
})?;
|
.iter()
|
||||||
let bthd_size = 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + os + 2 + ls + 4;
|
.inspect(|r| debug_assert_eq!(r.len(), rs))
|
||||||
let btlf_size = 4 + 1 + 1 + (records.len() * record_size as usize) + 4;
|
.flat_map(|r| r.iter().copied())
|
||||||
// libhdf5 sizes a leaf's capacity from the node size, and a leaf's
|
.collect();
|
||||||
// record count is a 2-byte field: a node with room for more than
|
build_btree_v2(
|
||||||
// 65 535 records makes it overflow that count when it adds one (the
|
BTreeV2Params {
|
||||||
// group can then no longer be listed). Cap the node at a full leaf.
|
tree_type: btree_type,
|
||||||
let max_node = btlf_size - records.len() * record_size as usize
|
node_size,
|
||||||
+ usize::from(u16::MAX) * record_size as usize;
|
record_size,
|
||||||
let node_size = btlf_size.next_power_of_two().max(512).min(max_node) as u32;
|
split_percent: DENSE_BT2_SPLIT_PERCENT,
|
||||||
let btlf_addr = addr + bthd_size as u64;
|
merge_percent: DENSE_BT2_MERGE_PERCENT,
|
||||||
|
},
|
||||||
let mut out = Vec::with_capacity(bthd_size + node_size as usize);
|
&flat,
|
||||||
out.extend_from_slice(b"BTHD");
|
addr,
|
||||||
out.push(0); // version
|
OFFSET_SIZE,
|
||||||
out.push(btree_type);
|
LENGTH_SIZE,
|
||||||
out.extend_from_slice(&node_size.to_le_bytes());
|
)
|
||||||
out.extend_from_slice(&record_size.to_le_bytes());
|
|
||||||
out.extend_from_slice(&0u16.to_le_bytes()); // depth = 0 (single leaf)
|
|
||||||
out.push(100); // split_percent
|
|
||||||
out.push(40); // merge_percent
|
|
||||||
write_offset(&mut out, btlf_addr, OFFSET_SIZE);
|
|
||||||
out.extend_from_slice(&num_records.to_le_bytes());
|
|
||||||
write_length(&mut out, records.len() as u64, LENGTH_SIZE);
|
|
||||||
let checksum = crate::checksum::jenkins_lookup3(&out);
|
|
||||||
out.extend_from_slice(&checksum.to_le_bytes());
|
|
||||||
debug_assert_eq!(out.len(), bthd_size);
|
|
||||||
|
|
||||||
let mut btlf = Vec::with_capacity(node_size as usize);
|
|
||||||
btlf.extend_from_slice(b"BTLF");
|
|
||||||
btlf.push(0); // version
|
|
||||||
btlf.push(btree_type);
|
|
||||||
for rec in records {
|
|
||||||
debug_assert_eq!(rec.len(), record_size as usize);
|
|
||||||
btlf.extend_from_slice(rec);
|
|
||||||
}
|
|
||||||
// The checksum follows the records, not the end of the node.
|
|
||||||
let checksum = crate::checksum::jenkins_lookup3(&btlf);
|
|
||||||
btlf.extend_from_slice(&checksum.to_le_bytes());
|
|
||||||
btlf.resize(node_size as usize, 0);
|
|
||||||
out.extend_from_slice(&btlf);
|
|
||||||
Ok(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build dense link storage for a group's links, laid out at `base_address`.
|
/// Build dense link storage for a group's links, laid out at `base_address`.
|
||||||
@@ -1039,14 +1088,18 @@ pub(crate) fn build_dense_links(
|
|||||||
let heap = build_single_block_fractal_heap(&serialized, base_address, 32, 7)?;
|
let heap = build_single_block_fractal_heap(&serialized, base_address, 32, 7)?;
|
||||||
let heap_id_length = heap.heap_id_length;
|
let heap_id_length = heap.heap_id_length;
|
||||||
|
|
||||||
// Type 5 records: hash(4) + heap_id. The B-tree's key is the name hash,
|
// Type 5 records: hash(4) + heap_id. The B-tree's key is the name hash
|
||||||
// so records are sorted by (hash, order).
|
// and, for names whose hashes collide, the name (libhdf5 compares them
|
||||||
|
// with `strcmp`): records out of that order are not found by name.
|
||||||
let mut by_name: Vec<(u32, usize)> = links
|
let mut by_name: Vec<(u32, usize)> = links
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, l)| (crate::checksum::jenkins_lookup3(l.name.as_bytes()), i))
|
.map(|(i, l)| (crate::checksum::jenkins_lookup3(l.name.as_bytes()), i))
|
||||||
.collect();
|
.collect();
|
||||||
by_name.sort_unstable();
|
by_name.sort_unstable_by(|&(ha, a), &(hb, b)| {
|
||||||
|
ha.cmp(&hb)
|
||||||
|
.then_with(|| links[a].name.as_bytes().cmp(links[b].name.as_bytes()))
|
||||||
|
});
|
||||||
let name_records: Vec<Vec<u8>> = by_name
|
let name_records: Vec<Vec<u8>> = by_name
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(hash, i)| {
|
.map(|&(hash, i)| {
|
||||||
@@ -1057,12 +1110,11 @@ pub(crate) fn build_dense_links(
|
|||||||
.collect();
|
.collect();
|
||||||
let name_bt_addr = heap.btree_addr;
|
let name_bt_addr = heap.btree_addr;
|
||||||
let mut blob = heap.blob;
|
let mut blob = heap.blob;
|
||||||
blob.extend_from_slice(&single_leaf_v2_btree(
|
blob.extend_from_slice(&dense_v2_btree(
|
||||||
5,
|
5,
|
||||||
4 + heap_id_length,
|
4 + heap_id_length,
|
||||||
&name_records,
|
&name_records,
|
||||||
name_bt_addr,
|
name_bt_addr,
|
||||||
"links in one group",
|
|
||||||
)?);
|
)?);
|
||||||
|
|
||||||
let link_info_message = if track_order {
|
let link_info_message = if track_order {
|
||||||
@@ -1082,12 +1134,11 @@ pub(crate) fn build_dense_links(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let order_bt_addr = base_address + blob.len() as u64;
|
let order_bt_addr = base_address + blob.len() as u64;
|
||||||
blob.extend_from_slice(&single_leaf_v2_btree(
|
blob.extend_from_slice(&dense_v2_btree(
|
||||||
6,
|
6,
|
||||||
8 + heap_id_length,
|
8 + heap_id_length,
|
||||||
&order_records,
|
&order_records,
|
||||||
order_bt_addr,
|
order_bt_addr,
|
||||||
"links in one group",
|
|
||||||
)?);
|
)?);
|
||||||
let next_order = by_order.last().map_or(0, |&(o, _)| o + 1);
|
let next_order = by_order.last().map_or(0, |&(o, _)| o + 1);
|
||||||
serialize_link_info(
|
serialize_link_info(
|
||||||
@@ -1147,12 +1198,25 @@ fn encode_managed_id(offset: u64, length: u64, max_heap_size: u16, id_length: u1
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_attribute_info(fh_addr: u64, btree_name_addr: u64) -> Vec<u8> {
|
/// Serialize an Attribute Info message (version 0). `order` — the next
|
||||||
|
/// creation order to assign and the creation-order index's address — is
|
||||||
|
/// present when creation order is tracked and indexed.
|
||||||
|
fn serialize_attribute_info(
|
||||||
|
fh_addr: u64,
|
||||||
|
btree_name_addr: u64,
|
||||||
|
order: Option<(u16, u64)>,
|
||||||
|
) -> Vec<u8> {
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
data.push(0); // version
|
data.push(0); // version
|
||||||
data.push(0x00); // flags
|
data.push(if order.is_some() { 0x03 } else { 0x00 }); // flags: tracked, indexed
|
||||||
|
if let Some((next, _)) = order {
|
||||||
|
data.extend_from_slice(&next.to_le_bytes());
|
||||||
|
}
|
||||||
data.extend_from_slice(&fh_addr.to_le_bytes());
|
data.extend_from_slice(&fh_addr.to_le_bytes());
|
||||||
data.extend_from_slice(&btree_name_addr.to_le_bytes());
|
data.extend_from_slice(&btree_name_addr.to_le_bytes());
|
||||||
|
if let Some((_, corder_addr)) = order {
|
||||||
|
data.extend_from_slice(&corder_addr.to_le_bytes());
|
||||||
|
}
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,8 +1291,7 @@ pub(crate) fn build_vds_dataset_oh(
|
|||||||
dt: &Datatype,
|
dt: &Datatype,
|
||||||
ds: &Dataspace,
|
ds: &Dataspace,
|
||||||
global_heap_addr: u64,
|
global_heap_addr: u64,
|
||||||
attrs: &[AttributeMessage],
|
attrs: AttrStorage<'_>,
|
||||||
dense_blob: Option<&DenseAttrBlob>,
|
|
||||||
fill_message: &[u8],
|
fill_message: &[u8],
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
) -> Result<Vec<u8>, FormatError> {
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
@@ -1243,13 +1306,7 @@ pub(crate) fn build_vds_dataset_oh(
|
|||||||
dl.extend_from_slice(&global_heap_addr.to_le_bytes());
|
dl.extend_from_slice(&global_heap_addr.to_le_bytes());
|
||||||
dl.extend_from_slice(&1u32.to_le_bytes()); // object index 1 in the collection
|
dl.extend_from_slice(&1u32.to_le_bytes()); // object index 1 in the collection
|
||||||
w.add_message(MessageType::DataLayout, dl);
|
w.add_message(MessageType::DataLayout, dl);
|
||||||
if let Some(blob) = dense_blob {
|
attrs.add_to(&mut w);
|
||||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
|
||||||
} else {
|
|
||||||
for attr in attrs {
|
|
||||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_refcount(&mut w, refcount);
|
add_refcount(&mut w, refcount);
|
||||||
w.serialize()
|
w.serialize()
|
||||||
}
|
}
|
||||||
@@ -1284,7 +1341,8 @@ fn write_undef_offset(buf: &mut Vec<u8>, offset_size: u8) {
|
|||||||
pub struct FileWriter {
|
pub struct FileWriter {
|
||||||
/// The root group's contents (its name is unused).
|
/// The root group's contents (its name is unused).
|
||||||
root: GroupBuilder,
|
root: GroupBuilder,
|
||||||
/// Default for groups that do not call [`GroupBuilder::track_order`].
|
/// Default for groups and datasets that do not set their own
|
||||||
|
/// `track_order`.
|
||||||
track_order: bool,
|
track_order: bool,
|
||||||
/// Global alignment threshold: datasets with raw data >= this many bytes
|
/// Global alignment threshold: datasets with raw data >= this many bytes
|
||||||
/// will have their data aligned to `alignment_bytes`.
|
/// will have their data aligned to `alignment_bytes`.
|
||||||
@@ -1319,11 +1377,18 @@ struct DsFlat {
|
|||||||
virtual_sources: Option<Vec<VdsMapping>>,
|
virtual_sources: Option<Vec<VdsMapping>>,
|
||||||
/// Number of hard links to the dataset.
|
/// Number of hard links to the dataset.
|
||||||
refcount: u32,
|
refcount: u32,
|
||||||
|
/// Track (and index) attribute creation order.
|
||||||
|
track_order: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a DatasetBuilder into a DsFlat, handling VDS (which does not
|
/// Convert a DatasetBuilder into a DsFlat, handling VDS (which does not
|
||||||
/// require a `data` field).
|
/// require a `data` field).
|
||||||
fn flatten_ds(db: DatasetBuilder, refcount: u32) -> Result<DsFlat, FormatError> {
|
fn flatten_ds(
|
||||||
|
db: DatasetBuilder,
|
||||||
|
refcount: u32,
|
||||||
|
default_track_order: bool,
|
||||||
|
) -> Result<DsFlat, FormatError> {
|
||||||
|
let track_order = db.track_order.unwrap_or(default_track_order);
|
||||||
let dt = db.datatype.ok_or(FormatError::DatasetMissingData)?;
|
let dt = db.datatype.ok_or(FormatError::DatasetMissingData)?;
|
||||||
let shape = db.shape.ok_or(FormatError::DatasetMissingShape)?;
|
let shape = db.shape.ok_or(FormatError::DatasetMissingShape)?;
|
||||||
let is_vds = db.virtual_sources.is_some();
|
let is_vds = db.virtual_sources.is_some();
|
||||||
@@ -1373,6 +1438,7 @@ fn flatten_ds(db: DatasetBuilder, refcount: u32) -> Result<DsFlat, FormatError>
|
|||||||
alignment: db.alignment,
|
alignment: db.alignment,
|
||||||
virtual_sources: db.virtual_sources,
|
virtual_sources: db.virtual_sources,
|
||||||
refcount,
|
refcount,
|
||||||
|
track_order,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1429,10 +1495,12 @@ impl FileWriter {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track (and index) link creation order in every group that does not
|
/// Track (and index) creation order — of links and attributes in every
|
||||||
/// set its own [`GroupBuilder::track_order`], the root included — as
|
/// group that does not set its own [`GroupBuilder::track_order`], the
|
||||||
/// h5py's `track_order=True`: libhdf5 then lists members in the order
|
/// root included, and of attributes on every dataset that does not set
|
||||||
/// they were added. Off by default (members are listed by name).
|
/// its own [`DatasetBuilder::track_order`] — as h5py's
|
||||||
|
/// `track_order=True`: libhdf5 then lists members and attributes in the
|
||||||
|
/// order they were added. Off by default (they are listed by name).
|
||||||
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||||
self.track_order = track;
|
self.track_order = track;
|
||||||
self
|
self
|
||||||
@@ -1503,7 +1571,7 @@ impl FileWriter {
|
|||||||
let all_ds: Vec<DsFlat> = tree
|
let all_ds: Vec<DsFlat> = tree
|
||||||
.datasets
|
.datasets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(db, refcount)| flatten_ds(db, refcount))
|
.map(|(db, refcount)| flatten_ds(db, refcount, self.track_order))
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
let groups: Vec<GrpFlat> = tree
|
let groups: Vec<GrpFlat> = tree
|
||||||
.groups
|
.groups
|
||||||
@@ -1520,6 +1588,15 @@ impl FileWriter {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Refuse up front what dense storage would refuse after the work.
|
||||||
|
let tracked = groups
|
||||||
|
.iter()
|
||||||
|
.map(|g| (g.track_order, g.attrs.len()))
|
||||||
|
.chain(all_ds.iter().map(|d| (d.track_order, d.attrs.len())));
|
||||||
|
for (track, n) in tracked {
|
||||||
|
check_tracked_attr_count(track, n)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Every datatype must have an on-disk encoding before anything is laid
|
// Every datatype must have an on-disk encoding before anything is laid
|
||||||
// out: `Datatype::serialize` itself cannot report a failure.
|
// out: `Datatype::serialize` itself cannot report a failure.
|
||||||
let group_attrs = groups.iter().flat_map(|g| &g.attrs);
|
let group_attrs = groups.iter().flat_map(|g| &g.attrs);
|
||||||
@@ -1574,7 +1651,7 @@ impl FileWriter {
|
|||||||
.map(|(gi, g)| {
|
.map(|(gi, g)| {
|
||||||
let dummy_links = g.link_messages(&[], &[]);
|
let dummy_links = g.link_messages(&[], &[]);
|
||||||
let attr_blob = group_dense[gi]
|
let attr_blob = group_dense[gi]
|
||||||
.then(|| build_dense_attrs(&g.attrs, 0))
|
.then(|| build_dense_attrs(&g.attrs, 0, g.track_order))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let li = if group_links_dense[gi] {
|
let li = if group_links_dense[gi] {
|
||||||
serialize_link_info(
|
serialize_link_info(
|
||||||
@@ -1590,8 +1667,11 @@ impl FileWriter {
|
|||||||
&dummy_links,
|
&dummy_links,
|
||||||
&li,
|
&li,
|
||||||
group_links_dense[gi],
|
group_links_dense[gi],
|
||||||
&g.attrs,
|
AttrStorage {
|
||||||
attr_blob.as_ref(),
|
attrs: &g.attrs,
|
||||||
|
dense: attr_blob.as_ref(),
|
||||||
|
track_order: g.track_order,
|
||||||
|
},
|
||||||
g.refcount,
|
g.refcount,
|
||||||
)
|
)
|
||||||
.map(|oh| oh.len())
|
.map(|oh| oh.len())
|
||||||
@@ -1610,7 +1690,7 @@ impl FileWriter {
|
|||||||
let mut dummy_cursor = 0u64;
|
let mut dummy_cursor = 0u64;
|
||||||
for (i, d) in all_ds.iter().enumerate() {
|
for (i, d) in all_ds.iter().enumerate() {
|
||||||
let dense_blob = ds_dense[i]
|
let dense_blob = ds_dense[i]
|
||||||
.then(|| build_dense_attrs(&d.attrs, 0))
|
.then(|| build_dense_attrs(&d.attrs, 0, d.track_order))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
if is_vds[i] {
|
if is_vds[i] {
|
||||||
// VDS: dummy OH with address 0 to get the OH size. The global
|
// VDS: dummy OH with address 0 to get the OH size. The global
|
||||||
@@ -1619,8 +1699,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
0, // dummy address
|
0, // dummy address
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1659,8 +1742,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
&result.layout_message,
|
&result.layout_message,
|
||||||
result.pipeline_message.as_deref(),
|
result.pipeline_message.as_deref(),
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1674,8 +1760,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
&d.raw,
|
&d.raw,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1690,8 +1779,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
0,
|
0,
|
||||||
d.raw.len() as u64,
|
d.raw.len() as u64,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
dense_blob.as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: dense_blob.as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1735,7 +1827,7 @@ impl FileWriter {
|
|||||||
group_link_blob_addrs.push(None);
|
group_link_blob_addrs.push(None);
|
||||||
}
|
}
|
||||||
if group_dense[gi] {
|
if group_dense[gi] {
|
||||||
let blob = build_dense_attrs(&g.attrs, cursor2 as u64)?;
|
let blob = build_dense_attrs(&g.attrs, cursor2 as u64, g.track_order)?;
|
||||||
cursor2 += blob.blob.len();
|
cursor2 += blob.blob.len();
|
||||||
group_dense_blobs.push(Some(blob));
|
group_dense_blobs.push(Some(blob));
|
||||||
} else {
|
} else {
|
||||||
@@ -1752,7 +1844,8 @@ impl FileWriter {
|
|||||||
let addr = cursor2 as u64;
|
let addr = cursor2 as u64;
|
||||||
cursor2 += sz;
|
cursor2 += sz;
|
||||||
if ds_dense[i] {
|
if ds_dense[i] {
|
||||||
let blob = build_dense_attrs(&all_ds[i].attrs, cursor2 as u64)?;
|
let blob =
|
||||||
|
build_dense_attrs(&all_ds[i].attrs, cursor2 as u64, all_ds[i].track_order)?;
|
||||||
cursor2 += blob.blob.len();
|
cursor2 += blob.blob.len();
|
||||||
ds_dense_blobs.push(Some(blob));
|
ds_dense_blobs.push(Some(blob));
|
||||||
} else {
|
} else {
|
||||||
@@ -1776,8 +1869,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
heap_addr,
|
heap_addr,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1804,8 +1900,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
&result.layout_message,
|
&result.layout_message,
|
||||||
result.pipeline_message.as_deref(),
|
result.pipeline_message.as_deref(),
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1820,8 +1919,11 @@ impl FileWriter {
|
|||||||
&d.dt,
|
&d.dt,
|
||||||
&d.ds,
|
&d.ds,
|
||||||
&d.raw,
|
&d.raw,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1846,8 +1948,11 @@ impl FileWriter {
|
|||||||
&d.ds,
|
&d.ds,
|
||||||
cursor2 as u64,
|
cursor2 as u64,
|
||||||
d.raw.len() as u64,
|
d.raw.len() as u64,
|
||||||
&d.attrs,
|
AttrStorage {
|
||||||
ds_dense_blobs[i].as_ref(),
|
attrs: &d.attrs,
|
||||||
|
dense: ds_dense_blobs[i].as_ref(),
|
||||||
|
track_order: d.track_order,
|
||||||
|
},
|
||||||
&d.fill_message,
|
&d.fill_message,
|
||||||
d.refcount,
|
d.refcount,
|
||||||
)?;
|
)?;
|
||||||
@@ -1915,8 +2020,11 @@ impl FileWriter {
|
|||||||
&links,
|
&links,
|
||||||
&li,
|
&li,
|
||||||
link_blob.is_some(),
|
link_blob.is_some(),
|
||||||
&g.attrs,
|
AttrStorage {
|
||||||
group_dense_blobs[gi].as_ref(),
|
attrs: &g.attrs,
|
||||||
|
dense: group_dense_blobs[gi].as_ref(),
|
||||||
|
track_order: g.track_order,
|
||||||
|
},
|
||||||
g.refcount,
|
g.refcount,
|
||||||
)?;
|
)?;
|
||||||
debug_assert_eq!(oh.len(), group_oh_sizes[gi]);
|
debug_assert_eq!(oh.len(), group_oh_sizes[gi]);
|
||||||
@@ -2147,6 +2255,92 @@ mod tests {
|
|||||||
assert_eq!(read_dataset_f64(&bytes, "data"), vec![1.0, 2.0, 3.0]);
|
assert_eq!(read_dataset_f64(&bytes, "data"), vec![1.0, 2.0, 3.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attribute names of the object at `path`, in the order the reader
|
||||||
|
/// lists them.
|
||||||
|
fn attr_names(bytes: &[u8], path: &str) -> Vec<String> {
|
||||||
|
let sig = signature::find_signature(bytes).unwrap();
|
||||||
|
let sb = Superblock::parse(bytes, sig).unwrap();
|
||||||
|
let addr = if path == "/" {
|
||||||
|
sb.root_group_address
|
||||||
|
} else {
|
||||||
|
resolve_path_any(bytes, &sb, path).unwrap()
|
||||||
|
};
|
||||||
|
let hdr =
|
||||||
|
ObjectHeader::parse(bytes, addr as usize, sb.offset_size, sb.length_size).unwrap();
|
||||||
|
crate::attribute::extract_attributes_full(bytes, &hdr, sb.offset_size, sb.length_size)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.name)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracked_attributes_are_read_in_creation_order() {
|
||||||
|
let set = |names: &[String]| -> Vec<(String, AttrValue)> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, n)| (n.clone(), AttrValue::I64(i as i64)))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let compact: Vec<String> = ["zeta", "alpha", "mid"].map(String::from).to_vec();
|
||||||
|
let dense: Vec<String> = (0..30).rev().map(|i| format!("a{i:02}")).collect();
|
||||||
|
let mut fw = FileWriter::new();
|
||||||
|
fw.track_order(true);
|
||||||
|
for (n, v) in set(&compact) {
|
||||||
|
fw.set_root_attr(&n, v);
|
||||||
|
}
|
||||||
|
let ds = fw.create_dataset("dense");
|
||||||
|
ds.with_i32_data(&[1]);
|
||||||
|
for (n, v) in set(&dense) {
|
||||||
|
ds.set_attr(&n, v);
|
||||||
|
}
|
||||||
|
let ds = fw.create_dataset("untracked");
|
||||||
|
ds.with_i32_data(&[1]).track_order(false);
|
||||||
|
for (n, v) in set(&dense) {
|
||||||
|
ds.set_attr(&n, v);
|
||||||
|
}
|
||||||
|
let mut g = fw.create_group("g");
|
||||||
|
g.track_order(false);
|
||||||
|
for (n, v) in set(&compact) {
|
||||||
|
g.set_attr(&n, v);
|
||||||
|
}
|
||||||
|
fw.add_group(g.finish());
|
||||||
|
let bytes = fw.finish().unwrap();
|
||||||
|
assert_eq!(attr_names(&bytes, "/"), compact);
|
||||||
|
assert_eq!(attr_names(&bytes, "dense"), dense);
|
||||||
|
// Without tracking: storage order (inline: as added; dense: hash).
|
||||||
|
assert_eq!(attr_names(&bytes, "g"), compact);
|
||||||
|
let mut by_hash = dense.clone();
|
||||||
|
by_hash.sort_by_key(|n| crate::checksum::jenkins_lookup3(n.as_bytes()));
|
||||||
|
assert_eq!(attr_names(&bytes, "untracked"), by_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn too_many_tracked_attributes_is_an_error() {
|
||||||
|
// libhdf5 numbers at most 65 535 attributes on an object that
|
||||||
|
// tracks their creation order (a 2-byte field). (`set_attr` looks
|
||||||
|
// for an earlier value, so 65 536 of them through the builder take
|
||||||
|
// a while; build the messages directly.)
|
||||||
|
let attrs: Vec<AttributeMessage> = (0..65_536)
|
||||||
|
.map(|i| build_attr_message(&format!("a{i}"), &AttrValue::I64(i)))
|
||||||
|
.collect();
|
||||||
|
let err = build_dense_attrs(&attrs, 0, true)
|
||||||
|
.err()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
assert!(err.contains("65536 attributes on one object"), "{err}");
|
||||||
|
assert!(build_dense_attrs(&attrs[1..], 0, true).is_ok());
|
||||||
|
assert!(build_dense_attrs(&attrs, 0, false).is_ok());
|
||||||
|
let mut fw = FileWriter::new();
|
||||||
|
let ds = fw.create_dataset("x");
|
||||||
|
ds.with_i32_data(&[1]).track_order(true);
|
||||||
|
for i in 0..20 {
|
||||||
|
ds.set_attr(&format!("a{i}"), AttrValue::I64(i));
|
||||||
|
}
|
||||||
|
assert!(fw.finish().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dense_attrs_root_group_self_roundtrip() {
|
fn dense_attrs_root_group_self_roundtrip() {
|
||||||
let mut fw = FileWriter::new();
|
let mut fw = FileWriter::new();
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ pub mod attribute;
|
|||||||
pub mod attribute_info;
|
pub mod attribute_info;
|
||||||
pub mod btree_v1;
|
pub mod btree_v1;
|
||||||
pub mod btree_v2;
|
pub mod btree_v2;
|
||||||
|
mod btree_v2_write;
|
||||||
mod bulk_alloc;
|
mod bulk_alloc;
|
||||||
pub mod checksum;
|
pub mod checksum;
|
||||||
pub mod chunk_cache;
|
pub mod chunk_cache;
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ use crate::message_type::MessageType;
|
|||||||
/// its size truncated to 16 bits produced files libhdf5 refuses.
|
/// its size truncated to 16 bits produced files libhdf5 refuses.
|
||||||
pub const MAX_MESSAGE_SIZE: usize = u16::MAX as usize;
|
pub const MAX_MESSAGE_SIZE: usize = u16::MAX as usize;
|
||||||
|
|
||||||
|
/// Object header flags: attribute creation order tracked (each message
|
||||||
|
/// then carries a 2-byte creation order) and indexed.
|
||||||
|
const OHDR_ATTR_CRT_ORDER_TRACKED: u8 = 0x04;
|
||||||
|
const OHDR_ATTR_CRT_ORDER_INDEXED: u8 = 0x08;
|
||||||
|
|
||||||
/// Writer for v2 object headers with proper checksums.
|
/// Writer for v2 object headers with proper checksums.
|
||||||
pub struct ObjectHeaderWriter {
|
pub struct ObjectHeaderWriter {
|
||||||
messages: Vec<(MessageType, Vec<u8>, u8)>, // (type, data, msg_flags)
|
messages: Vec<(MessageType, Vec<u8>, u8, u16)>, // (type, data, msg_flags, creation order)
|
||||||
|
/// Attribute creation order tracked and indexed.
|
||||||
|
attr_order: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectHeaderWriter {
|
impl ObjectHeaderWriter {
|
||||||
@@ -22,17 +29,33 @@ impl ObjectHeaderWriter {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
|
attr_order: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Track and index attribute creation order, as libhdf5 does for an
|
||||||
|
/// object created with `H5P_CRT_ORDER_TRACKED | H5P_CRT_ORDER_INDEXED`
|
||||||
|
/// (h5py's `track_order=True`): the header's flags say so, and every
|
||||||
|
/// message carries a creation order (an attribute's own; 0 for the
|
||||||
|
/// others). libhdf5 reads the setting back from these flags.
|
||||||
|
pub fn track_attr_order(&mut self) {
|
||||||
|
self.attr_order = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a message to the header with default flags (0).
|
/// Add a message to the header with default flags (0).
|
||||||
pub fn add_message(&mut self, msg_type: MessageType, data: Vec<u8>) {
|
pub fn add_message(&mut self, msg_type: MessageType, data: Vec<u8>) {
|
||||||
self.messages.push((msg_type, data, 0));
|
self.messages.push((msg_type, data, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a message with specific flags.
|
/// Add a message with specific flags.
|
||||||
pub fn add_message_with_flags(&mut self, msg_type: MessageType, data: Vec<u8>, flags: u8) {
|
pub fn add_message_with_flags(&mut self, msg_type: MessageType, data: Vec<u8>, flags: u8) {
|
||||||
self.messages.push((msg_type, data, flags));
|
self.messages.push((msg_type, data, flags, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a message with its creation order, which is written only when
|
||||||
|
/// attribute creation order is tracked ([`Self::track_attr_order`]).
|
||||||
|
pub fn add_message_with_order(&mut self, msg_type: MessageType, data: Vec<u8>, order: u16) {
|
||||||
|
self.messages.push((msg_type, data, 0, order));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize the complete v2 object header (OHDR + messages + checksum).
|
/// Serialize the complete v2 object header (OHDR + messages + checksum).
|
||||||
@@ -41,10 +64,10 @@ impl ObjectHeaderWriter {
|
|||||||
/// than [`MAX_MESSAGE_SIZE`] (e.g. an attribute over ~64 KiB, which would
|
/// than [`MAX_MESSAGE_SIZE`] (e.g. an attribute over ~64 KiB, which would
|
||||||
/// need dense attribute storage), rather than writing a corrupt header.
|
/// need dense attribute storage), rather than writing a corrupt header.
|
||||||
pub fn serialize(&self) -> Result<Vec<u8>, FormatError> {
|
pub fn serialize(&self) -> Result<Vec<u8>, FormatError> {
|
||||||
if let Some((msg_type, data, _)) = self
|
if let Some((msg_type, data, _, _)) = self
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, data, _)| data.len() > MAX_MESSAGE_SIZE)
|
.find(|(_, data, _, _)| data.len() > MAX_MESSAGE_SIZE)
|
||||||
{
|
{
|
||||||
return Err(FormatError::SerializationError(format!(
|
return Err(FormatError::SerializationError(format!(
|
||||||
"{msg_type:?} message is {} bytes; an object header message holds at most \
|
"{msg_type:?} message is {} bytes; an object header message holds at most \
|
||||||
@@ -52,11 +75,13 @@ impl ObjectHeaderWriter {
|
|||||||
data.len()
|
data.len()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// Calculate total message bytes: each message has type(1) + size(2) + flags(1) + data
|
// Calculate total message bytes: each message has type(1) + size(2) +
|
||||||
|
// flags(1) [+ creation order(2)] + data
|
||||||
|
let msg_header = if self.attr_order { 6 } else { 4 };
|
||||||
let msg_bytes_total: usize = self
|
let msg_bytes_total: usize = self
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, data, _)| 4 + data.len())
|
.map(|(_, data, _, _)| msg_header + data.len())
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
// Determine chunk size field width based on msg_bytes_total
|
// Determine chunk size field width based on msg_bytes_total
|
||||||
@@ -68,6 +93,12 @@ impl ObjectHeaderWriter {
|
|||||||
(0x02u8, 4)
|
(0x02u8, 4)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let flags = if self.attr_order {
|
||||||
|
flags | OHDR_ATTR_CRT_ORDER_TRACKED | OHDR_ATTR_CRT_ORDER_INDEXED
|
||||||
|
} else {
|
||||||
|
flags
|
||||||
|
};
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
// OHDR signature
|
// OHDR signature
|
||||||
@@ -85,7 +116,7 @@ impl ObjectHeaderWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
for (msg_type, data, msg_flags) in &self.messages {
|
for (msg_type, data, msg_flags, order) in &self.messages {
|
||||||
let type_id = msg_type.to_u16();
|
let type_id = msg_type.to_u16();
|
||||||
assert!(
|
assert!(
|
||||||
type_id <= 255,
|
type_id <= 255,
|
||||||
@@ -94,6 +125,9 @@ impl ObjectHeaderWriter {
|
|||||||
buf.push(type_id as u8); // type (1 byte in v2)
|
buf.push(type_id as u8); // type (1 byte in v2)
|
||||||
buf.extend_from_slice(&(data.len() as u16).to_le_bytes()); // size (2 bytes)
|
buf.extend_from_slice(&(data.len() as u16).to_le_bytes()); // size (2 bytes)
|
||||||
buf.push(*msg_flags); // flags
|
buf.push(*msg_flags); // flags
|
||||||
|
if self.attr_order {
|
||||||
|
buf.extend_from_slice(&order.to_le_bytes()); // creation order
|
||||||
|
}
|
||||||
buf.extend_from_slice(data);
|
buf.extend_from_slice(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +227,21 @@ mod tests {
|
|||||||
assert_eq!(hdr.messages.len(), 0);
|
assert_eq!(hdr.messages.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracked_attribute_order_is_in_the_flags_and_every_message() {
|
||||||
|
let mut writer = ObjectHeaderWriter::new();
|
||||||
|
writer.track_attr_order();
|
||||||
|
writer.add_message(MessageType::Dataspace, vec![1, 2, 3, 4]);
|
||||||
|
writer.add_message_with_order(MessageType::Attribute, vec![5, 6], 7);
|
||||||
|
let bytes = writer.serialize().unwrap();
|
||||||
|
assert_eq!(bytes[5] & 0x0C, 0x0C);
|
||||||
|
let hdr = ObjectHeader::parse(&bytes, 0, 8, 8).unwrap();
|
||||||
|
assert_eq!(hdr.messages.len(), 2);
|
||||||
|
assert_eq!(hdr.messages[0].creation_order, Some(0));
|
||||||
|
assert_eq!(hdr.messages[1].creation_order, Some(7));
|
||||||
|
assert_eq!(hdr.messages[1].data, vec![5, 6]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn two_messages_roundtrip() {
|
fn two_messages_roundtrip() {
|
||||||
let mut writer = ObjectHeaderWriter::new();
|
let mut writer = ObjectHeaderWriter::new();
|
||||||
|
|||||||
@@ -503,6 +503,9 @@ pub struct DatasetBuilder {
|
|||||||
/// `data` field is ignored; instead the global heap blob is built from
|
/// `data` field is ignored; instead the global heap blob is built from
|
||||||
/// these mappings and a VDS layout message is emitted.
|
/// these mappings and a VDS layout message is emitted.
|
||||||
pub(crate) virtual_sources: Option<Vec<VdsMapping>>,
|
pub(crate) virtual_sources: Option<Vec<VdsMapping>>,
|
||||||
|
/// Track (and index) attribute creation order; `None` follows the
|
||||||
|
/// file's default (`FileWriter::track_order`).
|
||||||
|
pub(crate) track_order: Option<bool>,
|
||||||
#[cfg(feature = "provenance")]
|
#[cfg(feature = "provenance")]
|
||||||
pub(crate) provenance: Option<ProvenanceConfig>,
|
pub(crate) provenance: Option<ProvenanceConfig>,
|
||||||
}
|
}
|
||||||
@@ -522,11 +525,22 @@ impl DatasetBuilder {
|
|||||||
compact: false,
|
compact: false,
|
||||||
alignment: 0,
|
alignment: 0,
|
||||||
virtual_sources: None,
|
virtual_sources: None,
|
||||||
|
track_order: None,
|
||||||
#[cfg(feature = "provenance")]
|
#[cfg(feature = "provenance")]
|
||||||
provenance: None,
|
provenance: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Track the creation order of this dataset's attributes, and index it,
|
||||||
|
/// as h5py's `create_dataset(..., track_order=True)` does: libhdf5 (and
|
||||||
|
/// h5py) then list the attributes in the order they were set rather
|
||||||
|
/// than by name. libhdf5 numbers at most 65 535 attributes on an object
|
||||||
|
/// that tracks their order; more is an error when the file is written.
|
||||||
|
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||||
|
self.track_order = Some(track);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_f64_data(&mut self, data: &[f64]) -> &mut Self {
|
pub fn with_f64_data(&mut self, data: &[f64]) -> &mut Self {
|
||||||
self.datatype = Some(make_f64_type());
|
self.datatype = Some(make_f64_type());
|
||||||
let mut b = Vec::with_capacity(data.len() * 8);
|
let mut b = Vec::with_capacity(data.len() * 8);
|
||||||
@@ -986,10 +1000,11 @@ impl GroupBuilder {
|
|||||||
self.attrs.push((name.to_string(), value));
|
self.attrs.push((name.to_string(), value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track the creation order of this group's links, and index it, as
|
/// Track the creation order of this group's links and attributes, and
|
||||||
/// h5py's `track_order=True` does: libhdf5 (and h5py) then list the
|
/// index it, as h5py's `track_order=True` does: libhdf5 (and h5py) then
|
||||||
/// group's members in the order they were added rather than by name.
|
/// list the group's members, and its attributes, in the order they were
|
||||||
/// Applies to links only, not to attributes.
|
/// added rather than by name. libhdf5 numbers at most 65 535 attributes
|
||||||
|
/// on an object that tracks their order.
|
||||||
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||||
self.track_order = Some(track);
|
self.track_order = Some(track);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -940,11 +940,17 @@ fn write_nested_links(dir: &Path) -> Vec<String> {
|
|||||||
g.create_dataset(&format!("n{i:02}")).with_i32_data(&[i]);
|
g.create_dataset(&format!("n{i:02}")).with_i32_data(&[i]);
|
||||||
}
|
}
|
||||||
g.add_hard_link("back", "/a/b/c");
|
g.add_hard_link("back", "/a/b/c");
|
||||||
|
// Attribute creation order tracked too: dense, with a type-9 index.
|
||||||
|
for i in (0..12).rev() {
|
||||||
|
g.set_attr(&format!("attr{i:02}"), AttrValue::I64(i));
|
||||||
|
}
|
||||||
b.add_group(g.finish());
|
b.add_group(g.finish());
|
||||||
let mut g = b.create_group("compact_ordered");
|
let mut g = b.create_group("compact_ordered");
|
||||||
g.track_order(true);
|
g.track_order(true);
|
||||||
g.create_dataset("z").with_i32_data(&[1]);
|
g.create_dataset("z").with_i32_data(&[1]);
|
||||||
g.create_dataset("a").with_i32_data(&[2]);
|
g.create_dataset("a").with_i32_data(&[2]);
|
||||||
|
g.set_attr("zz", AttrValue::I64(1));
|
||||||
|
g.set_attr("aa", AttrValue::I64(2));
|
||||||
b.add_group(g.finish());
|
b.add_group(g.finish());
|
||||||
let nested = dir.join("nested.h5");
|
let nested = dir.join("nested.h5");
|
||||||
b.write(&nested).unwrap();
|
b.write(&nested).unwrap();
|
||||||
@@ -1024,3 +1030,75 @@ fn check_files_with_big_dense_storage() {
|
|||||||
assert_eq!(code(&o), 0, "{p}:\n{}", stdout(&o));
|
assert_eq!(code(&o), 0, "{p}:\n{}", stdout(&o));
|
||||||
assert!(stdout(&o).contains("no problems found"), "{}", stdout(&o));
|
assert!(stdout(&o).contains("no problems found"), "{}", stdout(&o));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `(type, depth)` of every v2 B-tree header in a file written with 8-byte
|
||||||
|
/// offsets and lengths (found by signature and checksum).
|
||||||
|
fn btree_v2_depths(data: &[u8]) -> Vec<(u8, u16)> {
|
||||||
|
const LEN: usize = 4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + 8 + 2 + 8;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for at in 0..data.len().saturating_sub(LEN + 4) {
|
||||||
|
if &data[at..at + 4] != b"BTHD" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let stored = u32::from_le_bytes(data[at + LEN..at + LEN + 4].try_into().unwrap());
|
||||||
|
if jenkins_lookup3(&data[at..at + LEN]) == stored {
|
||||||
|
out.push((
|
||||||
|
data[at + 5],
|
||||||
|
u16::from_le_bytes([data[at + 12], data[at + 13]]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_files_with_deep_btrees() {
|
||||||
|
// Dense indexes and a chunk index too big for one leaf: the writer then
|
||||||
|
// builds internal nodes, whose child pointers carry record counts in
|
||||||
|
// widths derived from the node size. `check` reads every record through
|
||||||
|
// them and compares the count with the header's.
|
||||||
|
use clawhdf5::{AttrValue, FileBuilder};
|
||||||
|
const U: u64 = u64::MAX;
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut b = FileBuilder::new();
|
||||||
|
let x = b.create_dataset("x");
|
||||||
|
x.with_i32_data(&[7]);
|
||||||
|
for i in 0..70_000 {
|
||||||
|
x.set_attr(&format!("attr_{i}"), AttrValue::I64(i));
|
||||||
|
}
|
||||||
|
let mut g = b.create_group("g");
|
||||||
|
g.track_order(true);
|
||||||
|
for i in 0..100_000 {
|
||||||
|
g.add_hard_link(&format!("k{i}"), "/x");
|
||||||
|
}
|
||||||
|
b.add_group(g.finish());
|
||||||
|
let p = dir.path().join("deep.h5").to_string_lossy().into_owned();
|
||||||
|
b.write(&p).unwrap();
|
||||||
|
let o = h5rs(&["check", &p]);
|
||||||
|
assert_eq!(code(&o), 0, "{p}:\n{}", stdout(&o));
|
||||||
|
assert!(stdout(&o).contains("no problems found"), "{}", stdout(&o));
|
||||||
|
let mut depths = btree_v2_depths(&std::fs::read(&p).unwrap());
|
||||||
|
depths.sort();
|
||||||
|
assert_eq!(depths, [(5, 3), (6, 3), (8, 3)]);
|
||||||
|
|
||||||
|
let mut b = FileBuilder::new();
|
||||||
|
b.create_dataset("d")
|
||||||
|
.with_i32_data(&(0..200_000).collect::<Vec<i32>>())
|
||||||
|
.with_shape(&[400, 500])
|
||||||
|
.with_chunks(&[1, 1])
|
||||||
|
.with_maxshape(&[U, U]);
|
||||||
|
b.create_dataset("z")
|
||||||
|
.with_i32_data(&(0..70_000).collect::<Vec<i32>>())
|
||||||
|
.with_shape(&[70, 1000])
|
||||||
|
.with_chunks(&[1, 1])
|
||||||
|
.with_maxshape(&[U, U])
|
||||||
|
.with_deflate(1);
|
||||||
|
let p = dir.path().join("chunks.h5").to_string_lossy().into_owned();
|
||||||
|
b.write(&p).unwrap();
|
||||||
|
let o = h5rs(&["check", "--data", &p]);
|
||||||
|
assert_eq!(code(&o), 0, "{p}:\n{}", stdout(&o));
|
||||||
|
assert!(stdout(&o).contains("no problems found"), "{}", stdout(&o));
|
||||||
|
let mut depths = btree_v2_depths(&std::fs::read(&p).unwrap());
|
||||||
|
depths.sort();
|
||||||
|
assert_eq!(depths, [(10, 2), (11, 2)]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,9 +88,11 @@ impl FileBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track link creation order in every group that does not set its own
|
/// Track the creation order of links and attributes in every group, and
|
||||||
/// (`GroupBuilder::track_order`), as h5py's `track_order=True`: libhdf5
|
/// of attributes on every dataset, that does not set its own
|
||||||
/// then lists members in the order they were added.
|
/// (`GroupBuilder::track_order`, `DatasetBuilder::track_order`), as
|
||||||
|
/// h5py's `track_order=True`: libhdf5 then lists members and attributes
|
||||||
|
/// in the order they were added.
|
||||||
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||||
self.writer.track_order(track);
|
self.writer.track_order(track);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -559,20 +559,6 @@ fn we_write_btree_v2_for_several_unlimited_dims() {
|
|||||||
check_we_write(&cases);
|
check_we_write(&cases);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single-leaf B-tree has a 16-bit record count; beyond it the writer
|
|
||||||
/// refuses rather than writing a tree libhdf5 would misread.
|
|
||||||
#[test]
|
|
||||||
fn btree_v2_index_past_one_leaf_is_refused() {
|
|
||||||
let mut b = FileBuilder::new();
|
|
||||||
b.create_dataset("d")
|
|
||||||
.with_i32_data(&vec![0i32; 70_000])
|
|
||||||
.with_shape(&[70_000, 1])
|
|
||||||
.with_chunks(&[1, 1])
|
|
||||||
.with_maxshape(&[u64::MAX, u64::MAX]);
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
assert!(b.write(dir.path().join("too_many.h5")).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A maxshape equal to the shape cannot grow, so it needs no chunks: the
|
/// A maxshape equal to the shape cannot grow, so it needs no chunks: the
|
||||||
/// dataset stays contiguous (as h5py makes it) unless chunks are requested.
|
/// dataset stays contiguous (as h5py makes it) unless chunks are requested.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
//! Version-2 B-trees deeper than one leaf, as `FileBuilder` writes them for
|
||||||
|
//! big dense indexes: a group's links (name index, type 5, and creation
|
||||||
|
//! order index, type 6), an object's attributes (name index, type 8) and
|
||||||
|
//! the chunk index of a dataset with two unlimited dimensions (type 10 and,
|
||||||
|
//! with a filter, 11). Read back by h5py (libhdf5), h5dump and clawhdf5,
|
||||||
|
//! then modified by h5py in "r+" mode, which splits, merges and
|
||||||
|
//! redistributes the nodes the writer built.
|
||||||
|
//!
|
||||||
|
//! Skipped when python3 with h5py is unavailable, unless
|
||||||
|
//! `CLAWHDF5_REQUIRE_INTEROP=1`.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use clawhdf5::{AttrValue, File, FileBuilder};
|
||||||
|
|
||||||
|
fn python() -> String {
|
||||||
|
std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interop_required() -> bool {
|
||||||
|
std::env::var("CLAWHDF5_REQUIRE_INTEROP").is_ok_and(|v| v == "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn python_available() -> bool {
|
||||||
|
Command::new(python())
|
||||||
|
.args(["-c", "import h5py"])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! skip_if_no_python {
|
||||||
|
() => {
|
||||||
|
if !python_available() {
|
||||||
|
assert!(
|
||||||
|
!interop_required(),
|
||||||
|
"CLAWHDF5_REQUIRE_INTEROP=1 but python3 with h5py is not available"
|
||||||
|
);
|
||||||
|
eprintln!("SKIP: python3 with h5py not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `body` under h5py with `path` bound to the file's path.
|
||||||
|
fn h5py(path: &str, body: &str) -> String {
|
||||||
|
let script = format!("import h5py, numpy as np, json\npath = r'{path}'\n{body}");
|
||||||
|
let output = Command::new(python())
|
||||||
|
.args(["-c", &script])
|
||||||
|
.output()
|
||||||
|
.expect("failed to run python");
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!(
|
||||||
|
"Python script failed:\nSTDOUT: {}\nSTDERR: {}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// h5dump of `args` must succeed; returns its output.
|
||||||
|
fn h5dump(args: &[&str]) -> String {
|
||||||
|
let ok = Command::new("h5dump")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.is_ok_and(|o| o.status.success());
|
||||||
|
if !ok {
|
||||||
|
assert!(!interop_required(), "h5dump is not available");
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let o = Command::new("h5dump").args(args).output().unwrap();
|
||||||
|
let out = String::from_utf8_lossy(&o.stdout).to_string();
|
||||||
|
assert!(
|
||||||
|
o.status.success(),
|
||||||
|
"h5dump {args:?} failed:\n{out}{}",
|
||||||
|
String::from_utf8_lossy(&o.stderr)
|
||||||
|
);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 100 000 links in one group ----
|
||||||
|
|
||||||
|
const NLINKS: usize = 100_000;
|
||||||
|
|
||||||
|
/// The compact names: `k0`..`k99998` and `k155448`, whose name hash equals
|
||||||
|
/// that of `k69209` — a collision inside a many-level name index.
|
||||||
|
fn compact_name(i: usize) -> String {
|
||||||
|
if i == NLINKS - 1 {
|
||||||
|
"k155448".into()
|
||||||
|
} else {
|
||||||
|
format!("k{i}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The long names (111 bytes), added in a scrambled order so creation order
|
||||||
|
/// is not name order: the `i`th created is `long_name(scramble(i))`.
|
||||||
|
fn long_name(j: usize) -> String {
|
||||||
|
format!("link_{j:06}_{}", "x".repeat(100))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scramble(i: usize) -> usize {
|
||||||
|
i * 7919 % NLINKS
|
||||||
|
}
|
||||||
|
|
||||||
|
const PY_NAMES: &str = "\
|
||||||
|
N = 100000\n\
|
||||||
|
compact = ['k%d' % i for i in range(N - 1)] + ['k155448']\n\
|
||||||
|
def long_name(j): return 'link_%06d_' % j + 'x' * 100\n\
|
||||||
|
created = [long_name(i * 7919 % N) for i in range(N)]\n";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_hundred_thousand_links_in_one_group() {
|
||||||
|
skip_if_no_python!();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("links.h5").display().to_string();
|
||||||
|
let mut b = FileBuilder::new();
|
||||||
|
for v in 0..10 {
|
||||||
|
b.create_dataset(&format!("v{v}")).with_i32_data(&[v]);
|
||||||
|
}
|
||||||
|
// Name index only (type 5), 11-byte records: depth 3 in 512-byte nodes.
|
||||||
|
let mut g = b.create_group("compact");
|
||||||
|
for i in 0..NLINKS {
|
||||||
|
g.add_hard_link(&compact_name(i), &format!("/v{}", i % 10));
|
||||||
|
}
|
||||||
|
b.add_group(g.finish());
|
||||||
|
// Creation order tracked and indexed: a type-6 index as well.
|
||||||
|
let mut g = b.create_group("long");
|
||||||
|
g.track_order(true);
|
||||||
|
for i in 0..NLINKS {
|
||||||
|
let j = scramble(i);
|
||||||
|
g.add_hard_link(&long_name(j), &format!("/v{}", j % 10));
|
||||||
|
}
|
||||||
|
b.add_group(g.finish());
|
||||||
|
b.write(&path).unwrap();
|
||||||
|
|
||||||
|
let check = format!(
|
||||||
|
"{PY_NAMES}\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 g, l = f['compact'], f['long']\n\
|
||||||
|
\x20 out = [len(g), list(g) == sorted(compact), len(l), list(l) == created]\n\
|
||||||
|
\x20 out.append(all(int(g[compact[i]][0]) == i % 10 for i in range(0, N, 997)))\n\
|
||||||
|
\x20 out.append([int(g['k69209'][0]), int(g['k155448'][0]), 'k155448' in g, 'k100000' in g])\n\
|
||||||
|
\x20 out.append(all(int(l[long_name(j)][0]) == j % 10 for j in range(0, N, 1009)))\n\
|
||||||
|
\x20 out.append(h5py.h5o.get_info(f['v3'].id).rc)\n\
|
||||||
|
\x20 print(json.dumps(out))"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
h5py(&path, &check),
|
||||||
|
"[100000, true, 100000, true, true, [9, 9, true, false], true, 20001]"
|
||||||
|
);
|
||||||
|
let d = h5dump(&["-d", "/compact/k155448", &path]);
|
||||||
|
assert!(d.is_empty() || d.contains("(0): 9"), "{d}");
|
||||||
|
let d = h5dump(&["-d", &format!("/long/{}", long_name(99_999)), &path]);
|
||||||
|
assert!(d.is_empty() || d.contains("(0): 9"), "{d}");
|
||||||
|
|
||||||
|
// clawhdf5 reads both indexes back.
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
let g = f.group("compact").unwrap();
|
||||||
|
let mut names = g.datasets().unwrap();
|
||||||
|
names.sort();
|
||||||
|
let mut want: Vec<String> = (0..NLINKS).map(compact_name).collect();
|
||||||
|
want.sort();
|
||||||
|
assert_eq!(names, want);
|
||||||
|
assert_eq!(g.dataset("k155448").unwrap().read_i32().unwrap(), [9]);
|
||||||
|
let l = f.group("long").unwrap();
|
||||||
|
assert_eq!(l.datasets().unwrap().len(), NLINKS);
|
||||||
|
assert_eq!(
|
||||||
|
l.dataset(&long_name(12_345)).unwrap().read_i32().unwrap(),
|
||||||
|
[5]
|
||||||
|
);
|
||||||
|
drop(f);
|
||||||
|
|
||||||
|
// libhdf5 inserts into and removes from the trees we wrote.
|
||||||
|
let modify = format!(
|
||||||
|
"{PY_NAMES}\
|
||||||
|
with h5py.File(path, 'r+') as f:\n\
|
||||||
|
\x20 g, l = f['compact'], f['long']\n\
|
||||||
|
\x20 for i in range(3000):\n\
|
||||||
|
\x20 g['new%d' % i] = f['v1']\n\
|
||||||
|
\x20 for i in range(0, N, 7):\n\
|
||||||
|
\x20 del g[compact[i]]\n\
|
||||||
|
\x20 l['zz_new'] = f['v2']\n\
|
||||||
|
\x20 for j in range(0, N, 3):\n\
|
||||||
|
\x20 del l[long_name(j)]\n\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 g, l = f['compact'], f['long']\n\
|
||||||
|
\x20 left = sorted([n for i, n in enumerate(compact) if i % 7] + ['new%d' % i for i in range(3000)])\n\
|
||||||
|
\x20 kept = [n for n in created if int(n[5:11]) % 3] + ['zz_new']\n\
|
||||||
|
\x20 print(json.dumps([len(g), list(g) == left, int(g['new2999'][0]),\n\
|
||||||
|
\x20 int(g['k155448'][0]), len(l), list(l) == kept, int(l['zz_new'][0])]))"
|
||||||
|
);
|
||||||
|
assert_eq!(h5py(&path, &modify), "[88714, true, 1, 9, 66667, true, 2]");
|
||||||
|
h5dump(&["-d", "/compact/new0", &path]);
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
f.group("compact").unwrap().datasets().unwrap().len(),
|
||||||
|
88_714
|
||||||
|
);
|
||||||
|
assert_eq!(f.group("long").unwrap().datasets().unwrap().len(), 66_667);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 70 000 attributes on one object ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seventy_thousand_attributes_on_one_object() {
|
||||||
|
skip_if_no_python!();
|
||||||
|
const N: i64 = 70_000;
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("attrs.h5").display().to_string();
|
||||||
|
let mut b = FileBuilder::new();
|
||||||
|
let x = b.create_dataset("x");
|
||||||
|
x.with_i32_data(&[1]);
|
||||||
|
for i in 0..N {
|
||||||
|
x.set_attr(&format!("attr_{i}"), AttrValue::I64(i * 3));
|
||||||
|
}
|
||||||
|
let mut g = b.create_group("g");
|
||||||
|
for i in 0..N {
|
||||||
|
g.set_attr(&format!("s{i:05}"), AttrValue::String(format!("value {i}")));
|
||||||
|
}
|
||||||
|
b.add_group(g.finish());
|
||||||
|
b.write(&path).unwrap();
|
||||||
|
|
||||||
|
let check = "\
|
||||||
|
N = 70000\n\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 a, s = f['x'].attrs, f['g'].attrs\n\
|
||||||
|
\x20 names = list(a)\n\
|
||||||
|
\x20 out = [len(a), names == sorted('attr_%d' % i for i in range(N))]\n\
|
||||||
|
\x20 out.append(all(int(a['attr_%d' % i]) == 3 * i for i in range(0, N, 331)))\n\
|
||||||
|
\x20 out.append(len(s))\n\
|
||||||
|
\x20 v = dict(s.items())\n\
|
||||||
|
\x20 out.append(all(v['s%05d' % i].decode() == 'value %d' % i for i in range(N)))\n\
|
||||||
|
\x20 print(json.dumps(out))";
|
||||||
|
assert_eq!(h5py(&path, check), "[70000, true, true, 70000, true]");
|
||||||
|
let d = h5dump(&["-a", "/x/attr_69999", &path]);
|
||||||
|
assert!(d.is_empty() || d.contains("(0): 209997"), "{d}");
|
||||||
|
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
let attrs = f.dataset("x").unwrap().attrs().unwrap();
|
||||||
|
assert_eq!(attrs.len(), N as usize);
|
||||||
|
for i in [0, 1, 35_000, N - 1] {
|
||||||
|
assert!(
|
||||||
|
matches!(attrs[&format!("attr_{i}")], AttrValue::I64(v) if v == 3 * i),
|
||||||
|
"attr_{i}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let attrs = f.group("g").unwrap().attrs().unwrap();
|
||||||
|
assert_eq!(attrs.len(), N as usize);
|
||||||
|
drop(f);
|
||||||
|
|
||||||
|
let modify = "\
|
||||||
|
N = 70000\n\
|
||||||
|
with h5py.File(path, 'r+') as f:\n\
|
||||||
|
\x20 a = f['x'].attrs\n\
|
||||||
|
\x20 for i in range(2000):\n\
|
||||||
|
\x20 a['new_%d' % i] = i\n\
|
||||||
|
\x20 for i in range(0, N, 5):\n\
|
||||||
|
\x20 del a['attr_%d' % i]\n\
|
||||||
|
\x20 f['g'].attrs['s00000'] = 'changed'\n\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 a = f['x'].attrs\n\
|
||||||
|
\x20 want = sorted(['attr_%d' % i for i in range(N) if i % 5] + ['new_%d' % i for i in range(2000)])\n\
|
||||||
|
\x20 print(json.dumps([len(a), list(a) == want, int(a['attr_69999']), int(a['new_1999']),\n\
|
||||||
|
\x20 'attr_5' in a, f['g'].attrs['s00000'], len(f['g'].attrs)]))";
|
||||||
|
assert_eq!(
|
||||||
|
h5py(&path, modify),
|
||||||
|
r#"[58000, true, 209997, 1999, false, "changed", 70000]"#
|
||||||
|
);
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
assert_eq!(f.dataset("x").unwrap().attrs().unwrap().len(), 58_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 200 000 chunks with two unlimited dimensions ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_hundred_thousand_chunks_with_two_unlimited_dims() {
|
||||||
|
skip_if_no_python!();
|
||||||
|
const U: u64 = u64::MAX;
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("chunks.h5").display().to_string();
|
||||||
|
let data: Vec<i32> = (0..200_000).collect();
|
||||||
|
let small: Vec<i32> = (0..80_000).map(|v| v * 2).collect();
|
||||||
|
let mut b = FileBuilder::new();
|
||||||
|
// Type 10 (unfiltered): 24-byte records, depth 2 in 2048-byte nodes.
|
||||||
|
b.create_dataset("d")
|
||||||
|
.with_i32_data(&data)
|
||||||
|
.with_shape(&[400, 500])
|
||||||
|
.with_chunks(&[1, 1])
|
||||||
|
.with_maxshape(&[U, U]);
|
||||||
|
// Type 11 (filtered): each record also holds a size and filter mask.
|
||||||
|
b.create_dataset("z")
|
||||||
|
.with_i32_data(&small)
|
||||||
|
.with_shape(&[200, 400])
|
||||||
|
.with_chunks(&[1, 1])
|
||||||
|
.with_maxshape(&[U, U])
|
||||||
|
.with_deflate(1);
|
||||||
|
b.write(&path).unwrap();
|
||||||
|
|
||||||
|
let check = "\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 d, z = f['d'], f['z']\n\
|
||||||
|
\x20 print(json.dumps([d.shape, d.chunks, d.id.get_num_chunks(),\n\
|
||||||
|
\x20 bool(np.array_equal(d[()], np.arange(200000).reshape(400, 500))),\n\
|
||||||
|
\x20 int(d[399, 499]), z.id.get_num_chunks(),\n\
|
||||||
|
\x20 bool(np.array_equal(z[()], 2 * np.arange(80000).reshape(200, 400)))]))";
|
||||||
|
assert_eq!(
|
||||||
|
h5py(&path, check),
|
||||||
|
"[[400, 500], [1, 1], 200000, true, 199999, 80000, true]"
|
||||||
|
);
|
||||||
|
let d = h5dump(&["-d", "/d", "-s", "399,498", "-c", "1,2", &path]);
|
||||||
|
assert!(d.is_empty() || d.contains("199998, 199999"), "{d}");
|
||||||
|
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
assert_eq!(f.dataset("d").unwrap().read_i32().unwrap(), data);
|
||||||
|
assert_eq!(f.dataset("z").unwrap().read_i32().unwrap(), small);
|
||||||
|
drop(f);
|
||||||
|
|
||||||
|
// libhdf5 adds chunks to both trees.
|
||||||
|
let modify = "\
|
||||||
|
with h5py.File(path, 'r+') as f:\n\
|
||||||
|
\x20 d, z = f['d'], f['z']\n\
|
||||||
|
\x20 d.resize((401, 510))\n\
|
||||||
|
\x20 d[400, :] = -1\n\
|
||||||
|
\x20 d[:, 500:] = -2\n\
|
||||||
|
\x20 z.resize((201, 400))\n\
|
||||||
|
\x20 z[200, :] = 7\n\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 d, z = f['d'][()], f['z'][()]\n\
|
||||||
|
\x20 print(json.dumps([f['d'].id.get_num_chunks(),\n\
|
||||||
|
\x20 bool(np.array_equal(d[:400, :500], np.arange(200000).reshape(400, 500))),\n\
|
||||||
|
\x20 int(d[400, 3]), int(d[5, 505]), f['z'].id.get_num_chunks(),\n\
|
||||||
|
\x20 bool(np.array_equal(z[:200], 2 * np.arange(80000).reshape(200, 400))), int(z[200, 9])]))";
|
||||||
|
assert_eq!(
|
||||||
|
h5py(&path, modify),
|
||||||
|
"[204510, true, -1, -2, 80400, true, 7]"
|
||||||
|
);
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
let d = f.dataset("d").unwrap().read_i32().unwrap();
|
||||||
|
assert_eq!(d.len(), 401 * 510);
|
||||||
|
assert_eq!(d[499], 499);
|
||||||
|
assert_eq!(d[400 * 510 + 3], -1);
|
||||||
|
}
|
||||||
@@ -532,29 +532,94 @@ fn ten_thousand_links_in_one_group() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn more_links_than_one_index_leaf_holds_is_an_error() {
|
fn track_order_lists_attributes_in_creation_order() {
|
||||||
|
skip_if_no_python!();
|
||||||
|
// h5py's track_order=True orders an object's attributes as well as a
|
||||||
|
// group's links; the writer tracked links only, so h5py listed the
|
||||||
|
// attributes by name. Now the object header's flags say attribute
|
||||||
|
// creation order is tracked and indexed, an Attribute Info message
|
||||||
|
// holds the next order, inline attributes carry theirs, and dense
|
||||||
|
// storage gets a creation-order index (B-tree type 9).
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let small = ["zeta", "alpha", "mid"];
|
||||||
let mut b = FileBuilder::new();
|
let mut b = FileBuilder::new();
|
||||||
for i in 0..70_000 {
|
b.track_order(true); // the root, and every group and dataset by default
|
||||||
b.add_soft_link(&format!("s{i}"), "/x");
|
for (i, n) in small.iter().enumerate() {
|
||||||
|
b.set_attr(n, AttrValue::I64(i as i64));
|
||||||
}
|
}
|
||||||
let err = b.finish().unwrap_err().to_string();
|
let mut g = b.create_group("g"); // dense: 30 attributes
|
||||||
assert!(
|
for i in (0..30).rev() {
|
||||||
err.contains("70000 links in one group: at most 65535"),
|
g.set_attr(&format!("a{i:02}"), AttrValue::I64(i));
|
||||||
"{err}"
|
|
||||||
);
|
|
||||||
// Dense attributes have the same one-leaf index. Their count used to
|
|
||||||
// be written modulo 65 536.
|
|
||||||
let mut b = FileBuilder::new();
|
|
||||||
let x = b.create_dataset("x");
|
|
||||||
x.with_i32_data(&[1]);
|
|
||||||
for i in 0..70_000 {
|
|
||||||
x.set_attr(&format!("a{i}"), AttrValue::I64(i));
|
|
||||||
}
|
}
|
||||||
let err = b.finish().unwrap_err().to_string();
|
b.add_group(g.finish());
|
||||||
assert!(
|
let d = b.create_dataset("d");
|
||||||
err.contains("70000 attributes on one object: at most 65535"),
|
d.with_i32_data(&[1]);
|
||||||
"{err}"
|
for (i, n) in small.iter().enumerate() {
|
||||||
|
d.set_attr(n, AttrValue::I64(i as i64));
|
||||||
|
}
|
||||||
|
// 20 000 attributes: a one-leaf creation-order index of 20 000 records.
|
||||||
|
let big = b.create_dataset("big");
|
||||||
|
big.with_i32_data(&[2]);
|
||||||
|
for i in (0..20_000).rev() {
|
||||||
|
big.set_attr(&format!("b{i:05}"), AttrValue::I64(i));
|
||||||
|
}
|
||||||
|
let plain = b.create_dataset("plain");
|
||||||
|
plain.with_i32_data(&[3]).track_order(false);
|
||||||
|
for (i, n) in small.iter().enumerate() {
|
||||||
|
plain.set_attr(n, AttrValue::I64(i as i64));
|
||||||
|
}
|
||||||
|
let path = write(&dir, "attr_order.h5", b);
|
||||||
|
|
||||||
|
let out = h5py(
|
||||||
|
&path,
|
||||||
|
"def order(o):\n\
|
||||||
|
\x20 return o.id.get_create_plist().get_attr_creation_order()\n\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 big = list(f['big'].attrs)\n\
|
||||||
|
\x20 print(json.dumps([list(f.attrs), [int(v) for v in f.attrs.values()],\n\
|
||||||
|
\x20 list(f['g'].attrs)[:3], len(f['g'].attrs), list(f['d'].attrs),\n\
|
||||||
|
\x20 big[:2], big == ['b%05d' % i for i in range(19999, -1, -1)],\n\
|
||||||
|
\x20 int(f['big'].attrs['b00007']), list(f['plain'].attrs),\n\
|
||||||
|
\x20 [order(f['/']), order(f['g']), order(f['d']), order(f['plain'])]]))",
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out,
|
||||||
|
r#"[["zeta", "alpha", "mid"], [0, 1, 2], ["a29", "a28", "a27"], 30, ["zeta", "alpha", "mid"], ["b19999", "b19998"], true, 7, ["alpha", "mid", "zeta"], [3, 3, 3, 0]]"#
|
||||||
|
);
|
||||||
|
h5dump_ok(&path);
|
||||||
|
let f = File::open(&path).unwrap();
|
||||||
|
assert_eq!(f.dataset("big").unwrap().attrs().unwrap().len(), 20_000);
|
||||||
|
assert!(matches!(
|
||||||
|
f.group("g").unwrap().attrs().unwrap()["a07"],
|
||||||
|
AttrValue::I64(7)
|
||||||
|
));
|
||||||
|
drop(f);
|
||||||
|
|
||||||
|
// libhdf5 continues the numbering: new attributes come last, also when
|
||||||
|
// it moves the inline ones of `d` to dense storage.
|
||||||
|
let out = h5py(
|
||||||
|
&path,
|
||||||
|
"with h5py.File(path, 'r+') as f:\n\
|
||||||
|
\x20 f.attrs['new'] = 9\n\
|
||||||
|
\x20 del f.attrs['alpha']\n\
|
||||||
|
\x20 f['g'].attrs['new'] = 9\n\
|
||||||
|
\x20 del f['g'].attrs['a15']\n\
|
||||||
|
\x20 for i in range(8):\n\
|
||||||
|
\x20 f['d'].attrs['x%d' % i] = i\n\
|
||||||
|
\x20 f['big'].attrs['new'] = 9\n\
|
||||||
|
\x20 for i in range(0, 20000, 2):\n\
|
||||||
|
\x20 del f['big'].attrs['b%05d' % i]\n\
|
||||||
|
with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 big = list(f['big'].attrs)\n\
|
||||||
|
\x20 print(json.dumps([list(f.attrs), list(f['g'].attrs)[-2:], len(f['g'].attrs),\n\
|
||||||
|
\x20 list(f['d'].attrs)[:4], len(f['d'].attrs),\n\
|
||||||
|
\x20 big == ['b%05d' % i for i in range(19999, -1, -2)] + ['new']]))",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out,
|
||||||
|
r#"[["zeta", "mid", "new"], ["a00", "new"], 30, ["zeta", "alpha", "mid", "x0"], 11, true]"#
|
||||||
|
);
|
||||||
|
h5dump_ok(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -627,6 +692,49 @@ fn non_ascii_names_are_utf8() {
|
|||||||
assert_eq!(f.dataset("größe/wert").unwrap().read_i32().unwrap(), [1]);
|
assert_eq!(f.dataset("größe/wert").unwrap().read_i32().unwrap(), [1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn names_whose_hashes_collide_are_found_by_name() {
|
||||||
|
skip_if_no_python!();
|
||||||
|
// "k69209" and "k155448" have the same lookup3 hash (0x3a0b13e6). The
|
||||||
|
// dense name indexes (links: type 5, attributes: type 8) are ordered by
|
||||||
|
// hash and then by name, and libhdf5's lookup relies on it. The writer
|
||||||
|
// broke ties by insertion order, so with "k69209" added first libhdf5
|
||||||
|
// could not open "k155448" by name.
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut b = FileBuilder::new();
|
||||||
|
let mut g = b.create_group("g");
|
||||||
|
for (i, n) in ["k69209", "k155448"]
|
||||||
|
.into_iter()
|
||||||
|
.chain((0..10).map(|_| ""))
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let name = if n.is_empty() {
|
||||||
|
format!("d{i}")
|
||||||
|
} else {
|
||||||
|
n.into()
|
||||||
|
};
|
||||||
|
g.create_dataset(&name).with_i32_data(&[i as i32]);
|
||||||
|
}
|
||||||
|
b.add_group(g.finish());
|
||||||
|
let x = b.create_dataset("x");
|
||||||
|
x.with_i32_data(&[0]);
|
||||||
|
x.set_attr("k69209", AttrValue::I64(1));
|
||||||
|
x.set_attr("k155448", AttrValue::I64(2));
|
||||||
|
for i in 0..10 {
|
||||||
|
x.set_attr(&format!("a{i}"), AttrValue::I64(10 + i));
|
||||||
|
}
|
||||||
|
let path = write(&dir, "collide.h5", b);
|
||||||
|
let out = h5py(
|
||||||
|
&path,
|
||||||
|
"with h5py.File(path, 'r') as f:\n\
|
||||||
|
\x20 g, a = f['g'], f['x'].attrs\n\
|
||||||
|
\x20 print(json.dumps([int(g['k69209'][0]), int(g['k155448'][0]), 'k155448' in g,\n\
|
||||||
|
\x20 int(a['k69209']), int(a['k155448']), 'k155448' in a]))",
|
||||||
|
);
|
||||||
|
assert_eq!(out, "[0, 1, true, 1, 2, true]");
|
||||||
|
h5dump_ok(&path);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_group_attribute_set_again_takes_the_new_value() {
|
fn a_group_attribute_set_again_takes_the_new_value() {
|
||||||
skip_if_no_python!();
|
skip_if_no_python!();
|
||||||
|
|||||||
+30
-6
@@ -275,10 +275,29 @@ fill-value item that did is fixed).
|
|||||||
external links at any depth and optional creation-order tracking;
|
external links at any depth and optional creation-order tracking;
|
||||||
h5py, h5dump and `h5rs check --data` read them
|
h5py, h5dump and `h5rs check --data` read them
|
||||||
(`crates/clawhdf5/tests/writer_groups_interop.rs`,
|
(`crates/clawhdf5/tests/writer_groups_interop.rs`,
|
||||||
`crates/clawhdf5-tools/tests/h5rs_interop.rs`). Still missing: a group
|
`crates/clawhdf5-tools/tests/h5rs_interop.rs`). ~~Still missing:
|
||||||
with more than 65 535 links, or an object with more than 65 535 dense
|
attribute creation order is not tracked.~~ **Fixed 2026-09-26:**
|
||||||
attributes, is an error (the index is one B-tree leaf), and attribute
|
`track_order` (file default, `GroupBuilder`, and the new
|
||||||
creation order is not tracked.
|
`DatasetBuilder::track_order`) tracks and indexes attribute creation
|
||||||
|
order as h5py's `track_order=True` does; h5py lists the attributes in
|
||||||
|
the order they were set, inline and dense (20 000 on one dataset), and
|
||||||
|
keeps numbering them in "r+" mode (tank,
|
||||||
|
`cargo test -p clawhdf5 --test writer_groups_interop
|
||||||
|
track_order_lists_attributes_in_creation_order`). libhdf5 numbers at
|
||||||
|
most 65 535 attributes on such an object, so more is an error.
|
||||||
|
- ~~A group with more than 65 535 links, or an object with more than
|
||||||
|
65 535 dense attributes, is an error (the index is one B-tree leaf).~~
|
||||||
|
**Fixed 2026-09-26:** the dense indexes are v2 B-trees of any depth
|
||||||
|
(libhdf5's 512-byte nodes once the records outgrow the one-leaf layout,
|
||||||
|
which smaller indexes keep byte for byte). Tested on tank with 100 000
|
||||||
|
links in one group (short names, and 111-byte names with creation order
|
||||||
|
tracked) and 70 000 attributes on one object: h5py lists them in order
|
||||||
|
and reads the values, h5dump reads spot checks, `h5rs check` reads every
|
||||||
|
record, and h5py in "r+" mode adds and deletes thousands of links and
|
||||||
|
attributes in those trees (`cargo test -p clawhdf5 --test
|
||||||
|
deep_btree_interop`; `cargo test -p clawhdf5-tools --test h5rs_interop
|
||||||
|
check_files_with_deep_btrees`). The name indexes are ordered by hash and
|
||||||
|
then name, as libhdf5 needs for names whose hashes collide.
|
||||||
- ~~Dense link or attribute storage past 512 KiB of messages was written
|
- ~~Dense link or attribute storage past 512 KiB of messages was written
|
||||||
unreadable (child indirect blocks of the fractal heap written as direct
|
unreadable (child indirect blocks of the fractal heap written as direct
|
||||||
blocks).~~ **Fixed 2026-09-26** (it affected 2.7.0 too): tested with
|
blocks).~~ **Fixed 2026-09-26** (it affected 2.7.0 too): tested with
|
||||||
@@ -291,8 +310,13 @@ fill-value item that did is fixed).
|
|||||||
an object, or more than 8 links in a group) one attribute or link
|
an object, or more than 8 links in a group) one attribute or link
|
||||||
message over 65 515 bytes is an error.
|
message over 65 515 bytes is an error.
|
||||||
- Output that HDF5 1.8 can read.
|
- Output that HDF5 1.8 can read.
|
||||||
- A B-tree v2 chunk index larger than one leaf, so datasets with several
|
- ~~A B-tree v2 chunk index larger than one leaf, so datasets with
|
||||||
unlimited dimensions are limited to 65 535 chunks.
|
several unlimited dimensions are limited to 65 535 chunks.~~ **Fixed
|
||||||
|
2026-09-26:** more chunks get libhdf5's 2048-byte nodes with internal
|
||||||
|
nodes above the leaves. Tested on tank with 200 000 chunks (and 80 000
|
||||||
|
deflated): h5py and clawhdf5 read every value, and h5py resizes the
|
||||||
|
dataset and writes 4 510 new chunks into the tree (same commands as
|
||||||
|
above).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user