runewarp 0.1.0

Runewarp is an ingress tunneling tool for exposing local services without moving TLS termination to the edge. Clients connect out over QUIC, so you can publish services without putting your backend directly on the Internet or leaking your public IP.
Documentation
use std::env;
use std::fmt;
use std::path::PathBuf;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum XdgDirectory {
    Config,
    Data,
    State,
}

impl XdgDirectory {
    fn env_var(self) -> &'static str {
        match self {
            Self::Config => "XDG_CONFIG_HOME",
            Self::Data => "XDG_DATA_HOME",
            Self::State => "XDG_STATE_HOME",
        }
    }

    fn fallback_suffix(self) -> &'static str {
        match self {
            Self::Config => ".config",
            Self::Data => ".local/share",
            Self::State => ".local/state",
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Config => "config",
            Self::Data => "data",
            Self::State => "state",
        }
    }
}

#[derive(Debug)]
pub struct XdgPathError {
    directory: XdgDirectory,
}

impl fmt::Display for XdgPathError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "unable to resolve the XDG {} base directory: set {} or HOME",
            self.directory.label(),
            self.directory.env_var()
        )
    }
}

impl std::error::Error for XdgPathError {}

pub fn default_config_path() -> Result<PathBuf, XdgPathError> {
    Ok(runewarp_dir(XdgDirectory::Config)?.join("config.toml"))
}

pub fn default_client_identity_material_dir() -> Result<PathBuf, XdgPathError> {
    Ok(runewarp_dir(XdgDirectory::Data)?
        .join("client")
        .join("identity"))
}

pub fn default_client_server_ca_path() -> Result<PathBuf, XdgPathError> {
    Ok(runewarp_dir(XdgDirectory::Data)?
        .join("client")
        .join("server-ca.crt"))
}

pub fn default_server_cert_material_dir() -> Result<PathBuf, XdgPathError> {
    Ok(runewarp_dir(XdgDirectory::Data)?
        .join("server")
        .join("cert"))
}

pub fn default_server_acme_state_dir() -> Result<PathBuf, XdgPathError> {
    Ok(runewarp_dir(XdgDirectory::State)?
        .join("server")
        .join("acme"))
}

pub fn default_client_public_cert_material_dir() -> Result<PathBuf, XdgPathError> {
    Ok(runewarp_dir(XdgDirectory::Data)?
        .join("client")
        .join("public-cert"))
}

pub fn default_client_acme_state_dir() -> Result<PathBuf, XdgPathError> {
    Ok(runewarp_dir(XdgDirectory::State)?
        .join("client")
        .join("acme"))
}

fn runewarp_dir(directory: XdgDirectory) -> Result<PathBuf, XdgPathError> {
    Ok(xdg_base_dir(directory)?.join("runewarp"))
}

fn xdg_base_dir(directory: XdgDirectory) -> Result<PathBuf, XdgPathError> {
    if let Some(path) = env::var_os(directory.env_var()).filter(|value| !value.is_empty()) {
        return Ok(PathBuf::from(path));
    }

    let Some(home) = env::var_os("HOME").filter(|value| !value.is_empty()) else {
        return Err(XdgPathError { directory });
    };

    Ok(PathBuf::from(home).join(directory.fallback_suffix()))
}