use core::ops::ControlFlow;
pub trait DrainSink {
type Item: ?Sized;
fn accept(&mut self, item: &Self::Item) -> ControlFlow<()>;
#[must_use]
fn has_capacity(&self) -> bool {
true
}
}
pub struct RingSink<const SLOTS: usize, const SLOT: usize> {
arena: [[u8; SLOT]; SLOTS],
lengths: [usize; SLOTS],
read: usize,
write: usize,
count: usize,
}
impl<const SLOTS: usize, const SLOT: usize> Default for RingSink<SLOTS, SLOT> {
fn default() -> Self {
Self::new()
}
}
impl<const SLOTS: usize, const SLOT: usize> RingSink<SLOTS, SLOT> {
#[must_use]
pub const fn new() -> Self {
Self {
arena: [[0u8; SLOT]; SLOTS],
lengths: [0usize; SLOTS],
read: 0,
write: 0,
count: 0,
}
}
#[must_use]
pub fn is_full(&self) -> bool {
self.count == SLOTS
}
#[must_use]
pub fn len(&self) -> usize {
self.count
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.count == 0
}
#[must_use]
pub fn pop(&mut self) -> Option<&[u8]> {
if self.count == 0 {
return None;
}
let slot = self.read % SLOTS;
self.read = self.read.wrapping_add(1);
self.count -= 1;
Some(&self.arena[slot][..self.lengths[slot]])
}
}
impl<const SLOTS: usize, const SLOT: usize> DrainSink for RingSink<SLOTS, SLOT> {
type Item = [u8];
fn accept(&mut self, frame: &[u8]) -> ControlFlow<()> {
if self.count == SLOTS || frame.len() > SLOT {
return ControlFlow::Break(());
}
let slot = self.write % SLOTS;
self.arena[slot][..frame.len()].copy_from_slice(frame);
self.lengths[slot] = frame.len();
self.write = self.write.wrapping_add(1);
self.count += 1;
ControlFlow::Continue(())
}
fn has_capacity(&self) -> bool {
!self.is_full()
}
}
#[cfg(feature = "io-async")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum RingSinkWriteError {
#[error("write of {len} bytes exceeds the ring's {slot}-byte slot capacity")]
FrameTooLarge {
len: usize,
slot: usize,
},
}
#[cfg(feature = "io-async")]
impl<const SLOTS: usize, const SLOT: usize> proxima_core::io::AsyncWrite for RingSink<SLOTS, SLOT> {
type Error = RingSinkWriteError;
fn poll_write(
self: core::pin::Pin<&mut Self>,
_cx: &mut core::task::Context<'_>,
buf: &[u8],
) -> core::task::Poll<Result<usize, Self::Error>> {
if buf.len() > SLOT {
return core::task::Poll::Ready(Err(RingSinkWriteError::FrameTooLarge {
len: buf.len(),
slot: SLOT,
}));
}
match self.get_mut().accept(buf) {
ControlFlow::Continue(()) => core::task::Poll::Ready(Ok(buf.len())),
ControlFlow::Break(()) => core::task::Poll::Pending,
}
}
fn poll_flush(
self: core::pin::Pin<&mut Self>,
_cx: &mut core::task::Context<'_>,
) -> core::task::Poll<Result<(), Self::Error>> {
core::task::Poll::Ready(Ok(()))
}
fn poll_close(
self: core::pin::Pin<&mut Self>,
_cx: &mut core::task::Context<'_>,
) -> core::task::Poll<Result<(), Self::Error>> {
core::task::Poll::Ready(Ok(()))
}
}
#[cfg(all(feature = "io-async", feature = "std"))]
impl<const SLOTS: usize, const SLOT: usize> futures::io::AsyncWrite for RingSink<SLOTS, SLOT> {
fn poll_write(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
buf: &[u8],
) -> core::task::Poll<std::io::Result<usize>> {
match <Self as proxima_core::io::AsyncWrite>::poll_write(self, cx, buf) {
core::task::Poll::Ready(Ok(count)) => core::task::Poll::Ready(Ok(count)),
core::task::Poll::Ready(Err(error)) => {
core::task::Poll::Ready(Err(std::io::Error::other(error)))
}
core::task::Poll::Pending => core::task::Poll::Pending,
}
}
fn poll_flush(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> core::task::Poll<std::io::Result<()>> {
match <Self as proxima_core::io::AsyncWrite>::poll_flush(self, cx) {
core::task::Poll::Ready(Ok(())) => core::task::Poll::Ready(Ok(())),
core::task::Poll::Ready(Err(error)) => {
core::task::Poll::Ready(Err(std::io::Error::other(error)))
}
core::task::Poll::Pending => core::task::Poll::Pending,
}
}
fn poll_close(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> core::task::Poll<std::io::Result<()>> {
match <Self as proxima_core::io::AsyncWrite>::poll_close(self, cx) {
core::task::Poll::Ready(Ok(())) => core::task::Poll::Ready(Ok(())),
core::task::Poll::Ready(Err(error)) => {
core::task::Poll::Ready(Err(std::io::Error::other(error)))
}
core::task::Poll::Pending => core::task::Poll::Pending,
}
}
}
pub struct DrainFanOut<K, const N: usize> {
sinks: [K; N],
}
impl<K, const N: usize> DrainFanOut<K, N> {
#[must_use]
pub const fn new(sinks: [K; N]) -> Self {
Self { sinks }
}
#[must_use]
pub fn sinks_mut(&mut self) -> &mut [K; N] {
&mut self.sinks
}
}
impl<K: DrainSink, const N: usize> DrainFanOut<K, N> {
pub fn push_all(&mut self, item: &K::Item) -> ControlFlow<()> {
for sink in &mut self.sinks {
sink.accept(item)?;
}
ControlFlow::Continue(())
}
pub fn push_best_effort(&mut self, item: &K::Item) {
for sink in &mut self.sinks {
if sink.has_capacity() {
let _ = sink.accept(item);
}
}
}
}
mod marker_propagation {
use super::{DrainFanOut, DrainSink, RingSink};
use proxima_core::markers::{AllocFree, DropSafe, NoStd};
impl<const SLOTS: usize, const SLOT: usize> NoStd for RingSink<SLOTS, SLOT> {}
impl<const SLOTS: usize, const SLOT: usize> AllocFree for RingSink<SLOTS, SLOT> {}
impl<const SLOTS: usize, const SLOT: usize> DropSafe for RingSink<SLOTS, SLOT> {}
impl<K: DrainSink + NoStd, const N: usize> NoStd for DrainFanOut<K, N> {}
impl<K: DrainSink + AllocFree, const N: usize> AllocFree for DrainFanOut<K, N> {}
impl<K: DrainSink + DropSafe, const N: usize> DropSafe for DrainFanOut<K, N> {}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn ring_sink_accepts_then_pops_in_order() {
let mut sink: RingSink<4, 8> = RingSink::new();
assert_eq!(sink.accept(b"hello"), ControlFlow::Continue(()));
assert_eq!(sink.accept(b"world"), ControlFlow::Continue(()));
assert_eq!(sink.len(), 2);
assert_eq!(sink.pop(), Some(&b"hello"[..]));
assert_eq!(sink.pop(), Some(&b"world"[..]));
assert_eq!(sink.pop(), None);
}
#[test]
fn ring_sink_breaks_when_full_no_panic() {
let mut sink: RingSink<1, 4> = RingSink::new();
assert_eq!(sink.accept(b"abcd"), ControlFlow::Continue(()));
assert!(!sink.has_capacity());
assert_eq!(
sink.accept(b"efgh"),
ControlFlow::Break(()),
"full sink => Break"
);
}
#[test]
fn ring_sink_breaks_on_oversize_frame() {
let mut sink: RingSink<4, 4> = RingSink::new();
assert_eq!(
sink.accept(b"toolong!!"),
ControlFlow::Break(()),
"frame > SLOT => Break"
);
assert!(sink.is_empty());
}
#[test]
fn drain_fan_out_routes_item_to_all_sinks() {
let mut fan: DrainFanOut<RingSink<4, 8>, 3> =
DrainFanOut::new([RingSink::new(), RingSink::new(), RingSink::new()]);
assert_eq!(fan.push_all(b"ping"), ControlFlow::Continue(()));
for sink in fan.sinks_mut() {
assert_eq!(sink.pop(), Some(&b"ping"[..]));
}
}
#[test]
fn drain_fan_out_best_effort_skips_full_sinks() {
let mut fan: DrainFanOut<RingSink<1, 8>, 2> =
DrainFanOut::new([RingSink::new(), RingSink::new()]);
fan.push_best_effort(b"a"); fan.push_best_effort(b"b"); for sink in fan.sinks_mut() {
assert_eq!(sink.pop(), Some(&b"a"[..]));
assert_eq!(sink.pop(), None, "second push was skipped (full)");
}
}
#[cfg(feature = "io-async")]
#[test]
fn ring_sink_async_write_accepts_frames_then_reports_oversize_error() {
use core::pin::Pin;
use core::task::{Context, Poll, Waker};
use proxima_core::io::AsyncWrite as _;
const REQUEST_LINE: &[u8] = b"GET /orders?id=42 HTTP/1.1\r\n";
let mut sink: RingSink<2, 32> = RingSink::new();
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let written = match Pin::new(&mut sink).poll_write(&mut cx, REQUEST_LINE) {
Poll::Ready(Ok(count)) => count,
other => panic!("expected an immediate accept, got {other:?}"),
};
assert_eq!(written, REQUEST_LINE.len());
assert_eq!(sink.pop(), Some(REQUEST_LINE), "the write reached the ring");
let oversize = [0u8; 64];
match Pin::new(&mut sink).poll_write(&mut cx, &oversize) {
Poll::Ready(Err(RingSinkWriteError::FrameTooLarge { len, slot })) => {
assert_eq!(len, oversize.len());
assert_eq!(slot, 32);
}
other => panic!("expected FrameTooLarge, got {other:?}"),
}
}
}