greentic_component/store/
mod.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4#[cfg(not(feature = "oci"))]
5use anyhow::bail;
6use anyhow::{Context, Result, anyhow};
7use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9use tracing::instrument;
10
11use self::cache::Cache;
12use crate::path_safety::normalize_under_root;
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct ComponentId(pub String);
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum ComponentLocator {
19    Fs { path: PathBuf },
20    Oci { reference: String },
21}
22
23#[derive(Clone, Debug)]
24pub struct ComponentBytes {
25    pub id: ComponentId,
26    pub bytes: Bytes,
27    pub meta: MetaInfo,
28}
29
30pub type SourceId = String;
31
32#[derive(Clone, Debug)]
33pub struct ComponentStore {
34    sources: HashMap<SourceId, ComponentLocator>,
35    cache: Cache,
36    compat: CompatPolicy,
37}
38
39impl Default for ComponentStore {
40    fn default() -> Self {
41        Self::with_cache_dir(None, CompatPolicy::default())
42    }
43}
44
45impl ComponentStore {
46    pub fn with_cache_dir(cache_dir: Option<PathBuf>, compat: CompatPolicy) -> Self {
47        Self {
48            sources: HashMap::new(),
49            cache: Cache::new(cache_dir),
50            compat,
51        }
52    }
53
54    pub fn add_fs(&mut self, id: impl Into<SourceId>, path: impl Into<PathBuf>) -> &mut Self {
55        self.sources
56            .insert(id.into(), ComponentLocator::Fs { path: path.into() });
57        self
58    }
59
60    pub fn add_oci(&mut self, id: impl Into<SourceId>, reference: impl Into<String>) -> &mut Self {
61        self.sources.insert(
62            id.into(),
63            ComponentLocator::Oci {
64                reference: reference.into(),
65            },
66        );
67        self
68    }
69
70    #[instrument(level = "trace", skip_all, fields(source = %source_id))]
71    pub async fn get(&self, source_id: &str) -> Result<ComponentBytes> {
72        let loc = self
73            .sources
74            .get(source_id)
75            .ok_or_else(|| anyhow!("unknown source id: {source_id}"))?;
76
77        if let Some(hit) = self.cache.try_load(loc).await? {
78            compat::check(&self.compat, &hit.meta).map_err(anyhow::Error::new)?;
79            return Ok(hit);
80        }
81
82        let bytes = match loc {
83            ComponentLocator::Fs { path } => {
84                let (fs_root, candidate) = filesystem_root_and_path(path.as_path())?;
85                fs_source::fetch(&fs_root, &candidate).await?
86            }
87            ComponentLocator::Oci { reference } => {
88                #[cfg(feature = "oci")]
89                {
90                    oci_source::fetch(reference).await?
91                }
92                #[cfg(not(feature = "oci"))]
93                {
94                    bail!("OCI support disabled: enable the `oci` feature to fetch {reference}");
95                }
96            }
97        };
98
99        let (id, meta) = meta::compute_id_and_meta(bytes.as_ref()).await?;
100        let cb = ComponentBytes { id, bytes, meta };
101
102        compat::check(&self.compat, &cb.meta).map_err(anyhow::Error::new)?;
103        self.cache.store(loc, &cb).await?;
104        Ok(cb)
105    }
106}
107
108fn filesystem_root_and_path(path: &Path) -> Result<(PathBuf, PathBuf)> {
109    let canonical = path
110        .canonicalize()
111        .with_context(|| format!("failed to canonicalize {}", path.display()))?;
112    let root = canonical
113        .parent()
114        .map(Path::to_path_buf)
115        .context("filesystem source path has no parent")?;
116    let relative = canonical
117        .strip_prefix(&root)
118        .with_context(|| {
119            format!(
120                "failed to compute relative path for {}",
121                canonical.display()
122            )
123        })?
124        .to_path_buf();
125    // Double-check containment under the discovered root to enforce policy.
126    normalize_under_root(&root, &relative)?;
127    Ok((root, relative))
128}
129
130mod cache;
131mod compat;
132mod fs_source;
133mod meta;
134#[cfg(feature = "oci")]
135mod oci_source;
136
137pub use compat::{CompatError, CompatPolicy};
138pub use meta::MetaInfo;