Skip to content

rtb-assets

rtb-assets lets a Rust binary ship its own defaults and let users override them. It unifies three kinds of asset storage behind one read-only API:

  1. Embedded via rust-embed — files compiled into the executable. Default configs, templates, docs.
  2. Physical directory — system-wide and per-user override directories on disk.
  3. In-memory — test fixtures and scaffolder scratch space.

Byte reads follow last-wins shadowing: the highest-priority layer that has the file supplies the whole file. Structured reads (YAML, JSON) follow RFC 7396 deep merge, so an override file states only the keys it changes.

Part of the phpboyscout Rust toolkit; extracted from — and used by — rust-tool-base.

use rtb_assets::Assets;

let assets = Assets::builder()
    .embedded::<Defaults>("defaults")   // lowest priority
    .directory("/etc/mytool", "system")
    .directory(user_config_dir, "user") // highest priority
    .build();

let icon = assets.open("icons/app.png");            // user's, if they have one
let config: Config = assets.load_merged_yaml("config.yaml")?;  // all three, merged

Where to start

If you want to… Read
Get a working binary with overridable defaults Ship defaults a user can override
Design a config file that merges well Merge layered configuration
Add a layer the built-ins do not cover Write a custom asset source
Test code that reads assets Use assets in tests
Work out why a read returned nothing Diagnose a failed asset lookup
Look up a method, an error or a default Reference
Understand why it works this way Why an overlay filesystem
Find out whether it does the thing you need What rtb-assets does not do

The public API at a glance

Item Purpose
Assets Read-only overlay handle. open, open_text, exists, list_dir, load_merged_yaml, load_merged_json. Clone is refcount-only.
AssetsBuilder Registers layers in priority order, later wins: embedded, directory, memory, source.
AssetSource Trait for custom layers — read, list, and a diagnostic name().
EmbeddedSource<E> Layer over a rust-embed type — compile-time bundled defaults.
DirectorySource Layer over a directory on disk — user and system overrides.
MemorySource Layer over a HashMap<String, Vec<u8>> — tests, scaffolder scratch.
AssetError NotFound, NotUtf8, Parse — each a miette::Diagnostic with a stable code.

Full signatures and failure behaviour: Assets and AssetsBuilder reference. Rustdoc: docs.rs/rtb-assets.

What it does not do

It cannot write, delete or watch. It cannot tell you which layer supplied a value. It merges YAML and JSON and no other format, arrays replace rather than append, and null in an override deletes a key rather than setting it. The full list, with the reasoning and the workarounds, is What rtb-assets does not do — worth reading before you adopt the crate rather than after.

Path traversal is rejected lexically

Rejection is silent, and symlinks are not checked

DirectorySource refuses any path containing .., any absolute path, and any Windows prefix component, before it touches the filesystem. open("../../etc/passwd") returns None.

The check is lexical, not canonicalize()-based: a symlink inside an otherwise-trusted root still resolves outside it. And rejection is indistinguishable from a missing file — nothing logs, and no distinct error variant exists.

Why path traversal is rejected lexically sets out the threat model and the boundary.

How it is tested

The behaviour on this site is covered by 14 unit tests in tests/unit.rs (T1T14, including path traversal in T14) and 6 Gherkin scenarios in tests/features/assets.feature (S1S6) driven by a Cucumber runner. just ci runs the same gate CI does: fmt-check, clippy -D warnings, cargo doc, tests, and cargo deny.

Further reading

The blog carries a curated route through this subject: Rust, and what survived the port collects everything written about it, ordered so you can start at the beginning rather than newest-first.

Ask phpbotscout

phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.

Join the Discord