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
//! General queries and sets, `q` and `Q`, "Debugging with GDB" for GDB 18.1, section E.5: what the
//! server supports, the modes a debugger turns on, the target description and the memory map in
//! pieces, a checksum of memory, and the queries other files answer.

use std::fmt::Write as _;
use std::ops::Range;

use super::super::Core;
use super::memory::{Reads, span};
use super::packet::PACKET_SIZE;
use super::registers::description;
use super::threads::ours;
use super::{Asked, Debugger};
use crate::emulator::Alias;
use crate::soc::{Region, RegionKind};

/// What the server supports, as `qSupported` says it after the size of the largest packet it takes.
const FEATURES: &[&str] = &[
    "qXfer:features:read+",
    "vContSupported+",
    "multiprocess+",
    "swbreak+",
    "hwbreak+",
    "QStartNoAckMode+",
    "qXfer:memory-map:read+",
    "ConditionalBreakpoints+",
    "BreakpointCommands+",
    "QNonStop+",
];

impl Debugger<'_> {
    /// A `q` packet, whose name runs to its first `:` or `,`, or to its end.
    pub(super) fn query(&mut self, core: &mut Core, rest: &str) -> Asked {
        let reply = |text: &str| Asked::Reply(text.to_owned());
        let (name, arguments) = rest.split_once([':', ',']).unwrap_or((rest, ""));
        let negotiated = self.negotiated();
        match name {
            "Supported" => {
                let offered = |feature| arguments.split(';').any(|each| each == feature);
                if let Some(connection) = self.connection.as_mut() {
                    let negotiated = &mut connection.negotiated;
                    negotiated.multiprocess = offered("multiprocess+");
                    negotiated.swbreak = offered("swbreak+");
                    negotiated.hwbreak = offered("hwbreak+");
                }
                // A run that may go back is one GDB can take back, `bs` and `bc`.
                let reverse = if Self::reverses(core) {
                    ";ReverseStep+;ReverseContinue+"
                } else {
                    ""
                };
                reply(&format!(
                    "PacketSize={PACKET_SIZE:x};{}{reverse}",
                    FEATURES.join(";")
                ))
            }
            "Xfer" => match arguments.strip_prefix("memory-map:read:") {
                Some(annex) => reply(&piece(&self.map.xml, annex.strip_prefix(':'))),
                None => reply(&features(arguments, &description(core.machine.cpu()))),
            },
            "Attached" if arguments.is_empty() || ours(Some(arguments)) => reply("1"),
            "Attached" => reply("E01"),
            "Symbol" => reply(&self.threads.symbol(arguments)),
            "ThreadExtraInfo" => reply(&self.threads.extra_info(core, arguments)),
            "Rcmd" => self.monitor(core, arguments),
            "CRC" => reply(&checksum(core, arguments)),
            _ => {
                let attached = self.attached();
                reply(
                    &self
                        .threads
                        .query(core, rest, negotiated, attached)
                        .unwrap_or_default(),
                )
            }
        }
    }

    /// A `Q` packet.
    pub(super) fn general_set(&mut self, rest: &str) -> Asked {
        match (rest, self.connection.as_mut()) {
            // The `+` for this packet has gone, and the `OK` is the last packet GDB acknowledges.
            ("StartNoAckMode", Some(connection)) => {
                connection.negotiated.no_ack = true;
                Asked::Reply("OK".to_owned())
            }
            _ => Asked::Reply(String::new()),
        }
    }
}

/// `qCRC:ADDR,LENGTH`, the checksum of that much memory as a debugger peeks it, `C` and the CRC in
/// hex, or `E01` when something in the span doesn't answer.
fn checksum(core: &mut Core, arguments: &str) -> String {
    let Some((address, length)) = span(arguments) else {
        return "E01".to_owned();
    };
    let bytes = core.debugger_read(address, length, Reads::Peeks);
    if bytes.len() as u64 == length {
        format!("C{:08x}", crc(&bytes))
    } else {
        "E01".to_owned()
    }
}

/// The CRC-32 "Debugging with GDB" for GDB 18.1, section E.5 gives `qCRC`: IEEE 802.3's polynomial,
/// a byte at a time with its most significant bit first, from all ones, and never inverted at the
/// end.
fn crc(bytes: &[u8]) -> u32 {
    bytes.iter().fold(u32::MAX, |crc, &byte| {
        (0..8).fold(crc ^ u32::from(byte) << 24, |crc, _| {
            if crc & 1 << 31 == 0 {
                crc << 1
            } else {
                crc << 1 ^ IEEE_802_3
            }
        })
    })
}

/// IEEE 802.3's CRC-32 polynomial, its leading term left implicit.
const IEEE_802_3: u32 = 0x04C1_1DB7;

/// `qXfer`'s read of the target description, `features:read:target.xml:OFFSET,LENGTH`, answered
/// from `target_xml`, the one [`description`] gives. Another object is one the server doesn't
/// have, the empty reply, and another annex, or a span that doesn't read, is `E00`.
fn features(arguments: &str, target_xml: &str) -> String {
    let Some(annex) = arguments.strip_prefix("features:read:") else {
        return String::new();
    };
    piece(target_xml, annex.strip_prefix("target.xml:"))
}

/// The piece of `xml` that `span`, a `qXfer` read's `OFFSET,LENGTH`, asks for, `m` before it when
/// there's more and `l` when there isn't, or `E00` for an annex or a span that doesn't read,
/// "Debugging with GDB" for GDB 18.1, section E.5.
fn piece(xml: &str, span: Option<&str>) -> String {
    let Some((offset, length)) = span.and_then(super::memory::span) else {
        return "E00".to_owned();
    };
    let xml = xml.as_bytes();
    let start = usize::try_from(offset).unwrap_or(usize::MAX).min(xml.len());
    let end = start
        .saturating_add(usize::try_from(length).unwrap_or(usize::MAX))
        .min(xml.len());
    let more = if end < xml.len() { 'm' } else { 'l' };
    let chunk: String = xml[start..end]
        .iter()
        .map(|&byte| char::from(byte))
        .collect();
    format!("{more}{chunk}")
}

/// The memory map a debugger reads with `qXfer:memory-map:read`, "Debugging with GDB" for GDB 18.1,
/// section E.17, and where flash erases, which `vFlashErase` asks.
#[derive(Debug, Default)]
pub(in super::super) struct Map {
    xml: String,
    /// Each flash region, at its base and at each alias, as what it erases to, and the address each
    /// of its blocks starts at and the one past its last.
    flash: Vec<(u8, Vec<u64>)>,
}

impl Map {
    /// The map of `regions`, each at its base and its `aliases`: flash as `flash` in its blocks,
    /// RAM as `ram`, and the rest as `rom`. The `peripherals` and the processor's `own` space are
    /// `ram`, so a debugger reads them, merged into ranges that overlap nothing else, since GDB
    /// refuses a map whose regions overlap.
    pub(in super::super) fn new(
        regions: &[Region],
        aliases: &[Alias],
        peripherals: &[Range<u64>],
        own: &[Range<u64>],
    ) -> Self {
        let mut map = Self::default();
        let mut entries = Vec::new();
        let mut taken = Vec::new();
        for region in regions {
            let bases = aliases
                .iter()
                .filter(|alias| alias.region == region.name)
                .map(|alias| alias.at);
            for base in std::iter::once(region.base).chain(bases) {
                taken.push(base..base + region.size);
                match region.kind {
                    RegionKind::Flash => map.flash(base, region, &mut entries),
                    RegionKind::Ram => entries.push((base, memory("ram", base, region.size, None))),
                    RegionKind::System => {
                        entries.push((base, memory("rom", base, region.size, None)));
                    }
                }
            }
        }
        for range in disjoint(peripherals.iter().chain(own).cloned().collect(), &taken) {
            entries.push((
                range.start,
                memory("ram", range.start, range.end - range.start, None),
            ));
        }
        entries.sort_by_key(|&(start, _)| start);
        map.xml.push_str(MAP_HEADER);
        for (_, entry) in entries {
            map.xml.push_str(&entry);
        }
        map.xml.push_str("</memory-map>\n");
        map
    }

    /// `region`'s flash at `base`: one entry for each run of blocks of one size, a region without
    /// blocks being one block, and where each block starts.
    fn flash(&mut self, base: u64, region: &Region, entries: &mut Vec<(u64, String)>) {
        let runs: Vec<(u64, u64)> = if region.blocks.is_empty() {
            vec![(region.size, 1)]
        } else {
            region
                .blocks
                .iter()
                .map(|blocks| (blocks.size.get(), u64::from(blocks.count.get())))
                .collect()
        };
        let mut starts = vec![base];
        for (size, count) in runs {
            let at = *starts.last().expect("the region's base");
            entries.push((at, memory("flash", at, size * count, Some(size))));
            starts.extend((1..=count).map(|block| at + block * size));
        }
        self.flash.push((region.erased, starts));
    }

    /// What `length` bytes from `address` erase to, when they're whole blocks of one flash region.
    pub(super) fn erases(&self, address: u64, length: u64) -> Option<u8> {
        self.flash
            .iter()
            .find(|(_, starts)| {
                length > 0
                    && starts.binary_search(&address).is_ok()
                    && starts.binary_search(&(address + length)).is_ok()
            })
            .map(|&(erased, _)| erased)
    }

    /// Whether `length` bytes from `address` are all in one flash region.
    pub(super) fn holds(&self, address: u64, length: u64) -> bool {
        self.flash.iter().any(|(_, starts)| {
            starts.first().is_some_and(|&first| first <= address)
                && starts.last().is_some_and(|&last| address + length <= last)
        })
    }
}

/// What a memory map starts with, section E.17.
const MAP_HEADER: &str = "<?xml version=\"1.0\"?>\n<!DOCTYPE memory-map PUBLIC \"+//IDN gnu.org//DTD GDB \
                          Memory Map V1.0//EN\" \"http://sourceware.org/gdb/gdb-memory-map.dtd\">\n\
                          <memory-map>\n";

/// One region of a memory map, E.17, with its block size when it's flash.
fn memory(kind: &str, start: u64, length: u64, blocks: Option<u64>) -> String {
    let mut entry = format!("<memory type=\"{kind}\" start=\"{start:#x}\" length=\"{length:#x}\"");
    match blocks {
        Some(size) => writeln!(
            entry,
            "><property name=\"blocksize\">{size:#x}</property></memory>"
        ),
        None => writeln!(entry, "/>"),
    }
    .expect("a String takes it");
    entry
}

/// `ranges` merged where they overlap or touch, less every address in `taken`, in order.
fn disjoint(mut ranges: Vec<Range<u64>>, taken: &[Range<u64>]) -> Vec<Range<u64>> {
    ranges.sort_by_key(|range| range.start);
    let mut merged: Vec<Range<u64>> = Vec::new();
    for range in ranges {
        match merged.last_mut() {
            Some(last) if range.start <= last.end => last.end = last.end.max(range.end),
            _ => merged.push(range),
        }
    }
    for cut in taken {
        merged = merged
            .into_iter()
            .flat_map(|range| {
                [
                    range.start..cut.start.min(range.end),
                    cut.end.max(range.start)..range.end,
                ]
            })
            .filter(|range| !range.is_empty())
            .collect();
    }
    merged
}

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

    #[test]
    fn the_target_description_comes_in_pieces_and_says_when_it_ends() {
        const XML: &str =
            r#"<?xml version="1.0"?><target><architecture>any</architecture></target>"#;
        let first = features("features:read:target.xml:0,10", XML);
        assert!(first.starts_with('m'), "{first}");
        assert_eq!(first.len(), 0x11);
        let whole = XML.len();
        let last = features(
            &format!("features:read:target.xml:{:x},1000", whole - 4),
            XML,
        );
        let tail = last.strip_prefix('l').expect("the last piece");
        assert_eq!(tail.len(), 4);
        assert!(XML.ends_with(tail), "{last}");
    }

    /// Ranges that overlap or touch merge, and lose what the regions take, so the map's entries
    /// never overlap.
    #[test]
    fn ranges_merge_where_they_meet_and_lose_what_the_regions_take() {
        let ranges = vec![0x30..0x40, 0x10..0x20, 0x18..0x30, 0x50..0x60, 0x70..0x80];
        assert_eq!(
            disjoint(ranges, &[0x1C..0x24, 0x55..0x58, 0x70..0x90]),
            [0x10..0x1C, 0x24..0x40, 0x50..0x55, 0x58..0x60]
        );
    }

    /// Section E.5's CRC from its definition: nothing leaves the initial all ones, a zero byte is
    /// what a 32-bit shift register gives fed its eight bits most significant first, and the nine
    /// digits `123456789`, the usual check string, give `0376e6e7`. A CRC-32 that reflected its
    /// bytes or inverted its end, as the separate debug files' does, gives neither.
    #[test]
    fn qcrc_matches_e4s_crc_of_the_same_bytes() {
        assert_eq!(crc(&[]), 0xFFFF_FFFF);
        assert_eq!(crc(&[0]), 0x4E08_BFB4);
        assert_eq!(crc(b"123456789"), 0x0376_E6E7);
    }
}