mime_multipart_hyper1/
lib.rs

1// Copyright 2016-2025 mime-multipart Developers
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8pub mod error;
9
10#[cfg(test)]
11mod tests;
12
13pub use error::Error;
14
15use buf_read_ext::BufReadExt;
16use http::header::{HeaderMap, HeaderName, HeaderValue};
17use mime::Mime;
18use std::fs::File;
19use std::io::{BufRead, BufReader, Read, Write};
20use std::ops::Drop;
21use std::path::{Path, PathBuf};
22use std::str::FromStr;
23use textnonce::TextNonce;
24
25/// A multipart part which is not a file (stored in memory)
26#[derive(Clone, Debug, PartialEq)]
27pub struct Part {
28    pub headers: HeaderMap,
29    pub body: Vec<u8>,
30}
31impl Part {
32    /// Mime content-type specified in the header
33    pub fn content_type(&self) -> Option<Mime> {
34        match self.headers.get("content-type") {
35            Some(ct) => match ct.to_str() {
36                Ok(value) => match Mime::from_str(value) {
37                    Ok(value) => Some(value),
38                    Err(_) => None,
39                },
40                Err(_) => None,
41            },
42            None => None,
43        }
44    }
45}
46
47/// A file that is to be inserted into a `multipart/*` or alternatively an uploaded file that
48/// was received as part of `multipart/*` parsing.
49#[derive(Clone, Debug, PartialEq)]
50pub struct FilePart {
51    /// The headers of the part
52    pub headers: HeaderMap,
53    /// A temporary file containing the file content
54    pub path: PathBuf,
55    /// Optionally, the size of the file.  This is filled when multiparts are parsed, but is
56    /// not necessary when they are generated.
57    pub size: Option<usize>,
58    // The temporary directory the upload was put into, saved for the Drop trait
59    tempdir: Option<PathBuf>,
60}
61impl FilePart {
62    pub fn new(headers: HeaderMap, path: &Path) -> FilePart {
63        FilePart {
64            headers,
65            path: path.to_owned(),
66            size: None,
67            tempdir: None,
68        }
69    }
70
71    /// If you do not want the file on disk to be deleted when Self drops, call this
72    /// function.  It will become your responsibility to clean up.
73    pub fn do_not_delete_on_drop(&mut self) {
74        self.tempdir = None;
75    }
76
77    /// Create a new temporary FilePart (when created this way, the file will be
78    /// deleted once the FilePart object goes out of scope).
79    pub fn create(headers: HeaderMap) -> Result<FilePart, Error> {
80        // Setup a file to capture the contents.
81        let mut path = tempfile::Builder::new()
82            .prefix("mime_multipart")
83            .tempdir()?
84            .into_path();
85        let tempdir = Some(path.clone());
86        path.push(TextNonce::sized_urlsafe(32).unwrap().into_string());
87        Ok(FilePart {
88            headers,
89            path,
90            size: None,
91            tempdir,
92        })
93    }
94
95    /// Filename that was specified when the file was uploaded.  Returns `Ok<None>` if there
96    /// was no content-disposition header supplied.
97    pub fn filename(&self) -> Result<Option<String>, Error> {
98        match self.headers.get("content-disposition") {
99            Some(cd) => get_content_disposition_filename(cd),
100            None => Ok(None),
101        }
102    }
103
104    /// Mime content-type specified in the header
105    pub fn content_type(&self) -> Option<Mime> {
106        match self.headers.get("content-type") {
107            Some(ct) => match ct.to_str() {
108                Ok(value) => match Mime::from_str(value) {
109                    Ok(value) => Some(value),
110                    Err(_) => None,
111                },
112                Err(_) => None,
113            },
114            None => None,
115        }
116    }
117}
118impl Drop for FilePart {
119    fn drop(&mut self) {
120        if self.tempdir.is_some() {
121            let _ = std::fs::remove_file(&self.path);
122            let _ = std::fs::remove_dir(self.tempdir.as_ref().unwrap());
123        }
124    }
125}
126
127/// A multipart part which could be either a file, in memory, or another multipart
128/// container containing nested parts.
129#[derive(Clone, Debug)]
130pub enum Node {
131    /// A part in memory
132    Part(Part),
133    /// A part streamed to a file
134    File(FilePart),
135    /// A container of nested multipart parts
136    Multipart((HeaderMap, Vec<Node>)),
137}
138
139/// Parse a MIME `multipart/*` from a `Read`able stream into a `Vec` of `Node`s, streaming
140/// files to disk and keeping the rest in memory.  Recursive `multipart/*` parts will are
141/// parsed as well and returned within a `Node::Multipart` variant.
142///
143/// If `always_use_files` is true, all parts will be streamed to files.  If false, only parts
144/// with a `ContentDisposition` header set to `Attachment` or otherwise containing a `Filename`
145/// parameter will be streamed to files.
146///
147/// It is presumed that the headers are still in the stream.  If you have them separately,
148/// use `read_multipart_body()` instead.
149pub fn read_multipart<S: Read>(stream: &mut S, always_use_files: bool) -> Result<Vec<Node>, Error> {
150    let mut reader = BufReader::with_capacity(4096, stream);
151
152    let mut buf: Vec<u8> = Vec::new();
153
154    let (_, found) = reader.stream_until_token(b"\r\n\r\n", &mut buf)?;
155    if !found {
156        return Err(Error::EofInMainHeaders);
157    }
158
159    // Keep the CRLFCRLF as httparse will expect it
160    buf.extend(b"\r\n\r\n".iter().cloned());
161
162    // Parse the headers
163    let mut header_memory = [httparse::EMPTY_HEADER; 64];
164    let headers = match httparse::parse_headers(&buf, &mut header_memory) {
165        Ok(httparse::Status::Complete((_, raw_headers))) => {
166            let mut headers = HeaderMap::new();
167            for header in raw_headers {
168                if header.value.is_empty() {
169                    break;
170                }
171                let trim = header
172                    .value
173                    .iter()
174                    .rev()
175                    .take_while(|&&x| x == b' ')
176                    .count();
177                let value = &header.value[..header.value.len() - trim];
178
179                let header_value = match HeaderValue::from_bytes(value) {
180                    Ok(value) => value,
181                    Err(_) => return Err(Error::InvalidHeaderNameOrValue),
182                };
183
184                let header_name = header.name.to_owned();
185                let header_name = match HeaderName::from_str(&header_name) {
186                    Ok(value) => value,
187                    Err(_) => return Err(Error::InvalidHeaderNameOrValue),
188                };
189                headers.append(header_name, header_value);
190            }
191            Ok(headers)
192        }
193        Ok(httparse::Status::Partial) => Err(Error::PartialHeaders),
194        Err(err) => Err(From::from(err)),
195    }?;
196
197    inner(&mut reader, &headers, always_use_files)
198}
199
200/// Parse a MIME `multipart/*` from a `Read`able stream into a `Vec` of `Node`s, streaming
201/// files to disk and keeping the rest in memory.  Recursive `multipart/*` parts will are
202/// parsed as well and returned within a `Node::Multipart` variant.
203///
204/// If `always_use_files` is true, all parts will be streamed to files.  If false, only parts
205/// with a `ContentDisposition` header set to `Attachment` or otherwise containing a `Filename`
206/// parameter will be streamed to files.
207///
208/// It is presumed that you have the `Headers` already and the stream starts at the body.
209/// If the headers are still in the stream, use `read_multipart()` instead.
210pub fn read_multipart_body<S: Read>(
211    stream: &mut S,
212    headers: &HeaderMap,
213    always_use_files: bool,
214) -> Result<Vec<Node>, Error> {
215    let mut reader = BufReader::with_capacity(4096, stream);
216    inner(&mut reader, headers, always_use_files)
217}
218
219fn inner<R: BufRead>(
220    reader: &mut R,
221    headers: &HeaderMap,
222    always_use_files: bool,
223) -> Result<Vec<Node>, Error> {
224    let mut nodes: Vec<Node> = Vec::new();
225    let mut buf: Vec<u8> = Vec::new();
226
227    let boundary = get_multipart_boundary(headers)?;
228
229    // Read past the initial boundary
230    let (_, found) = reader.stream_until_token(&boundary, &mut buf)?;
231    if !found {
232        return Err(Error::EofBeforeFirstBoundary);
233    }
234
235    // Define the boundary, including the line terminator preceding it.
236    // Use their first line terminator to determine whether to use CRLF or LF.
237    let (lt, ltlt, lt_boundary) = {
238        let peeker = reader.fill_buf()?;
239        if peeker.len() > 1 && &peeker[..2] == b"\r\n" {
240            let mut output = Vec::with_capacity(2 + boundary.len());
241            output.push(b'\r');
242            output.push(b'\n');
243            output.extend(boundary.clone());
244            (vec![b'\r', b'\n'], vec![b'\r', b'\n', b'\r', b'\n'], output)
245        } else if !peeker.is_empty() && peeker[0] == b'\n' {
246            let mut output = Vec::with_capacity(1 + boundary.len());
247            output.push(b'\n');
248            output.extend(boundary.clone());
249            (vec![b'\n'], vec![b'\n', b'\n'], output)
250        } else {
251            return Err(Error::NoCrLfAfterBoundary);
252        }
253    };
254
255    loop {
256        // If the next two lookahead characters are '--', parsing is finished.
257        {
258            let peeker = reader.fill_buf()?;
259            if peeker.len() >= 2 && &peeker[..2] == b"--" {
260                return Ok(nodes);
261            }
262        }
263
264        // Read the line terminator after the boundary
265        let (_, found) = reader.stream_until_token(&lt, &mut buf)?;
266        if !found {
267            return Err(Error::NoCrLfAfterBoundary);
268        }
269
270        // Read the headers (which end in 2 line terminators)
271        buf.truncate(0); // start fresh
272        let (_, found) = reader.stream_until_token(&ltlt, &mut buf)?;
273        if !found {
274            return Err(Error::EofInPartHeaders);
275        }
276
277        // Keep the 2 line terminators as httparse will expect it
278        buf.extend(ltlt.iter().cloned());
279
280        // Parse the headers
281        let part_headers = {
282            let mut header_memory = [httparse::EMPTY_HEADER; 4];
283            match httparse::parse_headers(&buf, &mut header_memory) {
284                Ok(httparse::Status::Complete((_, raw_headers))) => {
285                    let mut headers = HeaderMap::new();
286                    for header in raw_headers {
287                        if header.value.is_empty() {
288                            break;
289                        }
290                        let trim = header
291                            .value
292                            .iter()
293                            .rev()
294                            .take_while(|&&x| x == b' ')
295                            .count();
296                        let value = &header.value[..header.value.len() - trim];
297
298                        let header_value = match HeaderValue::from_bytes(value) {
299                            Ok(value) => value,
300                            Err(_) => return Err(Error::InvalidHeaderNameOrValue),
301                        };
302
303                        let header_name = header.name.to_owned();
304                        let header_name = match HeaderName::from_str(&header_name) {
305                            Ok(value) => value,
306                            Err(_) => return Err(Error::InvalidHeaderNameOrValue),
307                        };
308                        headers.append(header_name, header_value);
309                    }
310                    Ok(headers)
311                }
312                Ok(httparse::Status::Partial) => Err(Error::PartialHeaders),
313                Err(err) => Err(From::from(err)),
314            }?
315        };
316
317        // Check for a nested multipart
318        let nested = {
319            match part_headers.get("content-type") {
320                Some(ct) => match ct.to_str() {
321                    Ok(value) => match Mime::from_str(value) {
322                        Ok(mime) => mime.type_() == mime::MULTIPART,
323                        Err(_) => return Err(Error::HeaderValueNotMime),
324                    },
325                    Err(err) => return Err(Error::ToStr(err)),
326                },
327                None => false,
328            }
329        };
330        if nested {
331            // Recurse:
332            let inner_nodes = inner(reader, &part_headers, always_use_files)?;
333            nodes.push(Node::Multipart((part_headers, inner_nodes)));
334            continue;
335        }
336
337        let is_file = always_use_files || {
338            match part_headers.get("content-disposition") {
339                Some(content) => match content.to_str() {
340                    Ok(value) => value.contains("attachment") || value.contains("filename"),
341                    Err(err) => return Err(Error::ToStr(err)),
342                },
343                None => false,
344            }
345        };
346        if is_file {
347            // Setup a file to capture the contents.
348            let mut filepart = FilePart::create(part_headers)?;
349            let mut file = File::create(filepart.path.clone())?;
350
351            // Stream out the file.
352            let (read, found) = reader.stream_until_token(&lt_boundary, &mut file)?;
353            if !found {
354                return Err(Error::EofInFile);
355            }
356            filepart.size = Some(read);
357
358            // TODO: Handle Content-Transfer-Encoding.  RFC 7578 section 4.7 deprecated
359            // this, and the authors state "Currently, no deployed implementations that
360            // send such bodies have been discovered", so this is very low priority.
361
362            nodes.push(Node::File(filepart));
363        } else {
364            buf.truncate(0); // start fresh
365            let (_, found) = reader.stream_until_token(&lt_boundary, &mut buf)?;
366            if !found {
367                return Err(Error::EofInPart);
368            }
369
370            nodes.push(Node::Part(Part {
371                headers: part_headers,
372                body: buf.clone(),
373            }));
374        }
375    }
376}
377
378/// Get the `multipart/*` boundary string from `hyper::Headers`
379pub fn get_multipart_boundary(headers: &HeaderMap) -> Result<Vec<u8>, Error> {
380    // Verify that the request is 'Content-Type: multipart/*'.
381    let mime = match headers.get("content-type") {
382        Some(ct) => match ct.to_str() {
383            Ok(value) => match Mime::from_str(value) {
384                Ok(value) => value,
385                Err(_) => return Err(Error::HeaderValueNotMime),
386            },
387            Err(err) => return Err(Error::ToStr(err)),
388        },
389        None => return Err(Error::NoRequestContentType),
390    };
391    let top_level = mime.type_();
392
393    if top_level != mime::MULTIPART {
394        return Err(Error::NotMultipart);
395    }
396
397    match mime.get_param(mime::BOUNDARY) {
398        None => Err(Error::BoundaryNotSpecified),
399        Some(content) => {
400            let mut boundary = vec![];
401            boundary.extend(b"--".iter().cloned());
402            boundary.extend(content.to_string().as_bytes());
403            Ok(boundary)
404        }
405    }
406}
407
408#[inline]
409fn get_content_disposition_filename(cd: &HeaderValue) -> Result<Option<String>, Error> {
410    match cd.to_str() {
411        Ok(value) => match value.contains("filename") {
412            true => match value.find("filename=") {
413                Some(index) => {
414                    let start = index + "filename=".len();
415                    Ok(Some(
416                        value.get(start..).unwrap().trim_matches('\"').to_owned(),
417                    ))
418                }
419                None => match value.find("filename*=UTF-8''") {
420                    Some(index) => {
421                        let start = index + "filename*=UTF-8''".len();
422                        Ok(Some(
423                            value.get(start..).unwrap().trim_matches('\"').to_owned(),
424                        ))
425                    }
426                    None => Ok(None),
427                },
428            },
429            false => Ok(None),
430        },
431        Err(err) => Err(Error::ToStr(err)),
432    }
433}
434
435/// Generate a valid multipart boundary, statistically unlikely to be found within
436/// the content of the parts.
437pub fn generate_boundary() -> Vec<u8> {
438    TextNonce::sized(68)
439        .unwrap()
440        .into_string()
441        .into_bytes()
442        .iter()
443        .map(|&ch| {
444            if ch == b'=' {
445                b'-'
446            } else if ch == b'/' {
447                b'.'
448            } else {
449                ch
450            }
451        })
452        .collect()
453}
454
455// Convenience method, like write_all(), but returns the count of bytes written.
456trait WriteAllCount {
457    fn write_all_count(&mut self, buf: &[u8]) -> std::io::Result<usize>;
458}
459impl<T: Write> WriteAllCount for T {
460    fn write_all_count(&mut self, buf: &[u8]) -> std::io::Result<usize> {
461        self.write_all(buf)?;
462        Ok(buf.len())
463    }
464}
465
466/// Stream a multipart body to the output `stream` given, made up of the `parts`
467/// given.  Top-level headers are NOT included in this stream; the caller must send
468/// those prior to calling write_multipart().
469/// Returns the number of bytes written, or an error.
470pub fn write_multipart<S: Write>(
471    stream: &mut S,
472    boundary: &[u8],
473    nodes: &Vec<Node>,
474) -> Result<usize, Error> {
475    let mut count: usize = 0;
476
477    for node in nodes {
478        // write a boundary
479        count += stream.write_all_count(b"--")?;
480        count += stream.write_all_count(boundary)?;
481        count += stream.write_all_count(b"\r\n")?;
482
483        match *node {
484            Node::Part(ref part) => {
485                // write the part's headers
486                for header in part.headers.iter() {
487                    count += stream.write_all_count(header.0.as_str().as_bytes())?;
488                    count += stream.write_all_count(b": ")?;
489                    count += stream.write_all_count(header.1.as_bytes())?;
490                    count += stream.write_all_count(b"\r\n")?;
491                }
492
493                // write the blank line
494                count += stream.write_all_count(b"\r\n")?;
495
496                // Write the part's content
497                count += stream.write_all_count(&part.body)?;
498            }
499            Node::File(ref filepart) => {
500                // write the part's headers
501                for header in filepart.headers.iter() {
502                    count += stream.write_all_count(header.0.as_str().as_bytes())?;
503                    count += stream.write_all_count(b": ")?;
504                    count += stream.write_all_count(header.1.as_bytes())?;
505                    count += stream.write_all_count(b"\r\n")?;
506                }
507
508                // write the blank line
509                count += stream.write_all_count(b"\r\n")?;
510
511                // Write out the files's content
512                let mut file = File::open(&filepart.path)?;
513                count += std::io::copy(&mut file, stream)? as usize;
514            }
515            Node::Multipart((ref headers, ref subnodes)) => {
516                // Get boundary
517                let boundary = get_multipart_boundary(headers)?;
518
519                // write the multipart headers
520                for header in headers.iter() {
521                    count += stream.write_all_count(header.0.as_str().as_bytes())?;
522                    count += stream.write_all_count(b": ")?;
523                    count += stream.write_all_count(header.1.as_bytes())?;
524                    count += stream.write_all_count(b"\r\n")?;
525                }
526
527                // write the blank line
528                count += stream.write_all_count(b"\r\n")?;
529
530                // Recurse
531                count += write_multipart(stream, &boundary, subnodes)?;
532            }
533        }
534
535        // write a line terminator
536        count += stream.write_all_count(b"\r\n")?;
537    }
538
539    // write a final boundary
540    count += stream.write_all_count(b"--")?;
541    count += stream.write_all_count(boundary)?;
542    count += stream.write_all_count(b"--")?;
543
544    Ok(count)
545}
546
547pub fn write_chunk<S: Write>(stream: &mut S, chunk: &[u8]) -> Result<(), ::std::io::Error> {
548    write!(stream, "{:x}\r\n", chunk.len())?;
549    stream.write_all(chunk)?;
550    stream.write_all(b"\r\n")?;
551    Ok(())
552}
553
554/// Stream a multipart body to the output `stream` given, made up of the `parts`
555/// given, using Tranfer-Encoding: Chunked.  Top-level headers are NOT included in this
556/// stream; the caller must send those prior to calling write_multipart_chunked().
557pub fn write_multipart_chunked<S: Write>(
558    stream: &mut S,
559    boundary: &[u8],
560    nodes: &Vec<Node>,
561) -> Result<(), Error> {
562    for node in nodes {
563        // write a boundary
564        write_chunk(stream, b"--")?;
565        write_chunk(stream, boundary)?;
566        write_chunk(stream, b"\r\n")?;
567
568        match *node {
569            Node::Part(ref part) => {
570                // write the part's headers
571                for header in part.headers.iter() {
572                    write_chunk(stream, header.0.as_str().as_bytes())?;
573                    write_chunk(stream, b": ")?;
574                    write_chunk(stream, header.1.as_bytes())?;
575                    write_chunk(stream, b"\r\n")?;
576                }
577
578                // write the blank line
579                write_chunk(stream, b"\r\n")?;
580
581                // Write the part's content
582                write_chunk(stream, &part.body)?;
583            }
584            Node::File(ref filepart) => {
585                // write the part's headers
586                for header in filepart.headers.iter() {
587                    write_chunk(stream, header.0.as_str().as_bytes())?;
588                    write_chunk(stream, b": ")?;
589                    write_chunk(stream, header.1.as_bytes())?;
590                    write_chunk(stream, b"\r\n")?;
591                }
592
593                // write the blank line
594                write_chunk(stream, b"\r\n")?;
595
596                // Write out the files's length
597                let metadata = std::fs::metadata(&filepart.path)?;
598                write!(stream, "{:x}\r\n", metadata.len())?;
599
600                // Write out the file's content
601                let mut file = File::open(&filepart.path)?;
602                std::io::copy(&mut file, stream)?;
603                stream.write_all(b"\r\n")?;
604            }
605            Node::Multipart((ref headers, ref subnodes)) => {
606                // Get boundary
607                let boundary = get_multipart_boundary(headers)?;
608
609                // write the multipart headers
610                for header in headers.iter() {
611                    write_chunk(stream, header.0.as_str().as_bytes())?;
612                    write_chunk(stream, b": ")?;
613                    write_chunk(stream, header.1.as_bytes())?;
614                    write_chunk(stream, b"\r\n")?;
615                }
616
617                // write the blank line
618                write_chunk(stream, b"\r\n")?;
619
620                // Recurse
621                write_multipart_chunked(stream, &boundary, subnodes)?;
622            }
623        }
624
625        // write a line terminator
626        write_chunk(stream, b"\r\n")?;
627    }
628
629    // write a final boundary
630    write_chunk(stream, b"--")?;
631    write_chunk(stream, boundary)?;
632    write_chunk(stream, b"--")?;
633
634    // Write an empty chunk to signal the end of the body
635    write_chunk(stream, b"")?;
636
637    Ok(())
638}