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(
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(®istry_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}