sim_lib_net_core/read.rs
1//! Policy-free, cap-as-argument bounded readers over `std::io`.
2//!
3//! Several runtime libs each hand-rolled the same defensive read loops: a
4//! single line bounded to a byte cap, and an HTTP head accumulated until the
5//! `\r\n\r\n` terminator. These readers are the one home for that framing. They
6//! stay policy-free: the caller passes the cap (64 KiB, 1 MiB, ...) as an
7//! argument and maps the returned outcome onto its own error (`HostError`,
8//! a `413` response, `ReadOutcome::TooLarge`, ...). No socket, TLS, or timeout
9//! policy lives here.
10
11use std::io::{self, BufRead, Read};
12
13/// The outcome of a single capped line read.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum CapOutcome {
16 /// A line (including its trailing newline, if any) was read within `cap`;
17 /// `buf` holds it.
18 Line,
19 /// The line reached `cap` bytes before a newline was seen; `buf` holds the
20 /// capped prefix. The caller decides how to reject it.
21 TooLarge,
22 /// End of input reached with no further bytes; `buf` is empty.
23 Eof,
24}
25
26/// The outcome of reading an HTTP head up to the `\r\n\r\n` terminator.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum HeadOutcome {
29 /// A complete head terminated by `\r\n\r\n`, returned as raw bytes
30 /// (terminator included) for the caller to parse.
31 Head(Vec<u8>),
32 /// The head reached `cap` bytes before the terminator; the caller rejects it.
33 TooLarge,
34 /// End of input before any byte was read: the peer opened and closed without
35 /// sending a request.
36 Eof,
37 /// End of input after a partial head: the connection was truncated
38 /// mid-headers. Carries the bytes read so far.
39 Truncated(Vec<u8>),
40}
41
42/// Read one line into `buf`, bounded to `cap` bytes.
43///
44/// `buf` is cleared first, then filled via a `Read::take(cap + 1)` ceiling so a
45/// hostile unterminated line is bounded before it can grow memory. The returned
46/// [`CapOutcome`] tells the caller whether a [`Line`](CapOutcome::Line) was read,
47/// the line was [`TooLarge`](CapOutcome::TooLarge), or input hit
48/// [`Eof`](CapOutcome::Eof); the caller maps that onto its own error type.
49pub fn read_capped_line<R: BufRead>(
50 reader: &mut R,
51 buf: &mut String,
52 cap: usize,
53) -> io::Result<CapOutcome> {
54 buf.clear();
55 let read = Read::take(&mut *reader, cap as u64 + 1).read_line(buf)?;
56 if buf.len() > cap {
57 return Ok(CapOutcome::TooLarge);
58 }
59 if read == 0 {
60 return Ok(CapOutcome::Eof);
61 }
62 Ok(CapOutcome::Line)
63}
64
65/// Read an HTTP head byte-by-byte until the `\r\n\r\n` terminator, bounded to
66/// `cap` bytes.
67///
68/// Retries on [`io::ErrorKind::Interrupted`]; any other I/O error is returned.
69/// The [`HeadOutcome`] distinguishes a complete [`Head`](HeadOutcome::Head), an
70/// over-cap [`TooLarge`](HeadOutcome::TooLarge), a clean [`Eof`](HeadOutcome::Eof)
71/// (nothing sent), and a [`Truncated`](HeadOutcome::Truncated) head (peer closed
72/// mid-headers), leaving the reject/None policy to the caller.
73pub fn read_head_until_double_crlf<R: Read>(reader: &mut R, cap: usize) -> io::Result<HeadOutcome> {
74 let mut head = Vec::new();
75 let mut byte = [0u8; 1];
76 loop {
77 match reader.read(&mut byte) {
78 Ok(0) if head.is_empty() => return Ok(HeadOutcome::Eof),
79 Ok(0) => return Ok(HeadOutcome::Truncated(head)),
80 Ok(_) => {
81 head.push(byte[0]);
82 if head.len() > cap {
83 return Ok(HeadOutcome::TooLarge);
84 }
85 if head.ends_with(b"\r\n\r\n") {
86 return Ok(HeadOutcome::Head(head));
87 }
88 }
89 Err(ref error) if error.kind() == io::ErrorKind::Interrupted => {}
90 Err(error) => return Err(error),
91 }
92 }
93}