use anyhow::{Context, Result};
use std::sync::Arc;
use std::sync::OnceLock;
use tokio::sync::Semaphore;
use crate::constants::STATIC_DIST_BASE_URL;
use crate::domain::RustRelease;
use crate::options::NetworkConfig;
static DATE_RE: OnceLock<regex_lite::Regex> = OnceLock::new();
fn date_regex() -> &'static regex_lite::Regex {
DATE_RE.get_or_init(|| {
regex_lite::Regex::new(r"(\d{4}-\d{2}-\d{2})")
.expect("DATE_RE regex pattern is statically valid")
})
}
pub(super) fn build_client(network: &NetworkConfig) -> Result<reqwest::Client> {
reqwest::Client::builder()
.user_agent(&network.user_agent)
.timeout(std::time::Duration::from_secs(network.timeout))
.build()
.context("Failed to build HTTP client")
}
pub(super) fn build_client_fallback(network: &NetworkConfig) -> reqwest::Client {
build_client(network).unwrap_or_else(|_| {
reqwest::Client::builder()
.user_agent(&network.user_agent)
.build()
.expect("reqwest default client builder should never fail")
})
}
pub(super) async fn probe_channel_history(
network: &NetworkConfig,
channel: &str,
days: u32,
) -> Result<Vec<RustRelease>> {
let client = Arc::new(build_client_fallback(network));
let semaphore = Arc::new(Semaphore::new(network.max_concurrency));
let today = chrono::Local::now().date_naive();
let mut handles = Vec::with_capacity(days as usize);
for i in 0..days {
let date = today - chrono::Duration::days(i64::from(i));
let date_str = date.format("%Y-%m-%d").to_string();
let client = client.clone();
let sem = semaphore.clone();
let channel = channel.to_string();
handles.push(tokio::spawn(async move {
let _permit = sem
.acquire()
.await
.expect("Semaphore is never closed during probe");
let url = format!("{STATIC_DIST_BASE_URL}/{date_str}/channel-rust-{channel}.toml");
match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => match resp.text().await {
Ok(text) => parse_channel_toml_version(&text, &channel, &date_str),
Err(_) => None,
},
_ => None,
}
}));
}
let total = handles.len();
let mut results = Vec::new();
let mut task_errors = 0u32;
for handle in handles {
match handle.await {
Ok(Some(r)) => results.push(r),
Ok(None) => {}
Err(_) => task_errors += 1,
}
}
if task_errors > 0 {
eprintln!(
"Warning: {}/{} probe tasks panicked or were cancelled. \
Results may be incomplete.",
task_errors, total
);
}
if results.is_empty() && task_errors > 0 && task_errors as usize == total {
anyhow::bail!(
"All {total} probe requests for '{channel}' channel failed — check network connectivity"
);
}
results.sort_by(|a, b| b.date.cmp(&a.date));
Ok(results)
}
fn parse_channel_toml_version(
text: &str,
channel: &str,
fallback_date: &str,
) -> Option<RustRelease> {
let marker = "[pkg.rust]";
let pos = text.find(marker)?;
let after = &text[pos + marker.len()..];
let version_prefix = r#"version = ""#;
let vpos = after.find(version_prefix)?;
let rest = &after[vpos + version_prefix.len()..];
let end = rest.find('"')?;
let version_str = &rest[..end];
let date = date_regex()
.captures(version_str)
.map_or_else(|| fallback_date.to_string(), |c| c[1].to_string());
Some(RustRelease {
version: version_str.to_string(),
date,
channel: channel.to_string(),
})
}