chiark / gitweb /
b858039c73e782c6993442548e70bf08c9f9be99
[hippotat.git] / src / multipart.rs
1 // Copyright 2021 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: GPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use crate::prelude::*;
6
7 #[derive(Debug)]
8 pub struct Component<'b> {
9   pub name: PartName,
10   pub payload_start: &'b [u8],
11 }
12
13 #[derive(Debug)]
14 #[derive(Eq,PartialEq,Ord,PartialOrd,Hash)]
15 #[allow(non_camel_case_types)]
16 pub enum PartName { m, d, Other }
17
18 pub type BoundaryFinder = memchr::memmem::Finder<'static>;
19
20 #[throws(AE)]
21 pub fn process_component<'b>(warnings: &mut Warnings,
22                              after_leader: &'b [u8], expected: PartName)
23                              -> Option<Component<'b>> {
24   let rhs = after_leader;
25   let mut rhs =
26        if let Some(rhs) = rhs.strip_prefix(b"\r\n") { rhs         }
27   else if let Some(_  ) = rhs.strip_prefix(b"--"  ) { return None }
28   else if let Some(rhs) = rhs.strip_prefix(b"\n"  ) { rhs         }
29   else { throw!(anyhow!("invalid multipart delimiter")) };
30
31   let mut part_name = None;
32
33   loop {
34     // RHS points to the start of a header line
35     let nl = memchr::memchr(b'\n', rhs)
36       .ok_or_else(|| anyhow!("part headers truncated"))?;
37     let l = &rhs[0..nl]; rhs = &rhs[nl+1..];
38     if l == b"\r" || l == b"" { break } // end of headers
39     if l.starts_with(b"--") { throw!(anyhow!("boundary in part headers")) }
40
41     match (||{
42       let l = str::from_utf8(l).context("interpret part headers as utf-8")?;
43
44       let (_, disposition) = if let Some(y) =
45         regex_captures!(r#"^Content-Disposition[ \t]*:[ \t]*(.*)$"#i, l) { y }
46         else { return Ok(()) };
47
48       let disposition = disposition.trim_end();
49       if disposition.len() >= 100 { throw!(anyhow!(
50         "Content-Disposition value implausibly long"
51       )) }
52       // This let's us pretend it's a mime type, so we can use mime::Mime
53       let disposition = format!("dummy/{}", disposition);
54
55       let disposition: mime::Mime = disposition.parse()
56         .context("parse Content-Disposition")?;
57       let name = disposition.get_param("name")
58         .ok_or_else(|| anyhow!(r#"find "name" in Content-Disposition"#))?;
59
60       let name = match name.as_ref() {
61         "m" => PartName::m,
62         "d" => PartName::d,
63         _   => PartName::Other,
64       };
65
66       if let Some(_) = mem::replace(&mut part_name, Some(name)) {
67         throw!(anyhow!(r#"multiple "name"s in Content-Disposition(s)"#))
68       }
69       Ok::<_,AE>(())
70     })() {
71       Err(e) => warnings.add(&e)?,
72       Ok(()) => { },
73     };
74   }
75
76   Some(Component { name: part_name.unwrap_or(expected), payload_start: rhs })
77 }
78
79 pub struct MetadataFieldIterator<'b> {
80   buf: &'b [u8],
81   last: Option<usize>,
82   iter: memchr::Memchr<'b>,
83 }
84
85 impl<'b> MetadataFieldIterator<'b> {
86   pub fn new(buf: &'b [u8]) -> Self { Self {
87     buf,
88     last: Some(0),
89     iter: memchr::Memchr::new(b'\n', buf),
90   } }
91
92   #[throws(AE)]
93   pub fn need_next(&mut self) -> &'b str
94   {
95     self.next().ok_or_else(|| anyhow!("missing"))??
96   }
97
98   #[throws(AE)]
99   pub fn need_parse<T>(&mut self) -> T
100   where T: FromStr,
101         AE: From<T::Err>,
102   {
103     self.parse()?.ok_or_else(|| anyhow!("missing"))?
104   }
105
106   #[throws(AE)]
107   pub fn parse<T>(&mut self) -> Option<T>
108   where T: FromStr,
109         AE: From<T::Err>,
110   {
111     let s = if let Some(r) = self.next() { r? } else { return None };
112     Some(s.parse()?)
113   }
114
115   pub fn remaining_bytes_len(&self) -> usize {
116     if let Some(last) = self.last {
117       self.buf.len() - last
118     } else {
119       0
120     }
121   }
122 }
123                                       
124 impl<'b> Iterator for MetadataFieldIterator<'b> {
125   type Item = Result<&'b str, std::str::Utf8Error>;
126   fn next(&mut self) -> Option<Result<&'b str, std::str::Utf8Error>> {
127     let last = self.last?;
128     let (s, last) = match self.iter.next() {
129       Some(nl) => (&self.buf[last..nl], Some(nl+1)),
130       None     => (&self.buf[last..],   None),
131     };
132     self.last = last;
133     let s = str::from_utf8(s).map(|s| s.trim());
134     Some(s)
135   }
136 }
137 impl<'b> std::iter::FusedIterator for MetadataFieldIterator<'b> { }