greentic_component/store/
meta.rs

1use anyhow::Result;
2use semver::Version;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5#[cfg(feature = "abi")]
6use std::collections::BTreeSet;
7use wasm_metadata::Producers;
8#[cfg(feature = "abi")]
9use wit_parser::{InterfaceId, PackageName, Resolve, WorldId, WorldItem, WorldKey};
10
11use super::ComponentId;
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct MetaInfo {
15    pub id: ComponentId,
16    pub size: u64,
17    pub abi_version: String,
18    pub provider_name: Option<String>,
19    pub provider_version: Option<String>,
20    pub capabilities: Vec<String>,
21}
22
23pub async fn compute_id_and_meta(bytes: &[u8]) -> Result<(ComponentId, MetaInfo)> {
24    let mut hasher = Sha256::new();
25    hasher.update(bytes);
26    let digest = hex::encode(hasher.finalize());
27    let id = ComponentId(format!("sha256:{digest}"));
28
29    let size = bytes.len() as u64;
30
31    let mut abi_version = "greentic-abi-0".to_string();
32    let mut provider_name = None;
33    let mut provider_version = None;
34    let mut capabilities = Vec::new();
35
36    #[cfg(feature = "abi")]
37    if let Some(extracted) = extract_from_wit_metadata(bytes) {
38        abi_version = extracted.abi_version;
39        provider_name = extracted.provider_name;
40        provider_version = extracted.provider_version;
41        capabilities = extracted.capabilities;
42    }
43
44    // Fall back to producers metadata to at least capture greentic-interfaces provenance.
45    if (provider_name.is_none() || provider_version.is_none())
46        && let Ok(Some(producers)) = Producers::from_wasm(bytes)
47        && let Some(processed_by) = producers.get("processed-by")
48        && let Some(version) = processed_by.get("greentic-interfaces")
49    {
50        provider_name.get_or_insert_with(|| "greentic-interfaces".to_string());
51        provider_version.get_or_insert_with(|| version.clone());
52        abi_version = format!("greentic-abi-{}", semver_major(version));
53    }
54
55    let meta = MetaInfo {
56        id: id.clone(),
57        size,
58        abi_version,
59        provider_name,
60        provider_version,
61        capabilities,
62    };
63
64    Ok((id, meta))
65}
66
67struct ExtractedMeta {
68    abi_version: String,
69    provider_name: Option<String>,
70    provider_version: Option<String>,
71    capabilities: Vec<String>,
72}
73
74#[cfg(feature = "abi")]
75fn extract_from_wit_metadata(bytes: &[u8]) -> Option<ExtractedMeta> {
76    let decoded = crate::wasm::decode_world(bytes).ok()?;
77    let resolve = decoded.resolve;
78    let world_id = decoded.world;
79
80    let world = &resolve.worlds[world_id];
81    let mut abi_version = "greentic-abi-0".to_string();
82    let mut provider_name = None;
83    let mut provider_version = None;
84
85    if let Some(pkg_id) = world.package {
86        let pkg = &resolve.packages[pkg_id];
87        provider_name = Some(pkg.name.name.clone());
88        if let Some(version) = &pkg.name.version {
89            provider_version = Some(version.to_string());
90            abi_version = format!("greentic-abi-{}", version.major);
91        }
92    }
93
94    let capabilities = collect_import_capabilities(&resolve, world_id);
95
96    Some(ExtractedMeta {
97        abi_version,
98        provider_name,
99        provider_version,
100        capabilities,
101    })
102}
103
104#[cfg(feature = "abi")]
105fn collect_import_capabilities(resolve: &Resolve, world_id: WorldId) -> Vec<String> {
106    let world = &resolve.worlds[world_id];
107    let mut caps = BTreeSet::new();
108
109    for (key, item) in &world.imports {
110        match item {
111            WorldItem::Interface { id, .. } => {
112                caps.insert(interface_label(resolve, *id, key));
113            }
114            WorldItem::Function(func) => {
115                caps.insert(format!("func:{}", func.name));
116            }
117            WorldItem::Type(_) => {}
118        }
119    }
120
121    caps.into_iter().collect()
122}
123
124#[cfg(feature = "abi")]
125fn interface_label(resolve: &Resolve, iface_id: InterfaceId, key: &WorldKey) -> String {
126    let iface = &resolve.interfaces[iface_id];
127    let name = iface
128        .name
129        .as_ref()
130        .map(|s| s.to_string())
131        .or_else(|| key_as_name(key))
132        .unwrap_or_else(|| "interface".to_string());
133
134    if let Some(pkg_id) = iface.package {
135        let pkg = &resolve.packages[pkg_id];
136        format_package(&pkg.name, Some(&name))
137    } else {
138        name
139    }
140}
141
142#[cfg(feature = "abi")]
143fn format_package(pkg: &PackageName, name: Option<&str>) -> String {
144    match (&pkg.name, &pkg.namespace, &pkg.version) {
145        (pkg_name, ns, Some(version)) => match name {
146            Some(name) => format!("{ns}:{pkg_name}/{name}@{version}"),
147            None => format!("{ns}:{pkg_name}@{version}"),
148        },
149        (pkg_name, ns, None) => match name {
150            Some(name) => format!("{ns}:{pkg_name}/{name}"),
151            None => format!("{ns}:{pkg_name}"),
152        },
153    }
154}
155
156#[cfg(feature = "abi")]
157fn key_as_name(key: &WorldKey) -> Option<String> {
158    match key {
159        WorldKey::Name(name) => Some(name.to_string()),
160        WorldKey::Interface(_) => None,
161    }
162}
163
164fn semver_major(version: &str) -> u64 {
165    Version::parse(version).map(|v| v.major).unwrap_or(0)
166}