Skip to main content

greentic_component_store/
lib.rs

1use std::fs as std_fs;
2use std::path::{Path, PathBuf};
3
4use percent_encoding::percent_decode_str;
5use sha2::{Digest as _, Sha256};
6use thiserror::Error;
7use tracing::debug;
8use url::Url;
9
10pub mod fs;
11#[cfg(feature = "http")]
12pub mod http;
13pub mod oci;
14pub mod verify;
15pub mod warg;
16
17pub use verify::{
18    DigestAlgorithm, DigestPolicy, SignaturePolicy, VerificationError, VerificationPolicy,
19    VerificationReport, VerifiedDigest, VerifiedSignature,
20};
21
22#[derive(Debug, Clone)]
23pub struct ComponentStore {
24    cache_root: PathBuf,
25    #[cfg(feature = "http")]
26    http_client: reqwest::blocking::Client,
27}
28
29impl ComponentStore {
30    pub fn new(cache_root: impl AsRef<Path>) -> Result<Self, StoreError> {
31        let cache_root = cache_root.as_ref().to_path_buf();
32        std_fs::create_dir_all(&cache_root)?;
33        Ok(Self {
34            cache_root,
35            #[cfg(feature = "http")]
36            http_client: http::build_client()?,
37        })
38    }
39
40    pub fn with_default_cache() -> Result<Self, StoreError> {
41        let default = default_cache_dir();
42        Self::new(default)
43    }
44
45    pub fn cache_root(&self) -> &Path {
46        &self.cache_root
47    }
48
49    pub fn fetch_from_str(
50        &self,
51        locator: &str,
52        policy: &VerificationPolicy,
53    ) -> Result<StoreArtifact, StoreError> {
54        let locator = StoreLocator::parse(locator)?;
55        self.fetch(&locator, policy)
56    }
57
58    pub fn fetch(
59        &self,
60        locator: &StoreLocator,
61        policy: &VerificationPolicy,
62    ) -> Result<StoreArtifact, StoreError> {
63        if let Some(expected) = policy.digest.as_ref().and_then(|d| d.expected()) {
64            let cache_path = self.cache_root.join(format!("{expected}.wasm"));
65            if cache_path.exists() {
66                debug!("cache hit for digest {expected}");
67                let bytes = std_fs::read(&cache_path)?;
68                let report = policy.verify(&bytes)?;
69                return Ok(StoreArtifact {
70                    locator: locator.clone(),
71                    path: cache_path,
72                    bytes,
73                    verification: report,
74                });
75            }
76        }
77
78        if let Some(artifact) = self.try_fetch_cached(locator, policy)? {
79            return Ok(artifact);
80        }
81
82        let bytes = self.fetch_bytes(locator)?;
83        let report = policy.verify(&bytes)?;
84        let digest = report
85            .digest
86            .clone()
87            .unwrap_or_else(|| VerifiedDigest::compute(DigestAlgorithm::Sha256, &bytes));
88        let cache_path = self.persist(locator, &bytes, &digest)?;
89        Ok(StoreArtifact {
90            locator: locator.clone(),
91            path: cache_path,
92            bytes,
93            verification: VerificationReport {
94                digest: Some(digest),
95                signature: report.signature,
96            },
97        })
98    }
99
100    fn try_fetch_cached(
101        &self,
102        locator: &StoreLocator,
103        policy: &VerificationPolicy,
104    ) -> Result<Option<StoreArtifact>, StoreError> {
105        let cache_key = self.compute_cache_key(locator);
106        let cache_path = self.cache_root.join(format!("{cache_key}.wasm"));
107        if !cache_path.exists() {
108            return Ok(None);
109        }
110
111        let bytes = std_fs::read(&cache_path)?;
112        let report = policy.verify(&bytes)?;
113        let digest = report
114            .digest
115            .clone()
116            .unwrap_or_else(|| VerifiedDigest::compute(DigestAlgorithm::Sha256, &bytes));
117        let digest_path = self.persist(locator, &bytes, &digest)?;
118        Ok(Some(StoreArtifact {
119            locator: locator.clone(),
120            path: digest_path,
121            bytes,
122            verification: VerificationReport {
123                digest: Some(digest),
124                signature: report.signature,
125            },
126        }))
127    }
128
129    fn fetch_bytes(&self, locator: &StoreLocator) -> Result<Vec<u8>, StoreError> {
130        match locator {
131            StoreLocator::Fs { path, .. } => crate::fs::fetch(path),
132            StoreLocator::Http(url) => {
133                #[cfg(feature = "http")]
134                {
135                    http::fetch(&self.http_client, url)
136                }
137                #[cfg(not(feature = "http"))]
138                {
139                    let _ = url;
140                    Err(StoreError::UnsupportedScheme("http".into()))
141                }
142            }
143            StoreLocator::Https(url) => {
144                #[cfg(feature = "http")]
145                {
146                    http::fetch(&self.http_client, url)
147                }
148                #[cfg(not(feature = "http"))]
149                {
150                    let _ = url;
151                    Err(StoreError::UnsupportedScheme("https".into()))
152                }
153            }
154            StoreLocator::Oci(reference) => oci::fetch(reference),
155            StoreLocator::Warg(reference) => warg::fetch(reference),
156        }
157    }
158
159    fn persist(
160        &self,
161        locator: &StoreLocator,
162        bytes: &[u8],
163        digest: &VerifiedDigest,
164    ) -> Result<PathBuf, StoreError> {
165        let mut file_name = digest.value.clone();
166        file_name.push_str(".wasm");
167        let path = self.cache_root.join(&file_name);
168        std_fs::write(&path, bytes)?;
169
170        let locator_cache = self
171            .cache_root
172            .join(format!("{}.wasm", self.compute_cache_key(locator)));
173        if locator_cache != path
174            && let Err(err) = std_fs::write(&locator_cache, bytes)
175        {
176            debug!(
177                "failed to update locator cache at {}: {}",
178                locator_cache.display(),
179                err
180            );
181        }
182
183        debug!("cached artifact {:?} at {}", locator, path.display());
184        Ok(path)
185    }
186}
187
188fn default_cache_dir() -> PathBuf {
189    std::env::temp_dir().join("greentic-component-cache")
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub enum StoreLocator {
194    Fs { path: PathBuf, locator: String },
195    Http(Url),
196    Https(Url),
197    Oci(String),
198    Warg(String),
199}
200
201impl StoreLocator {
202    pub fn parse(raw: &str) -> Result<Self, StoreError> {
203        if raw.contains("://") {
204            let url = Url::parse(raw).map_err(|err| StoreError::InvalidLocator {
205                locator: raw.to_string(),
206                reason: err.to_string(),
207            })?;
208            match url.scheme() {
209                "fs" => {
210                    let path = decode_fs_path(&url)?;
211                    Ok(StoreLocator::Fs {
212                        path,
213                        locator: raw.to_string(),
214                    })
215                }
216                "file" => {
217                    let path = url.to_file_path().map_err(|_| StoreError::InvalidLocator {
218                        locator: raw.to_string(),
219                        reason: "unable to convert file URL to path".into(),
220                    })?;
221                    Ok(StoreLocator::Fs {
222                        path,
223                        locator: raw.to_string(),
224                    })
225                }
226                "http" => Ok(StoreLocator::Http(url)),
227                "https" => Ok(StoreLocator::Https(url)),
228                "oci" => Ok(StoreLocator::Oci(url.to_string())),
229                "warg" => Ok(StoreLocator::Warg(url.to_string())),
230                other => Err(StoreError::UnsupportedScheme(other.to_string())),
231            }
232        } else {
233            let path = PathBuf::from(raw);
234            let path = canonicalize_or(path);
235            Ok(StoreLocator::Fs {
236                path,
237                locator: raw.to_string(),
238            })
239        }
240    }
241
242    pub fn as_cache_key(&self) -> String {
243        match self {
244            StoreLocator::Fs { locator, .. } => locator.clone(),
245            StoreLocator::Http(url) | StoreLocator::Https(url) => url.as_str().to_string(),
246            StoreLocator::Oci(reference) | StoreLocator::Warg(reference) => reference.clone(),
247        }
248    }
249}
250
251#[derive(Debug)]
252pub struct StoreArtifact {
253    pub locator: StoreLocator,
254    pub path: PathBuf,
255    pub bytes: Vec<u8>,
256    pub verification: VerificationReport,
257}
258
259#[derive(Debug, Error)]
260pub enum StoreError {
261    #[error("invalid locator `{locator}`: {reason}")]
262    InvalidLocator { locator: String, reason: String },
263    #[error("unsupported locator scheme `{0}`")]
264    UnsupportedScheme(String),
265    #[error(transparent)]
266    Io(#[from] std::io::Error),
267    #[cfg(feature = "http")]
268    #[error(transparent)]
269    Http(#[from] reqwest::Error),
270    #[error(transparent)]
271    Verification(#[from] VerificationError),
272}
273
274fn decode_fs_path(url: &Url) -> Result<PathBuf, StoreError> {
275    let mut path = String::new();
276    if let Some(host) = url.host_str().filter(|host| !host.is_empty()) {
277        path.push_str("//");
278        path.push_str(host);
279    }
280    path.push_str(url.path());
281    let decoded = percent_decode_str(&path)
282        .decode_utf8()
283        .map_err(|err: std::str::Utf8Error| StoreError::InvalidLocator {
284            locator: url.to_string(),
285            reason: err.to_string(),
286        })?;
287    let buf = PathBuf::from(decoded.as_ref());
288    Ok(canonicalize_or(buf))
289}
290
291fn canonicalize_or(path: PathBuf) -> PathBuf {
292    std_fs::canonicalize(&path).unwrap_or(path)
293}
294
295fn hash_locator(locator: &StoreLocator) -> String {
296    let mut hasher = Sha256::new();
297    hasher.update(locator.as_cache_key().as_bytes());
298    hex::encode(hasher.finalize())
299}
300
301impl ComponentStore {
302    fn compute_cache_key(&self, locator: &StoreLocator) -> String {
303        hash_locator(locator)
304    }
305}