Skip to content

Why an overlay filesystem

What problem does this crate actually solve?

A command-line tool ships with defaults: config templates, scaffolding files, help text, themes. Those need to be inside the binary, because a single downloadable executable that then hunts for a support directory is a worse tool.

Users then need to override them. Site administrators want a /etc/mytool copy that applies to everyone; individuals want their own under their config directory; the tool's own tests want neither, and need fixtures that exist only in memory for the length of a test.

Without an abstraction, every place in the tool that reads an asset grows the same ladder of if user_file_exists { … } else if system_file _exists { … } else { embedded }. It gets written slightly differently each time, the precedence drifts between call sites, and the test path becomes a separate branch that production never runs.

rtb-assets collapses that to one decision made once, at startup, where the layers are registered. Everywhere else, code asks for a path and gets bytes.

Why is it read-only?

Because the alternative is a filesystem, and a filesystem is a much larger promise.

Writing into an overlay raises questions that have no single right answer: which layer receives the write, what happens when the winning layer is an embedded one that physically cannot be written, whether a write to a shadowed path is visible, what happens to the merged view of a YAML file when one contributor is edited. Every answer is defensible and none is obviously correct, so the crate declines the question.

Tools that need to write — a scaffolder emitting a project, a command saving user config — write through their own code, with their own notion of where output belongs. That is a different concern, made clearer by not living behind a read API.

The read-only contract also buys the cheap Clone. Layers sit behind Arc and nothing mutates, so an Assets can be handed to every command, thread and task without a lock.

Why not use an existing virtual filesystem crate?

The obvious candidate is the vfs crate's OverlayFS, and it was considered and rejected on two counts.

OverlayFS is a two-layer construct: a writable upper and a read-only lower. The requirement here is N layers — embedded, system, user, and whatever else a tool wants to stack — with a precedence that is declared once and holds for the life of the process.

More importantly, no general-purpose virtual filesystem knows what a YAML file is, and it should not. A filesystem's job is bytes. But the central use case for asset layering is configuration, and for configuration, last-wins shadowing is the wrong answer: a user who wants to change one colour in a theme should not have to copy the whole theme file to do it. That requirement — structured deep merge across the same stack of layers that serves bytes — is what makes this a small purpose-built trait rather than a wrapper over a general one.

The trade is interoperability. An Assets is not a VfsPath and cannot be handed to code that expects one. If that ever bites, an adapter can be added without changing the model; the model cannot easily be added to an adapter.

Why a trait rather than a fixed set of three layers?

Three layers cover the tool cases: embedded, disk, memory. The trait exists so the crate does not have to grow a fourth, a fifth and a sixth for the cases it did not anticipate — an in-process archive, a remote-fetched theme pack, a layer synthesised from a database.

The trait is deliberately tiny — read, list, name — which keeps the cost of implementing one low and the risk of a badly-behaved layer small. There is no error channel, no capability negotiation and no metadata, because every one of those would have to be understood by Assets, and each would push behaviour into layers where callers cannot see it.

Where this sits in the toolkit

rtb-assets is one crate of the phpboyscout Rust toolkit, extracted from rust-tool-base so it can be used without adopting the framework. Within the framework, the application context holds an Assets, the CLI builder threads a user-constructed overlay through it, and the interactive docs browser reads its markdown through list_dir and open_text.

Outside it, the crate has no framework dependencies at all — the dependency ban list refuses rust-tool-base by name — so it stands alone in any binary that wants embedded defaults with overrides.