#![allow(unknown_lints)]
#![warn(clippy::all)]
#![allow(clippy::needless_return)]
use std::{path::PathBuf, str::FromStr};
extern crate futures;
extern crate indicatif;
extern crate reqwest;
extern crate tokio;
use anyhow::Context;
use indicatif::{MultiProgress, ProgressBar};
use thiserror::Error;
use tokio::io::AsyncWriteExt;
mod style {
use indicatif::ProgressStyle;
pub fn bar() -> ProgressStyle {
ProgressStyle::default_bar()
.template(
"[{elapsed_precise}] {bar:.cyan/blue} {bytes:>12}/{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()
}
}
pub enum SymFileInfo {
Exe(ExeInfo),
Pdb(PdbInfo),
RawHash(String),
}
impl ToString for SymFileInfo {
fn to_string(&self) -> String {
match self {
SymFileInfo::Exe(i) => i.to_string(),
SymFileInfo::Pdb(i) => i.to_string(),
SymFileInfo::RawHash(h) => h.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExeInfo {
pub timestamp: u32,
pub size: u32,
}
impl ToString for ExeInfo {
fn to_string(&self) -> String {
format!("{:08x}{:x}", self.timestamp, self.size)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PdbInfo {
pub guid: u128,
pub age: u32,
}
impl ToString for PdbInfo {
fn to_string(&self) -> String {
format!("{:032X}{:x}", self.guid, self.age)
}
}
#[derive(Error, Debug)]
pub enum DownloadError {
#[error("Server returned 404 not found")]
FileNotFound,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[derive(Debug)]
pub enum DownloadStatus {
AlreadyExists,
DownloadedOk,
}
enum RemoteFileType {
Url(reqwest::Response),
Path(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymSrv {
pub server_url: String,
pub cache_path: String,
}
impl FromStr for SymSrv {
type Err = anyhow::Error;
fn from_str(srv: &str) -> Result<Self, Self::Err> {
let directives: Vec<&str> = srv.split('*').collect();
match directives.first() {
Some(x) => {
if x.eq_ignore_ascii_case("SRV") {
if directives.len() != 3 {
anyhow::bail!("Unsupported server string form; only 'SRV*<CACHE_PATH>*<SYMBOL_SERVER>' supported");
}
return Ok(SymSrv {
server_url: directives[2].to_string(),
cache_path: directives[1].to_string(),
});
}
}
None => {
anyhow::bail!("Unsupported server string form; only 'SRV*<CACHE_PATH>*<SYMBOL_SERVER>' supported");
}
};
anyhow::bail!(
"Unsupported server string form; only 'SRV*<CACHE_PATH>*<SYMBOL_SERVER>' supported"
);
}
}
pub struct SymSrvList(pub Box<[SymSrv]>);
impl FromStr for SymSrvList {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let server_list: Vec<&str> = s.split(';').collect();
if server_list.is_empty() {
anyhow::bail!("Invalid server string");
}
let vec = server_list
.into_iter()
.map(|symstr| symstr.parse::<SymSrv>())
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(SymSrvList(vec.into_boxed_slice()))
}
}
async fn download_single(
client: &reqwest::Client,
srv: &SymSrv,
mp: Option<&MultiProgress>,
name: &str,
hash: &str,
) -> Result<(DownloadStatus, PathBuf), DownloadError> {
let file_rel_folder = format!("{}/{}", name, hash);
let file_name = format!("{}/{}/{}", srv.cache_path, file_rel_folder, name);
let file_folder_url = format!("{}/{}", srv.server_url, file_rel_folder);
let file_name_tmp = format!("{}.tmp", file_name);
let _ = tokio::fs::remove_file(&file_name_tmp).await;
if std::path::Path::new(&file_name).exists() {
return Ok((DownloadStatus::AlreadyExists, file_name.into()));
}
let remote_file = {
let pdb_req = client
.get::<&str>(&format!("{}/{}", file_folder_url, name))
.send()
.await
.context("failed to request remote file")?;
if pdb_req.status().is_success() {
if let Some(mime) = pdb_req.headers().get(reqwest::header::CONTENT_TYPE) {
let mime = mime
.to_str()
.expect("Content-Type header not a valid string")
.parse::<mime::Mime>()
.expect("Content-Type header not a valid MIME type");
if mime.subtype() == mime::HTML {
panic!(
"Server {} returned an invalid Content-Type of {mime}",
srv.server_url
);
}
}
RemoteFileType::Url(pdb_req)
} else {
let fileptr_req = client
.get::<&str>(&format!("{}/file.ptr", file_folder_url))
.send()
.await
.context("failed to request file.ptr")?;
if !fileptr_req.status().is_success() {
Err(DownloadError::FileNotFound)?;
}
let url = fileptr_req
.text()
.await
.context("failed to get file.ptr contents")?;
let mut url_iter = url.split(':');
let url_type = url_iter.next().unwrap();
let url = url_iter.next().unwrap();
match url_type {
"PATH" => RemoteFileType::Path(url.to_string()),
"MSG" => return Err(DownloadError::FileNotFound), typ => {
unimplemented!(
"Unknown symbol redirection pointer type {typ}!\n{url_type}:{url}"
);
}
}
}
};
tokio::fs::create_dir_all(format!("{}/{}", srv.cache_path, file_rel_folder))
.await
.context("failed to create symbol directory tree")?;
match remote_file {
RemoteFileType::Url(mut res) => {
let dl_pb = if let Some(m) = mp {
let dl_pb = match res.content_length() {
Some(len) => {
let dl_pb = m.add(ProgressBar::new(len));
dl_pb.set_style(style::bar());
dl_pb
}
None => {
let dl_pb = m.add(ProgressBar::new_spinner());
dl_pb.set_style(style::spinner());
dl_pb.enable_steady_tick(std::time::Duration::from_millis(5));
dl_pb
}
};
dl_pb.set_message(format!("{}/{}", hash, name));
Some(dl_pb)
} else {
None
};
let mut file = tokio::fs::File::create(&file_name_tmp)
.await
.context("failed to create output pdb")?;
while let Some(chunk) = res.chunk().await.context("failed to download pdb chunk")? {
if let Some(dl_pb) = &dl_pb {
dl_pb.inc(chunk.len() as u64);
}
file.write(&chunk)
.await
.context("failed to write pdb chunk")?;
}
tokio::fs::rename(&file_name_tmp, &file_name)
.await
.context("failed to rename pdb")?;
Ok((DownloadStatus::DownloadedOk, file_name.into()))
}
RemoteFileType::Path(path) => {
let mut remote_file = tokio::fs::File::open(path)
.await
.context("failed to open remote file")?;
let metadata = remote_file
.metadata()
.await
.context("failed to fetch remote metadata")?;
let dl_pb = if let Some(m) = mp {
let dl_pb = m.add(ProgressBar::new(metadata.len()));
dl_pb.set_style(style::bar());
dl_pb.set_message(format!("{}/{}", hash, name));
Some(dl_pb)
} else {
None
};
let mut file = tokio::fs::File::create(&file_name_tmp)
.await
.context("failed to create output pdb")?;
if let Some(dl_pb) = dl_pb {
tokio::io::copy(&mut dl_pb.wrap_async_read(remote_file), &mut file)
.await
.context("failed to copy pdb")?;
} else {
tokio::io::copy(&mut remote_file, &mut file)
.await
.context("failed to copy pdb")?;
}
tokio::fs::rename(&file_name_tmp, &file_name)
.await
.context("failed to rename pdb")?;
Ok((DownloadStatus::DownloadedOk, file_name.into()))
}
}
}
fn connect_pat(token: &str) -> anyhow::Result<reqwest::Client> {
use reqwest::header;
let b64 = base64::encode(format!(":{token}"));
let mut headers = header::HeaderMap::new();
let auth_value = header::HeaderValue::from_str(&format!("Basic {b64}"))?;
headers.insert(header::AUTHORIZATION, auth_value);
Ok(reqwest::Client::builder()
.default_headers(headers)
.build()?)
}
fn connect_server(srv: &SymSrv) -> anyhow::Result<reqwest::Client> {
use url::{Host, Url};
let url = Url::parse(&srv.server_url)?;
match url.host() {
Some(Host::Domain(d)) => {
match d {
d if d.ends_with("artifacts.visualstudio.com") => {
let pat = url
.password()
.map(|p| p.to_string())
.or_else(|| std::env::var("ADO_PAT").ok())
.context("PAT not specified for ADO")?;
if url.scheme() != "https" {
anyhow::bail!("This URL must be over https!");
}
Ok(connect_pat(&pat)?)
}
_ => {
Ok(reqwest::Client::new())
}
}
}
Some(Host::Ipv4(_) | Host::Ipv6(_)) | None => {
Ok(reqwest::Client::new())
}
}
}
pub struct SymContext {
servers: Box<[(SymSrv, reqwest::Client)]>,
}
impl SymContext {
pub fn new(srvstr: &str) -> anyhow::Result<Self> {
let servers = SymSrvList::from_str(srvstr)?;
let servers = servers
.0
.iter()
.map(|s| (s.clone(), connect_server(s).unwrap()))
.collect::<Box<[_]>>();
Ok(Self { servers })
}
pub fn find_file(&self, name: &str, info: &SymFileInfo) -> Option<PathBuf> {
for (srv, _) in self.servers.iter() {
let hash = info.to_string();
let path = PathBuf::from(&srv.cache_path)
.join(name)
.join(hash)
.join(name);
if path.exists() {
return Some(path);
}
}
None
}
pub async fn download_file(
&self,
name: &str,
info: &SymFileInfo,
) -> Result<PathBuf, DownloadError> {
for (srv, client) in self.servers.iter() {
let hash = info.to_string();
match download_single(client, srv, None, name, &hash).await {
Ok((_status, path)) => return Ok(path),
Err(e) => match e {
DownloadError::FileNotFound => continue,
e => return Err(e),
},
}
}
Err(DownloadError::FileNotFound)
}
pub async fn download_file_progress(
&self,
name: &str,
info: &SymFileInfo,
mp: &MultiProgress,
) -> Result<PathBuf, DownloadError> {
for (srv, client) in self.servers.iter() {
let hash = info.to_string();
match download_single(client, srv, Some(mp), name, &hash).await {
Ok((_status, path)) => return Ok(path),
Err(e) => match e {
DownloadError::FileNotFound => continue,
e => return Err(e),
},
}
}
Err(DownloadError::FileNotFound)
}
}