1use std::collections::HashMap;
7
8mod builtins;
9mod config;
10
11pub use builtins::*;
12pub use config::{StrategyConfig, TamperConfig};
13
14pub trait TamperStrategy: Send + Sync {
19 fn name(&self) -> &'static str;
21
22 fn description(&self) -> &'static str;
24
25 fn tamper(&self, payload: &str, context: Option<&str>) -> String;
31
32 fn tamper_with_params(
36 &self,
37 payload: &str,
38 context: Option<&str>,
39 _params: &HashMap<String, toml::Value>,
40 ) -> String {
41 self.tamper(payload, context)
42 }
43
44 fn aggressiveness(&self) -> f64;
46}
47
48#[derive(Default)]
50pub struct TamperRegistry {
51 strategies: HashMap<String, Box<dyn TamperStrategy>>,
52}
53
54const DEFAULT_NAMES: &[&str] = &[
56 "url_encode",
57 "double_url_encode",
58 "unicode_escape",
59 "html_entity",
60 "case_alternation",
61 "random_case",
62 "whitespace_insertion",
63 "sql_comment",
64 "null_byte",
65 "overlong_utf8",
66 "base64",
67 "hex_encode",
68];
69
70impl TamperRegistry {
71 #[must_use]
73 pub fn new() -> Self {
74 Self {
75 strategies: HashMap::new(),
76 }
77 }
78
79 #[must_use]
81 pub fn with_defaults() -> Self {
82 let mut registry = Self::new();
83 for name in DEFAULT_NAMES {
84 match *name {
85 "url_encode" => registry.register(Box::new(UrlEncodeTamper)),
86 "double_url_encode" => registry.register(Box::new(DoubleUrlEncodeTamper)),
87 "unicode_escape" => registry.register(Box::new(UnicodeEscapeTamper)),
88 "html_entity" => registry.register(Box::new(HtmlEntityTamper)),
89 "case_alternation" => registry.register(Box::new(CaseAlternationTamper)),
90 "random_case" => registry.register(Box::new(RandomCaseTamper)),
91 "whitespace_insertion" => registry.register(Box::new(WhitespaceInsertionTamper)),
92 "sql_comment" => registry.register(Box::new(SqlCommentTamper)),
93 "null_byte" => registry.register(Box::new(NullByteTamper)),
94 "overlong_utf8" => registry.register(Box::new(OverlongUtf8Tamper)),
95 "base64" => registry.register(Box::new(Base64Tamper)),
96 "hex_encode" => registry.register(Box::new(HexEncodeTamper)),
97 _ => {}
98 }
99 }
100 registry
101 }
102
103 pub fn register(&mut self, strategy: Box<dyn TamperStrategy>) {
105 self.strategies
106 .insert(strategy.name().to_string(), strategy);
107 }
108
109 pub fn unregister(&mut self, name: &str) -> Option<Box<dyn TamperStrategy>> {
111 self.strategies.remove(name)
112 }
113
114 pub fn clear(&mut self) {
116 self.strategies.clear();
117 }
118
119 #[must_use]
121 pub fn get(&self, name: &str) -> Option<&dyn TamperStrategy> {
122 self.strategies.get(name).map(|s| s.as_ref())
123 }
124
125 #[must_use]
127 pub fn names(&self) -> Vec<&str> {
128 self.strategies.keys().map(|s| s.as_str()).collect()
129 }
130
131 #[must_use]
133 pub fn by_aggressiveness(&self) -> Vec<&dyn TamperStrategy> {
134 let mut strategies: Vec<&dyn TamperStrategy> =
135 self.strategies.values().map(|s| s.as_ref()).collect();
136 strategies.sort_by(|a, b| {
137 let a_score = if a.aggressiveness().is_nan() {
138 1.0
139 } else {
140 a.aggressiveness()
141 };
142 let b_score = if b.aggressiveness().is_nan() {
143 1.0
144 } else {
145 b.aggressiveness()
146 };
147 a_score
148 .partial_cmp(&b_score)
149 .unwrap_or(std::cmp::Ordering::Equal)
150 });
151 strategies
152 }
153
154 pub fn tamper_with(
159 &self,
160 name: &str,
161 payload: &str,
162 context: Option<&str>,
163 ) -> Result<String, TamperError> {
164 self.get(name)
165 .map(|s| s.tamper(payload, context))
166 .ok_or_else(|| TamperError::StrategyNotFound(name.to_string()))
167 }
168
169 pub fn tamper_with_params(
174 &self,
175 name: &str,
176 payload: &str,
177 context: Option<&str>,
178 params: &HashMap<String, toml::Value>,
179 ) -> Result<String, TamperError> {
180 self.get(name)
181 .map(|s| s.tamper_with_params(payload, context, params))
182 .ok_or_else(|| TamperError::StrategyNotFound(name.to_string()))
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub enum TamperError {
189 StrategyNotFound(String),
191 InvalidConfig(String),
193 LoadError(String),
195}
196
197impl std::fmt::Display for TamperError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 Self::StrategyNotFound(name) => write!(f, "Strategy not found: {name}"),
201 Self::InvalidConfig(msg) => write!(f, "Invalid configuration: {msg}"),
202 Self::LoadError(msg) => write!(f, "Failed to load strategies: {msg}"),
203 }
204 }
205}
206
207impl std::error::Error for TamperError {}
208
209#[must_use]
211pub fn default_registry() -> TamperRegistry {
212 TamperRegistry::with_defaults()
213}
214
215pub fn tamper(strategy: &str, payload: &str, context: Option<&str>) -> Result<String, TamperError> {
220 let registry = default_registry();
221 registry.tamper_with(strategy, payload, context)
222}
223
224#[must_use]
226pub fn all_tamper_names() -> &'static [&'static str] {
227 DEFAULT_NAMES
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn registry_with_defaults_has_strategies() {
236 let registry = TamperRegistry::with_defaults();
237 assert!(!registry.names().is_empty());
238 assert!(registry.get("url_encode").is_some());
239 assert!(registry.get("base64").is_some());
240 }
241
242 #[test]
243 fn registry_lookup_fails_for_unknown() {
244 let registry = TamperRegistry::with_defaults();
245 assert!(registry.get("unknown_strategy").is_none());
246 }
247
248 #[test]
249 fn tamper_with_error_for_unknown() {
250 let registry = TamperRegistry::with_defaults();
251 let result = registry.tamper_with("unknown", "payload", None);
252 assert!(matches!(result, Err(TamperError::StrategyNotFound(_))));
253 }
254
255 #[test]
256 fn aggressiveness_sorting() {
257 let registry = TamperRegistry::with_defaults();
258 let strategies = registry.by_aggressiveness();
259 for i in 1..strategies.len() {
260 assert!(
261 strategies[i - 1].aggressiveness() <= strategies[i].aggressiveness(),
262 "Strategies should be sorted by aggressiveness"
263 );
264 }
265 }
266
267 #[test]
268 fn unregister_removes_strategy() {
269 let mut registry = TamperRegistry::with_defaults();
270 assert!(registry.get("url_encode").is_some());
271 let removed = registry.unregister("url_encode");
272 assert!(removed.is_some());
273 assert!(registry.get("url_encode").is_none());
274 }
275
276 #[test]
277 fn clear_removes_all() {
278 let mut registry = TamperRegistry::with_defaults();
279 registry.clear();
280 assert!(registry.names().is_empty());
281 }
282
283 #[test]
284 fn nan_aggressiveness_treated_as_one() {
285 struct NaNStrategy;
286 impl TamperStrategy for NaNStrategy {
287 fn name(&self) -> &'static str {
288 "nan_test"
289 }
290 fn description(&self) -> &'static str {
291 "test"
292 }
293 fn tamper(&self, _p: &str, _c: Option<&str>) -> String {
294 "test".to_string()
295 }
296 fn aggressiveness(&self) -> f64 {
297 f64::NAN
298 }
299 }
300 let mut registry = TamperRegistry::new();
301 registry.register(Box::new(NaNStrategy));
302 let sorted = registry.by_aggressiveness();
303 assert_eq!(sorted.len(), 1);
304 }
305
306 #[test]
307 fn all_tamper_names_static() {
308 let names = all_tamper_names();
309 assert!(!names.is_empty());
310 assert!(names.contains(&"url_encode"));
311 }
312
313 #[test]
314 fn tamper_error_display() {
315 let err = TamperError::StrategyNotFound("test".to_string());
316 assert_eq!(format!("{err}"), "Strategy not found: test");
317 }
318
319 #[test]
320 fn default_registry_function() {
321 let registry = default_registry();
322 assert!(!registry.names().is_empty());
323 }
324
325 #[test]
326 fn convenience_tamper_function() {
327 let result = tamper("url_encode", "test!", None);
328 assert!(result.is_ok());
329 assert_eq!(result.unwrap(), "test%21");
330 }
331
332 #[test]
333 fn convenience_tamper_function_error() {
334 let result = tamper("unknown", "test", None);
335 assert!(result.is_err());
336 }
337}