Skip to main content

isr_dl_windows/
lib.rs

1//! Download PDB files and PE binaries from Microsoft symbol servers.
2
3mod 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
20/// Microsoft's public symbol server.
21pub const DEFAULT_SERVER_URL: &str = "https://msdl.microsoft.com/download/symbols/";
22
23/// Downloads PDBs and PE binaries from one or more Microsoft symbol servers.
24#[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    /// Returns the cached path for `request`, or `None` if it is not on disk.
41    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    /// Returns the on-disk path for `request`, fetching from the configured
51    /// servers if the artifact is not cached.
52    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}