use crate::chars::{
eq_ignore_case, is_ows, is_tchar, is_valid_boundary, ows_trimmed_range, 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::Preamble { at_start } => self.preamble(input, eof, at_start),
State::Headers => self.headers(input, eof),
State::Body => self.body(input, eof),
State::NewPart => {
self.state = State::Headers;
self.line_len = 0;
self.header_count = 0;
Ok((0, Some(Ev::PartStart)))
}
State::Finished => Ok((input.len(), Some(Ev::Finished))),
State::Failed(e) => Err(e),
};
match step {
Ok(x) => Ok(x),
Err(e) => {
self.state = State::Failed(e);
Err(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,
) -> Result<(usize, Option<Ev>), Error> {
match scan(input, eof, at_start, self.boundary) {
Found::Delim {
at,
len,
close: true,
} => {
self.state = State::Finished;
Ok((at.saturating_add(len), Some(Ev::Finished)))
}
Found::Delim {
at,
len,
close: false,
} => {
self.state = State::Headers;
self.line_len = 0;
self.header_count = 0;
Ok((at.saturating_add(len), Some(Ev::PartStart)))
}
Found::HeldBack { at } => {
if at > 0 {
self.state = State::Preamble { at_start: false };
}
Ok((at, None))
}
Found::Nothing => {
if input.is_empty() {
if eof {
return Err(Error::UnexpectedEof);
}
return Ok((0, None));
}
self.state = State::Preamble { at_start: false };
Ok((input.len(), None))
}
}
}
fn headers(&mut self, input: &[u8], eof: bool) -> Result<(usize, Option<Ev>), Error> {
let lf = input.iter().position(|&b| b == b'\n');
let take = match lf {
Some(i) => i.saturating_add(1),
None => input.len(),
};
let chunk = input.get(..take).unwrap_or(input);
if chunk.is_empty() {
if eof {
return Err(Error::UnexpectedEof);
}
return Ok((0, None));
}
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; 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) -> Result<(usize, Option<Ev>), Error> {
match scan(input, eof, false, self.boundary) {
Found::Delim { at: 0, len, close } => {
self.state = if close {
State::Finished
} else {
State::NewPart
};
Ok((len, Some(Ev::PartEnd)))
}
Found::Delim { at, .. } | Found::HeldBack { at } => {
if at == 0 {
Ok((0, None))
} else {
Ok((at, Some(Ev::Data { end: at })))
}
}
Found::Nothing => {
if input.is_empty() {
if eof {
return Err(Error::UnexpectedEof);
}
return Ok((0, None));
}
Ok((input.len(), Some(Ev::Data { end: input.len() })))
}
}
}
}
#[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 Class {
Part(usize),
Close(usize),
No,
Partial,
}
fn classify(s: &[u8], lead: &[u8], boundary: &[u8]) -> Class {
let full = lead.len().saturating_add(boundary.len());
let mut matched = 0usize;
for (&have, &want) in s.iter().zip(lead.iter().chain(boundary)) {
if have != want {
return Class::No;
}
matched = matched.saturating_add(1);
}
if matched < full {
return Class::Partial;
}
let tail = s.get(full..).unwrap_or(&[]);
match tail.first() {
None => Class::Partial,
Some(b'-') => match tail.get(1) {
None => Class::Partial,
Some(b'-') => Class::Close(full.saturating_add(2)),
Some(_) => Class::No,
},
Some(_) => {
let pad = tail
.iter()
.take(PAD_PROBE)
.take_while(|&&b| is_ows(b))
.count();
if pad > MAX_PADDING {
return Class::No;
}
match (tail.get(pad), tail.get(pad.saturating_add(1))) {
(None, _) | (Some(b'\r'), None) => Class::Partial,
(Some(b'\r'), Some(b'\n')) => {
Class::Part(full.saturating_add(pad).saturating_add(2))
}
_ => Class::No,
}
}
}
}
const PAD_PROBE: usize = MAX_PADDING + 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Found {
Delim { at: usize, len: usize, close: bool },
HeldBack { at: usize },
Nothing,
}
fn scan(input: &[u8], eof: bool, allow_d0: bool, boundary: &[u8]) -> Found {
let mut c = 0usize;
let mut try_d0 = allow_d0;
loop {
let (at, lead): (usize, &[u8]) = if try_d0 && input.first() == Some(&b'-') {
try_d0 = false;
(0, b"--")
} else {
try_d0 = false;
match input
.get(c..)
.and_then(|r| r.iter().position(|&b| b == b'\r'))
{
Some(off) => (c.saturating_add(off), b"\r\n--"),
None => return Found::Nothing,
}
};
match classify(input.get(at..).unwrap_or(&[]), lead, boundary) {
Class::Part(len) => {
return Found::Delim {
at,
len,
close: false,
}
}
Class::Close(len) => {
return Found::Delim {
at,
len,
close: true,
}
}
Class::Partial if !eof => return Found::HeldBack { at },
Class::No | Class::Partial => c = 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 !eq_ignore_case(trim_ows(mtype), 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;
}
while let Some(after_semi) = params.strip_prefix(b";") {
let s = trim_ows(after_semi);
let eq = s.iter().position(|&b| b == b'=')?;
let (attr, after_eq) = s.split_at_checked(eq)?;
let attr = trim_ows(attr);
if !is_token(attr) {
return None;
}
let v = trim_ows(after_eq.get(1..)?);
let (value, rest) = if let Some(inner) = v.strip_prefix(b"\"") {
let close = quoted_string_end(inner)?;
let (value, rest) = inner.split_at_checked(close)?;
(value, trim_ows(rest.get(1..)?))
} else {
let end = v.iter().position(|&b| b == b';').unwrap_or(v.len());
let (value, rest) = v.split_at_checked(end)?;
let value = trim_ows(value);
if !is_token(value) {
return None;
}
(value, rest)
};
if eq_ignore_case(attr, b"boundary") {
return Some(value);
}
params = rest;
}
None
}
pub(crate) fn quoted_string_end(s: &[u8]) -> Option<usize> {
let mut i = 0usize;
let mut escaped = false;
for &b in s {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
return Some(i);
}
i = i.saturating_add(1);
}
None
}
pub(crate) fn is_token(s: &[u8]) -> bool {
!s.is_empty() && s.iter().all(|&b| is_tchar(b))
}