gdbc 0.1.0

comprehensive terminal-based GDB client
Documentation
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;

use crate::display::{DisplayMode, Endianness, format_memory_display};
use crate::error::Error;

const CONNECT_TIMEOUT_SECS: u64 = 5;

pub struct GdbClient {
    stream: Option<TcpStream>,
    server_address: String,
    server_port: u16,
    connected: bool,
}

impl GdbClient {
    pub fn new() -> Self {
        GdbClient {
            stream: None,
            server_address: String::new(),
            server_port: 0,
            connected: false,
        }
    }

    pub fn connect<A: ToSocketAddrs>(&mut self, addr: A) -> Result<(), Error> {
        match TcpStream::connect_timeout(
            &addr
                .to_socket_addrs()?
                .next()
                .ok_or_else(|| Error::ConnectionError("Invalid address".to_string()))?,
            Duration::from_secs(CONNECT_TIMEOUT_SECS),
        ) {
            Ok(stream) => {
                stream.set_read_timeout(Some(Duration::from_secs(2)))?;
                if let Ok(addr) = stream.peer_addr() {
                    self.server_address = addr.ip().to_string();
                    self.server_port = addr.port();
                }
                self.stream = Some(stream);
                self.connected = true;

                // Send handshake packet and initialize connection
                self.send_packet(b"qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+")?;

                Ok(())
            }
            Err(e) => Err(Error::ConnectionError(format!("Failed to connect: {}", e))),
        }
    }

    pub fn disconnect(&mut self) -> Result<(), Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        // Send disconnect packet if needed

