use std::collections::HashMap;
mod builtins;
mod config;
pub use builtins::*;
pub use config::{StrategyConfig, TamperConfig};
pub trait TamperStrategy: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn tamper(&self, payload: &str, context: Option<&str>) -> String;
fn tamper_with_params(
&self,
payload: &str,
context: Option<&str>,
_params: &HashMap<String, toml::Value>,
) -> String {
self.tamper(payload, context)
}
fn aggressiveness(&self) -> f64;
}
#[derive(Default)]
pub struct TamperRegistry {
strategies: HashMap<String, Box<dyn TamperStrategy>>,
}
const DEFAULT_NAMES: &[&str] = &[
"url_encode",
"double_url_encode",
"unicode_escape",
"html_entity",
"case_alternation",
"random_case",
"whitespace_insertion",
"sql_comment",
"null_byte",
"overlong_utf8",
"base64",
"hex_encode",
];
impl TamperRegistry {
#[must_use]
pub fn new() -> Self {
Self {
strategies: HashMap::new(),
}
}
#[must_use]
pub fn with_defaults() -> Self {
let mut registry = Self::new();
for name in DEFAULT_NAMES {
match *name {
"url_encode" => registry.register(Box::new(UrlEncodeTamper)),
"double_url_encode" => registry.register(Box::new(DoubleUrlEncodeTamper)),
"unicode_escape" => registry.register(Box::new(UnicodeEscapeTamper)),
"html_entity" => registry.register(Box::new(HtmlEntityTamper)),
"case_alternation" => registry.register(Box::new(CaseAlternationTamper)),
"random_case" => registry.register(Box::new(RandomCaseTamper)),
"whitespace_insertion" => registry.register(Box::new(WhitespaceInsertionTamper)),
"sql_comment" => registry.register(Box::new(SqlCommentTamper)),
"null_byte" => registry.register(Box::new(NullByteTamper)),
"overlong_utf8" => registry.register(Box::new(OverlongUtf8Tamper)),
"base64" => registry.register(Box::new(Base64Tamper)),
"hex_encode" => registry.register(Box::new(HexEncodeTamper)),
_ => {}
}
}
registry
}
pub fn register(&mut self, strategy: Box<dyn TamperStrategy>) {
self.strategies
.insert(strategy.name().to_string(), strategy);
}
pub fn unregister(&mut self, name: &str) -> Option<Box<dyn TamperStrategy>> {
self.strategies.remove(name)
}
pub fn clear(&mut self) {
self.strategies.clear();
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&dyn TamperStrategy> {
self.strategies.get(name).map(|s| s.as_ref())
}
#[must_use]
pub fn names(&self) -> Vec<&str> {
self.strategies.keys().map(|s| s.as_str()).collect()
}
#[must_use]
pub fn by_aggressiveness(&self) -> Vec<&dyn TamperStrategy> {
let mut strategies: Vec<&dyn TamperStrategy> =
self.strategies.values().map(|s| s.as_ref()).collect();
strategies.sort_by(|a, b| {
let a_score = if a.aggressiveness().is_nan() {
1.0
} else {
a.aggressiveness()
};
let b_score = if b.aggressiveness().is_nan() {
1.0
} else {
b.aggressiveness()
};
a_score
.partial_cmp(&b_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
strategies
}
pub fn tamper_with(
&self,
name: &str,
payload: &str,
context: Option<&str>,
) -> Result<String, TamperError> {
self.get(name)
.map(|s| s.tamper(payload, context))
.ok_or_else(|| TamperError::StrategyNotFound(name.to_string()))
}
pub fn tamper_with_params(
&self,
name: &str,
payload: &str,
context: Option<&str>,
params: &HashMap<String, toml::Value>,
) -> Result<String, TamperError> {
self.get(name)
.map(|s| s.tamper_with_params(payload, context, params))
.ok_or_else(|| TamperError::StrategyNotFound(name.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TamperError {
#[error("Strategy not found: {0}")]
StrategyNotFound(String),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Failed to load strategies: {0}")]
LoadError(String),
}
#[must_use]
pub fn default_registry() -> TamperRegistry {
TamperRegistry::with_defaults()
}
pub fn tamper(strategy: &str, payload: &str, context: Option<&str>) -> Result<String, TamperError> {
let registry = default_registry();
registry.tamper_with(strategy, payload, context)
}
#[must_use]
pub fn all_tamper_names() -> &'static [&'static str] {
DEFAULT_NAMES
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_with_defaults_has_strategies() {
let registry = TamperRegistry::with_defaults();
assert!(!registry.names().is_empty());
assert!(registry.get("url_encode").is_some());
assert!(registry.get("base64").is_some());
}
#[test]
fn registry_lookup_fails_for_unknown() {
let registry = TamperRegistry::with_defaults();
assert!(registry.get("unknown_strategy").is_none());
}
#[test]
fn tamper_with_error_for_unknown() {
let registry = TamperRegistry::with_defaults();
let result = registry.tamper_with("unknown", "payload", None);
assert!(matches!(result, Err(TamperError::StrategyNotFound(_))));
}
#[test]
fn aggressiveness_sorting() {
let registry = TamperRegistry::with_defaults();
let strategies = registry.by_aggressiveness();
for i in 1..strategies.len() {
assert!(
strategies[i - 1].aggressiveness() <= strategies[i].aggressiveness(),
"Strategies should be sorted by aggressiveness"
);
}
}
#[test]
fn unregister_removes_strategy() {
let mut registry = TamperRegistry::with_defaults();
assert!(registry.get("url_encode").is_some());
let removed = registry.unregister("url_encode");
assert!(removed.is_some());
assert!(registry.get("url_encode").is_none());
}
#[test]
fn clear_removes_all() {
let mut registry = TamperRegistry::with_defaults();
registry.clear();
assert!(registry.names().is_empty());
}
#[test]
fn nan_aggressiveness_treated_as_one() {
struct NaNStrategy;
impl TamperStrategy for NaNStrategy {
fn name(&self) -> &'static str {
"nan_test"
}
fn description(&self) -> &'static str {
"test"
}
fn tamper(&self, _p: &str, _c: Option<&str>) -> String {
"test".to_string()
}
fn aggressiveness(&self) -> f64 {
f64::NAN
}
}
let mut registry = TamperRegistry::new();
registry.register(Box::new(NaNStrategy));
let sorted = registry.by_aggressiveness();
assert_eq!(sorted.len(), 1);
}
#[test]
fn all_tamper_names_static() {
let names = all_tamper_names();
assert!(!names.is_empty());
assert!(names.contains(&"url_encode"));
}
#[test]
fn tamper_error_display() {
let err = TamperError::StrategyNotFound("test".to_string());
assert_eq!(format!("{err}"), "Strategy not found: test");
}
#[test]
fn default_registry_function() {
let registry = default_registry();
assert!(!registry.names().is_empty());
}
#[test]
fn convenience_tamper_function() {
let result = tamper("url_encode", "test!", None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "test%21");
}
#[test]
fn convenience_tamper_function_error() {
let result = tamper("unknown", "test", None);
assert!(result.is_err());
}
}