Skip to main content

greentic_component/
loader.rs

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