use std::io;
use std::sync::Arc;
use futures::io::{AsyncRead, AsyncReadExt};
use parking_lot::Mutex as ParkingMutex;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use crate::limits::Limits;
use crate::substrate::SubstrateError;
pub const PACK_SOURCE_CHANNEL_CAPACITY: usize = 4;
pub const PACK_SOURCE_CHUNK_SIZE: usize = 64 * 1024;
#[derive(Clone, Default)]
pub struct PackSourceFault(Arc<ParkingMutex<Option<SubstrateError>>>);
impl PackSourceFault {
fn record(&self, error: SubstrateError) {
*self.0.lock() = Some(error);
}
pub fn take_failure(&self) -> Option<SubstrateError> {
self.0.lock().take()
}
}
pub struct PackSource {
rx: mpsc::Receiver<io::Result<Vec<u8>>>,
chunk: Vec<u8>,
pos: usize,
failure: Option<(io::ErrorKind, String)>,
}
impl io::Read for PackSource {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
if self.pos < self.chunk.len() {
let n = buf.len().min(self.chunk.len() - self.pos);
buf[..n].copy_from_slice(&self.chunk[self.pos..self.pos + n]);
self.pos += n;
return Ok(n);
}
if let Some((kind, message)) = &self.failure {
return Err(io::Error::new(*kind, message.clone()));
}
match futures::executor::block_on(self.rx.recv()) {
Some(Ok(chunk)) => {
if chunk.is_empty() {
return Ok(0);
}
let n = buf.len().min(chunk.len());
buf[..n].copy_from_slice(&chunk[..n]);
self.chunk = chunk;
self.pos = n;
Ok(n)
}
Some(Err(error)) => {
let kind = error.kind();
let message = error.to_string();
self.failure = Some((kind, message.clone()));
Err(io::Error::new(kind, message))
}
None => Ok(0),
}
}
}
pub fn bridge_pack_source<R>(tail: R, limits: &Limits) -> PackSourceBridge
where
R: AsyncRead + Unpin + Send + 'static,
{
let (tx, rx) = mpsc::channel(PACK_SOURCE_CHANNEL_CAPACITY);
let fault = PackSourceFault::default();
let relay = tokio::spawn(relay_pack_chunks(
tail,
tx,
limits.max_pack_size,
fault.clone(),
));
PackSourceBridge {
end: PackSource {
rx,
chunk: Vec::new(),
pos: 0,
failure: None,
},
fault,
relay,
}
}
pub struct PackSourceBridge {
pub end: PackSource,
pub fault: PackSourceFault,
pub relay: JoinHandle<()>,
}
async fn relay_pack_chunks<R>(
mut tail: R,
tx: mpsc::Sender<io::Result<Vec<u8>>>,
budget: u64,
fault: PackSourceFault,
) where
R: AsyncRead + Unpin,
{
let mut chunk = vec![0u8; PACK_SOURCE_CHUNK_SIZE];
let mut relayed: u64 = 0;
loop {
match tail.read(&mut chunk).await {
Ok(0) => break,
Ok(n) => {
if relayed.saturating_add(n as u64) > budget {
let message = format!(
"the pack exceeds the max_pack_size budget ({budget}) — fail closed (ADR-009)"
);
fault.record(SubstrateError::Protocol(message.clone()));
let _ = tx
.send(Err(io::Error::new(io::ErrorKind::InvalidData, message)))
.await;
break;
}
relayed += n as u64;
if tx.send(Ok(chunk[..n].to_vec())).await.is_err() {
break;
}
}
Err(error) => {
let kind = error.kind();
let message = error.to_string();
fault.record(SubstrateError::Io(io::Error::new(kind, message.clone())));
let _ = tx.send(Err(io::Error::new(kind, message))).await;
break;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use futures::stream::{self};
use std::io::Read as _;
use std::time::Duration;
use tokio::time::timeout;
type BodyChunks = stream::Iter<std::vec::IntoIter<io::Result<Bytes>>>;
type TestTail = crate::stateless::BodyReader<BodyChunks, io::Error>;
fn body_reader(items: Vec<io::Result<Bytes>>) -> TestTail {
crate::stateless::BodyReader::new(stream::iter(items))
}
fn tail(chunks: Vec<Vec<u8>>) -> TestTail {
body_reader(
chunks
.into_iter()
.map(|bytes| io::Result::Ok(Bytes::from(bytes)))
.collect(),
)
}
fn failing_tail(chunks: Vec<Vec<u8>>) -> TestTail {
let mut items: Vec<io::Result<Bytes>> = chunks
.into_iter()
.map(|bytes| io::Result::Ok(Bytes::from(bytes)))
.collect();
items.push(io::Result::Err(io::Error::other("the body failed")));
body_reader(items)
}
fn multi_thread() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
}
#[test]
fn tail_bytes_reach_the_blocking_end_in_order_to_eof() {
let rt = multi_thread();
rt.block_on(async {
let data: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
let chunks = data.chunks(17_333).map(<[u8]>::to_vec).collect();
let bridge = bridge_pack_source(tail(chunks), &Limits::default());
let PackSourceBridge {
mut end,
fault,
relay,
} = bridge;
assert!(fault.take_failure().is_none());
let mut got = Vec::new();
end.read_to_end(&mut got).unwrap();
assert_eq!(got, data, "in order, byte-exact, to clean EOF");
relay.await.unwrap();
assert!(fault.take_failure().is_none(), "EOF is no failure");
});
}
#[test]
fn small_reads_keep_the_order_across_chunk_boundaries() {
let rt = multi_thread();
rt.block_on(async {
let data: Vec<u8> = (0..5000u32).map(|i| (i % 7) as u8).collect();
let chunks = vec![data[..3000].to_vec(), data[3000..].to_vec()];
let bridge = bridge_pack_source(tail(chunks), &Limits::default());
let PackSourceBridge { mut end, relay, .. } = bridge;
for expected_window in data.chunks(137) {
let mut got = vec![0u8; expected_window.len()];
end.read_exact(&mut got).unwrap();
assert_eq!(got, expected_window);
}
let mut eof = [0u8; 1];
assert_eq!(end.read(&mut eof).unwrap(), 0, "clean EOF");
relay.await.unwrap();
});
}
#[test]
fn a_slow_reader_gets_bounded_in_order_chunks_and_the_relay_parks_to_the_end() {
let rt = multi_thread();
rt.block_on(async {
let data: Vec<u8> = vec![0u8; PACK_SOURCE_CHUNK_SIZE * 3 + 7];
let bridge = bridge_pack_source(tail(vec![data.clone()]), &Limits::default());
let PackSourceBridge {
mut end,
fault,
relay,
} = bridge;
assert!(fault.take_failure().is_none());
let mut joined = tokio::spawn(relay);
let mut total = 0usize;
loop {
let mut got = vec![0u8; 997];
let n = end.read(&mut got).unwrap();
if n == 0 {
break;
}
assert!(n <= PACK_SOURCE_CHUNK_SIZE, "the per-chunk cap holds");
total += n;
}
assert_eq!(total, data.len(), "all bytes arrived, in order");
let _ = timeout(Duration::from_secs(5), &mut joined)
.await
.expect("the relay finished after the drain")
.unwrap();
});
}
#[test]
fn a_downstream_failure_breaks_the_relay() {
let rt = multi_thread();
rt.block_on(async {
let data: Vec<u8> = vec![0u8; PACK_SOURCE_CHUNK_SIZE * 20];
let bridge = bridge_pack_source(tail(vec![data]), &Limits::default());
let PackSourceBridge {
mut end,
fault,
relay,
} = bridge;
let mut got = vec![0u8; 100];
end.read_exact(&mut got).unwrap();
drop(end);
timeout(Duration::from_secs(5), relay)
.await
.expect("the relay breaks promptly after the downstream is gone")
.unwrap();
assert!(fault.take_failure().is_none());
});
}
#[test]
fn the_pack_size_budget_fails_closed_before_the_breach_bytes() {
let rt = multi_thread();
rt.block_on(async {
let data: Vec<u8> = (0..10_000u32).map(|i| (i % 251) as u8).collect();
let chunks = data.chunks(300).map(<[u8]>::to_vec).collect();
let limits = Limits {
max_pack_size: 1000,
..Limits::default()
};
let bridge = bridge_pack_source(tail(chunks), &limits);
let PackSourceBridge {
mut end,
fault,
relay,
} = bridge;
let mut before = Vec::new();
let mut got = vec![0u8; 4096];
loop {
match end.read(&mut got) {
Ok(0) => break,
Ok(n) => before.extend_from_slice(&got[..n]),
Err(_) => break,
}
}
assert!(
!before.is_empty() && before.len() <= 1000,
"some under-budget bytes delivered, none past the line: {}",
before.len()
);
let error = end.read(&mut got).unwrap_err();
assert!(
error.to_string().contains("max_pack_size"),
"the io error names the budget: {error}"
);
let error = end.read(&mut got).unwrap_err();
assert!(error.to_string().contains("max_pack_size"));
let failure = fault.take_failure().expect("the typed failure landed");
match failure {
SubstrateError::Protocol(message) => {
assert!(message.contains("max_pack_size"), "{message}");
}
other => panic!("the budget breach is the client-fault class: {other:?}"),
}
timeout(Duration::from_secs(5), relay)
.await
.expect("the relay ended at the budget breach")
.unwrap();
});
}
#[test]
fn a_transport_failure_surfaces_on_the_blocking_end() {
let rt = multi_thread();
rt.block_on(async {
let data: Vec<u8> = vec![b'x'; 2048];
let bridge = bridge_pack_source(failing_tail(vec![data]), &Limits::default());
let PackSourceBridge {
mut end,
fault,
relay,
} = bridge;
let mut got = vec![0u8; 4096];
match end.read(&mut got) {
Ok(n) if n <= 2048 && got[..n].iter().all(|b| *b == b'x') => {}
Ok(n) => panic!("unexpected clean read before the failure: {n}"),
Err(_) => {}
}
let error = end
.read(&mut got)
.expect_err("the transport failure surfaces on the blocking end");
assert!(error.to_string().contains("the body failed"), "{error}");
let failure = fault.take_failure().expect("the typed failure landed");
match failure {
SubstrateError::Io(message) => {
assert!(
message.to_string().contains("the body failed"),
"{message:?}"
);
}
other => panic!("a transport failure records as Io: {other:?}"),
}
timeout(Duration::from_secs(5), relay)
.await
.expect("the relay ended at the failure")
.unwrap();
});
}
#[test]
fn an_empty_tail_reads_as_clean_eof() {
let rt = multi_thread();
rt.block_on(async {
let bridge = bridge_pack_source(tail(Vec::new()), &Limits::default());
let PackSourceBridge {
mut end,
fault,
relay,
} = bridge;
let mut got = [0u8; 16];
assert_eq!(end.read(&mut got).unwrap(), 0);
relay.await.unwrap();
assert!(fault.take_failure().is_none());
});
}
#[test]
fn the_relay_chunks_at_the_cap() {
let rt = multi_thread();
rt.block_on(async {
let data: Vec<u8> = vec![0u8; PACK_SOURCE_CHUNK_SIZE * 4 + 33];
let bridge = bridge_pack_source(tail(vec![data.clone()]), &Limits::default());
let PackSourceBridge { mut end, relay, .. } = bridge;
let mut total = 0u64;
let mut biggest = 0usize;
let mut got = vec![0u8; 1 << 20];
loop {
let n = end.read(&mut got).unwrap();
if n == 0 {
break;
}
biggest = biggest.max(n);
total += n as u64;
}
assert_eq!(total, data.len() as u64);
assert!(
biggest <= PACK_SOURCE_CHUNK_SIZE,
"each delivered chunk respects the cap: {biggest}"
);
relay.await.unwrap();
});
}
}