use std::fmt::Write as _;
#[must_use]
pub fn unicode_encode(payload: &str) -> String {
let mut out = String::with_capacity(payload.len() * 6);
for ch in payload.chars() {
let _ = write!(&mut out, "\\u{:04X}", ch as u32);
}
out
}
#[must_use]
pub fn iis_unicode_encode(payload: &str) -> String {
let mut out = String::with_capacity(payload.len() * 6);
for ch in payload.chars() {
let _ = write!(&mut out, "%u{:04X}", ch as u32);
}
out
}
#[must_use]
pub fn json_string_encode(payload: &str) -> String {
let mut out = String::with_capacity(payload.len() * 2 + 2);
out.push('"');
for ch in payload.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\u{0008}' => out.push_str("\\b"),
'\u{000C}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(&mut out, "\\u{:04X}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
#[must_use]
pub fn html_entity_encode(payload: &str) -> String {
let mut out = String::with_capacity(payload.len() * 6);
for ch in payload.chars() {
let _ = write!(&mut out, "&#x{:X};", ch as u32);
}
out
}
#[must_use]
pub fn html_entity_decimal_encode(payload: &str) -> String {
let mut out = String::with_capacity(payload.len() * 6);
for ch in payload.chars() {
let _ = write!(&mut out, "&#{};", ch as u32);
}
out
}
#[must_use]
pub fn fullwidth_encode(payload: &str) -> String {
let mut out = String::with_capacity(payload.len() * 3);
for ch in payload.chars() {
let mapped = match ch {
' ' => '\u{3000}', c if ('\x21'..='\x7e').contains(&c) => {
char::from_u32(c as u32 + 0xFEE0).unwrap_or(c)
}
c => c,
};
out.push(mapped);
}
out
}
#[must_use]
pub fn homoglyph_encode(payload: &str) -> String {
let mut out = String::with_capacity(payload.len() * 4);
for ch in payload.chars() {
let mapped = match ch {
'\'' => '\u{2019}', '"' => '\u{201D}', '<' => '\u{FF1C}', '>' => '\u{FF1E}', '=' => '\u{FF1D}', '(' => '\u{FF08}', ')' => '\u{FF09}', ';' => '\u{FF1B}', '-' => '\u{2010}', '/' => '\u{2215}', c => c,
};
out.push(mapped);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unicode_encode_basic() {
assert_eq!(unicode_encode("A"), "\\u0041");
assert_eq!(unicode_encode("AB"), "\\u0041\\u0042");
}
#[test]
fn unicode_encode_special_chars() {
let encoded = unicode_encode("' OR 1=1--");
assert!(encoded.contains("\\u0027")); assert!(encoded.contains("\\u003D")); }
#[test]
fn unicode_encode_unicode() {
let encoded = unicode_encode("日本語");
assert_eq!(encoded, "\\u65E5\\u672C\\u8A9E");
}
#[test]
fn iis_unicode_encode_basic() {
assert_eq!(iis_unicode_encode("A"), "%u0041");
assert_eq!(iis_unicode_encode("AB"), "%u0041%u0042");
}
#[test]
fn json_encode_basic() {
assert_eq!(json_string_encode("A"), "\"A\"");
assert_eq!(json_string_encode("A\\B"), "\"A\\\\B\"");
assert_eq!(json_string_encode("A\"B"), "\"A\\\"B\"");
assert_eq!(json_string_encode("A\nB"), "\"A\\nB\"");
}
#[test]
fn json_encode_control_chars() {
assert_eq!(json_string_encode("\x01"), "\"\\u0001\"");
}
#[test]
fn html_entity_encode_basic() {
assert_eq!(html_entity_encode("A"), "A");
assert_eq!(html_entity_encode("AB"), "AB");
}
#[test]
fn html_entity_encode_special_chars() {
let encoded = html_entity_encode("<script>");
assert_eq!(encoded, "<script>");
}
#[test]
fn html_entity_decimal_encode_basic() {
assert_eq!(html_entity_decimal_encode("A"), "A");
assert_eq!(html_entity_decimal_encode("<"), "<");
}
#[test]
fn html_entity_encode_empty() {
assert_eq!(html_entity_encode(""), "");
}
#[test]
fn unicode_encode_empty() {
assert_eq!(unicode_encode(""), "");
}
#[test]
fn fullwidth_encode_sql_keywords() {
let encoded = fullwidth_encode("SELECT");
assert_eq!(encoded, "SELECT");
for ch in encoded.chars() {
assert!(
ch as u32 >= 0xFF01,
"expected fullwidth char, got {ch} (U+{:04X})",
ch as u32
);
}
}
#[test]
fn fullwidth_encode_spaces() {
let encoded = fullwidth_encode("A B");
assert!(
encoded.contains('\u{3000}'),
"space should become ideographic space"
);
}
#[test]
fn fullwidth_encode_preserves_non_ascii() {
let encoded = fullwidth_encode("日本語");
assert_eq!(encoded, "日本語", "non-ASCII should pass through unchanged");
}
#[test]
fn fullwidth_encode_operators() {
let encoded = fullwidth_encode("1=1");
assert_eq!(encoded, "1=1");
}
#[test]
fn fullwidth_encode_sqli_payload() {
let encoded = fullwidth_encode("' OR 1=1--");
assert!(!encoded.contains("OR"), "should not contain ASCII 'OR'");
assert!(encoded.contains("OR"), "should contain fullwidth 'OR'");
}
#[test]
fn fullwidth_encode_empty() {
assert_eq!(fullwidth_encode(""), "");
}
#[test]
fn homoglyph_replaces_quotes() {
let encoded = homoglyph_encode("' OR '1'='1");
assert!(
!encoded.contains('\''),
"ASCII single quote should be replaced"
);
assert!(
encoded.contains('\u{2019}'),
"should contain RIGHT SINGLE QUOTATION MARK"
);
}
#[test]
fn homoglyph_replaces_angle_brackets() {
let encoded = homoglyph_encode("<script>");
assert!(!encoded.contains('<'), "ASCII < should be replaced");
assert!(!encoded.contains('>'), "ASCII > should be replaced");
assert!(encoded.contains('\u{FF1C}'), "should contain fullwidth <");
assert!(encoded.contains('\u{FF1E}'), "should contain fullwidth >");
}
#[test]
fn homoglyph_replaces_equals() {
let encoded = homoglyph_encode("1=1");
assert!(!encoded.contains('='), "ASCII = should be replaced");
assert!(encoded.contains('\u{FF1D}'), "should contain fullwidth =");
}
#[test]
fn homoglyph_preserves_letters() {
let encoded = homoglyph_encode("SELECT");
assert_eq!(encoded, "SELECT", "letters should be preserved");
}
#[test]
fn homoglyph_encode_empty() {
assert_eq!(homoglyph_encode(""), "");
}
#[test]
fn homoglyph_replaces_parens() {
let encoded = homoglyph_encode("fn()");
assert!(encoded.contains('\u{FF08}'), "should contain fullwidth (");
assert!(encoded.contains('\u{FF09}'), "should contain fullwidth )");
}
}