rtb-assets¶
rtb-assets unifies three kinds of asset storage behind a single
read-only API:
- Embedded via
rust-embed— compile-time bundling with dev-mode disk passthrough. Default configs, templates, docs. - Physical directory — per-user overrides under
$XDG_CONFIG_HOME/<tool>/…. - In-memory — test fixtures and scaffolder scratch space.
Binary-like blobs follow last-wins shadowing; structured data (YAML/JSON) follows RFC-7396 deep merge so nested maps combine recursively.
Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base.
Overview¶
Downstream tools don't care where an asset lives. Assets::open,
Assets::list_dir, and Assets::load_merged_yaml offer a uniform
surface over heterogeneous backing stores. The crate ships three
built-in layer types plus an AssetSource trait for exotic cases
(HTTP overlays, in-process archives, etc.).
Design rationale¶
- Own
AssetSourcetrait, notvfs::OverlayFS. Thevfscrate'sOverlayFSis 2-layer; this crate needs N-layer merge with structured-data awareness. Anas_vfs()adapter can be added later if downstream interop demands it. json-patch::mergefor YAML. YAML round-trips throughserde_yaml::Value → serde_json::Valuebefore merging. Adequate for all realistic config shapes (maps, sequences, scalars, null).- Parse failures name the offending layer. Silent fallback to
a lower layer would hide bugs. A layer's
name()feedsAssetError::Parse.path. - Path traversal rejected lexically.
DirectorySource::readgoes throughsafe_join—.., absolute paths, and Windows prefix components are rejected at the front door, without a filesystem call.
Public API¶
| Item | Purpose |
|---|---|
Assets |
Read-only overlay handle; Clone is refcount-only. open, open_text, exists, list_dir, load_merged_yaml, load_merged_json. |
AssetsBuilder |
Registers layers in priority order (later wins): embedded, directory, memory, or any custom source. |
AssetSource |
Trait for custom layers — read, list, and a diagnostic name(). |
EmbeddedSource<E> |
Layer over rust-embed generated tables — compile-time bundled defaults. |
DirectorySource |
Layer over a directory on disk — user overrides, staging. |
MemorySource |
Layer over a HashMap<String, Vec<u8>> — tests, scaffolder scratch. |
AssetError |
NotFound, NotUtf8, Parse — each a miette::Diagnostic with a stable code. |
Full API reference: docs.rs/rtb-assets.
Usage patterns¶
Embedded defaults + user overrides¶
use rtb_assets::Assets;
#[derive(rust_embed::RustEmbed)]
#[folder = "assets/"]
struct Defaults;
let assets = Assets::builder()
.embedded::<Defaults>("defaults")
.directory("/etc/mytool/assets", "system")
.directory(dirs::config_dir().unwrap().join("mytool"), "user")
.build();
// First existing file wins (user > system > defaults).
let icon = assets.open("icons/app.png").expect("missing icon");
Deep-merged YAML config¶
#[derive(Deserialize)]
struct Theme { name: String, palette: Palette }
#[derive(Deserialize)]
struct Palette { primary: String, accent: String }
// defaults.yaml ships `name: "classic", palette: { primary: "blue", accent: "orange" }`.
// user.yaml overrides just the accent: `palette: { accent: "pink" }`.
let theme: Theme = assets.load_merged_yaml("theme.yaml")?;
assert_eq!(theme.name, "classic"); // from defaults
assert_eq!(theme.palette.primary, "blue"); // from defaults
assert_eq!(theme.palette.accent, "pink"); // from user
Security¶
Path traversal is rejected lexically
DirectorySource::read("../../etc/passwd") returns None
— safe_join rejects .. components, absolute paths, and
Windows prefixes before touching the filesystem. This is a
lexical check, not a canonicalize()-based one; symlink
following is a caller concern.
The test suite verifies this (T14) with a DirectorySource rooted
at /tmp/assets and a sibling /tmp/secret.txt the source
cannot reach.
See Engineering Standards §1.1 for the standing rule.
Where it's wired¶
In rust-tool-base, rtb-app holds an Arc<Assets> on the application
context, rtb-cli's builder threads in a user-constructed overlay, and
the TUI docs browser reads its markdown via list_dir + open_text.
Outside the framework the crate is dependency-light and stands alone.
Design record¶
The authoritative contract is the crate's v0.1 spec, retained in the
rust-tool-base
spec series.
Tests cover 19 acceptance criteria: 14 unit tests (tests/unit.rs,
T1–T14 including path traversal) and 6 Gherkin scenarios
(tests/features/assets.feature).