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 "html_entity_variants",
61 "math_bold",
62 "json_unicode_alnum",
63 "sql_concat_split",
64 "sql_char_decompose",
65 "pg_chr_decompose",
66 "sql_adjacent_string_concat",
67 "case_alternation",
68 "random_case",
69 "whitespace_insertion",
70 "sql_comment",
71 "null_byte",
72 "overlong_utf8",
73 "base64",
74 "hex_encode",
75 "zero_width_inject",
77 "postgres_dollar_quote",
78 "mysql_versioned_comment_wrap",
79 "bracket_confusable",
80 "hex_literal_keyword",
81 "bell_separator",
82 "mxss_namespace_wrap",
83 "json_dup_key",
84 "ct_starvation",
85];
86
87impl TamperRegistry {
88 #[must_use]
90 pub fn new() -> Self {
91 Self {
92 strategies: HashMap::new(),
93 }
94 }
95
96 #[must_use]
98 pub fn with_defaults() -> Self {
99 let mut registry = Self::new();
100 for name in DEFAULT_NAMES {
101 match *name {
102 "url_encode" => registry.register(Box::new(UrlEncodeTamper)),
103 "double_url_encode" => registry.register(Box::new(DoubleUrlEncodeTamper)),
104 "unicode_escape" => registry.register(Box::new(UnicodeEscapeTamper)),
105 "html_entity" => registry.register(Box::new(HtmlEntityTamper)),
106 "html_entity_variants" => {
107 registry.register(Box::new(HtmlEntityVariantsTamper));
108 }
109 "math_bold" => registry.register(Box::new(MathBoldTamper)),
110 "json_unicode_alnum" => {
111 registry.register(Box::new(JsonUnicodeAlnumTamper));
112 }
113 "sql_concat_split" => registry.register(Box::new(SqlConcatSplitTamper)),
114 "sql_char_decompose" => {
115 registry.register(Box::new(SqlCharDecomposeTamper));
116 }
117 "pg_chr_decompose" => registry.register(Box::new(PgChrDecomposeTamper)),
118 "sql_adjacent_string_concat" => {
119 registry.register(Box::new(SqlAdjacentStringConcatTamper));
120 }
121 "case_alternation" => registry.register(Box::new(CaseAlternationTamper)),
122 "random_case" => registry.register(Box::new(RandomCaseTamper)),
123 "whitespace_insertion" => registry.register(Box::new(WhitespaceInsertionTamper)),
124 "sql_comment" => registry.register(Box::new(SqlCommentTamper)),
125 "null_byte" => registry.register(Box::new(NullByteTamper)),
126 "overlong_utf8" => registry.register(Box::new(OverlongUtf8Tamper)),
127 "base64" => registry.register(Box::new(Base64Tamper)),
128 "hex_encode" => registry.register(Box::new(HexEncodeTamper)),
129 "zero_width_inject" => registry.register(Box::new(ZeroWidthInjectTamper)),
130 "postgres_dollar_quote" => {
131 registry.register(Box::new(PostgresDollarQuoteTamper));
132 }
133 "mysql_versioned_comment_wrap" => {
134 registry.register(Box::new(MysqlVersionedCommentWrapTamper));
135 }
136 "bracket_confusable" => registry.register(Box::new(BracketConfusableTamper)),
137 "hex_literal_keyword" => registry.register(Box::new(HexLiteralKeywordTamper)),
138 "bell_separator" => registry.register(Box::new(BellSeparatorTamper)),
139 "mxss_namespace_wrap" => {
140 registry.register(Box::new(MxssNamespaceWrapTamper));
141 }
142 "json_dup_key" => registry.register(Box::new(JsonDupKeyTamper)),
143 "ct_starvation" => registry.register(Box::new(CtStarvationTamper)),
144 _ => {}
145 }
146 }
147 registry
148 }
149
150 pub fn register(&mut self, strategy: Box<dyn TamperStrategy>) {
152 self.strategies
153 .insert(strategy.name().to_string(), strategy);
154 }
155
156 pub fn unregister(&mut self, name: &str) -> Option<Box<dyn TamperStrategy>> {
158 self.strategies.remove(name)
159 }
160
161 pub fn clear(&mut self) {
163 self.strategies.clear();
164 }
165
166 #[must_use]
168 pub fn get(&self, name: &str) -> Option<&dyn TamperStrategy> {
169 self.strategies.get(name).map(std::convert::AsRef::as_ref)
170 }
171
172 #[must_use]
174 pub fn names(&self) -> Vec<&str> {
175 self.strategies
176 .keys()
177 .map(std::string::String::as_str)
178 .collect()
179 }
180
181 #[must_use]
183 pub fn by_aggressiveness(&self) -> Vec<&dyn TamperStrategy> {
184 let mut strategies: Vec<&dyn TamperStrategy> = self
185 .strategies
186 .values()
187 .map(std::convert::AsRef::as_ref)
188 .collect();
189 strategies.sort_by(|a, b| {
190 let a_score = if a.aggressiveness().is_nan() {
191 1.0
192 } else {
193 a.aggressiveness()
194 };
195 let b_score = if b.aggressiveness().is_nan() {
196 1.0
197 } else {
198 b.aggressiveness()
199 };
200 a_score
201 .partial_cmp(&b_score)
202 .unwrap_or(std::cmp::Ordering::Equal)
203 });
204 strategies
205 }
206
207 pub fn tamper_with(
212 &self,
213 name: &str,
214 payload: &str,
215 context: Option<&str>,
216 ) -> Result<String, TamperError> {
217 self.get(name)
218 .map(|s| s.tamper(payload, context))
219 .ok_or_else(|| TamperError::StrategyNotFound(name.to_string()))
220 }
221
222 pub fn tamper_with_params(
227 &self,
228 name: &str,
229 payload: &str,
230 context: Option<&str>,
231 params: &HashMap<String, toml::Value>,
232 ) -> Result<String, TamperError> {
233 self.get(name)
234 .map(|s| s.tamper_with_params(payload, context, params))
235 .ok_or_else(|| TamperError::StrategyNotFound(name.to_string()))
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
241pub enum TamperError {
242 #[error("Strategy not found: {0}")]
244 StrategyNotFound(String),
245 #[error("Invalid configuration: {0}")]
247 InvalidConfig(String),
248 #[error("Failed to load strategies: {0}")]
250 LoadError(String),
251}
252
253#[must_use]
255pub fn default_registry() -> TamperRegistry {
256 TamperRegistry::with_defaults()
257}
258
259pub fn tamper(strategy: &str, payload: &str, context: Option<&str>) -> Result<String, TamperError> {
264 let registry = default_registry();
265 registry.tamper_with(strategy, payload, context)
266}
267
268#[must_use]
270pub fn all_tamper_names() -> &'static [&'static str] {
271 DEFAULT_NAMES
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn registry_with_defaults_has_strategies() {
280 let registry = TamperRegistry::with_defaults();
281 assert!(!registry.names().is_empty());
282 assert!(registry.get("url_encode").is_some());
283 assert!(registry.get("base64").is_some());
284 }
285
286 #[test]
287 fn registry_lookup_fails_for_unknown() {
288 let registry = TamperRegistry::with_defaults();
289 assert!(registry.get("unknown_strategy").is_none());
290 }
291
292 #[test]
293 fn tamper_with_error_for_unknown() {
294 let registry = TamperRegistry::with_defaults();
295 let result = registry.tamper_with("unknown", "payload", None);
296 assert!(matches!(result, Err(TamperError::StrategyNotFound(_))));
297 }
298
299 #[test]
300 fn aggressiveness_sorting() {
301 let registry = TamperRegistry::with_defaults();
302 let strategies = registry.by_aggressiveness();
303 for i in 1..strategies.len() {
304 assert!(
305 strategies[i - 1].aggressiveness() <= strategies[i].aggressiveness(),
306 "Strategies should be sorted by aggressiveness"
307 );
308 }
309 }
310
311 #[test]
312 fn unregister_removes_strategy() {
313 let mut registry = TamperRegistry::with_defaults();
314 assert!(registry.get("url_encode").is_some());
315 let removed = registry.unregister("url_encode");
316 assert!(removed.is_some());
317 assert!(registry.get("url_encode").is_none());
318 }
319
320 #[test]
321 fn clear_removes_all() {
322 let mut registry = TamperRegistry::with_defaults();
323 registry.clear();
324 assert!(registry.names().is_empty());
325 }
326
327 #[test]
328 fn nan_aggressiveness_treated_as_one() {
329 struct NaNStrategy;
330 impl TamperStrategy for NaNStrategy {
331 fn name(&self) -> &'static str {
332 "nan_test"
333 }
334 fn description(&self) -> &'static str {
335 "test"
336 }
337 fn tamper(&self, _p: &str, _c: Option<&str>) -> String {
338 "test".to_string()
339 }
340 fn aggressiveness(&self) -> f64 {
341 f64::NAN
342 }
343 }
344 let mut registry = TamperRegistry::new();
345 registry.register(Box::new(NaNStrategy));
346 let sorted = registry.by_aggressiveness();
347 assert_eq!(sorted.len(), 1);
348 }
349
350 #[test]
351 fn all_tamper_names_static() {
352 let names = all_tamper_names();
353 assert!(!names.is_empty());
354 assert!(names.contains(&"url_encode"));
355 }
356
357 #[test]
358 fn tamper_error_display() {
359 let err = TamperError::StrategyNotFound("test".to_string());
360 assert_eq!(format!("{err}"), "Strategy not found: test");
361 }
362
363 #[test]
364 fn default_registry_function() {
365 let registry = default_registry();
366 assert!(!registry.names().is_empty());
367 }
368
369 #[test]
370 fn convenience_tamper_function() {
371 let result = tamper("url_encode", "test!", None);
372 assert!(result.is_ok());
373 assert_eq!(result.unwrap(), "test%21");
374 }
375
376 #[test]
377 fn convenience_tamper_function_error() {
378 let result = tamper("unknown", "test", None);
379 assert!(result.is_err());
380 }
381}