use std::io::Read;
use std::sync::OnceLock;
use std::time::Duration;
use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait};
pub(crate) const MAX_DOWNLOAD_BYTES: u64 = 8 * 1024 * 1024 * 1024;
pub(crate) fn agent() -> &'static ureq::Agent {
static AGENT: OnceLock<ureq::Agent> = OnceLock::new();
AGENT.get_or_init(|| {
ureq::AgentBuilder::new()
.timeout_connect(Duration::from_secs(10))
.timeout_read(Duration::from_secs(30))
.timeout_write(Duration::from_secs(30))
.build()
})
}
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>>> {
Self::http_get_with(agent(), url)
}
fn http_get_with(agent: &ureq::Agent, url: &str) -> anyhow::Result<Option<Vec<u8>>> {
match agent.get(url).call() {
Ok(resp) => {
let mut buf = Vec::new();
resp.into_reader()
.take(MAX_DOWNLOAD_BYTES)
.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]
fn hung_upstream_times_out_instead_of_hanging() {
use std::io::Read as _;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let handle = std::thread::spawn(move || {
if let Ok((mut sock, _)) = listener.accept() {
let mut scratch = [0u8; 64];
let _ = sock.read(&mut scratch);
std::thread::sleep(Duration::from_secs(2));
}
});
let agent = ureq::AgentBuilder::new()
.timeout_connect(Duration::from_millis(500))
.timeout_read(Duration::from_millis(500))
.build();
let start = std::time::Instant::now();
let res = RustRegistryUpstream::http_get_with(&agent, &format!("http://{addr}/whatever"));
let elapsed = start.elapsed();
assert!(res.is_err(), "a hung upstream must surface an error, not Ok");
assert!(
elapsed < Duration::from_secs(5),
"GET must time out promptly (took {elapsed:?}), not block on the hung upstream"
);
let _ = handle.join();
}
#[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");
}
}