gdbc 0.1.0

comprehensive terminal-based GDB client
Documentation
use crate::client::GdbClient;
use crate::display::{DisplayMode, Endianness};
use crate::error::Error;
use regex::Regex;
use std::str::FromStr;

#[derive(Debug, Clone)]
pub enum Command {
    Connect(String, u16),
    Disconnect,
    Reconnect,
    ReadMemory {
        address: u64,
        size: usize,
        mode: DisplayMode,
        endianness: Endianness,
    },
    WriteMemory {
        address: u64,
        data: Vec<u8>,
    },
    SetBreakpoint(u64),
    RemoveBreakpoint(u64),
    Continue,
    Step,
    ReadRegisters,
    Help,
    Quit,
    Unknown(String),
}

impl FromStr for Command {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim();

        // Command parsing with regex
        let connect_re = Regex::new(r"^connect\s+([^\s:]+)(?::(\d+))?$").unwrap();
        let read_mem_re = Regex::new(
            r"^(?:readmem|rm)\s+(0x[0-9a-fA-F]+|\d+)\s+(\d+)(?:\s+([a-z0-9]+))?(?:\s+([a-z]+))?$",
        )
        .unwrap();
        let write_mem_re = Regex::new(r"^(?:writemem|wm)\s+(0x[0-9a-fA-F]+|\d+)\s+(.+)$").unwrap();
        let break_re = Regex::new(r"^(?:break|b)\s+(0x[0-9a-fA-F]+|\d+)$").unwrap();
        let delete_break_re = Regex::new(r"^(?:delete|d)\s+(0x[0-9a-fA-F]+|\d+)$").unwrap();

