rtb-assets 0.6.4

Embedded-asset + overlay filesystem abstraction. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Asset source reference
description: The AssetSource trait and the three built-in layers — EmbeddedSource, DirectorySource, MemorySource — with their path rules and failure behaviour.
tags: [reference, api, sources]
---

# 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

```rust
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](../explanation/path-safety.md).

## `EmbeddedSource<E>` — files compiled into the binary

```rust
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`](https://crates.io/crates/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:

```rust
#[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](crate-features.md#debug-embed-is-on-and-that-changes-rust-embeds-default-behaviour),
including in debug builds.

## `DirectorySource` — files on the host filesystem

```rust
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

```rust
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.