use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::path::Path;
use crate::builtins;
use crate::parse::config::Injection;
include!(concat!(env!("OUT_DIR"), "/builtin_config_constants.rs"));
use builtin_protocol::wire_format::{
serialize_table, CAPACITY, LEN_PREFIX_BYTES, MAGIC_BYTES, MAGIC_LEN,
};
pub(crate) const PROVIDER_BUILTIN_NAME: &str = "config-provider";
const MAX_PAYLOAD: usize = CAPACITY - MAGIC_LEN - LEN_PREFIX_BYTES;
pub fn validate_config_as_wave(injection: &mut Injection) -> Result<()> {
if injection.config_as_wave.is_some() {
return Ok(());
}
let Some(builtin_path) = injection.path.as_deref() else {
return Ok(());
};
let bytes = std::fs::read(builtin_path)
.with_context(|| format!("Failed to read materialized builtin '{}'", builtin_path))?;
let scan = builtin_protocol::scan_substrate_component(&bytes).map_err(|e| {
anyhow::anyhow!(
"injection '{name}': failed to scan builtin bytes: {e}",
name = injection.name,
)
})?;
if !scan.imports_substrate {
if !injection.builtin_config.is_empty() {
let mut keys: Vec<&str> = injection
.builtin_config
.keys()
.map(String::as_str)
.collect();
keys.sort();
anyhow::bail!(
"injection '{name}' set `config:` keys [{keys}], but the underlying \
component doesn't import `splicer:builtin-config/get` — splicer has \
nothing to seal the values into. Either drop the `config:` block, or \
inject a builtin that consumes the substrate.",
name = injection.name,
keys = keys.join(", "),
);
}
return Ok(());
}
let wave = match injection.builtin.as_deref() {
Some(builtin_name) => validate_against_manifest(injection, builtin_name, &scan.manifests)?,
None => loose_encode_user_config(injection)?,
};
injection.config_as_wave = Some(wave);
Ok(())
}
pub fn build_provider_for_edge(
injection: &mut Injection,
edge_id: &str,
splits_dir: &Path,
) -> Result<()> {
let Some(wave) = injection.config_as_wave.as_ref() else {
return Ok(());
};
let mut values = wave.clone();
let edge_id_wave =
builtin_protocol::loose_scalar_to_wave(&toml::Value::String(edge_id.to_string()))
.expect("string always encodes as WAVE");
values.insert(crate::wac::EDGE_ID_CONFIG_KEY.to_string(), edge_id_wave);
let provider_bytes = build_provider(&values).with_context(|| {
format!(
"Failed to build config provider for injection '{}'",
injection.name
)
})?;
let dir = splits_dir.join("builtins");
std::fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create builtins dir: {}", dir.display()))?;
let out = dir.join(format!("{}-config.wasm", injection.name));
std::fs::write(&out, &provider_bytes)
.with_context(|| format!("Failed to write config provider: {}", out.display()))?;
let out_str = out
.to_str()
.ok_or_else(|| {
anyhow::anyhow!(
"config provider path contains non-UTF-8 bytes: {}",
out.display()
)
})?
.to_string();
injection.config_provider_path = Some(out_str);
Ok(())
}
fn validate_against_manifest(
injection: &Injection,
expected: &str,
all: &[(String, builtin_protocol::Manifest)],
) -> Result<BTreeMap<String, String>> {
let manifest = match all.iter().find(|(n, _)| n == expected) {
Some((_, m)) => m.clone(),
None if all.is_empty() => {
if injection.builtin_config.is_empty() {
return Ok(BTreeMap::new());
}
anyhow::bail!(
"injection '{name}': shipped builtin '{expected}' is missing its \
embedded manifest, so splicer can't validate the `config:` keys \
you set. Rebuild against the current splicer-builtin-protocol crate.",
name = injection.name,
);
}
None => {
let names: Vec<&str> = all.iter().map(|(n, _)| n.as_str()).collect();
anyhow::bail!(
"injection '{name}': shipped builtin '{expected}' resolved to bytes \
carrying manifest(s) for [{}] — wrong OCI tag or build mismatch.",
names.join(", "),
name = injection.name,
);
}
};
let mut wave: BTreeMap<String, String> = BTreeMap::new();
let mut unknown: Vec<&str> = Vec::new();
let mut bad: Vec<String> = Vec::new();
for (key, value) in &injection.builtin_config {
match manifest.validate_value(key, value) {
Ok(Some(canonical)) => {
wave.insert(key.clone(), canonical);
}
Ok(None) => unknown.push(key.as_str()),
Err(msg) => bad.push(msg),
}
}
if unknown.is_empty() && bad.is_empty() {
return Ok(wave);
}
let known_keys: Vec<&str> = manifest.keys.iter().map(|k| k.name.as_str()).collect();
let mut msg = format!(
"injection '{name}' has invalid `config:` against its declared manifest. \
Run `splicer builtin {expected}` to see accepted keys.",
name = injection.name,
);
if !unknown.is_empty() {
unknown.sort();
msg.push_str(&format!(
"\n unknown keys: [{}]; known keys: [{}]",
unknown.join(", "),
known_keys.join(", "),
));
}
for b in &bad {
msg.push_str("\n ");
msg.push_str(b);
}
anyhow::bail!(msg);
}
fn loose_encode_user_config(injection: &Injection) -> Result<BTreeMap<String, String>> {
let mut wave = BTreeMap::new();
for (key, value) in &injection.builtin_config {
let encoded = builtin_protocol::loose_scalar_to_wave(value).map_err(|e| {
anyhow::anyhow!(
"injection '{name}': config key '{key}' (no manifest available): {e}",
name = injection.name,
)
})?;
wave.insert(key.clone(), encoded);
}
Ok(wave)
}
#[cfg(test)]
fn imports_substrate(bytes: &[u8]) -> bool {
builtin_protocol::scan_substrate_component(bytes)
.map(|s| s.imports_substrate)
.unwrap_or(false)
}
pub fn build_provider(values: &BTreeMap<String, String>) -> Result<Vec<u8>> {
let mut bytes = builtins::load_resolved_bytes(PROVIDER_BUILTIN_NAME)
.context("Failed to load config-provider template bytes")?;
patch_in_place(&mut bytes, values)?;
Ok(bytes)
}
fn patch_in_place(bytes: &mut [u8], values: &BTreeMap<String, String>) -> Result<()> {
let payload = serialize_table(values);
if payload.len() > MAX_PAYLOAD {
anyhow::bail!(
"config-provider payload of {} bytes exceeds reserved capacity of {} bytes \
(template `CAPACITY` minus magic + length header). Either trim the config or \
rebuild the provider with a larger `CAPACITY`.",
payload.len(),
MAX_PAYLOAD,
);
}
let offset = find_unique_magic(bytes)?;
let len_off = offset + MAGIC_LEN;
let payload_off = len_off + LEN_PREFIX_BYTES;
let payload_end = payload_off + payload.len();
bytes[len_off..len_off + LEN_PREFIX_BYTES]
.copy_from_slice(&(payload.len() as u32).to_le_bytes());
bytes[payload_off..payload_end].copy_from_slice(&payload);
Ok(())
}
fn find_unique_magic(bytes: &[u8]) -> Result<usize> {
let mut found: Option<usize> = None;
let mut i = 0;
while i + MAGIC_LEN <= bytes.len() {
if bytes[i..i + MAGIC_LEN] == MAGIC_BYTES {
if found.is_some() {
anyhow::bail!(
"config-provider template has multiple `MAGIC_BYTES` matches. The byte-scan \
patcher assumes exactly one. Check the template build."
);
}
found = Some(i);
i += MAGIC_LEN;
} else {
i += 1;
}
}
found.ok_or_else(|| {
anyhow::anyhow!(
"config-provider template is missing the magic sentinel. The template was \
either built against an out-of-date `MAGIC_BYTES`, or the compiler elided \
the KV buffer. Rebuild builtins/config-provider with the in-tree sources."
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::config::Injection;
use builtin_protocol::wire_format::deserialize_table;
const TEST_EDGE_ID: &str = "test:iface@0.1.0::caller->provider";
fn ensure_provider_for(injection: &mut Injection, splits_dir: &Path) -> Result<()> {
validate_config_as_wave(injection)?;
build_provider_for_edge(injection, TEST_EDGE_ID, splits_dir)
}
fn make_blob(payload: &[u8]) -> Vec<u8> {
let prefix = b"PREFIX_BYTES_BEFORE_MAGIC".to_vec();
let suffix = b"SUFFIX_BYTES_AFTER_TEMPLATE".to_vec();
let mut buf = Vec::with_capacity(prefix.len() + CAPACITY + suffix.len());
buf.extend_from_slice(&prefix);
buf.extend_from_slice(&MAGIC_BYTES);
buf.extend_from_slice(&(payload.len() as u32).to_le_bytes());
buf.extend_from_slice(payload);
let written = MAGIC_LEN + LEN_PREFIX_BYTES + payload.len();
buf.extend(std::iter::repeat_n(0xAA, CAPACITY - written));
buf.extend_from_slice(&suffix);
buf
}
fn parse_back(bytes: &[u8]) -> std::collections::HashMap<String, String> {
let off = find_unique_magic(bytes).expect("magic present");
let body = &bytes[off + MAGIC_LEN..];
let payload_len = u32::from_le_bytes(body[..LEN_PREFIX_BYTES].try_into().unwrap()) as usize;
let payload = &body[LEN_PREFIX_BYTES..LEN_PREFIX_BYTES + payload_len];
deserialize_table(payload)
}
#[test]
fn round_trip_empty() {
let mut blob = make_blob(&[]);
patch_in_place(&mut blob, &BTreeMap::new()).expect("patch");
let parsed = parse_back(&blob);
assert!(parsed.is_empty());
}
#[test]
fn round_trip_multi_key() {
let mut blob = make_blob(&[]);
let mut values = BTreeMap::new();
values.insert("buffer".to_string(), "100".to_string());
values.insert("flush_after_seconds".to_string(), "10.0".to_string());
values.insert("note".to_string(), "value with spaces and 🎉".to_string());
patch_in_place(&mut blob, &values).expect("patch");
let parsed = parse_back(&blob);
assert_eq!(parsed.len(), values.len());
for (k, v) in &values {
assert_eq!(parsed.get(k), Some(v));
}
}
#[test]
fn missing_key_after_patch_yields_nothing() {
let mut blob = make_blob(&[]);
let mut values = BTreeMap::new();
values.insert("buffer".to_string(), "100".to_string());
patch_in_place(&mut blob, &values).expect("patch");
let parsed = parse_back(&blob);
assert!(!parsed.contains_key("not-set"));
}
#[test]
fn patch_preserves_outside_window() {
let mut blob = make_blob(&[]);
let original = blob.clone();
let mut values = BTreeMap::new();
values.insert("k".to_string(), "v".to_string());
patch_in_place(&mut blob, &values).expect("patch");
let magic_off = find_unique_magic(&blob).expect("magic");
let template_end = magic_off + CAPACITY;
assert_eq!(&blob[..magic_off], &original[..magic_off]);
assert_eq!(&blob[template_end..], &original[template_end..]);
}
#[test]
fn overflow_rejects_cleanly() {
let mut blob = make_blob(&[]);
let mut values = BTreeMap::new();
values.insert("k".to_string(), "a".repeat(MAX_PAYLOAD));
let err = patch_in_place(&mut blob, &values).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("exceeds reserved capacity"), "{msg}");
}
#[test]
fn missing_magic_surfaces_clear_error() {
let bytes = vec![0u8; 1024];
let err = find_unique_magic(&bytes).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("missing the magic sentinel"), "{msg}");
}
#[test]
fn duplicate_magic_surfaces_clear_error() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&MAGIC_BYTES);
bytes.extend_from_slice(&[0u8; 64]);
bytes.extend_from_slice(&MAGIC_BYTES);
let err = find_unique_magic(&bytes).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("multiple `MAGIC_BYTES` matches"), "{msg}");
}
#[test]
#[ignore = "needs built/cached/registry-resolvable config-provider template"]
fn built_provider_has_unique_magic() {
build_provider(&BTreeMap::new())
.expect("template must resolve and have exactly one MAGIC_BYTES match");
}
#[test]
#[ignore = "needs built/cached/registry-resolvable hello-tier1 + config-provider"]
fn hello_tier1_smoke() {
let hello_bytes =
crate::builtins::load_resolved_bytes("hello-tier1").expect("hello-tier1 must resolve");
assert!(imports_substrate(&hello_bytes));
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let hello_path = builtin_dir.join("hello-tier1.wasm");
std::fs::write(&hello_path, &hello_bytes).unwrap();
let mut inj = Injection::from_path("hello-tier1", hello_path.to_str().unwrap());
inj.builtin_config.insert(
"greeting".to_string(),
toml::Value::String("wired-up-end-to-end".to_string()),
);
ensure_provider_for(&mut inj, splits.path()).expect("ensure_provider_for");
let provider = inj.config_provider_path.as_deref().expect("provider path");
let patched = std::fs::read(provider).expect("provider file");
let parsed = parse_back(&patched);
assert_eq!(
parsed.get("greeting").map(String::as_str),
Some("\"wired-up-end-to-end\"")
);
}
const CONSUMER_WAT: &str = r#"(component
(import "splicer:builtin-config/get@0.1.0" (instance
(export "get" (func (param "key" string) (result (option string))))
))
)"#;
const NON_CONSUMER_WAT: &str = r#"(component
(import "wasi:http/handler@0.3.0" (instance
(export "handle" (func (param "req" u32) (result u32)))
))
)"#;
#[test]
fn imports_substrate_detects_consumer() {
let bytes = wat::parse_str(CONSUMER_WAT).expect("wat");
assert!(imports_substrate(&bytes));
}
#[test]
fn imports_substrate_rejects_non_consumer() {
let bytes = wat::parse_str(NON_CONSUMER_WAT).expect("wat");
assert!(!imports_substrate(&bytes));
}
#[test]
fn imports_substrate_tolerates_garbage() {
assert!(!imports_substrate(b"not a wasm component"));
}
#[test]
fn ensure_provider_for_writes_patched_component() {
let mut template = Vec::new();
template.extend_from_slice(b"\0asm\x01\x00\x00\x00fake-prefix-");
template.extend_from_slice(&MAGIC_BYTES);
template.extend_from_slice(&0u32.to_le_bytes());
template.extend(std::iter::repeat_n(
0xAA,
CAPACITY - MAGIC_LEN - LEN_PREFIX_BYTES,
));
template.extend_from_slice(b"-fake-suffix");
let consumer_bytes = wat::parse_str(CONSUMER_WAT).expect("wat");
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("fake-consumer.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
std::fs::write(override_dir.path().join("config-provider.wasm"), &template).unwrap();
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_path("metrics", builtin_path.to_str().unwrap());
inj.builtin_config
.insert("buffer".into(), toml::Value::Integer(100));
inj.builtin_config
.insert("flush_after_seconds".into(), toml::Value::Float(10.0));
ensure_provider_for(&mut inj, splits.path()).expect("ensure");
let path = inj
.config_provider_path
.as_deref()
.expect("provider path stamped");
let patched = std::fs::read(path).expect("provider written");
let parsed = parse_back(&patched);
assert_eq!(parsed.get("buffer").map(String::as_str), Some("100"));
assert_eq!(
parsed.get("flush_after_seconds").map(String::as_str),
Some("10.0")
);
}
#[test]
fn build_provider_for_edge_stamps_edge_id() {
let mut template = Vec::new();
template.extend_from_slice(b"\0asm\x01\x00\x00\x00fake-prefix-");
template.extend_from_slice(&MAGIC_BYTES);
template.extend_from_slice(&0u32.to_le_bytes());
template.extend(std::iter::repeat_n(
0xAA,
CAPACITY - MAGIC_LEN - LEN_PREFIX_BYTES,
));
let consumer_bytes = wat::parse_str(CONSUMER_WAT).expect("wat");
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("recorder.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
std::fs::write(override_dir.path().join("config-provider.wasm"), &template).unwrap();
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj_a = Injection::from_path("recorder-a", builtin_path.to_str().unwrap());
validate_config_as_wave(&mut inj_a).expect("validate");
build_provider_for_edge(&mut inj_a, "ns:pkg/iface@1.0.0::A->B", splits.path())
.expect("build A");
let mut inj_b = Injection::from_path("recorder-b", builtin_path.to_str().unwrap());
validate_config_as_wave(&mut inj_b).expect("validate");
build_provider_for_edge(&mut inj_b, "ns:pkg/iface@1.0.0::C->B", splits.path())
.expect("build B");
let path_a = inj_a.config_provider_path.as_deref().expect("path a");
let path_b = inj_b.config_provider_path.as_deref().expect("path b");
assert_ne!(path_a, path_b, "per-edge providers land at distinct paths");
let parsed_a = parse_back(&std::fs::read(path_a).unwrap());
let parsed_b = parse_back(&std::fs::read(path_b).unwrap());
assert_eq!(
parsed_a
.get(crate::wac::EDGE_ID_CONFIG_KEY)
.map(String::as_str),
Some("\"ns:pkg/iface@1.0.0::A->B\""),
"WAVE-quoted edge_id baked into provider A",
);
assert_eq!(
parsed_b
.get(crate::wac::EDGE_ID_CONFIG_KEY)
.map(String::as_str),
Some("\"ns:pkg/iface@1.0.0::C->B\""),
"WAVE-quoted edge_id baked into provider B",
);
}
#[test]
fn ensure_provider_for_emits_provider_with_empty_config() {
let mut template = Vec::new();
template.extend_from_slice(b"\0asm\x01\x00\x00\x00fake-prefix-");
template.extend_from_slice(&MAGIC_BYTES);
template.extend_from_slice(&0u32.to_le_bytes());
template.extend(std::iter::repeat_n(
0xAA,
CAPACITY - MAGIC_LEN - LEN_PREFIX_BYTES,
));
let consumer_bytes = wat::parse_str(CONSUMER_WAT).expect("wat");
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("fake-consumer.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
std::fs::write(override_dir.path().join("config-provider.wasm"), &template).unwrap();
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_path("metrics", builtin_path.to_str().unwrap());
assert!(inj.builtin_config.is_empty());
ensure_provider_for(&mut inj, splits.path()).expect("ensure");
let path = inj.config_provider_path.as_deref().expect("provider path");
let patched = std::fs::read(path).expect("provider written");
let parsed = parse_back(&patched);
assert_eq!(parsed.len(), 1);
assert!(parsed.contains_key(crate::wac::EDGE_ID_CONFIG_KEY));
assert!(!parsed.contains_key("buffer"));
}
#[test]
fn ensure_provider_for_skips_non_consumer() {
let bytes = wat::parse_str(NON_CONSUMER_WAT).expect("wat");
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("hello.wasm");
std::fs::write(&builtin_path, &bytes).unwrap();
let mut inj = Injection::from_path("hello", builtin_path.to_str().unwrap());
let _guard = EnvGuard::clear("SPLICER_BUILTINS_DIR");
ensure_provider_for(&mut inj, splits.path()).expect("ensure");
assert!(inj.config_provider_path.is_none());
}
#[test]
fn ensure_provider_for_rejects_config_on_non_consumer() {
let bytes = wat::parse_str(NON_CONSUMER_WAT).expect("wat");
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("hello.wasm");
std::fs::write(&builtin_path, &bytes).unwrap();
let mut inj = Injection::from_path("hello", builtin_path.to_str().unwrap());
inj.builtin_config
.insert("buffer".to_string(), toml::Value::Integer(100));
inj.builtin_config
.insert("flush_after_seconds".to_string(), toml::Value::Float(10.0));
let _guard = EnvGuard::clear("SPLICER_BUILTINS_DIR");
let err = ensure_provider_for(&mut inj, splits.path()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("'hello'"), "{msg}");
assert!(msg.contains("buffer, flush_after_seconds"), "{msg}");
assert!(msg.contains("doesn't import"), "{msg}");
}
fn consumer_bytes_with_manifest(builtin_name: &str, manifest_toml: &str) -> Vec<u8> {
let escaped = escape_wat_string(manifest_toml);
let wat = format!(
r#"(component
(import "splicer:builtin-config/get@0.1.0" (instance
(export "get" (func (param "key" string) (result (option string))))
))
(@custom "{section}" "{escaped}")
)"#,
section = builtin_protocol::section_name_for(builtin_name),
);
wat::parse_str(&wat).expect("wat with embedded manifest section")
}
fn escape_wat_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() * 4);
for b in s.as_bytes() {
out.push_str(&format!("\\{b:02x}"));
}
out
}
fn write_provider_template_to(dir: &Path) {
let mut template = Vec::new();
template.extend_from_slice(b"\0asm\x01\x00\x00\x00fake-prefix-");
template.extend_from_slice(&MAGIC_BYTES);
template.extend_from_slice(&0u32.to_le_bytes());
template.extend(std::iter::repeat_n(
0xAA,
CAPACITY - MAGIC_LEN - LEN_PREFIX_BYTES,
));
std::fs::write(dir.join("config-provider.wasm"), &template).unwrap();
}
const SAMPLE_MANIFEST_TOML: &str = r#"
[builtin]
description = "test"
tier = 1
[[key]]
name = "buffer"
type = "u32"
default = 1
doc = "doc"
[[key]]
name = "greeting"
type = "string"
default = "hi"
doc = "doc"
"#;
#[test]
fn manifest_validation_rejects_unknown_key() {
let consumer_bytes = consumer_bytes_with_manifest("metrics", SAMPLE_MANIFEST_TOML);
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("metrics.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
write_provider_template_to(override_dir.path());
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_builtin("metrics");
inj.path = Some(builtin_path.to_str().unwrap().to_string());
inj.builtin_config
.insert("bufer".into(), toml::Value::Integer(100));
let err = ensure_provider_for(&mut inj, splits.path()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("unknown keys"), "{msg}");
assert!(msg.contains("bufer"), "{msg}");
assert!(msg.contains("buffer"), "{msg}");
}
#[test]
fn manifest_validation_rejects_type_mismatch() {
let consumer_bytes = consumer_bytes_with_manifest("metrics", SAMPLE_MANIFEST_TOML);
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("metrics.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
write_provider_template_to(override_dir.path());
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_builtin("metrics");
inj.path = Some(builtin_path.to_str().unwrap().to_string());
inj.builtin_config
.insert("buffer".into(), toml::Value::String("ten".into()));
let err = ensure_provider_for(&mut inj, splits.path()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("type mismatch"), "{msg}");
assert!(msg.contains("u32"), "{msg}");
}
#[test]
fn manifest_validation_accepts_valid_keys() {
let consumer_bytes = consumer_bytes_with_manifest("metrics", SAMPLE_MANIFEST_TOML);
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("metrics.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
write_provider_template_to(override_dir.path());
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_builtin("metrics");
inj.path = Some(builtin_path.to_str().unwrap().to_string());
inj.builtin_config
.insert("buffer".into(), toml::Value::Integer(100));
inj.builtin_config
.insert("greeting".into(), toml::Value::String("hi there".into()));
ensure_provider_for(&mut inj, splits.path()).expect("valid config passes");
assert!(inj.config_provider_path.is_some());
}
#[test]
fn manifest_validation_requires_manifest_for_shipped_builtin_with_config() {
let consumer_bytes = wat::parse_str(CONSUMER_WAT).expect("wat");
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("legacy.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
write_provider_template_to(override_dir.path());
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_builtin("legacy");
inj.path = Some(builtin_path.to_str().unwrap().to_string());
inj.builtin_config
.insert("anything".into(), toml::Value::Integer(1));
let err = ensure_provider_for(&mut inj, splits.path()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("missing its embedded manifest"), "{msg}");
}
#[test]
fn manifest_validation_rejects_mismatched_builtin_name() {
let consumer_bytes = consumer_bytes_with_manifest("hello-tier1", SAMPLE_MANIFEST_TOML);
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("metrics.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
write_provider_template_to(override_dir.path());
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_builtin("metrics");
inj.path = Some(builtin_path.to_str().unwrap().to_string());
inj.builtin_config
.insert("buffer".into(), toml::Value::Integer(100));
let err = ensure_provider_for(&mut inj, splits.path()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("wrong OCI tag or build mismatch"), "{msg}");
assert!(msg.contains("hello-tier1"), "{msg}");
}
#[test]
fn manifest_validation_rejects_mismatched_builtin_name_even_without_config() {
let consumer_bytes = consumer_bytes_with_manifest("hello-tier1", SAMPLE_MANIFEST_TOML);
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("metrics.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
write_provider_template_to(override_dir.path());
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_builtin("metrics");
inj.path = Some(builtin_path.to_str().unwrap().to_string());
assert!(inj.builtin_config.is_empty());
let err = ensure_provider_for(&mut inj, splits.path()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("wrong OCI tag or build mismatch"), "{msg}");
}
#[test]
fn manifest_validation_lenient_for_user_middleware() {
let consumer_bytes = wat::parse_str(CONSUMER_WAT).expect("wat");
let splits = tempfile::tempdir().unwrap();
let builtin_dir = splits.path().join("builtins");
std::fs::create_dir_all(&builtin_dir).unwrap();
let builtin_path = builtin_dir.join("user-mw.wasm");
std::fs::write(&builtin_path, &consumer_bytes).unwrap();
let override_dir = tempfile::tempdir().unwrap();
write_provider_template_to(override_dir.path());
let _guard = EnvGuard::set("SPLICER_BUILTINS_DIR", override_dir.path());
let mut inj = Injection::from_path("user-mw", builtin_path.to_str().unwrap());
inj.builtin_config
.insert("anything".into(), toml::Value::Integer(1));
ensure_provider_for(&mut inj, splits.path()).expect("user mw without manifest passes");
assert!(inj.config_provider_path.is_some());
}
struct EnvGuard {
key: &'static str,
prev: Option<std::ffi::OsString>,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl EnvGuard {
fn lock() -> std::sync::MutexGuard<'static, ()> {
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
fn set(key: &'static str, val: &std::path::Path) -> Self {
let lock = Self::lock();
let prev = std::env::var_os(key);
unsafe { std::env::set_var(key, val) };
Self {
key,
prev,
_lock: lock,
}
}
fn clear(key: &'static str) -> Self {
let lock = Self::lock();
let prev = std::env::var_os(key);
unsafe { std::env::remove_var(key) };
Self {
key,
prev,
_lock: lock,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match self.prev.take() {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
}
}