gtc 0.9.36

Greentic - The operation system for digital workers
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;

use greentic_distributor_client::oci_components::{DefaultRegistryClient, OciComponentResolver};
use greentic_distributor_client::{ComponentResolveOptions, ComponentsExtension, ComponentsMode};
use serde_json::Value;

pub fn pull_oci_reference_to_tempfile(oci_ref: &str, key: Option<&str>) -> Result<PathBuf, String> {
    if let Ok(root) = env::var("GTC_DIST_MOCK_ROOT") {
        let mock = FileDistAdapter::new(PathBuf::from(root))?;
        return mock.resolve_ref_to_cached_file(oci_ref);
    }
    OciDistAdapter.resolve_ref_to_cached_file(oci_ref, key.unwrap_or_default())
}

struct OciDistAdapter;

impl OciDistAdapter {
    fn resolve_ref_to_cached_file(&self, oci_ref: &str, key: &str) -> Result<PathBuf, String> {
        let cleaned = strip_oci_prefix(oci_ref);
        let cache = tempfile::tempdir().map_err(|e| e.to_string())?;
        let opts = ComponentResolveOptions {
            allow_tags: true,
            cache_dir: cache.path().join("cache"),
            ..Default::default()
        };

        let client = if key.is_empty() {
            DefaultRegistryClient::default()
        } else {
            DefaultRegistryClient::with_basic_auth("oauth2accesstoken", key)
        };
        let resolver = OciComponentResolver::with_client(client, opts);
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .map_err(|e| e.to_string())?;

        let resolved = rt
            .block_on(resolver.resolve_refs(&ComponentsExtension {
                refs: vec![cleaned.clone()],
                mode: ComponentsMode::Eager,
            }))
            .map_err(|e| format!("failed to pull '{cleaned}': {e}"))?;

        let artifact = resolved
            .into_iter()
            .next()
            .ok_or_else(|| format!("no layers found for '{cleaned}'"))?;

        let path = artifact.path;
        let bytes = fs::read(&path).map_err(|e| e.to_string())?;

        let staging = tempfile::tempdir().map_err(|e| e.to_string())?;
        let filename = path
            .file_name()
            .and_then(|v| v.to_str())
            .unwrap_or("artifact.bin");
        let out_path = staging.path().join(filename);
        fs::write(&out_path, bytes).map_err(|e| e.to_string())?;

        let keep_dir = staging.keep();
        drop(cache);

        Ok(keep_dir.join(filename))
    }
}

struct FileDistAdapter {
    root: PathBuf,
    index: HashMap<String, String>,
}

impl FileDistAdapter {
    fn new(root: PathBuf) -> Result<Self, String> {
        let index_path = root.join("index.json");
        let raw = fs::read_to_string(&index_path)
            .map_err(|e| format!("failed to read {}: {e}", index_path.display()))?;
        let value: Value = serde_json::from_str(&raw)
            .map_err(|e| format!("invalid {}: {e}", index_path.display()))?;
        let obj = value
            .as_object()
            .ok_or_else(|| "index.json root must be an object".to_string())?;

        let mut index = HashMap::new();
        for (k, v) in obj {
            let s = v
                .as_str()
                .ok_or_else(|| format!("index entry '{k}' must be a string"))?;
            index.insert(k.clone(), s.to_string());
        }

        Ok(Self { root, index })
    }

    fn map_ref(&self, oci_ref: &str) -> Result<PathBuf, String> {
        let rel = self
            .index
            .get(oci_ref)
            .ok_or_else(|| format!("missing mock mapping for '{oci_ref}'"))?;
        Ok(self.root.join(rel))
    }

    fn resolve_ref_to_cached_file(&self, oci_ref: &str) -> Result<PathBuf, String> {
        self.map_ref(oci_ref)
    }
}

fn strip_oci_prefix(value: &str) -> String {
    value.strip_prefix("oci://").unwrap_or(value).to_string()
}

#[cfg(test)]
mod tests {
    use super::strip_oci_prefix;

    #[test]
    fn strip_oci_prefix_removes_scheme() {
        assert_eq!(
            strip_oci_prefix("oci://ghcr.io/greentic-biz/customers-tools/demo:latest"),
            "ghcr.io/greentic-biz/customers-tools/demo:latest"
        );
    }

    #[test]
    fn strip_oci_prefix_leaves_plain_reference_unchanged() {
        assert_eq!(
            strip_oci_prefix("ghcr.io/greentic-biz/customers-tools/demo:latest"),
            "ghcr.io/greentic-biz/customers-tools/demo:latest"
        );
    }
}