Codex Handbook
core-plugins/src/marketplace_add/source.rs 392 lines
use super::MarketplaceAddError;use crate::marketplace::validate_marketplace_root;use codex_plugin::validate_plugin_segment;use std::path::Path;use std::path::PathBuf;#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) enum MarketplaceSource {    Git {        url: String,        ref_name: Option<String>,    },    Local {        path: PathBuf,    },}pub(crate) fn parse_marketplace_source(    source: &str,    explicit_ref: Option<String>,) -> Result<MarketplaceSource, MarketplaceAddError> {    let source = source.trim();    if source.is_empty() {        return Err(MarketplaceAddError::InvalidRequest(            "marketplace source must not be empty".to_string(),        ));    }    let (base_source, parsed_ref) = split_source_ref(source);    let ref_name = explicit_ref.or(parsed_ref);    if looks_like_local_path(&base_source) {        if ref_name.is_some() {            return Err(MarketplaceAddError::InvalidRequest(                "--ref is only supported for git marketplace sources".to_string(),            ));        }        let path = resolve_local_source_path(&base_source)?;        if path.is_file() {            return Err(MarketplaceAddError::InvalidRequest(                "local marketplace source must be a directory, not a file".to_string(),            ));        }        return Ok(MarketplaceSource::Local { path });    }    if is_ssh_git_url(&base_source) || is_git_url(&base_source) {        return Ok(MarketplaceSource::Git {            url: normalize_git_url(&base_source),            ref_name,        });    }    if looks_like_github_shorthand(&base_source) {        return Ok(MarketplaceSource::Git {            url: format!("https://github.com/{base_source}.git"),            ref_name,        });    }    Err(MarketplaceAddError::InvalidRequest(        "invalid marketplace source format; expected owner/repo, a git URL, or a local marketplace path"            .to_string(),    ))}pub(super) fn stage_marketplace_source<F>(    source: &MarketplaceSource,    sparse_paths: &[String],    staged_root: &Path,    clone_source: F,) -> Result<(), MarketplaceAddError>where    F: Fn(&str, Option<&str>, &[String], &Path) -> Result<(), MarketplaceAddError>,{    if !sparse_paths.is_empty() && !matches!(source, MarketplaceSource::Git { .. }) {        return Err(MarketplaceAddError::InvalidRequest(            "--sparse is only supported for git marketplace sources".to_string(),        ));    }    match source {        MarketplaceSource::Git { url, ref_name } => {            clone_source(url, ref_name.as_deref(), sparse_paths, staged_root)        }        MarketplaceSource::Local { .. } => unreachable!(            "local marketplace sources are added without staging a copied install root"        ),    }}pub(super) fn validate_marketplace_source_root(root: &Path) -> Result<String, MarketplaceAddError> {    let marketplace_name = validate_marketplace_root(root)        .map_err(|err| MarketplaceAddError::InvalidRequest(err.to_string()))?;    validate_plugin_segment(&marketplace_name, "marketplace name")        .map_err(MarketplaceAddError::InvalidRequest)?;    Ok(marketplace_name)}fn split_source_ref(source: &str) -> (String, Option<String>) {    if let Some((base, ref_name)) = source.rsplit_once('#') {        return (base.to_string(), non_empty_ref(ref_name));    }    if !source.contains("://")        && !is_ssh_git_url(source)        && let Some((base, ref_name)) = source.rsplit_once('@')    {        return (base.to_string(), non_empty_ref(ref_name));    }    (source.to_string(), None)}fn non_empty_ref(ref_name: &str) -> Option<String> {    let ref_name = ref_name.trim();    (!ref_name.is_empty()).then(|| ref_name.to_string())}fn normalize_git_url(url: &str) -> String {    let url = url.trim_end_matches('/');    if url.starts_with("https://github.com/") && !url.ends_with(".git") {        format!("{url}.git")    } else {        url.to_string()    }}fn looks_like_local_path(source: &str) -> bool {    Path::new(source).is_absolute()        || looks_like_windows_absolute_path(source)        || source.starts_with("./")        || source.starts_with(".\\")        || source.starts_with("../")        || source.starts_with("..\\")        || source.starts_with("~/")        || source == "."        || source == ".."}fn looks_like_windows_absolute_path(source: &str) -> bool {    let bytes = source.as_bytes();    bytes.len() >= 3        && bytes[0].is_ascii_alphabetic()        && bytes[1] == b':'        && matches!(bytes[2], b'\\' | b'/')        || source.starts_with(r"\\")}fn resolve_local_source_path(source: &str) -> Result<PathBuf, MarketplaceAddError> {    let path = expand_tilde_path(source);    let path = if path.is_absolute() {        path    } else {        std::env::current_dir()            .map_err(|err| {                MarketplaceAddError::Internal(format!(                    "failed to read current working directory for local marketplace source: {err}"                ))            })?            .join(path)    };    path.canonicalize().map_err(|err| {        MarketplaceAddError::InvalidRequest(format!(            "failed to resolve local marketplace source path: {err}"        ))    })}fn expand_tilde_path(source: &str) -> PathBuf {    let Some(rest) = source.strip_prefix("~/") else {        return PathBuf::from(source);    };    let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) else {        return PathBuf::from(source);    };    PathBuf::from(home).join(rest)}fn is_ssh_git_url(source: &str) -> bool {    source.starts_with("ssh://") || source.starts_with("git@") && source.contains(':')}fn is_git_url(source: &str) -> bool {    source.starts_with("http://") || source.starts_with("https://")}fn looks_like_github_shorthand(source: &str) -> bool {    let mut segments = source.split('/');    let owner = segments.next();    let repo = segments.next();    let extra = segments.next();    owner.is_some_and(is_github_shorthand_segment)        && repo.is_some_and(is_github_shorthand_segment)        && extra.is_none()}fn is_github_shorthand_segment(segment: &str) -> bool {    !segment.is_empty()        && segment            .chars()            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))}impl MarketplaceSource {    pub(super) fn display(&self) -> String {        match self {            Self::Git { url, ref_name } => match ref_name {                Some(ref_name) => format!("{url}#{ref_name}"),                None => url.clone(),            },            Self::Local { path } => path.display().to_string(),        }    }}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    use tempfile::TempDir;    #[test]    fn github_shorthand_parses_ref_suffix() {        assert_eq!(            parse_marketplace_source("owner/repo@main", /*explicit_ref*/ None).unwrap(),            MarketplaceSource::Git {                url: "https://github.com/owner/repo.git".to_string(),                ref_name: Some("main".to_string()),            }        );    }    #[test]    fn git_url_parses_fragment_ref() {        assert_eq!(            parse_marketplace_source(                "https://example.com/team/repo.git#v1",                /*explicit_ref*/ None            )            .unwrap(),            MarketplaceSource::Git {                url: "https://example.com/team/repo.git".to_string(),                ref_name: Some("v1".to_string()),            }        );    }    #[test]    fn explicit_ref_overrides_source_ref() {        assert_eq!(            parse_marketplace_source("owner/repo@main", Some("release".to_string())).unwrap(),            MarketplaceSource::Git {                url: "https://github.com/owner/repo.git".to_string(),                ref_name: Some("release".to_string()),            }        );    }    #[test]    fn github_shorthand_and_git_url_normalize_to_same_source() {        let shorthand = parse_marketplace_source("owner/repo", /*explicit_ref*/ None).unwrap();        let git_url = parse_marketplace_source(            "https://github.com/owner/repo.git",            /*explicit_ref*/ None,        )        .unwrap();        assert_eq!(shorthand, git_url);        assert_eq!(            shorthand,            MarketplaceSource::Git {                url: "https://github.com/owner/repo.git".to_string(),                ref_name: None,            }        );    }    #[test]    fn github_url_with_trailing_slash_normalizes_without_extra_path_segment() {        assert_eq!(            parse_marketplace_source("https://github.com/owner/repo/", /*explicit_ref*/ None)                .unwrap(),            MarketplaceSource::Git {                url: "https://github.com/owner/repo.git".to_string(),                ref_name: None,            }        );    }    #[test]    fn non_github_https_source_parses_as_git_url() {        assert_eq!(            parse_marketplace_source("https://gitlab.com/owner/repo", /*explicit_ref*/ None)                .unwrap(),            MarketplaceSource::Git {                url: "https://gitlab.com/owner/repo".to_string(),                ref_name: None,            }        );    }    #[test]    fn file_url_source_is_rejected() {        let err =            parse_marketplace_source("file:///tmp/marketplace.git", /*explicit_ref*/ None)                .unwrap_err();        assert!(            err.to_string()                .contains("invalid marketplace source format"),            "unexpected error: {err}"        );    }    #[test]    fn local_path_source_parses() {        let source = parse_marketplace_source(".", /*explicit_ref*/ None).unwrap();        let MarketplaceSource::Local { path } = source else {            panic!("expected local path source");        };        assert!(path.is_absolute());    }    #[test]    fn windows_absolute_paths_look_like_local_paths_on_every_host() {        assert!(looks_like_local_path(r"C:\Users\alice\marketplace"));        assert!(looks_like_local_path("C:/Users/alice/marketplace"));        assert!(looks_like_local_path(r"\\server\share\marketplace"));        assert!(!looks_like_local_path(r"C:relative\path"));    }    #[test]    fn local_file_source_is_rejected() {        let tempdir = TempDir::new().unwrap();        let file = tempdir.path().join("marketplace.json");        std::fs::write(&file, "{}").unwrap();        let err =            parse_marketplace_source(file.to_str().unwrap(), /*explicit_ref*/ None).unwrap_err();        assert!(            err.to_string()                .contains("local marketplace source must be a directory, not a file"),            "unexpected error: {err}"        );    }    #[test]    fn non_git_sources_reject_ref_override() {        let err = parse_marketplace_source("./marketplace", Some("main".to_string())).unwrap_err();        assert!(            err.to_string()                .contains("--ref is only supported for git marketplace sources"),            "unexpected error: {err}"        );    }    #[test]    fn non_git_sources_reject_sparse_checkout() {        let path = std::env::current_dir().unwrap();        let err = stage_marketplace_source(            &MarketplaceSource::Local { path },            &["plugins/foo".to_string()],            Path::new("/tmp"),            |_url, _ref_name, _sparse_paths, _staged_root| Ok(()),        )        .unwrap_err();        assert!(            err.to_string()                .contains("--sparse is only supported for git marketplace sources"),            "unexpected error: {err}"        );    }    #[test]    fn ssh_url_parses_as_git_url() {        assert_eq!(            parse_marketplace_source(                "ssh://git@github.com/owner/repo.git#main",                /*explicit_ref*/ None,            )            .unwrap(),            MarketplaceSource::Git {                url: "ssh://git@github.com/owner/repo.git".to_string(),                ref_name: Some("main".to_string()),            }        );    }}