use std::io::Read;
use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait};
pub struct RustRegistryUpstream {
name: String,
index_base: String,
dl_base: String,
}
impl RustRegistryUpstream {
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(),
}
}
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(),
}
}
fn crate_url(&self, name: &str, version: &str) -> String {
format!("{}/{name}/{name}-{version}.crate", self.dl_base)
}
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() {
[_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())),
}
}
[_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");
}
#[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");
}
}