cargo-overlay-registry 0.1.5

A local Cargo registry proxy that overlays local crates on top of crates.io
Documentation
use std::path::PathBuf;

use super::{AnyRegistry, LocalRegistry, OverlayRegistry, RemoteRegistry};

/// Registry specification for building overlay registries
#[derive(Debug, Clone)]
pub enum RegistrySpec {
    /// Local filesystem registry
    /// Path is optional, defaults to a temporary directory
    Local { path: Option<PathBuf> },
    /// Remote registry (read-only)
    Remote { api_url: String, index_url: String },
}

impl std::fmt::Display for RegistrySpec {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RegistrySpec::Local { path: None } => write!(f, "local"),
            RegistrySpec::Local { path: Some(p) } => write!(f, "local={}", p.display()),
            RegistrySpec::Remote { api_url, index_url } if api_url == index_url => {
                write!(f, "remote={}", api_url)
            }
            RegistrySpec::Remote { api_url, index_url }
                if api_url == "https://crates.io" && index_url == "https://index.crates.io" =>
            {
                write!(f, "crates.io")
            }
            RegistrySpec::Remote { api_url, index_url } => {
                write!(f, "remote={},{}", api_url, index_url)
            }
        }
    }
}

impl RegistrySpec {
    /// Shortcut for crates.io remote registry
    pub fn crates_io() -> Self {
        RegistrySpec::Remote {
            api_url: "https://crates.io".to_string(),
            index_url: "https://index.crates.io".to_string(),
        }
    }

    /// Create a local registry spec with a specific path
    pub fn local(path: impl Into<PathBuf>) -> Self {
        RegistrySpec::Local {
            path: Some(path.into()),
        }
    }

    /// Create a local registry spec that will use a temporary directory
    pub fn local_temp() -> Self {
        RegistrySpec::Local { path: None }
    }

    /// Create a remote registry spec
    pub fn remote(api_url: impl Into<String>, index_url: impl Into<String>) -> Self {
        RegistrySpec::Remote {
            api_url: api_url.into(),
            index_url: index_url.into(),
        }
    }
}

impl std::str::FromStr for RegistrySpec {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Handle shortcuts
        if s == "crates.io" || s == "crates-io" {
            return Ok(RegistrySpec::crates_io());
        }

        // Parse type=value format
        let (reg_type, value) = if let Some(idx) = s.find('=') {
            (&s[..idx], Some(&s[idx + 1..]))
        } else {
            (s, None)
        };

        match reg_type {
            "local" => {
                let path = value.map(PathBuf::from);
                Ok(RegistrySpec::Local { path })
            }
            "remote" => {
                let value = value.ok_or("remote registry requires a URL")?;
                let (api_url, index_url) = if let Some(idx) = value.find(',') {
                    (value[..idx].to_string(), value[idx + 1..].to_string())
                } else {
                    // If only one URL, use it for both API and index
                    (value.to_string(), value.to_string())
                };
                Ok(RegistrySpec::Remote { api_url, index_url })
            }
            _ => Err(format!(
                "unknown registry type '{}'. Use: local, remote, or crates.io",
                reg_type
            )),
        }
    }
}

/// Options for building a registry from specs
#[derive(Debug, Clone, Default)]
pub struct RegistryBuildOptions {
    /// Skip metadata validation on the topmost local registry
    pub permissive_publishing: bool,
    /// Make all registries read-only (no publishing)
    pub read_only: bool,
}

/// Result of building a registry from specs
pub struct BuiltRegistry {
    /// The constructed overlay registry
    pub registry: AnyRegistry,
    /// Upstream hosts extracted from remote registries (for MITM interception)
    pub upstream_hosts: Vec<String>,
    /// Temporary directories created for local registries (must be kept alive)
    pub temp_dirs: Vec<tempfile::TempDir>,
}

impl BuiltRegistry {
    /// Get the upstream API URL (from the bottom-most remote registry)
    pub fn upstream_api(&self, specs: &[RegistrySpec]) -> String {
        for spec in specs.iter().rev() {
            if let RegistrySpec::Remote { api_url, .. } = spec {
                return api_url.clone();
            }
        }
        "https://crates.io".to_string()
    }
}

/// Build an overlay registry from a list of specs (first = top, last = bottom)
pub fn build_registry(specs: &[RegistrySpec], options: &RegistryBuildOptions) -> BuiltRegistry {
    let mut upstream_hosts = Vec::new();
    let mut temp_dirs = Vec::new();

    // The topmost (first) registry is writable (if not read_only)
    let writable_idx = if options.read_only { None } else { Some(0) };

    // Build from bottom to top
    let mut registry: Option<AnyRegistry> = None;

    for (idx, spec) in specs.iter().enumerate().rev() {
        let layer: AnyRegistry = match spec {
            RegistrySpec::Local { path } => {
                let path = path.clone().unwrap_or_else(|| {
                    let temp_dir =
                        tempfile::tempdir().expect("Failed to create temporary directory");
                    let path = temp_dir.path().to_path_buf();
                    temp_dirs.push(temp_dir);
                    path
                });

                // Create index directory
                std::fs::create_dir_all(path.join("index")).ok();

                // Only the designated registry is writable
                if writable_idx == Some(idx) {
                    let validate = !options.permissive_publishing;
                    AnyRegistry::new(LocalRegistry::new(path, validate))
                } else {
                    AnyRegistry::new(LocalRegistry::read_only(path))
                }
            }
            RegistrySpec::Remote { api_url, index_url } => {
                // Extract hosts for MITM interception
                if let Ok(url) = url::Url::parse(api_url)
                    && let Some(host) = url.host_str()
                    && !upstream_hosts.contains(&host.to_string())
                {
                    upstream_hosts.push(host.to_string());
                }
                if let Ok(url) = url::Url::parse(index_url)
                    && let Some(host) = url.host_str()
                    && !upstream_hosts.contains(&host.to_string())
                {
                    upstream_hosts.push(host.to_string());
                }
                // Remote registries can also be writable (forwarding publishes)
                if writable_idx == Some(idx) {
                    AnyRegistry::new(RemoteRegistry::writable(index_url.clone(), api_url.clone()))
                } else {
                    AnyRegistry::new(RemoteRegistry::new(index_url.clone(), api_url.clone()))
                }
            }
        };

        registry = Some(match registry {
            None => layer,
            Some(bottom) => AnyRegistry::new(OverlayRegistry::new(layer, bottom)),
        });
    }

    // Add static.crates.io if crates.io is in hosts
    if upstream_hosts.iter().any(|h| h.contains("crates.io"))
        && !upstream_hosts.contains(&"static.crates.io".to_string())
    {
        upstream_hosts.push("static.crates.io".to_string());
    }

    BuiltRegistry {
        registry: registry.expect("At least one registry must be specified"),
        upstream_hosts,
        temp_dirs,
    }
}