A compressed trie-based dictionary format for fast, compact key-value storage.
TriePack encodes dictionaries into a compact binary format (.trp) optimized for fast lookups, prefix search, and ROM-safe deployment. It uses prefix sharing and bit-level packing with configurable symbol encoding and full value type support.
- Compact binary format — compressed tries with prefix sharing and bit-level packing
- Fast lookups — O(key-length) point queries via skip pointers
- Prefix search — iterate all keys matching a prefix, by descending the trie
- ROM-safe — readers work directly on
constbuffers with zero allocation - Typed values — null, bool, int, uint, float32, float64, string, blob
- Same bytes everywhere — every implementation encodes identically, checked by a conformance suite all ten run
- JSON support — encode/decode JSON documents to/from
.trpformat - 10 languages — C core with native implementations across 9 additional languages
- Small footprint — trie codec and bitstream are 49 KB of static library (Release, 64-bit); bindings are 934-1,796 source lines each
All bindings are native implementations that read/write the .trp binary format directly (no FFI).
| Language | Type | Source Lines | Binary/Library Size | Notes |
|---|---|---|---|---|
| C | Core library | 5,387 | 49 KB (codec + bitstream), 72 KB with JSON | C99, 32-bit and 64-bit, ROM-safe |
| C++ | Wrapper | 1,113 | 37 KB (static) | C++11 RAII, owning Value, iteration and prefix search |
| Python | Binding | 934 | pure source | No dependencies |
| JavaScript | Binding | 1,134 | pure source | Node.js and browser, ships type declarations |
| TypeScript | Binding | 49 | pure source | In-repo wrapper; published types come with the npm package |
| Go | Binding | 1,307 | pure source | No dependencies |
| Rust | Binding | 1,796 | pure source | No dependencies, no unsafe |
| Swift | Binding | 1,156 | pure source | SPM package |
| Kotlin | Binding | 1,150 | pure source | Kotlin/JVM, Gradle |
| Java | Binding | 1,585 | pure source | Java 11+, Gradle |
Static library sizes are a Release build; the archives above are the trie
codec plus bitstream, and the figure with JSON adds triepack_json.
# Install
git clone https://github.com/deftio/triepack.git
cd triepack
cmake -B build -DBUILD_TESTS=ON -DBUILD_JSON=ON
cmake --build build
ctest --test-dir build --output-on-failure#include "triepack/triepack.h"
tp_encoder *enc = NULL;
tp_encoder_create(&enc);
tp_value v = tp_value_int(42);
tp_encoder_add(enc, "hello", &v);
uint8_t *buf = NULL; size_t len = 0;
tp_encoder_build(enc, &buf, &len);
tp_dict *dict = NULL;
tp_dict_open(&dict, buf, len);
tp_value val;
if (tp_dict_lookup(dict, "hello", &val) == TP_OK)
printf("hello -> %lld\n", (long long)val.data.int_val);
tp_dict_close(&dict);
tp_encoder_destroy(&enc);
free(buf);pip install triepackfrom triepack import encode, decode
buf = encode({"hello": 42, "world": "foo"})
result = decode(buf)
print(result) # {'hello': 42, 'world': 'foo'}npm install triepackconst { encode, decode } = require('triepack');
const buf = encode({ hello: 42, world: 'foo' });
const result = decode(buf);
console.log(result); // { hello: 42, world: 'foo' }The npm package ships its own type declarations, so there is nothing extra to install:
npm install triepackimport { encode, decode, TriePackData } from 'triepack';
const data: TriePackData = { hello: 42, world: 'foo' };
const buf: Uint8Array = encode(data);
const result: TriePackData = decode(buf);# The module lives in bindings/go; vendor it or add a replace directive:
# replace github.com/deftio/triepack => ./path/to/triepack/bindings/godata := map[string]interface{}{"hello": uint64(42), "world": "foo"}
buf, _ := triepack.Encode(data)
result, _ := triepack.Decode(buf)# Not on crates.io yet; add from source:
# [dependencies]
# triepack = { path = "bindings/rust" }use triepack::{encode, decode, Value};
use std::collections::HashMap;
let mut data = HashMap::new();
data.insert("hello".into(), Value::UInt(42));
let buf = encode(&data); // infallible for String keys
let result = decode(&buf)?;# Add to Package.swift dependencies
# .package(path: "bindings/swift")import Triepack
let data: [String: TriepackValue] = ["hello": .uint(42)]
let buf = try Triepack.encode(data)
let result = try Triepack.decode(buf)# Add bindings/kotlin/ to your Gradle project
cd bindings/kotlin
gradle testimport com.deftio.triepack.*
val data = mapOf("hello" to TpValue.UInt(42))
val buf = encode(data)
val result = decode(buf)# Add bindings/java/ to your Gradle project
cd bindings/java
gradle testimport com.deftio.triepack.*;
Map<String, TpValue> data = new LinkedHashMap<>();
data.put("hello", TpValue.ofUInt(42));
byte[] buf = TriePack.encode(data);
Map<String, TpValue> result = TriePack.decode(buf);Every implementation reports the same metadata, so a polyglot system can ask each one and compare:
require('triepack').version()
// { name: 'triepack', implementation: 'javascript', version: '2.0.0',
// versionMajor: 2, versionMinor: 0, versionPatch: 0,
// formatVersionMajor: 1, formatVersionMinor: 0, maxAlphabetSize: 249 }The same call is tp_version() in C, triepack::version() in C++,
version() in Python, Rust and Kotlin, VersionMetadata() in Go,
Triepack.versionInfo() in Swift and TriePack.version() in Java. The
library version comes from triepack-version.txt at build time; the format
version is separate and moves only when the bytes change.
tp_iterator *it = NULL;
tp_dict_find_prefix(dict, "app", &it); /* descends, does not scan */
const char *key; size_t key_len; tp_value val;
while (tp_iter_next(it, &key, &key_len, &val) == TP_OK)
printf("%.*s\n", (int)key_len, key);
tp_iter_destroy(&it);Keys come out in lexicographic byte order. The bindings decode to a native map instead, so they iterate with whatever their language already provides.
See Examples for more detailed usage including JSON round-trips, file I/O, and cross-language interop.
triepack_json (JSON encode/decode)
|
triepack_core (trie codec: encoder, dictionary, iterator)
|
triepack_bitstream (bit-level I/O, VarInt, UTF-8)
Each layer can be used independently. triepack_wrapper provides C++11 RAII wrappers over all three.
| Option | Default | Description |
|---|---|---|
BUILD_TESTS |
ON | Build test suite |
BUILD_EXAMPLES |
ON | Build example programs |
BUILD_JSON |
ON | Build JSON library |
BUILD_DOCS |
OFF | Build Doxygen documentation |
ENABLE_COVERAGE |
OFF | Enable code coverage instrumentation |
ENABLE_SANITIZERS |
OFF | Build with AddressSanitizer and UndefinedBehaviorSanitizer |
- Magic bytes:
TRP\0(0x54 0x52 0x50 0x00) - File extension:
.trp - 32-byte fixed header
- Bit-packed prefix trie, with the value store following it
- CRC-32 integrity check over the whole buffer
A valid checksum means the buffer is intact, not that it is trustworthy — anyone who can supply a buffer can supply a matching CRC.
See docs/internals/ for format details.
v2.0.0. Core C library (bitstream, trie codec, JSON), C++ wrapper, and 8 language bindings (Python, JavaScript, TypeScript, Go, Rust, Swift, Kotlin, Java) are implemented. CI enforces floors of 97% lines and 80% branches on the C library, and runs the whole suite under AddressSanitizer and UndefinedBehaviorSanitizer — the leak half only exists on Linux, and it is what caught the leak fixed in 1.3.2.
What is not implemented is listed in Status, which exists because that gap was once large and undocumented. Read it before relying on anything a header names.
All ten implementations run a shared conformance suite: for each of 50 cases every one must decode the same C-generated fixture to the same values and re-encode it byte for byte, and reject the same 11 malformed buffers. About 1,600 tests in total.
scripts/make-release.sh --check builds and tests all ten targets locally;
scripts/test-ci-linux.sh runs the ubuntu-only jobs in a container.
- TypeScript binding (wraps JS implementation)
- Go binding
- Swift binding (with SPM package)
- Rust binding
- Kotlin binding
- Java binding
- npm package for JavaScript/TypeScript (ships bundled type declarations)
- PyPI package for Python
- crates.io package for Rust
The 249-symbol alphabet limit, the absent suffix table and the O(n) value lookup are not things v1 can be patched into. They are the subject of format v2, which breaks the header:
- LOUDS-encoded trie with rank/select — no alphabet limit, O(1) value lookup
- Suffix-merged tail pool (shared endings)
- 1 GB+ inputs within a bounded memory budget
- Bit vector, rank/select and tail pool libraries, with a measurement prototype
See the implementation plan for sequencing across ten implementations.
-
trpCLI: encode/decode/validate/inspect - Language binding conformance test suite
- Trie iteration and prefix search
- Fuzzy search (edit distance d<=2) — declared, returns
TP_ERR_UNSUPPORTED - Performance benchmarks across languages
Bug reports and pull requests are welcome. See CONTRIBUTING.md for how to build and test each target, and for what a change to the binary format has to satisfy — every implementation has to agree byte for byte, which the conformance suite checks.
Participation is covered by the Code of Conduct. Security issues go through SECURITY.md rather than the public tracker.
Releases are cut with ./scripts/make-release.sh; see RELEASE.md.
terseml/ is a separate thing that lives in this repository: a
positional encoding for tag / attribute / content trees (.tsml), with a
formal grammar and implementations in C, Python and JavaScript.
It links nothing from TriePack and TriePack links nothing from it. It is not published to any registry. TriePack stores a dictionary; terseml carries a tree; they do not pipe into each other. See the terseml page for what it is and how to build it.
BSD-2-Clause. See LICENSE.txt.
Copyright (c) 2026 M. A. Chatterjee