use std::convert::Infallible;
use std::env;
use std::io::Read;
use anyhow::anyhow;
use anyhow::Context as _;
use anyhow::Error;
use anyhow::Result;
use http::StatusCode;
use url::Url;
use crate::log::debug;
use crate::log::warn;
use crate::util::split_env_var_contents;
use crate::BuildId;
use crate::HttpClient;
use crate::HttpClientError;
use crate::Readable;
#[derive(Debug)]
pub struct Response<'url, R> {
pub data: R,
pub server_url: &'url str,
}
impl<'url, R: Read> Response<'url, R> {
fn new(data: R, server_url: &'url str) -> Self {
Self { data, server_url }
}
}
#[derive(Debug)]
pub struct Client {
base_urls: Vec<Url>,
client: Box<dyn HttpClient + Send + Sync>,
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn fetch_debug_info(
&self,
build_id: &BuildId,
) -> Result<Option<Response<'_, impl Readable>>> {
fn status_to_error(status: StatusCode) -> Error {
let reason = status
.canonical_reason()
.map(|reason| format!(" ({reason})"))
.unwrap_or_default();
anyhow!("request failed with HTTP status {status}{reason}")
}
let build_id = build_id.format();
let mut issue_err = None;
let mut server_err = None;
for base_url in &self.base_urls {
let mut url = base_url.clone();
let () = url.set_path(&format!("buildid/{build_id}/debuginfo"));
debug!("making GET request to {url}");
let result = self.client.get(url.as_str());
match result {
Ok(response) => return Ok(Some(Response::new(response, base_url.as_str()))),
Err(HttpClientError::StatusCode(StatusCode::NOT_FOUND)) => continue,
Err(HttpClientError::StatusCode(s)) => {
warn!(
"failed to retrieve debug info from `{url}`{}",
s.canonical_reason()
.map(|s| format!(" {s}"))
.unwrap_or_default()
);
server_err = server_err.or_else(|| Some(status_to_error(s)));
continue
},
Err(err) => {
warn!("failed to issue GET request `{url}`: {err}");
let err = Err::<Infallible, _>(err)
.with_context(|| format!("failed to issue request to `{url}`"))
.unwrap_err();
issue_err = issue_err.or_else(|| Some(err));
continue
},
};
}
if let Some(err) = server_err.or(issue_err) {
Err(err).with_context(|| format!("failed to fetch debug info for build ID `{build_id}`"))
} else {
Ok(None)
}
}
}
#[derive(Debug, Default)]
pub struct ClientBuilder<C = ()> {
client: C,
}
impl ClientBuilder<()> {
pub fn http_client<C>(self, client: C) -> ClientBuilder<C>
where
C: HttpClient + 'static,
{
ClientBuilder { client }
}
}
impl<C> ClientBuilder<C>
where
C: HttpClient + Send + Sync + 'static,
{
pub fn build<'url, U>(self, base_urls: U) -> Result<Option<Client>>
where
U: IntoIterator<Item = &'url str>,
{
let base_urls = base_urls
.into_iter()
.map(|url| Url::parse(url.trim()).with_context(|| format!("failed to parse URL `{url}`")))
.collect::<Result<Vec<_>>>()?;
if base_urls.is_empty() {
return Ok(None);
}
debug!("using debuginfod URLs: {base_urls:#?}");
let slf = Client {
base_urls,
client: Box::new(self.client),
};
Ok(Some(slf))
}
pub fn build_from_env(self) -> Result<Option<Client>> {
let urls_str = if let Some(urls_str) = env::var_os("DEBUGINFOD_URLS") {
urls_str
} else {
return Ok(None);
};
let urls_str = urls_str
.to_str()
.context("DEBUGINFOD_URLS does not contain valid Unicode")?;
let urls = split_env_var_contents(urls_str);
self.build(urls)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;
use std::fmt::Debug;
use std::io::copy;
use std::io::Error as IoError;
use std::io::ErrorKind;
use blazesym::symbolize::source::Elf;
use blazesym::symbolize::source::Source;
use blazesym::symbolize::Input;
use blazesym::symbolize::Symbolizer;
use reqwest::blocking::Client as ReqwestBlockingClient;
use tempfile::NamedTempFile;
use test_fork::fork;
use crate::Readable;
#[test]
fn no_valid_urls() {
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build([])
.unwrap();
assert!(client.is_none());
let _err = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build(["!#&*(@&!"])
.unwrap_err();
}
#[fork]
#[test]
fn from_env_creation() {
let () = unsafe { env::remove_var("DEBUGINFOD_URLS") };
let result = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build_from_env()
.unwrap();
assert!(result.is_none(), "{result:?}");
let urls = "https://debug.infod https://de.bug.info.d";
let () = unsafe { env::set_var("DEBUGINFOD_URLS", urls) };
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build_from_env()
.unwrap()
.unwrap();
assert_eq!(client.base_urls.len(), 2);
}
#[test]
fn fetch_debug_info() {
let urls = ["https://debuginfod.fedoraproject.org/"];
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build(urls)
.unwrap()
.unwrap();
let build_ids = vec![
BuildId::RawBytes(Cow::Borrowed(&[
0xae, 0xb9, 0xa9, 0x83, 0xac, 0xe1, 0xfb, 0x04, 0x7b, 0x23, 0x41, 0xb1, 0x95, 0x01, 0x65,
0x44, 0x0f, 0xb2, 0xa8, 0xb9,
])),
BuildId::Formatted("aeb9a983ace1fb047b2341b1950165440fb2a8b9".into()),
];
for build_id in build_ids {
let mut response = client.fetch_debug_info(&build_id).unwrap().unwrap();
assert_eq!(response.server_url, "https://debuginfod.fedoraproject.org/");
let mut file = NamedTempFile::new().unwrap();
let bytes = copy(&mut response.data, &mut file).unwrap();
assert_eq!(bytes, 112216);
let symbolizer = Symbolizer::new();
let src = Source::from(Elf::new(file.path()));
let sym = symbolizer
.symbolize_single(&src, Input::VirtOffset(0x2d70))
.unwrap()
.into_sym()
.unwrap();
assert_eq!(sym.name, "usage");
}
}
#[test]
fn fetch_debug_info_not_found() {
let urls = ["https://debuginfod.fedoraproject.org/"];
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build(urls)
.unwrap()
.unwrap();
let build_id = BuildId::RawBytes(Cow::Borrowed(&[0x00]));
let info = client.fetch_debug_info(&build_id).unwrap();
assert!(info.is_none());
}
#[derive(Debug)]
struct DummyHttpClient(fn(&str) -> Result<Box<dyn Readable>, HttpClientError>);
impl HttpClient for DummyHttpClient {
fn get(&self, url: &str) -> Result<Box<dyn Readable>, HttpClientError> {
(self.0)(url)
}
}
#[test]
fn custom_http_client_generic_error() {
let urls = ["https://debuginfod.fedoraproject.org/"];
let http_client = DummyHttpClient(|url| {
Err(HttpClientError::Other(Box::new(IoError::new(
ErrorKind::Other,
format!("DummyHttpClient cannot fetch {url}"),
))))
});
let client = Client::builder()
.http_client(http_client)
.build(urls)
.unwrap()
.unwrap();
let build_id = BuildId::RawBytes(Cow::Borrowed(&[0x00]));
let err = client.fetch_debug_info(&build_id).unwrap_err();
assert!(err
.root_cause()
.to_string()
.contains("DummyHttpClient cannot fetch"));
}
#[test]
fn custom_http_client_status_code_404() {
let urls = ["https://debuginfod.fedoraproject.org/"];
let http_client =
DummyHttpClient(|_url| Err(HttpClientError::StatusCode(StatusCode::NOT_FOUND)));
let client = Client::builder()
.http_client(http_client)
.build(urls)
.unwrap()
.unwrap();
let build_id = BuildId::RawBytes(Cow::Borrowed(&[0x00]));
let res = client.fetch_debug_info(&build_id);
assert!(res.unwrap().is_none());
}
#[test]
fn custom_http_client_found_second() {
let urls = [
"https://debuginfod.fedoraproject.org/",
"https://debuginfod.archlinux.org/",
];
let http_client = DummyHttpClient(|url: &str| {
if url.contains("debuginfod.archlinux.org") {
let data: &[u8] = b"Debug info!";
return Ok(Box::new(data));
}
Err(HttpClientError::StatusCode(StatusCode::NOT_FOUND))
});
let client = Client::builder()
.http_client(http_client)
.build(urls)
.unwrap()
.unwrap();
let build_id = BuildId::RawBytes(Cow::Borrowed(&[0x00]));
let mut info = client.fetch_debug_info(&build_id).unwrap().unwrap();
assert_eq!(info.server_url, "https://debuginfod.archlinux.org/");
let mut buf = String::new();
info.data.read_to_string(&mut buf).unwrap();
assert_eq!(buf, "Debug info!");
}
#[test]
fn custom_http_client_status_code_other() {
let urls = ["https://debuginfod.fedoraproject.org/"];
let http_client =
DummyHttpClient(|_url| Err(HttpClientError::StatusCode(StatusCode::IM_A_TEAPOT)));
let client = Client::builder()
.http_client(http_client)
.build(urls)
.unwrap()
.unwrap();
let build_id = BuildId::RawBytes(Cow::Borrowed(&[0x00]));
let err = client.fetch_debug_info(&build_id).unwrap_err();
assert!(err
.root_cause()
.to_string()
.contains("request failed with HTTP status 418"));
}
}