use bytes::Bytes;
use mime::Mime;
use mime_guess;
use std::path;
use std::fs;
use std::io;
use std::io::Write;
use crate::header::{ContentDisposition, Filename};
use crate::utils::BytesWriter;
const DEFAULT_BOUNDARY: &'static str = "yuki";
pub struct Form {
pub boundary: &'static str,
storage: BytesWriter,
}
impl Form {
pub fn new() -> Self {
Self::with_boundary(DEFAULT_BOUNDARY)
}
pub fn with_boundary(boundary: &'static str) -> Self {
debug_assert!(boundary.is_ascii());
Self {
boundary,
storage: BytesWriter::new()
}
}
pub fn add_field(&mut self, name: String, data: &[u8]) {
let content_disposition = ContentDisposition::FormData(Some(name), Filename::new());
let _ = write!(&mut self.storage, "--{}\r\nContent-Disposition: {}\r\n\r\n", self.boundary, content_disposition);
let _ = self.storage.write(data);
let _ = write!(&mut self.storage, "\r\n--{}\r\n", self.boundary);
}
pub fn add_file_field(&mut self, field_name: String, file_name: String, mime: &Mime, data: &[u8]) {
let content_disposition = ContentDisposition::FormData(Some(field_name), Filename::with_name(file_name));
let _ = write!(&mut self.storage, "--{}\r\nContent-Disposition: {}\r\n", self.boundary, content_disposition);
let _ = write!(&mut self.storage, "Content-Type: {}\r\n\r\n", mime);
let _ = self.storage.write(data);
let _ = write!(&mut self.storage, "\r\n--{}\r\n", self.boundary);
}
pub fn add_file<P: AsRef<path::Path>>(&mut self, field_name: String, path: P) -> io::Result<()> {
let original_len = self.storage.len();
let path = path.as_ref();
let mut file = fs::File::open(&path)?;
let file_name = match path.file_name().and_then(|file_name| file_name.to_str()) {
Some(file_name) => Filename::with_name(file_name.to_string()),
None => Filename::new(),
};
let file_meta = file.metadata()?;
let file_len = file_meta.len() as usize;
let mime = mime_guess::from_path(path).first_or_octet_stream();
let content_disposition = ContentDisposition::FormData(Some(field_name), file_name);
let _ = write!(&mut self.storage, "--{}\r\nContent-Disposition: {}\r\n", self.boundary, content_disposition);
let _ = write!(&mut self.storage, "Content-Type: {}\r\n\r\n", mime);
self.storage.reserve(file_len);
if let Err(error) = io::copy(&mut file, &mut self.storage) {
self.storage.split_off(original_len);
return Err(error);
}
let _ = write!(&mut self.storage, "\r\n--{}\r\n", self.boundary);
Ok(())
}
pub fn finish(self) -> (u64, Bytes) {
let mut bytes = self.storage.into_inner();
let len = bytes.len();
if len == 0 {
return (0, bytes.freeze());
}
bytes[len-2] = 45; bytes[len-1] = 45;
bytes.extend_from_slice("\r\n".as_bytes());
let len = len as u64 + 2;
(len, bytes.freeze())
}
}
#[cfg(test)]
mod tests {
use super::Form;
use mime::TEXT_PLAIN;
use std::{fs, str};
use std::io::Read;
#[test]
fn multipart_form_add_simple_field() {
const EXPECTED: &'static str = "--yuki\r\nContent-Disposition: form-data; name=\"SimpleField\"\r\n\r\nsimple test\r\n--yuki--\r\n";
let mut form = Form::new();
form.add_field("SimpleField".to_string(), "simple test".as_bytes());
let (len, body) = form.finish();
let str_body = str::from_utf8(&body).expect("To get str slice of body");
assert_eq!(len, EXPECTED.len() as u64);
assert_eq!(str_body, EXPECTED);
}
#[test]
fn multipart_form_add_file() {
const FILE_NAME: &'static str = "Cargo.toml";
let mut file_body = String::new();
let mut file = fs::File::open(FILE_NAME).expect("to open file");
file.read_to_string(&mut file_body).expect("Read to string");
let expected = format!("--yuki\r\nContent-Disposition: form-data; name=\"Cargo\"; filename=\"Cargo.toml\"\r\nContent-Type: text/x-toml\r\n\r\n{}\r\n--yuki--\r\n", file_body);
let mut form = Form::new();
form.add_file("Cargo".to_string(), FILE_NAME).expect("To read file");
let (len, body) = form.finish();
let str_body = str::from_utf8(&body).expect("To get str slice of body");
assert_eq!(len as usize, expected.len());
assert_eq!(str_body, expected);
}
#[test]
fn multipart_form_add_multiple_fields() {
const EXPECTED: &'static str = "--yuki\r\nContent-Disposition: form-data; name=\"SimpleField\"\r\n\r\nsimple test\r\n--yuki\r\n--yuki\r\nContent-Disposition: form-data; name=\"SimpleFile\"; filename=\"File.txt\"\r\nContent-Type: text/plain\r\n\r\nsimple file\r\n--yuki--\r\n";
let mut form = Form::new();
form.add_field("SimpleField".to_string(), "simple test".as_bytes());
form.add_file_field("SimpleFile".to_string(), "File.txt".to_string(), &TEXT_PLAIN, "simple file".as_bytes());
let (len, body) = form.finish();
let str_body = str::from_utf8(&body).expect("To get str slice of body");
assert_eq!(len, EXPECTED.len() as u64);
assert_eq!(str_body, EXPECTED);
}
}