//! K403: bounded single-consumer UART receive buffer. //! Polling and the one-byte-at-a-time producer are deliberate test limits; //! interrupt-driven RX and line editing belong to later moves. pub const CAPACITY: usize = 16; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PushResult { Stored, Dropped, } pub struct RxBuffer { bytes: [u8; CAPACITY], head: usize, length: usize, dropped: u64, } impl RxBuffer { pub const fn new() -> Self { Self { bytes: [0; CAPACITY], head: 0, length: 0, dropped: 0, } } pub const fn len(&self) -> usize { self.length } pub const fn dropped(&self) -> u64 { self.dropped } pub fn push(&mut self, byte: u8) -> PushResult { if self.length == CAPACITY { self.dropped = self.dropped.saturating_add(1); return PushResult::Dropped; } let tail = (self.head + self.length) % CAPACITY; self.bytes[tail] = byte; self.length += 1; PushResult::Stored } pub fn pop(&mut self) -> Option { if self.length == 0 { return None; } let byte = self.bytes[self.head]; self.head = (self.head + 1) % CAPACITY; self.length -= 1; Some(byte) } } impl Default for RxBuffer { fn default() -> Self { Self::new() } } /// K404 delimiter classification. CR terminates immediately; only the LF /// directly following that CR is consumed, including across receive calls. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum LineEvent { Data(u8), End, SuppressedLf, } pub struct LineEndings { after_cr: bool, } impl LineEndings { pub const fn new() -> Self { Self { after_cr: false } } pub fn feed(&mut self, byte: u8) -> LineEvent { match byte { b'\r' => { self.after_cr = true; LineEvent::End } b'\n' if self.after_cr => { self.after_cr = false; LineEvent::SuppressedLf } b'\n' => LineEvent::End, other => { self.after_cr = false; LineEvent::Data(other) } } } pub fn reset(&mut self) { self.after_cr = false; } } impl Default for LineEndings { fn default() -> Self { Self::new() } } /// K405: one bounded line. BS and DEL erase only stored bytes; an empty /// backspace is observable but cannot underflow or touch the previous line. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum EditResult { Inserted, Full, Erased, Empty, } pub struct LineEditor { bytes: [u8; CAPACITY], length: usize, } impl LineEditor { pub const fn new() -> Self { Self { bytes: [0; CAPACITY], length: 0, } } pub fn feed(&mut self, byte: u8) -> EditResult { if byte == 0x08 || byte == 0x7f { if self.length == 0 { return EditResult::Empty; } self.length -= 1; self.bytes[self.length] = 0; return EditResult::Erased; } if self.length == CAPACITY { return EditResult::Full; } self.bytes[self.length] = byte; self.length += 1; EditResult::Inserted } pub fn as_bytes(&self) -> &[u8] { &self.bytes[..self.length] } pub fn clear(&mut self) { self.bytes[..self.length].fill(0); self.length = 0; } } impl Default for LineEditor { fn default() -> Self { Self::new() } } /// K406: an overlong line is rejected as a whole. Later backspaces cannot /// make a previously lost byte trustworthy; the next delimiter resynchronizes. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BoundedLineEvent { Inserted, Erased, Empty, OverflowStarted, Discarded, Complete([u8; CAPACITY], usize), TooLong, SuppressedLf, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CancelledLine { pub stored_bytes: usize, pub overflowed: bool, } pub struct BoundedLine { editor: LineEditor, endings: LineEndings, overflowed: bool, } impl BoundedLine { pub const fn new() -> Self { Self { editor: LineEditor::new(), endings: LineEndings::new(), overflowed: false, } } pub fn feed(&mut self, byte: u8) -> BoundedLineEvent { match self.endings.feed(byte) { LineEvent::Data(_) if self.overflowed => BoundedLineEvent::Discarded, LineEvent::Data(value) => match self.editor.feed(value) { EditResult::Inserted => BoundedLineEvent::Inserted, EditResult::Erased => BoundedLineEvent::Erased, EditResult::Empty => BoundedLineEvent::Empty, EditResult::Full => { self.overflowed = true; BoundedLineEvent::OverflowStarted } }, LineEvent::End if self.overflowed => { self.editor.clear(); self.overflowed = false; BoundedLineEvent::TooLong } LineEvent::End => { let mut bytes = [0; CAPACITY]; let len = self.editor.as_bytes().len(); bytes[..len].copy_from_slice(self.editor.as_bytes()); self.editor.clear(); BoundedLineEvent::Complete(bytes, len) } LineEvent::SuppressedLf => BoundedLineEvent::SuppressedLf, } } pub fn len(&self) -> usize { self.editor.as_bytes().len() } /// K418: Ctrl-C discards the entire in-progress line, including an /// overflow state and a pending CRLF suppression bit. pub fn cancel(&mut self) -> CancelledLine { let cancelled = CancelledLine { stored_bytes: self.editor.as_bytes().len(), overflowed: self.overflowed, }; self.editor.clear(); self.overflowed = false; self.endings.reset(); cancelled } } impl Default for BoundedLine { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn k403_full_buffer_reports_each_drop_without_overwriting_live_bytes() { let mut rx = RxBuffer::new(); for byte in 0..CAPACITY { assert_eq!(rx.push(byte as u8), PushResult::Stored); } for byte in 0..4 { assert_eq!(rx.push((CAPACITY + byte) as u8), PushResult::Dropped); } assert_eq!(rx.len(), CAPACITY); assert_eq!(rx.dropped(), 4); for byte in 0..CAPACITY { assert_eq!(rx.pop(), Some(byte as u8)); } assert_eq!(rx.pop(), None); assert_eq!(rx.dropped(), 4); } #[test] fn k403_wraparound_preserves_order_and_empty_transition() { let mut rx = RxBuffer::new(); assert_eq!(rx.pop(), None); for byte in 0..CAPACITY { assert_eq!(rx.push(byte as u8), PushResult::Stored); } for byte in 0..8 { assert_eq!(rx.pop(), Some(byte)); } for byte in 16..24 { assert_eq!(rx.push(byte), PushResult::Stored); } for byte in 8..24 { assert_eq!(rx.pop(), Some(byte)); } assert_eq!(rx.pop(), None); assert_eq!(rx.len(), 0); assert_eq!(rx.dropped(), 0); } #[test] fn k404_crlf_is_one_terminator_and_mixed_endings_keep_four_lines() { let mut endings = LineEndings::new(); let events = (*b"A\r\nB\nC\rD\r\n").map(|byte| endings.feed(byte)); assert_eq!( events, [ LineEvent::Data(b'A'), LineEvent::End, LineEvent::SuppressedLf, LineEvent::Data(b'B'), LineEvent::End, LineEvent::Data(b'C'), LineEvent::End, LineEvent::Data(b'D'), LineEvent::End, LineEvent::SuppressedLf, ] ); } #[test] fn k404_only_adjacent_lf_is_suppressed() { let mut endings = LineEndings::new(); assert_eq!(endings.feed(b'\r'), LineEvent::End); assert_eq!(endings.feed(b'X'), LineEvent::Data(b'X')); assert_eq!(endings.feed(b'\n'), LineEvent::End); assert_eq!(endings.feed(b'\r'), LineEvent::End); assert_eq!(endings.feed(b'\r'), LineEvent::End); assert_eq!(endings.feed(b'\n'), LineEvent::SuppressedLf); assert_eq!(endings.feed(b'\n'), LineEvent::End); } #[test] fn k405_empty_backspace_and_del_cannot_underflow_or_cross_line() { let mut editor = LineEditor::new(); assert_eq!(editor.feed(0x08), EditResult::Empty); assert_eq!(editor.feed(0x7f), EditResult::Empty); assert!(editor.as_bytes().is_empty()); assert_eq!(editor.feed(b'A'), EditResult::Inserted); editor.clear(); assert_eq!(editor.feed(0x08), EditResult::Empty); assert!(editor.as_bytes().is_empty()); } #[test] fn k405_erases_only_last_byte_and_recovers_at_full_capacity() { let mut editor = LineEditor::new(); for _ in 0..CAPACITY { assert_eq!(editor.feed(b'X'), EditResult::Inserted); } assert_eq!(editor.feed(b'Y'), EditResult::Full); assert_eq!(editor.as_bytes(), &[b'X'; CAPACITY]); assert_eq!(editor.feed(0x7f), EditResult::Erased); assert_eq!(editor.feed(b'Z'), EditResult::Inserted); assert_eq!(editor.as_bytes(), b"XXXXXXXXXXXXXXXZ"); assert_eq!(editor.feed(0x08), EditResult::Erased); assert_eq!(editor.feed(0x08), EditResult::Erased); assert_eq!(editor.as_bytes(), &[b'X'; CAPACITY - 2]); } #[test] fn k406_exact_limit_is_accepted_and_line_state_resets() { let mut line = BoundedLine::new(); for _ in 0..CAPACITY { assert_eq!(line.feed(b'B'), BoundedLineEvent::Inserted); } let BoundedLineEvent::Complete(bytes, len) = line.feed(b'\r') else { panic!("exact limit was not accepted"); }; assert_eq!(len, CAPACITY); assert_eq!(&bytes[..len], &[b'B'; CAPACITY]); assert_eq!(line.feed(b'\n'), BoundedLineEvent::SuppressedLf); assert_eq!(line.len(), 0); assert_eq!(line.feed(b'O'), BoundedLineEvent::Inserted); } #[test] fn k406_overflow_stays_rejected_after_backspace_then_recovers() { let mut line = BoundedLine::new(); for _ in 0..CAPACITY { assert_eq!(line.feed(b'C'), BoundedLineEvent::Inserted); } assert_eq!(line.feed(b'C'), BoundedLineEvent::OverflowStarted); assert_eq!(line.feed(0x08), BoundedLineEvent::Discarded); assert_eq!(line.feed(b'D'), BoundedLineEvent::Discarded); assert_eq!(line.feed(b'\n'), BoundedLineEvent::TooLong); assert_eq!(line.len(), 0); assert_eq!(line.feed(b'K'), BoundedLineEvent::Inserted); let BoundedLineEvent::Complete(bytes, len) = line.feed(b'\n') else { panic!("next line did not recover"); }; assert_eq!(&bytes[..len], b"K"); } #[test] fn k418_cancel_resets_stored_overflow_and_pending_cr_state() { let mut line = BoundedLine::new(); for _ in 0..CAPACITY { assert_eq!(line.feed(b'X'), BoundedLineEvent::Inserted); } assert_eq!(line.feed(b'X'), BoundedLineEvent::OverflowStarted); assert_eq!( line.cancel(), CancelledLine { stored_bytes: CAPACITY, overflowed: true } ); assert_eq!(line.len(), 0); assert_eq!( line.feed(b'\n'), BoundedLineEvent::Complete([0; CAPACITY], 0) ); assert_eq!( line.feed(b'\r'), BoundedLineEvent::Complete([0; CAPACITY], 0) ); assert_eq!( line.cancel(), CancelledLine { stored_bytes: 0, overflowed: false } ); assert_eq!( line.feed(b'\n'), BoundedLineEvent::Complete([0; CAPACITY], 0) ); assert_eq!(line.feed(b'O'), BoundedLineEvent::Inserted); let BoundedLineEvent::Complete(bytes, len) = line.feed(b'\n') else { panic!("new line did not recover"); }; assert_eq!(&bytes[..len], b"O"); } }