Skip to content

How layer precedence works

One stack, two different answers

Assets holds an ordered stack of layers. Registration order is priority order, lowest first, so the last layer you register is the one with the final say.

What the stack does with that ordering depends on what you ask it for, and this is the single most important thing to understand about the crate:

Question How the stack answers
open, open_text, exists walks from the top and stops at the first layer that has the path
list_dir asks every layer and unions the results
load_merged_yaml, load_merged_json reads every layer that has the path and folds them together

A tool can therefore have a user override that completely replaces an icon, while the same user's config.yaml only replaces the two keys it mentions. That is not an inconsistency; it is the point.

Why blobs shadow and config merges

A PNG has no meaningful union with another PNG. There is exactly one sensible answer to "the user replaced the logo", and it is the user's logo. Byte reads therefore shadow: highest layer wins, whole file.

Configuration is the opposite. A user who wants a different accent colour should write three lines, not copy a hundred-line theme file they will then never keep in step with the tool's updates. Copying a whole file to change one field is how config files rot: the defaults move on, the copy does not, and the user is left running last year's settings with this year's binary.

So structured reads merge. Only the keys a layer actually mentions are overridden, which means an override file states a difference rather than a replacement, and untouched keys keep tracking the defaults that ship with the binary.

The exact rules for what merges and what replaces are in Merge semantics, and two of them — null deletes, and an empty file wipes — are surprising enough to be worth reading before you design a config schema.

Why list_dir unions instead of shadowing

Because a directory is not a value, it is a place. If the embedded layer ships three templates and the user adds a fourth in their override directory, the answer to "what templates are there" is four, not one. Shadowing directories would mean a user could only ever replace a template set wholesale, never extend it.

The union is deduplicated, so a file present on three layers appears once, and the entry does not tell you which layer supplied it. If you then read one of those names, the normal shadowing rule decides which layer's bytes you get.

Why exists asks every layer

exists returns true if any layer provides the path. It is deliberately not "would a read succeed from the top layer" — that is the same question, because a read walks down until it finds something.

It exists mainly so callers can branch without allocating. It does allocate, as it happens, because the trait's only lookup method returns the bytes; if you are going to read the file anyway, call open and match on the Option rather than testing first.

Reasoning about a stack that surprised you

The debug output is the fastest tool here. Assets prints its layer labels in priority order, lowest first:

Assets { layers: ["defaults", "system", "user"] }

That one line answers most precedence confusion: whether a layer was registered at all, and whether it was registered in the order you intended. Labels are also what a parse error names, which is why it is worth giving them meaningful values even though nothing looks them up.

When a lookup returns the wrong bytes, work through it in this order:

  1. Is the layer in the stack? Check the Debug output.
  2. Is it in the right position? Later beats earlier, always.
  3. Is the path keyed the way you asked for it? Embedded and in-memory layers match keys literally, so ./config.yaml and config.yaml are different assets on those layers and the same asset on a directory layer.
  4. Is it a merge, not a read? A merged result is not any single layer's file, and no layer's file will match it.

Precedence is fixed at build time

There is no way to reorder, insert, remove or disable a layer after build(). The stack is frozen behind an Arc<[…]> and there is no mutating API.

That is a real constraint if your tool wants to change its overlay in response to a config reload or a --no-user-overrides flag. The answer is to build a new Assets and swap it in — construction is cheap, no asset bytes are copied, and the old handle stays valid for anything still holding it. It is not the answer if you wanted the change to be visible to code already holding a clone; that code keeps the stack it was given.