use std::path::Path;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use indexmap::IndexMap;
use rand::distr::{Alphanumeric, SampleString};
use urlencoding::encode;
pub trait DeboaForm {
fn content_type(&self) -> String;
fn field(&mut self, key: &str, value: &str) -> &mut Self;
fn build(&self) -> String;
}
pub enum Form {
EncodedForm(EncodedForm),
MultiPartForm(MultiPartForm),
}
impl From<EncodedForm> for Form {
fn from(val: EncodedForm) -> Self {
Form::EncodedForm(val)
}
}
impl From<MultiPartForm> for Form {
fn from(val: MultiPartForm) -> Self {
Form::MultiPartForm(val)
}
}
#[derive(Debug)]
pub struct EncodedForm {
fields: IndexMap<String, String>,
}
impl EncodedForm {
pub fn builder() -> Self {
Self {
fields: IndexMap::new(),
}
}
}
impl DeboaForm for EncodedForm {
fn content_type(&self) -> String {
"application/x-www-form-urlencoded".to_string()
}
fn field(&mut self, key: &str, value: &str) -> &mut Self {
self.fields.insert(key.to_string(), value.to_string());
self
}
fn build(&self) -> String {
self.fields
.iter()
.map(|(key, value)| format!("{}={}", key, encode(value)))
.collect::<Vec<String>>()
.join("&")
}
}
#[derive(Debug)]
pub struct MultiPartForm {
fields: IndexMap<String, String>,
boundary: String,
}
impl MultiPartForm {
pub fn builder() -> Self {
let boundary = Alphanumeric.sample_string(&mut rand::rng(), 10);
Self {
fields: IndexMap::new(),
boundary: format!("-----DeboaFormBdry{}", boundary),
}
}
pub fn file<F>(&mut self, key: &str, value: F) -> &mut Self
where
F: AsRef<std::path::Path>,
{
self.fields.insert(
key.to_string(),
value.as_ref().to_str().unwrap().to_string(),
);
self
}
pub fn boundary(&self) -> &String {
&self.boundary
}
}
impl DeboaForm for MultiPartForm {
fn content_type(&self) -> String {
format!("multipart/form-data; boundary={}", self.boundary)
}
fn field(&mut self, key: &str, value: &str) -> &mut Self {
self.fields.insert(key.to_string(), value.to_string());
self
}
fn build(&self) -> String {
let mut form = String::new();
let boundary = self.boundary();
form.push_str("\r\n");
for (key, value) in &self.fields {
if Path::is_file(value.as_ref()) {
let kind = minimime::lookup_by_filename(value);
let path = Path::new(value);
if let Some(kind) = kind {
let file_name = path.file_name();
let file_content = std::fs::read(value).unwrap();
if file_name.is_some() {
form.push_str(&format!(
"Content-Disposition: file; form-data=\"{}\"; filename=\"{}\"",
key,
file_name.unwrap().to_str().unwrap()
));
form.push_str("\r\n");
form.push_str(&format!("Content-Type: {}\r\n", kind.content_type));
form.push_str("\r\n");
if kind.is_binary() {
form.push_str(&STANDARD.encode(&file_content));
} else {
form.push_str(&String::from_utf8_lossy(&file_content));
}
}
}
} else {
form.push_str(&format!("Content-Disposition: form-data; name=\"{}\"", key));
form.push_str("\r\n");
form.push_str("\r\n");
form.push_str(value);
form.push_str("\r\n");
}
form.push_str(boundary);
if key != self.fields.last().unwrap().0 {
form.push_str("\r\n");
} else {
form.push_str("--");
}
}
form
}
}