use core::mem;
use heapless::Deque;
#[derive(Debug)]
pub struct HostOutputQueue<const CAPACITY: usize> {
bytes: Deque<u8, CAPACITY>,
dropped_bytes: usize,
}
impl<const CAPACITY: usize> HostOutputQueue<CAPACITY> {
pub const fn new() -> Self {
Self {
bytes: Deque::new(),
dropped_bytes: 0,
}
}
pub fn begin_transaction(&mut self) -> HostOutputTransaction<'_, CAPACITY> {
HostOutputTransaction {
queue: self,
accepted_bytes: 0,
source_bytes: 0,
overflowed: false,
}
}
pub fn enqueue(&mut self, bytes: &[u8]) {
self.begin_transaction().enqueue(bytes);
}
pub fn dequeue(&mut self, output: &mut [u8]) -> usize {
let mut count = 0;
for slot in output {
let Some(byte) = self.bytes.pop_front() else {
break;
};
*slot = byte;
count += 1;
}
count
}
pub fn take_dropped_bytes(&mut self) -> usize {
mem::take(&mut self.dropped_bytes)
}
}
pub struct HostOutputTransaction<'a, const CAPACITY: usize> {
queue: &'a mut HostOutputQueue<CAPACITY>,
accepted_bytes: usize,
source_bytes: usize,
overflowed: bool,
}
impl<const CAPACITY: usize> HostOutputTransaction<'_, CAPACITY> {
pub fn enqueue(&mut self, bytes: &[u8]) {
self.source_bytes = self.source_bytes.saturating_add(bytes.len());
if self.overflowed {
return;
}
for &byte in bytes {
if self.queue.bytes.push_back(byte).is_err() {
self.overflowed = true;
self.rollback();
return;
}
self.accepted_bytes += 1;
}
}
pub fn has_activity(&self) -> bool {
self.source_bytes != 0
}
fn rollback(&mut self) {
for _ in 0..self.accepted_bytes {
self.queue
.bytes
.pop_back()
.expect("accepted transaction bytes remain at the queue tail");
}
self.accepted_bytes = 0;
}
}
impl<const CAPACITY: usize> Drop for HostOutputTransaction<'_, CAPACITY> {
fn drop(&mut self) {
if self.overflowed {
self.queue.dropped_bytes = self.queue.dropped_bytes.saturating_add(self.source_bytes);
}
}
}
impl<const CAPACITY: usize> Default for HostOutputQueue<CAPACITY> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_fifo_order_without_overflow() {
let mut queue = HostOutputQueue::<8>::new();
queue.enqueue(b"abc");
queue.enqueue(b"def");
let mut output = [0; 8];
assert_eq!(queue.dequeue(&mut output), 6);
assert_eq!(&output[..6], b"abcdef");
assert_eq!(queue.take_dropped_bytes(), 0);
}
#[test]
fn overflow_preserves_queued_transactions_and_drops_the_new_one() {
let mut queue = HostOutputQueue::<4>::new();
queue.enqueue(b"abc");
queue.enqueue(b"def");
let mut output = [0; 4];
assert_eq!(queue.dequeue(&mut output), 3);
assert_eq!(&output[..3], b"abc");
assert_eq!(queue.take_dropped_bytes(), 3);
assert_eq!(queue.take_dropped_bytes(), 0);
}
#[test]
fn oversized_transaction_is_dropped_in_full() {
let mut queue = HostOutputQueue::<3>::new();
queue.enqueue(b"12345");
let mut output = [0; 3];
assert_eq!(queue.dequeue(&mut output), 0);
assert_eq!(queue.take_dropped_bytes(), 5);
}
#[test]
fn streamed_overflow_rolls_back_every_chunk_of_the_transaction() {
let mut queue = HostOutputQueue::<5>::new();
queue.enqueue(b"old");
{
let mut transaction = queue.begin_transaction();
transaction.enqueue(b"12");
transaction.enqueue(b"34");
assert!(transaction.has_activity());
}
let mut output = [0; 5];
assert_eq!(queue.dequeue(&mut output), 3);
assert_eq!(&output[..3], b"old");
assert_eq!(queue.take_dropped_bytes(), 4);
}
}