A chunk B-tree node sized for the one chunk it held. That single defect meant every chunked dataset OxiH5’s writer had ever produced was unreadable by libhdf5 — and it’s just one of nine correctness bugs 0.2.1 fixes, alongside five major new write-path features.
Today we released OxiH5 0.2.1 — a release that adds DEFLATE/gzip compression on write, genuine per-chunk tiling for unlimited-dimension datasets, non-destructive in-place dataset overwrite, groups nested to any depth, and attributes on groups and arrays — while fixing nine correctness defects across the write and read paths, several of them severe enough to make previously-written files unreadable by the reference implementation.
No libhdf5. No FFI. No -sys crates. OxiH5 parses and now writes the real HDF5 binary format — superblock, object headers, B-trees, heaps, filter pipelines, all eleven datatype classes — using nothing but std. #![forbid(unsafe_code)] guards oxih5-core; the facade crate carries exactly one documented unsafe block, the mmap call behind open_mmap. It compiles to a single static binary, needs no system libraries and no C toolchain at build time, and stays WASM-friendly by construction.
Why OxiH5 0.2.1 is a game changer
0.2.0 hardened the reader against real-world superblocks. 0.2.1’s audit turned to the write path — and to library-wide assumptions about how many links a group can hold — and found defects that had been there since the writer’s first line:
- Every chunked dataset the writer had ever produced was unreadable by libhdf5. A chunk B-tree node was sized for the one chunk it held — 80 bytes for a 1-D dataset — but libhdf5 computes the node image size it expects before it has seen the node, from a fixed superblock-derived constant. It asked for 2096 bytes, got 80, and refused the file outright.
- A 2-D unlimited variable read back mostly zeros.
create_dataset_unlimitedstored the whole dataset as one physical chunk regardless of the declaredchunk_shape— so a[5, 3]variable tiled as[5, 1]on paper while all fifteen values sat in one chunk on disk, and every reader (this one included) clamped to the declared volume and silently dropped the rest. - Groups with 9 or more links were unreadable by libhdf5. The writer emitted one oversized symbol-table node per group while the superblock declared libhdf5’s real default fan-out — so the 9th entry ran the reference parser off the end of its own buffer.
- Datasets declared out of name order vanished — without an error. Symbol-table lookup binary-searches by name; an unsorted node doesn’t fail loudly, it just hides the link that’s out of place.
- A dataset could silently shadow a group of the same name, producing two links the format cannot represent.
- Dangling object references quietly became the undefined-address sentinel instead of a typed error — so a misspelled
DIMENSION_LISTtarget failed, if ever, at dereference time. - Soft links inside h5py’s default (old-style) groups didn’t resolve at all —
Corrupted("object header offset 18446744073709551615 too large")on an ordinary file, because the old-style symbol-table entry encodes a soft link completely differently from a new-style Link message. - HDF5 layout message version 4 — what
libver='latest'actually writes — was entirely unsupported. Every dataset in such a file failed outright, and the four different chunk-indexing schemes v4 can point at (single chunk, implicit, fixed array, extensible array, v2 B-tree) each needed their own decoder.
OxiH5 0.2.1 ends all of that — and adds the write-side features those fixes unlocked:
- DEFLATE compression on write via
set_deflate(path, level), zlib levels 0–9, using the Pure-Rustoxiarc-deflate— sooxih5gains no new dependency. - Real multi-chunk tiling.
create_dataset_unlimitednow actually cuts a dataset intoceil(shape[d] / chunk_shape[d])chunks per dimension, each independently compressed and keyed by its own B-tree entry — verified at ragged 1-D, 2-D, and 3-D shapes, and at two- and three-level chunk indices (up to 8500 chunks). - In-place dataset overwrite via
write_dataset_in_place, for patching a file oxih5 didn’t author — a MATLAB v7.3.matfile, for instance — without touching the attributes, object references, or#refs#group that rebuilding fromFileWriter’s model would destroy. - Nested groups at any depth, with automatic intermediate-group creation matching h5py’s
create_intermediate_group=True. - Attributes on groups at any depth, plus new array-valued (
i64[]/f64[]/string) attributes — previously only scalars were representable.
Technical Deep Dive: how the write-path and layout-v4 fixes fit together
- Chunk B-tree node width —
oxih5-format. Nodes are now emitted at the full width libhdf5 reads before it has seen their contents (2096 bytes for 1-D, 2616 for 2-D, 3136 for 3-D, plus a fixed increment per index level past 64 chunks), with unused slots zero-filled and the terminal key carrying the real dataset extent instead of being left at zero. - The compression pipeline runs during layout, not emit. A compressed dataset’s byte length isn’t derivable from anything the caller supplied, and the chunk B-tree key has to record it before any byte is written — so a new
write/payload.rsownsdata_size()for both passes to agree on, and a newwrite/pipeline.rsencodes the filter-pipeline message (0x000B). - Group symbol tables are chunked and B-tree indexed. Links are now split into 328-byte SNOD nodes — the width libhdf5’s default
leaf_node_K = 4actually implies — indexed by a group B-tree that grows a level once 32 nodes fill, verified at 0, 1, 8, 9, 32, 255, 256, 257, 300, and 1000 root links. - One resolver for both group styles. Rather than teach the old-style symbol-table path about soft links as a special case,
GroupRef+resolve_path_targetnow walk a path through groups of either style identically — so a soft link resolves the same way whichever style holds it, including a value that’s relative to the group holding the link rather than to the file root. - The layout-v4 decoder was rewritten against the specification, not against the fixed-array files it had been empirically derived from: chunk dimensions are now read at their declared encoded width instead of assumed to be one byte, and the indexing-type information block preceding the index address is sized per index type (0 bytes for implicit, 12 for a filtered single chunk, 1/5/6 for fixed-array/extensible-array/v2-B-tree) instead of assumed uniform — which is what made extensible-array and B-tree-v2 datasets read nonsense addresses before.
Getting Started
cargo add oxih5
use oxih5::FileWriter;
let path = std::env::temp_dir().join("out.h5");
// New in 0.2.1: nested groups, DEFLATE compression, and a tiled unlimited dataset.
let mut w = FileWriter::new();
w.write_dataset_f32("/sensors/lab1/signal", &[1.0f32, 2.0, 3.0, 4.0], &[4])?;
w.set_deflate("/sensors/lab1/signal", 6)?;
w.write_string_attr("/sensors/lab1", "units", "volts")?;
w.build(&path)?;
// New in 0.2.1: patch an existing file's dataset bytes in place —
// e.g. a MATLAB .mat file — without touching anything else in it.
let bytes: Vec<u8> = [9.0f64, 9.0, 9.0].iter().flat_map(|v| v.to_le_bytes()).collect();
oxih5::write_dataset_in_place(&path.with_extension("mat"), "/results/values", &bytes).ok();
# Ok::<(), oxih5::OxiH5Error>(())
What’s New in 0.2.1
- DEFLATE compression on write —
FileWriter::set_deflate(path, level), converting contiguous storage to chunked as needed; refuses variable-length string datasets with a typed error rather than writing a file only this library could read back. - Genuine multi-chunk tiling for
create_dataset_unlimited, with full-size overhang chunks padded by the fill value, matching what libhdf5 itself stores. write_dataset_in_place— non-destructive raw-byte overwrite of an existing contiguous dataset, plus ten typed element-width wrappers anddataset_data_extent()to size a buffer or check overwritability up front.- Nested groups at any path depth, with auto-created intermediate groups; attributes on groups at any depth; array-valued attributes (
i64[]/f64[]/string). - Fixed: the chunk B-tree node sizing defect that made every chunked dataset unreadable by libhdf5; 2-D unlimited variables reading back mostly zeros; groups with 9+ links being unreadable; out-of-order-declared links vanishing silently; a dataset able to shadow a same-named group; dangling object references becoming a silent sentinel instead of an error; soft links inside old-style (default-
libver) groups not resolving; HDF5 layout message version 4, including single-chunk, implicit, fixed-array, extensible-array-location, and v2-B-tree chunk indexing. - Known limitation: an extensible array’s chunk records (as opposed to its index location) aren’t decoded yet — reading such a dataset now fails with a typed
NotImplementedinstead of a nonsense address or silent zeros. - 682 tests pass (
--all-features; 661 with default features) across all four crates (~27.9k SLOC).
Tips
- If you wrote any chunked dataset, or any group with 9+ links, with oxih5 before 0.2.1 — re-verify it opens in libhdf5/h5py. oxih5 itself could always read its own output back; the defect was specifically in what got written to disk. Regenerate those files with 0.2.1.
set_deflateis a net size loss below roughly 8 KB of data. The chunk-index overhead (2+ KB) outweighs what gzip saves on a small array — reach for it on bulk arrays, not coordinate variables.- Prefer
write_dataset_in_placeoverFileWriterwhen patching a file oxih5 didn’t author. Rebuilding viaFileWriterreconstructs the file from its own in-memory model and silently drops anything that model doesn’t represent —MATLAB_classattributes and the#refs#group on a.matfile, for instance. - You don’t need
create_groupbefore writing into a nested path.write_dataset_f64("/a/b/x", …)createsaanda/bautomatically, matching h5py’screate_intermediate_group=True;create_groupis only for a group that should hold nothing yet. - If you previously worked around soft links failing on h5py-default files, drop that workaround. Old-style (default-
libver) groups now resolve soft links identically to new-style ones, including relative link values. - A chunked dataset indexed by an extensible array (one unlimited dimension,
libver='latest') still isn’t fully readable. It now fails loudly withNotImplementednaming the index type — treat that as “not yet”, not as a silent correctness bug.
This is the foundation
OxiH5 belongs to NoFFI — the COOLJAPAN initiative retiring every C/C++/Fortran/-sys FFI dependency in the Rust ecosystem with a clean, memory-safe, Pure-Rust implementation. It already has real consumers pinned at 0.2.1 across the workspace:
oxinetcdf— ships in the same workspace, and its NetCDF-4 write path benefits directly from the tiling and group fixes in this release.- OxiGeo (the project formerly known as OxiGDAL) — depends on both
oxih5andoxinetcdfat 0.2.1 for its HDF5/NetCDF raster paths. - OxiProj — depends on
oxih50.2.1 for coordinate-reference-system data. - SciRS2 —
scirs2-ioandscirs2-datasetsdepend onoxih5-core/oxih5-format0.2.1, staged behind an optionalhdf5_iofeature alongside the Chdf5crate until OxiH5’s write support covers enough of their needs to drop it.
Repository: https://github.com/cool-japan/oxih5
Star the repo ⭐ if you want an HDF5 writer that doesn’t just accept your data — it produces files the reference implementation can actually open.
The era of a Pure-Rust writer whose own output only its own reader could open is over. Pure Rust HDF5 is here — and as of 0.2.1, libhdf5 agrees.
— KitaSan at COOLJAPAN OÜ July 21, 2026