1pub mod wumanber;
15use crate::WuManber;
16use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
17use regex::Regex;
18use std::sync::Arc;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum MatchAlgorithm {
35 AhoCorasick,
38 WuManber,
41 Regex,
44}
45
46impl std::fmt::Display for MatchAlgorithm {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::AhoCorasick => write!(f, "Aho-Corasick"),
50 Self::WuManber => write!(f, "Wu-Manber"),
51 Self::Regex => write!(f, "Regex"),
52 }
53 }
54}
55
56pub struct MultiPatternEngine {
58 algorithm: MatchAlgorithm, ac: Option<Arc<AhoCorasick>>, wm: Option<Arc<WuManber>>, regex_set: Option<Regex>, patterns: Vec<String>, }
64
65impl std::fmt::Debug for MultiPatternEngine {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("MultiPatternEngine")
68 .field("algorithm", &self.algorithm)
69 .field("pattern_count", &self.patterns.len())
70 .field("has_ac", &self.ac.is_some())
71 .field("has_wm", &self.wm.is_some())
72 .field("has_regex", &self.regex_set.is_some())
73 .finish()
74 }
75}
76
77impl Default for MultiPatternEngine {
78 fn default() -> Self {
79 Self { algorithm: MatchAlgorithm::AhoCorasick, ac: None, wm: None, regex_set: None, patterns: Vec::new() }
80 }
81}
82
83impl MultiPatternEngine {
84 pub fn new(algorithm: Option<MatchAlgorithm>, patterns: &[String]) -> Self {
98 let algorithm = algorithm.unwrap_or_else(|| Self::recommend_algorithm(patterns.len()));
99 let mut engine = Self { algorithm, ..Default::default() };
100
101 engine.rebuild(patterns);
102 engine
103 }
104
105 pub fn rebuild(&mut self, patterns: &[String]) {
107 self.patterns = patterns.to_vec();
108
109 let recommended = Self::recommend_algorithm(patterns.len());
111 if self.algorithm != recommended {
112 self.algorithm = recommended;
113 }
114
115 self.build_engines();
116 }
117
118 pub fn recommend_algorithm(word_count: usize) -> MatchAlgorithm {
124 match word_count {
125 0..=100 => MatchAlgorithm::WuManber,
126 101..=10_000 => MatchAlgorithm::AhoCorasick,
127 _ => MatchAlgorithm::Regex,
128 }
129 }
130
131 pub fn rebuild_with_algorithm(&mut self, patterns: &[String], algorithm: MatchAlgorithm) {
133 self.patterns = patterns.to_vec();
134 self.algorithm = algorithm;
135 self.build_engines();
136 }
137
138 fn build_engines(&mut self) {
140 self.ac = None;
142 self.wm = None;
143 self.regex_set = None;
144
145 match self.algorithm {
147 MatchAlgorithm::AhoCorasick => {
148 if !self.patterns.is_empty() {
149 match AhoCorasickBuilder::new()
150 .match_kind(aho_corasick::MatchKind::LeftmostLongest)
151 .build(&self.patterns)
152 {
153 Ok(ac) => self.ac = Some(Arc::new(ac)),
154 Err(_) => {
155 self.algorithm = MatchAlgorithm::WuManber;
157 self.wm = Some(Arc::new(WuManber::new_chinese(self.patterns.clone())));
158 }
159 }
160 }
161 }
162 MatchAlgorithm::WuManber => {
163 if !self.patterns.is_empty() {
164 self.wm = Some(Arc::new(WuManber::new_chinese(self.patterns.clone())));
165 }
166 }
167 MatchAlgorithm::Regex => {
168 if !self.patterns.is_empty() {
169 let escaped_patterns: Vec<String> = self.patterns.iter().map(|p| regex::escape(p)).collect();
170 let pattern = escaped_patterns.join("|");
171
172 match Regex::new(&pattern) {
173 Ok(regex) => self.regex_set = Some(regex),
174 Err(_) => {
175 self.algorithm = MatchAlgorithm::WuManber;
177 self.wm = Some(Arc::new(WuManber::new_chinese(self.patterns.clone())));
178 }
179 }
180 }
181 }
182 }
183 }
184
185 pub fn current_algorithm(&self) -> MatchAlgorithm {
187 self.algorithm
188 }
189
190 pub fn get_patterns(&self) -> &[String] {
192 &self.patterns
193 }
194
195 pub fn find_first(&self, text: &str) -> Option<String> {
208 match self.algorithm {
209 MatchAlgorithm::AhoCorasick => {
210 self.ac.as_ref()?.find(text).map(|mat| text[mat.start()..mat.end()].to_string())
211 }
212 MatchAlgorithm::WuManber => {
213 self.wm.as_ref()?.search_string(text)
215 }
216 MatchAlgorithm::Regex => self.regex_set.as_ref()?.find(text).map(|mat| mat.as_str().to_string()),
217 }
218 }
219
220 pub fn replace_all(&self, text: &str, replacement: &str) -> String {
222 match self.algorithm {
223 MatchAlgorithm::AhoCorasick => {
224 if let Some(ac) = &self.ac {
225 ac.replace_all(text, &[replacement]).to_string()
226 } else {
227 text.to_string()
228 }
229 }
230 MatchAlgorithm::WuManber => {
231 if let Some(wm) = &self.wm {
232 if replacement.is_empty() {
233 wm.remove_all(text)
234 } else {
235 let repl_char = replacement.chars().next().unwrap_or('*');
236 wm.replace_all(text, repl_char)
237 }
238 } else {
239 text.to_string()
240 }
241 }
242 MatchAlgorithm::Regex => {
243 if let Some(regex) = &self.regex_set {
244 regex.replace_all(text, replacement).to_string()
245 } else {
246 text.to_string()
247 }
248 }
249 }
250 }
251
252 pub fn find_all(&self, text: &str) -> Vec<String> {
265 match self.algorithm {
266 MatchAlgorithm::AhoCorasick => {
267 if let Some(ac) = &self.ac {
268 ac.find_iter(text).map(|mat| text[mat.start()..mat.end()].to_string()).collect()
269 } else {
270 Vec::new()
271 }
272 }
273 MatchAlgorithm::WuManber => {
274 if let Some(wm) = &self.wm {
275 wm.search_all_strings(text)
276 } else {
277 Vec::new()
278 }
279 }
280 MatchAlgorithm::Regex => {
281 if let Some(regex) = &self.regex_set {
282 regex.find_iter(text).map(|mat| mat.as_str().to_string()).collect()
283 } else {
284 Vec::new()
285 }
286 }
287 }
288 }
289
290 pub fn find_matches_with_positions(&self, text: &str) -> Vec<MatchInfo> {
292 match self.algorithm {
293 MatchAlgorithm::AhoCorasick => {
294 if let Some(ac) = &self.ac {
295 ac.find_iter(text)
296 .map(|mat| MatchInfo {
297 pattern: text[mat.start()..mat.end()].to_string(),
298 start: mat.start(),
299 end: mat.end(),
300 })
301 .collect()
302 } else {
303 Vec::new()
304 }
305 }
306 MatchAlgorithm::WuManber => {
307 if let Some(wm) = &self.wm {
308 wm.find_matches(text)
309 .into_iter()
310 .filter_map(|m| {
311 let pattern = text.get(m.start..m.end)?;
312 Some(MatchInfo { pattern: pattern.to_string(), start: m.start, end: m.end })
313 })
314 .collect()
315 } else {
316 Vec::new()
317 }
318 }
319 MatchAlgorithm::Regex => {
320 if let Some(regex) = &self.regex_set {
321 regex
322 .find_iter(text)
323 .map(|mat| MatchInfo { pattern: mat.as_str().to_string(), start: mat.start(), end: mat.end() })
324 .collect()
325 } else {
326 Vec::new()
327 }
328 }
329 }
330 }
331
332 pub fn contains_any(&self, text: &str) -> bool {
334 self.find_first(text).is_some()
335 }
336
337 pub fn stats(&self) -> EngineStats {
339 EngineStats {
340 algorithm: self.algorithm,
341 pattern_count: self.patterns.len(),
342 memory_usage: self.estimate_memory_usage(),
343 }
344 }
345
346 fn estimate_memory_usage(&self) -> usize {
348 let patterns_memory = self.patterns.iter().map(|p| p.len()).sum::<usize>();
349
350 let engine_memory = match self.algorithm {
351 MatchAlgorithm::WuManber => {
352 if let Some(wm) = &self.wm {
353 wm.memory_stats().total_memory
354 } else {
355 0
356 }
357 }
358 _ => patterns_memory * 2, };
360
361 patterns_memory + engine_memory
362 }
363}
364
365#[derive(Debug, Clone)]
367pub struct MatchInfo {
368 pub pattern: String,
369 pub start: usize,
370 pub end: usize,
371}
372
373#[derive(Debug, Clone)]
375pub struct EngineStats {
376 pub algorithm: MatchAlgorithm,
377 pub pattern_count: usize,
378 pub memory_usage: usize,
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 fn engine_with(patterns: &[&str]) -> MultiPatternEngine {
387 let owned: Vec<String> = patterns.iter().map(|s| s.to_string()).collect();
388 MultiPatternEngine::new(None, &owned)
389 }
390
391 #[test]
392 fn test_engine_find_first() {
393 let engine = engine_with(&["赌博", "色情"]);
394 assert_eq!(engine.find_first("含有赌博"), Some("赌博".to_string()));
395 assert_eq!(engine.find_first("正常"), None);
396 }
397
398 #[test]
399 fn test_engine_find_all() {
400 let engine = engine_with(&["赌博", "色情"]);
401 let results = engine.find_all("含有赌博和色情");
402 assert_eq!(results.len(), 2);
403 }
404
405 #[test]
406 fn test_engine_replace_all_wumanber() {
407 let engine = engine_with(&["赌博"]);
410 assert_eq!(engine.current_algorithm(), MatchAlgorithm::WuManber);
411 assert_eq!(engine.replace_all("含有赌博内容", "*"), "含有**内容");
412 }
413
414 #[test]
415 fn test_engine_replace_all_empty_is_removal() {
416 let engine = engine_with(&["赌博"]);
417 assert_eq!(engine.replace_all("含有赌博内容", ""), "含有内容");
418 }
419
420 #[test]
421 fn test_engine_contains_any() {
422 let engine = engine_with(&["赌博"]);
423 assert!(engine.contains_any("含有赌博"));
424 assert!(!engine.contains_any("正常"));
425 }
426
427 #[test]
428 fn test_engine_find_matches_with_positions() {
429 let mut engine = engine_with(&["赌博"]);
433 engine.rebuild_with_algorithm(&["赌博".to_string()], MatchAlgorithm::AhoCorasick);
434 let text = "含有赌博内容";
435 let matches = engine.find_matches_with_positions(text);
436 assert_eq!(matches.len(), 1);
437 assert_eq!(matches[0].pattern, "赌博");
438 assert_eq!(matches[0].start, 6); assert_eq!(matches[0].end, 12); assert_eq!(&text[matches[0].start..matches[0].end], "赌博");
441 }
442
443 #[test]
444 fn test_engine_find_matches_with_positions_regex() {
445 let mut engine = engine_with(&["赌博"]);
446 engine.rebuild_with_algorithm(&["赌博".to_string()], MatchAlgorithm::Regex);
447 let text = "含有赌博内容";
448 let matches = engine.find_matches_with_positions(text);
449 assert_eq!(matches.len(), 1);
450 assert_eq!(matches[0].pattern, "赌博");
451 assert_eq!(&text[matches[0].start..matches[0].end], "赌博");
452 }
453
454 #[test]
455 fn test_engine_stats() {
456 let engine = engine_with(&["赌博", "色情"]);
457 let stats = engine.stats();
458 assert_eq!(stats.pattern_count, 2);
459 assert_eq!(stats.algorithm, MatchAlgorithm::WuManber); }
461
462 #[test]
463 fn test_engine_empty() {
464 let engine = MultiPatternEngine::default();
465 assert!(engine.find_all("任何文本").is_empty());
466 assert_eq!(engine.find_first("任何文本"), None);
467 assert!(!engine.contains_any("任何文本"));
468 }
469
470 #[test]
471 fn test_engine_get_patterns() {
472 let engine = engine_with(&["赌博", "色情"]);
473 let patterns = engine.get_patterns();
474 assert_eq!(patterns.len(), 2);
475 assert!(patterns.contains(&"赌博".to_string()));
476 }
477
478 #[test]
479 fn test_engine_algorithm_recommendation() {
480 assert_eq!(MultiPatternEngine::recommend_algorithm(0), MatchAlgorithm::WuManber);
481 assert_eq!(MultiPatternEngine::recommend_algorithm(100), MatchAlgorithm::WuManber);
482 assert_eq!(MultiPatternEngine::recommend_algorithm(101), MatchAlgorithm::AhoCorasick);
483 assert_eq!(MultiPatternEngine::recommend_algorithm(10_000), MatchAlgorithm::AhoCorasick);
484 assert_eq!(MultiPatternEngine::recommend_algorithm(10_001), MatchAlgorithm::Regex);
485 }
486
487 #[test]
488 fn test_match_algorithm_display() {
489 assert_eq!(MatchAlgorithm::AhoCorasick.to_string(), "Aho-Corasick");
490 assert_eq!(MatchAlgorithm::WuManber.to_string(), "Wu-Manber");
491 assert_eq!(MatchAlgorithm::Regex.to_string(), "Regex");
492 }
493
494 #[test]
495 fn test_engine_force_algorithm_aho_corasick() {
496 let mut engine = engine_with(&["赌博"]);
497 engine.rebuild_with_algorithm(&["赌博".to_string()], MatchAlgorithm::AhoCorasick);
498 assert_eq!(engine.current_algorithm(), MatchAlgorithm::AhoCorasick);
499 assert_eq!(engine.find_first("含有赌博"), Some("赌博".to_string()));
500 assert_eq!(engine.replace_all("含有赌博内容", "*"), "含有*内容");
502 }
503
504 #[test]
505 fn test_engine_force_algorithm_regex() {
506 let mut engine = engine_with(&["赌博"]);
507 engine.rebuild_with_algorithm(&["赌博".to_string()], MatchAlgorithm::Regex);
508 assert_eq!(engine.current_algorithm(), MatchAlgorithm::Regex);
509 assert!(engine.contains_any("含有赌博"));
510 assert_eq!(engine.replace_all("含有赌博内容", "*"), "含有*内容");
512 }
513
514 #[test]
515 fn test_engine_force_algorithm_wumanber() {
516 let mut engine = MultiPatternEngine::default();
517 engine.rebuild_with_algorithm(&["赌博".to_string()], MatchAlgorithm::WuManber);
518 assert_eq!(engine.current_algorithm(), MatchAlgorithm::WuManber);
519 assert_eq!(engine.find_all("含有赌博").len(), 1);
520 }
521
522 #[test]
523 fn test_engine_find_matches_with_positions_wumanber() {
524 let engine = engine_with(&["赌博"]);
527 assert_eq!(engine.current_algorithm(), MatchAlgorithm::WuManber);
528 let text = "含有赌博内容";
529 let matches = engine.find_matches_with_positions(text);
530 assert_eq!(matches.len(), 1);
531 assert_eq!(matches[0].pattern, "赌博");
532 assert_eq!(&text[matches[0].start..matches[0].end], "赌博");
533 }
534}