use crate::chars::{is_ows, is_token, is_valid_boundary, ows_trimmed_range, param, trim_ows};
use crate::Error;
pub const MAX_PADDING: usize = 8;
const LOOKAHEAD_EXTRA: usize = 4 + MAX_PADDING + 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event<'i, 'p> {
PartStart,
Header {
name: &'p [u8],
value: &'p [u8],
},
HeadersEnd,
Data(&'i [u8]),
PartEnd,
Finished,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Preamble { at_start: bool },
Headers,
Body,
NewPart,
Finished,
Failed(Error),
}
#[derive(Debug, Clone)]
pub struct Parser<'b, const LINE: usize = 512, const MAX_HEADERS: usize = 16> {
boundary: &'b [u8],
state: State,
line: [u8; LINE],
line_len: usize,
header_count: usize,
}
impl<'b, const LINE: usize, const MAX_HEADERS: usize> Parser<'b, LINE, MAX_HEADERS> {
pub fn new(boundary: &'b [u8]) -> Result<Self, Error> {
if !is_valid_boundary(boundary) {
return Err(Error::Boundary);
}
Ok(Self {
boundary,
state: State::Preamble { at_start: true },
line: [0; LINE],
line_len: 0,
header_count: 0,
})
}
pub fn from_content_type(content_type: &'b [u8]) -> Result<Self, Error> {
match boundary_param(content_type) {
Some(b) => Self::new(b),
None => Err(Error::Boundary),
}
}
#[must_use]
pub const fn boundary(&self) -> &'b [u8] {
self.boundary
}
#[must_use]
pub const fn min_buffer(&self) -> usize {
self.boundary.len().saturating_add(LOOKAHEAD_EXTRA)
}
#[must_use]
pub const fn is_finished(&self) -> bool {
matches!(self.state, State::Finished)
}
pub fn parse<'i>(
&mut self,
input: &'i [u8],
eof: bool,
) -> Result<(usize, Option<Event<'i, '_>>), Error> {
let (consumed, ev) = self.step(input, eof)?;
Ok((consumed, ev.map(|ev| self.materialize(input, ev))))
}
pub(crate) fn step(&mut self, input: &[u8], eof: bool) -> Result<(usize, Option<Ev>), Error> {
let step = match self.state {
State::NewPart => {
self.state = State::Headers;
return Ok((0, Some(Ev::PartStart)));
}
State::Finished => return Ok((input.len(), Some(Ev::Finished))),
State::Failed(e) => Err(e),
_ if input.is_empty() && eof => Err(Error::UnexpectedEof),
_ if input.is_empty() => Ok((0, None)),
State::Preamble { at_start } => Ok(self.preamble(input, eof, at_start)),
State::Headers => self.headers(input),
State::Body => Ok(self.body(input, eof)),
};
step.inspect_err(|&e| self.state = State::Failed(e))
}
pub(crate) fn materialize<'i>(&self, input: &'i [u8], ev: Ev) -> Event<'i, '_> {
match ev {
Ev::PartStart => Event::PartStart,
Ev::HeadersEnd => Event::HeadersEnd,
Ev::PartEnd => Event::PartEnd,
Ev::Finished => Event::Finished,
Ev::Data { end } => Event::Data(input.get(..end).unwrap_or(input)),
Ev::Header {
name_end,
value_start,
value_end,
} => Event::Header {
name: self.line.get(..name_end).unwrap_or(&[]),
value: self.line.get(value_start..value_end).unwrap_or(&[]),
},
}
}
fn preamble(&mut self, input: &[u8], eof: bool, at_start: bool) -> (usize, Option<Ev>) {
let (consumed, ev) = match scan(input, eof, at_start, self.boundary) {
Found::Delim { at, len, close } => {
let (state, ev) = if close {
(State::Finished, Ev::Finished)
} else {
(State::Headers, Ev::PartStart)
};
self.state = state;
return (at.saturating_add(len), Some(ev));
}
Found::HeldBack { at } => (at, None),
Found::Nothing => (input.len(), None),
};
if consumed > 0 {
self.state = State::Preamble { at_start: false };
}
(consumed, ev)
}
fn headers(&mut self, input: &[u8]) -> Result<(usize, Option<Ev>), Error> {
let lf = input.iter().position(|&b| b == b'\n');
let take = lf.map_or(input.len(), |i| i.saturating_add(1));
let chunk = input.get(..take).unwrap_or(input);
let new_len = self.line_len.saturating_add(chunk.len());
if new_len > LINE {
return Err(Error::HeaderTooLong); }
for (dst, &src) in self.line.iter_mut().skip(self.line_len).zip(chunk) {
*dst = src;
}
self.line_len = new_len;
if lf.is_none() {
return Ok((take, None));
}
self.line_len = 0;
let line = self.line.get(..new_len).unwrap_or(&[]);
let Some(content) = line.strip_suffix(b"\r\n") else {
return Err(Error::Header); };
if content.is_empty() {
self.state = State::Body; self.header_count = 0;
return Ok((take, Some(Ev::HeadersEnd)));
}
self.header_count = self.header_count.saturating_add(1);
if self.header_count > MAX_HEADERS {
return Err(Error::TooManyHeaders); }
let Some(colon) = content.iter().position(|&b| b == b':') else {
return Err(Error::Header); };
if !is_token(content.get(..colon).unwrap_or(&[])) {
return Err(Error::Header); }
if content.contains(&b'\r') {
return Err(Error::Header); }
let value_from = colon.saturating_add(1);
let (start, end) = ows_trimmed_range(content.get(value_from..).unwrap_or(&[]));
Ok((
take,
Some(Ev::Header {
name_end: colon,
value_start: value_from.saturating_add(start),
value_end: value_from.saturating_add(end),
}),
))
}
fn body(&mut self, input: &[u8], eof: bool) -> (usize, Option<Ev>) {
let end = match scan(input, eof, false, self.boundary) {
Found::Delim { at: 0, len, close } => {
self.state = if close {
State::Finished
} else {
State::NewPart
};
return (len, Some(Ev::PartEnd));
}
Found::Delim { at, .. } | Found::HeldBack { at } => at,
Found::Nothing => input.len(),
};
(end, (end > 0).then_some(Ev::Data { end }))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Ev {
PartStart,
Header {
name_end: usize,
value_start: usize,
value_end: usize,
},
HeadersEnd,
Data {
end: usize,
},
PartEnd,
Finished,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Found {
Delim { at: usize, len: usize, close: bool },
HeldBack { at: usize },
Nothing,
}
fn classify(s: &[u8], lead: &[u8], boundary: &[u8]) -> Found {
let full = lead.len().saturating_add(boundary.len());
if !s
.iter()
.zip(lead.iter().chain(boundary))
.all(|(a, b)| a == b)
{
return Found::Nothing;
}
let Some(tail) = s.get(full..) else {
return Found::HeldBack { at: 0 };
};
let pad = tail
.iter()
.take(MAX_PADDING + 1)
.take_while(|&&b| is_ows(b))
.count();
let (len, close) = match (
tail.first(),
tail.get(1),
tail.get(pad),
tail.get(pad.saturating_add(1)),
) {
(Some(b'-'), Some(b'-'), ..) => (2, true),
(None | Some(b'-'), None, ..) | (_, _, None | Some(b'\r'), None) if pad <= MAX_PADDING => {
return Found::HeldBack { at: 0 }
}
(Some(b'-'), ..) => return Found::Nothing,
(.., Some(b'\r'), Some(b'\n')) if pad <= MAX_PADDING => (pad.saturating_add(2), false),
_ => return Found::Nothing,
};
Found::Delim {
at: 0,
len: full.saturating_add(len),
close,
}
}
fn scan(input: &[u8], eof: bool, allow_d0: bool, boundary: &[u8]) -> Found {
let mut from = 0usize;
let mut try_d0 = allow_d0 && input.first() == Some(&b'-');
loop {
let (at, lead): (usize, &[u8]) = if try_d0 {
(0, b"--")
} else {
match input
.get(from..)
.and_then(|r| r.iter().position(|&b| b == b'\r'))
{
Some(off) => (from.saturating_add(off), b"\r\n--"),
None => return Found::Nothing,
}
};
try_d0 = false;
match classify(input.get(at..).unwrap_or(&[]), lead, boundary) {
Found::Delim { len, close, .. } => return Found::Delim { at, len, close },
Found::HeldBack { .. } if !eof => return Found::HeldBack { at },
_ => from = at.saturating_add(1),
}
}
}
fn boundary_param(ct: &[u8]) -> Option<&[u8]> {
let slash = ct.iter().position(|&b| b == b'/')?;
let (mtype, rest) = ct.split_at_checked(slash)?;
if !trim_ows(mtype).eq_ignore_ascii_case(b"multipart") {
return None;
}
let rest = rest.get(1..)?;
let semi = rest.iter().position(|&b| b == b';').unwrap_or(rest.len());
let (subtype, mut params) = rest.split_at_checked(semi)?;
if !is_token(trim_ows(subtype)) {
return None;
}
loop {
let (attr, value, quoted, rest) = param(params.strip_prefix(b";")?).ok()??;
if !quoted && !is_token(value) {
return None;
}
if attr.eq_ignore_ascii_case(b"boundary") {
return Some(value);
}
params = rest;
}
}