#[cfg(feature = "stream")]
use std::io;
#[cfg(feature = "stream")]
use std::path::Path;
use std::{borrow::Cow, fmt, pin::Pin};
use compio::bytes::Bytes;
#[cfg(feature = "stream")]
use compio::fs::File;
use futures_util::{Stream, StreamExt, TryStreamExt, future, stream};
use http::HeaderMap;
use mime::Mime;
use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};
use send_wrapper::SendWrapper;
use crate::Body;
pub struct Form {
inner: FormParts<Part>,
}
pub struct Part {
meta: PartMetadata,
value: Body,
body_length: Option<u64>,
}
pub(crate) struct FormParts<P> {
pub(crate) boundary: String,
pub(crate) computed_headers: Vec<Vec<u8>>,
pub(crate) fields: Vec<(Cow<'static, str>, P)>,
pub(crate) percent_encoding: PercentEncoding,
}
pub(crate) struct PartMetadata {
mime: Option<Mime>,
file_name: Option<Cow<'static, str>>,
pub(crate) headers: HeaderMap,
}
pub(crate) trait PartProps {
fn value_len(&self) -> Option<u64>;
fn metadata(&self) -> &PartMetadata;
}
impl Default for Form {
fn default() -> Self {
Self::new()
}
}
impl Form {
pub fn new() -> Form {
Form {
inner: FormParts::new(),
}
}
#[inline]
pub fn boundary(&self) -> &str {
self.inner.boundary()
}
pub fn text<T, U>(self, name: T, value: U) -> Form
where
T: Into<Cow<'static, str>>,
U: Into<Cow<'static, str>>,
{
self.part(name, Part::text(value))
}
#[cfg(feature = "stream")]
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
pub async fn file<T, U>(self, name: T, path: U) -> io::Result<Form>
where
T: Into<Cow<'static, str>>,
U: AsRef<Path>,
{
Ok(self.part(name, Part::file(path).await?))
}
pub fn part<T>(self, name: T, part: Part) -> Form
where
T: Into<Cow<'static, str>>,
{
self.with_inner(move |inner| inner.part(name, part))
}
pub fn percent_encode_path_segment(self) -> Form {
self.with_inner(|inner| inner.percent_encode_path_segment())
}
pub fn percent_encode_attr_chars(self) -> Form {
self.with_inner(|inner| inner.percent_encode_attr_chars())
}
pub fn percent_encode_noop(self) -> Form {
self.with_inner(|inner| inner.percent_encode_noop())
}
pub(crate) fn stream(self) -> Body {
if self.inner.fields.is_empty() {
return Body::empty();
}
Body::stream(self.into_stream())
}
pub fn into_stream(mut self) -> impl Stream<Item = Result<Bytes, crate::Error>> + Send + Sync {
if self.inner.fields.is_empty() {
let empty_stream: Pin<
Box<dyn Stream<Item = Result<Bytes, crate::Error>> + Send + Sync>,
> = Box::pin(futures_util::stream::empty());
return empty_stream;
}
let (name, part) = self.inner.fields.remove(0);
let start = Box::pin(SendWrapper::new(self.part_stream(name, part)))
as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>;
let fields = self.inner.take_fields();
let stream = fields.into_iter().fold(start, |memo, (name, part)| {
let part_stream = self.part_stream(name, part);
Box::pin(SendWrapper::new(memo.chain(part_stream)))
as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>
});
let last = stream::once(future::ready(Ok(
format!("--{}--\r\n", self.boundary()).into()
)));
Box::pin(stream.chain(last))
}
pub(crate) fn part_stream<T>(
&mut self,
name: T,
part: Part,
) -> impl Stream<Item = Result<Bytes, crate::Error>> + use<T>
where
T: Into<Cow<'static, str>>,
{
let boundary = stream::once(future::ready(Ok(
format!("--{}\r\n", self.boundary()).into()
)));
let header = stream::once(future::ready(Ok({
let mut h = self
.inner
.percent_encoding
.encode_headers(&name.into(), &part.meta);
h.extend_from_slice(b"\r\n\r\n");
h.into()
})));
boundary
.chain(header)
.chain(part.value.into_stream())
.chain(stream::once(future::ready(Ok("\r\n".into()))))
}
pub(crate) fn compute_length(&mut self) -> Option<u64> {
self.inner.compute_length()
}
fn with_inner<F>(self, func: F) -> Self
where
F: FnOnce(FormParts<Part>) -> FormParts<Part>,
{
Form {
inner: func(self.inner),
}
}
}
impl fmt::Debug for Form {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.inner.fmt_fields("Form", f)
}
}
impl Part {
pub fn text<T>(value: T) -> Part
where
T: Into<Cow<'static, str>>,
{
let body = match value.into() {
Cow::Borrowed(slice) => Body::from(slice),
Cow::Owned(string) => Body::from(string),
};
Part::new(body, None)
}
pub fn bytes<T>(value: T) -> Part
where
T: Into<Cow<'static, [u8]>>,
{
let body = match value.into() {
Cow::Borrowed(slice) => Body::from(slice),
Cow::Owned(vec) => Body::from(vec),
};
Part::new(body, None)
}
pub fn stream<T: Into<Body>>(value: T) -> Part {
Part::new(value.into(), None)
}
pub fn stream_with_length<T: Into<Body>>(value: T, length: u64) -> Part {
Part::new(value.into(), Some(length))
}
#[cfg(feature = "stream")]
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
pub async fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
let path = path.as_ref();
let file_name = path
.file_name()
.map(|filename| filename.to_string_lossy().into_owned());
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
let mime = mime_guess::from_ext(ext).first_or_octet_stream();
let file = File::open(path).await?;
let len = file.metadata().await.map(|m| m.len()).ok();
let field = match len {
Some(len) => Part::stream_with_length(file, len),
None => Part::stream(file),
}
.mime(mime);
Ok(if let Some(file_name) = file_name {
field.file_name(file_name)
} else {
field
})
}
fn new(value: Body, body_length: Option<u64>) -> Part {
Part {
meta: PartMetadata::new(),
value,
body_length,
}
}
pub fn mime(self, mime: Mime) -> Part {
self.with_inner(move |inner| inner.mime(mime))
}
pub fn file_name<T>(self, filename: T) -> Part
where
T: Into<Cow<'static, str>>,
{
self.with_inner(move |inner| inner.file_name(filename))
}
pub fn headers(self, headers: HeaderMap) -> Part {
self.with_inner(move |inner| inner.headers(headers))
}
fn with_inner<F>(self, func: F) -> Self
where
F: FnOnce(PartMetadata) -> PartMetadata,
{
Part {
meta: func(self.meta),
..self
}
}
}
impl fmt::Debug for Part {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut dbg = f.debug_struct("Part");
dbg.field("value", &self.value);
self.meta.fmt_fields(&mut dbg);
dbg.finish()
}
}
impl PartProps for Part {
fn value_len(&self) -> Option<u64> {
if self.body_length.is_some() {
self.body_length
} else {
self.value.content_length()
}
}
fn metadata(&self) -> &PartMetadata {
&self.meta
}
}
impl<P: PartProps> FormParts<P> {
pub(crate) fn new() -> Self {
FormParts {
boundary: gen_boundary(),
computed_headers: Vec::new(),
fields: Vec::new(),
percent_encoding: PercentEncoding::PathSegment,
}
}
pub(crate) fn boundary(&self) -> &str {
&self.boundary
}
pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
where
T: Into<Cow<'static, str>>,
{
self.fields.push((name.into(), part));
self
}
pub(crate) fn percent_encode_path_segment(mut self) -> Self {
self.percent_encoding = PercentEncoding::PathSegment;
self
}
pub(crate) fn percent_encode_attr_chars(mut self) -> Self {
self.percent_encoding = PercentEncoding::AttrChar;
self
}
pub(crate) fn percent_encode_noop(mut self) -> Self {
self.percent_encoding = PercentEncoding::NoOp;
self
}
pub(crate) fn compute_length(&mut self) -> Option<u64> {
let mut length = 0u64;
for (name, field) in self.fields.iter() {
match field.value_len() {
Some(value_length) => {
let header = self.percent_encoding.encode_headers(name, field.metadata());
let header_length = header.len();
self.computed_headers.push(header);
length += 2
+ self.boundary().len() as u64
+ 2
+ header_length as u64
+ 4
+ value_length
+ 2
}
_ => return None,
}
}
if !self.fields.is_empty() {
length += 2 + self.boundary().len() as u64 + 4
}
Some(length)
}
fn take_fields(&mut self) -> Vec<(Cow<'static, str>, P)> {
std::mem::take(&mut self.fields)
}
}
impl<P: fmt::Debug> FormParts<P> {
pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct(ty_name)
.field("boundary", &self.boundary)
.field("parts", &self.fields)
.finish()
}
}
impl PartMetadata {
pub(crate) fn new() -> Self {
PartMetadata {
mime: None,
file_name: None,
headers: HeaderMap::default(),
}
}
pub(crate) fn mime(mut self, mime: Mime) -> Self {
self.mime = Some(mime);
self
}
pub(crate) fn file_name<T>(mut self, filename: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.file_name = Some(filename.into());
self
}
pub(crate) fn headers<T>(mut self, headers: T) -> Self
where
T: Into<HeaderMap>,
{
self.headers = headers.into();
self
}
}
impl PartMetadata {
pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
&self,
debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
debug_struct
.field("mime", &self.mime)
.field("file_name", &self.file_name)
.field("headers", &self.headers)
}
}
const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`');
const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');
const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET.add(b'/').add(b'%');
const ATTR_CHAR_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'!')
.remove(b'#')
.remove(b'$')
.remove(b'&')
.remove(b'+')
.remove(b'-')
.remove(b'.')
.remove(b'^')
.remove(b'_')
.remove(b'`')
.remove(b'|')
.remove(b'~');
pub(crate) enum PercentEncoding {
PathSegment,
AttrChar,
NoOp,
}
impl PercentEncoding {
pub(crate) fn encode_headers(&self, name: &str, field: &PartMetadata) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"Content-Disposition: form-data; ");
match self.percent_encode(name) {
Cow::Borrowed(value) => {
buf.extend_from_slice(b"name=\"");
buf.extend_from_slice(value.as_bytes());
buf.extend_from_slice(b"\"");
}
Cow::Owned(value) => {
buf.extend_from_slice(b"name*=utf-8''");
buf.extend_from_slice(value.as_bytes());
}
}
if let Some(filename) = &field.file_name {
buf.extend_from_slice(b"; filename=\"");
let legal_filename = filename
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\r', "\\\r")
.replace('\n', "\\\n");
buf.extend_from_slice(legal_filename.as_bytes());
buf.extend_from_slice(b"\"");
}
if let Some(mime) = &field.mime {
buf.extend_from_slice(b"\r\nContent-Type: ");
buf.extend_from_slice(mime.as_ref().as_bytes());
}
for (k, v) in field.headers.iter() {
buf.extend_from_slice(b"\r\n");
buf.extend_from_slice(k.as_str().as_bytes());
buf.extend_from_slice(b": ");
buf.extend_from_slice(v.as_bytes());
}
buf
}
fn percent_encode<'a>(&self, value: &'a str) -> Cow<'a, str> {
use percent_encoding::utf8_percent_encode as percent_encode;
match self {
Self::PathSegment => percent_encode(value, PATH_SEGMENT_ENCODE_SET).into(),
Self::AttrChar => percent_encode(value, ATTR_CHAR_ENCODE_SET).into(),
Self::NoOp => value.into(),
}
}
}
fn gen_boundary() -> String {
use crate::util::fast_random as random;
let a = random();
let b = random();
let c = random();
let d = random();
format!("{a:016x}-{b:016x}-{c:016x}-{d:016x}")
}