From 027458c2c4605a05e1393a0fdaeb60ba8b6f2582 Mon Sep 17 00:00:00 2001 From: Crauzer Date: Mon, 24 Aug 2026 23:24:42 +0200 Subject: [PATCH 01/10] docs: add ROADMAP.md --- CLAUDE.md | 3 +- README.md | 3 + docs/ROADMAP.md | 424 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 docs/ROADMAP.md diff --git a/CLAUDE.md b/CLAUDE.md index 27efb0c..effccab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,8 @@ Cargo workspace (`resolver = "2"`), four crates under `crates/`: | `ltk_mimir_cli` | The `mimir` binary (`build` / `get` / `verify` / `stats` / `gen` / `update` / `merge` / `bundle`) | Docs live in `docs/`: `FORMAT.md` (byte-level spec, format version 1), `CONSUMERS.md` -(integration API), `BENCHMARKS.md` (frame-size/compression measurements). +(integration API), `BENCHMARKS.md` (frame-size/compression measurements), `ROADMAP.md` +(planned work, in dependency order, with the additive-only constraints that govern it). ## Conventions diff --git a/README.md b/README.md index 890a667..708d533 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,9 @@ publishing (`mimir bundle` + a scheduled CI job that ships every table as versio download-driven `mimir update` flow, and the hunt engine - including WAD string mining (`mimir gen --wad`) - are in place. +Planned work - a reader frame cache, the pre-1.0 API cleanup, and arena-order indexing - +is tracked in [`docs/ROADMAP.md`](docs/ROADMAP.md). + ## License Licensed under either of diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..acff2f8 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,424 @@ +# Roadmap + +> Planned work on the format, the reader, and the shared cache, in dependency order. +> Findings are lettered (A/B/C) from a consumer-integration review; tasks are numbered by wave. + +Everything here is `ltk_hashdb`, `ltk_mimir_cache`, or `ltk_mimir_cli`. The waves are +ordered by what each unblocks and by three constraints that are not negotiable - read +those first, because two of them decide what may ship at all. + +## Ordering constraints + +### 1. `version` and `schema` are equality gates, so everything is additive from here + +`Header::decode` rejects any `version != FORMAT_VERSION` (`header.rs:114`), and +`Manifest::from_slice` rejects any `schema != SCHEMA_VERSION` (`manifest.rs:76`). Both +compare for equality, not for a floor. + +The shared cache exists so that several independently-versioned tools point at one +directory. That premise breaks the moment a version is bumped: the first tool to sync +publishes files and a manifest that every not-yet-upgraded tool on the machine refuses. +The manifest gate is the sharper of the two - it is not confined to reading a table, so a +consumer that merely reports cache status fails outright rather than degrading. + +**`schema` stays at `1` and `FORMAT_VERSION` stays at `1`.** Task 3.1 relaxes the gates +for future readers; it cannot help the ones already shipped, which is the argument for +never bumping rather than for bumping once more first. + +### 2. The additive room is in the reserved fields, not in the flag bits + +| Room | Where | Status | +|------|-------|--------| +| Header reserved `[u8;8]` | bytes 72..80 (`header.rs:20`) | **Safe.** Written as zero, ignored on read - wide enough for a section offset. | +| Header reserved `[u8;2]` | bytes 14..16 (`header.rs:12`) | **Safe**, but only 2 bytes - a discriminant or a small count, not an offset. | +| New manifest fields | `manifest.rs` | **Safe.** serde ignores unknown fields; existing optional fields already use `#[serde(default)]`. | +| New `flags` bits | `header.rs:38`, checked at `header.rs:120` | **Not safe.** `KNOWN_FLAGS` rejects any unknown bit, and `FORMAT.md` specifies "other bits must be 0". | + +So a new *feature* can be announced through a reserved header field without a version +bump, but a new *flag* cannot. Task 3.1 opens the flag byte; task 4.1 is written to need +only the reserved `u64`, so it does not depend on that half landing first. + +### 3. Breaking Rust API changes are free until the first crates.io release + +`ltk_hashdb` and `ltk_mimir_cache` sit at `0.1.0` with `release = true, publish = true` in +`release-plz.toml`, but no crate tag exists yet (`git tag -l` shows only the +`hashes-*` table-data namespace) and neither crate has a changelog. There are no external +consumers pinned to a published version. + +Every breaking signature change is therefore one rev bump in one downstream repo today, +and a semver event forever after. **Wave 2 exists to spend that window before the first +`cargo publish`.** + +## Wave 1 - Additive reader work + +Purely additive: no signature changes, so this wave can ship to existing git-pinned +consumers without touching a call site. + +### 1.1 Move `HashDb`'s state behind an `Arc` and add a frame cache + +> Findings **A1** and **B4**. One refactor, not two - the shared read-only state and the +> shared frame cache want the same `Arc`. + +`FrameCache` (`reader.rs:52`) is a local `&mut` threaded through one call. It lives for +the duration of a `get_batch` (`reader.rs:154`), an `iter` (`reader.rs:230`), or a +`verify` (`reader.rs:249`), and dies when that call returns. `get` (`reader.rs:143`) +passes `&mut None`. Nothing caches a decompressed frame across calls. + +The cost is the gap the benchmarks already record: on `game` a point hit is 8.5 µs against +3.6 µs batched and 143 ns for a miss; on `binentries` it is 10.5 µs against 1.2 µs. That +whole difference is frame decompression, and it is re-paid on every lookup even when the +previous one decompressed the very frame the answer is in. Because the arena is +path-ordered, consecutive lookups in a real workload are usually in the same frame - so a +consumer resolving an archive's chunks one at a time decompresses a handful of frames tens +of thousands of times. + +Point lookups are not a niche shape. `ltk_wad`'s `PathResolver` is point-shaped by +construction, so every WAD extractor resolves one hash per chunk whether it wants to or +not. + +- `HashDb { inner: Arc }`, deriving `Clone`; the mmap, header, section ranges, and + seek table move into `Inner`. +- A byte-capped LRU of decompressed frames beside them - a few MiB by default, + `with_frame_cache(bytes)` to tune, `0` to disable. Published files are immutable, so this + is pure memoisation with no invalidation. +- `HashStore::open_shared(table)` keeping a weak cache keyed on the manifest's active + filename, so reopening after an update happens by itself. `open` (`store.rs:106`) today + re-mmaps and re-parses the seek table on every call - ~10 400 frame records for `game`. + +Design constraints: a frame held behind a `Mutex` cannot be borrowed out of the guard, so +`get` keeps returning `Cow::Owned` and the allocation only goes away with task 1.2. +Measure lock contention before choosing between one mutex, sharding by frame index, and a +thread-local cache. + +**Done when** a bench resolving one archive's chunks point-wise lands within a small factor +of the same set batched; the `misses_never_decompress` invariant still holds; and +`Clone + Send + Sync` is asserted for `HashDb` *and* `LayeredHashDb`. + +### 1.2 Closure and buffer accessors for allocation-free reads + +> Finding **A3**. Depends on 1.1. + +For a compressed arena - every published `.lhdb` - `str_at_cached` (`reader.rs:348`) always +takes the owned branch, because there is no borrow to be had from a decompression buffer +that dies with the call. `BENCHMARKS.md` already names the frame buffer as the only +per-lookup allocation. A consumer building a whole-install index therefore allocates a +`String` per chunk and keeps a fraction of them. + +- `with_str(&self, hash, f: impl FnOnce(&str) -> R) -> Option`, reading straight out + of the cached frame. +- `get_into(&self, hash, &mut String) -> bool` for callers that own a reusable buffer. +- Document that `with_str`'s closure runs under the cache lock and must stay short. + +**Done when** a filter-only pass over a WAD's chunks - resolve, test the extension, discard +- allocates nothing per hit. + +### 1.3 `Debug` on every public reader + +> Finding **B3**. + +None of `HashDb`, `LayeredHashDb` (`layered.rs:36` derives `Default` only), or +`ExtendedHashDb` (`extended.rs:10`) implements `Debug`. Consumers wrapping them end up +hand-writing `Debug` for their own types to compensate. Print counts, widths, and flags - +never entries. + +`tests/roundtrip.rs:48` asserts `Send + Sync` for `HashDb` and `ExtendedHashDb` but not for +`LayeredHashDb`, which is the type most consumers actually hold. Add it. + +**Done when** all three print their shape and the assertion covers all three. + +### 1.4 Tell a corrupt table apart from a table of unknown hashes + +> Finding **A4**. + +`str_at_cached` (`reader.rs:348`) swallows every `frame_bytes` (`reader.rs:366`) error into +`None`. That is right for a lookup path - `FORMAT.md` specifies a failed frame as a miss - +but it means a table whose arena no longer decompresses degrades silently into "this build +knows nothing". A consumer renders an entire install under hex names with no signal to +distinguish that from an incomplete table. + +Nothing re-verifies an installed table after its download checksum (`store.rs:106`: the +manifest sha256 is trusted), so bit rot and truncating writes both land here. + +- A sticky `AtomicBool` set on the first swallowed error, plus `HashDb::is_healthy()`. +- Optionally `try_get -> Result, VerifyError>` for callers that want the error + at the call site. + +**Done when** a table with a deliberately corrupted frame reports unhealthy after the first +affected lookup, and `get` still never panics. + +### 1.5 Write down why the mmap is sound + +> Finding **A5**. + +`unsafe { memmap2::Mmap::map(&file)? }` (`reader.rs:58`) carries no safety comment. What +discharges it - published `.lhdb` files are immutable, `commit` never renames over an +existing name, and `gc` only unlinks - is documented on `HashStore` in a different crate. +`CONSUMERS.md` explicitly invites using `ltk_hashdb` alone for embedded or self-built +tables, and such a consumer gets no warning that truncating a mapped file is undefined +behaviour rather than an error. + +Add a `// SAFETY:` at the block and a paragraph in `HashDb::open`'s docs stating the +caller's obligation. + +## Wave 2 - Breaking API cleanup + +Every breaking signature change worth making, batched into one release. See constraint 3. + +### 2.1 Give `Table` its own metadata + +> Finding **B2**. Blocks 2.2. + +`Table` (`lib.rs:38`) exposes `id()` (`lib.rs:63`) and `from_id()` (`lib.rs:77`) and nothing +else. It cannot report its key width, hash algorithm, or casing, though those are fixed per +table and the workspace knows them - so they are written out three times: the CLI's private +`Table` enum (`ltk_mimir_cli/src/main.rs:32`, methods at `:43`-`:64`), `bundle.rs`'s +`SPECS` (`ltk_mimir_cli/src/bundle.rs:76`), and prose in consumers explaining which tables +share a hash universe. + +- `key_width()`, `hash_kind()`, `casing()`, and a `universe()` discriminator on the library + enum. +- Delete the CLI's duplicate methods and fold `TableSpec` (`bundle.rs:49`) down to input + filename plus split flag. + +**Done when** each table's width, algorithm, and casing is stated once in the workspace. + +### 2.2 Make the layered key-config invariant a hard error + +> Finding **B1**. Depends on 2.1. + +`LayeredHashDb` requires every base to agree on `(key_width, hash_kind, casing)`, because +lookups take a pre-hashed `u64` and no base re-hashes. It documents this at length and then +enforces it with `debug_assert_eq!` (`layered.rs:57` in `from_bases`, `layered.rs:75` in +`push_base`) - which compiles out of release builds. + +In release, layering `binfields` under `binentries` does not fail. It answers a property +hash with an object's path, across four unrelated 32-bit FNV-1a universes totalling +~500 000 rows, often enough to be a certainty rather than a risk. A wrong name is worse +than a number. + +- `push_base` and `from_bases` return `Result<_, KeyConfigMismatch>` naming both configs. +- `open_layered` (`store.rs:124`) refuses a mismatched set using `Table::universe()` before + it opens anything. + +**Done when** layering `binfields` under `binentries` fails in a release build. + +### 2.3 `#[non_exhaustive]`, `Display`, and `FromStr` on `Table` + +> Finding **B7**. + +The table set will grow, and without `#[non_exhaustive]` each addition is a breaking +change. `Display`/`FromStr` over `id()` remove `table.id().to_owned()` from every consumer +boundary. `OpenError::TableNotFound` formats with `{0:?}`, so it prints `BinEntries` where +every other surface says `binentries`. An optional serde feature belongs here too. + +### 2.4 Split `Casing` so the League rule has its own name + +> Finding **A6**. Narrows a documented decision; does not reverse it. + +`FORMAT.md` already specifies Unicode-aware lowercasing deliberately, and already carries +the stability note that only the ASCII part of the mapping is bit-stable across toolchains, +recommending that non-ASCII publishers pre-lowercase and hash case-sensitively. The gap is +not that the decision is wrong - it is that `Casing::Insensitive` (`hash.rs:18`, variant at +`hash.rs:27`) is one name covering two rules, and the League tables, which are the +overwhelmingly common case, get the Unicode one by default. + +Introduce `Casing::AsciiInsensitive` as what League tables mean and what +`FLAG_CASE_INSENSITIVE` maps to for them; keep the Unicode variant as a deliberate, +separately-named choice or drop it (see *Open questions*). No data migration - every +published table is ASCII, so the bytes on disk do not change, only which variant names +them. `FORMAT.md`'s `case_insensitive` section needs updating either way. + +### 2.5 Signature parity and a streaming batch + +> Finding **B5**. + +`HashDb::get_batch` (`reader.rs:154`) collects eagerly so the returned iterator does not +borrow the input. `LayeredHashDb::get_batch` (`layered.rs:126`) keeps the borrow, because +its tail zips over the input slice - so the same method on the two types has two contracts, +and the layered one refuses a temporary. + +- Drop the `'a` from the layered signature's `hashes`. +- Add `for_each_batch(&self, hashes, f: impl FnMut(usize, u64, Option<&str>))` to both, + which inherits task 1.2's allocation-free path. Both current forms materialise a `Vec` of + every result before yielding the first. + +### 2.6 Retire `ExtendedHashDb`, grow `LayeredHashDb` + +> Finding **B6**. + +`layered.rs` already describes `LayeredHashDb` as `ExtendedHashDb` generalised to N bases, +and it has everything the narrow type has plus `get_batch`. Deprecate `ExtendedHashDb`, +lead `CONSUMERS.md` with the layered type rather than teaching the narrower one first, and +add `iter()` and `len()` to `LayeredHashDb` - today it cannot be enumerated at all. + +## Wave 3 - Distribution + +Task 3.1 gates the rest of this wave *and* the flag-byte half of wave 4. Nothing here may +add a manifest field or a header flag until the version gates stop being fatal. + +### 3.1 Make the version gates survivable + +> Finding **A7**. Blocks 3.2, 3.3, and the flag-byte option in 4.1. + +- **Manifest.** Accept `schema >= SCHEMA_VERSION` (`manifest.rs:76`) and rely on serde + ignoring unknown fields. Add an optional `min_reader_schema` for the day something + genuinely incompatible is needed. Policy: `schema` stays at `1`. +- **Header.** Split `flags` into a required byte - unknown bit rejects, today's behaviour + at `header.rs:120` - and an optional byte whose unknown bits are ignored. Update + `FORMAT.md`, which currently specifies "other bits must be 0". +- **Manifest.** `format_version` per `TableEntry` (`manifest.rs:50`), so a reader skips a + table it cannot open the way `UpdateReport::unknown_tables` already skips an unknown id. +- **Release.** Publish a per-format channel filename alongside `latest`, so an old build + keeps updating within the format it can read. `ReleaseSource::github` hardcodes + `/releases/latest/download`. + +**Done when** a manifest carrying an unknown table, an unknown field, and a higher `schema` +still installs the tables this build understands, and a `.hashdb` with an unknown +*optional* flag bit opens normally. + +### 3.2 Version and time on `TableEntry` + +> Finding **C6**. Depends on 3.1. + +`TableEntry` (`manifest.rs:50`) carries `file`, `sha256`, `entries`, and `key_width` - no +version and no timestamp. The version exists only inside the filename, extracted by +`version_of`, which is private (`update.rs:345`). `generated_at` (`manifest.rs:23`) is a +`String`, so "how stale is this cache" is every consumer's parsing problem despite the +crate already depending on `time`. + +Add `version`; expose `generated_at` parsed beside the raw string. + +**Done when** a consumer can show `game · 2026-07-10` instead of `game-2026-07-10.lhdb`. + +### 3.3 Move provenance onto the table + +> Finding **C5**. Depends on 3.1. + +`commit` (`store.rs:155`) assigns `manifest.source = source` wholesale (`store.rs:169`), so +a run installing only `game` restamps `repo`, `commit`, and `inputs_sha256` for the seven +tables it did not touch - the manifest then claims a CommunityDragon commit for tables +built from a different one. + +Move `commit` and `inputs_sha256` onto `TableEntry`; keep a manifest-level record named for +what it actually is, the last run. + +### 3.4 Add `HashStore::check` + +> Finding **C1**. + +`update` (`update.rs:161`) is the only path to the remote manifest, and it takes the +exclusive lock before fetching anything. There is no lock-free "what would change?", so a +UI cannot show an updates-available state and a startup check cannot stay out of the CLI's +way. + +Expose the first half of the private `plan` (`update.rs:226`) as +`check(&remote) -> Result, _>`: fetch, compare sha256s, install nothing, +take no lock. + +**Done when** a consumer can render "3 tables behind" without touching the update lock. + +### 3.5 Stream the download and stop copying the result + +> Findings **C3** and **C2**. One change - the copy only exists because the fetcher hands +> back a whole buffer. + +`Fetch` (`update.rs:29`) and `AsyncFetch` (`update.rs:66`) return `Vec`. For each table +the bytes then move four times: `verify_and_stage` (`update.rs:269`) runs `sha256_bytes` +(`fsutil.rs:46`) over the buffer and writes a `.download.tmp`; `commit` opens that file, +`atomic_copy` (`fsutil.rs:36`) reads and writes it again into a second temp, renames, and +`sha256_file` (`fsutil.rs:51`) reads the destination a third time to recompute a digest it +was just handed. + +A ~52 MiB corpus therefore costs several hundred MiB of I/O and peaks at the size of the +largest table in memory (38.3 MiB for `game`). The buffer return also makes byte-level +progress and mid-download cancellation impossible, which is why the bundled fetchers are +documented as silent. + +- `Fetch::fetch_to(&self, filename, sink: &mut dyn Write)` as the primitive, with the + `Vec` form kept as a default method over it. Same for `AsyncFetch`. +- Stage straight into the cache directory while hashing the stream. +- A `CommitItem` constructor that *takes* an already-staged sibling - rename, reuse the + digest - alongside the existing copy path for files built elsewhere. + +**Done when** installing the full corpus reads and writes each table's bytes once, peak +memory does not track the largest table, and a caller's sink can report bytes and cancel. + +### 3.6 Say who holds the update lock + +> Finding **C4**. + +`UpdateLock::try_acquire` (`lock.rs:20`) is non-blocking and reports only presence - no +holder, no start time, no bounded wait. Consumers can only surface "another process is +already syncing", which reads the same as a crashed updater. + +Write pid and an RFC-3339 start time into the lock file's body, and expose `lock_holder()` +plus `lock_update_timeout(Duration)`. The OS lock stays the source of truth; the body +exists only so a message can name who and since when. + +### 3.7 A cheap integrity tier between `open` and `verify` + +> Finding **C7**. Pairs with 1.4. + +`open` validates structure only; `verify` (`reader.rs:249`) reads the whole file. Add +`verify_index()` - checksum the keys, offsets, and lengths sections and skip the arena. +Milliseconds rather than a full pass, and it catches most real post-install damage. +Together with 1.4's health flag a consumer gets both a proactive check and a reactive +signal. + +## Wave 4 - Format capability + +### 4.1 Record arena order in the file + +> Finding **A2**. Depends on 3.1 only if the flag-byte variant is chosen; the reserved-`u64` +> variant below needs nothing. + +The writer sorts the arena lexicographically by path (`writer.rs:84`-`93`) - that ordering +is what earns the ~4× compression win. But offsets and lengths are stored in *key* order, +so nothing can walk the arena forward without first reconstructing the permutation. +`arena_order` (`reader.rs:301`) does that with an `O(n log n)` sort over an `n`-word +allocation, on every `iter` and every `verify`: on `game` that is a 2 086 643-element sort +and ~16 MiB of scratch, recomputed per call. `BENCHMARKS.md` already lists the vector under +*Memory profile*. + +The larger cost is what it forecloses. A table that is already a sorted list of strings +cannot answer "which paths start with `assets/characters/ahri/`" - the query an asset +browser and a name-autocomplete both want. Consumers work around it by downloading the +CommunityDragon txt list separately just to enumerate names. + +Two candidate layouts: + +| Layout | Content | Size on `game` | +|--------|---------|---------------:| +| **Sparse per-frame index** | Per zstd frame: entry index and arena offset of the first entry starting in it | ~10 400 frames × 12 B ≈ **122 KiB** | +| Dense rank array | `arena_rank → entry_index`, `u32 × entry_count` | ~8 MiB (+21 % on a 38.3 MiB file) | + +Point the reader at the section from the reserved header `u64` at bytes 72..80 +(`header.rs:20`), which current readers already ignore - so no version bump, no flag bit, +and a reader built before this change still opens the new file. + +Then: `values()` streaming with no sort and no allocation, `prefix(&str, limit)` as a +binary search over the arena, and `iter`/`verify` stop rebuilding the permutation. + +`FORMAT.md` needs a new section; its "readers must treat the arena as opaque bytes +addressed by (offset, length)" invariant is unaffected, since this adds an index rather +than changing how an entry is addressed. + +**Done when** `iter` on the game table allocates no index vector, +`prefix("assets/characters/ahri/")` returns in single-digit milliseconds, and a reader +built before the change still opens the file. Decide between the two layouts against a +measured prefix benchmark, not on paper. + +## Open questions + +- **Does `ExtendedHashDb` get deleted or deprecated forever?** Deprecation costs nothing + but keeps two overlapping types in the docs; deletion is free right now (constraint 3) + and a semver event later. +- **Does the Unicode `Casing` variant survive task 2.4?** Nothing in the League corpus + needs it, `FORMAT.md` already tells non-ASCII publishers to pre-lowercase instead, and + keeping it means keeping a way to hash a path to a key no published table holds. Dropping + it would simplify `hash.rs` and remove the Unicode-version stability caveat from the spec. +- **Should mimir ship a `PathResolver` impl for `ltk_wad`?** Every WAD consumer writes the + same ~40-line adapter. The dependency direction is the awkward part: `ltk_wad` + implementing the trait for `LayeredHashDb` is cleaner than mimir depending on `ltk_wad`, + but it puts the impl in a crate that would then need an `ltk_hashdb` dependency. A + feature-gated impl here is the third option. From f2378c703370988734c030455cea6a0f64e4e5b9 Mon Sep 17 00:00:00 2001 From: Crauzer Date: Mon, 24 Aug 2026 23:24:42 +0200 Subject: [PATCH 02/10] refactor(hashdb)!: retire ExtendedHashDb in favour of LayeredHashDb --- CLAUDE.md | 2 +- crates/ltk_hashdb/src/extended.rs | 59 ---------------------------- crates/ltk_hashdb/src/layered.rs | 8 ++-- crates/ltk_hashdb/src/lib.rs | 2 - crates/ltk_hashdb/tests/roundtrip.rs | 33 ++++++++-------- docs/CONSUMERS.md | 31 +++++++-------- 6 files changed, 36 insertions(+), 99 deletions(-) delete mode 100644 crates/ltk_hashdb/src/extended.rs diff --git a/CLAUDE.md b/CLAUDE.md index effccab..021e3de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ Cargo workspace (`resolver = "2"`), four crates under `crates/`: | Crate | Role | |-------|------| -| `ltk_hashdb` | The `.hashdb` format: `mmap` reader (`HashDb`) + streaming writer (`HashDbWriter`), `ExtendedHashDb` overlay | +| `ltk_hashdb` | The `.hashdb` format: `mmap` reader (`HashDb`) + streaming writer (`HashDbWriter`), `LayeredHashDb` overlay | | `ltk_mimir_cache` | Shared cache dir, `manifest.json`, versioned publish, update lock, GC, in-process updater (`HashStore`, `HashStore::update`) | | `ltk_mimir_gen` | Hash-discovery ("hunt") engine - guessers that resolve unknown hashes | | `ltk_mimir_cli` | The `mimir` binary (`build` / `get` / `verify` / `stats` / `gen` / `update` / `merge` / `bundle`) | diff --git a/crates/ltk_hashdb/src/extended.rs b/crates/ltk_hashdb/src/extended.rs deleted file mode 100644 index 5968d5c..0000000 --- a/crates/ltk_hashdb/src/extended.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! A [`HashDb`] plus an in-memory overlay of extra entries. - -use std::borrow::Cow; -use std::collections::HashMap; - -use crate::HashDb; - -/// A `HashDb` with a mutable in-memory overlay (e.g. runtime mod hashes). The base -/// file is never mutated; lookups consult the overlay first, then the table. -pub struct ExtendedHashDb { - base: HashDb, - overlay: HashMap>, -} - -impl ExtendedHashDb { - pub fn new(base: HashDb) -> Self { - Self { - base, - overlay: HashMap::new(), - } - } - - pub fn insert(&mut self, hash: u64, path: impl Into>) { - self.overlay.insert(hash, path.into()); - } - - /// Hash `path` with the base table's algorithm, insert it, and return the hash - - /// "register this path" without knowing the algorithm. - pub fn insert_path(&mut self, path: &str) -> u64 { - let hash = self.base.hash_path(path); - self.insert(hash, path); - hash - } - - pub fn extend<'a>(&mut self, it: impl IntoIterator) { - self.overlay - .extend(it.into_iter().map(|(k, p)| (k, Box::from(p)))); - } - - /// Overlay first, then the base table. - pub fn get(&self, hash: u64) -> Option> { - match self.overlay.get(&hash) { - Some(path) => Some(Cow::Borrowed(&**path)), - None => self.base.get(hash), - } - } - - pub fn contains(&self, hash: u64) -> bool { - self.overlay.contains_key(&hash) || self.base.contains(hash) - } - - pub fn base(&self) -> &HashDb { - &self.base - } - - pub fn overlay_len(&self) -> usize { - self.overlay.len() - } -} diff --git a/crates/ltk_hashdb/src/layered.rs b/crates/ltk_hashdb/src/layered.rs index 816953c..8e3ef32 100644 --- a/crates/ltk_hashdb/src/layered.rs +++ b/crates/ltk_hashdb/src/layered.rs @@ -15,9 +15,9 @@ fn base_config(db: &HashDb) -> (KeyWidth, HashKind, Casing) { /// A writable in-memory overlay on top of ordered read-only [`HashDb`] bases. /// /// Lookups consult the overlay first, then each base in push order; the first hit -/// wins. Base files are never mutated. This generalises [`ExtendedHashDb`] (one -/// base) to the N-base case consumers need when several tables (e.g. League's -/// `game` and `lcu`) sit under one overlay. +/// wins. Base files are never mutated. One base covers the "table plus my own +/// runtime hashes" case; several cover the one consumers reach for when a workload +/// spans more than one table (e.g. League's `game` and `lcu` under one overlay). /// /// # Base configuration invariant /// @@ -31,8 +31,6 @@ fn base_config(db: &HashDb) -> (KeyWidth, HashKind, Casing) { /// [`from_bases`](Self::from_bases) `debug_assert!` this; release builds skip the /// check. League's `game`/`lcu` tables are uniform (XXH64 / U64 / case-insensitive), /// so the common path always satisfies it. -/// -/// [`ExtendedHashDb`]: crate::ExtendedHashDb #[derive(Default)] pub struct LayeredHashDb { overlay: HashMap>, diff --git a/crates/ltk_hashdb/src/lib.rs b/crates/ltk_hashdb/src/lib.rs index edb17b8..77cae87 100644 --- a/crates/ltk_hashdb/src/lib.rs +++ b/crates/ltk_hashdb/src/lib.rs @@ -9,7 +9,6 @@ //! See `docs/FORMAT.md` for the byte-level spec. mod error; -mod extended; mod hash; mod header; mod layered; @@ -17,7 +16,6 @@ mod reader; mod writer; pub use error::{BuildError, OpenError, VerifyError}; -pub use extended::ExtendedHashDb; pub use hash::{Casing, HashKind}; pub use header::{FORMAT_VERSION, HEADER_SIZE, MAGIC}; pub use layered::LayeredHashDb; diff --git a/crates/ltk_hashdb/tests/roundtrip.rs b/crates/ltk_hashdb/tests/roundtrip.rs index b96a0db..30b99d9 100644 --- a/crates/ltk_hashdb/tests/roundtrip.rs +++ b/crates/ltk_hashdb/tests/roundtrip.rs @@ -4,7 +4,7 @@ use std::borrow::Cow; use std::io::Cursor; use ltk_hashdb::{ - BuildError, Casing, Compression, ExtendedHashDb, HashDb, HashDbWriter, HashKind, KeyWidth, + BuildError, Casing, Compression, HashDb, HashDbWriter, HashKind, KeyWidth, LayeredHashDb, OpenError, VerifyError, }; @@ -42,13 +42,14 @@ const GAME_ENTRIES: &[(u64, &str)] = &[ ), ]; -/// `docs/CONSUMERS.md` promises a `HashDb` can be shared across threads -/// (all lookups take `&self`); keep this a compile-time guarantee. +/// `docs/CONSUMERS.md` promises a reader can be shared across threads (all +/// lookups take `&self`); keep this a compile-time guarantee for every public +/// one, `LayeredHashDb` included - it is the type most consumers hold. #[test] -fn hashdb_is_send_sync() { +fn readers_are_send_sync() { fn assert_send_sync() {} assert_send_sync::(); - assert_send_sync::(); + assert_send_sync::(); } #[test] @@ -335,28 +336,28 @@ fn bad_magic_rejected() { } #[test] -fn extended_overlay_first_then_base() { +fn overlay_shadows_a_real_base_table() { let bytes = build(KeyWidth::U64, HashKind::Xxh64, GAME_ENTRIES); let db = HashDb::open_bytes(bytes).expect("open"); - let mut ext = ExtendedHashDb::new(db); + let mut layered = LayeredHashDb::from_bases(vec![db]); // Base entries still resolve. assert_eq!( - ext.get(1).as_deref(), + layered.get(1).as_deref(), Some("assets/characters/aatrox/aatrox.bin") ); // Overlay shadows the base. - ext.insert(1, "overridden/path.bin"); - assert_eq!(ext.get(1).as_deref(), Some("overridden/path.bin")); + layered.insert(1, "overridden/path.bin"); + assert_eq!(layered.get(1).as_deref(), Some("overridden/path.bin")); - // insert_path hashes with the base table's algorithm. + // insert_path hashes with the first base's algorithm. let path = "assets/custom/mod/thing.dds"; - let h = ext.insert_path(path); - assert_eq!(h, ext.base().hash_path(path)); - assert_eq!(ext.get(h).as_deref(), Some(path)); - assert!(ext.contains(h)); - assert_eq!(ext.overlay_len(), 2); + let h = layered.insert_path(path).expect("has a base"); + assert_eq!(h, layered.bases()[0].hash_path(path)); + assert_eq!(layered.get(h).as_deref(), Some(path)); + assert!(layered.contains(h)); + assert_eq!(layered.overlay_len(), 2); } #[test] diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 69e4ba3..82035f3 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -141,33 +141,32 @@ size in private memory, so reach for it last. ## Extending a table with custom hashes Mod tooling often introduces paths the community tables don't know. The sanctioned way -is `ExtendedHashDb` - an in-memory overlay consulted before the immutable base, so you -don't hand-roll a second map plus fallback: +is `LayeredHashDb` - an in-memory overlay consulted before one or more immutable base +tables, so you don't hand-roll a second map plus fallback: ```rust -use ltk_hashdb::ExtendedHashDb; +use ltk_hashdb::LayeredHashDb; -let mut ext = ExtendedHashDb::new(store.open(Table::Game)?); +let mut db = LayeredHashDb::from_bases(vec![store.open(Table::Game)?]); -// Hashes with the base table's algorithm and returns the hash: -let h = ext.insert_path("assets/mymod/custom.dds"); -ext.insert(precomputed_hash, "assets/mymod/other.bin"); // or bring your own hash -ext.extend(pairs); // or bulk-load +// Hashes with the first base's algorithm and returns the hash: +let h = db.insert_path("assets/mymod/custom.dds").expect("has a base"); +db.insert(precomputed_hash, "assets/mymod/other.bin"); // or bring your own hash +db.extend(pairs); // or bulk-load -assert!(ext.contains(h)); -let path = ext.get(h); // overlay first, then base +assert!(db.contains(h)); +let path = db.get(h); // overlay first, then each base in order ``` -The base file is never mutated. `ext.base()` exposes the underlying `HashDb`; -`ext.overlay_len()` counts overlay-only entries. Overlay entries are per-process and -not persisted - if you want them shared or durable, contribute them upstream to the +Base files are never mutated. `db.bases()` exposes the underlying tables in priority +order; `db.overlay_len()` counts overlay-only entries. Overlay entries are per-process +and not persisted - if you want them shared or durable, contribute them upstream to the CommunityDragon txt lists (the canonical source). ### Layering several base tables under one overlay -When a workload spans **more than one** table - WAD chunk resolution consults both -`Game` and `Lcu` - reach for `LayeredHashDb` instead of hand-rolling a `Vec` -plus fallback. It is `ExtendedHashDb` generalised to N ordered bases: lookups consult +The same type takes N bases, which is what you want when a workload spans **more than +one** table - WAD chunk resolution consults both `Game` and `Lcu`. Lookups consult the overlay first, then each base in push order, first hit wins. ```rust From 4fa431af90d65c116ae02444b5feebf408d4667a Mon Sep 17 00:00:00 2001 From: Crauzer Date: Mon, 24 Aug 2026 23:32:48 +0200 Subject: [PATCH 03/10] feat(hashdb)!: return PathRef from lookups and cache decompressed frames --- crates/ltk_hashdb/src/cache.rs | 243 +++++++++++ crates/ltk_hashdb/src/layered.rs | 13 +- crates/ltk_hashdb/src/lib.rs | 5 +- crates/ltk_hashdb/src/path.rs | 263 ++++++++++++ crates/ltk_hashdb/src/reader.rs | 586 ++++++++++++++++++++------- crates/ltk_hashdb/tests/roundtrip.rs | 13 +- 6 files changed, 956 insertions(+), 167 deletions(-) create mode 100644 crates/ltk_hashdb/src/cache.rs create mode 100644 crates/ltk_hashdb/src/path.rs diff --git a/crates/ltk_hashdb/src/cache.rs b/crates/ltk_hashdb/src/cache.rs new file mode 100644 index 0000000..f05b214 --- /dev/null +++ b/crates/ltk_hashdb/src/cache.rs @@ -0,0 +1,243 @@ +//! Shared, byte-capped store of decompressed arena frames. +//! +//! A published table is immutable, so a decompressed frame never goes stale - this is +//! pure memoisation with nothing to invalidate. Frames are keyed by a dense, contiguous +//! index, which lets the store be an N-way set-associative table sized once at open: +//! no map, no eviction list, no per-insert allocation, and one lock per set, so +//! concurrent readers stay off each other's backs without a sharding decision. +//! +//! Buffers from evicted frames are recycled, so a steady-state miss decompresses into a +//! buffer that already exists instead of allocating a new one. + +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, PoisonError}; + +/// One decompressed arena frame, shared by every path resolved out of it. +pub(crate) struct Frame(Vec); + +impl Frame { + pub(crate) fn bytes(&self) -> &[u8] { + &self.0 + } +} + +impl From> for Frame { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +/// Frames per set. Four is enough associativity that the arena's forward-walking +/// access pattern never thrashes, and small enough that a set scan stays trivial. +const WAYS: usize = 4; + +/// Recycled buffers kept for reuse. Scales with concurrent decompressions, not with +/// the cache size - a buffer only sits here between one frame's eviction and the next +/// frame's decompression. +const SPARE_BUFFERS: usize = 8; + +/// A fixed-size set-associative cache of decompressed frames. +pub(crate) struct FrameCache { + /// Empty when caching is disabled; every operation then no-ops. + sets: Box<[Mutex<[Slot; WAYS]>]>, + + /// Monotonic access counter; a slot's last value orders eviction within its set. + clock: AtomicU64, + + spare: Mutex>>, +} + +#[derive(Default)] +struct Slot { + /// Meaningful only while `frame` is `Some`. + index: u32, + + used: u64, + + frame: Option>, +} + +impl FrameCache { + /// A cache holding at most `budget` bytes worth of `frame_size` frames. + /// + /// Never sized beyond the `frames` the file actually has, so a small table costs a + /// small table's worth of slots. A zero budget (or a table with no frames at all) + /// disables caching outright. + pub(crate) fn new(budget: usize, frame_size: usize, frames: usize) -> Self { + let wanted = match budget.checked_div(frame_size) { + Some(n) => n.min(frames), + None => 0, + }; + + Self { + sets: (0..wanted.div_ceil(WAYS)) + .map(|_| Mutex::new(std::array::from_fn(|_| Slot::default()))) + .collect(), + clock: AtomicU64::new(0), + spare: Mutex::new(Vec::new()), + } + } + + /// How many frames this cache can hold at once; `0` when disabled. + pub(crate) fn capacity(&self) -> usize { + self.sets.len() * WAYS + } + + /// The cached frame `index`, if it is still resident. + pub(crate) fn get(&self, index: u32) -> Option> { + let mut slots = self + .set(index)? + .lock() + .unwrap_or_else(PoisonError::into_inner); + let slot = slots + .iter_mut() + .find(|slot| slot.frame.is_some() && slot.index == index)?; + + slot.used = self.clock.fetch_add(1, Ordering::Relaxed); + slot.frame.clone() + } + + /// Publish `frame` under `index`, evicting the coldest frame in its set. + pub(crate) fn insert(&self, index: u32, frame: &Arc) { + let Some(set) = self.set(index) else { return }; + let used = self.clock.fetch_add(1, Ordering::Relaxed); + + let evicted = { + let mut slots = set.lock().unwrap_or_else(PoisonError::into_inner); + // Prefer the slot this frame already occupies (a racing reader decompressed + // it too), then an empty one, then the least recently used. `+ 1` keeps an + // occupied slot from tying with an empty one at clock zero. + let slot = match slots + .iter() + .position(|slot| slot.frame.is_some() && slot.index == index) + { + Some(position) => &mut slots[position], + None => slots + .iter_mut() + .min_by_key(|slot| match slot.frame { + None => 0, + Some(_) => slot.used.saturating_add(1), + }) + .expect("WAYS is nonzero"), + }; + + slot.index = index; + slot.used = used; + slot.frame.replace(Arc::clone(frame)) + }; + + if let Some(evicted) = evicted { + self.recycle(evicted); + } + } + + /// A buffer to decompress into, reusing an evicted frame's allocation when there + /// is one to reuse. + pub(crate) fn take_buffer(&self, capacity: usize) -> Vec { + let mut spare = self.spare.lock().unwrap_or_else(PoisonError::into_inner); + match spare.pop() { + Some(mut buffer) => { + buffer.clear(); + buffer.reserve(capacity); + buffer + } + None => Vec::with_capacity(capacity), + } + } + + /// Reclaim an evicted frame's buffer, unless a caller still holds a path into it. + fn recycle(&self, frame: Arc) { + let Some(frame) = Arc::into_inner(frame) else { + return; + }; + + let mut spare = self.spare.lock().unwrap_or_else(PoisonError::into_inner); + if spare.len() < SPARE_BUFFERS { + let mut buffer = frame.0; + buffer.clear(); + spare.push(buffer); + } + } + + fn set(&self, index: u32) -> Option<&Mutex<[Slot; WAYS]>> { + if self.sets.is_empty() { + return None; + } + + Some(&self.sets[index as usize % self.sets.len()]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn frame(byte: u8) -> Arc { + Arc::new(Frame::from(vec![byte; 16])) + } + + #[test] + fn a_zero_budget_disables_caching() { + let cache = FrameCache::new(0, 16, 100); + assert_eq!(cache.capacity(), 0); + + cache.insert(0, &frame(1)); + assert!(cache.get(0).is_none()); + } + + #[test] + fn slots_never_exceed_the_frames_in_the_file() { + // Budget for 1000 frames, but the file only has 3. + let cache = FrameCache::new(16_000, 16, 3); + assert_eq!(cache.capacity(), WAYS); + } + + #[test] + fn frames_round_trip_until_their_set_fills() { + // One set: four ways, so the fifth frame mapping to it evicts the coldest. + let cache = FrameCache::new(16 * WAYS, 16, WAYS); + assert_eq!(cache.sets.len(), 1); + + for i in 0..WAYS as u32 { + cache.insert(i, &frame(i as u8)); + } + // Touch 0 so it is no longer the coldest, then force one eviction. + assert!(cache.get(0).is_some()); + cache.insert(99, &frame(99)); + + assert!(cache.get(0).is_some(), "recently used frame survived"); + assert!(cache.get(99).is_some(), "newly inserted frame is resident"); + assert!(cache.get(1).is_none(), "coldest frame was evicted"); + } + + #[test] + fn evicted_buffers_are_recycled() { + let cache = FrameCache::new(16 * WAYS, 16, WAYS); + for i in 0..WAYS as u32 + 1 { + cache.insert(i, &frame(i as u8)); + } + + // The evicted frame's allocation came back, so this buffer is not a fresh one. + let buffer = cache.take_buffer(16); + assert!(buffer.is_empty()); + assert!(buffer.capacity() >= 16); + assert!( + cache.spare.lock().unwrap().is_empty(), + "buffer was handed out" + ); + } + + /// A frame still held by a caller must not be recycled out from under them. + #[test] + fn a_held_frame_is_not_recycled() { + let cache = FrameCache::new(16 * WAYS, 16, WAYS); + let held = frame(0); + cache.insert(0, &held); + for i in 1..WAYS as u32 + 1 { + cache.insert(i, &frame(i as u8)); + } + + assert_eq!(held.bytes(), &[0u8; 16]); + assert!(cache.spare.lock().unwrap().is_empty()); + } +} diff --git a/crates/ltk_hashdb/src/layered.rs b/crates/ltk_hashdb/src/layered.rs index 8e3ef32..cdd5639 100644 --- a/crates/ltk_hashdb/src/layered.rs +++ b/crates/ltk_hashdb/src/layered.rs @@ -1,9 +1,8 @@ //! An in-memory overlay layered over an ordered list of read-only base tables. -use std::borrow::Cow; use std::collections::HashMap; -use crate::{Casing, HashDb, HashKind, KeyWidth}; +use crate::{Casing, HashDb, HashKind, KeyWidth, PathRef}; /// The key configuration a base hashes under. Every base in a [`LayeredHashDb`] /// must agree on this triple; a base that diverges can never be hit by a caller's @@ -105,9 +104,9 @@ impl LayeredHashDb { } /// Overlay first, then each base in push order; the first hit wins. - pub fn get(&self, hash: u64) -> Option> { + pub fn get(&self, hash: u64) -> Option> { if let Some(path) = self.overlay.get(&hash) { - return Some(Cow::Borrowed(&**path)); + return Some(PathRef::borrowed(path)); } self.bases.iter().find_map(|base| base.get(hash)) } @@ -124,8 +123,8 @@ impl LayeredHashDb { pub fn get_batch<'a>( &'a self, hashes: &'a [u64], - ) -> impl Iterator>)> + 'a { - let mut results: Vec>> = Vec::new(); + ) -> impl Iterator>)> + 'a { + let mut results: Vec>> = Vec::new(); results.resize_with(hashes.len(), || None); // Layer 0: overlay, O(1) per hash. Positions still missing stay in @@ -133,7 +132,7 @@ impl LayeredHashDb { let mut residual: Vec = Vec::new(); for (i, &h) in hashes.iter().enumerate() { match self.overlay.get(&h) { - Some(path) => results[i] = Some(Cow::Borrowed(&**path)), + Some(path) => results[i] = Some(PathRef::borrowed(path)), None => residual.push(i), } } diff --git a/crates/ltk_hashdb/src/lib.rs b/crates/ltk_hashdb/src/lib.rs index 77cae87..72fa33a 100644 --- a/crates/ltk_hashdb/src/lib.rs +++ b/crates/ltk_hashdb/src/lib.rs @@ -8,10 +8,12 @@ //! //! See `docs/FORMAT.md` for the byte-level spec. +mod cache; mod error; mod hash; mod header; mod layered; +mod path; mod reader; mod writer; @@ -19,7 +21,8 @@ pub use error::{BuildError, OpenError, VerifyError}; pub use hash::{Casing, HashKind}; pub use header::{FORMAT_VERSION, HEADER_SIZE, MAGIC}; pub use layered::LayeredHashDb; -pub use reader::HashDb; +pub use path::PathRef; +pub use reader::{HashDb, HashDbOptions, DEFAULT_FRAME_CACHE_BYTES}; pub use writer::{BuildStats, HashDbWriter}; /// Width of the integer keys in a table. diff --git a/crates/ltk_hashdb/src/path.rs b/crates/ltk_hashdb/src/path.rs new file mode 100644 index 0000000..c57c329 --- /dev/null +++ b/crates/ltk_hashdb/src/path.rs @@ -0,0 +1,263 @@ +//! [`PathRef`]: a resolved path, borrowed from wherever its bytes already live. + +use std::borrow::Borrow; +use std::fmt; +use std::sync::Arc; + +use crate::cache::Frame; + +/// A path resolved from a table, without copying its bytes. +/// +/// Derefs to [`str`], so it reads like one at the call site - `path.ends_with(".dds")`, +/// `&*path`, `path.to_owned()`, and `Option::as_deref` all work unchanged. What it adds +/// is where the bytes come from: a raw arena lends them straight out of the mmap, and a +/// compressed arena lends them out of a decompressed frame the table is already holding. +/// Neither allocates. Only two cases do: an entry that straddles a frame boundary, and +/// one whose bytes are not valid UTF-8 (replaced lossily, as [`HashDb::get`] has always +/// done). +/// +/// A `PathRef` keeps its frame alive, so holding many of them across lookups pins that +/// many frames in memory. Call [`into_owned`](Self::into_owned) to detach one. +/// +/// [`HashDb::get`]: crate::HashDb::get +#[derive(Clone)] +pub struct PathRef<'a> { + repr: Repr<'a>, +} + +#[derive(Clone)] +enum Repr<'a> { + /// Straight out of a raw arena's mmap, or out of a `LayeredHashDb` overlay. + Borrowed(&'a str), + + /// A range of a shared decompressed frame, kept alive by the `Arc`. + /// + /// The range is always valid UTF-8: `PathRef::from_frame` checks it and falls + /// back to `Owned` when it is not. + Frame { + frame: Arc, + start: u32, + len: u32, + }, + + /// Spliced across a frame boundary, or lossily replaced invalid UTF-8. + Owned(Box), +} + +impl<'a> PathRef<'a> { + /// Borrow a path that is already a `&str` (raw arena, overlay entry). + pub(crate) fn borrowed(path: &'a str) -> Self { + Self { + repr: Repr::Borrowed(path), + } + } + + /// Take ownership of a path that had to be built (spliced or lossy). + pub(crate) fn owned(path: impl Into>) -> Self { + Self { + repr: Repr::Owned(path.into()), + } + } + + /// Borrow `frame[start..start + len]`, keeping the frame alive. + /// + /// Falls back to an owned, lossily-replaced copy when those bytes are not valid + /// UTF-8 - which also establishes the invariant [`Deref`](std::ops::Deref) relies on. + pub(crate) fn from_frame(frame: Arc, start: usize, len: usize) -> Self { + let bytes = &frame.bytes()[start..start + len]; + match std::str::from_utf8(bytes) { + Ok(_) => Self { + repr: Repr::Frame { + start: start as u32, + len: len as u32, + frame, + }, + }, + Err(_) => Self::owned(String::from_utf8_lossy(bytes).into_owned()), + } + } + + /// The path as a plain `&str`. + pub fn as_str(&self) -> &str { + match &self.repr { + Repr::Borrowed(path) => path, + Repr::Frame { frame, start, len } => { + let bytes = &frame.bytes()[*start as usize..(*start + *len) as usize]; + // SAFETY: `Repr::Frame` is built only by `from_frame`, which validates + // exactly this range as UTF-8 and takes the `Owned` branch when it fails. + // The frame is immutable behind its `Arc`, so those bytes cannot change + // between that check and this read. + unsafe { std::str::from_utf8_unchecked(bytes) } + } + Repr::Owned(path) => path, + } + } + + /// Whether these bytes were copied rather than borrowed. + /// + /// False for the paths a table lends out directly - from a raw arena's mmap, from + /// a cached frame, or from an overlay - which is every path in a well-formed table + /// but the two exceptions: one spliced across a frame boundary, and one whose bytes + /// were not valid UTF-8 and got lossily replaced. Assert on it to pin down that a + /// hot path allocates nothing; [`into_owned`](Self::into_owned) is free when it is + /// true. + pub fn is_owned(&self) -> bool { + matches!(self.repr, Repr::Owned(_)) + } + + /// Detach the path from its frame, copying it if it was borrowed. + /// + /// Use this to keep a handful of paths out of a large scan without pinning the + /// frames they came from. + pub fn into_owned(self) -> String { + match self.repr { + Repr::Owned(path) => path.into_string(), + _ => self.as_str().to_owned(), + } + } +} + +impl std::ops::Deref for PathRef<'_> { + type Target = str; + + fn deref(&self) -> &str { + self.as_str() + } +} + +impl AsRef for PathRef<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Borrow for PathRef<'_> { + fn borrow(&self) -> &str { + self.as_str() + } +} + +/// Prints the path itself, quoted - a `PathRef` is a string as far as a reader cares. +impl fmt::Debug for PathRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +impl fmt::Display for PathRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl PartialEq for PathRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} + +impl Eq for PathRef<'_> {} + +impl PartialOrd for PathRef<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PathRef<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl std::hash::Hash for PathRef<'_> { + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } +} + +impl PartialEq for PathRef<'_> { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl PartialEq<&str> for PathRef<'_> { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl PartialEq for PathRef<'_> { + fn eq(&self, other: &String) -> bool { + self.as_str() == other.as_str() + } +} + +impl PartialEq> for str { + fn eq(&self, other: &PathRef<'_>) -> bool { + self == other.as_str() + } +} + +impl PartialEq> for &str { + fn eq(&self, other: &PathRef<'_>) -> bool { + *self == other.as_str() + } +} + +impl PartialEq> for String { + fn eq(&self, other: &PathRef<'_>) -> bool { + self.as_str() == other.as_str() + } +} + +impl From> for String { + fn from(path: PathRef<'_>) -> Self { + path.into_owned() + } +} + +impl From> for Box { + fn from(path: PathRef<'_>) -> Self { + match path.repr { + Repr::Owned(path) => path, + _ => path.as_str().into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn borrowed_and_owned_compare_and_print_alike() { + let borrowed = PathRef::borrowed("assets/foo.dds"); + let owned = PathRef::owned("assets/foo.dds"); + + assert_eq!(borrowed, owned); + assert_eq!(borrowed, "assets/foo.dds"); + assert_eq!("assets/foo.dds", borrowed); + assert_eq!(borrowed.to_string(), "assets/foo.dds"); + assert_eq!(format!("{owned:?}"), "\"assets/foo.dds\""); + + // Deref means str's whole API is available without an accessor. + assert!(borrowed.ends_with(".dds")); + assert_eq!(Some(owned).as_deref(), Some("assets/foo.dds")); + } + + /// A frame-backed path borrows its bytes; invalid UTF-8 falls back to a lossy copy. + #[test] + fn frame_backed_paths_borrow_or_replace() { + let frame = Arc::new(Frame::from(b"aa/one.binbb/two.bin".to_vec())); + let path = PathRef::from_frame(Arc::clone(&frame), 10, 10); + assert_eq!(path, "bb/two.bin"); + assert!(matches!(path.repr, Repr::Frame { .. })); + + let invalid = Arc::new(Frame::from(vec![b'a', 0xff, b'b'])); + let path = PathRef::from_frame(invalid, 0, 3); + assert_eq!(path, "a\u{fffd}b"); + assert!(matches!(path.repr, Repr::Owned(_))); + } +} diff --git a/crates/ltk_hashdb/src/reader.rs b/crates/ltk_hashdb/src/reader.rs index 4f412e3..3433ce1 100644 --- a/crates/ltk_hashdb/src/reader.rs +++ b/crates/ltk_hashdb/src/reader.rs @@ -1,24 +1,115 @@ //! Read-only, mmap-backed `.hashdb` hash table. use std::borrow::Cow; +use std::cell::RefCell; use std::collections::HashMap; +use std::fmt; use std::fs::File; use std::ops::Range; use std::path::Path; use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; use xxhash_rust::xxh3::Xxh3; use zeekstd::SeekTable; +use crate::cache::{Frame, FrameCache}; use crate::header::Header; -use crate::{Casing, HashKind, KeyWidth, OpenError, VerifyError}; +use crate::{Casing, HashKind, KeyWidth, OpenError, PathRef, VerifyError}; + +/// Decompressed frame bytes a table caches by default: 4 MiB, i.e. 256 frames at the +/// published 16 KiB frame size. +/// +/// Enough that a consumer walking a WAD's chunks in path order keeps hitting the frames +/// it just decompressed, small enough to be an unremarkable cost per open table. +pub const DEFAULT_FRAME_CACHE_BYTES: usize = 4 << 20; + +/// Open-time knobs for a [`HashDb`]. +/// +/// ```no_run +/// # use ltk_hashdb::HashDb; +/// // A table used for one bulk pass needs no cache; one behind a UI wants a bigger one. +/// let scratch = HashDb::options().frame_cache_bytes(0).open("game.lhdb")?; +/// let resident = HashDb::options().frame_cache_bytes(32 << 20).open("game.lhdb")?; +/// # Ok::<(), ltk_hashdb::OpenError>(()) +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct HashDbOptions { + frame_cache_bytes: usize, +} + +impl Default for HashDbOptions { + fn default() -> Self { + Self { + frame_cache_bytes: DEFAULT_FRAME_CACHE_BYTES, + } + } +} + +impl HashDbOptions { + pub fn new() -> Self { + Self::default() + } + + /// Cap the decompressed frames this table keeps cached; `0` disables caching. + /// + /// Only compressed arenas cache anything - a raw arena is read straight out of the + /// mmap, so the budget is ignored. + pub fn frame_cache_bytes(mut self, bytes: usize) -> Self { + self.frame_cache_bytes = bytes; + self + } + + /// mmap `path` read-only and validate it. + /// + /// # Errors + /// + /// Fails if the file cannot be opened or mapped, or if the header or section + /// bounds do not validate - see [`OpenError`]. + /// + /// # Safety of the mapping + /// + /// See [`HashDb::open`] for the obligation a caller takes on by mapping a file. + pub fn open(self, path: impl AsRef) -> Result { + let file = File::open(path)?; + // SAFETY: `Mmap::map` is unsound if the mapped bytes change underneath us, so + // every writer of a `.hashdb` this crate ships must leave published files + // immutable. `ltk_mimir_cache` upholds that: `commit` writes a new versioned + // filename and renames it into place rather than over an existing one, and `gc` + // only unlinks (an unlinked file keeps its pages alive for anyone still mapping + // it). A caller mapping a file some other process may truncate or rewrite in + // place gets undefined behaviour, not an error - `HashDb::open` documents this. + let mmap = unsafe { memmap2::Mmap::map(&file)? }; + HashDb::from_backing(Backing::Mmap(mmap), self) + } + + /// Open an in-memory image (embedded tables, tests). + /// + /// # Errors + /// + /// Fails if the header or section bounds do not validate - see [`OpenError`]. + pub fn open_bytes(self, bytes: impl Into>) -> Result { + HashDb::from_backing(Backing::Bytes(bytes.into()), self) + } +} /// A read-only `.hashdb` hash table. /// /// `open` validates the (untrusted) header and section bounds. Lookups binary-search /// the mmap'd key array, so a miss never touches the arena; a hit on a compressed -/// arena decompresses only the containing frame(s). +/// arena decompresses only the containing frame, and keeps it cached for the lookups +/// after it. +/// +/// Cloning is cheap - every clone shares one mapping, one seek table, and one frame +/// cache - so a `HashDb` is passed around rather than reopened. +#[derive(Clone)] pub struct HashDb { + inner: Arc, +} + +/// Everything a table's clones share: the bytes, where the sections are, and the +/// frames decompressed out of them so far. +struct Inner { backing: Backing, header: Header, keys: Range, @@ -29,6 +120,8 @@ pub struct HashDb { /// Present iff the arena is a zeekstd seekable stream. seek_table: Option, + cache: FrameCache, + /// Frames decompressed so far; misses must never bump it (see unit tests). decompressions: AtomicU64, } @@ -47,24 +140,102 @@ impl Backing { } } -/// A decompressed run of frames (raw-arena range it covers + the bytes), so -/// in-order consumers decompress each frame once rather than once per entry. -type FrameCache = Option<(Range, Vec)>; +/// Entry bytes as they were found: lent by the mmap, lent by a cached frame, or +/// spliced together because the entry crossed a frame boundary. +enum Bytes<'a> { + Borrowed(&'a [u8]), + Frame { + frame: Arc, + start: usize, + len: usize, + }, + Spliced(Vec), +} + +impl Bytes<'_> { + fn as_slice(&self) -> &[u8] { + match self { + Self::Borrowed(bytes) => bytes, + Self::Frame { frame, start, len } => &frame.bytes()[*start..*start + *len], + Self::Spliced(bytes) => bytes, + } + } +} + +impl<'a> From> for PathRef<'a> { + fn from(bytes: Bytes<'a>) -> Self { + match bytes { + Bytes::Borrowed(bytes) => match String::from_utf8_lossy(bytes) { + Cow::Borrowed(path) => PathRef::borrowed(path), + Cow::Owned(path) => PathRef::owned(path), + }, + Bytes::Frame { frame, start, len } => PathRef::from_frame(frame, start, len), + Bytes::Spliced(bytes) => match String::from_utf8(bytes) { + Ok(path) => PathRef::owned(path), + Err(e) => PathRef::owned(String::from_utf8_lossy(e.as_bytes()).into_owned()), + }, + } + } +} + +thread_local! { + /// One decompression context per thread: creating one per frame would cost more + /// than the decompression, and sharing one across threads would serialize them. + static DCTX: RefCell>> = const { RefCell::new(None) }; +} + +/// Run `f` against this thread's decompression context, creating it on first use. +fn with_dctx( + f: impl FnOnce(&mut zstd::bulk::Decompressor<'static>) -> Result, +) -> Result { + DCTX.with(|cell| { + let mut slot = cell.borrow_mut(); + let dctx = match &mut *slot { + Some(dctx) => dctx, + slot => slot.insert(zstd::bulk::Decompressor::new()?), + }; + + f(dctx) + }) +} impl HashDb { /// mmap `path` read-only and validate it. + /// + /// # Errors + /// + /// Fails if the file cannot be opened or mapped, or if the header or section + /// bounds do not validate - see [`OpenError`]. + /// + /// # Safety of the mapping + /// + /// The mapping is only sound while the file's bytes do not change. Published + /// `.hashdb` files are immutable by contract - a new version ships as a new + /// filename, and old versions are only ever unlinked - which is what discharges + /// the `unsafe` here. If you manage your own tables, uphold the same rule: never + /// truncate or rewrite a file in place while a `HashDb` maps it. Doing so is + /// undefined behaviour rather than an error you can catch. Build to a temporary + /// name and rename, or use [`open_bytes`](HashDb::open_bytes) for images you + /// mutate. pub fn open(path: impl AsRef) -> Result { - let file = File::open(path)?; - let mmap = unsafe { memmap2::Mmap::map(&file)? }; - Self::from_backing(Backing::Mmap(mmap)) + HashDbOptions::default().open(path) } /// Open an in-memory image (embedded tables, tests). + /// + /// # Errors + /// + /// Fails if the header or section bounds do not validate - see [`OpenError`]. pub fn open_bytes(bytes: impl Into>) -> Result { - Self::from_backing(Backing::Bytes(bytes.into())) + HashDbOptions::default().open_bytes(bytes) + } + + /// Open-time knobs - the frame cache budget, today. + pub fn options() -> HashDbOptions { + HashDbOptions::default() } - fn from_backing(backing: Backing) -> Result { + fn from_backing(backing: Backing, options: HashDbOptions) -> Result { let data = backing.bytes(); let header = Header::decode(data)?; @@ -123,30 +294,45 @@ impl HashDb { None }; + // Size the cache to this table: never more slots than it has frames, and + // nothing at all for a raw arena, which is read straight out of the mmap. + let cache = match &seek_table { + Some(st) => FrameCache::new( + options.frame_cache_bytes, + st.max_frame_size_decomp() as usize, + st.num_frames() as usize, + ), + None => FrameCache::new(0, 0, 0), + }; + // Per-entry extents aren't validated here (keeps `open` O(1)); each read // bounds-checks its own, reading out-of-bounds as a miss. `verify()` reports them. Ok(Self { - backing, - header, - keys, - offsets, - lengths, - arena, - seek_table, - decompressions: AtomicU64::new(0), + inner: Arc::new(Inner { + backing, + header, + keys, + offsets, + lengths, + arena, + seek_table, + cache, + decompressions: AtomicU64::new(0), + }), }) } - /// Look up a hash. Raw arenas borrow the path from the mmap; compressed arenas - /// decompress its frame(s). Returns `None` for a miss or an entry that won't - /// decompress (corrupt file - see [`HashDb::verify`]). - pub fn get(&self, hash: u64) -> Option> { - self.index_of(hash).and_then(|i| self.str_at(i)) + /// Look up a hash. The path is lent by the mmap or by a cached frame, so a hit + /// allocates nothing. Returns `None` for a miss or an entry that won't decompress + /// (corrupt file - see [`HashDb::verify`]). + pub fn get(&self, hash: u64) -> Option> { + let i = self.inner.index_of(hash)?; + self.inner.bytes_at(i).ok().flatten().map(PathRef::from) } /// Membership test; never touches the arena. pub fn contains(&self, hash: u64) -> bool { - self.index_of(hash).is_some() + self.inner.index_of(hash).is_some() } /// Bulk lookup. Hits resolve in arena order so each frame decompresses at most @@ -154,147 +340,166 @@ impl HashDb { pub fn get_batch<'a>( &'a self, hashes: &[u64], - ) -> impl Iterator>)> + 'a { - let indices: Vec> = hashes.iter().map(|&h| self.index_of(h)).collect(); + ) -> impl Iterator>)> + 'a { + let indices: Vec> = hashes.iter().map(|&h| self.inner.index_of(h)).collect(); let mut order: Vec = (0..hashes.len()).collect(); // Resolve hits in arena order (misses sort last) so each frame decompresses once. - order.sort_unstable_by_key(|&p| indices[p].map_or(u64::MAX, |i| self.offset_at(i))); + order.sort_unstable_by_key(|&p| indices[p].map_or(u64::MAX, |i| self.inner.offset_at(i))); - let mut results: Vec>> = Vec::new(); + let mut results: Vec>> = Vec::new(); results.resize_with(hashes.len(), || None); - let mut cache: FrameCache = None; for p in order { if let Some(i) = indices[p] { - results[p] = self.str_at_cached(i, &mut cache); + results[p] = self.inner.bytes_at(i).ok().flatten().map(PathRef::from); } } - let out: Vec<(u64, Option>)> = hashes.iter().copied().zip(results).collect(); + let out: Vec<(u64, Option>)> = hashes.iter().copied().zip(results).collect(); out.into_iter() } pub fn len(&self) -> usize { - self.header.entry_count as usize + self.inner.len() } pub fn is_empty(&self) -> bool { - self.header.entry_count == 0 + self.inner.header.entry_count == 0 } pub fn key_width(&self) -> KeyWidth { - self.header.key_width + self.inner.header.key_width } pub fn hash_kind(&self) -> HashKind { - self.header.hash_kind + self.inner.header.hash_kind } /// Whether the keys hash the lowercased path (from the `case_insensitive` /// header flag). pub fn casing(&self) -> Casing { - self.header.casing() + self.inner.header.casing() } /// Whether the arena is zeekstd-compressed on disk. pub fn is_compressed(&self) -> bool { - self.header.arena_compressed() + self.inner.header.arena_compressed() } /// Total length of all path strings (the raw arena), in bytes. pub fn arena_decompressed_size(&self) -> u64 { - self.header.arena_decompressed_size + self.inner.header.arena_decompressed_size } /// Bytes the arena occupies on disk (== decompressed size for raw arenas). pub fn arena_compressed_size(&self) -> u64 { - self.header.arena_compressed_size + self.inner.header.arena_compressed_size } /// Number of zstd frames this table has decompressed over its lifetime. #[cfg(test)] pub(crate) fn decompressions(&self) -> u64 { - self.decompressions.load(Ordering::Relaxed) + self.inner.decompressions.load(Ordering::Relaxed) } /// Hash a path string with **this table's** algorithm and casing rule (from /// the `hash_kind` header field - falling back on key width when /// unspecified - and the `case_insensitive` flag). pub fn hash_path(&self, path: &str) -> u64 { - self.header - .hash_kind - .hash(path, self.header.casing(), self.header.key_width) + self.inner.header.hash_kind.hash( + path, + self.inner.header.casing(), + self.inner.header.key_width, + ) } /// Iterate entries in arena order (path order, **not** key order) so each frame /// decompresses once. Entries that fail to decompress are skipped; `verify()` reports them. - pub fn iter(&self) -> impl Iterator)> { - let mut cache: FrameCache = None; - self.arena_order().into_iter().filter_map(move |i| { - let path = self.str_at_cached(i, &mut cache)?; - Some((self.key_at(i), path)) + pub fn iter(&self) -> impl Iterator)> { + self.inner.arena_order().into_iter().filter_map(move |i| { + let bytes = self.inner.bytes_at(i).ok().flatten()?; + Some((self.inner.key_at(i), PathRef::from(bytes))) }) } /// Opt-in fully-resident mode: decode everything into an owned map. pub fn load_all(&self) -> HashMap> { - self.iter() - .map(|(k, s)| (k, s.into_owned().into_boxed_str())) - .collect() + self.iter().map(|(k, path)| (k, path.into())).collect() } /// Full integrity check, skipped by `open`: /// - xxh3 checksum over the stored sections /// - keys strictly ascending /// - every entry in bounds and valid UTF-8 in the arena + /// + /// # Errors + /// + /// Fails with [`VerifyError`] on the first problem it finds: a checksum mismatch, + /// out-of-order keys, an entry that runs past the arena, one that is not valid + /// UTF-8, or a frame that will not decompress. pub fn verify(&self) -> Result<(), VerifyError> { - let data = self.backing.bytes(); + let inner = &*self.inner; + let data = inner.backing.bytes(); let mut hasher = Xxh3::new(); - hasher.update(&data[self.keys.clone()]); - hasher.update(&data[self.offsets.clone()]); - hasher.update(&data[self.lengths.clone()]); - hasher.update(&data[self.arena.clone()]); - if hasher.digest() != self.header.checksum { + hasher.update(&data[inner.keys.clone()]); + hasher.update(&data[inner.offsets.clone()]); + hasher.update(&data[inner.lengths.clone()]); + hasher.update(&data[inner.arena.clone()]); + if hasher.digest() != inner.header.checksum { return Err(VerifyError::ChecksumMismatch); } - let n = self.len(); + let n = inner.len(); for i in 1..n { - if self.key_at(i - 1) >= self.key_at(i) { + if inner.key_at(i - 1) >= inner.key_at(i) { return Err(VerifyError::Malformed("keys not strictly ascending")); } } - if self.seek_table.is_none() { - let arena = &data[self.arena.clone()]; + // Compressed arenas are walked in arena order so each frame decompresses once + // and only the current run is resident - never the whole arena at once. A raw + // arena is already resident, so key order is as good and needs no permutation. + if inner.seek_table.is_none() { for i in 0..n { - let slice = self - .extent_of(i) - .and_then(|(start, end)| arena.get(start as usize..end as usize)) - .ok_or(VerifyError::Malformed("entry extends past the arena"))?; - if std::str::from_utf8(slice).is_err() { - return Err(VerifyError::Malformed("entry is not valid UTF-8")); - } + inner.verify_entry(i)?; } - return Ok(()); - } - - // Compressed: walk entries in arena order so each frame decompresses once - // and only the current run is resident - never the whole arena at once. - let mut cache: FrameCache = None; - for i in self.arena_order() { - let (start, end) = self - .extent_of(i) - .ok_or(VerifyError::Malformed("entry extends past the arena"))?; - if start == end { - continue; - } - let slice = self.frame_bytes(start, end, &mut cache)?; - if std::str::from_utf8(slice).is_err() { - return Err(VerifyError::Malformed("entry is not valid UTF-8")); + } else { + for i in inner.arena_order() { + inner.verify_entry(i)?; } } + Ok(()) } +} + +impl fmt::Debug for HashDb { + /// Shape only - counts, widths, and flags. Never entries. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = &*self.inner; + f.debug_struct("HashDb") + .field("entries", &inner.header.entry_count) + .field("key_width", &inner.header.key_width) + .field("hash_kind", &inner.header.hash_kind) + .field("casing", &inner.header.casing()) + .field("compressed", &inner.header.arena_compressed()) + .field( + "arena_decompressed_size", + &inner.header.arena_decompressed_size, + ) + .field("arena_compressed_size", &inner.header.arena_compressed_size) + .field( + "frames", + &inner.seek_table.as_ref().map_or(0, SeekTable::num_frames), + ) + .field("cached_frames", &inner.cache.capacity()) + .finish() + } +} + +impl Inner { + fn len(&self) -> usize { + self.header.entry_count as usize + } /// Entry indices sorted by arena offset (path order); walking them this way /// decompresses each frame once, keeping only the current run resident. @@ -339,84 +544,117 @@ impl HashDb { (end <= self.header.arena_decompressed_size).then_some((start, end)) } - /// The path for entry `i`, or `None` if out of bounds. Invalid UTF-8 is replaced - /// lossily rather than panicking; `verify()` reports both. - fn str_at(&self, i: usize) -> Option> { - self.str_at_cached(i, &mut None) - } + /// Entry `i`'s bytes, or `None` if its extent runs past the arena. + /// + /// Errors mean a corrupt file ([`VerifyError`]); the lookup path swallows them + /// into a miss, only `verify` surfaces them. + fn bytes_at(&self, i: usize) -> Result>, VerifyError> { + let Some((start, end)) = self.extent_of(i) else { + return Ok(None); + }; - fn str_at_cached(&self, i: usize, cache: &mut FrameCache) -> Option> { - let (start, end) = self.extent_of(i)?; - if self.seek_table.is_none() { + let Some(seek_table) = self.seek_table.as_ref() else { let range = self.arena.start + start as usize..self.arena.start + end as usize; - return Some(String::from_utf8_lossy(&self.backing.bytes()[range])); - } + return Ok(Some(Bytes::Borrowed(&self.backing.bytes()[range]))); + }; if start == end { - return Some(Cow::Borrowed("")); + return Ok(Some(Bytes::Borrowed(&[]))); + } + + let first = seek_table.frame_index_decomp(start); + let last = seek_table.frame_index_decomp(end - 1); + let frame = self.frame(first)?; + let frame_start = seek_table.frame_start_decomp(first)?; + let offset = (start - frame_start) as usize; + + if first == last { + let len = (end - start) as usize; + if offset + len > frame.bytes().len() { + return Err(VerifyError::Malformed("entry extends past its frame")); + } + + return Ok(Some(Bytes::Frame { + frame, + start: offset, + len, + })); + } + + // The entry straddles a frame boundary - roughly one entry per frame. Splice + // the pieces into a buffer of its own rather than lending out either frame. + let mut spliced = Vec::with_capacity((end - start) as usize); + let head = frame + .bytes() + .get(offset..) + .ok_or(VerifyError::Malformed("entry starts past its frame"))?; + spliced.extend_from_slice(head); + + for index in first + 1..=last { + let frame = self.frame(index)?; + let frame_start = seek_table.frame_start_decomp(index)?; + let take = (end - frame_start).min(frame.bytes().len() as u64) as usize; + let tail = frame.bytes().get(..take).ok_or(VerifyError::Malformed( + "frame shorter than the seek table says", + ))?; + spliced.extend_from_slice(tail); } - let bytes = self.frame_bytes(start, end, cache).ok()?; - Some(Cow::Owned(String::from_utf8_lossy(bytes).into_owned())) + + Ok(Some(Bytes::Spliced(spliced))) } - /// Decompressed bytes for extent `start..end`, filling `cache` with the - /// containing frame(s) on a miss. Caller must handle the empty (`start == end`) case. - /// - /// Errors mean a corrupt file ([`VerifyError`]); the lookup path swallows - /// them into a miss, only `verify` surfaces them. - fn frame_bytes<'c>( - &self, - start: u64, - end: u64, - cache: &'c mut FrameCache, - ) -> Result<&'c [u8], VerifyError> { - let covered = cache - .as_ref() - .is_some_and(|(range, _)| range.start <= start && end <= range.end); - if !covered { - let st = self.seek_table.as_ref().unwrap(); - let lo = st.frame_index_decomp(start); - let hi = st.frame_index_decomp(end - 1); - let (cov_start, bytes) = self.read_frames(lo, hi)?; - *cache = Some((cov_start..cov_start + bytes.len() as u64, bytes)); + /// Check one entry the way `verify` needs it checked: in bounds and valid UTF-8. + fn verify_entry(&self, i: usize) -> Result<(), VerifyError> { + let bytes = self + .bytes_at(i)? + .ok_or(VerifyError::Malformed("entry extends past the arena"))?; + if std::str::from_utf8(bytes.as_slice()).is_err() { + return Err(VerifyError::Malformed("entry is not valid UTF-8")); } - let (range, bytes) = cache.as_ref().unwrap(); - Ok(&bytes[(start - range.start) as usize..(end - range.start) as usize]) + + Ok(()) } - /// Decompress frames `lo..=hi`, returning the run's decompressed-space start - /// offset plus the bytes. Frame content is untrusted, so every extent and - /// output size is checked. - fn read_frames(&self, lo: u32, hi: u32) -> Result<(u64, Vec), VerifyError> { - let st = self.seek_table.as_ref().expect("compressed arena"); + /// Frame `index`, decompressing it only if it is not already cached. + fn frame(&self, index: u32) -> Result, VerifyError> { + if let Some(frame) = self.cache.get(index) { + return Ok(frame); + } + + // Decompressed outside any cache lock, so concurrent readers never wait on each + // other. Two threads racing on one frame both decompress it; the loser's copy is + // simply dropped, which is cheaper than serializing every miss. + let frame = Arc::new(Frame::from(self.decompress(index)?)); + self.cache.insert(index, &frame); + + Ok(frame) + } + + /// Decompress one frame. Frame content is untrusted, so the extent and the + /// resulting size are both checked. + fn decompress(&self, index: u32) -> Result, VerifyError> { + let seek_table = self.seek_table.as_ref().expect("compressed arena"); let arena = &self.backing.bytes()[self.arena.clone()]; - let d_start = st.frame_start_decomp(lo)?; - let total = st.frame_end_decomp(hi)? - d_start; + let start = seek_table.frame_start_comp(index)? as usize; + let end = seek_table.frame_end_comp(index)? as usize; + let size = seek_table.frame_size_decomp(index)? as usize; + let compressed = arena + .get(start..end) + .ok_or(VerifyError::Malformed("frame extent out of arena bounds"))?; + // Cap the capacity hint at the (header-pinned) arena size so a corrupt seek - // table can't force a huge allocation; a full read still allocates once. - let cap = total.min(self.header.arena_decompressed_size) as usize; - // Decompress each frame straight into `out` via a cursor - one reusable - // context, no per-frame buffer, no second copy. - let mut out = std::io::Cursor::new(Vec::with_capacity(cap)); - let mut dctx = zstd::bulk::Decompressor::new()?; - for f in lo..=hi { - let c_start = st.frame_start_comp(f)? as usize; - let c_end = st.frame_end_comp(f)? as usize; - let d_size = st.frame_size_decomp(f)? as usize; - let frame = arena - .get(c_start..c_end) - .ok_or(VerifyError::Malformed("frame extent out of arena bounds"))?; - self.decompressions.fetch_add(1, Ordering::Relaxed); - out.get_mut().reserve(d_size); - let pos = out.position(); - let n = dctx.decompress_to_buffer(frame, &mut out)?; - if n != d_size { - return Err(VerifyError::Malformed( - "frame decompressed to unexpected size", - )); - } - out.set_position(pos + n as u64); + // table can't force a huge allocation. + let capacity = size.min(self.header.arena_decompressed_size as usize); + let mut buffer = self.cache.take_buffer(capacity); + + self.decompressions.fetch_add(1, Ordering::Relaxed); + let written = with_dctx(|dctx| Ok(dctx.decompress_to_buffer(compressed, &mut buffer)?))?; + if written != size { + return Err(VerifyError::Malformed( + "frame decompressed to unexpected size", + )); } - Ok((d_start, out.into_inner())) + + Ok(buffer) } } @@ -449,6 +687,10 @@ mod tests { use crate::{Compression, HashDbWriter, KeyWidth}; fn compressed_db(frame_size: u32) -> HashDb { + compressed_db_with(frame_size, super::DEFAULT_FRAME_CACHE_BYTES) + } + + fn compressed_db_with(frame_size: u32, cache_bytes: usize) -> HashDb { let mut w = HashDbWriter::new( KeyWidth::U64, Compression::Zeekstd { @@ -464,7 +706,10 @@ mod tests { } let mut out = Cursor::new(Vec::new()); w.build(&mut out).expect("build"); - HashDb::open_bytes(out.into_inner()).expect("open") + HashDb::options() + .frame_cache_bytes(cache_bytes) + .open_bytes(out.into_inner()) + .expect("open") } /// A miss is decided by the key array alone - never a frame decompression. @@ -475,10 +720,10 @@ mod tests { assert_eq!(db.get(probe), None); } assert!(!db.contains(999)); - assert_eq!(db.decompressions.load(Ordering::Relaxed), 0); + assert_eq!(db.inner.decompressions.load(Ordering::Relaxed), 0); assert!(db.get(0).is_some()); - assert!(db.decompressions.load(Ordering::Relaxed) > 0); + assert!(db.inner.decompressions.load(Ordering::Relaxed) > 0); } /// In-order iteration decompresses each frame once, not once per entry. @@ -486,9 +731,40 @@ mod tests { fn iter_decompresses_each_frame_once() { let db = compressed_db(128); assert_eq!(db.iter().count(), 100); - let frames = db.seek_table.as_ref().unwrap().num_frames() as u64; + let frames = db.inner.seek_table.as_ref().unwrap().num_frames() as u64; assert!(frames > 1, "fixture should span multiple frames"); // Boundary-straddling entries decompress both frames, so allow one re-read each. - assert!(db.decompressions.load(Ordering::Relaxed) <= 2 * frames); + assert!(db.inner.decompressions.load(Ordering::Relaxed) <= 2 * frames); + } + + /// The point of the cache: repeating a lookup must not decompress again, and + /// neighbouring keys share the frame their neighbour just paid for. + #[test] + fn cached_frames_are_not_decompressed_twice() { + let db = compressed_db(128); + assert!(db.get(0).is_some()); + let after_first = db.decompressions(); + + for _ in 0..10 { + assert!(db.get(0).is_some()); + } + assert_eq!(db.decompressions(), after_first, "repeat lookups were free"); + + // A clone shares the cache rather than starting its own. + let clone = db.clone(); + assert!(clone.get(0).is_some()); + assert_eq!(clone.decompressions(), after_first); + } + + /// With caching off, every lookup pays for its own frame - the old behaviour, + /// still available for one-shot passes. + #[test] + fn a_disabled_cache_decompresses_every_time() { + let db = compressed_db_with(128, 0); + assert!(db.get(0).is_some()); + let after_first = db.decompressions(); + + assert!(db.get(0).is_some()); + assert!(db.decompressions() > after_first); } } diff --git a/crates/ltk_hashdb/tests/roundtrip.rs b/crates/ltk_hashdb/tests/roundtrip.rs index 30b99d9..e8c9cd2 100644 --- a/crates/ltk_hashdb/tests/roundtrip.rs +++ b/crates/ltk_hashdb/tests/roundtrip.rs @@ -1,6 +1,5 @@ //! Round-trip and behavioral tests: txt-shaped data → `HashDbWriter` → `HashDb`. -use std::borrow::Cow; use std::io::Cursor; use ltk_hashdb::{ @@ -104,11 +103,12 @@ fn empty_table() { db.verify().expect("verify"); } +/// A raw arena lends its bytes straight out of the mapping - no copy per lookup. #[test] -fn get_returns_borrowed_for_raw_arena() { +fn get_borrows_from_a_raw_arena() { let bytes = build(KeyWidth::U64, HashKind::Xxh64, GAME_ENTRIES); let db = HashDb::open_bytes(bytes).expect("open"); - assert!(matches!(db.get(1), Some(Cow::Borrowed(_)))); + assert!(!db.get(1).expect("hit").is_owned()); } #[test] @@ -193,7 +193,12 @@ fn compressed_roundtrip() { assert!(db.is_compressed()); for &(k, p) in GAME_ENTRIES { assert_eq!(db.get(k).as_deref(), Some(p), "frame_size {frame_size}"); - assert!(matches!(db.get(k), Some(Cow::Owned(_)))); + // A frame larger than the whole arena holds every entry whole, so the + // hit lends its bytes out of the cached frame rather than copying them. + // The tiny frame sizes above are the straddling case, which must splice. + if frame_size == 1 << 20 { + assert!(!db.get(k).expect("hit").is_owned()); + } } assert_eq!(db.get(2), None); db.verify().expect("verify"); From 539bd0b94909ec00bbbcafef337b9c69ac31e57f Mon Sep 17 00:00:00 2001 From: Crauzer Date: Mon, 24 Aug 2026 23:32:48 +0200 Subject: [PATCH 04/10] test(hashdb): add a frame-cache point-vs-batch example --- crates/ltk_hashdb/examples/frame_cache.rs | 122 ++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 crates/ltk_hashdb/examples/frame_cache.rs diff --git a/crates/ltk_hashdb/examples/frame_cache.rs b/crates/ltk_hashdb/examples/frame_cache.rs new file mode 100644 index 0000000..62cb998 --- /dev/null +++ b/crates/ltk_hashdb/examples/frame_cache.rs @@ -0,0 +1,122 @@ +//! Measures what the frame cache is for: point lookups against batched ones. +//! +//! A WAD extractor resolves one chunk hash at a time - `ltk_wad`'s `PathResolver` is +//! point-shaped by construction - but the chunks of any one archive share a path prefix, +//! so they land in a handful of arena frames. This walks that shape: take a contiguous +//! run of paths (one "archive"), probe them in hash order (a WAD's table-of-contents +//! order, which is unrelated to path order), and compare point lookups with the cache on, +//! with it off, and one `get_batch` over the same keys. +//! +//! ```text +//! cargo run --release -p ltk_hashdb --example frame_cache -- [chunks] +//! ``` + +use std::time::Instant; + +use ltk_hashdb::HashDb; + +fn main() { + let mut args = std::env::args_os().skip(1); + let Some(path) = args.next() else { + eprintln!("usage: frame_cache [chunks]"); + std::process::exit(2); + }; + let chunks: usize = args + .next() + .and_then(|n| n.to_str().and_then(|n| n.parse().ok())) + .unwrap_or(20_000); + + let db = HashDb::open(&path).expect("open"); + println!( + "{}: {} entries, arena {:.1} MiB raw / {:.1} MiB on disk\n", + path.to_string_lossy(), + db.len(), + db.arena_decompressed_size() as f64 / (1 << 20) as f64, + db.arena_compressed_size() as f64 / (1 << 20) as f64, + ); + + // `iter` yields in arena order, so a window of it is a run of neighbouring paths - + // one archive's worth. Probing order is by hash, as a WAD's TOC would give them. + let started = Instant::now(); + let arena_order: Vec = db.iter().map(|(key, _)| key).collect(); + println!( + "full scan: {:?} for {} entries", + started.elapsed(), + arena_order.len() + ); + + let window = arena_order.len().min(chunks); + let start = (arena_order.len() - window) / 2; + let mut keys: Vec = arena_order[start..start + window].to_vec(); + keys.sort_unstable(); + + let cached = HashDb::open(&path).expect("open"); + let uncached = HashDb::options() + .frame_cache_bytes(0) + .open(&path) + .expect("open"); + + // Warm the mapping so the first pass isn't paying for page faults the others skip. + let _ = uncached.get(keys[0]); + + let point_uncached = time(|| { + for &key in &keys { + std::hint::black_box(uncached.get(key)); + } + }); + let point_cached = time(|| { + for &key in &keys { + std::hint::black_box(cached.get(key)); + } + }); + let batched = time(|| { + for entry in cached.get_batch(&keys) { + std::hint::black_box(entry); + } + }); + + let per = |d: std::time::Duration| d.as_secs_f64() * 1e9 / keys.len() as f64; + println!("\n{window} chunks, probed in hash order:"); + println!(" point, no cache : {:>8.0} ns/lookup", per(point_uncached)); + println!(" point, cached : {:>8.0} ns/lookup", per(point_cached)); + println!(" get_batch : {:>8.0} ns/lookup", per(batched)); + println!( + "\n cache speedup : {:>8.1}x point/batch ratio: {:.2}x", + point_uncached.as_secs_f64() / point_cached.as_secs_f64(), + point_cached.as_secs_f64() / batched.as_secs_f64(), + ); + + // The other end of the spectrum: keys spread over the whole table, where no cache + // of any size holds the working set. This is what the cache must not make slower. + let stride = arena_order.len() / window.max(1); + let mut scattered: Vec = arena_order.iter().copied().step_by(stride.max(1)).collect(); + scattered.truncate(window); + scattered.sort_unstable(); + + let scattered_uncached = time(|| { + for &key in &scattered { + std::hint::black_box(uncached.get(key)); + } + }); + let scattered_cached = time(|| { + for &key in &scattered { + std::hint::black_box(cached.get(key)); + } + }); + + println!("\n{} keys spread across the whole table:", scattered.len()); + println!( + " point, no cache : {:>8.0} ns/lookup", + scattered_uncached.as_secs_f64() * 1e9 / scattered.len() as f64 + ); + println!( + " point, cached : {:>8.0} ns/lookup", + scattered_cached.as_secs_f64() * 1e9 / scattered.len() as f64 + ); +} + +fn time(mut f: impl FnMut()) -> std::time::Duration { + let started = Instant::now(); + f(); + started.elapsed() +} From bebb77ffd6bb925bf9e3e22602314aaa14bc19aa Mon Sep 17 00:00:00 2001 From: Crauzer Date: Mon, 24 Aug 2026 23:36:21 +0200 Subject: [PATCH 05/10] feat(hashdb)!: add try_get, get_into, for_each_batch, and a health signal --- crates/ltk_hashdb/src/layered.rs | 174 ++++++++++++++++++++++++++++++- crates/ltk_hashdb/src/reader.rs | 170 ++++++++++++++++++++++++++++-- 2 files changed, 334 insertions(+), 10 deletions(-) diff --git a/crates/ltk_hashdb/src/layered.rs b/crates/ltk_hashdb/src/layered.rs index cdd5639..fd13d84 100644 --- a/crates/ltk_hashdb/src/layered.rs +++ b/crates/ltk_hashdb/src/layered.rs @@ -1,6 +1,7 @@ //! An in-memory overlay layered over an ordered list of read-only base tables. use std::collections::HashMap; +use std::fmt; use crate::{Casing, HashDb, HashKind, KeyWidth, PathRef}; @@ -122,7 +123,7 @@ impl LayeredHashDb { /// order. This is the payoff over calling [`get`](Self::get) N times. pub fn get_batch<'a>( &'a self, - hashes: &'a [u64], + hashes: &[u64], ) -> impl Iterator>)> + 'a { let mut results: Vec>> = Vec::new(); results.resize_with(hashes.len(), || None); @@ -156,7 +157,101 @@ impl LayeredHashDb { residual = next; } - hashes.iter().copied().zip(results) + let out: Vec<(u64, Option>)> = hashes.iter().copied().zip(results).collect(); + out.into_iter() + } + + /// Resolve a batch without collecting it, calling `f` per hash as it resolves. + /// + /// The streaming counterpart to [`get_batch`](Self::get_batch), with the same + /// staging: the overlay answers first, then each base takes the residual. Hits + /// arrive layer by layer and, within a base, in arena order rather than input + /// order, so the first argument is the hash's position in `hashes`; hashes no + /// layer answers are reported last. + pub fn for_each_batch(&self, hashes: &[u64], mut f: impl FnMut(usize, u64, Option<&str>)) { + let mut residual: Vec = Vec::new(); + for (i, &hash) in hashes.iter().enumerate() { + match self.overlay.get(&hash) { + Some(path) => f(i, hash, Some(path)), + None => residual.push(i), + } + } + + for base in &self.bases { + if residual.is_empty() { + break; + } + + let sub: Vec = residual.iter().map(|&i| hashes[i]).collect(); + let mut next: Vec = Vec::new(); + base.for_each_batch(&sub, |p, hash, path| match path { + Some(path) => f(residual[p], hash, Some(path)), + None => next.push(residual[p]), + }); + residual = next; + } + + for i in residual { + f(i, hashes[i], None); + } + } + + /// Copy a path into `buf`, replacing what was there. `false` for a miss. + /// + /// See [`HashDb::get_into`] - the layered form consults the overlay first, then + /// each base in push order. + pub fn get_into(&self, hash: u64, buf: &mut String) -> bool { + if let Some(path) = self.overlay.get(&hash) { + buf.clear(); + buf.push_str(path); + return true; + } + + self.bases.iter().any(|base| base.get_into(hash, buf)) + } + + /// Every entry, overlay first and then each base in priority order. + /// + /// Shadowed entries are yielded once, by the layer that answers them - so this + /// enumerates exactly what [`get`](Self::get) can resolve. Each base is walked in + /// its own arena order, so its frames decompress once. + pub fn iter(&self) -> impl Iterator)> { + let overlay = self + .overlay + .iter() + .map(|(&hash, path)| (hash, PathRef::borrowed(path))); + + let bases = self + .bases + .iter() + .enumerate() + .flat_map(move |(layer, base)| { + base.iter() + .filter(move |(hash, _)| !self.shadows(*hash, layer)) + }); + + overlay.chain(bases) + } + + /// Whether a layer above `layer` already answers `hash`. + fn shadows(&self, hash: u64, layer: usize) -> bool { + self.overlay.contains_key(&hash) + || self.bases[..layer].iter().any(|base| base.contains(hash)) + } + + /// Entries across every base, counting a key that appears in several of them once + /// per base rather than once outright. + /// + /// An upper bound on what [`iter`](Self::iter) yields, and O(bases) to compute - + /// the exact figure would cost a lookup per entry, so it is `iter().count()` when + /// a caller genuinely needs it. + pub fn base_len(&self) -> usize { + self.bases.iter().map(HashDb::len).sum() + } + + /// Whether every base has read cleanly so far - see [`HashDb::is_healthy`]. + pub fn is_healthy(&self) -> bool { + self.bases.iter().all(HashDb::is_healthy) } /// The base tables, in priority order. @@ -170,6 +265,16 @@ impl LayeredHashDb { } } +impl fmt::Debug for LayeredHashDb { + /// Shape only - the overlay's size and each base's shape. Never entries. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LayeredHashDb") + .field("overlay", &self.overlay.len()) + .field("bases", &self.bases) + .finish() + } +} + #[cfg(test)] mod tests { use std::io::Cursor; @@ -316,6 +421,71 @@ mod tests { let _ = LayeredHashDb::from_bases(vec![u64_base, u32_base]); } + /// `iter` enumerates exactly what `get` can resolve: every shadowed key is + /// yielded once, by the layer that answers it. + #[test] + fn iter_yields_each_key_once_from_its_answering_layer() { + let base0 = raw_db(&[(1, "base0/one"), (2, "base0/two")]); + let base1 = raw_db(&[(2, "base1/two"), (3, "base1/three")]); + let mut db = LayeredHashDb::from_bases(vec![base0, base1]); + db.insert(1, "overlay/one"); + + let mut seen: Vec<(u64, String)> = db + .iter() + .map(|(hash, path)| (hash, path.into_owned())) + .collect(); + seen.sort(); + + assert_eq!( + seen, + vec![ + (1, "overlay/one".to_owned()), + (2, "base0/two".to_owned()), + (3, "base1/three".to_owned()), + ] + ); + + // Every yielded pair is what `get` answers with. + for (hash, path) in &seen { + assert_eq!(db.get(*hash).as_deref(), Some(path.as_str())); + } + + // `base_len` counts the shadowed key in both bases, as documented. + assert_eq!(db.base_len(), 4); + } + + #[test] + fn for_each_batch_matches_get_batch() { + let base0 = raw_db(&[(10, "base0/ten"), (20, "shadowed")]); + let base1 = raw_db(&[(20, "base1/twenty"), (30, "base1/thirty")]); + let mut db = LayeredHashDb::from_bases(vec![base0, base1]); + db.insert(5, "overlay/five"); + + let probes = [5u64, 10, 20, 30, 999, 10]; + let mut streamed: Vec<(u64, Option)> = vec![(0, None); probes.len()]; + db.for_each_batch(&probes, |i, hash, path| { + streamed[i] = (hash, path.map(str::to_owned)); + }); + + let collected: Vec<(u64, Option)> = db + .get_batch(&probes) + .map(|(hash, path)| (hash, path.map(|p| p.into_owned()))) + .collect(); + assert_eq!(streamed, collected); + } + + /// Debug prints shape, never entries. + #[test] + fn debug_prints_shape_only() { + let mut db = LayeredHashDb::from_bases(vec![raw_db(&[(1, "secret/path.bin")])]); + db.insert(2, "overlay/secret.bin"); + + let shown = format!("{db:?}"); + assert!(shown.contains("overlay: 1"), "{shown}"); + assert!(shown.contains("entries: 1"), "{shown}"); + assert!(!shown.contains("secret"), "{shown}"); + } + #[test] fn insert_path_uses_first_base() { let base = raw_db(&[(1, "seed")]); diff --git a/crates/ltk_hashdb/src/reader.rs b/crates/ltk_hashdb/src/reader.rs index 3433ce1..6b02258 100644 --- a/crates/ltk_hashdb/src/reader.rs +++ b/crates/ltk_hashdb/src/reader.rs @@ -7,7 +7,7 @@ use std::fmt; use std::fs::File; use std::ops::Range; use std::path::Path; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use xxhash_rust::xxh3::Xxh3; @@ -124,6 +124,10 @@ struct Inner { /// Frames decompressed so far; misses must never bump it (see unit tests). decompressions: AtomicU64, + + /// Cleared for good the first time a lookup swallows a decompression failure, + /// so "this build knows nothing" can be told apart from "this file is broken". + healthy: AtomicBool, } enum Backing { @@ -318,6 +322,7 @@ impl HashDb { seek_table, cache, decompressions: AtomicU64::new(0), + healthy: AtomicBool::new(true), }), }) } @@ -327,7 +332,58 @@ impl HashDb { /// (corrupt file - see [`HashDb::verify`]). pub fn get(&self, hash: u64) -> Option> { let i = self.inner.index_of(hash)?; - self.inner.bytes_at(i).ok().flatten().map(PathRef::from) + self.inner.lookup(i).map(PathRef::from) + } + + /// Look up a hash, surfacing a corrupt arena instead of reporting it as a miss. + /// + /// [`get`](HashDb::get) follows the format's rule that an entry which will not + /// decompress reads as a miss. That is right for resolving names and wrong for + /// telling a user why every name in an install came back unknown, so this returns + /// the failure at the call site instead. `Ok(None)` is a genuine miss. + /// + /// # Errors + /// + /// Fails with [`VerifyError`] when the entry's frame will not decompress or its + /// extent runs outside the arena - always a corrupt file. + pub fn try_get(&self, hash: u64) -> Result>, VerifyError> { + let Some(i) = self.inner.index_of(hash) else { + return Ok(None); + }; + + Ok(self.inner.bytes_at(i)?.map(PathRef::from)) + } + + /// Copy a path into `buf`, replacing what was there. `false` for a miss. + /// + /// The counterpart to [`get`](HashDb::get) for a caller looping over a reusable + /// buffer: it copies the bytes out and holds no frame afterwards, where a retained + /// [`PathRef`] keeps its frame resident. + pub fn get_into(&self, hash: u64, buf: &mut String) -> bool { + let Some(i) = self.inner.index_of(hash) else { + return false; + }; + let Some(bytes) = self.inner.lookup(i) else { + return false; + }; + + buf.clear(); + match std::str::from_utf8(bytes.as_slice()) { + Ok(path) => buf.push_str(path), + Err(_) => buf.push_str(&String::from_utf8_lossy(bytes.as_slice())), + } + + true + } + + /// Whether every lookup so far has read cleanly. + /// + /// Turns false for good the first time a lookup hits an entry that will not + /// decompress - bit rot, a truncated write - and stays false. Nothing re-verifies + /// an installed table after its download checksum, so this is the cheap signal that + /// a table needs [`verify`](HashDb::verify) rather than a redownload of the world. + pub fn is_healthy(&self) -> bool { + self.inner.healthy.load(Ordering::Relaxed) } /// Membership test; never touches the arena. @@ -350,7 +406,7 @@ impl HashDb { results.resize_with(hashes.len(), || None); for p in order { if let Some(i) = indices[p] { - results[p] = self.inner.bytes_at(i).ok().flatten().map(PathRef::from); + results[p] = self.inner.lookup(i).map(PathRef::from); } } @@ -358,6 +414,29 @@ impl HashDb { out.into_iter() } + /// Resolve a batch without collecting it, calling `f` per hash as it resolves. + /// + /// The allocation-free batch: where [`get_batch`](HashDb::get_batch) materialises + /// every result before yielding the first, this hands each one straight to `f`. + /// Calls arrive in **arena order**, not input order - that is what lets each frame + /// decompress once - so the first argument is the hash's position in `hashes`. + pub fn for_each_batch(&self, hashes: &[u64], mut f: impl FnMut(usize, u64, Option<&str>)) { + let indices: Vec> = hashes.iter().map(|&h| self.inner.index_of(h)).collect(); + let mut order: Vec = (0..hashes.len()).collect(); + order.sort_unstable_by_key(|&p| indices[p].map_or(u64::MAX, |i| self.inner.offset_at(i))); + + for p in order { + let path = indices[p].and_then(|i| self.inner.lookup(i)); + match path { + Some(bytes) => { + let path = PathRef::from(bytes); + f(p, hashes[p], Some(&path)); + } + None => f(p, hashes[p], None), + } + } + } + pub fn len(&self) -> usize { self.inner.len() } @@ -416,7 +495,7 @@ impl HashDb { /// decompresses once. Entries that fail to decompress are skipped; `verify()` reports them. pub fn iter(&self) -> impl Iterator)> { self.inner.arena_order().into_iter().filter_map(move |i| { - let bytes = self.inner.bytes_at(i).ok().flatten()?; + let bytes = self.inner.lookup(i)?; Some((self.inner.key_at(i), PathRef::from(bytes))) }) } @@ -602,6 +681,18 @@ impl Inner { Ok(Some(Bytes::Spliced(spliced))) } + /// Entry `i`'s bytes for a lookup: a failure to decompress reads as a miss, and + /// trips the health flag on its way past. + fn lookup(&self, i: usize) -> Option> { + match self.bytes_at(i) { + Ok(bytes) => bytes, + Err(_) => { + self.healthy.store(false, Ordering::Relaxed); + None + } + } + } + /// Check one entry the way `verify` needs it checked: in bounds and valid UTF-8. fn verify_entry(&self, i: usize) -> Result<(), VerifyError> { let bytes = self @@ -691,6 +782,14 @@ mod tests { } fn compressed_db_with(frame_size: u32, cache_bytes: usize) -> HashDb { + HashDb::options() + .frame_cache_bytes(cache_bytes) + .open_bytes(compressed_bytes(frame_size)) + .expect("open") + } + + /// 100 clustered paths, keyed `i * 3`, spanning several frames. + fn compressed_bytes(frame_size: u32) -> Vec { let mut w = HashDbWriter::new( KeyWidth::U64, Compression::Zeekstd { @@ -706,10 +805,7 @@ mod tests { } let mut out = Cursor::new(Vec::new()); w.build(&mut out).expect("build"); - HashDb::options() - .frame_cache_bytes(cache_bytes) - .open_bytes(out.into_inner()) - .expect("open") + out.into_inner() } /// A miss is decided by the key array alone - never a frame decompression. @@ -756,6 +852,64 @@ mod tests { assert_eq!(clone.decompressions(), after_first); } + /// A table whose arena no longer decompresses must degrade to misses, say so via + /// `is_healthy`, and surface the reason through `try_get` - never panic. + #[test] + fn a_corrupt_frame_reports_unhealthy() { + let mut bytes = compressed_bytes(128); + // The seek table lives at the end of the arena, so scribbling over the start + // leaves the file openable and breaks only the frames it lands on. + let arena_offset = u64::from_le_bytes(bytes[40..48].try_into().unwrap()) as usize; + for byte in &mut bytes[arena_offset..arena_offset + 64] { + *byte = 0xff; + } + + let db = HashDb::open_bytes(bytes).expect("open"); + assert!(db.is_healthy(), "nothing has been read yet"); + + let keys: Vec = (0..100u64).map(|i| i * 3).collect(); + let swallowed = keys.iter().filter(|&&k| db.get(k).is_none()).count(); + assert!(swallowed > 0, "the corrupt frame should swallow lookups"); + assert!(!db.is_healthy(), "and say so afterwards"); + + // The same lookups report the corruption when asked to. + let surfaced = keys.iter().filter(|&&k| db.try_get(k).is_err()).count(); + assert_eq!(surfaced, swallowed); + + // Entries in the untouched frames still resolve. + assert!(keys.iter().any(|&k| db.get(k).is_some())); + } + + #[test] + fn for_each_batch_matches_get_batch() { + let db = compressed_db(128); + let probes = [0u64, 3, 297, 1, 99, 0]; + + let mut streamed: Vec<(u64, Option)> = vec![(0, None); probes.len()]; + db.for_each_batch(&probes, |i, hash, path| { + streamed[i] = (hash, path.map(str::to_owned)); + }); + + let collected: Vec<(u64, Option)> = db + .get_batch(&probes) + .map(|(hash, path)| (hash, path.map(|p| p.into_owned()))) + .collect(); + assert_eq!(streamed, collected); + } + + #[test] + fn get_into_reuses_the_callers_buffer() { + let db = compressed_db(128); + let mut buf = String::from("stale contents"); + + assert!(db.get_into(0, &mut buf)); + assert_eq!(buf, "assets/characters/champ0/skins/skin0.bin"); + + // A miss leaves the buffer alone rather than clearing it. + assert!(!db.get_into(1, &mut buf)); + assert_eq!(buf, "assets/characters/champ0/skins/skin0.bin"); + } + /// With caching off, every lookup pays for its own frame - the old behaviour, /// still available for one-shot passes. #[test] From f159c4d4d4e0557724ea4f77856542db13102756 Mon Sep 17 00:00:00 2001 From: Crauzer Date: Mon, 24 Aug 2026 23:38:26 +0200 Subject: [PATCH 06/10] feat(cache): add HashStore::open_shared backed by a weak table register --- crates/ltk_hashdb/src/lib.rs | 2 +- crates/ltk_hashdb/src/reader.rs | 26 +++++++++- crates/ltk_mimir_cache/src/store.rs | 72 ++++++++++++++++++++++++--- crates/ltk_mimir_cache/tests/store.rs | 32 ++++++++++++ 4 files changed, 123 insertions(+), 9 deletions(-) diff --git a/crates/ltk_hashdb/src/lib.rs b/crates/ltk_hashdb/src/lib.rs index 72fa33a..8defb6f 100644 --- a/crates/ltk_hashdb/src/lib.rs +++ b/crates/ltk_hashdb/src/lib.rs @@ -22,7 +22,7 @@ pub use hash::{Casing, HashKind}; pub use header::{FORMAT_VERSION, HEADER_SIZE, MAGIC}; pub use layered::LayeredHashDb; pub use path::PathRef; -pub use reader::{HashDb, HashDbOptions, DEFAULT_FRAME_CACHE_BYTES}; +pub use reader::{HashDb, HashDbOptions, WeakHashDb, DEFAULT_FRAME_CACHE_BYTES}; pub use writer::{BuildStats, HashDbWriter}; /// Width of the integer keys in a table. diff --git a/crates/ltk_hashdb/src/reader.rs b/crates/ltk_hashdb/src/reader.rs index 6b02258..66041c8 100644 --- a/crates/ltk_hashdb/src/reader.rs +++ b/crates/ltk_hashdb/src/reader.rs @@ -8,7 +8,7 @@ use std::fs::File; use std::ops::Range; use std::path::Path; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use xxhash_rust::xxh3::Xxh3; use zeekstd::SeekTable; @@ -107,6 +107,23 @@ pub struct HashDb { inner: Arc, } +/// A handle that does not keep its table mapped. +/// +/// For a registry of open tables: keep these rather than [`HashDb`]s so the registry +/// does not pin every table anyone ever opened. [`upgrade`](WeakHashDb::upgrade) hands +/// back a live handle while one is still held elsewhere. +#[derive(Clone, Debug)] +pub struct WeakHashDb { + inner: Weak, +} + +impl WeakHashDb { + /// A live handle to the table, or `None` once the last one was dropped. + pub fn upgrade(&self) -> Option { + self.inner.upgrade().map(|inner| HashDb { inner }) + } +} + /// Everything a table's clones share: the bytes, where the sections are, and the /// frames decompressed out of them so far. struct Inner { @@ -239,6 +256,13 @@ impl HashDb { HashDbOptions::default() } + /// A handle that does not keep this table mapped - see [`WeakHashDb`]. + pub fn downgrade(&self) -> WeakHashDb { + WeakHashDb { + inner: Arc::downgrade(&self.inner), + } + } + fn from_backing(backing: Backing, options: HashDbOptions) -> Result { let data = backing.bytes(); let header = Header::decode(data)?; diff --git a/crates/ltk_mimir_cache/src/store.rs b/crates/ltk_mimir_cache/src/store.rs index cf8d385..a3cc0e8 100644 --- a/crates/ltk_mimir_cache/src/store.rs +++ b/crates/ltk_mimir_cache/src/store.rs @@ -1,9 +1,11 @@ //! [`HashStore`]: the shared cache directory and everything that reads or mutates it - //! opening the active table, committing new immutable versions, and GC'ing old ones. +use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, PoisonError}; -use ltk_hashdb::{HashDb, LayeredHashDb}; +use ltk_hashdb::{HashDb, LayeredHashDb, WeakHashDb}; use crate::manifest::{Manifest, Source, TableEntry}; use crate::{ @@ -21,9 +23,18 @@ const TABLE_EXT: &str = "lhdb"; /// /// Construction is cheap and does not touch the filesystem; the directory is created /// lazily on the first [`commit`](HashStore::commit). +/// +/// A store also remembers the tables it has opened through +/// [`open_shared`](HashStore::open_shared), weakly - clones of a store share that +/// register, two stores built separately do not, and a table drops out of it as soon as +/// the last handle to it does. #[derive(Debug, Clone)] pub struct HashStore { dir: PathBuf, + + /// Tables opened through `open_shared`, keyed by their active file. Weak, so the + /// register never keeps a superseded version mapped. + opened: Arc>>, } /// One table to install in a [`commit`](HashStore::commit) call. @@ -63,14 +74,15 @@ impl HashStore { /// Resolve the cache directory from the environment / platform. Does not /// create it. pub fn discover() -> Result { - Ok(Self { - dir: dir::resolve()?, - }) + Ok(Self::at(dir::resolve()?)) } /// Use an explicit cache directory (tests, `--dir` overrides). pub fn at(dir: impl Into) -> Self { - Self { dir: dir.into() } + Self { + dir: dir.into(), + opened: Arc::new(Mutex::new(HashMap::new())), + } } /// The cache directory. @@ -103,10 +115,53 @@ impl HashStore { /// Structure is validated on open; the download-time sha256 in the manifest is /// trusted, so this stays cheap and lazy. Use [`HashDb::verify`] for a full /// checksum pass. + /// + /// Every call maps the file afresh. Use [`open_shared`](HashStore::open_shared) + /// when the same table may already be open in this process. pub fn open(&self, table: Table) -> Result { Ok(HashDb::open(self.path_for(table)?)?) } + /// Open the active version of `table`, reusing a handle this store already has. + /// + /// [`open`](HashStore::open) maps the file and parses its seek table every time - + /// on `game` that is over ten thousand frame records for a table the process may + /// already have open. This hands back the existing handle instead, and because the + /// register is keyed on the manifest's active filename, an update published in the + /// meantime opens the new version by itself rather than serving the old one. + /// + /// Prefer it wherever a table is opened more than once. The returned handle is a + /// [`HashDb`] like any other - cheap to clone, shared frame cache. + /// + /// # Errors + /// + /// Fails like [`open`](HashStore::open): a missing manifest, a table the manifest + /// does not carry, or a file that does not validate. + pub fn open_shared(&self, table: Table) -> Result { + let path = self.path_for(table)?; + if let Some(db) = self.registered(&path) { + return Ok(db); + } + + // Opened outside the lock: two threads racing on one table each get a working + // handle and the loser's simply isn't the one registered, which is cheaper than + // holding a lock across an mmap and a seek-table parse. + let db = HashDb::open(&path)?; + + let mut opened = self.opened.lock().unwrap_or_else(PoisonError::into_inner); + opened.insert(path, db.downgrade()); + // Superseded versions leave dead entries behind; sweep them while we hold the + // lock, so a long-lived store doesn't accumulate one per update. + opened.retain(|_, weak| weak.upgrade().is_some()); + + Ok(db) + } + + fn registered(&self, path: &Path) -> Option { + let opened = self.opened.lock().unwrap_or_else(PoisonError::into_inner); + opened.get(path).and_then(WeakHashDb::upgrade) + } + /// Open several tables, pairing each with its result so callers can warn-and-skip /// missing ones instead of aborting on the first error. Results are returned in /// `tables` order. @@ -121,11 +176,14 @@ impl HashStore { /// A tool stays usable when a table is missing: its hashes just miss. This is /// the shape most WAD consumers want - e.g. /// `open_layered(&[Table::Game, Table::Lcu])`. + /// + /// Tables are opened through [`open_shared`](HashStore::open_shared), so calling + /// this twice does not map anything twice. pub fn open_layered(&self, tables: &[Table]) -> (LayeredHashDb, Vec<(Table, OpenError)>) { let mut layered = LayeredHashDb::new(); let mut errors = Vec::new(); - for (table, res) in self.open_many(tables) { - match res { + for &table in tables { + match self.open_shared(table) { Ok(db) => layered.push_base(db), Err(e) => errors.push((table, e)), } diff --git a/crates/ltk_mimir_cache/tests/store.rs b/crates/ltk_mimir_cache/tests/store.rs index 10cb3c3..8216908 100644 --- a/crates/ltk_mimir_cache/tests/store.rs +++ b/crates/ltk_mimir_cache/tests/store.rs @@ -427,3 +427,35 @@ fn gc_handles_mapped_superseded_file() { assert!(mapped.contains(0x2222)); assert!(store.open(Table::Game).unwrap().contains(0x3333)); } + +/// `open_shared` hands back the handle it already has, and follows the manifest to a +/// new version once one is published. +#[test] +fn open_shared_reuses_handles_until_the_version_changes() { + let tmp = tempdir().unwrap(); + let store = HashStore::at(tmp.path()); + + let one = build_table(&tmp.path().join("game-1.build"), &[(1, "assets/one.bin")]); + store + .commit(&[CommitItem::new(Table::Game, "1", &one)], None) + .unwrap(); + + let first = store.open_shared(Table::Game).unwrap(); + let second = store.open_shared(Table::Game).unwrap(); + assert_eq!(first.get(1).as_deref(), Some("assets/one.bin")); + assert_eq!(second.get(1).as_deref(), Some("assets/one.bin")); + + // Publishing a new version repoints the manifest, so the next call opens that one + // rather than serving the handle it already holds. + let two = build_table( + &tmp.path().join("game-2.build"), + &[(1, "assets/one.bin"), (2, "assets/two.bin")], + ); + store + .commit(&[CommitItem::new(Table::Game, "2", &two)], None) + .unwrap(); + + let after = store.open_shared(Table::Game).unwrap(); + assert_eq!(after.len(), 2, "followed the manifest to the new version"); + assert_eq!(first.len(), 1, "the old handle keeps reading the old file"); +} From edc4af477bd69d99512f10d9092d71d724f1b499 Mon Sep 17 00:00:00 2001 From: Crauzer Date: Mon, 24 Aug 2026 23:57:47 +0200 Subject: [PATCH 07/10] docs: move the design narrative into docs/DESIGN.md and rewrite the README around the API --- CLAUDE.md | 12 +- README.md | 414 ++++++++++++++++++++++++++++++++------------- docs/BENCHMARKS.md | 2 +- docs/DESIGN.md | 147 ++++++++++++++++ 4 files changed, 449 insertions(+), 126 deletions(-) create mode 100644 docs/DESIGN.md diff --git a/CLAUDE.md b/CLAUDE.md index 021e3de..f3fbf8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,9 +26,15 @@ Cargo workspace (`resolver = "2"`), four crates under `crates/`: | `ltk_mimir_gen` | Hash-discovery ("hunt") engine - guessers that resolve unknown hashes | | `ltk_mimir_cli` | The `mimir` binary (`build` / `get` / `verify` / `stats` / `gen` / `update` / `merge` / `bundle`) | -Docs live in `docs/`: `FORMAT.md` (byte-level spec, format version 1), `CONSUMERS.md` -(integration API), `BENCHMARKS.md` (frame-size/compression measurements), `ROADMAP.md` -(planned work, in dependency order, with the additive-only constraints that govern it). +Docs live in `docs/`: `DESIGN.md` (rationale, how the format works, measurements - +the narrative the README used to carry), `FORMAT.md` (byte-level spec, format version 1), +`CONSUMERS.md` (integration API), `BENCHMARKS.md` (frame-size/compression measurements), +`ROADMAP.md` (planned work, in dependency order, with the additive-only constraints that +govern it). + +`README.md` is the usage front door: install, quick start, the API surface with examples, +and the CLI. Keep its examples true to the current API - they are typechecked by hand, not +by CI. ## Conventions diff --git a/README.md b/README.md index 708d533..9f63b2f 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,320 @@ -# mimir - -A toolkit for **generating**, **storing**, and **serving** League of Legends -hash → path tables as a compact, memory-mapped, seekable binary format (`.hashdb`). -It reimplements and extends CommunityDragon's CDTB `hashes.py` and the -CommunityDragon/Data txt artifacts. - -The `.hashdb` format is general-purpose - an mmap-backed, read-only map from integer keys -to string values, with nothing League-specific in its layout. League Toolkit distributes -its own tables in this format under the `.lhdb` extension (identical bytes). - -## Why this exists - -Almost every League of Legends tool - WAD unpackers, `.bin` inspectors, mod loaders, -asset browsers - hits the same wall: the game identifies files and fields by **hash**, -not by name. To show a human-readable path you need a **hash table** that maps each -hash back to its original string. - -Today that table is the CommunityDragon `hashes.*.txt` set: **~348 MB of plain text**, -which every tool downloads and keeps around. That approach has two costs that compound -the moment a machine runs more than one of these tools: - -- **Memory.** To resolve hashes efficiently a program has to load the whole table into - an in-memory map. Run three tools that each need the game hashes and you pay for - **three private copies** of the same hundreds-of-megabytes table, all resident at once. -- **Startup & distribution.** Every tool ships (or re-downloads) the same giant text - files and spends time parsing them into a map before it can answer a single query. - -**mimir** replaces that text blob with a purpose-built, **read-only** binary format for -hash storage. The design goals are, in order: - -1. **Usable as shipped** - no unpack or full-expansion step; a consumer `mmap`s the file - and immediately does lookups. -2. **Small** - the game table drops from ~348 MB of text to roughly **~50 MB** on disk. -3. **Memory-efficient across processes** - the file is memory-mapped, so the OS page - cache holds **one** copy that every tool on the machine shares. Resident RAM stays - low because pages are faulted in lazily and dropped under pressure, and a lookup - *miss* touches zero string data. - -## How it works - -A `.hashdb` file is a single logical table laid out for direct, zero-parse use over an -`mmap`: - -- **Sorted key array** - the integer hashes, stored strictly ascending so a lookup is a - **binary search straight over the mapped bytes**. A miss is decided here and never - reads any string data. -- **Parallel offset + length arrays** - for a found key, where its path lives in the arena - and how long it is. -- **String arena** - all the path strings concatenated with no separators, compressed as - a **Zstandard Seekable Format** stream. The seek table means a hit decompresses just the - **one small frame** that holds its path (default 16 KiB frames), not the whole table - - so partial, on-demand reads stay cheap. Paths are packed in **lexicographic order** so - a directory's files land in the same frames, which both compresses far better (~4× vs. - key order on the real game table) and makes directory-local batch lookups touch fewer - frames. - -The file is immutable once published; updates ship as new versioned files, and a downloaded -file is treated as untrusted - the header is validated on open and every read bounds-checks -its own extent. See [`docs/FORMAT.md`](docs/FORMAT.md) for the byte-level specification. - -Because it's memory-mapped and read-only, a lazy consumer (say, a mod loader) can open the -table only when it first needs to resolve a hash, share the page cache with every other -mimir-backed tool running, and drop the handle to reclaim its (already small) footprint. - -## Performance - -Real-data measurements against the CommunityDragon `hashes.*.txt` snapshot of -2026-07-07 (~2.97 M entries across 8 tables). Full tables, methodology, and -reproduction steps are in [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md). - - - - Bar chart comparing on-disk size of hashes.*.txt vs zstd .hashdb per table: game 198.3 MiB → 38.3 MiB, binentries 27.9 → 5.5, lcu 16.1 → 2.7, the five remaining tables 17.6 → 5.7 - - -The whole corpus drops from **~253 MiB of txt to ~52 MiB** of `.hashdb` - and the -binary is usable as-shipped: `open` is a header validation plus an `mmap`, with no -parse or expansion step before the first lookup. - - - - Bar chart of per-lookup latency on the compressed 2.09-million-entry game table: point hit 8.5 µs, batch hit 3.6 µs, miss 143 ns - - -A hit decompresses exactly one small frame; batched lookups amortize that through -the reader's frame cache. A **miss is decided by binary search over the raw key -section and never touches string data** - ~143 ns whether the file is raw or -compressed, which matters because hash hunting hammers misses. - - - - Bar chart of compressed size of the 162.5 MiB game string arena by layout: key-order 45.7 MiB, key-order with a trained dictionary 30.0 MiB, solid non-seekable stream 17.5 MiB, path-order 10.4 MiB - - -Sorting the arena by path packs each directory into the same frames, so the seekable -arena compresses **~4× better than key order** - beating even a solid, non-seekable -zstd stream - while making hits faster and directory-local batches frame-coherent. - -## Layout +
+ + LeagueToolkit logo + +