        if s.is_empty() {
            return Ok(Command::Unknown(s.to_string()));
        } else if connect_re.is_match(s) {
            let caps = connect_re.captures(s).unwrap();
            let host = caps.get(1).map_or("localhost", |m| m.as_str());
            let port = caps
                .get(2)
                .map_or("55555", |m| m.as_str())
                .parse::<u16>()
                .unwrap_or(55555);
            Ok(Command::Connect(host.to_string(), port))
        } else if s == "disconnect" {
            Ok(Command::Disconnect)
        } else if s == "reconnect" {
            Ok(Command::Reconnect)
        } else if read_mem_re.is_match(s) {
            let caps = read_mem_re.captures(s).unwrap();

            let addr_str = caps.get(1).unwrap().as_str();
            let address = if addr_str.starts_with("0x") {
                u64::from_str_radix(&addr_str[2..], 16)
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            } else {
                addr_str
                    .parse::<u64>()
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            };

            let size = caps
                .get(2)
                .unwrap()
                .as_str()
                .parse::<usize>()
                .map_err(|_| Error::ParseError("Invalid size".to_string()))?;

            let mode = match caps.get(3).map(|m| m.as_str().to_lowercase()) {
                Some(m) if m == "str" || m == "ascii" => DisplayMode::Ascii,
                Some(m) if m == "int16" => DisplayMode::Int16,
                Some(m) if m == "int32" => DisplayMode::Int32,
                Some(m) if m == "int64" => DisplayMode::Int64,
                _ => DisplayMode::Hex,
            };

            let endianness = match caps.get(4).map(|m| m.as_str().to_lowercase()) {
                Some(e) if e == "big" => Endianness::Big,
                _ => Endianness::Little, // Default to little endian
            };

            Ok(Command::ReadMemory {
                address,
                size,
                mode,
                endianness,
            })
        } else if write_mem_re.is_match(s) {
            let caps = write_mem_re.captures(s).unwrap();

            let addr_str = caps.get(1).unwrap().as_str();
            let address = if addr_str.starts_with("0x") {
                u64::from_str_radix(&addr_str[2..], 16)
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            } else {
                addr_str
                    .parse::<u64>()
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            };

            let data_str = caps.get(2).unwrap().as_str();

            // Parse data: could be hex string, quoted string, etc.
            let data = if data_str.starts_with("0x") {
                // Hex data
                let hex_str = data_str[2..].replace(" ", "");
                if hex_str.len() % 2 != 0 {
                    return Err(Error::ParseError(
                        "Hex data must have even length".to_string(),
                    ));
                }

                let mut data = Vec::with_capacity(hex_str.len() / 2);
                for i in (0..hex_str.len()).step_by(2) {
                    if i + 1 < hex_str.len() {
                        let hex_byte = &hex_str[i..i + 2];
                        data.push(
                            u8::from_str_radix(hex_byte, 16)
                                .map_err(|_| Error::ParseError("Invalid hex data".to_string()))?,
                        );
                    }
                }
                data
            } else if data_str.starts_with("\"") && data_str.ends_with("\"") {
                // String data
                let str_data = &data_str[1..data_str.len() - 1];
                str_data.as_bytes().to_vec()
            } else {
                // Try to parse as space-separated bytes
                let bytes: Result<Vec<u8>, _> = data_str
                    .split_whitespace()
                    .map(|s| s.parse::<u8>())
                    .collect();

                match bytes {
                    Ok(b) => b,
                    Err(_) => return Err(Error::ParseError("Invalid data format".to_string())),
                }
            };

            Ok(Command::WriteMemory { address, data })
        } else if break_re.is_match(s) {
            let caps = break_re.captures(s).unwrap();

            let addr_str = caps.get(1).unwrap().as_str();
            let address = if addr_str.starts_with("0x") {
                u64::from_str_radix(&addr_str[2..], 16)
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            } else {
                addr_str
                    .parse::<u64>()
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            };

            Ok(Command::SetBreakpoint(address))
        } else if delete_break_re.is_match(s) {
            let caps = delete_break_re.captures(s).unwrap();

            let addr_str = caps.get(1).unwrap().as_str();
            let address = if addr_str.starts_with("0x") {
                u64::from_str_radix(&addr_str[2..], 16)
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            } else {
                addr_str
                    .parse::<u64>()
                    .map_err(|_| Error::ParseError("Invalid address".to_string()))?
            };

            Ok(Command::RemoveBreakpoint(address))
        } else if s == "continue" || s == "c" {
            Ok(Command::Continue)
        } else if s == "step" || s == "s" {
            Ok(Command::Step)
        } else if s == "registers" || s == "regs" || s == "info registers" {
            Ok(Command::ReadRegisters)
        } else if s == "help" || s == "h" || s == "?" {
            Ok(Command::Help)
        } else if s == "quit" || s == "exit" || s == "q" {
            Ok(Command::Quit)
        } else {
            Ok(Command::Unknown(s.to_string()))
        }
    }
}

pub fn execute_command(
    cmd: Command,
    client: &mut GdbClient,
    use_colors: bool,
) -> Result<(), Error> {
    match cmd {
        Command::Connect(host, port) => {
            let addr = format!("{}:{}", host, port);
            println!("Connecting to {}...", addr);
            client.connect(addr)?;
            println!("Connected to GDB server at {}:{}", host, port);
            Ok(())
        }
        Command::Disconnect => {
            client.disconnect()?;
            println!("Disconnected from GDB server");
            Ok(())
        }
        Command::Reconnect => {
            println!("Reconnecting to GDB server...");
            client.reconnect()?;
            println!("Reconnected to GDB server");
            Ok(())
        }
        Command::ReadMemory {
            address,
            size,
            mode,
            endianness,
        } => {
            let result = client.read_memory(address, size, mode, endianness)?;
            println!("{}", crate::display::colorize_output(&result, use_colors));
            Ok(())
        }
        Command::WriteMemory { address, data } => {
            client.write_memory(address, &data)?;
            println!("Memory written at address 0x{:x}", address);
            Ok(())
        }
        Command::SetBreakpoint(addr) => {
            client.set_breakpoint(addr)?;
            println!("Breakpoint set at address 0x{:x}", addr);
            Ok(())
        }
        Command::RemoveBreakpoint(addr) => {
            client.remove_breakpoint(addr)?;
            println!("Breakpoint removed from address 0x{:x}", addr);
            Ok(())
        }
        Command::Continue => {
            println!("Continuing execution...");
            client.continue_execution()?;
            Ok(())
        }
        Command::Step => {
            println!("Stepping...");
            client.step()?;
            Ok(())
        }
        Command::ReadRegisters => {
            let registers = client.read_registers()?;
            println!(
                "{}",
                crate::display::colorize_output(&registers, use_colors)
            );
            Ok(())
        }
        Command::Help => {
            print_help();
            Ok(())
        }
        Command::Quit => {
            println!("Exiting...");
            if client.is_connected() {
                client.disconnect()?;
            }
            std::process::exit(0);
        }
        Command::Unknown(cmd) => {
            if cmd.is_empty() {
                return Ok(());
            }
            println!("Unknown command: {}", cmd);
            println!("Type 'help' for available commands");
            Ok(())
        }
    }
}

