use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use cviz::parse::component::parse_component;
use wac_graph::EncodeOptions;
use wac_parser::Document;
use wac_resolver::{packages, FileSystemPackageResolver};
use crate::builtins;
use crate::compose::{build_graph_from_components, filename_from_path};
use crate::contract::ContractResult;
use crate::parse::config::{parse_yaml, SpliceRule};
use crate::split::split_out_composition;
use crate::wac::{generate_wac, GeneratedAdapter};
#[derive(Debug, Clone)]
pub struct SpliceRequest {
pub composition_wasm: PathBuf,
pub rules_yaml: String,
pub package_name: String,
pub splits_dir: PathBuf,
pub skip_type_check: bool,
}
#[derive(Debug, Clone)]
pub struct ComponentInput {
pub alias: Option<String>,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct ComposeRequest {
pub components: Vec<ComponentInput>,
pub package_name: String,
}
#[derive(Debug, Clone)]
pub struct Bundle {
pub wac: String,
pub wac_deps: BTreeMap<String, PathBuf>,
pub diagnostics: Vec<ContractResult>,
pub generated_adapters: Vec<GeneratedAdapter>,
pub any_rule_matched: bool,
}
impl Bundle {
pub fn wac_compose_cmd(&self, wac_path: &str) -> String {
format_wac_compose_cmd(wac_path, &self.wac_deps)
}
pub fn to_wasm(&self) -> Result<Vec<u8>> {
compose_wac(&self.wac, &self.wac_deps)
}
}
pub fn splice(req: SpliceRequest) -> Result<Bundle> {
let SpliceRequest {
composition_wasm,
rules_yaml,
package_name,
splits_dir,
skip_type_check,
} = req;
let mut cfg = parse_yaml(&rules_yaml).context("Failed to parse splice rules YAML")?;
let bytes = std::fs::read(&composition_wasm).with_context(|| {
format!(
"Failed to read composition wasm: {}",
composition_wasm.display()
)
})?;
let graph = parse_component(&bytes).with_context(|| {
format!(
"Failed to parse composition graph from: {}",
composition_wasm.display()
)
})?;
let splits_dir_str = splits_dir
.to_str()
.ok_or_else(|| {
anyhow::anyhow!(
"splits_dir contains non-UTF-8 bytes: {}",
splits_dir.display()
)
})?
.to_string();
let (splits_path, shim_comps) =
split_out_composition(&composition_wasm, &Some(splits_dir_str))?;
materialize_tier1_2_builtins(&mut cfg, std::path::Path::new(&splits_path))?;
let out = generate_wac(shim_comps, &splits_path, &graph, &cfg, None, &package_name)?;
if !skip_type_check {
for diag in &out.diagnostics {
if let ContractResult::Error(msg) = diag {
anyhow::bail!("Contract type-check error: {msg}");
}
}
}
let mut wac_deps = out.wac_deps;
canonicalize_wac_deps(&mut wac_deps)?;
Ok(Bundle {
wac: out.wac,
wac_deps,
diagnostics: out.diagnostics,
generated_adapters: out.generated_adapters,
any_rule_matched: out.any_rule_matched,
})
}
pub fn compose(req: ComposeRequest) -> Result<Bundle> {
let ComposeRequest {
components,
package_name,
} = req;
let mut resolved: Vec<(String, PathBuf, Vec<u8>)> = Vec::with_capacity(components.len());
for ComponentInput { alias, path } in &components {
let name = alias.clone().unwrap_or_else(|| filename_from_path(path));
let bytes = std::fs::read(path)
.with_context(|| format!("Failed to read Wasm component: {}", path.display()))?;
resolved.push((name, path.clone(), bytes));
}
{
let mut seen: HashMap<&str, &PathBuf> = HashMap::new();
for (name, path, _) in &resolved {
if let Some(prev) = seen.insert(name.as_str(), path) {
anyhow::bail!(
"Name conflict: '{}' and '{}' both resolve to the name '{}'.\n\
Use aliases to disambiguate, e.g.:\n\
\t{}0={} {}1={}",
prev.display(),
path.display(),
name,
name,
prev.display(),
name,
path.display(),
);
}
}
}
let (graph, node_paths) = build_graph_from_components(&resolved)?;
let out = generate_wac(
HashMap::new(),
"",
&graph,
&[],
Some(&node_paths),
&package_name,
)?;
let mut wac_deps = out.wac_deps;
canonicalize_wac_deps(&mut wac_deps)?;
Ok(Bundle {
wac: out.wac,
wac_deps,
diagnostics: out.diagnostics,
generated_adapters: out.generated_adapters,
any_rule_matched: out.any_rule_matched,
})
}
pub fn compose_wac(wac: &str, wac_deps: &BTreeMap<String, PathBuf>) -> Result<Vec<u8>> {
let doc = Document::parse(wac).context("Failed to parse generated WAC source")?;
let keys = packages(&doc).context("Failed to discover packages from WAC")?;
let overrides: HashMap<String, PathBuf> = wac_deps.clone().into_iter().collect();
let resolver = FileSystemPackageResolver::new(Path::new("."), overrides, true);
let pkgs = resolver
.resolve(&keys)
.context("Failed to resolve WAC packages")?;
let resolution = doc
.resolve(pkgs)
.context("Failed to resolve WAC document")?;
let composed: Vec<u8> = resolution
.encode(EncodeOptions::default())
.context("Failed to encode composed component")?;
let mut validator = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
validator
.validate_all(&composed)
.context("Composed component bytes failed wasmparser validation")?;
Ok(composed)
}
fn materialize_tier1_2_builtins(
rules: &mut [SpliceRule],
splits_dir: &std::path::Path,
) -> Result<()> {
for rule in rules.iter_mut() {
for inj in rule.inject_mut().iter_mut() {
let Some(builtin) = inj.builtin.as_deref() else {
continue;
};
if crate::strategies::is_embedded_builtin(builtin) {
continue;
}
let path = builtins::materialize_into(splits_dir, builtin)
.with_context(|| format!("Failed to materialize builtin '{builtin}'"))?;
let path_str = path
.to_str()
.ok_or_else(|| {
anyhow::anyhow!(
"materialized builtin path contains non-UTF-8 bytes: {}",
path.display()
)
})?
.to_string();
inj.path = Some(path_str);
crate::config_provider::validate_config_as_wave(inj)?;
}
}
Ok(())
}
fn canonicalize_wac_deps(deps: &mut BTreeMap<String, PathBuf>) -> Result<()> {
for (key, path) in deps.iter_mut() {
let canonical = std::fs::canonicalize(&*path).with_context(|| {
format!(
"Failed to canonicalize wac_deps path for '{key}': {}",
path.display()
)
})?;
*path = canonical;
}
Ok(())
}
pub fn format_wac_compose_cmd(wac_path: &str, deps: &BTreeMap<String, PathBuf>) -> String {
let mut cmd = format!("wac compose {wac_path} ");
for (pkg_key, pkg_path) in deps {
cmd.push_str(&format!(
"\\\n --dep {pkg_key}=\"{}\" ",
pkg_path.display()
));
}
cmd
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builtins::with_fake_builtins;
use crate::parse::config::parse_yaml;
use std::path::Path;
#[test]
fn builtin_yaml_roundtrips_through_materialize() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:logging/log@0.1.0
inject:
- builtin: hello-tier1
"#;
with_fake_builtins(&["hello-tier1"], || {
let mut rules = parse_yaml(yaml).expect("parse");
let tmp = tempfile::tempdir().unwrap();
materialize_tier1_2_builtins(&mut rules, tmp.path()).expect("materialize");
let inj = &rules[0].inject()[0];
assert_eq!(inj.builtin.as_deref(), Some("hello-tier1"));
let path = inj.path.as_deref().expect("path stamped");
let bytes = std::fs::read(path).expect("file written");
assert!(bytes.starts_with(b"\0asm"), "materialized bytes are wasm");
assert!(
Path::new(path).ends_with("builtins/hello-tier1.wasm"),
"path lives under splits_dir/builtins/: {path}"
);
});
}
#[test]
fn user_form_injection_left_alone() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:logging/log@0.1.0
inject:
- name: tracing
path: /opt/middleware/tracing.wasm
"#;
let mut rules = parse_yaml(yaml).expect("parse");
let tmp = tempfile::tempdir().unwrap();
materialize_tier1_2_builtins(&mut rules, tmp.path()).expect("materialize");
let inj = &rules[0].inject()[0];
assert!(inj.builtin.is_none());
assert_eq!(inj.path.as_deref(), Some("/opt/middleware/tracing.wasm"));
assert!(!tmp.path().join("builtins").exists());
}
#[test]
fn mixed_user_and_builtin_injections() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:logging/log@0.1.0
inject:
- name: tracing
path: ./tracing.wasm
- builtin: hello-tier1
"#;
with_fake_builtins(&["hello-tier1"], || {
let mut rules = parse_yaml(yaml).expect("parse");
let tmp = tempfile::tempdir().unwrap();
materialize_tier1_2_builtins(&mut rules, tmp.path()).expect("materialize");
let inject = &rules[0].inject();
assert_eq!(inject[0].path.as_deref(), Some("./tracing.wasm"));
let materialized = inject[1].path.as_deref().unwrap();
assert!(Path::new(materialized).ends_with("builtins/hello-tier1.wasm"));
});
}
#[test]
fn builtin_long_form_alias_used_as_wac_var() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:logging/log@0.1.0
inject:
- builtin:
name: hello-tier1
alias: greeter
"#;
with_fake_builtins(&["hello-tier1"], || {
let mut rules = parse_yaml(yaml).expect("parse");
let tmp = tempfile::tempdir().unwrap();
materialize_tier1_2_builtins(&mut rules, tmp.path()).expect("materialize");
let inj = &rules[0].inject()[0];
assert_eq!(inj.name, "greeter");
assert_eq!(inj.builtin.as_deref(), Some("hello-tier1"));
let materialized = inj.path.as_deref().unwrap();
assert!(Path::new(materialized).ends_with("builtins/hello-tier1.wasm"));
});
}
#[test]
fn canonicalize_wac_deps_makes_paths_absolute() {
let tmp = tempfile::tempdir().unwrap();
let abs = tmp.path().join("a.wasm");
std::fs::write(&abs, b"\0asm\x0d\0\0\0").unwrap();
let prev_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let mut deps: BTreeMap<String, PathBuf> = BTreeMap::new();
deps.insert("my:relative".into(), PathBuf::from("a.wasm"));
deps.insert("my:absolute".into(), abs.clone());
canonicalize_wac_deps(&mut deps).unwrap();
std::env::set_current_dir(prev_cwd).unwrap();
for (key, p) in &deps {
assert!(p.is_absolute(), "{key} -> {} is not absolute", p.display());
}
}
#[test]
fn canonicalize_wac_deps_errors_on_missing_path() {
let mut deps: BTreeMap<String, PathBuf> = BTreeMap::new();
deps.insert(
"my:ghost".into(),
PathBuf::from("/definitely/does/not/exist/ghost.wasm"),
);
let err = canonicalize_wac_deps(&mut deps).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("my:ghost"), "error names key: {msg}");
assert!(msg.contains("ghost.wasm"), "error names path: {msg}");
}
#[test]
fn unknown_builtin_errors_with_available() {
use crate::parse::config::{Injection, SpliceRule};
let mut rules = vec![SpliceRule::Before {
interface: "wasi:logging/log@0.1.0".into(),
provider_name: None,
provider_alias: None,
inject: vec![Injection {
name: "ghost".into(),
path: None,
builtin: Some("does-not-exist".into()),
builtin_config: Default::default(),
config_as_wave: None,
config_provider_path: None,
adapter_info: None,
tier: None,
}],
}];
let tmp = tempfile::tempdir().unwrap();
let err = materialize_tier1_2_builtins(&mut rules, tmp.path()).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("does-not-exist"), "error names builtin: {msg}");
assert!(msg.contains("hello-tier1"), "error lists available: {msg}");
}
}