use std::path::{Path, PathBuf};
use serde::Deserialize;
const RELEASE_INDEX_SCHEMA: &str = "greentic.release-index.v1";
const INSTALLED_TOOLCHAIN_SCHEMA: &str = "greentic.installed-toolchain.v1";
fn greentic_home() -> Option<PathBuf> {
if let Ok(root) = std::env::var("GREENTIC_HOME") {
return Some(PathBuf::from(root));
}
std::env::var("HOME").ok().map(|home| {
let mut path = PathBuf::from(home);
path.push(".greentic");
path
})
}
fn distribution_cache_root() -> PathBuf {
if let Some(dir) = std::env::var("GREENTIC_CACHE_DIR")
.or_else(|_| std::env::var("GREENTIC_DIST_CACHE_DIR"))
.ok()
.map(PathBuf::from)
{
return dir;
}
match greentic_home() {
Some(home) => home.join("cache").join("distribution"),
None => PathBuf::from(".greentic")
.join("cache")
.join("distribution"),
}
}
#[derive(Debug, Deserialize)]
struct InstalledToolchain {
schema: String,
channel: Option<String>,
version: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ReleaseIndex {
schema: String,
#[serde(default)]
refs: std::collections::BTreeMap<String, ReleaseIndexEntry>,
}
#[derive(Debug, Deserialize)]
struct ReleaseIndexEntry {
#[serde(default)]
digest: Option<String>,
#[serde(default)]
canonical_ref: Option<String>,
}
fn installed_release_in(toolchain_root: &Path) -> Option<(String, String)> {
let raw = std::fs::read_to_string(toolchain_root.join("installed.json")).ok()?;
let installed: InstalledToolchain = serde_json::from_str(&raw).ok()?;
if installed.schema != INSTALLED_TOOLCHAIN_SCHEMA {
return None;
}
let channel = installed.channel?.trim().to_string();
let version = installed.version?.trim().to_string();
(!channel.is_empty() && !version.is_empty()).then_some((channel, version))
}
pub(crate) fn pinned_ref_for(reference: &str) -> Option<String> {
let home = greentic_home()?;
pinned_ref_for_in(
&distribution_cache_root(),
&home.join("toolchain"),
reference,
)
}
pub(crate) fn pinned_ref_for_in(
cache_root: &Path,
toolchain_root: &Path,
reference: &str,
) -> Option<String> {
if reference.contains("@sha256:") {
return None;
}
let (channel, release) = installed_release_in(toolchain_root)?;
let index_path = cache_root
.join("release-index")
.join("v1")
.join(&channel)
.join(format!("{release}.json"));
let raw = std::fs::read_to_string(&index_path).ok()?;
let index: ReleaseIndex = serde_json::from_str(&raw).ok()?;
if index.schema != RELEASE_INDEX_SCHEMA {
return None;
}
let key = reference.strip_prefix("oci://").unwrap_or(reference).trim();
let entry = index.refs.get(key)?;
if let Some(canonical) = entry
.canonical_ref
.as_deref()
.map(str::trim)
.filter(|value| value.contains("@sha256:"))
{
return Some(canonical.to_string());
}
let digest = entry
.digest
.as_deref()
.map(str::trim)
.filter(|digest| digest.starts_with("sha256:"))?;
let repo = key.rsplit_once(':').map(|(repo, _tag)| repo).unwrap_or(key);
Some(format!("oci://{repo}@{digest}"))
}
#[cfg(test)]
mod tests {
use super::*;
const SLACK_TAG: &str = "oci://ghcr.io/greenticai/packs/messaging/messaging-slack:stable";
const SLACK_DIGEST: &str =
"sha256:163a5c0ab582798a0000000000000000000000000000000000000000000000ab";
fn write(path: PathBuf, body: &str) {
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(path, body).expect("write");
}
fn stage(dir: &Path, release: &str, index_body: &str) -> (PathBuf, PathBuf) {
let cache_root = dir.join("cache").join("distribution");
let toolchain_root = dir.join("toolchain");
write(
toolchain_root.join("installed.json"),
&format!(
r#"{{"schema":"greentic.installed-toolchain.v1","channel":"stable","version":"{release}"}}"#
),
);
write(
cache_root
.join("release-index")
.join("v1")
.join("stable")
.join(format!("{release}.json")),
index_body,
);
(cache_root, toolchain_root)
}
fn index_with_slack(canonical: bool) -> String {
let entry = if canonical {
format!(
r#"{{"version":"0.5.18","digest":"{SLACK_DIGEST}","canonical_ref":"oci://ghcr.io/greenticai/packs/messaging/messaging-slack@{SLACK_DIGEST}"}}"#
)
} else {
format!(r#"{{"version":"0.5.18","digest":"{SLACK_DIGEST}"}}"#)
};
format!(
r#"{{"schema":"greentic.release-index.v1","refs":{{"ghcr.io/greenticai/packs/messaging/messaging-slack:stable":{entry}}}}}"#
)
}
#[test]
fn resolves_a_tag_to_the_indexed_canonical_ref() {
let dir = tempfile::tempdir().expect("tempdir");
let (cache, toolchain) = stage(dir.path(), "1.1.10", &index_with_slack(true));
assert_eq!(
pinned_ref_for_in(&cache, &toolchain, SLACK_TAG).as_deref(),
Some(
format!("oci://ghcr.io/greenticai/packs/messaging/messaging-slack@{SLACK_DIGEST}")
.as_str()
),
"a tagged ref must resolve to the digest the installed release pinned"
);
}
#[test]
fn composes_a_pinned_ref_when_the_entry_has_only_a_digest() {
let dir = tempfile::tempdir().expect("tempdir");
let (cache, toolchain) = stage(dir.path(), "1.1.10", &index_with_slack(false));
let resolved = pinned_ref_for_in(&cache, &toolchain, SLACK_TAG).expect("resolves");
assert!(
resolved.ends_with(&format!("@{SLACK_DIGEST}")),
"{resolved}"
);
assert!(!resolved.contains(":stable"), "tag must be dropped");
}
#[test]
fn accepts_a_reference_without_the_oci_scheme() {
let dir = tempfile::tempdir().expect("tempdir");
let (cache, toolchain) = stage(dir.path(), "1.1.10", &index_with_slack(true));
assert!(
pinned_ref_for_in(
&cache,
&toolchain,
"ghcr.io/greenticai/packs/messaging/messaging-slack:stable"
)
.is_some(),
"index keys carry no scheme, so both spellings must resolve"
);
}
#[test]
fn resolves_against_the_installed_release_not_the_newest_index() {
let dir = tempfile::tempdir().expect("tempdir");
let (cache, toolchain) = stage(dir.path(), "1.1.10", &index_with_slack(true));
write(
cache
.join("release-index")
.join("v1")
.join("stable")
.join("1.1.13.json"),
r#"{"schema":"greentic.release-index.v1","refs":{"ghcr.io/greenticai/packs/messaging/messaging-slack:stable":{"version":"0.5.21","digest":"sha256:deadbeef00000000000000000000000000000000000000000000000000000000"}}}"#,
);
let resolved = pinned_ref_for_in(&cache, &toolchain, SLACK_TAG).expect("resolves");
assert!(
resolved.contains(SLACK_DIGEST),
"must use 1.1.10's digest, got {resolved}"
);
assert!(
!resolved.contains("deadbeef"),
"must not use 1.1.13's digest"
);
}
#[test]
fn falls_back_to_the_tag_when_resolution_is_not_possible() {
let dir = tempfile::tempdir().expect("tempdir");
let (cache, toolchain) = stage(dir.path(), "1.1.10", &index_with_slack(true));
assert_eq!(
pinned_ref_for_in(&cache, &toolchain, "oci://ghcr.io/other/pack:stable"),
None
);
assert_eq!(
pinned_ref_for_in(
&cache,
&toolchain,
&format!("oci://ghcr.io/greenticai/packs/messaging/messaging-slack@{SLACK_DIGEST}")
),
None
);
let bare = tempfile::tempdir().expect("tempdir");
let (bare_cache, bare_toolchain) = stage(bare.path(), "1.1.10", &index_with_slack(true));
std::fs::remove_file(
bare_cache
.join("release-index")
.join("v1")
.join("stable")
.join("1.1.10.json"),
)
.expect("remove index");
assert_eq!(
pinned_ref_for_in(&bare_cache, &bare_toolchain, SLACK_TAG),
None
);
let empty = tempfile::tempdir().expect("tempdir");
assert_eq!(
pinned_ref_for_in(empty.path(), empty.path(), SLACK_TAG),
None
);
}
#[test]
fn rejects_unexpected_schemas() {
let dir = tempfile::tempdir().expect("tempdir");
let (cache, toolchain) = stage(dir.path(), "1.1.10", &index_with_slack(true));
write(
cache
.join("release-index")
.join("v1")
.join("stable")
.join("1.1.10.json"),
r#"{"schema":"something.else.v9","refs":{}}"#,
);
assert_eq!(pinned_ref_for_in(&cache, &toolchain, SLACK_TAG), None);
let other = tempfile::tempdir().expect("tempdir");
let (other_cache, other_toolchain) = stage(other.path(), "1.1.10", &index_with_slack(true));
write(
other_toolchain.join("installed.json"),
r#"{"schema":"nope.v1","channel":"stable","version":"1.1.10"}"#,
);
assert_eq!(
pinned_ref_for_in(&other_cache, &other_toolchain, SLACK_TAG),
None
);
}
}