fn print_help() {
    use colored::*;

    println!("{}", "Available commands:".bold().underline());

    // Command definitions with consistent formatting
    let cmd_width: usize = 24; // Width for command column
    // let desc_width: usize = 50; // Width for description column

    // Helper function to format commands with colors
    let format_cmd = |cmd: &str, args: &str, desc: &str| {
        let main_cmd = cmd.bright_green().bold();
        let args_fmt = args.yellow();
        let full_cmd = format!("  {} {}", main_cmd, args_fmt);

        // Pad to consistent width
        let pad_len = cmd_width.saturating_sub(cmd.len() + args.len() + 3);
        let padding = " ".repeat(pad_len);

        println!("{}{}- {}", full_cmd, padding, desc);
    };

    // Connection commands
    format_cmd(
        "connect",
        "HOST[:PORT]",
        "Connect to GDB server (default port 55555)",
    );
    format_cmd("disconnect", "", "Disconnect from server");
    format_cmd("reconnect", "", "Reconnect to the last server");

    // Memory operations
    format_cmd("readmem|rm", "ADDR SIZE [MODE] [ENDIAN]", "Read memory");
    println!(
        "{}MODE: {}, {}, {}, {}, {}",
        " ".repeat(cmd_width + 2),
        "hex".cyan().italic(),
        "ascii/str".cyan().italic(),
        "int16".cyan().italic(),
        "int32".cyan().italic(),
        "int64".cyan().italic()
    );
    println!(
        "{}ENDIAN: {}, {}",
        " ".repeat(cmd_width + 2),
        "little".cyan().italic(),
        "big".cyan().italic()
    );

    format_cmd(
        "writemem|wm",
        "ADDR DATA",
        "Write memory (DATA as hex, string, or bytes)",
    );

    // Breakpoint management
    format_cmd("break|b", "ADDR", "Set breakpoint");
    format_cmd("delete|d", "ADDR", "Remove breakpoint");

    // Execution control
    format_cmd("continue|c", "", "Continue execution");
    format_cmd("step|s", "", "Single step");

    // Information commands
    format_cmd("registers|regs", "", "Read registers");

    // Help and exit
    format_cmd("help|h|?", "", "Show this help");
    format_cmd("quit|exit|q", "", "Exit the program");

    // Examples section
    println!("\n{}", "Examples:".bold().underline());

    let example_width: usize = 28; // Width for example column

    // Helper function to format examples with colors
    let format_example = |cmd: &str, desc: &str| {
        let example = format!("  {}", cmd).bright_blue();

        // Pad to consistent width
        let pad_len = example_width.saturating_sub(cmd.len() + 2);
        let padding = " ".repeat(pad_len);

        println!("{}{}- {}", example, padding, desc);
    };

    format_example("readmem 0x1000 16", "Read 16 bytes at 0x1000 (hex display)");
    format_example(
        "rm 0x1000 100 str",
        "Read 100 bytes at 0x1000 as ASCII string",
    );
    format_example(
        "rm 0x1000 16 int32 big",
        "Read 4 int32 values in big endian",
    );
    format_example("writemem 0x1000 \"Hello\"", "Write string at 0x1000");
    format_example("wm 0x1000 0x01020304", "Write hex bytes at 0x1000");
}