Codex Handbook
core-plugins/src/marketplace_add/metadata.rs 315 lines
use super::MarketplaceAddError;use super::source::MarketplaceSource;use crate::installed_marketplaces::resolve_configured_marketplace_root;use crate::marketplace::validate_marketplace_root;use codex_config::CONFIG_TOML_FILE;use codex_config::MarketplaceConfigUpdate;use codex_config::record_user_marketplace;use std::fs;use std::io::ErrorKind;use std::path::Path;use std::path::PathBuf;use std::time::SystemTime;use std::time::UNIX_EPOCH;#[derive(Debug, Clone, PartialEq, Eq)]pub(super) struct MarketplaceInstallMetadata {    source: InstalledMarketplaceSource,}#[derive(Debug, Clone, PartialEq, Eq)]enum InstalledMarketplaceSource {    Git {        url: String,        ref_name: Option<String>,        sparse_paths: Vec<String>,    },    Local {        path: String,    },}pub(super) fn record_added_marketplace_entry(    codex_home: &Path,    marketplace_name: &str,    install_metadata: &MarketplaceInstallMetadata,) -> Result<(), MarketplaceAddError> {    let source = install_metadata.config_source();    let timestamp = utc_timestamp_now()?;    let update = MarketplaceConfigUpdate {        last_updated: &timestamp,        last_revision: None,        source_type: install_metadata.config_source_type(),        source: &source,        ref_name: install_metadata.ref_name(),        sparse_paths: install_metadata.sparse_paths(),    };    record_user_marketplace(codex_home, marketplace_name, &update).map_err(|err| {        MarketplaceAddError::Internal(format!(            "failed to add marketplace '{marketplace_name}' to user config.toml: {err}"        ))    })}pub(super) fn installed_marketplace_root_for_source(    codex_home: &Path,    install_root: &Path,    install_metadata: &MarketplaceInstallMetadata,) -> Result<Option<PathBuf>, MarketplaceAddError> {    let config_path = codex_home.join(CONFIG_TOML_FILE);    let config = match fs::read_to_string(&config_path) {        Ok(config) => config,        Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),        Err(err) => {            return Err(MarketplaceAddError::Internal(format!(                "failed to read user config {}: {err}",                config_path.display()            )));        }    };    let config: toml::Value = toml::from_str(&config).map_err(|err| {        MarketplaceAddError::Internal(format!(            "failed to parse user config {}: {err}",            config_path.display()        ))    })?;    let Some(marketplaces) = config.get("marketplaces").and_then(toml::Value::as_table) else {        return Ok(None);    };    for (marketplace_name, marketplace) in marketplaces {        if !install_metadata.matches_config(marketplace) {            continue;        }        let Some(root) =            resolve_configured_marketplace_root(marketplace_name, marketplace, install_root)        else {            continue;        };        if validate_marketplace_root(&root).is_ok() {            return Ok(Some(root));        }    }    Ok(None)}pub(super) fn find_marketplace_root_by_name(    codex_home: &Path,    install_root: &Path,    marketplace_name: &str,) -> Result<Option<PathBuf>, MarketplaceAddError> {    let config_path = codex_home.join(CONFIG_TOML_FILE);    let config = match fs::read_to_string(&config_path) {        Ok(config) => config,        Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),        Err(err) => {            return Err(MarketplaceAddError::Internal(format!(                "failed to read user config {}: {err}",                config_path.display()            )));        }    };    let config: toml::Value = toml::from_str(&config).map_err(|err| {        MarketplaceAddError::Internal(format!(            "failed to parse user config {}: {err}",            config_path.display()        ))    })?;    let Some(marketplace) = config        .get("marketplaces")        .and_then(toml::Value::as_table)        .and_then(|marketplaces| marketplaces.get(marketplace_name))    else {        return Ok(None);    };    let Some(root) =        resolve_configured_marketplace_root(marketplace_name, marketplace, install_root)    else {        return Ok(None);    };    if validate_marketplace_root(&root).is_ok() {        Ok(Some(root))    } else {        Ok(None)    }}impl MarketplaceInstallMetadata {    pub(super) fn from_source(source: &MarketplaceSource, sparse_paths: &[String]) -> Self {        let source = match source {            MarketplaceSource::Git { url, ref_name } => InstalledMarketplaceSource::Git {                url: url.clone(),                ref_name: ref_name.clone(),                sparse_paths: sparse_paths.to_vec(),            },            MarketplaceSource::Local { path } => InstalledMarketplaceSource::Local {                path: path.display().to_string(),            },        };        Self { source }    }    fn config_source_type(&self) -> &'static str {        match &self.source {            InstalledMarketplaceSource::Git { .. } => "git",            InstalledMarketplaceSource::Local { .. } => "local",        }    }    fn config_source(&self) -> String {        match &self.source {            InstalledMarketplaceSource::Git { url, .. } => url.clone(),            InstalledMarketplaceSource::Local { path } => path.clone(),        }    }    fn ref_name(&self) -> Option<&str> {        match &self.source {            InstalledMarketplaceSource::Git { ref_name, .. } => ref_name.as_deref(),            InstalledMarketplaceSource::Local { .. } => None,        }    }    fn sparse_paths(&self) -> &[String] {        match &self.source {            InstalledMarketplaceSource::Git { sparse_paths, .. } => sparse_paths,            InstalledMarketplaceSource::Local { .. } => &[],        }    }    fn matches_config(&self, marketplace: &toml::Value) -> bool {        marketplace.get("source_type").and_then(toml::Value::as_str)            == Some(self.config_source_type())            && marketplace.get("source").and_then(toml::Value::as_str)                == Some(self.config_source().as_str())            && marketplace.get("ref").and_then(toml::Value::as_str) == self.ref_name()            && config_sparse_paths(marketplace) == self.sparse_paths()    }}fn config_sparse_paths(marketplace: &toml::Value) -> Vec<String> {    marketplace        .get("sparse_paths")        .and_then(toml::Value::as_array)        .map(|paths| {            paths                .iter()                .filter_map(toml::Value::as_str)                .map(str::to_string)                .collect()        })        .unwrap_or_default()}fn utc_timestamp_now() -> Result<String, MarketplaceAddError> {    let duration = SystemTime::now()        .duration_since(UNIX_EPOCH)        .map_err(|err| {            MarketplaceAddError::Internal(format!("system clock is before Unix epoch: {err}"))        })?;    Ok(format_utc_timestamp(duration.as_secs() as i64))}fn format_utc_timestamp(seconds_since_epoch: i64) -> String {    const SECONDS_PER_DAY: i64 = 86_400;    let days = seconds_since_epoch.div_euclid(SECONDS_PER_DAY);    let seconds_of_day = seconds_since_epoch.rem_euclid(SECONDS_PER_DAY);    let (year, month, day) = civil_from_days(days);    let hour = seconds_of_day / 3_600;    let minute = (seconds_of_day % 3_600) / 60;    let second = seconds_of_day % 60;    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")}fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) {    let days = days_since_epoch + 719_468;    let era = if days >= 0 { days } else { days - 146_096 } / 146_097;    let day_of_era = days - era * 146_097;    let year_of_era =        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;    let mut year = year_of_era + era * 400;    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);    let month_prime = (5 * day_of_year + 2) / 153;    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;    let month = month_prime + if month_prime < 10 { 3 } else { -9 };    year += if month <= 2 { 1 } else { 0 };    (year, month, day)}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    use tempfile::TempDir;    #[test]    fn utc_timestamp_formats_unix_epoch_as_rfc3339_utc() {        assert_eq!(            format_utc_timestamp(/*seconds_since_epoch*/ 0),            "1970-01-01T00:00:00Z"        );        assert_eq!(            format_utc_timestamp(/*seconds_since_epoch*/ 1_775_779_200),            "2026-04-10T00:00:00Z"        );    }    #[test]    fn installed_marketplace_root_for_source_propagates_config_read_errors() {        let codex_home = TempDir::new().unwrap();        let config_path = codex_home.path().join(CONFIG_TOML_FILE);        fs::create_dir(&config_path).unwrap();        let install_root = codex_home.path().join("marketplaces");        let source = MarketplaceSource::Git {            url: "https://github.com/owner/repo.git".to_string(),            ref_name: None,        };        let install_metadata = MarketplaceInstallMetadata::from_source(&source, &[]);        let err = installed_marketplace_root_for_source(            codex_home.path(),            &install_root,            &install_metadata,        )        .unwrap_err();        assert!(            err.to_string().contains(&format!(                "failed to read user config {}:",                config_path.display()            )),            "unexpected error: {err}"        );    }    #[test]    fn installed_marketplace_root_for_source_uses_local_source_root() {        let codex_home = TempDir::new().unwrap();        let install_root = codex_home.path().join("marketplaces");        let source_root = codex_home.path().join("source");        fs::create_dir_all(source_root.join(".agents/plugins")).unwrap();        fs::write(            source_root.join(".agents/plugins/marketplace.json"),            r#"{"name":"debug","plugins":[]}"#,        )        .unwrap();        let source = MarketplaceSource::Local {            path: source_root.clone(),        };        let install_metadata = MarketplaceInstallMetadata::from_source(&source, &[]);        record_added_marketplace_entry(codex_home.path(), "debug", &install_metadata).unwrap();        let root = installed_marketplace_root_for_source(            codex_home.path(),            &install_root,            &install_metadata,        )        .unwrap();        assert_eq!(root, Some(source_root));    }}