iczelia
portable, embeddable gzip decompressor in under 600loc, 0bsd. faster than zlib.
| 2026-08-12 14:56 | update stuff | Kamila Szewczyk |
| 2026-08-12 14:25 | pdgzip: style, correctness. | Kamila Szewczyk |
| 2026-08-12 14:13 | misc style | Kamila Szewczyk |
| 2026-08-12 14:02 | readme: updates | Kamila Szewczyk |
| 2026-08-12 14:00 | Merge remote-tracking branch 'origin/main' | Kamila Szewczyk |
| 2026-08-12 14:00 | fuzz: style, retire unused paths | Kamila Szewczyk |
| 2026-06-09 22:25 | license clarification | Kamila Szewczyk |
| 2026-05-07 20:19 | fix license header in pdgzip.c | Kamila Szewczyk |
| 2026-05-07 20:18 | fix license header | Kamila Szewczyk |
| 2026-05-06 19:22 | nail reserved bits to 0 | Kamila Szewczyk |
| main (2026-08-12 14:56) |
no tags.
pdgzip - a public-domain, embeddable gzip decoder in c11. report issues to kamila szewczyk <k@iczelia.net>. project homepage: https://github.com/iczelia/pdgzip.
pdgzip is a reasonably fast gzip decoder. we have a couple of tricks up our sleeve:
decompressors, here implemented in a low code volume.
a hotspot to semi-space memcpy() back to history buffer.
huffman trees.
slicing-by-8 algorithm.
load per ~7 decoded bits, vs. byte-at-a-time refills.
allocation (instead accepting a fixed-size arena from the caller) and only depends on <string.h>, the necessary functions of which are re-implemented upon absence of the header.
comparisons:
extras:
appreciated but not required.
minor adjustments.
building:
you give the decoder one callback to pull compressed bytes and one constant-size scratch buffer (see pdgzip_state_size() / pdgzip_state_align()). Then you call pdgzip_read(gz, buf, n) in a loop. positive return means bytes produced; zero means clean end-of-stream with the CRC32 trailer already verified; negative means one of PDGZIP_E_FORMAT / PDGZIP_E_CHECKSUM / PDGZIP_E_IO. Enable cfg.concat to decode back-to-back gzip members as one logical stream.
peak memory is a single caller-owned scratch buffer of pdgzip_state_size() bytes; about 236 KiB with default tuning (HUFF_BITS = 9). Raising HUFF_BITS shrinks it at a measurable decode-speed cost: 9 -> 236 KiB, 10 -> 168 KiB, 11 -> 140 KiB.
threat model: input is treated as untrusted. we validate:
FCOMMENT / FHCRC).
preceding header byte).
over-subscribed sets and incomplete ones are rejected, the latter except for the lone one-bit code zlib also permits), per-symbol length limits, sub-table capacity.
vector.
~LEN field; distance/lengthcode symbol ranges.
zlib reports as an invalid distance too far back.
PDGZIP_E_IO rather than decoded as an endless run of literals.
total_out mod 2^32 (matches zlib; forstreams >= 4 GiB this wraps, which is an inherent gzip format limitation, not a bug).
there is no decompressed size cap (must be enforced by the caller) and seeking/random access is not supported. because of a file format limitation, the caller must manually reset the underlying stream that they supplied and recreate a decoder instance. this is typically not a problem for typical use cases.
concat=1 mode allows the decoder to seamlessly decode back-to-back gzip members as one logical stream, which is a common use case.
harnesses:
catches uninit. reads; runs both concat=0 and concat=1 paths.
cd fuzz
make libfuzzer # ASAN + UBSAN + libFuzzer
make libfuzzer-msan # MSAN (decode & huff only; diff needs an MSAN libz)
make afl # AFL++ persistent mode
make repro # Sanitizer replay binary for crash repros
make check # cppcheck static sweepthe static sweep runs clean on cppcheck 2.21 with --enable=all --std=c11 --inconclusive. The source builds with zero warnings under gcc and clang at -std=c11 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wstrict-prototypes -Wcast-qual -Wvla -Wdouble-promotion -Wformat=2.
reference gzip-compatible compressor, under 1000 lines. also public domain.
round-trip.
incompatibilities.
