use wafrift_types::hash::{FNV_PRIME_64, fnv1a_64};
const SQL_BLANK_CHARS: &[char] = &['\t', '\n', '\r', '\x0b', '\x0c'];
pub fn whitespace_insert(payload: &str) -> String {
payload.replace(' ', "\t")
}
pub fn space_to_comment(payload: &str) -> String {
payload.replace(' ', "/**/")
}
pub fn space_to_dash(payload: &str) -> String {
payload.replace(' ', "--\n")
}
pub fn space_to_hash(payload: &str) -> String {
payload.replace(' ', "#\n")
}
pub fn space_to_plus(payload: &str) -> String {
payload.replace(' ', "+")
}
pub fn space_to_random_blank(payload: &str) -> String {
let seed: u64 = fnv1a_64(payload.as_bytes());
let mut out = String::with_capacity(payload.len());
for (i, c) in payload.chars().enumerate() {
if c == ' ' {
let pick = seed.wrapping_add(i as u64).wrapping_mul(FNV_PRIME_64);
out.push(SQL_BLANK_CHARS[(pick as usize) % SQL_BLANK_CHARS.len()]);
} else {
out.push(c);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whitespace_insert_replaces_spaces() {
assert_eq!(whitespace_insert("SELECT * FROM"), "SELECT\t*\tFROM");
}
#[test]
fn space_to_comment_replaces_spaces() {
assert_eq!(space_to_comment("SELECT * FROM"), "SELECT/**/*/**/FROM");
}
#[test]
fn space_to_dash_replaces_spaces() {
assert_eq!(space_to_dash("SELECT * FROM"), "SELECT--\n*--\nFROM");
}
#[test]
fn space_to_hash_replaces_spaces_with_terminated_line_comment() {
assert_eq!(
space_to_hash("SELECT * FROM"),
"SELECT#\n*#\nFROM",
"must end the # comment with a newline so * and FROM survive"
);
let out = space_to_hash("UNION SELECT 1 FROM users");
for kw in ["UNION", "SELECT", "1", "FROM", "users"] {
assert!(out.contains(kw), "{kw} dropped from payload: {out}");
}
}
#[test]
fn space_to_plus_replaces_spaces() {
assert_eq!(space_to_plus("SELECT * FROM"), "SELECT+*+FROM");
}
#[test]
fn space_to_random_blank_replaces_spaces() {
let result = space_to_random_blank("SELECT * FROM");
assert!(!result.contains(' '));
assert_eq!(result.len(), "SELECT * FROM".len());
}
#[test]
fn space_to_random_blank_is_deterministic() {
let a = space_to_random_blank("SELECT * FROM users");
let b = space_to_random_blank("SELECT * FROM users");
assert_eq!(a, b, "space_to_random_blank must be deterministic");
}
#[test]
fn space_to_random_blank_uses_more_than_one_blank() {
let mut seen = std::collections::HashSet::new();
for payload in [
"a b c d e f g h i j",
"x y z 1 2 3 4 5 6 7",
"UNION SELECT 1 2 3 4 5 FROM dual t1 t2",
"INSERT INTO t VALUES 1 2 3 4 5",
"WHERE id = 1 OR 1 = 1 AND 2 = 2",
] {
for c in space_to_random_blank(payload).chars() {
if SQL_BLANK_CHARS.contains(&c) {
seen.insert(c);
}
}
}
assert!(
seen.len() >= 3,
"space_to_random_blank should rotate through at least 3 of 5 blank chars across diverse payloads, saw {seen:?}"
);
}
}