Skip to main content

go_http/mime/
multipart.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Multipart reader/writer — port of Go's `mime/multipart`.
4use std::collections::HashMap;
5use std::io::{self, BufRead, BufReader, Read, Write};
6
7use crate::header::Header;
8use super::MimeError;
9
10// ---------------------------------------------------------------------------
11// Reader
12// ---------------------------------------------------------------------------
13
14/// Reads a MIME multipart body, iterating over its parts.
15/// Port of Go's `multipart.NewReader`.
16#[allow(dead_code)]
17pub struct Reader<R: Read> {
18    inner:    BufReader<R>,
19    boundary: String,
20    /// `--<boundary>` prefix used to detect part boundaries.
21    delim:    Vec<u8>,
22    done:     bool,
23}
24
25impl<R: Read> Reader<R> {
26    /// Create a new `Reader` with the given boundary (without leading `--`).
27    pub fn new(inner: R, boundary: &str) -> Self {
28        let delim = format!("--{boundary}").into_bytes();
29        Self {
30            inner: BufReader::new(inner),
31            boundary: boundary.to_owned(),
32            delim,
33            done: false,
34        }
35    }
36
37    /// Advance to and return the next part, or `None` if the final boundary
38    /// has been reached or the body is exhausted.
39    pub fn next_part(&mut self) -> Result<Option<Part<'_>>, MimeError> {
40        if self.done {
41            return Ok(None);
42        }
43
44        // Read lines until we find the boundary delimiter.
45        loop {
46            let mut line = String::new();
47            let n = self.inner
48                .read_line(&mut line)
49                .map_err(|e| MimeError::Other(e.to_string()))?;
50            if n == 0 {
51                self.done = true;
52                return Ok(None);
53            }
54            let trimmed = line.trim_end_matches(['\r', '\n']);
55            if trimmed.as_bytes() == self.delim.as_slice() {
56                // Found a part boundary — read headers.
57                let header = read_part_headers(&mut self.inner)?;
58                return Ok(Some(Part {
59                    header,
60                    boundary: self.delim.clone(),
61                    inner: &mut self.inner as *mut BufReader<R> as *mut (),
62                    _marker: std::marker::PhantomData,
63                }));
64            }
65            // Check for closing boundary "--<boundary>--".
66            let mut close = self.delim.clone();
67            close.extend_from_slice(b"--");
68            if trimmed.as_bytes() == close.as_slice() {
69                self.done = true;
70                return Ok(None);
71            }
72            // Otherwise it is a preamble line — skip it.
73        }
74    }
75
76    /// Read all parts into a `Form`, respecting a memory budget.
77    ///
78    /// Files larger than `max_memory` bytes are spilled to a `Vec<u8>` (no
79    /// actual temp-file in this implementation; matches Go's in-memory path).
80    ///
81    /// Port of Go's `(*Reader).ReadForm`.
82    pub fn read_form(&mut self, max_memory: i64) -> Result<Form, MimeError> {
83        let mut form = Form::default();
84        let mut mem_used: i64 = 0;
85
86        while let Some(mut part) = self.next_part()? {
87            let cd = part.header.get("Content-Disposition").unwrap_or("").to_owned();
88            let (_, params) = super::parse_media_type(&cd)
89                .unwrap_or_else(|_| ("".into(), HashMap::new()));
90
91            let name = params.get("name").cloned().unwrap_or_default();
92            let filename = params.get("filename").cloned();
93
94            let mut body = Vec::new();
95            part.read_to_end(&mut body)
96                .map_err(|e| MimeError::Other(e.to_string()))?;
97
98            if let Some(fname) = filename {
99                let size = body.len() as i64;
100                form.file
101                    .entry(name)
102                    .or_default()
103                    .push(FileHeader {
104                        filename: fname,
105                        header: part.header.clone(),
106                        size,
107                        content: body,
108                    });
109            } else {
110                mem_used += body.len() as i64;
111                if max_memory >= 0 && mem_used > max_memory {
112                    return Err(MimeError::Other("multipart: message too large".into()));
113                }
114                if let Ok(s) = String::from_utf8(body) {
115                    form.value.entry(name).or_default().push(s);
116                }
117            }
118        }
119
120        Ok(form)
121    }
122}
123
124fn read_part_headers<R: Read>(r: &mut BufReader<R>) -> Result<Header, MimeError> {
125    let mut h = Header::new();
126    loop {
127        let mut line = String::new();
128        let n = r
129            .read_line(&mut line)
130            .map_err(|e| MimeError::Other(e.to_string()))?;
131        if n == 0 {
132            break;
133        }
134        let trimmed = line.trim_end_matches(['\r', '\n']);
135        if trimmed.is_empty() {
136            break;
137        }
138        if let Some(colon) = trimmed.find(':') {
139            let name  = trimmed[..colon].trim();
140            let value = trimmed[colon + 1..].trim();
141            h.add(name, value);
142        }
143    }
144    Ok(h)
145}
146
147// ---------------------------------------------------------------------------
148// Part — a single multipart part
149// ---------------------------------------------------------------------------
150
151/// A single part within a multipart message.
152#[allow(dead_code)]
153pub struct Part<'a> {
154    pub header: Header,
155    boundary:   Vec<u8>,
156    inner:      *mut (),
157    _marker:    std::marker::PhantomData<&'a mut ()>,
158}
159
160impl<'a> Read for Part<'a> {
161    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
162        // Safety: inner is a *mut BufReader<R> cast to *mut ().
163        // This simplified implementation reads one byte at a time; a production
164        // version would use a boundary-scanning ring buffer.
165        if buf.is_empty() {
166            return Ok(0);
167        }
168        unsafe {
169            let r = &mut *(self.inner as *mut BufReader<std::io::Empty>);
170            r.read(&mut buf[..1])
171        }
172    }
173}
174
175// ---------------------------------------------------------------------------
176// Form
177// ---------------------------------------------------------------------------
178
179/// The result of `Reader::read_form`.
180#[derive(Debug, Default)]
181pub struct Form {
182    /// Non-file form values keyed by field name.
183    pub value: HashMap<String, Vec<String>>,
184    /// File parts keyed by field name.
185    pub file: HashMap<String, Vec<FileHeader>>,
186}
187
188/// A file part from a multipart form.
189#[derive(Debug)]
190pub struct FileHeader {
191    pub filename: String,
192    pub header:   Header,
193    pub size:     i64,
194    content:      Vec<u8>,
195}
196
197impl FileHeader {
198    /// Read the file content.
199    pub fn open(&self) -> impl Read + '_ {
200        std::io::Cursor::new(self.content.as_slice())
201    }
202}
203
204// ---------------------------------------------------------------------------
205// Writer
206// ---------------------------------------------------------------------------
207
208/// Writes a MIME multipart body.
209/// Port of Go's `multipart.NewWriter`.
210#[allow(dead_code)]
211pub struct Writer<W: Write> {
212    inner:    W,
213    boundary: String,
214    last:     bool,
215}
216
217impl<W: Write> Writer<W> {
218    pub fn new(inner: W) -> Self {
219        let boundary = random_boundary();
220        Self { inner, boundary, last: false }
221    }
222
223    pub fn boundary(&self) -> &str {
224        &self.boundary
225    }
226
227    /// The Content-Type value for the overall multipart body.
228    pub fn form_data_content_type(&self) -> String {
229        format!("multipart/form-data; boundary={}", self.boundary)
230    }
231
232    /// Create a new part with the given MIME headers.
233    pub fn create_part(&mut self, header: Header) -> io::Result<PartWriter<'_, W>> {
234        write!(self.inner, "--{}\r\n", self.boundary)?;
235        header.write_to(&mut self.inner)?;
236        self.inner.write_all(b"\r\n")?;
237        Ok(PartWriter { inner: &mut self.inner })
238    }
239
240    /// Convenience: create a plain form field part.
241    pub fn create_form_field(&mut self, fieldname: &str) -> io::Result<PartWriter<'_, W>> {
242        let mut h = Header::new();
243        h.set(
244            "Content-Disposition",
245            format!("form-data; name=\"{fieldname}\""),
246        );
247        self.create_part(h)
248    }
249
250    /// Convenience: create a file upload part.
251    pub fn create_form_file(
252        &mut self,
253        fieldname: &str,
254        filename: &str,
255    ) -> io::Result<PartWriter<'_, W>> {
256        let mut h = Header::new();
257        h.set(
258            "Content-Disposition",
259            format!("form-data; name=\"{fieldname}\"; filename=\"{filename}\""),
260        );
261        h.set("Content-Type", "application/octet-stream");
262        self.create_part(h)
263    }
264
265    /// Write the closing boundary and flush.
266    pub fn close(mut self) -> io::Result<W> {
267        write!(self.inner, "--{}--\r\n", self.boundary)?;
268        self.inner.flush()?;
269        Ok(self.inner)
270    }
271}
272
273/// A writer for a single part's body.
274pub struct PartWriter<'a, W: Write> {
275    inner: &'a mut W,
276}
277
278impl<'a, W: Write> Write for PartWriter<'a, W> {
279    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
280        self.inner.write(buf)
281    }
282    fn flush(&mut self) -> io::Result<()> {
283        self.inner.flush()
284    }
285}
286
287fn random_boundary() -> String {
288    use std::time::{SystemTime, UNIX_EPOCH};
289    let t = SystemTime::now()
290        .duration_since(UNIX_EPOCH)
291        .unwrap_or_default()
292        .subsec_nanos();
293    format!("{t:016x}x")
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use std::io::Write;
300
301    #[test]
302    fn writer_produces_valid_output() {
303        let mut buf = Vec::new();
304        {
305            let mut w = Writer::new(&mut buf);
306            let boundary = w.boundary().to_owned();
307            {
308                let mut part = w.create_form_field("greeting").unwrap();
309                part.write_all(b"Hello").unwrap();
310            }
311            w.close().unwrap();
312            // Basic structure checks.
313            let s = String::from_utf8(buf.clone()).unwrap();
314            assert!(s.contains(&format!("--{boundary}")));
315            assert!(s.contains("Content-Disposition: form-data; name=\"greeting\""));
316            assert!(s.contains("Hello"));
317            assert!(s.contains(&format!("--{boundary}--")));
318        }
319    }
320}