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