COOLJAPAN
← All posts

OxiArc 0.3.5 Released — Canonical LZH/LHA Interop With Real `lha`

OxiArc 0.3.5 rewrites the -lh4-/-lh5-/-lh6-/-lh7- codec from a private bitstream format into genuine canonical LHA wire format, validated against real third-party .lzh archives and a live lha (Lhasa) CLI oracle. Also fixes a Miri-flagged CRC UB class, three writer resource leaks, and a CLI exit-code bug. 1,878 tests passing, Pure Rust.

release oxiarc lzh lha interop miri pure-rust compression

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.

Technical Deep Dive

  1. 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 new MsbBitReader/MsbBitWriter module — mirroring LHA’s own getbits/putbits, including zero-bit padding past end-of-input — replaces the reversal hack and is re-exported at the crate root and in prelude for anyone else implementing an MSB-first format.
  2. 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.
  3. Code-table length encoding. Temp-tree symbol values were mapped to code lengths as v - 3, leaving an always-unused v == 3 slot; canonical LHA uses v - 2, with a zero-run skip mechanism that is independent of — not layered onto — the temp table’s own index-2 skip-count field.
  4. 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 in LzhMethod::offset_bits/history_bits/max_offset_codes instead of being hand-coded per call site.
  5. Miri-clean CRC fast paths (oxiarc-core). The scalar slice-by-8 loop, the x86 PCLMULQDQ fold, and the aarch64 PMULL fold all computed ptr.add(n) speculatively just to compare it against end — 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

Tips

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

↑ Back to all posts