greentic_component/
abi.rs1use std::fs;
2use std::path::Path;
3
4use thiserror::Error;
5use wit_parser::{Resolve, WorldId, WorldItem};
6
7use crate::lifecycle::Lifecycle;
8use crate::wasm::{self, WorldSource};
9
10const WASI_TARGET_MARKER: &str = "wasm32-wasip2";
11const DEFAULT_REQUIRED_EXPORTS: [&str; 1] = ["describe"];
12
13#[derive(Debug, Error)]
14pub enum AbiError {
15 #[error("failed to read component: {source}")]
16 Io {
17 #[from]
18 source: std::io::Error,
19 },
20 #[error("failed to decode embedded component metadata: {0}")]
21 Metadata(anyhow::Error),
22 #[error("component world mismatch (expected `{expected}`, found `{found}`)")]
23 WorldMismatch { expected: String, found: String },
24 #[error("invalid world reference `{raw}`; expected namespace:package/world[@version]")]
25 InvalidWorldReference { raw: String },
26 #[error("component does not export any callable interfaces in `{world}`")]
27 MissingExports { world: String },
28 #[error("component must target wasm32-wasip2")]
29 MissingWasiTarget,
30}
31
32pub fn check_world(wasm_path: &Path, expected: &str) -> Result<(), AbiError> {
33 let bytes = fs::read(wasm_path)?;
34 ensure_wasi_target(&bytes)?;
35
36 let decoded = wasm::decode_world(&bytes).map_err(AbiError::Metadata)?;
37 let found = format_world(&decoded.resolve, decoded.world);
38 if let WorldSource::Metadata = decoded.source {
39 let normalized_expected = normalize_world_ref(expected)?;
40 if !worlds_match(&found, &normalized_expected) {
41 return Err(AbiError::WorldMismatch {
42 expected: normalized_expected,
43 found,
44 });
45 }
46 }
47
48 ensure_required_exports(&decoded.resolve, decoded.world, &found)?;
49 Ok(())
50}
51
52pub fn has_lifecycle(wasm_path: &Path) -> Result<Lifecycle, AbiError> {
53 let bytes = fs::read(wasm_path)?;
54 let names = extract_export_names(&bytes).unwrap_or_default();
55 Ok(Lifecycle {
56 init: names.iter().any(|name| name.eq_ignore_ascii_case("init")),
57 health: names.iter().any(|name| name.eq_ignore_ascii_case("health")),
58 shutdown: names
59 .iter()
60 .any(|name| name.eq_ignore_ascii_case("shutdown")),
61 })
62}
63
64fn ensure_wasi_target(bytes: &[u8]) -> Result<(), AbiError> {
65 if bytes
66 .windows(WASI_TARGET_MARKER.len())
67 .any(|window| window == WASI_TARGET_MARKER.as_bytes())
68 {
69 Ok(())
70 } else {
71 Err(AbiError::MissingWasiTarget)
72 }
73}
74
75fn normalize_world_ref(input: &str) -> Result<String, AbiError> {
76 let raw = input.trim();
77 if !raw.contains('/') {
78 return Ok(raw.to_string());
79 }
80 let (pkg_part, version) = match raw.split_once('@') {
81 Some((pkg, ver)) if !pkg.is_empty() && !ver.is_empty() => (pkg, Some(ver)),
82 _ => (raw, None),
83 };
84
85 let (pkg, world) =
86 pkg_part
87 .rsplit_once('/')
88 .ok_or_else(|| AbiError::InvalidWorldReference {
89 raw: input.to_string(),
90 })?;
91 let (namespace, name) =
92 pkg.rsplit_once(':')
93 .ok_or_else(|| AbiError::InvalidWorldReference {
94 raw: input.to_string(),
95 })?;
96
97 let mut id = format!("{namespace}:{name}/{world}");
98 if let Some(ver) = version {
99 id.push('@');
100 id.push_str(ver);
101 }
102 Ok(id)
103}
104
105fn format_world(resolve: &Resolve, world_id: WorldId) -> String {
106 let world = &resolve.worlds[world_id];
107 if let Some(pkg_id) = world.package {
108 let pkg = &resolve.packages[pkg_id];
109 if let Some(version) = &pkg.name.version {
110 format!(
111 "{}:{}/{}@{}",
112 pkg.name.namespace, pkg.name.name, world.name, version
113 )
114 } else {
115 format!("{}:{}/{}", pkg.name.namespace, pkg.name.name, world.name)
116 }
117 } else {
118 world.name.clone()
119 }
120}
121
122fn worlds_match(found: &str, expected: &str) -> bool {
123 if found == expected {
124 return true;
125 }
126 let found_base = found.split('@').next().unwrap_or(found);
127 let expected_base = expected.split('@').next().unwrap_or(expected);
128 if found_base == expected_base {
129 return true;
130 }
131 if !expected_base.contains('/') {
132 if let Some((_, world)) = found_base.rsplit_once('/') {
133 return world == expected_base;
134 }
135 return found_base == expected_base;
136 }
137 false
138}
139
140fn ensure_required_exports(
141 resolve: &Resolve,
142 world_id: WorldId,
143 display: &str,
144) -> Result<(), AbiError> {
145 let world = &resolve.worlds[world_id];
146 let has_exports = world.exports.iter().any(|(_, item)| match item {
147 WorldItem::Function(_) => true,
148 WorldItem::Interface { id, .. } => !resolve.interfaces[*id].functions.is_empty(),
149 WorldItem::Type(_) => false,
150 });
151
152 if !has_exports {
153 return Err(AbiError::MissingExports {
154 world: display.to_string(),
155 });
156 }
157
158 let mut satisfied = DEFAULT_REQUIRED_EXPORTS
161 .iter()
162 .map(|name| (*name, false))
163 .collect::<Vec<_>>();
164
165 for (_key, item) in &world.exports {
166 match item {
167 WorldItem::Function(func) => mark_export(func.name.as_str(), &mut satisfied),
168 WorldItem::Interface { id, .. } => {
169 for (func, _) in resolve.interfaces[*id].functions.iter() {
170 mark_export(func, &mut satisfied);
171 }
172 }
173 WorldItem::Type(_) => {}
174 }
175
176 if satisfied.iter().all(|(_, hit)| *hit) {
177 break;
178 }
179 }
180
181 Ok(())
182}
183
184fn mark_export(name: &str, satisfied: &mut [(&str, bool)]) {
185 for (needle, flag) in satisfied.iter_mut() {
186 if name.eq_ignore_ascii_case(needle) {
187 *flag = true;
188 }
189 }
190}
191
192fn extract_export_names(bytes: &[u8]) -> Result<Vec<String>, AbiError> {
193 use wasmparser::{ComponentExternalKind, ExternalKind, Parser, Payload};
194
195 let mut names = Vec::new();
196 for payload in Parser::new(0).parse_all(bytes) {
197 let payload = payload.map_err(|err| AbiError::Metadata(err.into()))?;
198 match payload {
199 Payload::ComponentExportSection(section) => {
200 for export in section {
201 let export = export.map_err(|err| AbiError::Metadata(err.into()))?;
202 if let ComponentExternalKind::Func = export.kind {
203 names.push(export.name.0.to_string());
204 }
205 }
206 }
207 Payload::ExportSection(section) => {
208 for export in section {
209 let export = export.map_err(|err| AbiError::Metadata(err.into()))?;
210 if let ExternalKind::Func = export.kind {
211 names.push(export.name.to_string());
212 }
213 }
214 }
215 _ => {}
216 }
217 }
218 Ok(names)
219}