use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LayerView {
pub includes: Vec<Include>,
pub tools: Vec<String>,
}
pub fn view(bytes: &[u8]) -> Result<LayerView, ComposeError> {
let json: serde_json::Value =
serde_json::from_slice(bytes).map_err(|e| ComposeError::Unreadable(e.to_string()))?;
let mut v = LayerView::default();
let Some(entries) = json["manifests"].as_array() else {
return Ok(v);
};
for e in entries {
let ann = &e["annotations"];
let digest = e["digest"].as_str().unwrap_or_default().to_string();
match ann[crate::kind::ANN_KIND].as_str() {
Some("layer") => v.includes.push(Include {
digest,
realm: ann[ANN_INCLUDE_REALM]
.as_str()
.filter(|s| !s.is_empty())
.map(|s| s.to_string()),
layer: ann[ANN_INCLUDE_LAYER].as_str().map(|s| s.to_string()),
}),
None => {
if let Some(t) = ann["eu.pulseengine.tool"].as_str() {
v.tools.push(t.to_string());
}
}
Some(_) => {}
}
}
Ok(v)
}
pub const ANN_INCLUDE_REALM: &str = "eu.pulseengine.varve.include.realm";
pub const ANN_INCLUDE_LAYER: &str = "eu.pulseengine.varve.include.layer";
pub const MAX_DEPTH: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Include {
pub digest: String,
pub realm: Option<String>,
pub layer: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ComposeError {
#[error(
"composition cycle: layer {digest} includes itself, directly or through \
{via} — refusing to follow it"
)]
Cycle { digest: String, via: String },
#[error(
"composition is more than {MAX_DEPTH} layers deep — refusing to walk further \
(a layer graph this deep is a mistake, not a design)"
)]
TooDeep,
#[error("layer manifest could not be read for composition: {0}")]
Unreadable(String),
#[error(
"tool '{tool}' is provided by more than one layer of this composition — {first} \
and {second} — and the pin has not chosen between them. varve does not pick a \
winner: what a bare name runs is decided by the pin, never by install order. \
{fix}"
)]
AmbiguousTool {
tool: String,
first: String,
second: String,
fix: String,
},
#[error(
"this project's pin selects '{selector}', but no layer of this composition from \
realm '{realm}' provides '{tool}' — it is provided by: {providers}. Fix the \
qualifier in varve.toml; varve will not substitute another realm's binary for \
the one the pin named."
)]
RealmProvidesNoSuchTool {
selector: String,
realm: String,
tool: String,
providers: String,
},
#[error(transparent)]
ConflictingPayload(#[from] Box<PayloadConflict>),
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[error(
"{name} {version} is offered by two layers in this composition with DIFFERENT bytes: \
{first} has {first_digest}, {second} has {second_digest} — refusing to choose. \
Two realms disagreeing about what one name-and-version IS cannot both be exported; \
a name at different VERSIONS is legal and both export, but one (name, version) must \
be one artifact. Re-deposit one of the layers against the other's bytes, or drop the \
duplicate from the composition."
)]
pub struct PayloadConflict {
pub name: String,
pub version: String,
pub first: String,
pub first_digest: String,
pub second: String,
pub second_digest: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PayloadOrigin {
pub name: String,
pub version: String,
pub digest: String,
pub realm: String,
pub layer: String,
}
impl PayloadOrigin {
fn describe(&self) -> String {
format!("realm '{}' layer {}", self.realm, self.layer)
}
}
pub fn union_payloads<T>(
items: Vec<(PayloadOrigin, T)>,
) -> Result<Vec<(PayloadOrigin, T)>, ComposeError> {
let mut first_seen: BTreeMap<(String, String), PayloadOrigin> = BTreeMap::new();
let mut out = Vec::new();
for (origin, payload) in items {
let key = (origin.name.clone(), origin.version.clone());
match first_seen.get(&key) {
Some(first) if first.digest != origin.digest => {
return Err(ComposeError::ConflictingPayload(Box::new(
PayloadConflict {
name: origin.name.clone(),
version: origin.version.clone(),
first: first.describe(),
first_digest: first.digest.clone(),
second: origin.describe(),
second_digest: origin.digest,
},
)));
}
Some(_) => continue,
None => {
first_seen.insert(key, origin.clone());
out.push((origin, payload));
}
}
}
Ok(out)
}
pub fn includes(v: &LayerView) -> Vec<Include> {
v.includes.clone()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Walked {
pub digest: String,
pub realm: String,
pub view: LayerView,
}
pub fn walk<F>(
root_digest: &str,
root_realm: &str,
root: &LayerView,
mut fetch: F,
) -> Result<Vec<Walked>, ComposeError>
where
F: FnMut(&str) -> Option<LayerView>,
{
let mut out = vec![Walked {
digest: root_digest.to_string(),
realm: root_realm.to_string(),
view: root.clone(),
}];
let mut emitted: BTreeSet<String> = BTreeSet::new();
emitted.insert(root_digest.to_string());
let mut stack: Vec<(String, String, LayerView, BTreeSet<String>)> = vec![(
root_digest.to_string(),
root_realm.to_string(),
root.clone(),
BTreeSet::from([root_digest.to_string()]),
)];
while let Some((from, realm, view, path)) = stack.pop() {
if path.len() > MAX_DEPTH {
return Err(ComposeError::TooDeep);
}
for inc in includes(&view) {
if path.contains(&inc.digest) {
return Err(ComposeError::Cycle {
digest: inc.digest.clone(),
via: from.clone(),
});
}
let Some(child) = fetch(&inc.digest) else {
continue;
};
let child_realm = inc.realm.clone().unwrap_or_else(|| realm.clone());
if emitted.insert(inc.digest.clone()) {
out.push(Walked {
digest: inc.digest.clone(),
realm: child_realm.clone(),
view: child.clone(),
});
}
let mut child_path = path.clone();
child_path.insert(inc.digest.clone());
stack.push((inc.digest.clone(), child_realm, child, child_path));
}
}
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolProvider {
pub tool: String,
pub realm: String,
pub layer: String,
pub digest: String,
}
impl ToolProvider {
pub fn qualified(&self) -> Option<String> {
(!self.realm.is_empty()).then(|| format!("{}/{}", self.realm, self.tool))
}
fn describe(&self) -> String {
if self.realm.is_empty() {
format!("layer {} (no realm named)", self.layer)
} else {
format!("realm '{}' layer {}", self.realm, self.layer)
}
}
}
pub fn select_tools(
providers: &[ToolProvider],
chosen: &BTreeMap<String, String>,
) -> Result<BTreeMap<String, ToolProvider>, ComposeError> {
let mut by_name: BTreeMap<&str, Vec<&ToolProvider>> = BTreeMap::new();
for p in providers {
let slot = by_name.entry(p.tool.as_str()).or_default();
if !slot.iter().any(|q| q.digest == p.digest) {
slot.push(p);
}
}
let mut out = BTreeMap::new();
for (tool, offers) in by_name {
let picked: Vec<&ToolProvider> = match chosen.get(tool) {
Some(realm) => offers
.iter()
.copied()
.filter(|p| &p.realm == realm)
.collect(),
None => offers.clone(),
};
match picked.as_slice() {
[only] => {
out.insert(tool.to_string(), (*only).clone());
}
[] => {
let realm = chosen.get(tool).cloned().unwrap_or_default();
return Err(ComposeError::RealmProvidesNoSuchTool {
selector: format!("{realm}/{tool}"),
realm,
tool: tool.to_string(),
providers: offers
.iter()
.map(|p| p.describe())
.collect::<Vec<_>>()
.join(", "),
});
}
[first, second, ..] => {
return Err(ComposeError::AmbiguousTool {
tool: tool.to_string(),
first: first.describe(),
second: second.describe(),
fix: fix_for(tool, first, second, chosen.contains_key(tool)),
});
}
}
}
Ok(out)
}
fn fix_for(
tool: &str,
first: &ToolProvider,
second: &ToolProvider,
already_qualified: bool,
) -> String {
match (first.qualified(), second.qualified()) {
(Some(a), Some(b)) if first.realm != second.realm => format!(
"Choose one in varve.toml: tools = [\"{a}\"] — or tools = [\"{b}\"]. The layer you \
do not choose stays installed and verified, and `varve run {a}` / `varve run {b}` \
still reach either one."
),
_ if already_qualified || first.realm == second.realm => format!(
"Both are in realm '{}', so a realm qualifier cannot separate them — one of those \
two layers must stop exposing '{tool}', or pin the layer that provides the one \
you want directly.",
first.realm
),
_ => format!(
"One of these layers belongs to no named realm, so there is no qualified form for \
it: define its realm in varve-realms.toml and name it in the pin's `realm`, then \
choose with tools = [\"<realm>/{tool}\"]."
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn manifest(layer: &str, tools: &[&str], includes: &[(&str, &str)]) -> LayerView {
let mut entries: Vec<String> = tools
.iter()
.map(|t| {
format!(
r#"{{"digest":"sha256:{t}","annotations":{{"eu.pulseengine.tool":"{t}"}}}}"#
)
})
.collect();
for (digest, realm) in includes {
entries.push(format!(
r#"{{"digest":"{digest}","annotations":{{"eu.pulseengine.varve.kind":"layer","{ANN_INCLUDE_REALM}":"{realm}"}}}}"#
));
}
let json = format!(
r#"{{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json",
"artifactType":"application/vnd.pulseengine.varve.layer.v1+json",
"annotations":{{"eu.pulseengine.varve.layer":"{layer}","eu.pulseengine.varve.channel":"qualified",
"eu.pulseengine.varve.counter":"1","org.opencontainers.image.created":"2026-08-01T00:00:00Z"}},
"manifests":[{}]}}"#,
entries.join(",")
);
let _ = layer;
view(json.as_bytes()).unwrap()
}
fn providers(walked: &[Walked]) -> Vec<ToolProvider> {
walked
.iter()
.flat_map(|w| {
w.view.tools.iter().map(|t| ToolProvider {
tool: t.clone(),
realm: w.realm.clone(),
layer: "2026.08.0".into(),
digest: w.digest.clone(),
})
})
.collect()
}
fn unchosen(walked: &[Walked]) -> Result<BTreeMap<String, ToolProvider>, ComposeError> {
select_tools(&providers(walked), &BTreeMap::new())
}
#[test]
fn a_composition_exposes_both_layers_tools() {
let upstream = manifest("2026.08.0", &["wasm-tools", "cargo-component"], &[]);
let root = manifest(
"2026.08.0",
&["rivet", "meld"],
&[("sha256:up", "bytecodealliance")],
);
let inc = includes(&root);
assert_eq!(inc.len(), 1);
assert_eq!(inc[0].digest, "sha256:up");
assert_eq!(inc[0].realm.as_deref(), Some("bytecodealliance"));
let layers = walk("sha256:root", "pulseengine", &root, |d| {
(d == "sha256:up").then(|| upstream.clone())
})
.unwrap();
assert_eq!(layers.len(), 2, "root plus the included layer");
assert_eq!(layers[0].realm, "pulseengine");
assert_eq!(layers[1].realm, "bytecodealliance");
let tools = unchosen(&layers).unwrap();
for t in ["rivet", "meld", "wasm-tools", "cargo-component"] {
assert!(tools.contains_key(t), "{t} missing from the composition");
}
assert_eq!(tools["wasm-tools"].digest, "sha256:up");
assert_eq!(tools["rivet"].digest, "sha256:root");
}
#[test]
fn a_tool_in_two_layers_is_an_error_not_a_silent_choice() {
let upstream = manifest("2026.08.0", &["wasm-tools"], &[]);
let root = manifest(
"2026.08.0",
&["wasm-tools"],
&[("sha256:up", "bytecodealliance")],
);
let layers = walk("sha256:root", "pulseengine", &root, |d| {
(d == "sha256:up").then(|| upstream.clone())
})
.unwrap();
match unchosen(&layers) {
Err(ComposeError::AmbiguousTool { tool, .. }) => assert_eq!(tool, "wasm-tools"),
other => panic!("expected AmbiguousTool, got {other:?}"),
}
}
#[test]
fn a_realm_qualifier_settles_a_collision_the_tools_filter_never_could() {
let upstream = manifest("2026.08.0", &["wasm-tools"], &[]);
let root = manifest(
"2026.09.0",
&["wasm-tools", "rivet"],
&[("sha256:up", "bytecodealliance")],
);
let layers = walk("sha256:root", "pulseengine", &root, |d| {
(d == "sha256:up").then(|| upstream.clone())
})
.unwrap();
let all = providers(&layers);
for (realm, digest) in [
("bytecodealliance", "sha256:up"),
("pulseengine", "sha256:root"),
] {
let chosen = BTreeMap::from([("wasm-tools".to_string(), realm.to_string())]);
let picked = select_tools(&all, &chosen).unwrap();
assert_eq!(
picked["wasm-tools"].digest, digest,
"the pin chose realm '{realm}'"
);
assert_eq!(picked["wasm-tools"].realm, realm);
assert_eq!(picked["rivet"].digest, "sha256:root");
}
}
#[test]
fn a_qualifier_naming_a_realm_that_provides_nothing_is_refused_not_ignored() {
let upstream = manifest("2026.08.0", &["wasm-tools"], &[]);
let root = manifest(
"2026.09.0",
&["wasm-tools"],
&[("sha256:up", "bytecodealliance")],
);
let layers = walk("sha256:root", "pulseengine", &root, |d| {
(d == "sha256:up").then(|| upstream.clone())
})
.unwrap();
let chosen = BTreeMap::from([("wasm-tools".to_string(), "acme".to_string())]);
let err = select_tools(&providers(&layers), &chosen).unwrap_err();
let msg = err.to_string();
assert!(
matches!(err, ComposeError::RealmProvidesNoSuchTool { .. }),
"{msg}"
);
assert!(msg.contains("acme/wasm-tools"), "{msg}");
assert!(
msg.contains("pulseengine") && msg.contains("bytecodealliance"),
"{msg}"
);
}
#[test]
fn the_refusal_names_both_realms_and_shows_a_qualified_form_that_works() {
let upstream = manifest("2026.08.0", &["wasm-tools"], &[]);
let root = manifest(
"2026.09.0",
&["wasm-tools"],
&[("sha256:up", "bytecodealliance")],
);
let layers = walk("sha256:root", "pulseengine", &root, |d| {
(d == "sha256:up").then(|| upstream.clone())
})
.unwrap();
let msg = unchosen(&layers).unwrap_err().to_string();
assert!(
msg.contains("realm 'pulseengine'") && msg.contains("realm 'bytecodealliance'"),
"both providers must be named WITH their realms: {msg}"
);
assert!(
msg.contains("tools = [\"pulseengine/wasm-tools\"]")
&& msg.contains("tools = [\"bytecodealliance/wasm-tools\"]"),
"both qualified forms must be there to copy: {msg}"
);
assert!(
!msg.contains("Restrict the pin's `tools`"),
"the advice that cannot work must be gone: {msg}"
);
}
#[test]
fn two_layers_of_one_realm_are_told_a_qualifier_cannot_help_them() {
let base = manifest("2026.08.0", &["wasm-tools"], &[]);
let root = manifest("2026.09.0", &["wasm-tools"], &[("sha256:base", "")]);
let layers = walk("sha256:root", "pulseengine", &root, |d| {
(d == "sha256:base").then(|| base.clone())
})
.unwrap();
let msg = unchosen(&layers).unwrap_err().to_string();
assert!(
msg.contains("a realm qualifier cannot separate them"),
"an unusable qualified form must not be offered: {msg}"
);
}
#[test]
fn one_layer_declaring_a_name_twice_is_not_a_collision_with_itself() {
let twice = vec![
ToolProvider {
tool: "rivet".into(),
realm: "pulseengine".into(),
layer: "2026.09.0".into(),
digest: "sha256:root".into(),
},
ToolProvider {
tool: "rivet".into(),
realm: "pulseengine".into(),
layer: "2026.09.0".into(),
digest: "sha256:root".into(),
},
];
let picked = select_tools(&twice, &BTreeMap::new()).unwrap();
assert_eq!(picked["rivet"].digest, "sha256:root");
}
#[test]
fn an_include_inherits_the_including_realm_where_it_names_none() {
let base = manifest("2026.08.0", &["base"], &[]);
let root = manifest("2026.09.0", &["rivet"], &[("sha256:base", "")]);
let layers = walk("sha256:root", "pulseengine", &root, |d| {
(d == "sha256:base").then(|| base.clone())
})
.unwrap();
assert_eq!(layers[1].realm, "pulseengine");
let picked = unchosen(&layers).unwrap();
assert_eq!(
picked["base"].qualified().as_deref(),
Some("pulseengine/base")
);
}
#[test]
fn depth_is_bounded_so_a_long_chain_cannot_exhaust_the_walker() {
let leaf = manifest("2026.08.0", &["leaf"], &[]);
let chain: Vec<LayerView> = (0..=MAX_DEPTH + 2)
.map(|i| manifest("2026.08.0", &["t"], &[(&format!("sha256:{}", i + 1), "r")]))
.collect();
let err = walk("sha256:0", "r", &chain[0], |d: &str| {
let n: usize = d.trim_start_matches("sha256:").parse().ok()?;
chain.get(n).cloned().or_else(|| Some(leaf.clone()))
})
.unwrap_err();
assert!(matches!(err, ComposeError::TooDeep), "got {err:?}");
}
#[test]
fn a_chain_exactly_at_the_bound_is_walked_not_refused() {
let chain: Vec<LayerView> = (0..MAX_DEPTH)
.map(|i| {
let tool = format!("t{i}");
if i + 1 == MAX_DEPTH {
manifest("2026.08.0", &[&tool], &[])
} else {
manifest(
"2026.08.0",
&[&tool],
&[(&format!("sha256:{}", i + 1), "r")],
)
}
})
.collect();
let walked = walk("sha256:0", "r", &chain[0], |d: &str| {
let n: usize = d.trim_start_matches("sha256:").parse().ok()?;
chain.get(n).cloned()
})
.expect("a chain exactly MAX_DEPTH long is within the bound");
assert_eq!(walked.len(), MAX_DEPTH);
}
#[test]
fn a_collision_involving_a_layer_with_no_realm_says_there_is_no_qualified_form() {
let upstream = manifest("2026.08.0", &["wasm-tools"], &[]);
let root = manifest(
"2026.09.0",
&["wasm-tools"],
&[("sha256:up", "bytecodealliance")],
);
let layers = walk("sha256:root", "", &root, |d| {
(d == "sha256:up").then(|| upstream.clone())
})
.unwrap();
let msg = unchosen(&layers).unwrap_err().to_string();
assert!(
msg.contains("belongs to no named realm") && msg.contains("varve-realms.toml"),
"the refusal must name the fix that exists, not a qualified form that does not: {msg}"
);
assert!(
!msg.contains("Both are in realm"),
"these two are NOT in one realm; one has none: {msg}"
);
}
#[test]
fn a_diamond_is_walked_once_not_refused_as_a_cycle() {
let d = manifest("2026.08.0", &["base"], &[]);
let b = manifest("2026.08.0", &["b"], &[("sha256:d", "r")]);
let c = manifest("2026.08.0", &["c"], &[("sha256:d", "r")]);
let a = manifest("2026.08.0", &["a"], &[("sha256:b", "r"), ("sha256:c", "r")]);
let walked = walk("sha256:a", "r", &a, |q| match q {
"sha256:b" => Some(b.clone()),
"sha256:c" => Some(c.clone()),
"sha256:d" => Some(d.clone()),
_ => None,
})
.unwrap();
assert_eq!(walked.len(), 4, "A, B, C and D each once: {walked:?}");
let tools = unchosen(&walked).unwrap();
assert_eq!(tools["base"].digest, "sha256:d");
}
#[test]
fn a_cycle_is_refused_not_followed() {
let a = manifest("2026.08.0", &["x"], &[("sha256:b", "r")]);
let b = manifest("2026.08.0", &["y"], &[("sha256:a", "r")]);
let (ac, bc) = (a.clone(), b.clone());
let err = walk("sha256:a", "r", &a, move |d| match d {
"sha256:b" => Some(bc.clone()),
"sha256:a" => Some(ac.clone()),
_ => None,
})
.unwrap_err();
assert!(matches!(err, ComposeError::Cycle { .. }), "got {err:?}");
}
#[test]
fn an_uninstalled_include_is_skipped_for_the_caller_to_report() {
let root = manifest("2026.08.0", &["rivet"], &[("sha256:missing", "other")]);
let layers = walk("sha256:root", "r", &root, |_| None).unwrap();
assert_eq!(layers.len(), 1, "only the root resolved");
assert_eq!(
includes(&root).len(),
1,
"but the include is still declared"
);
}
fn offered(realm: &str, name: &str, version: &str, digest: &str) -> (PayloadOrigin, ()) {
(
PayloadOrigin {
name: name.into(),
version: version.into(),
digest: format!("sha256:{digest}"),
realm: realm.into(),
layer: "2026.08.0".into(),
},
(),
)
}
#[test]
fn two_versions_of_one_crate_both_export() {
let kept = union_payloads(vec![
offered("pulseengine", "serde", "1.0.200", "aa"),
offered("bytecodealliance", "serde", "1.0.210", "bb"),
])
.unwrap();
assert_eq!(kept.len(), 2);
let mut vers: Vec<&str> = kept.iter().map(|(o, _)| o.version.as_str()).collect();
vers.sort();
assert_eq!(vers, ["1.0.200", "1.0.210"]);
}
#[test]
fn the_same_name_and_version_with_the_same_bytes_exports_once() {
let kept = union_payloads(vec![
offered("pulseengine", "cfg-if", "1.0.0", "aa"),
offered("bytecodealliance", "cfg-if", "1.0.0", "aa"),
])
.unwrap();
assert_eq!(kept.len(), 1, "one copy of agreed bytes: {kept:?}");
assert_eq!(kept[0].0.realm, "pulseengine", "the first offer wins");
}
#[test]
fn the_same_name_and_version_with_different_bytes_names_both_realms() {
let err = union_payloads(vec![
offered("pulseengine", "cfg-if", "1.0.0", "aa"),
offered("bytecodealliance", "cfg-if", "1.0.0", "bb"),
])
.unwrap_err();
let msg = err.to_string();
assert!(matches!(err, ComposeError::ConflictingPayload(_)), "{msg}");
assert!(msg.contains("cfg-if") && msg.contains("1.0.0"), "{msg}");
assert!(
msg.contains("pulseengine") && msg.contains("bytecodealliance"),
"both realms must be named: {msg}"
);
assert!(
msg.contains("sha256:aa") && msg.contains("sha256:bb"),
"both digests must be named: {msg}"
);
}
#[test]
fn a_layer_without_includes_composes_to_itself() {
let plain = manifest("2026.08.0", &["rivet", "meld"], &[]);
assert!(includes(&plain).is_empty());
let layers = walk("sha256:root", "r", &plain, |_| None).unwrap();
assert_eq!(layers.len(), 1);
assert_eq!(unchosen(&layers).unwrap().len(), 2);
}
}