mannequin/src/nexus/prod.rs
use super::NexusAsset;
use std::collections::HashMap;
use std::sync::Mutex;
/// Prod repo: pre-loaded with a few well-known crates. Read-only.
pub struct ProdRepo {
assets: Mutex<HashMap<String, Vec<u8>>>,
base_url: String,
http_client: reqwest::Client,
}
struct SeedCrate {
name: &'static str,
version: &'static str,
}
const SEED_CRATES: &[SeedCrate] = &[
SeedCrate {
name: "serde",
version: "1.0.200",
},
SeedCrate {
name: "anyhow",
version: "1.0.86",
},
SeedCrate {
name: "log",
version: "0.4.21",
},
SeedCrate {
name: "rand",
version: "0.8.5",
},
];
impl ProdRepo {
pub fn new(base_url: &str) -> Self {
Self {
assets: Mutex::new(HashMap::new()),
base_url: base_url.to_string(),
http_client: tls_roots(reqwest::Client::builder())
.build()
.expect("reqwest client"),
}
}
/// Seed the 4 default crates (fetched lazily on first download).
pub fn seed_defaults(&mut self) {
// We just register the paths; actual data is fetched lazily.
// Nothing to do here — download() handles lazy fetch.
}
pub fn seeded_paths() -> Vec<String> {
SEED_CRATES
.iter()
.map(|c| format!("crates/{}/{}-{}.crate", c.name, c.name, c.version))
.collect()
}
pub fn list_assets(&self) -> Vec<NexusAsset> {
let cached: Vec<_> = self.assets.lock().unwrap().keys().cloned().collect();
let mut all: Vec<_> = Self::seeded_paths().into_iter().collect();
for path in cached {
if !all.contains(&path) {
all.push(path);
}
}
all.into_iter()
.map(|path| NexusAsset {
download_url: format!("{}/repository/prod/{}", self.base_url, path),
content_type: Some("application/octet-stream".into()),
size: None,
path,
})
.collect()
}
/// Async (M1): previously this used `block_in_place` + `Handle::block_on`,
/// which **panics** on a current-thread runtime (in-process integration
/// tests) and otherwise parks a worker thread for the whole fetch. Awaiting
/// the fetch directly — like `ProxyRepo::download` — is correct on any
/// runtime flavor.
pub async fn download(&self, path: &str) -> Option<Vec<u8>> {
// Check if already fetched
if let Some(data) = self.assets.lock().unwrap().get(path) {
return Some(data.clone());
}
// Check if it's a seeded path — if so, lazily fetch it.
if Self::seeded_paths().contains(&path.to_string()) {
let filename = path.rsplit('/').next()?;
let stem = filename.strip_suffix(".crate")?;
let crate_name = stem.split_once('-').map(|(n, _)| n).unwrap_or(stem);
let url = format!(
"https://static.crates.io/crates/{}/{}.crate",
crate_name, stem
);
tracing::info!("prod: lazy-fetching {}", url);
let resp = self.http_client.get(&url).send().await.ok()?;
if !resp.status().is_success() {
return None;
}
let data = resp.bytes().await.ok().map(|b| b.to_vec())?;
tracing::info!("prod: cached {} ({} bytes)", path, data.len());
self.assets
.lock()
.unwrap()
.insert(path.to_string(), data.clone());
return Some(data);
}
None
}
}
/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
let _ = rustls::crypto::ring::default_provider().install_default();
b.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|c| reqwest::Certificate::from_der(c).ok()),
)
}