use std::collections::HashMap;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use crate::error::{Result, ToolchainError};
use zlayer_types::package_index::PackageIndexConfig;
pub const SEED_TOOLS: &[&str] = &[
"protobuf",
"cmake",
"ninja",
"pkgconf",
"jq",
"git",
"autoconf",
"automake",
"libtool",
"gettext",
"pcre2",
"oniguruma",
"openssl@3",
"zstd",
"xz",
];
pub const REPOSYNC_HMAC_SECRET_ENV: &str = "ZLAYER_REPOSYNC_HMAC_SECRET";
const REPOSYNC_HMAC_SECRET_BAKED: Option<&str> = option_env!("ZLAYER_REPOSYNC_HMAC_SECRET");
const REPOSYNC_SIGNATURE_HEADER: &str = "x-reposync-signature";
const COVERAGE_PAGES_BASE: &str = "https://zachhandley.github.io/RepoSources/coverage";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CoverageStatus {
Built,
Failed,
NetFallback,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageRecord {
pub tool: String,
pub platform: String,
pub arch: String,
pub status: CoverageStatus,
pub version: String,
#[serde(default)]
pub registry_ref: String,
#[serde(default)]
pub registry_digest: String,
#[serde(default)]
pub error_tail: String,
pub recorded_at: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CoverageMap {
#[serde(default)]
pub metadata: serde_json::Value,
#[serde(default)]
pub tools: HashMap<String, CoverageEntry>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CoverageEntry {
#[serde(default)]
pub version: String,
#[serde(default)]
pub status: String,
#[serde(default)]
pub source: String,
#[serde(default)]
pub updated_at: String,
}
#[must_use]
pub fn now_rfc3339() -> String {
chrono::Utc::now().to_rfc3339()
}
pub async fn record(rec: &CoverageRecord) {
let Some(secret) = resolve_hmac_secret() else {
warn!(
tool = %rec.tool,
"no reposync HMAC secret (env or compile-time bake); skipping coverage record"
);
return;
};
let body = match serde_json::to_vec(rec) {
Ok(b) => b,
Err(e) => {
warn!(tool = %rec.tool, error = %e, "coverage record failed to serialize; skipping");
return;
}
};
let signature = crate::package_index::sign(&secret, &body);
let url = coverage_post_url(&PackageIndexConfig::from_env().base_url);
match http_client()
.post(&url)
.header(REPOSYNC_SIGNATURE_HEADER, signature)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(body)
.send()
.await
{
Ok(resp) if resp.status().is_success() => {
debug!(
tool = %rec.tool,
platform = %rec.platform,
arch = %rec.arch,
status = %resp.status(),
"recorded toolchain coverage"
);
}
Ok(resp) => {
let status = resp.status();
let snippet: String = resp
.text()
.await
.unwrap_or_default()
.chars()
.take(200)
.collect();
warn!(
tool = %rec.tool,
%status,
body = %snippet,
"coverage record rejected (non-fatal)"
);
}
Err(e) => {
warn!(tool = %rec.tool, error = %e, "coverage record failed to send (non-fatal)");
}
}
}
pub async fn fetch_coverage_map(platform: &str, arch: &str) -> Result<CoverageMap> {
let url = pages_map_url(platform, arch);
let resp = http_client()
.get(&url)
.send()
.await
.map_err(|e| ToolchainError::RegistryError {
message: format!("failed to GET {url}: {e}"),
})?;
if !resp.status().is_success() {
return Err(ToolchainError::RegistryError {
message: format!("GET {url} returned status {}", resp.status()),
});
}
let bytes = resp
.bytes()
.await
.map_err(|e| ToolchainError::RegistryError {
message: format!("failed to read body from {url}: {e}"),
})?;
parse_coverage_map(&bytes).map_err(|e| ToolchainError::RegistryError {
message: format!("failed to parse coverage map from {url}: {e}"),
})
}
fn resolve_hmac_secret() -> Option<String> {
resolve_hmac_secret_from(
std::env::var(REPOSYNC_HMAC_SECRET_ENV).ok(),
REPOSYNC_HMAC_SECRET_BAKED,
)
}
fn resolve_hmac_secret_from(env_value: Option<String>, baked: Option<&str>) -> Option<String> {
env_value
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.or_else(|| {
baked
.filter(|s| !s.is_empty())
.map(std::string::ToString::to_string)
})
}
fn coverage_post_url(base: &str) -> String {
format!("{}/v1/coverage", base.trim_end_matches('/'))
}
fn pages_map_url(platform: &str, arch: &str) -> String {
format!("{COVERAGE_PAGES_BASE}/{platform}_{arch}.json")
}
fn parse_coverage_map(bytes: &[u8]) -> serde_json::Result<CoverageMap> {
serde_json::from_slice(bytes)
}
fn http_client() -> reqwest::Client {
reqwest::Client::builder()
.user_agent("zlayer-toolchain")
.timeout(Duration::from_secs(10))
.build()
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_record() -> CoverageRecord {
CoverageRecord {
tool: "jq".to_string(),
platform: "macos".to_string(),
arch: "arm64".to_string(),
status: CoverageStatus::Built,
version: "1.8.2".to_string(),
registry_ref: "forge.blackleafdigital.com/zlayer/toolchains/jq:1.8.2-macos-arm64"
.to_string(),
registry_digest: "sha256:abc123".to_string(),
error_tail: String::new(),
recorded_at: "2026-07-06T00:00:00+00:00".to_string(),
}
}
#[test]
fn secret_precedence_env_over_baked() {
assert_eq!(
resolve_hmac_secret_from(Some("from-env".into()), Some("from-bake")).as_deref(),
Some("from-env"),
);
assert_eq!(
resolve_hmac_secret_from(None, Some("from-bake")).as_deref(),
Some("from-bake"),
);
assert_eq!(
resolve_hmac_secret_from(Some(" \n".into()), Some("from-bake")).as_deref(),
Some("from-bake"),
);
assert_eq!(resolve_hmac_secret_from(None, Some("")), None);
assert_eq!(resolve_hmac_secret_from(None, None), None);
assert_eq!(
resolve_hmac_secret_from(Some(" s3cret\n".into()), None).as_deref(),
Some("s3cret"),
);
}
#[test]
fn resolve_hmac_secret_honors_runtime_env() {
std::env::set_var(REPOSYNC_HMAC_SECRET_ENV, "runtime-secret-probe");
assert_eq!(
resolve_hmac_secret().as_deref(),
Some("runtime-secret-probe")
);
std::env::remove_var(REPOSYNC_HMAC_SECRET_ENV);
assert_eq!(
resolve_hmac_secret(),
REPOSYNC_HMAC_SECRET_BAKED
.filter(|s| !s.is_empty())
.map(str::to_string),
);
}
#[test]
fn status_tokens_are_snake_case() {
assert_eq!(
serde_json::to_string(&CoverageStatus::Built).unwrap(),
"\"built\""
);
assert_eq!(
serde_json::to_string(&CoverageStatus::Failed).unwrap(),
"\"failed\""
);
assert_eq!(
serde_json::to_string(&CoverageStatus::NetFallback).unwrap(),
"\"net_fallback\""
);
assert_eq!(
serde_json::from_str::<CoverageStatus>("\"net_fallback\"").unwrap(),
CoverageStatus::NetFallback
);
}
#[test]
fn record_serializes_to_server_contract() {
let v = serde_json::to_value(sample_record()).unwrap();
assert_eq!(v["tool"], "jq");
assert_eq!(v["platform"], "macos");
assert_eq!(v["arch"], "arm64");
assert_eq!(v["status"], "built");
assert_eq!(v["version"], "1.8.2");
assert_eq!(
v["registry_ref"],
"forge.blackleafdigital.com/zlayer/toolchains/jq:1.8.2-macos-arm64"
);
assert_eq!(v["registry_digest"], "sha256:abc123");
assert_eq!(v["error_tail"], "");
assert_eq!(v["recorded_at"], "2026-07-06T00:00:00+00:00");
assert_eq!(v.as_object().unwrap().len(), 9);
}
#[test]
fn record_round_trips_and_optionals_default() {
let rec: CoverageRecord = serde_json::from_str(
r#"{"tool":"cmake","platform":"windows","arch":"x86_64",
"status":"failed","version":"4.0.3",
"recorded_at":"2026-07-06T00:00:00+00:00"}"#,
)
.unwrap();
assert_eq!(rec.status, CoverageStatus::Failed);
assert_eq!(rec.registry_ref, "");
assert_eq!(rec.registry_digest, "");
assert_eq!(rec.error_tail, "");
}
#[test]
fn signing_matches_package_index_sign_for_record_bytes() {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let body = serde_json::to_vec(&sample_record()).unwrap();
let via_shared_signer = crate::package_index::sign("secret", &body);
let mut mac = Hmac::<Sha256>::new_from_slice(b"secret").unwrap();
mac.update(&body);
let independent = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
assert_eq!(via_shared_signer, independent);
}
#[test]
fn coverage_post_url_joins_base() {
assert_eq!(
coverage_post_url("https://packages.zlayer.dev"),
"https://packages.zlayer.dev/v1/coverage"
);
assert_eq!(
coverage_post_url("https://packages.zlayer.dev/"),
"https://packages.zlayer.dev/v1/coverage"
);
}
#[test]
fn pages_map_url_shape() {
assert_eq!(
pages_map_url("macos", "arm64"),
"https://zachhandley.github.io/RepoSources/coverage/macos_arm64.json"
);
assert_eq!(
pages_map_url("windows", "x86_64"),
"https://zachhandley.github.io/RepoSources/coverage/windows_x86_64.json"
);
}
const PAGES_FIXTURE: &str = r#"{
"metadata": {
"platform": "macos",
"arch": "arm64",
"generated_at": "2026-07-06T12:00:00Z",
"tool_count": 2
},
"tools": {
"jq": {
"version": "1.8.2",
"status": "built",
"source": "source_build",
"updated_at": "2026-07-06T11:58:00Z"
},
"cmake": {
"version": "4.0.3",
"status": "net_fallback",
"source": "brew_emulate",
"updated_at": "2026-07-05T09:00:00Z"
}
}
}"#;
#[test]
fn coverage_map_parses_pages_fixture() {
let map = parse_coverage_map(PAGES_FIXTURE.as_bytes()).unwrap();
assert_eq!(map.tools.len(), 2);
let jq = &map.tools["jq"];
assert_eq!(jq.version, "1.8.2");
assert_eq!(jq.status, "built");
assert_eq!(jq.source, "source_build");
assert_eq!(jq.updated_at, "2026-07-06T11:58:00Z");
let cmake = &map.tools["cmake"];
assert_eq!(cmake.status, "net_fallback");
assert_eq!(map.metadata["platform"], "macos");
assert_eq!(map.metadata["tool_count"], 2);
}
#[test]
fn coverage_map_is_permissive() {
let map = parse_coverage_map(
br#"{"tools":{"git":{"version":"2.55.0","status":"someday","extra":true}},"future_top_level":1}"#,
)
.unwrap();
assert_eq!(map.tools["git"].version, "2.55.0");
assert_eq!(map.tools["git"].status, "someday");
assert_eq!(map.tools["git"].source, "");
assert!(map.metadata.is_null());
let empty = parse_coverage_map(b"{}").unwrap();
assert!(empty.tools.is_empty());
assert!(parse_coverage_map(b"not json").is_err());
}
#[test]
fn now_rfc3339_parses_back() {
let ts = now_rfc3339();
let parsed =
chrono::DateTime::parse_from_rfc3339(&ts).expect("recorded_at must be valid RFC3339");
assert!(parsed.timestamp() > 1_700_000_000);
}
#[test]
fn seed_tools_shape() {
assert_eq!(SEED_TOOLS.len(), 15);
assert!(SEED_TOOLS.contains(&"protobuf"), "protoc's formula");
assert!(SEED_TOOLS.contains(&"openssl@3"));
let mut sorted: Vec<_> = SEED_TOOLS.to_vec();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), SEED_TOOLS.len());
}
}