use std::process::{Command, ExitStatus, Stdio};
use hyper::Uri;
mod tests;
#[repr(transparent)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CommandFailedOutput(pub Vec<u8>);
impl std::fmt::Display for CommandFailedOutput {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", String::from_utf8_lossy(&self.0))
}
}
impl std::fmt::Debug for CommandFailedOutput {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl From<CommandFailedOutput> for Vec<u8> {
#[inline(always)]
fn from(out: CommandFailedOutput) -> Self {
out.0
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("This system does not have an HTTP client installed")]
SystemHTTPClientNotFound,
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("{0}")]
InvalidUrl(#[from] hyper::http::uri::InvalidUri),
#[error("Process exited with code {status:?}")]
CommandFailed {
status: ExitStatus,
stdout: CommandFailedOutput,
stderr: CommandFailedOutput
}
}
pub(crate) trait SystemHTTPClient: Sized + Send + Sync {
const COMMAND: &'static str;
fn installed_spawn() -> Command {
Command::new(Self::COMMAND)
}
fn installed() -> bool {
!matches!(Self::installed_spawn().stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()).status(), Err(err) if err.kind() == std::io::ErrorKind::NotFound)
}
fn get(&self, uri: &str) -> Result<Vec<u8>, Error>;
}
macro_rules! system_http_clients {
{$($(#[$cfg:meta])? mod $mod:ident::$client:ident;)*} => {
$($(#[$cfg])? mod $mod;)*
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug)]
enum ResolvedSystemHTTPClient {
$($(#[$cfg])? $client,)*
}
impl SystemHTTPClient for ResolvedSystemHTTPClient {
const COMMAND: &'static str = "";
fn installed() -> bool {
unimplemented!()
}
fn get(&self, uri: &str) -> Result<Vec<u8>, Error> {
match self {
$($(#[$cfg])? Self::$client => $mod::$client.get(uri)),*
}
}
}
lazy_static::lazy_static! {
static ref HTTP_CLIENT: Option<ResolvedSystemHTTPClient> = {
#[allow(clippy::never_loop)]
loop {
$($(#[$cfg])? {
if <$mod::$client as SystemHTTPClient>::installed() {
break Some(ResolvedSystemHTTPClient::$client);
}
})*
break None;
}
};
}
pub fn supported_http_clients() -> &'static [&'static str] {
&[
$($(#[$cfg])? { stringify!($client) }),*
]
}
#[cfg(test)]
fn all_http_clients() -> impl Iterator<Item = ResolvedSystemHTTPClient> {
[$($(#[$cfg])? { if <$mod::$client>::installed() { Some(ResolvedSystemHTTPClient::$client) } else { None } }),*].into_iter().flatten()
}
};
}
system_http_clients! {
mod wget::wget;
mod powershell::PowerShell;
#[cfg(not(windows))] mod curl::cURL;
}
fn http_client() -> Result<ResolvedSystemHTTPClient, Error> {
match *HTTP_CLIENT {
Some(client) => Ok(client),
None => Err(Error::SystemHTTPClientNotFound)
}
}
#[inline]
pub fn installed() -> bool {
HTTP_CLIENT.is_some()
}
pub fn get(uri: impl AsRef<str>) -> Result<Vec<u8>, Error> {
let uri = uri.as_ref();
let _: Uri = uri.try_into()?;
http_client()?.get(uri)
}