rtb-assets 0.6.5

Embedded-asset + overlay filesystem abstraction. Part of the phpboyscout Rust toolkit.
Documentation
---
title: What rtb-assets does not do
description: The capabilities deliberately absent from the crate, the combinations that do not work, and what to reach for instead.
tags: [explanation, limitations]
---

# What `rtb-assets` does not do

Every item here is a deliberate absence or a known constraint, checked
against version 0.6.3. If you are evaluating the crate, read this page
before the API reference — it is faster to find out here that something
is not supported than to look for the method that would have done it.

## It cannot write, create or delete

There is no write API of any kind, and none is planned. `Assets` is a
read handle over a fixed stack. Tools that emit files — scaffolders,
`config set` commands, cache writers — do their own writing with their
own notion of where output belongs. [Why an overlay
filesystem](why-an-overlay-filesystem.md) sets out the reasoning.

## It cannot tell you which layer supplied a path

`open` returns bytes. `list_dir` returns names. Neither says where the
value came from, and there is no `origin_of(path)` or `layers()`
accessor. The only visibility is `Debug`, which prints the layer
labels but not their contents.

If your tool wants to show a user "this setting came from
`/etc/mytool/config.yaml`", it must keep its own record of the roots it
registered and check them itself. Layered *configuration* with
provenance is a different problem, solved by a config crate, not by an
asset overlay.

## It cannot distinguish a file from a directory

`list_dir` returns bare names, mixing files and subdirectories with no
marker. `exists` returns `false` for a directory. Probing with `exists`
after a listing is the only way to tell them apart, and it costs a read
attempt per name.

## It has no metadata

No size, no modification time, no MIME type, no permissions, no
checksum. A `rust-embed` `EmbeddedFile` carries metadata that
`EmbeddedSource` discards on the way through, because a
`DirectorySource` and a `MemorySource` cannot supply the same fields
and a partial metadata API is worse than none.

## It has no streaming reads

`read` returns an owned `Vec<u8>`. A 500 MB asset is a 500 MB
allocation, in full, before the caller sees the first byte, and there
is no `Read`/`Seek` handle to be had. The crate is built for
config-and-template-sized files; large media belongs on disk with a
path you open yourself.

## It caches nothing and watches nothing

Every `open` on a `DirectorySource` re-reads from disk. There is no
cache to invalidate — which also means there is nothing stale — and no
file watching, no reload hook, and no notification when an override
file changes. A tool that wants hot reload builds a new `Assets` when
it decides something changed, and swaps it in.

Note also that a clone taken before the swap keeps the old stack. There
is no way to mutate an `Assets` that other code is already holding.

## The layer stack is fixed once built

No reordering, no insertion, no removal, no enabling and disabling. A
`--no-user-overrides` flag has to be honoured at construction, by not
registering the layer, rather than by turning it off later.

## Only YAML and JSON are merged

`load_merged_yaml` and `load_merged_json` are the whole set. There is
no TOML, INI, XML or `.env` loader, and no way to plug one in — the
merge is not generic over a format trait.

A TOML file can still live in an overlay; read it with `open_text` and
parse it yourself. It just gets last-wins shadowing rather than a
merge, so a user override replaces the entire file.

## Merging cannot express "set this to null"

The merge is RFC 7396, in which `null` in a higher layer *deletes* the
key rather than setting it. There is no escape hatch. If your schema
needs a genuine tri-state, encode it as a value the format can carry
rather than as `null`. See [Merge
semantics](../reference/merge-semantics.md#why-does-setting-a-key-to-null-remove-it).

## Arrays cannot be extended by an override

Also RFC 7396: an array in a higher layer replaces the array below it
entirely. There is no append, no merge-by-index, and no merge-by-key.
A user who wants to add one entry to a list must restate the whole
list, and will not pick up new default entries in later releases.

If that matters, model the collection as a map keyed by name rather
than as a list. Maps merge; arrays do not.

## YAML is converted to JSON before merging, and loses things on the way

`.inf` and `.nan` have no JSON equivalent, so they fail the parse.
Timestamps become strings. Unknown tags are dropped. `yes` and `no` are
strings, not booleans, because the parser is held to YAML 1.2
booleans, and `<<` merge keys are ordinary keys. The full conversion table is in [Merge
semantics](../reference/merge-semantics.md#what-does-yaml-lose-on-the-way-through).

## A single embedded folder cannot be split across layers

`EmbeddedSource` adapts one `#[derive(RustEmbed)]` type, and a
`RustEmbed` type embeds one folder. Two embedded layers means two types
and two folders. Filtering within a folder is done with rust-embed's
own `#[include]`/`#[exclude]` attributes, not by anything this crate
offers.

## There is no dev-mode disk passthrough for embedded assets

`rtb-assets` enables rust-embed's `debug-embed` feature, which turns
off the usual read-from-disk-in-debug-builds behaviour, for the whole
dependency graph. Editing an embedded asset needs a rebuild. Layer a
`DirectorySource` over the embedded one during development if you want
live editing. See [Crate features](../reference/crate-features.md#debug-embed-is-on-and-that-changes-rust-embeds-default-behaviour).

## Path traversal protection has limits worth knowing

The `DirectorySource` check is lexical and does not follow symlinks, so
a symlink inside an otherwise-trusted root reaches outside it. It is
also silent — a rejected path is indistinguishable from a missing file.
[Why path traversal is rejected lexically](path-safety.md) covers the
boundary in full.

## It is not async, and layers must not block

Every method is synchronous. A custom source that performs a network
fetch will block whatever thread the caller is on, including inside a
merge that touches several layers. Cache behind the trait and decide
there what a slow or failed fetch means; the trait has no way to report
either.

## The YAML conversion is part of the contract

YAML is parsed by `serde-saphyr`, configured to keep the behaviour the
crate had under `serde_yaml`, which it replaced because
`serde_yaml` is unmaintained (RUSTSEC-2024-0449). Changing the parser or
its options changes the conversion behaviour documented above, so treat
that table as part of the contract, not as an implementation detail.