---
title: rtb-assets
description: Overlay asset filesystem over rust-embed, physical directories and in-memory fixtures, with YAML/JSON deep-merge across layers.
---
# `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`][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](https://www.rfc-editor.org/rfc/rfc7396) deep
merge**, so an override file states only the keys it changes.
Part of the [phpboyscout Rust toolkit](https://rust.phpboyscout.uk);
extracted from — and used by —
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base).
```rust
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
| Get a working binary with overridable defaults | [Ship defaults a user can override](tutorials/ship-defaults-a-user-can-override.md) |
| Design a config file that merges well | [Merge layered configuration](how-to/merge-layered-configuration.md) |
| Add a layer the built-ins do not cover | [Write a custom asset source](how-to/write-a-custom-asset-source.md) |
| Test code that reads assets | [Use assets in tests](how-to/use-assets-in-tests.md) |
| Work out why a read returned nothing | [Diagnose a failed asset lookup](how-to/diagnose-a-failed-asset-lookup.md) |
| Look up a method, an error or a default | [Reference](reference/assets-api.md) |
| Understand why it works this way | [Why an overlay filesystem](explanation/why-an-overlay-filesystem.md) |
| Find out whether it does the thing you need | [What rtb-assets does not do](explanation/limitations.md) |
## The public API at a glance
| `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](reference/assets-api.md). Rustdoc: [docs.rs/rtb-assets](https://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](explanation/limitations.md)
— worth reading before you adopt the crate rather than after.
## Path traversal is rejected lexically
!!! warning "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](explanation/path-safety.md)
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`
(`T1`–`T14`, including path traversal in `T14`) and 6 Gherkin scenarios
in `tests/features/assets.feature` (`S1`–`S6`) driven by a
[Cucumber](https://github.com/cucumber-rs/cucumber) runner. `just ci`
runs the same gate CI does: `fmt-check`, `clippy -D warnings`,
`cargo doc`, tests, and `cargo deny`.
[rust-embed]: https://crates.io/crates/rust-embed
## Further reading
The blog carries a curated route through this subject: **[Rust, and what survived the port](https://phpboyscout.uk/topics/rust/)** collects
everything written about it, ordered so you can start at the beginning rather
than newest-first.
!!! tip "Ask phpbotscout"
{ width="84" align=left style="border-radius:10px;margin-right:1rem" }
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](https://discord.gg/mQzGbmGyzZ){ .md-button .md-button--primary }