greentic_component/
abi.rs1use std::fs;
2use std::path::Path;
3
4use thiserror::Error;
5use wit_parser::{Resolve, WorldId, WorldItem};
6
7use wasmparser::Parser;
8
9use crate::lifecycle::Lifecycle;
10use crate::wasm::{self, WorldSource};
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, found) = decode_world(&bytes)?;
37 if let WorldSource::Metadata = decoded.source {
38 let normalized_expected = normalize_world_ref(expected)?;
39 if !worlds_match(&found, &normalized_expected) {
40 return Err(AbiError::WorldMismatch {
41 expected: normalized_expected,
42 found,
43 });
44 }
45 }
46
47 ensure_required_exports(&decoded.resolve, decoded.world, &found)?;
48 Ok(())
49}
50
51pub fn check_world_base(wasm_path: &Path, expected: &str) -> Result<String, AbiError> {
52 let bytes = fs::read(wasm_path)?;
53 ensure_wasi_target(&bytes)?;
54
55 let (decoded, found) = decode_world(&bytes)?;
56 let normalized_expected = normalize_world_ref(expected)?;
57 if !worlds_match(&found, &normalized_expected) {
58 return Err(AbiError::WorldMismatch {
59 expected: normalized_expected,
60 found,
61 });
62 }
63 ensure_required_exports(&decoded.resolve, decoded.world, &found)?;
64 Ok(found)
65}
66
67pub fn has_lifecycle(wasm_path: &Path) -> Result<Lifecycle, AbiError> {
68 let bytes = fs::read(wasm_path)?;
69 let names = extract_export_names(&bytes).unwrap_or_default();
70 Ok(Lifecycle {
71 init: names.iter().any(|name| name.eq_ignore_ascii_case("init")),
72 health: names.iter().any(|name| name.eq_ignore_ascii_case("health")),
73 shutdown: names
74 .iter()
75 .any(|name| name.eq_ignore_ascii_case("shutdown")),
76 })
77}
78
79fn ensure_wasi_target(bytes: &[u8]) -> Result<(), AbiError> {
80 if Parser::is_component(bytes) || Parser::is_core_wasm(bytes) {
83 Ok(())
84 } else {
85 Err(AbiError::MissingWasiTarget)
86 }
87}
88
89fn decode_world(bytes: &[u8]) -> Result<(wasm::DecodedWorld, String), AbiError> {
90 let decoded = wasm::decode_world(bytes).map_err(AbiError::Metadata)?;
91 let found = format_world(&decoded.resolve, decoded.world);
92 Ok((decoded, found))
93}
94
95fn normalize_world_ref(input: &str) -> Result<String, AbiError> {
96 let raw = input.trim();
97 if !raw.contains('/') {
98 return Ok(raw.to_string());
99 }
100 let (pkg_part, version) = match raw.split_once('@') {
101 Some((pkg, ver)) if !pkg.is_empty() && !ver.is_empty() => (pkg, Some(ver)),
102 _ => (raw, None),
103 };
104
105 let (pkg, world) =
106 pkg_part
107 .rsplit_once('/')
108 .ok_or_else(|| AbiError::InvalidWorldReference {
109 raw: input.to_string(),
110 })?;
111 let (namespace, name) =
112 pkg.rsplit_once(':')
113 .ok_or_else(|| AbiError::InvalidWorldReference {
114 raw: input.to_string(),
115 })?;
116
117 let mut id = format!("{namespace}:{name}/{world}");
118 if let Some(ver) = version {
119 id.push('@');
120 id.push_str(ver);
121 }
122 Ok(id)
123}
124
125fn format_world(resolve: &Resolve, world_id: WorldId) -> String {
126 let world = &resolve.worlds[world_id];
127 if let Some(pkg_id) = world.package {
128 let pkg = &resolve.packages[pkg_id];
129 if let Some(version) = &pkg.name.version {
130 format!(
131 "{}:{}/{}@{}",
132 pkg.name.namespace, pkg.name.name, world.name, version
133 )
134 } else {
135 format!("{}:{}/{}", pkg.name.namespace, pkg.name.name, world.name)
136 }
137 } else {
138 world.name.clone()
139 }
140}
141
142fn worlds_match(found: &str, expected: &str) -> bool {
143 if found == expected {
144 return true;
145 }
146 let found_base = found.split('@').next().unwrap_or(found);
147 let expected_base = expected.split('@').next().unwrap_or(expected);
148 if found_base == expected_base {
149 return true;
150 }
151 if !expected_base.contains('/') {
152 if let Some((_, world)) = found_base.rsplit_once('/') {
153 return world == expected_base;
154 }
155 return found_base == expected_base;
156 }
157 false
158}
159
160fn ensure_required_exports(
161 resolve: &Resolve,
162 world_id: WorldId,
163 display: &str,
164) -> Result<(), AbiError> {
165 let world = &resolve.worlds[world_id];
166 let has_exports = world.exports.iter().any(|(_, item)| match item {
167 WorldItem::Function(_) => true,
168 WorldItem::Interface { id, .. } => !resolve.interfaces[*id].functions.is_empty(),
169 WorldItem::Type { .. } => false,
170 });
171
172 if !has_exports {
173 return Err(AbiError::MissingExports {
174 world: display.to_string(),
175 });
176 }
177
178 let mut satisfied = DEFAULT_REQUIRED_EXPORTS
181 .iter()
182 .map(|name| (*name, false))
183 .collect::<Vec<_>>();
184
185 for (_key, item) in &world.exports {
186 match item {
187 WorldItem::Function(func) => mark_export(func.name.as_str(), &mut satisfied),
188 WorldItem::Interface { id, .. } => {
189 for (func, _) in resolve.interfaces[*id].functions.iter() {
190 mark_export(func, &mut satisfied);
191 }
192 }
193 WorldItem::Type { .. } => {}
194 }
195
196 if satisfied.iter().all(|(_, hit)| *hit) {
197 break;
198 }
199 }
200
201 Ok(())
202}
203
204fn mark_export(name: &str, satisfied: &mut [(&str, bool)]) {
205 for (needle, flag) in satisfied.iter_mut() {
206 if name.eq_ignore_ascii_case(needle) {
207 *flag = true;
208 }
209 }
210}
211
212fn extract_export_names(bytes: &[u8]) -> Result<Vec<String>, AbiError> {
213 use wasmparser::{ComponentExternalKind, ExternalKind, Parser, Payload};
214
215 let mut names = Vec::new();
216 for payload in Parser::new(0).parse_all(bytes) {
217 let payload = payload.map_err(|err| AbiError::Metadata(err.into()))?;
218 match payload {
219 Payload::ComponentExportSection(section) => {
220 for export in section {
221 let export = export.map_err(|err| AbiError::Metadata(err.into()))?;
222 if let ComponentExternalKind::Func = export.kind {
223 names.push(export.name.0.to_string());
224 }
225 }
226 }
227 Payload::ExportSection(section) => {
228 for export in section {
229 let export = export.map_err(|err| AbiError::Metadata(err.into()))?;
230 if let ExternalKind::Func = export.kind {
231 names.push(export.name.to_string());
232 }
233 }
234 }
235 _ => {}
236 }
237 }
238 Ok(names)
239}