use std::env;
use std::fs::create_dir_all;
use std::fs::File;
use std::io::copy;
use std::ops::Deref;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context as _;
use anyhow::Result;
use dirs::cache_dir;
use dirs::home_dir;
use tempfile::NamedTempFile;
use crate::log::debug;
use crate::BuildId;
use crate::Client;
#[derive(Debug)]
pub struct CachingClient {
client: Client,
cache_dir: PathBuf,
}
impl CachingClient {
pub fn new<P>(client: Client, cache_dir: P) -> Result<Self>
where
P: AsRef<Path>,
{
let cache_dir = cache_dir.as_ref();
let () = create_dir_all(cache_dir)
.with_context(|| format!("failed to create cache directory `{}`", cache_dir.display()))?;
let slf = Self {
client,
cache_dir: cache_dir.to_path_buf(),
};
Ok(slf)
}
pub fn from_env(client: Client) -> Result<Self> {
let cache_path = env::var_os("DEBUGINFOD_CACHE_PATH")
.map(PathBuf::from)
.or_else(|| cache_dir().map(|dir| dir.join("debuginfod_client")))
.or_else(|| home_dir().map(|dir| dir.join(".cache").join("debuginfod_client")))
.context("DEBUGINFOD_CACHE_PATH environment variable not found")?;
Self::new(client, cache_path)
}
#[inline]
fn debuginfo_path(&self, build_id: &BuildId) -> PathBuf {
self
.cache_dir
.join(build_id.format().deref())
.join("debuginfo")
}
pub fn fetch_debug_info(&self, build_id: &BuildId) -> Result<Option<PathBuf>> {
let path = self.debuginfo_path(build_id);
if path.try_exists()? {
debug!("cache hit on `{}`", path.display());
return Ok(Some(path))
}
let mut response = if let Some(debug_info) = self.client.fetch_debug_info(build_id)? {
debug_info
} else {
return Ok(None)
};
let mut tempfile =
NamedTempFile::new_in(&self.cache_dir).context("failed to create temporary file")?;
let _count = copy(&mut response.data, &mut tempfile)
.context("failed to write debug info to file system")?;
let dir = path.parent().unwrap();
let () = create_dir_all(dir)
.with_context(|| format!("failed to create directory `{}`", dir.display()))?;
let _file = tempfile.persist_noclobber(&path).map_err(|err| {
let src_path = err.file.path().to_path_buf();
Result::<File, _>::Err(err)
.with_context(|| {
format!(
"failed to move temporary file `{}` to `{}`",
src_path.display(),
path.display()
)
})
.unwrap_err()
})?;
Ok(Some(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;
use std::ffi::OsStr;
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::tempdir;
use test_fork::fork;
#[fork]
#[test]
fn from_env_creation() {
let () = unsafe { env::remove_var("DEBUGINFOD_CACHE_PATH") };
let urls = ["https://debug.infod"];
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build(urls)
.unwrap()
.unwrap();
let client = CachingClient::from_env(client).unwrap();
assert_eq!(
client.cache_dir.file_name().unwrap(),
OsStr::new("debuginfod_client")
);
let cache_dir = tempdir().unwrap();
let () = unsafe { env::set_var("DEBUGINFOD_CACHE_PATH", cache_dir.path()) };
let urls = ["https://debug.infod"];
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build(urls)
.unwrap()
.unwrap();
let client = CachingClient::from_env(client).unwrap();
assert_eq!(client.cache_dir, cache_dir.path());
}
#[test]
fn fetch_debug_info() {
let cache_dir = tempdir().unwrap();
let urls = ["https://debuginfod.fedoraproject.org/"];
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build(urls)
.unwrap()
.unwrap();
let client = CachingClient::new(client, cache_dir.path()).unwrap();
let build_id = BuildId::RawBytes(Cow::Borrowed(&[
0xae, 0xb9, 0xa9, 0x83, 0xac, 0xe1, 0xfb, 0x04, 0x7b, 0x23, 0x41, 0xb1, 0x95, 0x01, 0x65,
0x44, 0x0f, 0xb2, 0xa8, 0xb9,
]));
let path = client.fetch_debug_info(&build_id).unwrap().unwrap();
let symbolizer = Symbolizer::new();
let src = Source::from(Elf::new(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 cache_dir = tempdir().unwrap();
let urls = ["https://debuginfod.fedoraproject.org/"];
let client = Client::builder()
.http_client(ReqwestBlockingClient::new())
.build(urls)
.unwrap()
.unwrap();
let client = CachingClient::new(client, cache_dir.path()).unwrap();
let build_id = BuildId::RawBytes(Cow::Borrowed(&[0x00]));
let info = client.fetch_debug_info(&build_id).unwrap();
assert!(info.is_none());
}
}