1mod codeview;
4mod error;
5mod image_signature;
6mod request;
7
8use std::path::{Path, PathBuf};
9
10use bon::Builder;
11pub use isr_dl::{Error, ProgressEvent, ProgressFn};
12use reqwest::{StatusCode, blocking::Client};
13use url::Url;
14
15pub use self::{
16 codeview::CodeView, error::DownloaderError, image_signature::ImageSignature,
17 request::SymbolRequest,
18};
19
20pub const DEFAULT_SERVER_URL: &str = "https://msdl.microsoft.com/download/symbols/";
22
23#[derive(Builder)]
25pub struct SymbolDownloader {
26 #[builder(default)]
27 client: Client,
28 #[builder(
29 default = vec![DEFAULT_SERVER_URL.try_into().unwrap()],
30 with = |iter: impl IntoIterator<Item = impl Into<Url>>| {
31 iter.into_iter().map(Into::into).collect()
32 }
33 )]
34 servers: Vec<Url>,
35 output_directory: PathBuf,
36 progress: Option<ProgressFn>,
37}
38
39impl SymbolDownloader {
40 pub fn lookup(&self, request: &SymbolRequest) -> Option<PathBuf> {
42 let path = self
43 .output_directory
44 .join(request.subdirectory())
45 .join(request.name());
46
47 path.exists().then_some(path)
48 }
49
50 pub fn download(&self, request: SymbolRequest) -> Result<PathBuf, Error> {
53 let output_directory = self.output_directory.join(request.subdirectory());
54 std::fs::create_dir_all(&output_directory)?;
55
56 let name = request.name();
57 let hash = request.hash();
58 let output = output_directory.join(name);
59
60 let mut last_error = None;
61 for server in &self.servers {
62 let url = match server.join(&format!("{name}/{hash}/{name}")) {
63 Ok(url) => url,
64 Err(err) => {
65 tracing::debug!(%server, error = %err, "invalid symbol URL, skipping server");
66 last_error = Some(Error::Other(Box::new(DownloaderError::from(err))));
67 continue;
68 }
69 };
70
71 match self.fetch(&url, &output) {
72 Ok(()) => return Ok(output),
73 Err(DownloaderError::Http(err)) if err.status() == Some(StatusCode::NOT_FOUND) => {
74 last_error = Some(Error::ArtifactNotFound);
75 }
76 Err(err) => {
77 tracing::debug!(%server, error = %err, "server error, trying next");
78 last_error = Some(Error::Other(Box::new(err)));
79 }
80 }
81 }
82
83 Err(last_error.unwrap_or(Error::ArtifactNotFound))
84 }
85
86 fn fetch(&self, url: &Url, output: &Path) -> Result<(), DownloaderError> {
87 if output.exists() {
88 tracing::debug!(path = %output.display(), "skipping download");
89 return Ok(());
90 }
91
92 tracing::debug!(%url, "requesting symbol");
93 let mut response = self.client.get(url.clone()).send()?.error_for_status()?;
94
95 let total_bytes = response.content_length();
96 isr_dl::stream_download(
97 &mut response,
98 output,
99 url,
100 total_bytes,
101 self.progress.clone(),
102 )?;
103
104 Ok(())
105 }
106}