mco_http/multipart/
mult_part.rs

1use std::io::{ErrorKind, Read, Write};
2use crate::multipart::{Node, Part, FilePart};
3use mime::{Mime, TopLevel, SubLevel};
4use crate::multipart::error::Error;
5use crate::header::{ContentDisposition, ContentType, DispositionParam, DispositionType, Headers};
6
7/// The extracted text fields and uploaded files from a `multipart/form-data` request.
8///
9/// Use `parse_multipart` to devise this object from a request.
10#[derive(Clone, Debug)]
11pub struct FormData {
12    /// Name-value pairs for plain text fields. Technically, these are form data parts with no
13    /// filename specified in the part's `Content-Disposition`.
14    pub fields: Vec<(String, String)>,
15    /// Name-value pairs for temporary files. Technically, these are form data parts with a filename
16    /// specified in the part's `Content-Disposition`.
17    pub files: Vec<(String, FilePart)>,
18}
19
20impl FormData {
21    pub fn new() -> FormData {
22        FormData { fields: vec![], files: vec![] }
23    }
24
25    /// Create a mime-multipart Vec<Node> from this FormData
26    pub fn to_multipart(&mut self) -> Result<Vec<Node>, Error> {
27        // Translate to Nodes
28        let mut nodes: Vec<Node> = Vec::with_capacity(self.fields.len() + self.files.len());
29
30        for &(ref name, ref value) in &self.fields {
31            let mut h = crate::header::Headers::with_capacity(2);
32            h.set(ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![])));
33            h.set(ContentDisposition {
34                disposition: DispositionType::Ext("form-data".to_owned()),
35                parameters: vec![DispositionParam::Ext("name".to_owned(), name.clone())],
36            });
37            nodes.push(Node::Part(Part {
38                headers: h,
39                body: value.as_bytes().to_owned(),
40            }));
41        }
42
43        for &(ref name, ref filepart) in &self.files {
44            let mut filepart = filepart.clone();
45            // We leave all headers that the caller specified, except that we rewrite
46            // Content-Disposition.
47            while filepart.headers.remove::<ContentDisposition>() {};
48            let filename = match filepart.filename() {
49                Ok(fname) => fname.to_string(),
50                Err(_) => return Err(Error::Io(std::io::Error::new(ErrorKind::InvalidData, "not a file"))),
51            };
52            filepart.headers.set(ContentDisposition {
53                disposition: DispositionType::Ext("form-data".to_owned()),
54                parameters: vec![DispositionParam::Ext("name".to_owned(), name.clone()),
55                                 DispositionParam::Ext("filename".to_owned(), filename)],
56            });
57            nodes.push(Node::File(filepart));
58        }
59
60        Ok(nodes)
61    }
62}
63
64
65/// Parse MIME `multipart/form-data` information from a stream as a `FormData`.
66pub fn read_formdata<S: Read>(stream: &mut S, headers: &Headers, f: Option<fn(name: &mut FilePart) -> std::io::Result<()>>) -> Result<FormData, Error>
67{
68    let nodes = crate::multipart::read_multipart_body(stream, headers, false, f)?;
69    let mut formdata = FormData::new();
70    fill_formdata(&mut formdata, nodes)?;
71    Ok(formdata)
72}
73
74// order and nesting are irrelevant, so we interate through the nodes and put them
75// into one of two buckets (fields and files);  If a multipart node is found, it uses
76// the name in its headers as the key (rather than the name in the headers of the
77// subparts), which is how multiple file uploads work.
78fn fill_formdata(formdata: &mut FormData, nodes: Vec<Node>) -> Result<(), Error>
79{
80    for node in nodes {
81        match node {
82            Node::Part(part) => {
83                let cd_name: Option<String> = {
84                    let cd: &ContentDisposition = match part.headers.get() {
85                        Some(cd) => cd,
86                        None => return Err(Error::MissingDisposition),
87                    };
88                    get_content_disposition_name(&cd)
89                };
90                let key = cd_name.ok_or(Error::NoName)?;
91                let val = String::from_utf8(part.body)?;
92                formdata.fields.push((key, val));
93            }
94            Node::File(part) => {
95                let cd_name: Option<String> = {
96                    let cd: &ContentDisposition = match part.headers.get() {
97                        Some(cd) => cd,
98                        None => return Err(Error::MissingDisposition),
99                    };
100                    get_content_disposition_name(&cd)
101                };
102                let key = cd_name.ok_or(Error::NoName)?;
103                formdata.files.push((key, part));
104            }
105            Node::Multipart((headers, nodes)) => {
106                let cd_name: Option<String> = {
107                    let cd: &ContentDisposition = match headers.get() {
108                        Some(cd) => cd,
109                        None => return Err(Error::MissingDisposition),
110                    };
111                    get_content_disposition_name(&cd)
112                };
113                let key = cd_name.ok_or(Error::NoName)?;
114                for node in nodes {
115                    match node {
116                        Node::Part(part) => {
117                            let val = String::from_utf8(part.body)?;
118                            formdata.fields.push((key.clone(), val));
119                        }
120                        Node::File(part) => {
121                            formdata.files.push((key.clone(), part));
122                        }
123                        _ => {} // don't recurse deeper
124                    }
125                }
126            }
127        }
128    }
129    Ok(())
130}
131
132#[inline]
133pub fn get_content_disposition_name(cd: &ContentDisposition) -> Option<String> {
134    if let Some(&DispositionParam::Ext(_, ref value)) = cd.parameters.iter()
135        .find(|&x| match *x {
136            DispositionParam::Ext(ref token, _) => &*token == "name",
137            _ => false,
138        })
139    {
140        Some(value.clone())
141    } else {
142        None
143    }
144}
145
146
147/// Stream out `multipart/form-data` body content matching the passed in `formdata`.  This
148/// does not stream out headers, so the caller must stream those out before calling
149/// write_formdata().
150pub fn write_formdata<S: Write,W:Write>(stream: &mut S, boundary: &Vec<u8>, formdata: &mut FormData, w: Option<fn(name: &mut FilePart) -> std::io::Result<()>>)
151                                        -> Result<usize, Error>
152{
153    let mut nodes = formdata.to_multipart()?;
154
155    // Write out
156    let count = crate::multipart::write_multipart(stream, boundary, &mut nodes,w)?;
157
158    Ok(count)
159}
160
161/// Stream out `multipart/form-data` body content matching the passed in `formdata` as
162/// Transfer-Encoding: Chunked.  This does not stream out headers, so the caller must stream
163/// those out before calling write_formdata().
164pub fn write_formdata_chunked<S: Write>(stream: &mut S, boundary: &Vec<u8>, formdata: &mut FormData)
165                                        -> Result<(), Error>
166{
167    let nodes = formdata.to_multipart()?;
168
169    // Write out
170    crate::multipart::write_multipart_chunked(stream, boundary, &nodes)?;
171
172    Ok(())
173}