greentic_component/
loader.rs

1use std::ffi::OsStr;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use directories::BaseDirs;
6use thiserror::Error;
7
8use crate::manifest::{ComponentManifest, parse_manifest};
9use crate::signing::{SigningError, verify_manifest_hash};
10
11const MANIFEST_NAME: &str = "component.manifest.json";
12
13#[derive(Debug, Clone)]
14pub struct ComponentHandle {
15    pub manifest: ComponentManifest,
16    pub wasm_path: PathBuf,
17    pub root: PathBuf,
18    pub manifest_path: PathBuf,
19}
20
21#[derive(Debug, Error)]
22pub enum LoadError {
23    #[error("component not found for `{0}`")]
24    NotFound(String),
25    #[error("failed to read {path}: {source}")]
26    Io {
27        path: PathBuf,
28        #[source]
29        source: std::io::Error,
30    },
31    #[error("manifest parse failed at {path}: {source}")]
32    Manifest {
33        path: PathBuf,
34        #[source]
35        source: crate::manifest::ManifestError,
36    },
37    #[error("missing artifact `{path}` declared in manifest")]
38    MissingArtifact { path: PathBuf },
39    #[error("hash verification failed: {0}")]
40    Signing(#[from] SigningError),
41}
42
43pub fn discover(path_or_id: &str) -> Result<ComponentHandle, LoadError> {
44    if let Some(handle) = try_explicit(path_or_id)? {
45        return Ok(handle);
46    }
47    if let Some(handle) = try_workspace(path_or_id)? {
48        return Ok(handle);
49    }
50    if let Some(handle) = try_registry(path_or_id)? {
51        return Ok(handle);
52    }
53    Err(LoadError::NotFound(path_or_id.to_string()))
54}
55
56fn try_explicit(arg: &str) -> Result<Option<ComponentHandle>, LoadError> {
57    let path = Path::new(arg);
58    if !path.exists() {
59        return Ok(None);
60    }
61
62    let target = if path.is_dir() {
63        path.join(MANIFEST_NAME)
64    } else if path.extension().and_then(OsStr::to_str) == Some("json") {
65        path.to_path_buf()
66    } else if path.extension().and_then(OsStr::to_str) == Some("wasm") {
67        path.parent()
68            .map(|dir| dir.join(MANIFEST_NAME))
69            .unwrap_or_else(|| path.to_path_buf())
70    } else {
71        path.join(MANIFEST_NAME)
72    };
73
74    if target.exists() {
75        return load_from_manifest(&target).map(Some);
76    }
77
78    Ok(None)
79}
80
81fn try_workspace(id: &str) -> Result<Option<ComponentHandle>, LoadError> {
82    let cwd = std::env::current_dir().map_err(|e| LoadError::Io {
83        path: PathBuf::from("."),
84        source: e,
85    })?;
86    let target = cwd.join("target").join("wasm32-wasip2");
87    let file_name = format!("{id}.wasm");
88
89    for profile in ["release", "debug"] {
90        let candidate = target.join(profile).join(&file_name);
91        if candidate.exists() {
92            let manifest_path = candidate
93                .parent()
94                .map(|dir| dir.join(MANIFEST_NAME))
95                .unwrap_or_else(|| candidate.with_extension("manifest.json"));
96            if manifest_path.exists() {
97                return load_from_manifest(&manifest_path).map(Some);
98            }
99        }
100    }
101
102    Ok(None)
103}
104
105fn try_registry(id: &str) -> Result<Option<ComponentHandle>, LoadError> {
106    let Some(base) = BaseDirs::new() else {
107        return Ok(None);
108    };
109    let registry_root = base.home_dir().join(".greentic").join("components");
110    if !registry_root.exists() {
111        return Ok(None);
112    }
113
114    let mut candidates = Vec::new();
115    for entry in fs::read_dir(&registry_root).map_err(|err| LoadError::Io {
116        path: registry_root.clone(),
117        source: err,
118    })? {
119        let entry = entry.map_err(|err| LoadError::Io {
120            path: registry_root.clone(),
121            source: err,
122        })?;
123        let name = entry.file_name();
124        let name = name.to_string_lossy();
125        if name == id || (!id.contains('@') && name.starts_with(id)) {
126            candidates.push(entry.path());
127        }
128    }
129
130    candidates.sort();
131    candidates.reverse();
132
133    for dir in candidates {
134        let manifest_path = dir.join(MANIFEST_NAME);
135        if manifest_path.exists() {
136            return load_from_manifest(&manifest_path).map(Some);
137        }
138    }
139
140    Ok(None)
141}
142
143fn load_from_manifest(path: &Path) -> Result<ComponentHandle, LoadError> {
144    let contents = fs::read_to_string(path).map_err(|source| LoadError::Io {
145        path: path.to_path_buf(),
146        source,
147    })?;
148    let manifest = parse_manifest(&contents).map_err(|source| LoadError::Manifest {
149        path: path.to_path_buf(),
150        source,
151    })?;
152    let root = path
153        .parent()
154        .map(|p| p.to_path_buf())
155        .unwrap_or_else(|| PathBuf::from("."));
156    let wasm_path = root.join(manifest.artifacts.component_wasm());
157    if !wasm_path.exists() {
158        return Err(LoadError::MissingArtifact { path: wasm_path });
159    }
160    verify_manifest_hash(&manifest, &root)?;
161    Ok(ComponentHandle {
162        manifest,
163        wasm_path,
164        root,
165        manifest_path: path.to_path_buf(),
166    })
167}