greentic_component/cmd/info/
reader.rs1use std::path::Path;
2
3use anyhow::{Context, Result, anyhow};
4
5use super::report::{Capabilities, InfoReport, ManifestSource};
6use crate::capabilities::Capabilities as ManifestCapabilities;
7
8pub fn read(path: &Path) -> Result<InfoReport> {
14 let bytes = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
15 let size_bytes = bytes.len() as u64;
16
17 let engine = wasmtime::Engine::default();
18 let component =
21 wasmtime::component::Component::from_binary(&engine, &bytes).map_err(|err| {
22 anyhow!(
23 "{}: not a valid wasm32-wasip2 component: {err}",
24 path.display()
25 )
26 })?;
27 let ct = component.component_type();
28 let exports: Vec<String> = ct.exports(&engine).map(|(n, _)| n.to_string()).collect();
29 let imports: Vec<String> = ct.imports(&engine).map(|(n, _)| n.to_string()).collect();
30
31 let (manifest_source, id, name, version, description, capabilities, world_from_manifest) =
32 load_manifest(path, &bytes);
33
34 let wit_world = world_from_manifest.or_else(|| pick_wit_world(&exports));
37
38 Ok(InfoReport {
39 info_schema_version: 1,
40 component_id: id,
41 name,
42 version,
43 description,
44 artifact_type: "component/wasm".into(),
45 size_bytes,
46 wit_world,
47 exports,
48 imports,
49 capabilities,
50 manifest_source,
51 })
52}
53
54type ManifestFields = (
55 ManifestSource,
56 Option<String>, Option<String>, Option<String>, Option<String>, Option<Capabilities>,
61 Option<String>, );
63
64fn load_manifest(path: &Path, bytes: &[u8]) -> ManifestFields {
65 if let Ok(Some(verified)) =
67 crate::embedded_descriptor::read_and_verify_embedded_component_manifest_section_v1(bytes)
68 {
69 let m = &verified.manifest;
70 return (
71 ManifestSource::Embedded,
72 Some(m.id.clone()),
73 Some(m.name.clone()),
74 Some(m.version.clone()),
75 None,
77 Some(extract_capabilities(&m.capabilities)),
78 Some(m.world.clone()),
79 );
80 }
81
82 let stem = path
85 .file_stem()
86 .and_then(|s| s.to_str())
87 .unwrap_or("component");
88 let candidates = [
89 path.with_file_name(format!("{stem}.manifest.json")),
90 path.with_file_name("component.manifest.json"),
91 ];
92
93 for candidate in &candidates {
94 if !candidate.exists() {
95 continue;
96 }
97 let Ok(raw) = std::fs::read_to_string(candidate) else {
98 continue;
99 };
100 let Ok(m) = crate::manifest::parse_manifest(&raw) else {
101 continue;
102 };
103 return (
104 ManifestSource::Sibling,
105 Some(m.id.as_str().to_string()),
106 Some(m.name.clone()),
107 Some(m.version.to_string()),
108 None,
110 Some(extract_capabilities(&m.capabilities)),
111 Some(m.world.as_str().to_string()),
112 );
113 }
114
115 (ManifestSource::None, None, None, None, None, None, None)
116}
117
118fn extract_capabilities(caps: &ManifestCapabilities) -> Capabilities {
122 let mut host: Vec<String> = Vec::new();
123 let mut wasi: Vec<String> = Vec::new();
124
125 if let Some(fs) = &caps.wasi.filesystem {
127 use crate::capabilities::FilesystemMode::{None as FsNone, ReadOnly, Sandbox};
128 match fs.mode {
129 FsNone => {}
130 ReadOnly => wasi.push("filesystem.read_only".into()),
131 Sandbox => wasi.push("filesystem.sandbox".into()),
132 }
133 }
134 if let Some(env) = &caps.wasi.env
135 && !env.allow.is_empty()
136 {
137 wasi.push("env".into());
138 }
139 if caps.wasi.random {
140 wasi.push("random".into());
141 }
142 if caps.wasi.clocks {
143 wasi.push("clocks".into());
144 }
145
146 if let Some(messaging) = &caps.host.messaging {
148 if messaging.inbound {
149 host.push("messaging.inbound".into());
150 }
151 if messaging.outbound {
152 host.push("messaging.outbound".into());
153 }
154 }
155 if let Some(events) = &caps.host.events {
156 if events.inbound {
157 host.push("events.inbound".into());
158 }
159 if events.outbound {
160 host.push("events.outbound".into());
161 }
162 }
163 if let Some(http) = &caps.host.http {
164 if http.client {
165 host.push("http.client".into());
166 }
167 if http.server {
168 host.push("http.server".into());
169 }
170 }
171 if let Some(state) = &caps.host.state {
172 if state.read {
173 host.push("state.read".into());
174 }
175 if state.write {
176 host.push("state.write".into());
177 }
178 }
179 if let Some(secrets) = &caps.host.secrets
180 && !secrets.required.is_empty()
181 {
182 host.push(format!("secrets.required[{}]", secrets.required.len()));
183 }
184 if let Some(telemetry) = &caps.host.telemetry {
185 use crate::capabilities::TelemetryScope::*;
186 let scope = match telemetry.scope {
187 Tenant => "tenant",
188 Pack => "pack",
189 Node => "node",
190 };
191 host.push(format!("telemetry.{scope}"));
192 }
193 if let Some(iac) = &caps.host.iac {
194 if iac.write_templates {
195 host.push("iac.write_templates".into());
196 }
197 if iac.execute_plans {
198 host.push("iac.execute_plans".into());
199 }
200 }
201
202 Capabilities { host, wasi }
203}
204
205fn pick_wit_world(exports: &[String]) -> Option<String> {
211 exports
212 .iter()
213 .find(|e| e.contains("/world@") || e.contains("/descriptor@"))
214 .cloned()
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use std::path::PathBuf;
221
222 fn fixture_wasm() -> PathBuf {
223 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
224 .join("tests/contract/fixtures/component_v0_6_0/component.wasm")
225 }
226
227 #[test]
228 fn reads_size_and_exports_from_fixture() {
229 let path = fixture_wasm();
230 if !path.exists() {
231 eprintln!("skipping: no fixture at {}", path.display());
232 return;
233 }
234 let r = read(&path).expect("read info");
235 assert!(r.size_bytes > 0);
236 assert!(!r.exports.is_empty());
237 assert_eq!(r.info_schema_version, 1);
238 assert_eq!(r.artifact_type, "component/wasm");
239 assert!(matches!(
243 r.manifest_source,
244 ManifestSource::Embedded | ManifestSource::Sibling | ManifestSource::None
245 ));
246 }
247
248 #[test]
249 fn pick_wit_world_prefers_world_segments() {
250 let exports = vec![
251 "wasi:io/streams@0.2.0".to_string(),
252 "greentic:component/descriptor@0.6.0".to_string(),
253 ];
254 let picked = pick_wit_world(&exports);
255 assert_eq!(
256 picked.as_deref(),
257 Some("greentic:component/descriptor@0.6.0")
258 );
259 }
260
261 #[test]
262 fn extract_capabilities_empty_when_default() {
263 let caps = ManifestCapabilities::default();
264 let out = extract_capabilities(&caps);
265 assert!(out.host.is_empty());
266 assert!(out.wasi.is_empty());
267 }
268
269 #[test]
270 fn extract_capabilities_flags_enabled_surfaces() {
271 use crate::capabilities::{HostCapabilities, MessagingCapabilities, StateCapabilities};
272 let caps = ManifestCapabilities {
273 wasi: crate::capabilities::WasiCapabilities {
274 filesystem: None,
275 env: None,
276 random: false,
277 clocks: true,
278 },
279 host: HostCapabilities {
280 messaging: Some(MessagingCapabilities {
281 inbound: true,
282 outbound: false,
283 }),
284 events: None,
285 http: None,
286 secrets: None,
287 state: Some(StateCapabilities {
288 read: true,
289 write: false,
290 }),
291 telemetry: None,
292 iac: None,
293 },
294 };
295 let out = extract_capabilities(&caps);
296 assert_eq!(out.wasi, vec!["clocks".to_string()]);
297 assert_eq!(
298 out.host,
299 vec!["messaging.inbound".to_string(), "state.read".to_string()]
300 );
301 }
302}