breakmancer 0.6.0

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

use regex::Regex;

// 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.
pub fn shell_escape(raw: &str) -> String {
    let mut out = String::from("'");
    for ch in raw.chars() {
        match ch {
            '!' | '\\' => {
                out.push_str("'\\");
                out.push(ch);
                out.push('\'');
            }
            '\n' => {
                out.push_str("'$'\\n''");
            }
            '\r' => {
                out.push_str("'$'\\r''");
            }
            _ => out.push(ch),
        }
    }
    out.push('\'');
    out
}