use clap::Parser; use indicatif::{DecimalBytes, MultiProgress, ProgressBar}; use symsrv::{ get_home_sym_dir, parse_nt_symbol_path, CabExtractionError, CachePath, DownloadError, NtSymbolPathEntry, SymsrvDownloader, SymsrvObserver, }; use std::collections::HashMap; use std::error::Error; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, MutexGuard}; #[derive(Parser)] #[clap( version, about = "Fetch a symbol file.", long_about = " Fetch a single symbol file from a symbol server, or find it in a local cache directory. Prints the local path to the file to stdout. Supports symbol servers which serve files with cab compression; the local file will be decompressed if necessary, and the printed path always refers to a decompressed file.", override_usage = r#"symfetch [OPTIONS] Examples: symfetch --server "https://msdl.microsoft.com/download/symbols/" --cache ~/sym winmine.exe 3B7D847520000 symfetch --server "https://msdl.microsoft.com/download/symbols/" --cache ~/sym combase.pdb 071849A7C75FD246A3367704EE1CA85B1 symfetch --server "https://renderdoc.org/symbols" --cache ~/sym renderdoc.pdb 6D1DFFC4DC524537962CCABC000820641 symfetch --symbol-path "srv**https://renderdoc.org/symbols" renderdoc.pdb 6D1DFFC4DC524537962CCABC000820641 _NT_SYMBOL_PATH="srv**https://msdl.microsoft.com/download/symbols/*https://chromium-browser-symsrv.commondatastorage.googleapis.com/" symfetch --use-env-symbol-path chrome.dll.pdb 93B17FC546DE07D14C4C44205044422E1"# )] struct Args { /// The server URL to use for downloading symbol files. Can be specified multiple times. #[clap(long)] server: Vec, /// The local cache directory to use for storing files that are downloaded from servers specified via --server. #[clap(long)] cache: Option, /// If set, nothing is printed to stderr (no progress bars and no status messages). #[clap(long)] quiet: bool, /// Respect the _NT_SYMBOL_PATH environment variable. #[clap(long, short = 'e')] use_env_symbol_path: bool, /// The "symbol path" in the format used by the _NT_SYMBOL_PATH environment variable. /// See https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/advanced-symsrv-use #[clap(long)] symbol_path: Option, /// The local cache directory which should be used if no other cache directories are specified in the symbol path. #[clap(long)] default_downstream_store: Option, /// The file name of the symbol file to fetch. name: String, /// The hash / "ID" of the symbol file, e.g. B2A2B092E45739B84C4C44205044422E1 or 61015E74442b000. /// /// For PDBs this is ``, with `` in uppercase and `` in lowercase hex. /// For binaries this is ``, with `` printed as eight uppercase /// hex digits (with leading zeros added as needed) and `` in lowercase hex // digits with as many digits as needed. hash: String, } #[tokio::main] async fn main() -> Result<(), Box> { let args: Args = Args::parse(); let mut parsed_nt_symbol_path = Vec::new(); let nt_symbol_path = if args.use_env_symbol_path { std::env::var("_NT_SYMBOL_PATH").ok() } else { args.symbol_path }; if let Some(nt_symbol_path) = nt_symbol_path { parsed_nt_symbol_path.extend(parse_nt_symbol_path(&nt_symbol_path)); } if !args.server.is_empty() { let mut cache_paths = Vec::new(); if let Some(cache) = args.cache { cache_paths.push(CachePath::Path(cache)); } let urls = args.server; parsed_nt_symbol_path.push(NtSymbolPathEntry::Chain { dll: "symsrv.dll".into(), cache_paths, urls, }); } let observer = Arc::new(SymFetchObserver::new()); let mut downloader = SymsrvDownloader::new(parsed_nt_symbol_path); downloader.set_default_downstream_store(get_home_sym_dir()); downloader.set_observer(Some(observer)); let path = downloader.get_file(&args.name, &args.hash).await?; println!("{}", path.to_string_lossy()); Ok(()) } struct SymFetchObserver { inner: Mutex, } impl SymFetchObserver { pub fn new() -> Self { Self { inner: Mutex::new(SymFetchObserverInner::new()), } } fn get_inner(&self) -> MutexGuard { self.inner.lock().unwrap() } } impl SymsrvObserver for SymFetchObserver { fn on_new_download_before_connect(&self, download_id: u64, url: &str) { self.get_inner() .on_new_download_before_connect(download_id, url); } fn on_download_failed(&self, download_id: u64, error: DownloadError) { self.get_inner().on_download_failed(download_id, error); } fn on_download_canceled(&self, download_id: u64) { self.get_inner().on_download_canceled(download_id); } fn on_download_started(&self, download_id: u64) { self.get_inner().on_download_started(download_id); } fn on_download_progress(&self, download_id: u64, bytes_so_far: u64, total_bytes: Option) { self.get_inner() .on_download_progress(download_id, bytes_so_far, total_bytes); } fn on_download_completed( &self, download_id: u64, uncompressed_size_in_bytes: u64, _time_until_headers: std::time::Duration, _time_until_completed: std::time::Duration, ) { self.get_inner() .on_download_completed(download_id, uncompressed_size_in_bytes); } fn on_new_cab_extraction(&self, extraction_id: u64, dest_path: &Path) { self.get_inner() .on_new_cab_extraction(extraction_id, dest_path); } fn on_cab_extraction_progress(&self, extraction_id: u64, bytes_so_far: u64, total_bytes: u64) { self.get_inner() .on_cab_extraction_progress(extraction_id, bytes_so_far, total_bytes); } fn on_cab_extraction_completed( &self, extraction_id: u64, uncompressed_size_in_bytes: u64, _time_until_completed: std::time::Duration, ) { self.get_inner() .on_cab_extraction_completed(extraction_id, uncompressed_size_in_bytes); } fn on_cab_extraction_failed(&self, extraction_id: u64, reason: CabExtractionError) { self.get_inner() .on_cab_extraction_failed(extraction_id, reason); } fn on_cab_extraction_canceled(&self, extraction_id: u64) { self.get_inner().on_cab_extraction_canceled(extraction_id); } fn on_file_missed(&self, _path: &Path) {} fn on_file_created(&self, _path: &Path, _size_in_bytes: u64) {} fn on_file_accessed(&self, _path: &Path) {} } struct SymFetchObserverInner { multi_progress: MultiProgress, requests: HashMap, extractions: HashMap, } impl SymFetchObserverInner { pub fn new() -> Self { Self { multi_progress: MultiProgress::new(), requests: HashMap::new(), extractions: HashMap::new(), } } fn on_new_download_before_connect(&mut self, download_id: u64, url: &str) { let progress_bar = self.multi_progress.add(ProgressBar::new_spinner()); progress_bar.set_style(style::spinner()); progress_bar.set_message(format!("Connecting to {url}...")); self.requests.insert( download_id, RequestData { progress_bar, url: url.to_owned(), is_determinate: false, }, ); } fn on_download_failed(&mut self, download_id: u64, error: DownloadError) { let request = self.requests.remove(&download_id).unwrap(); request.progress_bar.finish_and_clear(); self.multi_progress.remove(&request.progress_bar); let url = request.url; self.multi_progress .println(format!("Request to {url} failed: {error}")) .unwrap(); } fn on_download_canceled(&mut self, download_id: u64) { let request = self.requests.remove(&download_id).unwrap(); request.progress_bar.finish_and_clear(); self.multi_progress.remove(&request.progress_bar); let url = request.url; self.multi_progress .println(format!("Canceled request to {url}.")) .unwrap(); } fn message_for_url(url: &str) -> String { format!("Downloading from {url}...") } fn on_download_started(&mut self, download_id: u64) { let request = self.requests.get_mut(&download_id).unwrap(); let message = Self::message_for_url(&request.url); request.progress_bar.set_message(message); } fn on_download_progress( &mut self, download_id: u64, bytes_so_far: u64, total_bytes: Option, ) { let request = self.requests.get_mut(&download_id).unwrap(); match (request.is_determinate, total_bytes) { (false, Some(total_bytes)) => { let progress_bar = self.multi_progress.insert_after( &request.progress_bar, ProgressBar::new(total_bytes).with_elapsed(request.progress_bar.elapsed()), ); self.multi_progress.remove(&request.progress_bar); progress_bar.set_style(style::bar()); progress_bar.set_message(Self::message_for_url(&request.url)); request.progress_bar = progress_bar; request.is_determinate = true; } (true, None) => { let progress_bar = self.multi_progress.insert_after( &request.progress_bar, ProgressBar::new_spinner().with_elapsed(request.progress_bar.elapsed()), ); self.multi_progress.remove(&request.progress_bar); progress_bar.set_style(style::spinner()); progress_bar.set_message(Self::message_for_url(&request.url)); request.progress_bar = progress_bar; request.is_determinate = false; } _ => {} } request.progress_bar.set_position(bytes_so_far); } fn on_download_completed(&mut self, download_id: u64, uncompressed_size_in_bytes: u64) { let request = self.requests.remove(&download_id).unwrap(); request.progress_bar.finish(); self.multi_progress.remove(&request.progress_bar); let url = request.url; self.multi_progress .println(format!( "Successfully downloaded {} from {url}.", DecimalBytes(uncompressed_size_in_bytes) )) .unwrap(); } fn on_new_cab_extraction(&mut self, extraction_id: u64, dest_path: &Path) { let progress_bar = self.multi_progress.add(ProgressBar::new_spinner()); progress_bar.set_style(style::spinner()); progress_bar.set_message(format!( "Extracting to {dest_path}...", dest_path = dest_path.to_string_lossy() )); self.extractions.insert( extraction_id, ExtractionData { progress_bar, extracted_path: dest_path.to_owned(), is_determinate: false, }, ); } fn on_cab_extraction_progress( &mut self, extraction_id: u64, bytes_so_far: u64, total_bytes: u64, ) { let extraction = self.extractions.get_mut(&extraction_id).unwrap(); if !extraction.is_determinate { let progress_bar = self.multi_progress.insert_after( &extraction.progress_bar, ProgressBar::new(total_bytes).with_elapsed(extraction.progress_bar.elapsed()), ); self.multi_progress.remove(&extraction.progress_bar); progress_bar.set_style(style::bar()); progress_bar.set_message(format!( "Extracting {path}...", path = extraction.extracted_path.to_string_lossy() )); extraction.progress_bar = progress_bar; extraction.is_determinate = true; } extraction.progress_bar.set_position(bytes_so_far); } fn on_cab_extraction_completed(&mut self, extraction_id: u64, uncompressed_size_in_bytes: u64) { let extraction = self.extractions.remove(&extraction_id).unwrap(); extraction.progress_bar.finish(); self.multi_progress.remove(&extraction.progress_bar); self.multi_progress .println(format!( "Successfully extracted {} to {path}.", DecimalBytes(uncompressed_size_in_bytes), path = extraction.extracted_path.to_string_lossy() )) .unwrap(); } fn on_cab_extraction_failed(&mut self, extraction_id: u64, reason: CabExtractionError) { let extraction = self.extractions.remove(&extraction_id).unwrap(); extraction.progress_bar.finish_and_clear(); self.multi_progress.remove(&extraction.progress_bar); self.multi_progress .println(format!( "Failed to extract {path}: {reason}.", path = extraction.extracted_path.to_string_lossy(), reason = reason )) .unwrap(); } fn on_cab_extraction_canceled(&mut self, extraction_id: u64) { let extraction = self.extractions.remove(&extraction_id).unwrap(); extraction.progress_bar.finish_and_clear(); self.multi_progress.remove(&extraction.progress_bar); self.multi_progress .println(format!( "Canceled extraction of {path}.", path = extraction.extracted_path.to_string_lossy() )) .unwrap(); } } struct RequestData { progress_bar: ProgressBar, url: String, is_determinate: bool, } struct ExtractionData { progress_bar: ProgressBar, extracted_path: PathBuf, is_determinate: bool, } mod style { use indicatif::ProgressStyle; pub fn bar() -> ProgressStyle { ProgressStyle::default_bar() .template( "[{elapsed_precise}] {bar:.cyan/blue} {decimal_bytes:>12}/{decimal_total_bytes:12} {wide_msg}", ) .unwrap() .progress_chars("█▉▊▋▌▍▎▏ ") } pub fn spinner() -> ProgressStyle { ProgressStyle::default_bar() .template("[{elapsed_precise}] {spinner} {bytes_per_sec:>10} {wide_msg}") .unwrap() } }