        self.stream = None;
        self.connected = false;
        Ok(())
    }

    pub fn reconnect(&mut self) -> Result<(), Error> {
        if self.connected {
            self.disconnect()?;
        }

        let addr = format!("{}:{}", self.server_address, self.server_port);
        self.connect(addr)
    }

    pub fn is_connected(&self) -> bool {
        self.connected
    }

    fn send_packet(&mut self, data: &[u8]) -> Result<Vec<u8>, Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        let stream = self
            .stream
            .as_mut()
            .ok_or_else(|| Error::ConnectionError("Stream not available".to_string()))?;

        // Format packet according to GDB Remote Protocol
        let mut packet = Vec::new();
        packet.push(b'$');
        packet.extend_from_slice(data);

        // Calculate checksum (sum of all bytes modulo 256)
        let checksum = data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x));

        // Add checksum
        packet.push(b'#');
        packet.push(hex_digit((checksum >> 4) & 0xf));
        packet.push(hex_digit(checksum & 0xf));

        // Send packet
        stream.write_all(&packet)?;
        stream.flush()?;

        // Read response
        self.read_packet()
    }

    fn read_packet(&mut self) -> Result<Vec<u8>, Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        let stream = self
            .stream
            .as_mut()
            .ok_or_else(|| Error::ConnectionError("Stream not available".to_string()))?;

        let mut buffer = [0u8; 1];
        let mut response = Vec::new();
        let in_packet;

        // Find start of packet
        loop {
            stream.read_exact(&mut buffer)?;

            match buffer[0] {
                b'$' => {
                    in_packet = true;
                    break;
                }
                _ => continue,
            }
        }

        // Read packet data
        loop {
            stream.read_exact(&mut buffer)?;

            match buffer[0] {
                b'#' => break,
                _ if in_packet => response.push(buffer[0]),
                _ => continue,
            }
        }

        // Read checksum (2 hex digits)
        let mut checksum_buf = [0u8; 2];
        stream.read_exact(&mut checksum_buf)?;

        // Acknowledge packet
        stream.write_all(b"+")?;
        stream.flush()?;

        Ok(response)
    }

    // Read memory from target
    pub fn read_memory(
        &mut self,
        address: u64,
        size: usize,
        display_mode: DisplayMode,
        endianness: Endianness,
    ) -> Result<String, Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        // Format m packet: m addr,length
        let packet = format!("m {:x},{:x}", address, size);
        let response = self.send_packet(packet.as_bytes())?;

        // Check for error response
        if response.len() > 0 && response[0] == b'E' {
            let error_code = String::from_utf8_lossy(&response[1..3]);
            return Err(Error::ProtocolError(format!(
                "GDB returned error: {}",
                error_code
            )));
        }

        // Convert hex response to binary
        let mut binary_data = Vec::with_capacity(response.len() / 2);
        let hex_string = String::from_utf8_lossy(&response);

        for i in (0..hex_string.len()).step_by(2) {
            if i + 1 < hex_string.len() {
                let hex_byte = &hex_string[i..i + 2];
                if let Ok(byte) = u8::from_str_radix(hex_byte, 16) {
                    binary_data.push(byte);
                } else {
                    return Err(Error::ParseError(format!("Invalid hex data: {}", hex_byte)));
                }
            }
        }

        // Format according to display mode
        Ok(format_memory_display(
            &binary_data,
            address,
            display_mode,
            endianness,
        ))
    }

    // Set breakpoint
    pub fn set_breakpoint(&mut self, address: u64) -> Result<u64, Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        // Z0 packet for software breakpoint
        let packet = format!("Z0,{:x},1", address);
        let response = self.send_packet(packet.as_bytes())?;

        if response == b"OK" {
            Ok(address)
        } else if response.len() > 0 && response[0] == b'E' {
            let error_code = String::from_utf8_lossy(&response[1..3]);
            Err(Error::ProtocolError(format!(
                "Failed to set breakpoint: {}",
                error_code
            )))
        } else {
            Err(Error::ProtocolError(
                "Unknown response for breakpoint".to_string(),
            ))
        }
    }

    // Remove breakpoint
    pub fn remove_breakpoint(&mut self, address: u64) -> Result<(), Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        // z0 packet for removing software breakpoint
        let packet = format!("z0,{:x},1", address);
        let response = self.send_packet(packet.as_bytes())?;

        if response == b"OK" {
            Ok(())
        } else if response.len() > 0 && response[0] == b'E' {
            let error_code = String::from_utf8_lossy(&response[1..3]);
            Err(Error::ProtocolError(format!(
                "Failed to remove breakpoint: {}",
                error_code
            )))
        } else {
            Err(Error::ProtocolError(
                "Unknown response for removing breakpoint".to_string(),
            ))
        }
    }

    // Continue execution
    pub fn continue_execution(&mut self) -> Result<(), Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        let response = self.send_packet(b"c")?;

        // Process response (typically will be a signal/stop reason)
        if response.len() > 0 && response[0] == b'T' {
            // Target stopped, extract signal
            let signal = String::from_utf8_lossy(&response[1..3]);
            println!("Program stopped (signal {})", signal);
            Ok(())
        } else if response.len() > 0 && response[0] == b'W' {
            // Target exited
            let exit_code = String::from_utf8_lossy(&response[1..3]);
            println!("Program exited with code {}", exit_code);
            Ok(())
        } else {
            Err(Error::ProtocolError(format!(
                "Unexpected response: {:?}",
                String::from_utf8_lossy(&response)
            )))
        }
    }

    // Single step
    pub fn step(&mut self) -> Result<(), Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        let response = self.send_packet(b"s")?;

        // Process response (typically will be a signal/stop reason)
        if response.len() > 0 && response[0] == b'T' {
            // Target stopped, extract signal
            let signal = String::from_utf8_lossy(&response[1..3]);
            println!("Program stopped after step (signal {})", signal);
            Ok(())
        } else {
            Err(Error::ProtocolError(format!(
                "Unexpected response: {:?}",
                String::from_utf8_lossy(&response)
            )))
        }
    }

    // Read registers
    pub fn read_registers(&mut self) -> Result<String, Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        // g packet for reading all registers
        let response = self.send_packet(b"g")?;

        if response.len() > 0 && response[0] == b'E' {
            let error_code = String::from_utf8_lossy(&response[1..3]);
            return Err(Error::ProtocolError(format!(
                "Failed to read registers: {}",
                error_code
            )));
        }

        // Parse register data
        // Format depends on target architecture, here we give a general display
        let mut result = String::new();
        let hex_data = String::from_utf8_lossy(&response);

        // Different architectures have different register sizes and count
        // For x86-64, registers are typically 8 bytes (16 hex chars)
        const REG_SIZE: usize = 16; // Hex chars per register
        let reg_names = [
            "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", "r9", "r10", "r11",
            "r12", "r13", "r14", "r15", "rip", "eflags", "cs", "ss", "ds", "es", "fs", "gs",
        ];

        for (i, chunk) in hex_data.as_bytes().chunks(REG_SIZE).enumerate() {
            if i >= reg_names.len() {
                break;
            }

            let reg_value = String::from_utf8_lossy(chunk);
            result.push_str(&format!("{:<8}: 0x{}\n", reg_names[i], reg_value));
        }

        Ok(result)
    }

    // Write to memory
    pub fn write_memory(&mut self, address: u64, data: &[u8]) -> Result<(), Error> {
        if !self.connected {
            return Err(Error::ConnectionError("Not connected".to_string()));
        }

        // Convert data to hex
        let mut hex_data = String::new();
        for byte in data {
            hex_data.push_str(&format!("{:02x}", byte));
        }

        // M addr,size:XX...
        let packet = format!("M {:x},{:x}:{}", address, data.len(), hex_data);
        let response = self.send_packet(packet.as_bytes())?;

        if response == b"OK" {
            Ok(())
        } else if response.len() > 0 && response[0] == b'E' {
            let error_code = String::from_utf8_lossy(&response[1..3]);
            Err(Error::ProtocolError(format!(
                "Failed to write memory: {}",
                error_code
            )))
        } else {
            Err(Error::ProtocolError(
                "Unknown response for memory write".to_string(),
            ))
        }
    }
}

fn hex_digit(digit: u8) -> u8 {
    match digit {
        0..=9 => b'0' + digit,
        10..=15 => b'a' + (digit - 10),
        _ => b'0',
    }
}