Skip to content
mimir logo

mimir

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

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_cli

Quick start

Pull the published tables into the shared cache, then resolve a hash out of it:

mimir update
mimir get 0x1234abcd --table game

The 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.

Library API

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.

Resolving a hash

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));

Resolving many hashes

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}"),
});

Layering tables and adding your own hashes

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.

Enumerating a table

// 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();

Updating the cache

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.

Building your own table

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.

API surface

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 - newhash_kind / casinginsert / extendbuild, or with_key_config when a Table already states all three.

CLI

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

Crates

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

Documentation

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

Status

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.

License

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.

About

Hashing toolkit for delivering small and cost-optimized hashtables

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages