Skip to content

Asset source reference

A source is one layer of the overlay. rtb-assets ships three, and the AssetSource trait lets you add your own. All four are re-exported at the crate root from rtb_assets::source.

The AssetSource trait

pub trait AssetSource: Send + Sync + 'static {
    fn read(&self, path: &str) -> Option<Vec<u8>>;
    fn list(&self, dir: &str) -> Vec<String>;
    fn name(&self) -> &str;
}
Method Contract
read The bytes at path if this layer provides it, None otherwise. None must mean "not here", never "here but broken" — there is no error channel.
list The immediate entries of dir — files and subdirectory names, with the dir prefix stripped. Empty when dir does not exist on this layer.
name A diagnostic-only label, quoted in parse errors and printed by Debug. The empty string is acceptable for anonymous sources.

The Send + Sync + 'static bound is not optional: Assets stores every layer as Arc<dyn AssetSource> and is itself Send + Sync, so a source holding a Rc, a RefCell or a borrowed lifetime will not compile.

Implementations are shared, never cloned — Assets::clone bumps a refcount. A source that is expensive to construct is constructed once.

read returns an owned Vec<u8> rather than a borrow, so every call allocates and copies. That is the price of letting a layer synthesise bytes rather than only hand out ones it is already holding.

What paths are accepted?

Asset paths are /-separated strings, and the same string is expected to address the same asset on every layer. The rules differ subtly by layer, which is worth knowing before you debug a lookup:

Path EmbeddedSource / MemorySource DirectorySource
a/b.txt matches the key exactly joined onto the root
./a/b.txt no match — keys are compared literally matches; . is a no-op component
/a/b.txt no match rejected — absolute
a/../b.txt no match rejected — .. is refused even when it stays inside the root
a\b.txt no match on Unix on Unix, one filename containing a backslash

Two consequences worth internalising:

  • Do not prefix asset paths with ./. It works on a directory layer and fails on an embedded or in-memory one, which makes for a bug that only appears once a user adds an override directory.
  • Do not build asset paths with std::path::Path. On Windows it will hand you backslashes, which no layer treats as separators for key lookup.

Path rejection on DirectorySource is lexical and silent — see Why path traversal is rejected lexically.

EmbeddedSource<E> — files compiled into the binary

pub struct EmbeddedSource<E: RustEmbed + Send + Sync + 'static> { /* private */ }

impl<E: RustEmbed + Send + Sync + 'static> EmbeddedSource<E> {
    pub const fn new(name: &'static str) -> Self;
}

Adapts a rust-embed type. The struct is zero-sized: all the bytes live in the static tables #[derive(RustEmbed)] generates, so an embedded layer costs nothing at runtime beyond the Arc.

new is const and takes a &'static str, so an embedded layer's label cannot be computed at runtime — unlike the other two sources, which take impl Into<String>. Register it through the builder:

#[derive(rust_embed::RustEmbed)]
#[folder = "assets/"]
struct Defaults;

let assets = Assets::builder().embedded::<Defaults>("defaults").build();

The #[folder] path is resolved relative to the consuming crate's Cargo.toml. A missing folder is a compile error unless you add #[allow_missing = true], which yields an empty layer instead.

read delegates to E::get; list walks E::iter() and keeps the first path segment after the prefix. Both see exactly what was embedded at build time — assets are never read from disk at runtime, including in debug builds.

DirectorySource — files on the host filesystem

pub struct DirectorySource { /* private */ }

impl DirectorySource {
    pub fn new(root: impl Into<PathBuf>, name: impl Into<String>) -> Self;
}

Reads under a fixed root. Nothing about the root is validated at construction: a root that does not exist, is not a directory, or is not readable constructs fine and then behaves as an empty layer. That is deliberate — an override directory the user has not created yet is the normal case, not an error.

Call Result
read on a file that exists and is readable Some(bytes)
read on a missing file, a directory, or a permission-denied file None
read on a rejected path (.., absolute, Windows prefix) None, no filesystem call made
list("") or list(".") entries of the root
list on a missing or rejected directory []

Because None covers both "absent" and "present but unreadable", a file the process lacks permission to read is indistinguishable from one that is not there, and a lower-priority layer will supply it instead. If that matters to your tool, check the file yourself before building the overlay.

Entries returned by list are sorted, and include subdirectory names alongside filenames. Symlinks are followed by the underlying std::fs calls; the lexical path check does not attempt to detect them.

MemorySource — a map of bytes

pub struct MemorySource { /* private */ }

impl MemorySource {
    pub fn new(name: impl Into<String>, files: HashMap<String, Vec<u8>>) -> Self;
}

Backed by a HashMap<String, Vec<u8>>. Keys are matched literally, so they should be written the way callers will ask for them: "templates/readme.md", not "./templates/readme.md" and not an absolute path.

The map is moved in and is immutable thereafter — there is no way to add, remove, or replace an entry on a built layer. Build a new Assets instead; it is cheap.

Intended for tests and for scaffolders that generate content in memory before writing it out. Nothing stops you using it in production, but a MemorySource holds every byte resident for the life of the process.

Writing your own source

Implement the trait and register it with .source(Arc::new(MySource)). Three rules keep a custom layer well-behaved:

  1. Never panic, and never block indefinitely. read is called on whatever thread the caller is on, including inside a merge across layers.
  2. Return None for anything you cannot serve, including your own internal errors. Callers cannot distinguish causes and a lower layer is expected to get its turn.
  3. Make list agree with read. A name returned by list should be readable by read under dir/name, or be a directory. Listings that promise files that cannot be read are the one failure the overlay cannot paper over.

There is no way for a custom source to report an error, request a retry, or veto lower layers. If your layer is a network fetch, cache it behind the trait and decide there what a failure means.