kzstd
0.1.0indexedPure zero-runtime-dependency Zstandard codec producing standard zstd frames with dictionary support; immutable digested dictionaries reusable across threads, one-shot API, and decompression-bomb guard.
Pure zero-runtime-dependency Zstandard codec producing standard zstd frames with dictionary support; immutable digested dictionaries reusable across threads, one-shot API, and decompression-bomb guard.
A pure-Kotlin, multiplatform Zstandard (zstd) codec with dictionary support. It produces and reads standard zstd frames that interoperate with libzstd in both directions, and it has zero runtime dependencies — just the Kotlin standard library, on every target.
kzstd was extracted from TAKPacket-SDK,
where it replaced three native binding stacks — zstd-jni on the JVM, a per-target
libzstd cinterop on Kotlin/Native, and @bokuweb/zstd-wasm on JS/Wasm — with one
implementation that compiles everywhere Kotlin does.
JVM · JS (browser + Node) · Wasm/JS · Wasm/WASI · and nine Kotlin/Native targets: iOS (arm64, simulator-arm64, x64), macOS (arm64), tvOS (arm64, simulator-arm64), Linux (x64, arm64), and Windows (mingw-x64).
// Maven Central
implementation("org.meshtastic:kzstd:0.1.2")
import org.meshtastic.kzstd.Zstd
import org.meshtastic.kzstd.ZstdDictionary
import org.meshtastic.kzstd.ZstdException
// Without a dictionary
val frame = Zstd.compress(data)
val original = Zstd.decompress(frame, maxSize = 64 * 1024)
// With a dictionary — digest it once, reuse it everywhere
dict = ZstdDictionary(dictionaryBytes)
small = Zstd.compress(, dict)
back = Zstd.decompress(small, dict, maxSize = * )
ZstdDictionary(bytes) digests a dictionary once in its constructor (parsing
its entropy tables and indexing its content) and is immutable afterward, so a
single instance is safe to share across threads and cheap to reuse. may
be a trained dictionary ( / ) or any raw byte prefix.kzstd reads frames produced by libzstd (including dictionary-compressed frames that use the dictionary's Huffman/FSE entropy tables), and libzstd reads frames produced by kzstd — including frames kzstd itself entropy-codes: dictionary frames using the dictionary's trained tables and repeat-offset codes, and dictionary-free frames using Huffman/FSE tables built from the block's own data (or the RLE forms, when a block, its literals or a symbol stream is constant). Each of those forms is picked only when it is the smallest valid encoding. The test suite cross-checks both directions against zstd-jni (a JVM-test-only oracle, never a runtime dependency).
./gradlew build # compile every target, run tests, check the API baseline
./gradlew jvmTest # JVM tests only (includes the libzstd interop oracle)
./gradlew apiDump # refresh the binary-compatibility API baseline
The test dictionary (src/commonTest's TestVectors) is a genuinely trained zstd
dictionary regenerated reproducibly by scripts/train_test_dict.py (requires the
zstd CLI) — its content is generic structured JSON, not domain data.
Contributions are welcome. See CLAUDE.md for the architecture, build
and test commands, and the design invariants, and CHANGELOG.md for
release notes. Run ./gradlew build (JDK 21) before opening a PR, and refresh the
binary-compatibility baseline with ./gradlew apiDump after any public-API change.
GPL-3.0. See LICENSE.
byteszstd --trainZDICTmaxSize on decompress is a required decompression-bomb guard: decoding
stops and throws if the output would exceed it.ZstdException.InputStream/OutputStream
interface; each call handles a whole frame from one byte array, with no cross-call
state, so every frame is independently decodable (what packet and mesh transports
need).level (1–22) governs match-finding search depth only. The encoder uses a
single fixed greedy/lazy strategy at every level — it does not implement zstd's
other per-level parameters (window log, target length, etc.) — but a higher
level does search more candidate matches per position, which can shrink output
at the cost of more work. Frames remain fully libzstd-compatible at every level.Zstd.compress
cuts input into zstd's 128 KiB Block_Maximum_Size chunks and emits one
multi-block frame. Each chunk is matched only against itself and the
dictionary, never against an earlier block's output, so a large input
compresses less well than a windowed encoder manages — 3 MB of synthetic
JSON telemetry lands between libzstd's levels 3 and 19. Entropy tables and the
repeat offsets ARE carried across blocks. A windowed matcher is a planned
improvement.ZSTD_WINDOWLOG_LIMIT_DEFAULT), so Zstd.compress throws ZstdException
rather than emit a frame most real-world libzstd consumers would refuse to
decode.Zstd.decompress reads the 4-stream layout and FSE-compressed weight descriptions
that libzstd emits. The 1023-byte cap is why a full 128 KiB block keeps raw
literals: on large inputs the ratio comes from the sequence tables alone.Surfaced from shared tags and platforms — no rankings paid for.