holger-server-lib 0.6.6

Holger server library: config, wiring, gRPC service, Rust API
//! Read-only **remote Cargo registry** upstream (default: crates.io).
//!
//! Used as a [`ProxyBackend`](crate::proxy::ProxyBackend) upstream so a writable
//! `/cache` primary pulls + caches crates on demand (a transparent pull-through
//! cache). HTTP is SYNC via `ureq` — runtime-agnostic, so it is safe both inside
//! holger's async hyper handler (where blocking on `reqwest` would panic) and in
//! plain unit tests.

use std::io::Read;

use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait};

/// A read-only proxy to a remote sparse Cargo registry.
pub struct RustRegistryUpstream {
    name: String,
    /// Sparse index base, e.g. `https://index.crates.io`.
    index_base: String,
    /// Crate download base, e.g. `https://static.crates.io/crates`.
    dl_base: String,
}

impl RustRegistryUpstream {
    /// crates.io defaults: the sparse index + the static CDN.
    pub fn crates_io(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            index_base: "https://index.crates.io".to_string(),
            dl_base: "https://static.crates.io/crates".to_string(),
        }
    }

    /// Point at an arbitrary sparse registry (for tests / private mirrors).
    pub fn new(
        name: impl Into<String>,
        index_base: impl Into<String>,
        dl_base: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            index_base: index_base.into(),
            dl_base: dl_base.into(),
        }
    }

    /// Download URL for one `.crate` (the cargo `dl` convention).
    fn crate_url(&self, name: &str, version: &str) -> String {
        format!("{}/{name}/{name}-{version}.crate", self.dl_base)
    }

    /// Blocking GET. `Ok(None)` on 404, `Ok(Some(bytes))` on 200, `Err` otherwise.
    fn http_get(url: &str) -> anyhow::Result<Option<Vec<u8>>> {
        match ureq::get(url).call() {
            Ok(resp) => {
                let mut buf = Vec::new();
                resp.into_reader()
                    .read_to_end(&mut buf)
                    .map_err(|e| anyhow::anyhow!("read upstream body {url}: {e}"))?;
                Ok(Some(buf))
            }
            Err(ureq::Error::Status(404, _)) => Ok(None),
            Err(ureq::Error::Status(code, _)) => anyhow::bail!("upstream {url}: HTTP {code}"),
            Err(e) => Err(anyhow::anyhow!("upstream {url}: {e}")),
        }
    }
}

impl RepositoryBackendTrait for RustRegistryUpstream {
    fn name(&self) -> &str {
        &self.name
    }

    fn format(&self) -> ArtifactFormat {
        ArtifactFormat::Rust
    }

    fn is_writable(&self) -> bool {
        false
    }

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
        Self::http_get(&self.crate_url(&id.name, &id.version))
    }

    fn put(&self, _id: &ArtifactId, _data: &[u8]) -> anyhow::Result<()> {
        anyhow::bail!("remote registry upstream is read-only")
    }

    fn handle_http2_request(
        &self,
        _method: &str,
        suburl: &str,
        _body: &[u8],
    ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
        let parts: Vec<&str> = suburl.trim_start_matches('/').split('/').collect();
        match parts.as_slice() {
            // Sparse index metadata: /{repo}/index/{path...}. cargo has already
            // encoded the correct prefix (1/2/3-char rules), so forward verbatim.
            [_repo, "index", rest @ ..] if !rest.is_empty() && rest != ["config.json"] => {
                let url = format!("{}/{}", self.index_base, rest.join("/"));
                match Self::http_get(&url)? {
                    Some(body) => Ok((
                        200,
                        vec![("Content-Type".into(), "text/plain".into())],
                        body,
                    )),
                    None => Ok((404, Vec::new(), b"Not found upstream".to_vec())),
                }
            }
            // Crate download: /{repo}/crates/{name}/{version}/download.
            [_repo, "crates", name, version, "download"] => {
                match Self::http_get(&self.crate_url(name, version))? {
                    Some(body) => Ok((
                        200,
                        vec![("Content-Type".into(), "application/octet-stream".into())],
                        body,
                    )),
                    None => Ok((404, Vec::new(), b"Crate not found upstream".to_vec())),
                }
            }
            _ => Ok((404, Vec::new(), b"Not found".to_vec())),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn crates_io_defaults_build_expected_urls() {
        let up = RustRegistryUpstream::crates_io("crates-io");
        assert_eq!(up.name(), "crates-io");
        assert!(!up.is_writable());
        assert_eq!(
            up.crate_url("serde", "1.0.0"),
            "https://static.crates.io/crates/serde/serde-1.0.0.crate"
        );
    }

    #[test]
    fn custom_base_urls_are_honored() {
        let up = RustRegistryUpstream::new("mirror", "https://idx.example", "https://dl.example/c");
        assert_eq!(
            up.crate_url("tokio", "1.40.0"),
            "https://dl.example/c/tokio/tokio-1.40.0.crate"
        );
    }

    #[test]
    fn put_is_rejected_read_only() {
        let up = RustRegistryUpstream::crates_io("crates-io");
        let id = ArtifactId { namespace: None, name: "x".into(), version: "1".into() };
        assert!(up.put(&id, b"data").is_err(), "upstream must be read-only");
    }

    /// LIVE network test — hits real crates.io. Ignored by default; run with
    /// `cargo test -p holger-server-lib --lib upstream_rust -- --ignored`.
    #[test]
    #[ignore]
    fn live_fetch_serde_from_crates_io() {
        let up = RustRegistryUpstream::crates_io("crates-io");
        let id = ArtifactId { namespace: None, name: "serde".into(), version: "1.0.0".into() };
        let got = up.fetch(&id).unwrap();
        assert!(got.is_some(), "crates.io must serve serde 1.0.0");
        assert!(got.unwrap().len() > 1000, "a real .crate is non-trivial in size");
    }
}