breakmancer 0.9.0

Drop a breakpoint into any shell.
Documentation
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::LazyLock;

use regex::Regex;
use tracing::Level;
use tracing_subscriber::FmtSubscriber;

// Not as strict as it could be, but close enough.
static DOMAIN_NAME_PATTERN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.?$").unwrap());

pub fn check_host(host: &str) -> Result<(), String> {
    if IpAddr::from_str(host).is_err() {
        // Maybe it's a domain name?
        if !DOMAIN_NAME_PATTERN.is_match(host) {
            return Err("Host portion is not IPv4, IPv6, or domain name".to_string());
        }
    }
    Ok(())
}

/// Parse a HOST:PORT address into its two parts.
///
/// This also validates that the host portion is an IPv4 address, IPv6
/// address, or something that looks more or less like a domain name.
pub fn parse_address(addr: &str) -> Result<(&str, u16), String> {
    match addr.rsplit_once(':') {
        None => Err("Missing port. Use HOST:PORT format.".to_string()),
        Some((host, port)) => {
            check_host(host)?;
            let port = match port.parse::<u16>() {
                Ok(parsed) => parsed,
                Err(err) => return Err(format!("Invalid port: {err}")),
            };
            Ok((host, port))
        }
    }
}

/// Escape as a shell string.
///
/// Panics if string contains NUL bytes.
pub fn shell_escape(raw: &str) -> String {
    shlex::try_quote(raw).unwrap().into()
}

/// Enable debug logging, depending on settings.
pub fn configure_logging() {
    if std::env::var("BM_DEBUG").unwrap_or_default() == "true" {
        let trace_subscriber = FmtSubscriber::builder()
            .with_max_level(Level::DEBUG)
            .finish();
        tracing::subscriber::set_global_default(trace_subscriber).unwrap();
    }
}