use serde::{Deserialize, Serialize};
use std::sync::Arc;
pub trait Encoder {
fn encode(&self, input: &str) -> String;
}
impl<F> Encoder for F
where
F: Fn(&str) -> String,
{
fn encode(&self, input: &str) -> String {
self(input)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
#[non_exhaustive]
pub enum EncodingError {
#[error("unknown encoding transform '{transform}'. Fix: use a known built-in or register a custom encoding.")]
UnknownTransform {
transform: String,
},
}
#[derive(Clone)]
pub struct CustomEncoder {
func: Arc<dyn Fn(&str) -> String + Send + Sync>,
}
impl CustomEncoder {
pub fn new<F>(func: F) -> Self
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
Self {
func: Arc::new(func),
}
}
pub fn encode(&self, input: &str) -> String {
(self.func)(input)
}
}
impl std::fmt::Debug for CustomEncoder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomEncoder").finish_non_exhaustive()
}
}
impl Default for CustomEncoder {
fn default() -> Self {
Self::new(std::string::ToString::to_string)
}
}
impl Encoder for CustomEncoder {
fn encode(&self, input: &str) -> String {
self.encode(input)
}
}
impl std::fmt::Display for CustomEncoder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("CustomEncoder(..)")
}
}
pub fn apply_encoding(s: &str, transform: &str) -> Result<String, EncodingError> {
let encoding: BuiltinEncoding = transform.parse()?;
Ok(encoding.apply(s))
}
fn percent_hex_encode(s: &str) -> String {
s.bytes()
.fold(String::with_capacity(s.len() * 3), |mut acc, b| {
use std::fmt::Write;
let _ = write!(&mut acc, "%{b:02x}");
acc
})
}
fn unicode_escape(s: &str) -> String {
s.chars()
.fold(String::with_capacity(s.len() * 6), |mut acc, c| {
use std::fmt::Write;
let u = c as u32;
if u > 0xFFFF {
let code = u - 0x1_0000;
let high = 0xD800 + (code >> 10);
let low = 0xDC00 + (code & 0x3FF);
let _ = write!(&mut acc, "\\u{:04x}\\u{:04x}", high, low);
} else {
let _ = write!(&mut acc, "\\u{:04x}", u);
}
acc
})
}
fn octal_escape(s: &str) -> String {
s.bytes()
.fold(String::with_capacity(s.len() * 4), |mut acc, b| {
use std::fmt::Write;
let _ = write!(&mut acc, "\\{b:03o}");
acc
})
}
fn js_charcode(s: &str) -> String {
let codes: Vec<String> = s.chars().map(|c| (c as u32).to_string()).collect();
format!("String.fromCodePoint({})", codes.join(","))
}
fn js_concat_split(s: &str) -> String {
let parts: Vec<String> = s
.chars()
.map(|c| match c {
'\'' => "'\\''".to_string(),
'\\' => "'\\\\'".to_string(),
'\n' => "'\\n'".to_string(),
'\r' => "'\\r'".to_string(),
'\t' => "'\\t'".to_string(),
other => format!("'{other}'"),
})
.collect();
parts.join("+")
}
pub(crate) fn alternate_case(s: &str, offset: usize) -> String {
let mut out = String::with_capacity(s.len());
for (i, c) in s.chars().enumerate() {
if (i + offset) % 2 == 0 {
out.extend(c.to_lowercase());
} else {
out.extend(c.to_uppercase());
}
}
out
}
fn join_chars_with(s: &str, separator: &str) -> String {
let char_count = s.chars().count();
let mut out = String::with_capacity(s.len() + separator.len() * char_count.saturating_sub(1));
let mut chars = s.chars();
if let Some(first) = chars.next() {
out.push(first);
for c in chars {
out.push_str(separator);
out.push(c);
}
}
out
}
fn php_chr_concat(s: &str) -> String {
let parts: Vec<String> = s.bytes().map(|b| format!("chr({b})")).collect();
parts.join(".")
}
fn python_chr_join(s: &str) -> String {
let parts: Vec<String> = s.chars().map(|c| format!("chr({})", c as u32)).collect();
format!("\"\".join([{}])", parts.join(","))
}
fn sql_char_concat(s: &str) -> String {
let parts: Vec<String> = s.bytes().map(|b| format!("CHAR({b})")).collect();
format!("CONCAT({})", parts.join(","))
}
fn rot13_encode(s: &str) -> String {
s.chars()
.map(|c| match c {
'a'..='m' | 'A'..='M' => (c as u8 + 13) as char,
'n'..='z' | 'N'..='Z' => (c as u8 - 13) as char,
_ => c,
})
.collect()
}
fn css_escape(s: &str) -> String {
s.chars()
.fold(String::with_capacity(s.len() * 6), |mut acc, c| {
use std::fmt::Write;
let _ = write!(&mut acc, "\\{:02x}", c as u32);
acc
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinEncoding {
Identity,
UrlEncode,
DoubleUrl,
Hex,
Unicode,
HtmlEntities,
NullByte,
Base64,
Octal,
JsCharCode,
JsConcat,
CaseAlternate,
TabSplit,
NewlineSplit,
PhpChr,
PythonChr,
SqlChar,
CssEscape,
Rot13,
}
impl std::fmt::Display for BuiltinEncoding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = match self {
Self::Identity => "identity",
Self::UrlEncode => "url_encode",
Self::DoubleUrl => "double_url",
Self::Hex => "hex",
Self::Unicode => "unicode",
Self::HtmlEntities => "html_entities",
Self::NullByte => "null_byte",
Self::Base64 => "base64",
Self::Octal => "octal",
Self::JsCharCode => "js_charcode",
Self::JsConcat => "js_concat",
Self::CaseAlternate => "case_alternate",
Self::TabSplit => "tab_split",
Self::NewlineSplit => "newline_split",
Self::PhpChr => "php_chr",
Self::PythonChr => "python_chr",
Self::SqlChar => "sql_char",
Self::CssEscape => "css_escape",
Self::Rot13 => "rot13",
};
f.write_str(value)
}
}
impl std::str::FromStr for BuiltinEncoding {
type Err = EncodingError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
let variant = match name {
"identity" | "raw" => Self::Identity,
"url_encode" | "url" => Self::UrlEncode,
"double_url" => Self::DoubleUrl,
"hex" => Self::Hex,
"unicode" => Self::Unicode,
"html_entities" | "html" => Self::HtmlEntities,
"null_byte" => Self::NullByte,
"base64" => Self::Base64,
"octal" => Self::Octal,
"charcode" | "js_charcode" => Self::JsCharCode,
"concat_split" | "js_concat" => Self::JsConcat,
"case_alternate" => Self::CaseAlternate,
"tab_split" => Self::TabSplit,
"newline_split" => Self::NewlineSplit,
"php_chr" => Self::PhpChr,
"python_chr" => Self::PythonChr,
"sql_char" => Self::SqlChar,
"css_escape" => Self::CssEscape,
"rot13" => Self::Rot13,
other => {
return Err(EncodingError::UnknownTransform {
transform: other.to_string(),
})
}
};
Ok(variant)
}
}
impl BuiltinEncoding {
pub fn is_builtin(name: &str) -> bool {
name.parse::<Self>().is_ok()
}
fn apply(self, s: &str) -> String {
match self {
Self::Identity => s.to_string(),
Self::UrlEncode => urlencoding::encode(s).into_owned(),
Self::DoubleUrl => urlencoding::encode(&urlencoding::encode(s)).into_owned(),
Self::Hex => percent_hex_encode(s),
Self::Unicode => unicode_escape(s),
Self::HtmlEntities => html_encode(s),
Self::NullByte => format!("{s}%00"),
Self::Base64 => encodex::base64::encode(s.as_bytes()),
Self::Octal => octal_escape(s),
Self::JsCharCode => js_charcode(s),
Self::JsConcat => js_concat_split(s),
Self::CaseAlternate => alternate_case(s, 0),
Self::TabSplit => join_chars_with(s, "\t"),
Self::NewlineSplit => join_chars_with(s, "\n"),
Self::PhpChr => php_chr_concat(s),
Self::PythonChr => python_chr_join(s),
Self::SqlChar => sql_char_concat(s),
Self::CssEscape => css_escape(s),
Self::Rot13 => rot13_encode(s),
}
}
pub const ALL: &'static [&'static str] = &[
"identity",
"raw",
"url_encode",
"url",
"double_url",
"hex",
"unicode",
"html_entities",
"html",
"null_byte",
"base64",
"octal",
"charcode",
"js_charcode",
"concat_split",
"js_concat",
"case_alternate",
"tab_split",
"newline_split",
"php_chr",
"python_chr",
"sql_char",
"css_escape",
"rot13",
];
}
fn html_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len() * 2);
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
'`' => out.push_str("`"),
'/' => out.push_str("/"),
_ => out.push(c),
}
}
out
}