greentic_component/
loader.rs1use 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(®istry_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}