use crate::regex::Regex;
use http::HeaderValue;
use once_cell::sync::Lazy;
use std::fmt::{self, Display};
use std::str::FromStr;
use crate::tina::data::app_error::AppError;
use crate::tina::data::AppResult;
use language_tags::LanguageTag;
use percent_encoding::{AsciiSet, CONTROLS};
fn split_once(haystack: &str, needle: char) -> (&str, &str) {
haystack.find(needle).map_or_else(
|| (haystack, ""),
|sc| {
let (first, last) = haystack.split_at(sc);
(first, last.split_at(1).1)
},
)
}
fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
let (first, last) = split_once(haystack, needle);
(first.trim_end(), last.trim_start())
}
#[derive(Clone, Debug, PartialEq)]
pub enum DispositionType {
Inline,
Attachment,
FormData,
Ext(String),
}
impl<'a> From<&'a str> for DispositionType {
fn from(origin: &'a str) -> DispositionType {
if origin.eq_ignore_ascii_case("inline") {
DispositionType::Inline
} else if origin.eq_ignore_ascii_case("attachment") {
DispositionType::Attachment
} else if origin.eq_ignore_ascii_case("form-data") {
DispositionType::FormData
} else {
DispositionType::Ext(origin.to_owned())
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum DispositionParam {
Name(String),
Filename(String),
FilenameExt(ExtendedValue),
Unknown(String, String),
UnknownExt(String, ExtendedValue),
}
impl DispositionParam {
#[inline]
pub fn is_name(&self) -> bool {
self.as_name().is_some()
}
#[inline]
pub fn is_filename(&self) -> bool {
self.as_filename().is_some()
}
#[inline]
pub fn is_filename_ext(&self) -> bool {
self.as_filename_ext().is_some()
}
#[inline]
pub fn is_unknown<T: AsRef<str>>(&self, name: T) -> bool {
self.as_unknown(name).is_some()
}
#[inline]
pub fn is_unknown_ext<T: AsRef<str>>(&self, name: T) -> bool {
self.as_unknown_ext(name).is_some()
}
#[inline]
pub fn as_name(&self) -> Option<&str> {
match self {
DispositionParam::Name(ref name) => Some(name.as_str()),
_ => None,
}
}
#[inline]
pub fn as_filename(&self) -> Option<&str> {
match self {
DispositionParam::Filename(ref filename) => Some(filename.as_str()),
_ => None,
}
}
#[inline]
pub fn as_filename_ext(&self) -> Option<&ExtendedValue> {
match self {
DispositionParam::FilenameExt(ref value) => Some(value),
_ => None,
}
}
#[inline]
pub fn as_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
match self {
DispositionParam::Unknown(ref ext_name, ref value) if ext_name.eq_ignore_ascii_case(name.as_ref()) => Some(value.as_str()),
_ => None,
}
}
#[inline]
pub fn as_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
match self {
DispositionParam::UnknownExt(ref ext_name, ref value) if ext_name.eq_ignore_ascii_case(name.as_ref()) => Some(value),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition {
pub disposition: DispositionType,
pub parameters: Vec<DispositionParam>,
}
impl ContentDisposition {
pub fn from_raw(hv: &HeaderValue) -> AppResult<Self> {
let hv = String::from_utf8(hv.as_bytes().to_vec()).map_err(crate::app_error_from!())?;
let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
if disp_type.is_empty() {
return Err(crate::app_system_error!("Invalid Header provided"));
}
let mut cd = ContentDisposition {
disposition: disp_type.into(),
parameters: Vec::new(),
};
while !left.is_empty() {
let (param_name, new_left) = split_once_and_trim(left, '=');
if param_name.is_empty() || param_name == "*" || new_left.is_empty() {
return Err(crate::app_system_error!("Invalid Header provided"));
}
left = new_left;
if let Some(param_name) = param_name.strip_suffix('*') {
let (ext_value, new_left) = split_once_and_trim(left, ';');
left = new_left;
let ext_value = parse_extended_value(ext_value)?;
let param = if param_name.eq_ignore_ascii_case("filename") {
DispositionParam::FilenameExt(ext_value)
} else {
DispositionParam::UnknownExt(param_name.to_owned(), ext_value)
};
cd.parameters.push(param);
} else {
let value = if left.starts_with('"') {
let mut escaping = false;
let mut quoted_string = vec![];
let mut end = None;
for (i, &c) in left.as_bytes().iter().skip(1).enumerate() {
if escaping {
escaping = false;
quoted_string.push(c);
} else if c == 0x5c {
escaping = true;
} else if c == 0x22 {
end = Some(i + 1); break;
} else {
quoted_string.push(c);
}
}
left = &left[end.ok_or(crate::app_system_error!("Invalid Header provided"))? + 1..];
left = split_once(left, ';').1.trim_start();
String::from_utf8(quoted_string).map_err(crate::app_error_from!())?
} else {
let (token, new_left) = split_once_and_trim(left, ';');
left = new_left;
if token.is_empty() {
return Err(crate::app_system_error!("Invalid Header provided"));
}
token.to_owned()
};
let param = if param_name.eq_ignore_ascii_case("name") {
DispositionParam::Name(value)
} else if param_name.eq_ignore_ascii_case("filename") {
DispositionParam::Filename(value)
} else {
DispositionParam::Unknown(param_name.to_owned(), value)
};
cd.parameters.push(param);
}
}
Ok(cd)
}
pub fn is_inline(&self) -> bool {
matches!(self.disposition, DispositionType::Inline)
}
pub fn is_attachment(&self) -> bool {
matches!(self.disposition, DispositionType::Attachment)
}
pub fn is_form_data(&self) -> bool {
matches!(self.disposition, DispositionType::FormData)
}
pub fn is_ext<T: AsRef<str>>(&self, disp_type: T) -> bool {
matches!(self.disposition, DispositionType::Ext(ref t) if t.eq_ignore_ascii_case(disp_type.as_ref()))
}
pub fn get_name(&self) -> Option<&str> {
self.parameters.iter().filter_map(|p| p.as_name()).next()
}
pub fn get_filename(&self) -> Option<&str> {
self.parameters.iter().filter_map(|p| p.as_filename()).next()
}
pub fn get_filename_ext(&self) -> Option<&ExtendedValue> {
self.parameters.iter().filter_map(|p| p.as_filename_ext()).next()
}
pub fn get_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
let name = name.as_ref();
self.parameters.iter().filter_map(|p| p.as_unknown(name)).next()
}
pub fn get_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
let name = name.as_ref();
self.parameters.iter().filter_map(|p| p.as_unknown_ext(name)).next()
}
}
impl fmt::Display for DispositionType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DispositionType::Inline => write!(f, "inline"),
DispositionType::Attachment => write!(f, "attachment"),
DispositionType::FormData => write!(f, "form-data"),
DispositionType::Ext(ref s) => write!(f, "{}", s),
}
}
}
impl fmt::Display for DispositionParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
static RE: Lazy<AppResult<Regex>> = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").map_err(crate::app_error_from!()));
let re = RE.as_ref().map_err(|err| {
tracing::error!("{}", err);
fmt::Error::default()
})?;
match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value),
DispositionParam::Filename(ref value) => {
write!(f, "filename=\"{}\"", re.replace_all(value, "\\$0").as_ref())
}
DispositionParam::Unknown(ref name, ref value) => write!(f, "{}=\"{}\"", name, re.replace_all(value, "\\$0").as_ref()),
DispositionParam::FilenameExt(ref ext_value) => {
write!(f, "filename*={}", ext_value)
}
DispositionParam::UnknownExt(ref name, ref ext_value) => {
write!(f, "{}*={}", name, ext_value)
}
}
}
}
impl From<ContentDisposition> for HeaderValue {
fn from(value: ContentDisposition) -> Self {
let str = value.to_string();
HeaderValue::from_str(str.as_str()).unwrap_or_else(|_| panic!("ContentDisposition into HeaderValue failed: {}", str))
}
}
impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.disposition)?;
self.parameters.iter().try_for_each(|param| write!(f, "; {}", param))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtendedValue {
pub charset: Charset,
pub language_tag: Option<LanguageTag>,
pub value: Vec<u8>,
}
pub fn parse_extended_value(val: &str) -> AppResult<ExtendedValue> {
let mut parts = val.splitn(3, '\'');
let charset: Charset = match parts.next() {
None => return Err(crate::app_system_error!("Invalid Header provided")),
Some(n) => FromStr::from_str(n).map_err(crate::app_error_from!())?,
};
let language_tag: Option<LanguageTag> = match parts.next() {
None => return Err(crate::app_system_error!("Invalid Header provided")),
Some("") => None,
Some(s) => match s.parse() {
Ok(lt) => Some(lt),
Err(_) => return Err(crate::app_system_error!("Invalid Header provided")),
},
};
let value: Vec<u8> = match parts.next() {
None => return Err(crate::app_system_error!("Invalid Header provided")),
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
};
Ok(ExtendedValue {
charset,
language_tag,
value,
})
}
impl std::fmt::Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded_value = percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else {
write!(f, "{}''{}", self.charset, encoded_value)
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types)]
pub enum Charset {
Us_Ascii,
Iso_8859_1,
Iso_8859_2,
Iso_8859_3,
Iso_8859_4,
Iso_8859_5,
Iso_8859_6,
Iso_8859_7,
Iso_8859_8,
Iso_8859_9,
Iso_8859_10,
Shift_Jis,
Euc_Jp,
Iso_2022_Kr,
Euc_Kr,
Iso_2022_Jp,
Iso_2022_Jp_2,
Iso_8859_6_E,
Iso_8859_6_I,
Iso_8859_8_E,
Iso_8859_8_I,
Gb2312,
Big5,
Koi8_R,
Ext(String),
}
impl Charset {
fn label(&self) -> &str {
match self {
Charset::Us_Ascii => "US-ASCII",
Charset::Iso_8859_1 => "ISO-8859-1",
Charset::Iso_8859_2 => "ISO-8859-2",
Charset::Iso_8859_3 => "ISO-8859-3",
Charset::Iso_8859_4 => "ISO-8859-4",
Charset::Iso_8859_5 => "ISO-8859-5",
Charset::Iso_8859_6 => "ISO-8859-6",
Charset::Iso_8859_7 => "ISO-8859-7",
Charset::Iso_8859_8 => "ISO-8859-8",
Charset::Iso_8859_9 => "ISO-8859-9",
Charset::Iso_8859_10 => "ISO-8859-10",
Charset::Shift_Jis => "Shift-JIS",
Charset::Euc_Jp => "EUC-JP",
Charset::Iso_2022_Kr => "ISO-2022-KR",
Charset::Euc_Kr => "EUC-KR",
Charset::Iso_2022_Jp => "ISO-2022-JP",
Charset::Iso_2022_Jp_2 => "ISO-2022-JP-2",
Charset::Iso_8859_6_E => "ISO-8859-6-E",
Charset::Iso_8859_6_I => "ISO-8859-6-I",
Charset::Iso_8859_8_E => "ISO-8859-8-E",
Charset::Iso_8859_8_I => "ISO-8859-8-I",
Charset::Gb2312 => "GB2312",
Charset::Big5 => "big5",
Charset::Koi8_R => "KOI8-R",
Charset::Ext(ref s) => s,
}
}
}
impl Display for Charset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
impl FromStr for Charset {
type Err = AppError;
fn from_str(s: &str) -> AppResult<Charset> {
Ok(match s.to_ascii_uppercase().as_ref() {
"US-ASCII" => Charset::Us_Ascii,
"ISO-8859-1" => Charset::Iso_8859_1,
"ISO-8859-2" => Charset::Iso_8859_2,
"ISO-8859-3" => Charset::Iso_8859_3,
"ISO-8859-4" => Charset::Iso_8859_4,
"ISO-8859-5" => Charset::Iso_8859_5,
"ISO-8859-6" => Charset::Iso_8859_6,
"ISO-8859-7" => Charset::Iso_8859_7,
"ISO-8859-8" => Charset::Iso_8859_8,
"ISO-8859-9" => Charset::Iso_8859_9,
"ISO-8859-10" => Charset::Iso_8859_10,
"SHIFT-JIS" => Charset::Shift_Jis,
"EUC-JP" => Charset::Euc_Jp,
"ISO-2022-KR" => Charset::Iso_2022_Kr,
"EUC-KR" => Charset::Euc_Kr,
"ISO-2022-JP" => Charset::Iso_2022_Jp,
"ISO-2022-JP-2" => Charset::Iso_2022_Jp_2,
"ISO-8859-6-E" => Charset::Iso_8859_6_E,
"ISO-8859-6-I" => Charset::Iso_8859_6_I,
"ISO-8859-8-E" => Charset::Iso_8859_8_E,
"ISO-8859-8-I" => Charset::Iso_8859_8_I,
"GB2312" => Charset::Gb2312,
"BIG5" => Charset::Big5,
"KOI8-R" => Charset::Koi8_R,
s => Charset::Ext(s.to_owned()),
})
}
}
pub const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'%')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'-')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'{')
.add(b'}');