Skip to content

Add the LZHUF compressor - #16

Merged
colemancda merged 1 commit into
mainfrom
feature/lzhuf-compressor
Jul 18, 2026
Merged

colemancda merged 1 commit into
mainfrom
feature/lzhuf-compressor

Conversation

@colemancda

Copy link
Copy Markdown
Owner

Summary

Adds LZHUF.compress — the encode side of the classic public-domain LZHUF.C the client embeds. Static analysis of the original binary recently named its copy of the encoder (EncodeLZHUFBlock 0x4ea760, with LZHUFPutCode/LZHUFDeleteNode/LZHUFUpdate matching the reference's Putcode/DeleteNode/update, used by its XFS write-back path), confirming the client ships the reference compressor, not just the decoder.

  • Greedy LZSS matching over the 4096-byte ring via the reference's per-first-byte binary search tree (InsertNode/DeleteNode ported exactly, including full-match node replacement and lowest-position tie-breaking), each literal or (length, position) pair adaptive-Huffman coded.
  • encodeChar lives on the same HuffmanTree the decoder evolves, and the encoder position tables are derived from the decoder's dCode/dLen rather than transcribed — the two sides are inverse by construction and cannot drift apart.
  • BitWriter mirrors BitReader (Putcode semantics, including the two-byte spill path).
  • Output carries no size header, matching every GunBound container (decoded size is stored out of band).

Tests

Six new tests in LZHUFCompressTests:

  • Byte-for-byte identity with the reference encoder on all three .lzh fixtures (they were produced by the original LZHUF.C; a faithful port must reproduce them exactly, minus the CLI's 4-byte size header) — the strongest validation possible.
  • Round trips of the sample fixtures, the real characterdata.dat payload, degenerate inputs (empty / single byte / all-identical), >32K-symbol data forcing the adaptive-Huffman reconst rebuild on both sides, and multi-window repetitive data exercising both position-code bands.

Test plan

  • swift test — 566 tests passing
  • CI green on this PR

Ports the encode side of the classic LZHUF.C the client embeds
(static analysis named its copy EncodeLZHUFBlock 0x4ea760, with
LZHUFPutCode/LZHUFDeleteNode/LZHUFUpdate as the reference's
Putcode/DeleteNode/update, used by its XFS write-back path):
greedy LZSS matching over the 4096-byte ring via the per-first-byte
binary search tree, each literal or (length, position) pair
adaptive-Huffman coded. encodeChar lives on the same HuffmanTree the
decoder evolves, and the encoder position tables are derived from the
decoder's dCode/dLen instead of transcribed, so the two sides cannot
drift apart.

Tests round-trip the sample fixtures, real characterdata.dat, and
degenerate/reconst-forcing/multi-window inputs — and pin the encoder
byte-for-byte against the .lzh fixtures produced by the original
reference LZHUF.C.
@colemancda colemancda changed the title Add the LZHUF compressor with round-trip tests Add the LZHUF compressor Jul 18, 2026
@colemancda
colemancda merged commit b5d2f33 into main Jul 18, 2026
5 checks passed
@colemancda
colemancda deleted the feature/lzhuf-compressor branch July 18, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant