use std::fmt::Write as _;
use std::io::{self, ErrorKind, Read, Write};
use std::net::TcpStream;
use super::Held;
pub(super) const PACKET_SIZE: usize = 0x1000;
const INTERRUPT: u8 = 0x03;
pub(super) trait Carry: Read + Write + Send {
fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()>;
}
impl Carry for TcpStream {
fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
TcpStream::set_nonblocking(self, nonblocking)
}
}
#[derive(Clone, Copy, Debug, Default)]
#[expect(
clippy::struct_excessive_bools,
reason = "each is one feature or mode the debugger names on its own, and no two make a state"
)]
pub(super) struct Negotiated {
pub(super) multiprocess: bool,
pub(super) swbreak: bool,
pub(super) hwbreak: bool,
pub(super) no_ack: bool,
pub(super) extended: bool,
pub(super) non_stop: bool,
}
#[expect(
clippy::struct_excessive_bools,
reason = "whether it has the core, has taken it, an interrupt waits, a packet is dropped, the far end closed and a stop is told are six things apart"
)]
pub(super) struct Connection {
pub(super) stream: Box<dyn Carry>,
pub(super) negotiated: Negotiated,
pub(super) attached: bool,
pub(super) taken: bool,
pub(super) unspoken: Option<(Held, bool)>,
received: Vec<u8>,
sent: Vec<u8>,
pub(super) interrupt: bool,
dropping: bool,
closed: bool,
pub(super) telling: bool,
}
impl Connection {
pub(super) fn new(stream: Box<dyn Carry>, found: (Held, bool)) -> Self {
Self {
stream,
negotiated: Negotiated::default(),
attached: true,
taken: false,
unspoken: Some(found),
received: Vec::new(),
sent: Vec::new(),
interrupt: false,
dropping: false,
closed: false,
telling: false,
}
}
pub(super) fn send(&mut self, contents: &str) -> io::Result<()> {
self.sent = frame(b'$', contents);
self.stream.write_all(&self.sent)
}
pub(super) fn notify(&mut self, contents: &str) -> io::Result<()> {
self.stream.write_all(&frame(b'%', contents))
}
pub(super) fn interrupted(&mut self) -> io::Result<bool> {
self.receive_waiting(64)?;
let mut interrupted = false;
let mut inside = false;
let mut checksum = 0;
self.received.retain(|&byte| {
if inside {
inside = byte != b'#';
checksum = if inside { 0 } else { 2 };
} else if checksum > 0 {
checksum -= 1;
} else if byte == INTERRUPT {
interrupted = true;
return false;
} else {
inside = byte == b'$';
}
true
});
let over = self.received.len().saturating_sub(PACKET_SIZE + 3);
self.received.drain(..over);
if self.closed && !interrupted {
return Err(io::Error::from(ErrorKind::UnexpectedEof));
}
Ok(interrupted)
}
pub(super) fn poll(&mut self) -> io::Result<Vec<Framed>> {
self.receive_waiting(4096)?;
let mut framed = Vec::new();
while let Some(each) = take_packet(&mut self.received, &mut self.dropping) {
framed.extend(self.acknowledge(each)?);
}
if self.closed && framed.is_empty() {
return Err(io::Error::from(ErrorKind::UnexpectedEof));
}
Ok(framed)
}
pub(super) fn hear_out(&mut self) {
let mut bytes = [0; PACKET_SIZE];
for _ in 0..4 {
if !matches!(self.stream.read(&mut bytes), Ok(1..)) {
return;
}
}
}
pub(super) fn closed(&mut self) -> bool {
let most = self.received.len() + 4 * PACKET_SIZE;
while !self.closed && self.received.len() < most {
let before = self.received.len();
if self.receive_waiting(PACKET_SIZE).is_err() {
self.closed = true;
}
if self.received.len() == before {
break;
}
}
self.closed
}
pub(super) fn next_packet(&mut self) -> io::Result<String> {
loop {
if let Some(framed) = take_packet(&mut self.received, &mut self.dropping) {
match self.acknowledge(framed)? {
Some(Framed::Packet(contents)) => return Ok(contents),
Some(Framed::Interrupt) => self.interrupt = true,
_ => {}
}
continue;
}
if self.closed {
return Err(io::Error::from(ErrorKind::UnexpectedEof));
}
let mut bytes = [0; 4096];
match self.stream.read(&mut bytes)? {
0 => self.closed = true,
read => self.received.extend_from_slice(&bytes[..read]),
}
}
}
fn receive_waiting(&mut self, most: usize) -> io::Result<()> {
if self.closed {
return Ok(());
}
self.stream.set_nonblocking(true)?;
let mut bytes = vec![0; most];
let read = match self.stream.read(&mut bytes) {
Ok(0) => {
self.closed = true;
Ok(0)
}
Ok(read) => Ok(read),
Err(error) if error.kind() == ErrorKind::WouldBlock => Ok(0),
Err(error) => Err(error),
};
self.stream.set_nonblocking(false)?;
self.received.extend_from_slice(&bytes[..read?]);
Ok(())
}
fn acknowledge(&mut self, framed: Framed) -> io::Result<Option<Framed>> {
let acknowledged = !self.negotiated.no_ack;
match framed {
Framed::Packet(_) => {
self.unspoken = None;
if acknowledged {
self.stream.write_all(b"+")?;
}
Ok(Some(framed))
}
Framed::Interrupt => Ok(Some(framed)),
Framed::Corrupt if acknowledged => self.stream.write_all(b"-").map(|()| None),
Framed::Again if acknowledged => self.stream.write_all(&self.sent).map(|()| None),
Framed::Corrupt | Framed::Again => Ok(None),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Framed {
Packet(String),
Corrupt,
Again,
Interrupt,
}
fn take_packet(received: &mut Vec<u8>, dropping: &mut bool) -> Option<Framed> {
if *dropping {
let Some(end) = received.iter().position(|&byte| byte == b'#') else {
received.clear();
return None;
};
if received.len() < end + 3 {
received.drain(..end);
return None;
}
received.drain(..end + 3);
*dropping = false;
}
let Some(start) = received
.iter()
.position(|&byte| matches!(byte, b'$' | b'-' | INTERRUPT))
else {
received.clear();
return None;
};
received.drain(..start);
match received[0] {
b'-' => {
received.remove(0);
return Some(Framed::Again);
}
INTERRUPT => {
received.remove(0);
return Some(Framed::Interrupt);
}
_ => {}
}
let Some(end) = received.iter().position(|&byte| byte == b'#') else {
if received.len() - 1 > PACKET_SIZE {
received.clear();
*dropping = true;
return Some(Framed::Corrupt);
}
return None;
};
let checksum = received.get(end + 1..end + 3)?;
let sent = std::str::from_utf8(checksum)
.ok()
.and_then(|text| u8::from_str_radix(text, 16).ok());
let contents = &received[1..end];
let framed = if sent == Some(sum(contents)) {
Framed::Packet(contents.iter().map(|&byte| char::from(byte)).collect())
} else {
Framed::Corrupt
};
received.drain(..end + 3);
Some(framed)
}
fn sum(bytes: &[u8]) -> u8 {
bytes.iter().fold(0, |sum, &byte| sum.wrapping_add(byte))
}
fn frame(start: u8, contents: &str) -> Vec<u8> {
let mut escaped = Vec::with_capacity(contents.len());
for byte in contents.bytes() {
if matches!(byte, b'#' | b'$' | b'}' | b'*') {
escaped.extend([b'}', byte ^ 0x20]);
} else {
escaped.push(byte);
}
}
let mut framed = vec![start];
framed.extend_from_slice(&escaped);
framed.extend(format!("#{:02x}", sum(&escaped)).bytes());
framed
}
pub(super) fn hex(text: &str) -> Option<u64> {
u64::from_str_radix(text, 16).ok()
}
pub(super) fn to_hex(bytes: &[u8]) -> String {
let mut text = String::with_capacity(bytes.len() * 2);
for byte in bytes {
write!(text, "{byte:02x}").expect("a String takes what's written to it");
}
text
}
pub(super) fn from_hex(text: &str) -> Option<Vec<u8>> {
(0..text.len())
.step_by(2)
.map(|at| u8::from_str_radix(text.get(at..at + 2)?, 16).ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
struct Waiting(std::collections::VecDeque<u8>);
impl Read for Waiting {
fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
if self.0.is_empty() {
return Err(io::Error::from(ErrorKind::WouldBlock));
}
self.0.read(bytes)
}
}
impl Write for Waiting {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Carry for Waiting {
fn set_nonblocking(&self, _nonblocking: bool) -> io::Result<()> {
Ok(())
}
}
#[test]
fn a_running_core_hears_its_interrupt_and_keeps_a_packets_worth() {
let packet = b"$X20000000,1:\x03#7a";
let sent = |bytes: &[u8]| {
Connection::new(
Box::new(Waiting(bytes.iter().copied().collect())),
(Held::Running, false),
)
};
let mut connection = sent(packet);
assert!(!connection.interrupted().unwrap(), "inside a packet");
assert_eq!(connection.received, packet);
let mut connection = sent(&[packet.as_slice(), &[INTERRUPT]].concat());
assert!(connection.interrupted().unwrap());
assert_eq!(connection.received, packet);
let mut connection = sent(&vec![b'a'; 64 << 10]);
for _ in 0..1024 {
assert!(!connection.interrupted().unwrap());
}
assert!(connection.received.len() <= PACKET_SIZE + 3);
}
#[test]
fn a_packet_is_framed_and_read_back() {
let framed = frame(b'$', "m8000000,4");
assert_eq!(framed, b"$m8000000,4#25");
let mut received = b"+".to_vec();
received.extend_from_slice(&framed);
received.extend_from_slice(b"$g#67");
assert_eq!(
take_packet(&mut received, &mut false),
Some(Framed::Packet("m8000000,4".to_owned()))
);
assert_eq!(
take_packet(&mut received, &mut false),
Some(Framed::Packet("g".to_owned()))
);
assert!(received.is_empty());
}
#[test]
fn a_packet_waits_for_its_checksum_and_a_bad_one_is_corrupt() {
let take = |bytes: &[u8]| take_packet(&mut bytes.to_vec(), &mut false);
assert_eq!(take(b"$g#6"), None);
assert_eq!(take(b"$g#00"), Some(Framed::Corrupt));
assert_eq!(take(b"\x03"), Some(Framed::Interrupt));
assert_eq!(take(b"-"), Some(Framed::Again));
}
#[test]
fn nothing_piles_up_outside_a_packet_or_past_the_largest_one() {
let mut dropping = false;
let mut received = b"++noise".to_vec();
assert_eq!(take_packet(&mut received, &mut dropping), None);
assert!(received.is_empty());
let mut received = vec![b'+', b'$'];
received.resize(2 + PACKET_SIZE, b'a');
assert_eq!(
take_packet(&mut received, &mut dropping),
None,
"as long as one can be"
);
received.push(b'a');
assert_eq!(
take_packet(&mut received, &mut dropping),
Some(Framed::Corrupt)
);
assert!(received.is_empty());
assert!(dropping);
}
#[test]
fn the_rest_of_a_dropped_packet_is_passed_over_through_its_checksum() {
let mut dropping = true;
let mut received = b"a-a\x03a#0".to_vec();
assert_eq!(
take_packet(&mut received, &mut dropping),
None,
"its checksum is still to come"
);
received.extend_from_slice(b"0$g#67");
assert_eq!(
take_packet(&mut received, &mut dropping),
Some(Framed::Packet("g".to_owned()))
);
assert!(received.is_empty());
assert!(!dropping);
}
#[test]
fn a_reply_escapes_what_the_protocol_reserves() {
assert_eq!(frame(b'$', "a}b"), b"$a}]b#9d");
}
#[test]
fn hex_reads_back_what_it_writes() {
assert_eq!(to_hex(&[0x00, 0xAB, 0x10]), "00ab10");
assert_eq!(from_hex("00ab10"), Some(vec![0x00, 0xAB, 0x10]));
assert_eq!(from_hex("0g"), None);
}
}