mimir

+
-| 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 | +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](#install)** · **[Quick start](#quick-start)** · **[Library API](#library-api)** · +**[CLI](#cli)** · **[Design](docs/DESIGN.md)** + +
+ +## Install + +Nothing is on crates.io yet, so depend on the repository directly: + +```toml +[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"] } +``` -## Using it +The CLI: -As a library - open a table and resolve a hash: +```sh +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: + +```sh +mimir update +mimir get 0x1234abcd --table game +``` + +The same thing from Rust: ```rust -use ltk_hashdb::HashDb; +use ltk_mimir_cache::{HashStore, Table}; + +let store = HashStore::discover()?; +let db = store.open_shared(Table::Game)?; // mmap + validate header; no parse step -let db = HashDb::open("game.hashdb")?; // mmap + validate header, lazy if let Some(path) = db.get(0x1234_5678_9abc_def0) { println!("{path}"); } ``` -From the CLI: +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: + +```rust +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 + 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: + +```rust +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`: + +```rust +// 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`: + +```rust +use ltk_mimir_cache::{HashStore, Table}; + +let store = HashStore::discover()?; + +// Missing tables are reported, not fatal - the tool stays usable and their hashes miss. +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. `game` and `lcu` do; the four +> 32-bit `bin*` tables are separate hash universes and must not be layered together. + +### Enumerating a table + +```rust +// Streams in arena order (lexicographic path order), one decompress per frame. +for (hash, path) in db.iter() { + 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: + +```rust +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 + +```rust +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::Insensitive); // League tables hash the 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` | +| `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` · `load_all` | enumerate in arena order, or decode into an owned map | +| `hash_path` | hash a string with this table's algorithm and casing | +| `verify` · `is_healthy` | full integrity pass; sticky flag set by a failed read | +| `len` · `key_width` · `hash_kind` · `casing` · `is_compressed` | 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` | every entry, 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 into one `LayeredHashDb`, reporting per-table errors | +| `manifest` · `path_for` | what is installed, and where | +| `update` · `update_async` | compare → download → verify → install → GC | +| `commit` · `gc` · `try_lock_update` | publish versions, sweep old ones, take the lock | + +**`PathRef`** - a resolved path. `Deref`, plus `as_str`, `is_owned` +(whether the bytes were copied rather than borrowed), and `into_owned`. + +**`HashDbWriter`** - `new` → `hash_kind` / `casing` → `insert` / `extend` → `build`. + +## CLI + +``` +mimir + +build Build a .hashdb table from a txt hash list (lines of ` `) +get Resolve one hash from a .hashdb file or the shared cache +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 +stats Sizes, entry counts, compression ratio of a .hashdb file +``` ```sh -# Build a .hashdb table from a CDragon ` ` txt list +# Build a table from a CDragon txt list mimir build --input hashes.game.txt --table game --out game.hashdb -# Resolve one hash +# 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 update +mimir update --force -# Validate a downloaded file (structure + checksum) +# Inspect and validate +mimir stats game.hashdb mimir verify game.hashdb ``` -See [`docs/CONSUMERS.md`](docs/CONSUMERS.md) for the shared-cache and -custom-hash-extension APIs. +## 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`](docs/DESIGN.md) | Why this exists, how the format works, what it measures | +| [`docs/FORMAT.md`](docs/FORMAT.md) | Byte-level specification of `.hashdb`, format version 1 | +| [`docs/CONSUMERS.md`](docs/CONSUMERS.md) | Integration guide: lookup patterns, threading, custom pipelines | +| [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) | Frame-size and compression measurements, with reproduction steps | +| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Planned work, in dependency order | ## Status -Early development. The `.hashdb` format, reader/writer, the shared cache, release -publishing (`mimir bundle` + a scheduled CI job that ships every table as versioned -`.lhdb` GitHub release assets, rebuilt from the canonical CommunityDragon txt lists), the +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 in place. +(`mimir gen --wad`) - are all in place. -Planned work - a reader frame cache, the pre-1.0 API cleanup, and arena-order indexing - -is tracked in [`docs/ROADMAP.md`](docs/ROADMAP.md). +The txt lists stay canonical; the binaries are generated release artifacts, never the +source of truth. ## License diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 85edd10..3d2c0bc 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -17,7 +17,7 @@ arena**, the **16 KiB default frame size**, and the **level 19** publishing defa ``` `bench_real` also writes its measurements to `/bench_real.json`; the - README performance charts (`docs/assets/bench-*.svg`) regenerate from that report + performance charts in `DESIGN.md` (`docs/assets/bench-*.svg`) regenerate from that report via `cargo run -p ltk_hashdb --example gen_charts`. The arena-layout chart is the exception - its numbers come from `compression_lab` and live in `gen_charts.rs`. diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..461add8 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,147 @@ +# Design + +> Why mimir exists, how the `.hashdb` format is put together, and what it measures. +> For the byte-level specification see [`FORMAT.md`](FORMAT.md); for the integration +> API see [`CONSUMERS.md`](CONSUMERS.md). + +## Why this exists + +Almost every League of Legends tool - WAD unpackers, `.bin` inspectors, mod loaders, +asset browsers - hits the same wall: the game identifies files and fields by **hash**, +not by name. To show a human-readable path you need a **hash table** that maps each +hash back to its original string. + +Today that table is the CommunityDragon `hashes.*.txt` set: **~348 MB of plain text**, +which every tool downloads and keeps around. That approach has two costs that compound +the moment a machine runs more than one of these tools: + +- **Memory.** To resolve hashes efficiently a program has to load the whole table into + an in-memory map. Run three tools that each need the game hashes and you pay for + **three private copies** of the same hundreds-of-megabytes table, all resident at once. +- **Startup & distribution.** Every tool ships (or re-downloads) the same giant text + files and spends time parsing them into a map before it can answer a single query. + +**mimir** replaces that text blob with a purpose-built, **read-only** binary format for +hash storage. The design goals are, in order: + +1. **Usable as shipped** - no unpack or full-expansion step; a consumer `mmap`s the file + and immediately does lookups. +2. **Small** - the game table drops from ~348 MB of text to roughly **~50 MB** on disk. +3. **Memory-efficient across processes** - the file is memory-mapped, so the OS page + cache holds **one** copy that every tool on the machine shares. Resident RAM stays + low because pages are faulted in lazily and dropped under pressure, and a lookup + *miss* touches zero string data. + +## How it works + +A `.hashdb` file is a single logical table laid out for direct, zero-parse use over an +`mmap`: + +- **Sorted key array** - the integer hashes, stored strictly ascending so a lookup is a + **binary search straight over the mapped bytes**. A miss is decided here and never + reads any string data. +- **Parallel offset + length arrays** - for a found key, where its path lives in the arena + and how long it is. +- **String arena** - all the path strings concatenated with no separators, compressed as + a **Zstandard Seekable Format** stream. The seek table means a hit decompresses just the + **one small frame** that holds its path (default 16 KiB frames), not the whole table - + so partial, on-demand reads stay cheap. Paths are packed in **lexicographic order** so + a directory's files land in the same frames, which both compresses far better (~4× vs. + key order on the real game table) and makes directory-local batch lookups touch fewer + frames. + +The file is immutable once published; updates ship as new versioned files, and a downloaded +file is treated as untrusted - the header is validated on open and every read bounds-checks +its own extent. See [`FORMAT.md`](FORMAT.md) for the byte-level specification. + +Because it's memory-mapped and read-only, a lazy consumer (say, a mod loader) can open the +table only when it first needs to resolve a hash, share the page cache with every other +mimir-backed tool running, and drop the handle to reclaim its (already small) footprint. + +### Reading a path without copying it + +A lookup returns a `PathRef`, which borrows rather than allocates. A raw arena lends the +bytes straight out of the mapping; a compressed arena lends them out of the decompressed +frame the table is holding, keeping that frame alive for as long as the path is. Only two +cases copy: an entry that straddles a frame boundary (about one per frame), and one whose +bytes are not valid UTF-8. `PathRef` derefs to `str`, so it reads like one at the call +site. + +Decompressed frames are cached in a fixed, byte-capped, N-way set-associative table sized +at open - frames are keyed by a dense index, so no map and no eviction list is needed, and +one lock per set keeps concurrent readers off each other. Buffers from evicted frames are +recycled, so a steady-state miss decompresses into an allocation that already exists. +Because published files are immutable, this is pure memoisation with nothing to +invalidate. + +This matters because point lookups are not a niche shape: `ltk_wad`'s `PathResolver` is +point-shaped by construction, so every WAD extractor resolves one hash per chunk whether +it wants to or not. + +## Performance + +Real-data measurements against the CommunityDragon `hashes.*.txt` snapshot of +2026-07-07 (~2.97 M entries across 8 tables). Full tables, methodology, and +reproduction steps are in [`BENCHMARKS.md`](BENCHMARKS.md). + + + + Bar chart comparing on-disk size of hashes.*.txt vs zstd .hashdb per table: game 198.3 MiB → 38.3 MiB, binentries 27.9 → 5.5, lcu 16.1 → 2.7, the five remaining tables 17.6 → 5.7 + + +The whole corpus drops from **~253 MiB of txt to ~52 MiB** of `.hashdb` - and the +binary is usable as-shipped: `open` is a header validation plus an `mmap`, with no +parse or expansion step before the first lookup. + + + + Bar chart of per-lookup latency on the compressed 2.09-million-entry game table: point hit 8.5 µs, batch hit 3.6 µs, miss 143 ns + + +A hit decompresses exactly one small frame; batched lookups amortize that by resolving in +arena order. A **miss is decided by binary search over the raw key section and never +touches string data** - ~143 ns whether the file is raw or compressed, which matters +because hash hunting hammers misses. + +Those point-lookup figures predate the reader's frame cache. With it, one archive's worth +of paths (20 000 entries of the 2 291 324-entry `game-2026-08-14` table, probed in hash +order) measures **1.07 µs cached against 5.92 µs uncached, and 0.66 µs batched** - point +lookups land within 1.6× of the batch path instead of an order of magnitude off it. Keys +scattered across the whole table, where no cache holds the working set, are unchanged at +~6.7 µs. Reproduce with: + +```sh +cargo run --release -p ltk_hashdb --example frame_cache -- +``` + + + + Bar chart of compressed size of the 162.5 MiB game string arena by layout: key-order 45.7 MiB, key-order with a trained dictionary 30.0 MiB, solid non-seekable stream 17.5 MiB, path-order 10.4 MiB + + +Sorting the arena by path packs each directory into the same frames, so the seekable +arena compresses **~4× better than key order** - beating even a solid, non-seekable +zstd stream - while making hits faster and directory-local batches frame-coherent. + +## Distribution + +The txt lists stay canonical: they are what the community PRs against and what git +merges. The binary is a generated release artifact, rebuilt from them, never the source +of truth. + +Tables ship as versioned GitHub release assets (`game-2026-08-14.lhdb`) alongside a +`manifest.json` naming the active version, sha256, and entry count per table. A machine +keeps one shared cache directory - `%LOCALAPPDATA%\LeagueToolkit\hashes` on Windows, +`$XDG_DATA_HOME/LeagueToolkit/hashes` on Linux, `~/Library/Application Support/…` on +macOS - so every mimir-backed tool on it reads the same files through the same page +cache. Installs are atomic: table files land under immutable versioned names first, and +the manifest is swapped last, so a reader sees either the whole old version or the whole +new one. A single-updater lock keeps two tools from downloading at once; readers never +take it. + +## Planned work + +[`ROADMAP.md`](ROADMAP.md) tracks what is next, in dependency order, along with the three +constraints that govern it - the version and schema equality gates that make every format +change additive, the reserved header fields that are the room left to be additive in, and +the pre-publish window in which breaking Rust API changes are free. From 8905da16a12a5f6b5578508a494d038b0c220a0b Mon Sep 17 00:00:00 2001 From: Crauzer Date: Tue, 25 Aug 2026 00:04:35 +0200 Subject: [PATCH 08/10] docs: use a local logo in the README header --- README.md | 2 +- docs/assets/logo.png | Bin 0 -> 168929 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/assets/logo.png diff --git a/README.md b/README.md index 9f63b2f..0e7a1bf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- LeagueToolkit logo + mimir logo

mimir

diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1fd1ae75d9588899ac4be8d0da3d5bc32bf33604 GIT binary patch literal 168929 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

*|}30RpkKt@;A1%K9bBdLcP6@ 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 %6YJTG97+*{ZB05Iez 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 literal 0 HcmV?d00001 From 5c1b65670ceb14a339268ee236c208e3ea649d6f Mon Sep 17 00:00:00 2001 From: Crauzer Date: Tue, 25 Aug 2026 00:06:31 +0200 Subject: [PATCH 09/10] chore: license under Apache-2.0 only --- Cargo.toml | 2 +- LICENSE-APACHE => LICENSE | 0 LICENSE-MIT | 21 --------------------- README.md | 11 +++-------- crates/ltk_mimir_cache/README.md | 2 +- 5 files changed, 5 insertions(+), 31 deletions(-) rename LICENSE-APACHE => LICENSE (100%) delete mode 100644 LICENSE-MIT diff --git a/Cargo.toml b/Cargo.toml index ff7e1ec..3b6cc58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*"] [workspace.package] edition = "2021" -license = "MIT OR Apache-2.0" +license = "Apache-2.0" repository = "https://github.com/LeagueToolkit/mimir" authors = ["League Toolkit contributors"] diff --git a/LICENSE-APACHE b/LICENSE similarity index 100% rename from LICENSE-APACHE rename to LICENSE diff --git a/LICENSE-MIT b/LICENSE-MIT deleted file mode 100644 index aad79e4..0000000 --- a/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 League Toolkit contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index 0e7a1bf..11871d3 100644 --- a/README.md +++ b/README.md @@ -318,14 +318,9 @@ source of truth. ## License -Licensed under either of - -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or - http://www.apache.org/licenses/LICENSE-2.0) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. +Licensed under the Apache License, Version 2.0 ([LICENSE](LICENSE) or +http://www.apache.org/licenses/LICENSE-2.0). 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 -dual licensed as above, without any additional terms or conditions. +licensed as above, without any additional terms or conditions. diff --git a/crates/ltk_mimir_cache/README.md b/crates/ltk_mimir_cache/README.md index dfb02b7..ff56e17 100644 --- a/crates/ltk_mimir_cache/README.md +++ b/crates/ltk_mimir_cache/README.md @@ -100,4 +100,4 @@ See [`docs/CONSUMERS.md`](../../docs/CONSUMERS.md) for the consumer-facing integ ## License -MIT OR Apache-2.0. +Apache-2.0. From 167af840da2a40a5cf70aad1be55a9b4e5c6dc7f Mon Sep 17 00:00:00 2001 From: Crauzer Date: Tue, 25 Aug 2026 00:11:34 +0200 Subject: [PATCH 10/10] chore: add NOTICE naming the copyright holder --- Cargo.toml | 2 +- NOTICE | 16 ++++++++++++++++ README.md | 5 ++++- crates/ltk_mimir_cache/README.md | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 NOTICE diff --git a/Cargo.toml b/Cargo.toml index 3b6cc58..9b2d690 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = ["crates/*"] edition = "2021" license = "Apache-2.0" repository = "https://github.com/LeagueToolkit/mimir" -authors = ["League Toolkit contributors"] +authors = ["Crauzer <0xcrauzer@proton.me>", "League Toolkit contributors"] [workspace.dependencies] # Internal crates (path-linked within this workspace; the published ones also diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..fb7005c --- /dev/null +++ b/NOTICE @@ -0,0 +1,16 @@ +mimir +Copyright 2026 Crauzer <0xcrauzer@proton.me> + +This product includes software developed by Crauzer and the League Toolkit +contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/README.md b/README.md index 11871d3..32cd3b1 100644 --- a/README.md +++ b/README.md @@ -318,8 +318,11 @@ source of truth. ## License +Copyright 2026 Crauzer <0xcrauzer@proton.me> + Licensed under the Apache License, Version 2.0 ([LICENSE](LICENSE) or -http://www.apache.org/licenses/LICENSE-2.0). +http://www.apache.org/licenses/LICENSE-2.0). Attribution requirements are in +[NOTICE](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 diff --git a/crates/ltk_mimir_cache/README.md b/crates/ltk_mimir_cache/README.md index ff56e17..a63e643 100644 --- a/crates/ltk_mimir_cache/README.md +++ b/crates/ltk_mimir_cache/README.md @@ -100,4 +100,4 @@ See [`docs/CONSUMERS.md`](../../docs/CONSUMERS.md) for the consumer-facing integ ## License -Apache-2.0. +Apache-2.0. Copyright 2026 Crauzer <0xcrauzer@proton.me>.