#![warn(clippy::pedantic)]
use rand::{thread_rng, RngCore};
use std::ffi::OsStr;
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Result, Write};
use std::path::Path;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct FormData<W> {
writer: Option<W>,
boundary: String,
}
impl<W: Write> FormData<W> {
pub fn new(writer: W) -> FormData<W> {
let mut buf = [0; 24];
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time should be after the Unix epoch");
(&mut buf[..4]).copy_from_slice(&now.subsec_nanos().to_ne_bytes());
(&mut buf[4..12]).copy_from_slice(&now.as_secs().to_ne_bytes());
thread_rng().fill_bytes(&mut buf[12..]);
let boundary = format!("{:->68}", base64::encode_config(&buf, base64::URL_SAFE));
FormData {
writer: Some(writer),
boundary,
}
}
pub fn finish(&mut self) -> Result<W> {
let mut writer = self
.writer
.take()
.ok_or_else(|| Error::new(ErrorKind::Other, "you can only finish once"))?;
write!(writer, "--{}--\r\n", self.boundary)?;
Ok(writer)
}
fn write_header(
&mut self,
name: &str,
filename: Option<&OsStr>,
content_type: Option<&str>,
) -> Result<&mut W> {
let writer = self.writer.as_mut().ok_or_else(|| {
Error::new(
ErrorKind::Other,
"this method cannot be used after using `finish()`",
)
})?;
write!(writer, "--{}\r\n", self.boundary)?;
write!(writer, "Content-Disposition: form-data; name=\"{}\"", name)?;
if let Some(filename) = filename {
write!(writer, "; filename=\"{}\"", filename.to_string_lossy())?;
}
write!(writer, "\r\n")?;
if let Some(content_type) = content_type {
write!(writer, "Content-Type: {}\r\n", content_type)?;
}
write!(writer, "\r\n")?;
Ok(writer)
}
pub fn write_field(&mut self, name: &str, value: &str) -> Result<()> {
let writer = self.write_header(name, None, None)?;
write!(writer, "{}\r\n", value)
}
pub fn write_file<R: Read>(
&mut self,
name: &str,
mut reader: R,
filename: Option<&OsStr>,
content_type: &str,
) -> Result<()> {
let writer = self.write_header(name, filename, Some(content_type))?;
std::io::copy(&mut reader, writer)?;
write!(writer, "\r\n")
}
pub fn write_path<P: AsRef<Path>>(
&mut self,
name: &str,
path: P,
content_type: &str,
) -> Result<()> {
self.write_file(
name,
&mut File::open(path.as_ref())?,
path.as_ref().file_name(),
content_type,
)
}
pub fn content_type_header(&self) -> String {
format!("multipart/form-data; boundary={}", self.boundary)
}
}
#[cfg(test)]
mod tests {
use crate::FormData;
use std::ffi::OsString;
use std::io::Cursor;
use std::path::Path;
#[test]
fn smoke_test() {
const CORRECT: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/testdata/form-data.bin"
));
const CORRO: &str =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/corro.svg"));
const TEXT_A: &str =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/text-a.txt"));
const TEXT_B: &str =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/text-b.txt"));
let mut form = FormData::new(Vec::new());
assert_eq!(form.boundary.len(), 68);
assert_eq!(form.boundary[..(36)], "-".repeat(36));
form.boundary = "---------------------------20598614689265574691413388431".to_owned();
form.write_path(
"file-a",
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("testdata")
.join("rustacean-flat-noshadow.png"),
"image/png",
)
.unwrap();
form.write_field("text-a", TEXT_A.trim()).unwrap();
form.write_file(
"file-b",
&mut Cursor::new(CORRO.as_bytes()),
Some(&OsString::from("corro.svg")),
"image/svg+xml",
)
.unwrap();
form.write_field("text-b", TEXT_B.trim()).unwrap();
assert_eq!(form.finish().unwrap(), CORRECT);
}
}