wafrift_encoding/tamper/
mod.rs1use 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(std::convert::AsRef::as_ref)
123 }
124
125 #[must_use]
127 pub fn names(&self) -> Vec<&str> {
128 self.strategies.keys().map(std::string::String::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(std::convert::AsRef::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, thiserror::Error)]
188pub enum TamperError {
189 #[error("Strategy not found: {0}")]
191 StrategyNotFound(String),
192 #[error("Invalid configuration: {0}")]
194 InvalidConfig(String),
195 #[error("Failed to load strategies: {0}")]
197 LoadError(String),
198}
199
200#[must_use]
202pub fn default_registry() -> TamperRegistry {
203 TamperRegistry::with_defaults()
204}
205
206pub fn tamper(strategy: &str, payload: &str, context: Option<&str>) -> Result<String, TamperError> {
211 let registry = default_registry();
212 registry.tamper_with(strategy, payload, context)
213}
214
215#[must_use]
217pub fn all_tamper_names() -> &'static [&'static str] {
218 DEFAULT_NAMES
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn registry_with_defaults_has_strategies() {
227 let registry = TamperRegistry::with_defaults();
228 assert!(!registry.names().is_empty());
229 assert!(registry.get("url_encode").is_some());
230 assert!(registry.get("base64").is_some());
231 }
232
233 #[test]
234 fn registry_lookup_fails_for_unknown() {
235 let registry = TamperRegistry::with_defaults();
236 assert!(registry.get("unknown_strategy").is_none());
237 }
238
239 #[test]
240 fn tamper_with_error_for_unknown() {
241 let registry = TamperRegistry::with_defaults();
242 let result = registry.tamper_with("unknown", "payload", None);
243 assert!(matches!(result, Err(TamperError::StrategyNotFound(_))));
244 }
245
246 #[test]
247 fn aggressiveness_sorting() {
248 let registry = TamperRegistry::with_defaults();
249 let strategies = registry.by_aggressiveness();
250 for i in 1..strategies.len() {
251 assert!(
252 strategies[i - 1].aggressiveness() <= strategies[i].aggressiveness(),
253 "Strategies should be sorted by aggressiveness"
254 );
255 }
256 }
257
258 #[test]
259 fn unregister_removes_strategy() {
260 let mut registry = TamperRegistry::with_defaults();
261 assert!(registry.get("url_encode").is_some());
262 let removed = registry.unregister("url_encode");
263 assert!(removed.is_some());
264 assert!(registry.get("url_encode").is_none());
265 }
266
267 #[test]
268 fn clear_removes_all() {
269 let mut registry = TamperRegistry::with_defaults();
270 registry.clear();
271 assert!(registry.names().is_empty());
272 }
273
274 #[test]
275 fn nan_aggressiveness_treated_as_one() {
276 struct NaNStrategy;
277 impl TamperStrategy for NaNStrategy {
278 fn name(&self) -> &'static str {
279 "nan_test"
280 }
281 fn description(&self) -> &'static str {
282 "test"
283 }
284 fn tamper(&self, _p: &str, _c: Option<&str>) -> String {
285 "test".to_string()
286 }
287 fn aggressiveness(&self) -> f64 {
288 f64::NAN
289 }
290 }
291 let mut registry = TamperRegistry::new();
292 registry.register(Box::new(NaNStrategy));
293 let sorted = registry.by_aggressiveness();
294 assert_eq!(sorted.len(), 1);
295 }
296
297 #[test]
298 fn all_tamper_names_static() {
299 let names = all_tamper_names();
300 assert!(!names.is_empty());
301 assert!(names.contains(&"url_encode"));
302 }
303
304 #[test]
305 fn tamper_error_display() {
306 let err = TamperError::StrategyNotFound("test".to_string());
307 assert_eq!(format!("{err}"), "Strategy not found: test");
308 }
309
310 #[test]
311 fn default_registry_function() {
312 let registry = default_registry();
313 assert!(!registry.names().is_empty());
314 }
315
316 #[test]
317 fn convenience_tamper_function() {
318 let result = tamper("url_encode", "test!", None);
319 assert!(result.is_ok());
320 assert_eq!(result.unwrap(), "test%21");
321 }
322
323 #[test]
324 fn convenience_tamper_function_error() {
325 let result = tamper("unknown", "test", None);
326 assert!(result.is_err());
327 }
328}