rigless 0.2.2

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! What the firmware asks of the host while a debugger has it: its console in GDB's, as `O`
//! packets, "Debugging with GDB" for GDB 18.1, section E.4, and its files on GDB's host, through
//! the File-I/O extension of section E.14. GDB waits on the core through both, as it waits for a
//! stop. The section numbers here are those of the manual for GDB 18.1.

use std::io::{self, ErrorKind};

use super::super::Core;
use super::history::Input;
use super::packet::{PACKET_SIZE, hex, to_hex};
use super::threads::Reason;
use super::{Debugger, Held};

/// What GDB answered a file call with, section E.14's `F` reply.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in super::super) struct Fresult {
    /// The call's return code.
    pub(in super::super) result: i64,
    /// The error number it set, in the protocol's own numbering, when it set one.
    pub(in super::super) errno: Option<u64>,
}

impl Debugger<'_> {
    /// Whether a debugger has the core and waits on it, which it does while the core runs or steps,
    /// except in non-stop mode, where section E.4 allows no `O` packet and a file call answers -1.
    pub(in super::super) fn waits_on_core(&self) -> bool {
        self.attached() && self.core != Held::Halted && !self.non_stop()
    }

    /// Shows `bytes` the firmware wrote to its console in GDB's, as `O` packets while GDB waits on
    /// the core, section E.4. A connection that fails goes as any other does.
    pub(in super::super) fn console(&mut self, core: &mut Core, bytes: &[u8]) {
        if !self.waits_on_core() {
            return;
        }
        let sent = bytes
            .chunks((PACKET_SIZE - 1) / 2)
            .try_for_each(|chunk| self.connection()?.send(&format!("O{}", to_hex(chunk))));
        if sent.is_err() {
            self.disconnect(core);
        }
    }

    /// Has GDB make the file call `request`, such as `Fopen,PATHPTR/LEN,FLAGS,MODE`, section E.14,
    /// in place of a stop reply, answering the memory reads and writes GDB makes while it does. A
    /// Ctrl-C GDB sends back with its answer stops the core once the call is answered, as an
    /// interrupt does. `None` when no debugger waits on the core, or the connection failed.
    pub(in super::super) fn file_call(
        &mut self,
        core: &mut Core,
        request: &str,
    ) -> Option<Fresult> {
        // A run going forward over a call it made has GDB's answer already, and asks nothing.
        if let Some(answer) = self.answered_here(core) {
            return answer;
        }
        let here = self.new_input(core);
        let answer = if self.waits_on_core() {
            let answer = self.converse_call(core, request);
            if answer.is_err() {
                self.disconnect(core);
            }
            answer.ok().flatten()
        } else {
            None
        };
        self.noted(here, Input::HostCall(answer));
        answer
    }

    /// The exchange [`Debugger::file_call`] makes, until GDB's `F` reply.
    fn converse_call(&mut self, core: &mut Core, request: &str) -> io::Result<Option<Fresult>> {
        self.connection()?.send(request)?;
        loop {
            let packet = match self.connection()?.next_packet() {
                Err(error)
                    if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) =>
                {
                    self.refuse_others();
                    continue;
                }
                packet => packet?,
            };
            if let Some(reply) = packet.strip_prefix('F') {
                let Some((answer, interrupted)) = f_reply(reply) else {
                    return Ok(None);
                };
                // A Ctrl-C GDB sent on its own while it made the call is the `C` flag's.
                self.interrupted_call =
                    interrupted || std::mem::take(&mut self.connection()?.interrupt);
                return Ok(Some(answer));
            }
            // Section E.14 has GDB read and write the target's memory, and nothing else, while it
            // makes the call.
            let reply = match packet.chars().next() {
                Some('m' | 'M' | 'X') => match self.answer(core, &packet) {
                    super::Asked::Reply(reply) => reply,
                    _ => String::new(),
                },
                _ => String::new(),
            };
            self.connection()?.send(&reply)?;
        }
    }

    /// Stops the core for the Ctrl-C GDB sent back with the file call just answered, if it did,
    /// with the interrupt's stop reply, section E.14's "The Ctrl-C Message".
    pub(in super::super) fn after_file_call(&mut self, core: &mut Core) {
        if std::mem::take(&mut self.interrupted_call)
            && self.halt_core(core, Reason::Interrupt).is_err()
        {
            self.disconnect(core);
        }
    }

    /// Tells an attached debugger that the firmware ended the run with `code`, of which a stop
    /// reply carries the low byte, section E.4's `W`.
    pub(in super::super) fn ended(mut self, code: u32) {
        self.exit_reply(code & 0xFF);
    }
}

/// An `F` reply's contents after the `F`: the return code and the error number in hex, and `C`
/// when the user pressed Ctrl-C, then an attachment the server has no use for.
fn f_reply(reply: &str) -> Option<(Fresult, bool)> {
    let (fields, _attachment) = reply.split_once(';').unwrap_or((reply, ""));
    let mut fields = fields.split(',');
    let code = fields.next()?;
    let result = match code.strip_prefix('-') {
        Some(magnitude) => -i64::try_from(hex(magnitude)?).ok()?,
        None => i64::try_from(hex(code)?).ok()?,
    };
    let errno = match fields.next() {
        Some(errno) => Some(hex(errno)?),
        None => None,
    };
    let interrupted = match fields.next() {
        Some("C") => true,
        Some(_) => return None,
        None => false,
    };
    Some((Fresult { result, errno }, interrupted))
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Section E.14's reply: a return code, negative or not, an error number that a successful
    /// call may leave out, and the Ctrl-C flag, which comes with an error number.
    #[test]
    fn an_f_reply_reads_its_code_errno_and_ctrl_c() {
        let reply = |result, errno| Fresult { result, errno };
        assert_eq!(f_reply("6"), Some((reply(6, None), false)));
        assert_eq!(f_reply("-1,9"), Some((reply(-1, Some(9)), false)));
        assert_eq!(f_reply("-1,4,C"), Some((reply(-1, Some(4)), true)));
        assert_eq!(f_reply("0,0,C;ignored"), Some((reply(0, Some(0)), true)));
        assert_eq!(f_reply("1f"), Some((reply(0x1f, None), false)));
        for bad in ["", "x", "1,y", "1,2,D", "-"] {
            assert_eq!(f_reply(bad), None, "{bad}");
        }
    }
}