Hash → path tables for League of Legends tooling, stored as a compact, memory-mapped,
seekable binary format (.hashdb). It replaces CommunityDragon's ~348 MB of
hashes.*.txt with a ~52 MiB binary that is usable as shipped - no parse step before
the first lookup, and one copy in the page cache that every tool on the machine
shares.
The format itself is general-purpose: a read-only map from integer keys to string values,
with nothing League-specific in its layout. League Toolkit distributes its own tables
under the .lhdb extension (identical bytes).
Install · Quick start · Library API · CLI · Design
Nothing is on crates.io yet, so depend on the repository directly:
[dependencies]
ltk_hashdb = { git = "https://github.com/LeagueToolkit/mimir" }
# Only if you want the shared cache and the download-driven updater.
# `ureq` gives you a blocking fetcher, `reqwest` an async one; both are optional.
ltk_mimir_cache = { git = "https://github.com/LeagueToolkit/mimir", features = ["ureq"] }The CLI:
cargo install --git https://github.com/LeagueToolkit/mimir ltk_mimir_cliPull the published tables into the shared cache, then resolve a hash out of it:
mimir update
mimir get 0x1234abcd --table gameThe same thing from Rust:
use ltk_mimir_cache::{HashStore, Table};
let store = HashStore::discover()?;
let db = store.open_shared(Table::Game)?; // mmap + validate header; no parse step
if let Some(path) = db.get(0x1234_5678_9abc_def0) {
println!("{path}");
}The cache lives in the platform data directory - %LOCALAPPDATA%\LeagueToolkit\hashes on
Windows, $XDG_DATA_HOME/LeagueToolkit/hashes on Linux, ~/Library/Application Support/LeagueToolkit/hashes on macOS - and MIMIR_DIR overrides it.
Two crates matter to consumers. ltk_hashdb is the format: open a file, resolve hashes.
ltk_mimir_cache is everything around it: where tables live on disk, which version is
active, and how they get updated. A tool that ships its own tables needs only the first.
get returns a PathRef, which borrows its bytes instead of copying them - out of
the mapping for a raw arena, out of the cached decompressed frame for a compressed one.
It derefs to str, so it behaves like one:
use ltk_hashdb::HashDb;
let db = HashDb::open("game.lhdb")?;
if let Some(path) = db.get(hash) {
println!("{path}"); // Display
if path.ends_with(".dds") { /* … */ } // Deref<Target = str>
let owned: String = path.into_owned(); // copy only when you keep it
}
// A miss is decided by binary search over the key array and never touches the arena.
assert!(!db.contains(0xdead_beef));Hashing a path with the table's own algorithm, so you never have to know which one it is:
let hash = db.hash_path("assets/characters/ahri/ahri.bin");
assert!(db.contains(hash));Both forms resolve hits in arena order so each compressed frame is decompressed once.
Use get_batch when you want results back in input order, and for_each_batch when you
want them streamed with no intermediate Vec:
// Collected, in input order.
for (hash, path) in db.get_batch(&chunk_hashes) {
match path {
Some(path) => println!("{path}"),
None => println!("{hash:016x} (unknown)"),
}
}
// Streamed. Calls arrive in arena order, so the first argument is the input position.
db.for_each_batch(&chunk_hashes, |i, hash, path| match path {
Some(path) => println!("{i}: {path}"),
None => println!("{i}: {hash:016x}"),
});LayeredHashDb puts a writable in-memory overlay over one or more read-only bases.
Lookups consult the overlay first, then each base in push order; the first hit wins, and
no base file is ever mutated. This is what WAD consumers want, since chunk resolution
spans both game and lcu:
use ltk_mimir_cache::{HashStore, Table};
let store = HashStore::discover()?;
// Missing tables are reported, not fatal - the tool stays usable and their hashes miss.
// The call itself only fails if you ask for tables from different hash universes.
let (mut db, errors) = store.open_layered(&[Table::Game, Table::Lcu])?;
for (table, e) in &errors {
eprintln!("skipping {table}: {e}");
}
// Register a path your mod introduced; it is hashed with the first base's algorithm.
let hash = db.insert_path("assets/mymod/custom.dds").expect("has a base");
assert_eq!(db.get(hash).as_deref(), Some("assets/mymod/custom.dds"));Note
Every base must agree on key width, hash algorithm, and casing, because lookups take a
hash the caller already computed and no base re-hashes it - push_base returns a
KeyConfigMismatch rather than layering one that doesn't. game and lcu agree; the
four 32-bit bin* tables agree too, and still must not be layered, because they are
separate hash universes - so open_layered refuses that set outright.
// Streams in arena order (lexicographic path order), one decompress per frame.
for (hash, path) in db.iter() {
println!("{hash:016x} {path}");
}
// The same walk without reading a key.
for path in db.values() {
println!("{path}");
}
// The arena is sorted by path, so a directory is one contiguous run and finding it
// is a binary search - single-digit milliseconds on a 2.3M-entry table. Lazy, so
// `take` stops the walk rather than filtering a list that was already built.
for (hash, path) in db.prefix("assets/characters/ahri/").take(50) {
println!("{hash:016x} {path}");
}
// Opt-in resident mode: the whole table as an owned map. Costs the full decompressed
// size in private memory and forfeits the shared page cache, so reach for it last.
let map = db.load_all();The crate ships no HTTP client of its own - you hand it a fetcher. UreqFetch (feature
ureq) and ReqwestFetch (feature reqwest, async) cover the common case; both stream
into the cache rather than buffering a table. Progress is an UpdateObserver you hand to
UpdateOptions - it gets the run's whole plan, byte total included, before the first
connection - and wrapping a fetcher is how you cancel a download in flight (see
docs/CONSUMERS.md):
use ltk_mimir_cache::{HashStore, ReleaseSource, UpdateOptions, UpdateOutcome, UreqFetch};
let store = HashStore::discover()?;
let remote = UreqFetch::new(ReleaseSource::github("LeagueToolkit/mimir"));
match store.update(&remote, UpdateOptions::default())? {
UpdateOutcome::Completed(report) => println!("installed {:?}", report.installed),
UpdateOutcome::Locked => println!("another process is already updating"),
}Only tables whose sha256 differs are downloaded. Installs are atomic - versioned files land first, the manifest pointer flips last - so a reader sees either the whole old version or the whole new one, and readers never take a lock.
use std::fs::File;
use ltk_hashdb::{Casing, Compression, HashDbWriter, HashKind, KeyWidth};
let mut writer = HashDbWriter::new(KeyWidth::U64, Compression::default())
.hash_kind(HashKind::Xxh64) // recorded, so readers can hash new paths
.casing(Casing::AsciiInsensitive); // League tables hash the ASCII-lowercased path
writer.insert(hash, "assets/characters/ahri/ahri.bin");
writer.extend(pairs);
let stats = writer.build(File::create("mine.hashdb")?)?;
println!("{} entries, {} bytes", stats.entries, stats.file_len);Compression::default() is the publishing configuration: 16 KiB frames at level 19, the
measured size/latency knee. Compression::None writes a raw arena that lookups borrow
straight out of the mapping.
HashDb - one .hashdb file. Cheap to clone; every clone shares the mapping and the
frame cache. Send + Sync.
| Method | |
|---|---|
open · open_bytes |
mmap a file, or open an in-memory image |
options() |
open-time knobs: frame_cache_bytes(n), 0 disables |
get |
resolve a hash → Option<PathRef> |
try_get |
as get, but a corrupt arena errors instead of reading as a miss |
get_into |
copy into a reusable String; holds no frame afterwards |
contains |
membership, never touches the arena |
get_batch · for_each_batch |
bulk resolve, collected or streamed |
iter · values |
enumerate in arena order, with keys or without |
prefix |
every entry under a path prefix, by binary search |
load_all |
decode the whole table into an owned map |
hash_path |
hash a string with this table's algorithm and casing |
verify_index · verify |
checksum + key order; the same plus a full arena walk |
is_healthy |
sticky flag set by a failed read |
len · key_width · hash_kind · casing · is_compressed · arena_order_size |
shape |
downgrade |
a WeakHashDb for registries that must not pin the table |
LayeredHashDb - an overlay over N ordered bases.
| Method | |
|---|---|
from_bases · push_base |
layer read-only tables, highest priority first |
insert · insert_path · extend |
write to the overlay, shadowing every base |
get · contains · get_into |
overlay first, then each base in order |
get_batch · for_each_batch |
staged bulk resolve; each base sees only the residual |
iter · prefix |
every entry, or those under a prefix; each shadowed key yielded once by the layer that answers it |
bases · overlay_len · base_len · is_healthy |
shape |
HashStore - the shared cache directory.
| Method | |
|---|---|
discover · at |
resolve the platform cache dir, or point at your own |
open_shared |
open the active version, reusing a handle this store already has |
open · open_many |
open a fresh mapping, one table or several |
open_layered |
open several of one universe into a LayeredHashDb, reporting per-table errors |
manifest · path_for |
what is installed, and where |
check · check_async |
what an update would do - no download, no lock |
update · update_async |
compare → download → verify → install → GC |
commit · gc |
publish versions, sweep superseded ones |
try_lock_update · lock_update_timeout · lock_holder |
take the update lock, wait for it, or ask who has it |
Table - which logical table, and how it hashes.
| Method | |
|---|---|
ALL · id · Display · FromStr · serde |
the stable spellings (game, binentries, rst-xxh3) |
key_config · key_width · hash_kind · casing |
how this table's keys were produced |
universe |
which hashes it can answer - only same-universe tables may be layered |
PathRef - a resolved path. Deref<Target = str>, plus as_str, is_owned
(whether the bytes were copied rather than borrowed), and into_owned.
HashDbWriter - new → hash_kind / casing → insert / extend → build, or
with_key_config when a Table already states all three.
mimir <COMMAND>
build Build a .hashdb table from a txt hash list (lines of `<hex-hash> <path>`)
ls List the paths under a prefix, from a file or the shared cache
get Resolve one hash from a .hashdb file or the shared cache
check Say what an update would do, without downloading or locking anything
update Download the latest published tables into the shared cache
gen Run the hunt engine: discover paths for still-unknown hashes
merge Sorted dedup merge of CDragon txt hash lists
bundle Build all tables + manifest from CDragon txt inputs, staged for a GH release
verify Structural + checksum validation of a .hashdb file (--index-only to skip the arena)
stats Sizes, entry counts, compression ratio of a .hashdb file
# Build a table from a CDragon txt list
mimir build --input hashes.game.txt --table game --out game.hashdb
mimir build --input hashes.game.txt --table game --out game.hashdb --arena-order
# List the paths under a prefix (empty lists the table, in path order)
mimir ls assets/characters/ahri/ --file game.lhdb
mimir ls data/ --table game --limit 20
# Resolve a hash, from a file or from the shared cache
mimir get 0x1234abcd --file game.hashdb
mimir get 0x1234abcd --table game
# Keep the shared cache current (--url for a mirror, --dir for a private cache)
mimir check
mimir update
mimir update --force
# Inspect and validate
mimir stats game.hashdb
mimir verify game.hashdb
mimir verify --index-only game.hashdb # checksum + key order, no arena decode| Crate | Role |
|---|---|
ltk_hashdb |
The .hashdb format: mmap reader (HashDb) + streaming writer |
ltk_mimir_cache |
Shared cache dir, manifest, versioned publish, update lock, GC, in-process updater |
ltk_mimir_gen |
Hash-discovery ("hunt") engine for still-unknown hashes |
ltk_mimir_cli |
The mimir binary |
docs/DESIGN.md |
Why this exists, how the format works, what it measures |
docs/FORMAT.md |
Byte-level specification of .hashdb, format version 1 |
docs/CONSUMERS.md |
Integration guide: lookup patterns, threading, custom pipelines |
docs/BENCHMARKS.md |
Frame-size and compression measurements, with reproduction steps |
docs/ROADMAP.md |
Planned work, in dependency order |
Early development. The format, reader/writer, shared cache, release publishing
(mimir bundle plus a scheduled CI job that ships every table as versioned .lhdb
release assets, rebuilt from the canonical CommunityDragon txt lists), the
download-driven mimir update flow, and the hunt engine - including WAD string mining
(mimir gen --wad) - are all in place.
The txt lists stay canonical; the binaries are generated release artifacts, never the source of truth.
Copyright 2026 Crauzer 0xcrauzer@proton.me
Licensed under the Apache License, Version 2.0 (LICENSE or http://www.apache.org/licenses/LICENSE-2.0). Attribution requirements are in NOTICE.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.