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.0")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
val dict = ZstdDictionary(dictionaryBytes) // parses tables + indexes content once
val small = Zstd.compress(data, dict)
val back = Zstd.decompress(small, dict, maxSize = 64 * 1024)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.bytesmay be a trained dictionary (zstd --train/ZDICT) or any raw byte prefix.maxSizeondecompressis a required decompression-bomb guard: decoding stops and throws if the output would exceed it.- Failures surface as a single
ZstdException.
- No streaming. The API is one-shot only — no
InputStream/OutputStreaminterface; 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). levelis currently a no-op. The encoder uses a single fixed greedy/lazy strategy rather than zstd's 1–22 levels. Thelevelparameter is accepted for call-site familiarity and forward compatibility but does not (yet) change the output. Frames remain fully libzstd-compatible.- Single block per frame (≤ 128 KiB input).
Zstd.compressemits one zstd block, so its input is bounded by zstd's 128 KiBBlock_Maximum_Size; a larger input throwsZstdException. (Zstd.decompressreads multi-block frames from any encoder.) Multi-block encoding to lift the cap is planned.
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. 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 baselineThe 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.