An archive format that only your own reader can open isn’t really an archive format — it’s a private serialization scheme that happens to look like one.
Today we released OxiArc 0.3.5 — a LZH/LHA interoperability release. The -lh4-/-lh5-/-lh6-/-lh7- codec is rewritten from OxiArc’s own private, non-canonical bitstream format to genuine canonical LHA wire format, closing the interoperability gap left open by the 0.3.4 hardening pass (which covered LZMA, bzip2, 7z, ZIP, TAR, and XZ). It’s validated bidirectionally against a corpus of real third-party .lzh archives and a live lha (Lhasa) CLI oracle.
No C, no Fortran, no zlib, no libarchive, no external shared libraries. Just clean, memory-safe, blazing-fast archiving and compression that compiles to a single static binary, targets WASM, and runs everywhere.
OxiArc is the Pure Rust replacement for the zip, tar, gzip, zstd, and 7-zip tools — and for the Rust crates zip, flate2, zstd, bzip2, lz4, tar, snap, brotli, and miniz_oxide.
Why 0.3.5 closes the gap 0.3.4 left open
0.3.4 fixed six codecs — LZMA, bzip2, 7z, ZIP, TAR, XZ — by validating each one bidirectionally against its reference implementation instead of trusting only round-trip tests against itself. LZH’s -lh4- through -lh7- methods were the one holdout: they round-tripped perfectly through OxiArc’s own reader, so nothing in the self-consistency test suite ever complained. It took real third-party .lzh files, and a live lha CLI, to reveal that the bitstream itself was OxiArc’s own invention rather than the LHA spec’s.
- LZH/LHA now speaks genuine canonical wire format. Four independent deviations from canonical LHA — bit order, the block command-count field’s meaning, code-table length encoding, and offset-table field widths — are fixed together across the encoder, decoder, and streaming paths.
- Validated against real fixtures, not just itself. 6 genuine third-party
.lzhfixtures spanning header levels 0/1/2 (including a 1.24 MB multi-block archive), plus a new opt-inlha-oraclefeature that shells out to a reallha(Lhasa) CLI for round-trip verification. - A parallel-path header bug is fixed too.
parallel::lzh_compress_parallel’s level-1 header builder undersized its header-length byte by 5 bytes — the archives it produced were internally self-consistent, but reallhareported them as containing zero entries. - Three unrelated hardening fixes ride along: a Miri-flagged undefined-behavior class in the CRC-32/CRC-64 fast paths, resource leaks in three archive writers’
into_inner(), and a CLI exit-code bug that reported success for unreadable archives.
Technical Deep Dive
- MSB-first bit I/O (
oxiarc-core::msb_bitstream). The old encoder packed bits LSB-first and bit-reversed every Huffman code to fake MSB semantics; canonical LHA is natively MSB-first. A newMsbBitReader/MsbBitWritermodule — mirroring LHA’s owngetbits/putbits, including zero-bit padding past end-of-input — replaces the reversal hack and is re-exported at the crate root and inpreludefor anyone else implementing an MSB-first format. - Block command-count semantics (
oxiarc-lzhuf). The 16-bit per-block field was treated as a byte count; canonically it counts commands (literals or copies), so a copy-heavy block can cover far more output bytes than its count suggests. Getting this wrong desynchronizes decoding partway through any block with enough back-references. - Code-table length encoding. Temp-tree symbol values were mapped to code lengths as
v - 3, leaving an always-unusedv == 3slot; canonical LHA usesv - 2, with a zero-run skip mechanism that is independent of — not layered onto — the temp table’s own index-2 skip-count field. - Method-dependent offset-table field widths. The offset (P-tree) code-count field and history-buffer size vary by method: 4 bits / 16 KiB for
-lh4-/-lh5-, 5 bits / 64 KiB for-lh6-, 5 bits / 128 KiB for-lh7-. These are now centralized inLzhMethod::offset_bits/history_bits/max_offset_codesinstead of being hand-coded per call site. - Miri-clean CRC fast paths (
oxiarc-core). The scalar slice-by-8 loop, the x86 PCLMULQDQ fold, and the aarch64 PMULL fold all computedptr.add(n)speculatively just to compare it againstend— technically undefined behavior the moment the result lands more than one byte past the allocation, even though the pointer was never dereferenced. All 7 call sites now use address subtraction instead; no behavioral or performance change.
Getting Started
CLI:
cargo install oxiarc-cli
Library:
cargo add oxiarc-archive oxiarc-lzhuf
The fixes are wire-format corrections, not API changes — the same calls now produce archives real LHA tools can open:
use oxiarc_archive::{LzhReader, LzhWriter};
use std::io::Cursor;
// LzhWriter defaults to -lh5- compression with level-2 headers —
// exactly the codec rewritten to canonical LHA format in 0.3.5.
let mut writer = LzhWriter::new(Cursor::new(Vec::new()));
writer.add_file("hello.txt", b"Hello from OxiArc 0.3.5!")?;
let cursor = writer.into_inner()?;
// Reads back through OxiArc...
let mut reader = LzhReader::new(cursor)?;
for entry in reader.entries() {
let data = reader.extract_to_vec(&entry)?;
println!("{}: {} bytes", entry.name, data.len());
}
…and, unlike before 0.3.5, the same bytes now open cleanly in real lha, LHarc, and every other LHA-compatible tool. If you have lha (Lhasa) on PATH, enable the lha-oracle feature to verify this yourself on every build instead of taking our word for it.
What’s New in 0.3.5
- oxiarc-lzhuf:
-lh4-/-lh5-/-lh6-/-lh7-encoder and decoder rewritten to genuine canonical LHA wire format acrossencode.rs,decode.rs,huffman.rs,methods.rs,optimal.rs, andstreaming/{decoder,huffman}.rs— fixing bit order, block command-count semantics, code-table length encoding, and offset-table field widths. Validated against 6 real third-party.lzhfixtures and a livelhaCLI oracle;oxiarc-archive’sLzhWriter/LzhReaderneeded no changes since the default level-2 header format was already spec-conformant. - oxiarc-lzhuf: Fixed
parallel::lzh_compress_parallel’s level-1 header-size byte, which was 5 bytes short of spec and produced archives reallharead as empty. - oxiarc-core: Fixed Miri-flagged undefined behavior in the scalar and SIMD (x86 PCLMULQDQ, aarch64 PMULL) CRC-32/CRC-64 fast paths — 7 call sites switched from speculative pointer arithmetic to address subtraction.
- oxiarc-core: Added
msb_bitstream::{MsbBitReader, MsbBitWriter}— public MSB-first bit I/O, the canonical-LZH/LHA-oriented sibling of the existing LSB-firstbitstreammodule used by DEFLATE. - oxiarc-archive: Fixed resource leaks in
ZipWriter::into_inner,TarWriter::into_inner, andLzhWriter::into_inner— each wrapped its whole struct inManuallyDropto stopfinish()re-running on drop, which also silently suppressed every other owned field’s drop, most notably theArc<dyn ProgressSink>progress-handle clone (plusZipWriter’sentriesVec). - oxiarc-lzhuf / oxiarc-archive: New opt-in
lha-oracleCargo feature (off by default) that shells out to a reallha(Lhasa) CLI to validate OxiArc-produced archives; self-skips cleanly whenlhaisn’t onPATH. - oxiarc-cli:
oxiarc testandoxiarc list(including--json) now return a non-zero exit code with a clean error message on unrecognized or corrupt archive formats, instead of printing a message and exiting0. - 1,878 tests passing (all features, 0 skipped — 79 more than 0.3.4); zero clippy, check, and rustdoc warnings across the workspace.
Tips
- Re-encode any
-lh4-/-lh5-/-lh6-/-lh7-archives you produced with a pre-0.3.5 OxiArc if you need them to open in real LHA tools — the old encoder wrote a private bitstream that only OxiArc’s own reader understood. - Enable the
lha-oraclefeature in CI if you havelha(Lhasa) installed — it round-trips every archive OxiArc produces through the real reference tool instead of only trusting OxiArc’s own reader. - If you call
into_inner()onZipWriter,TarWriter, orLzhWriterin a long-running process withProgressSinkhandles, upgrade — pre-0.3.5 versions leaked the handle’sArcrefcount on every call. - Update any script that checks
oxiarc test/oxiarc listexit codes — unrecognized or corrupt archives now correctly return non-zero instead of silently exiting0. oxiarc-core’s CRC fast paths are now Miri-clean. If you run Miri over code that depends onoxiarc-core, this release removes a UB class you’d otherwise have to suppress or work around.- The new
msb_bitstreammodule is public — reach forMsbBitReader/MsbBitWriterdirectly if you’re implementing another MSB-first format on top ofoxiarc-core.
This is the foundation
Interoperability at the bitstream level is what lets OxiArc sit underneath the rest of the ecosystem without surprises: the same archive layer that now speaks canonical LHA is exercised by FVRS, OxiMedia, SciRS2/NumRS2, RusMES, OxiGDAL, and ToRSh wherever they read or write archives that also need to open correctly outside the COOLJAPAN ecosystem.
Repository: https://github.com/cool-japan/oxiarc
Star the repo if you want an archiver whose output speaks the format it claims to, not just a format it invented for itself.
The era of “readable only by its own author” is over. Pure Rust archiving that’s byte-for-byte compatible — LZH included — is here.
— KitaSan at COOLJAPAN OÜ July 7, 2026