use futures_core::Stream;
use futures_util::StreamExt;
pub(crate) enum BodyPlan {
Buffer,
Refuse,
Stream,
}
pub(crate) fn body_plan(content_length: Option<&str>, limit: usize) -> BodyPlan {
let Some(raw) = content_length else {
return BodyPlan::Stream;
};
match raw.trim().parse::<usize>() {
Ok(len) if len > limit => BodyPlan::Refuse,
Ok(_) => BodyPlan::Buffer,
Err(_) => BodyPlan::Stream,
}
}
pub(crate) enum Capped {
Within(Vec<u8>),
TooLarge,
}
pub(crate) async fn read_capped<S, E>(stream: S, limit: usize) -> Result<Capped, E>
where
S: Stream<Item = Result<Vec<u8>, E>>,
{
futures_util::pin_mut!(stream);
let mut body: Vec<u8> = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
if body.len().saturating_add(chunk.len()) > limit {
return Ok(Capped::TooLarge);
}
body.extend_from_slice(&chunk);
}
Ok(Capped::Within(body))
}
#[cfg(test)]
mod tests {
use super::{BodyPlan, Capped, body_plan, read_capped};
use futures_core::Stream;
use futures_util::stream;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
const LIMIT: usize = 4096;
const CHUNK: usize = 1024;
#[derive(Debug, PartialEq)]
struct TestError;
struct CountingChunks {
remaining: usize,
pulled: Arc<AtomicUsize>,
}
impl CountingChunks {
fn new(chunks: usize) -> (Self, Arc<AtomicUsize>) {
let pulled = Arc::new(AtomicUsize::new(0));
(
Self {
remaining: chunks,
pulled: Arc::clone(&pulled),
},
pulled,
)
}
}
impl Stream for CountingChunks {
type Item = Result<Vec<u8>, TestError>;
fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.remaining == 0 {
return Poll::Ready(None);
}
this.remaining -= 1;
this.pulled.fetch_add(1, Ordering::SeqCst);
Poll::Ready(Some(Ok(vec![b'x'; CHUNK])))
}
}
struct Unbounded {
pulled: Arc<AtomicUsize>,
}
impl Unbounded {
fn new() -> (Self, Arc<AtomicUsize>) {
let pulled = Arc::new(AtomicUsize::new(0));
(
Self {
pulled: Arc::clone(&pulled),
},
pulled,
)
}
}
impl Stream for Unbounded {
type Item = Result<Vec<u8>, TestError>;
fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
this.pulled.fetch_add(1, Ordering::SeqCst);
Poll::Ready(Some(Ok(vec![b'x'; CHUNK])))
}
}
#[test]
fn a_declared_length_at_or_under_the_ceiling_is_buffered() {
assert!(matches!(body_plan(Some("4096"), LIMIT), BodyPlan::Buffer));
assert!(matches!(body_plan(Some("4095"), LIMIT), BodyPlan::Buffer));
assert!(matches!(body_plan(Some("0"), LIMIT), BodyPlan::Buffer));
assert!(matches!(body_plan(Some(" 4095 "), LIMIT), BodyPlan::Buffer));
}
#[test]
fn a_declared_length_one_over_the_ceiling_is_refused_unread() {
assert!(matches!(body_plan(Some("4097"), LIMIT), BodyPlan::Refuse));
}
#[test]
fn a_body_without_a_usable_declared_length_is_streamed_under_the_cap() {
assert!(matches!(body_plan(None, LIMIT), BodyPlan::Stream));
assert!(matches!(body_plan(Some("chunks"), LIMIT), BodyPlan::Stream));
assert!(matches!(body_plan(Some(""), LIMIT), BodyPlan::Stream));
assert!(matches!(body_plan(Some("-1"), LIMIT), BodyPlan::Stream));
assert!(matches!(
body_plan(Some("99999999999999999999999"), LIMIT),
BodyPlan::Stream
));
}
#[test]
fn a_chunked_body_at_or_under_the_cap_is_read_intact() {
let (chunks, _) = CountingChunks::new(4);
match pollster::block_on(read_capped(chunks, LIMIT)).expect("no stream error") {
Capped::Within(body) => {
assert_eq!(body.len(), LIMIT);
assert!(body.iter().all(|byte| *byte == b'x'));
}
Capped::TooLarge => panic!("a body exactly at the cap must read"),
}
let (chunks, _) = CountingChunks::new(3);
match pollster::block_on(read_capped(chunks, LIMIT)).expect("no stream error") {
Capped::Within(body) => assert_eq!(body.len(), 3 * CHUNK),
Capped::TooLarge => panic!("a body under the cap must read"),
}
}
#[test]
fn a_chunked_body_of_varied_chunks_is_read_intact_in_order() {
let parts: Vec<Result<Vec<u8>, TestError>> = vec![
Ok(b"hello ".to_vec()),
Ok(b"capped ".to_vec()),
Ok(b"world".to_vec()),
];
match pollster::block_on(read_capped(stream::iter(parts), LIMIT)).expect("no stream error")
{
Capped::Within(body) => assert_eq!(body, b"hello capped world"),
Capped::TooLarge => panic!("a body under the cap must read"),
}
}
#[test]
fn a_chunked_body_larger_than_the_cap_is_refused() {
let (chunks, pulled) = CountingChunks::new(6);
let verdict = pollster::block_on(read_capped(chunks, LIMIT)).expect("no stream error");
assert!(matches!(verdict, Capped::TooLarge));
assert_eq!(
pulled.load(Ordering::SeqCst),
5,
"reading stops at the chunk that crosses the cap"
);
}
#[test]
fn an_endless_body_is_cut_off_once_it_passes_the_cap() {
let (endless, pulled) = Unbounded::new();
let verdict = pollster::block_on(read_capped(endless, LIMIT)).expect("no stream error");
assert!(matches!(verdict, Capped::TooLarge));
let consumed = pulled.load(Ordering::SeqCst);
assert!(consumed > LIMIT / CHUNK, "the cap must actually be crossed");
assert!(
consumed <= LIMIT / CHUNK + 1,
"abort within one chunk of the cap, not long after: {consumed}"
);
assert!(
(consumed - 1) * CHUNK <= LIMIT,
"the accumulated body never exceeded the cap"
);
}
#[test]
fn a_stream_error_stops_the_read_and_propagates() {
let parts: Vec<Result<Vec<u8>, TestError>> = vec![Ok(b"ok".to_vec()), Err(TestError)];
let verdict = pollster::block_on(read_capped(stream::iter(parts), LIMIT));
assert!(matches!(verdict, Err(TestError)));
}
}