1use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use std::fmt::Write;
9
10pub trait Encoder {
40 fn encode(&self, input: &str) -> String;
42}
43
44impl<F> Encoder for F
45where
46 F: Fn(&str) -> String,
47{
48 fn encode(&self, input: &str) -> String {
49 self(input)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
58#[non_exhaustive]
59pub enum EncodingError {
60 #[error("unknown encoding transform '{transform}'. Fix: use a known built-in or register a custom encoding.")]
62 UnknownTransform {
63 transform: String,
65 },
66}
67
68#[derive(Clone)]
86pub struct CustomEncoder {
87 func: Arc<dyn Fn(&str) -> String + Send + Sync>,
88}
89
90impl CustomEncoder {
91 pub fn new<F>(func: F) -> Self
101 where
102 F: Fn(&str) -> String + Send + Sync + 'static,
103 {
104 Self {
105 func: Arc::new(func),
106 }
107 }
108
109 pub fn encode(&self, input: &str) -> String {
119 (self.func)(input)
120 }
121}
122
123impl std::fmt::Debug for CustomEncoder {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.debug_struct("CustomEncoder").finish_non_exhaustive()
126 }
127}
128
129impl Default for CustomEncoder {
130 fn default() -> Self {
131 Self::new(std::string::ToString::to_string)
132 }
133}
134
135impl Encoder for CustomEncoder {
136 fn encode(&self, input: &str) -> String {
137 self.encode(input)
138 }
139}
140
141impl std::fmt::Display for CustomEncoder {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 f.write_str("CustomEncoder(..)")
144 }
145}
146
147pub fn apply_encoding(s: &str, transform: &str) -> Result<String, EncodingError> {
159 let encoding: BuiltinEncoding = transform.parse()?;
162 Ok(encoding.apply(s))
163}
164
165fn percent_hex_encode(s: &str) -> String {
166 s.bytes()
167 .fold(String::with_capacity(s.len() * 3), |mut acc, b| {
168 write!(&mut acc, "%{b:02x}").expect("writing to a String sink is infallible");
169 acc
170 })
171}
172
173fn unicode_escape(s: &str) -> String {
174 s.chars()
175 .fold(String::with_capacity(s.len() * 6), |mut acc, c| {
176 use std::fmt::Write;
177 let u = c as u32;
178 if u > 0xFFFF {
179 let code = u - 0x1_0000;
181 let high = 0xD800 + (code >> 10);
182 let low = 0xDC00 + (code & 0x3FF);
183 write!(&mut acc, "\\u{:04x}\\u{:04x}", high, low)
184 .expect("writing to a String sink is infallible");
185 } else {
186 write!(&mut acc, "\\u{:04x}", u)
187 .expect("writing to a String sink is infallible");
188 }
189 acc
190 })
191}
192
193fn octal_escape(s: &str) -> String {
194 s.bytes()
195 .fold(String::with_capacity(s.len() * 4), |mut acc, b| {
196 write!(&mut acc, "\\{b:03o}").expect("writing to a String sink is infallible");
197 acc
198 })
199}
200
201fn js_charcode(s: &str) -> String {
202 let codes: Vec<String> = s.chars().map(|c| (c as u32).to_string()).collect();
203 format!("String.fromCodePoint({})", codes.join(","))
204}
205
206fn js_concat_split(s: &str) -> String {
207 let parts: Vec<String> = s
212 .chars()
213 .map(|c| match c {
214 '\'' => "'\\''".to_string(),
215 '\\' => "'\\\\'".to_string(),
216 '\n' => "'\\n'".to_string(),
217 '\r' => "'\\r'".to_string(),
218 '\t' => "'\\t'".to_string(),
219 other => format!("'{other}'"),
220 })
221 .collect();
222 parts.join("+")
223}
224
225pub(crate) fn alternate_case(s: &str, offset: usize) -> String {
230 let mut out = String::with_capacity(s.len());
234 for (i, c) in s.chars().enumerate() {
235 if (i + offset) % 2 == 0 {
236 out.extend(c.to_lowercase());
237 } else {
238 out.extend(c.to_uppercase());
239 }
240 }
241 out
242}
243
244fn join_chars_with(s: &str, separator: &str) -> String {
245 let char_count = s.chars().count();
248 let mut out = String::with_capacity(s.len() + separator.len() * char_count.saturating_sub(1));
249 let mut chars = s.chars();
250 if let Some(first) = chars.next() {
251 out.push(first);
252 for c in chars {
253 out.push_str(separator);
254 out.push(c);
255 }
256 }
257 out
258}
259
260fn php_chr_concat(s: &str) -> String {
261 let parts: Vec<String> = s.bytes().map(|b| format!("chr({b})")).collect();
262 parts.join(".")
263}
264
265fn python_chr_join(s: &str) -> String {
266 let parts: Vec<String> = s.chars().map(|c| format!("chr({})", c as u32)).collect();
267 format!("\"\".join([{}])", parts.join(","))
268}
269
270fn sql_char_concat(s: &str) -> String {
271 let parts: Vec<String> = s.bytes().map(|b| format!("CHAR({b})")).collect();
272 format!("CONCAT({})", parts.join(","))
273}
274
275fn rot13_encode(s: &str) -> String {
276 s.chars()
277 .map(|c| match c {
278 'a'..='m' | 'A'..='M' => (c as u8 + 13) as char,
279 'n'..='z' | 'N'..='Z' => (c as u8 - 13) as char,
280 _ => c,
281 })
282 .collect()
283}
284
285fn css_escape(s: &str) -> String {
286 s.chars()
287 .fold(String::with_capacity(s.len() * 6), |mut acc, c| {
288 write!(&mut acc, "\\{:02x}", c as u32)
289 .expect("writing to a String sink is infallible");
290 acc
291 })
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
299#[non_exhaustive]
300pub enum BuiltinEncoding {
301 Identity,
303 UrlEncode,
305 DoubleUrl,
307 Hex,
309 Unicode,
311 HtmlEntities,
313 NullByte,
315 Base64,
317 Octal,
319 JsCharCode,
321 JsConcat,
323 CaseAlternate,
325 TabSplit,
327 NewlineSplit,
329 PhpChr,
331 PythonChr,
333 SqlChar,
335 CssEscape,
337 Rot13,
339}
340
341impl std::fmt::Display for BuiltinEncoding {
342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343 let value = match self {
344 Self::Identity => "identity",
345 Self::UrlEncode => "url_encode",
346 Self::DoubleUrl => "double_url",
347 Self::Hex => "hex",
348 Self::Unicode => "unicode",
349 Self::HtmlEntities => "html_entities",
350 Self::NullByte => "null_byte",
351 Self::Base64 => "base64",
352 Self::Octal => "octal",
353 Self::JsCharCode => "js_charcode",
354 Self::JsConcat => "js_concat",
355 Self::CaseAlternate => "case_alternate",
356 Self::TabSplit => "tab_split",
357 Self::NewlineSplit => "newline_split",
358 Self::PhpChr => "php_chr",
359 Self::PythonChr => "python_chr",
360 Self::SqlChar => "sql_char",
361 Self::CssEscape => "css_escape",
362 Self::Rot13 => "rot13",
363 };
364 f.write_str(value)
365 }
366}
367
368impl std::str::FromStr for BuiltinEncoding {
369 type Err = EncodingError;
370
371 fn from_str(name: &str) -> Result<Self, Self::Err> {
377 let variant = match name {
378 "identity" | "raw" => Self::Identity,
379 "url_encode" | "url" => Self::UrlEncode,
380 "double_url" => Self::DoubleUrl,
381 "hex" => Self::Hex,
382 "unicode" => Self::Unicode,
383 "html_entities" | "html" => Self::HtmlEntities,
384 "null_byte" => Self::NullByte,
385 "base64" => Self::Base64,
386 "octal" => Self::Octal,
387 "charcode" | "js_charcode" => Self::JsCharCode,
388 "concat_split" | "js_concat" => Self::JsConcat,
389 "case_alternate" => Self::CaseAlternate,
390 "tab_split" => Self::TabSplit,
391 "newline_split" => Self::NewlineSplit,
392 "php_chr" => Self::PhpChr,
393 "python_chr" => Self::PythonChr,
394 "sql_char" => Self::SqlChar,
395 "css_escape" => Self::CssEscape,
396 "rot13" => Self::Rot13,
397 other => {
398 return Err(EncodingError::UnknownTransform {
399 transform: other.to_string(),
400 })
401 }
402 };
403 Ok(variant)
404 }
405}
406
407impl BuiltinEncoding {
408 pub fn is_builtin(name: &str) -> bool {
410 name.parse::<Self>().is_ok()
411 }
412
413 fn apply(self, s: &str) -> String {
415 match self {
416 Self::Identity => s.to_string(),
417 Self::UrlEncode => urlencoding::encode(s).into_owned(),
418 Self::DoubleUrl => urlencoding::encode(&urlencoding::encode(s)).into_owned(),
419 Self::Hex => percent_hex_encode(s),
420 Self::Unicode => unicode_escape(s),
421 Self::HtmlEntities => html_encode(s),
422 Self::NullByte => format!("{s}%00"),
423 Self::Base64 => encodex::base64::encode(s.as_bytes()),
424 Self::Octal => octal_escape(s),
425 Self::JsCharCode => js_charcode(s),
426 Self::JsConcat => js_concat_split(s),
427 Self::CaseAlternate => alternate_case(s, 0),
428 Self::TabSplit => join_chars_with(s, "\t"),
429 Self::NewlineSplit => join_chars_with(s, "\n"),
430 Self::PhpChr => php_chr_concat(s),
431 Self::PythonChr => python_chr_join(s),
432 Self::SqlChar => sql_char_concat(s),
433 Self::CssEscape => css_escape(s),
434 Self::Rot13 => rot13_encode(s),
435 }
436 }
437
438 pub const ALL: &'static [&'static str] = &[
440 "identity",
441 "raw",
442 "url_encode",
443 "url",
444 "double_url",
445 "hex",
446 "unicode",
447 "html_entities",
448 "html",
449 "null_byte",
450 "base64",
451 "octal",
452 "charcode",
453 "js_charcode",
454 "concat_split",
455 "js_concat",
456 "case_alternate",
457 "tab_split",
458 "newline_split",
459 "php_chr",
460 "python_chr",
461 "sql_char",
462 "css_escape",
463 "rot13",
464 ];
465}
466
467fn html_encode(s: &str) -> String {
468 let mut out = String::with_capacity(s.len() * 2);
469 for c in s.chars() {
470 match c {
471 '&' => out.push_str("&"),
472 '<' => out.push_str("<"),
473 '>' => out.push_str(">"),
474 '"' => out.push_str("""),
475 '\'' => out.push_str("'"),
476 '`' => out.push_str("`"),
477 '/' => out.push_str("/"),
478 _ => out.push(c),
479 }
480 }
481 out
482}