1use crate::engine::MatchAlgorithm;
2use crate::{engine::MultiPatternEngine, variant::VariantDetector};
3use lru::LruCache;
4#[cfg(feature = "parallel")]
5use rayon::prelude::*;
6use regex::Regex;
7use std::collections::HashSet;
8use std::num::NonZero;
9use std::sync::{Arc, Mutex};
10use std::{
11 fs::File,
12 io::{self, BufRead, BufReader},
13 path::Path,
14};
15
16pub struct Filter {
18 engine: MultiPatternEngine, variant_detector: VariantDetector, noise: Regex, cache: Arc<Mutex<LruCache<String, Vec<String>>>>,
22 #[cfg(feature = "net")]
23 http_client: reqwest::blocking::Client, }
25
26impl std::fmt::Debug for Filter {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("Filter")
29 .field("engine", &self.engine)
30 .field("variant_detector", &self.variant_detector)
31 .field("noise", &self.noise)
32 .field("cache", &"<LruCache>")
33 .finish()
34 }
35}
36
37impl Filter {
38 pub fn new() -> Self {
40 Self {
41 engine: MultiPatternEngine::new(None, &[]),
42 variant_detector: VariantDetector::new(),
43 noise: Regex::new(r"[^\w\s\u4e00-\u9fff]").unwrap(),
44 cache: Arc::new(Mutex::new(LruCache::new(NonZero::new(1000).unwrap()))), #[cfg(feature = "net")]
46 http_client: reqwest::blocking::Client::builder()
47 .timeout(std::time::Duration::from_secs(5))
48 .build()
49 .unwrap(),
50 }
51 }
52
53 fn check_cache(&self, text: &str) -> Option<Vec<String>> {
54 self.cache.lock().unwrap_or_else(|e| e.into_inner()).get(text).cloned()
55 }
56
57 fn cache_result(&self, text: &str, results: &[String]) {
58 self.cache.lock().unwrap_or_else(|e| e.into_inner()).put(text.to_string(), results.to_vec());
59 }
60
61 fn word_match_variants(word: &str) -> Vec<String> {
62 let mut variants = vec![word.to_string()];
63 if word.chars().any(char::is_whitespace) {
64 let folded: String = word.chars().filter(|c| !c.is_whitespace()).collect();
65 if !folded.is_empty() && folded != word {
66 variants.push(folded);
67 }
68 }
69 variants
70 }
71
72 fn extend_patterns_with_word_variants(patterns: &mut Vec<String>, words: &[&str]) {
73 let mut seen: HashSet<String> = patterns.iter().cloned().collect();
74 for word in words {
75 for variant in Self::word_match_variants(word) {
76 if seen.insert(variant.clone()) {
77 patterns.push(variant);
78 }
79 }
80 }
81 }
82
83 pub fn clear_cache(&self) {
85 self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
86 }
87
88 pub fn with_algorithm(algorithm: MatchAlgorithm) -> Self {
90 Self { engine: MultiPatternEngine::new(Some(algorithm), &[]), ..Self::new() }
91 }
92
93 pub fn with_default_dict() -> io::Result<Self> {
95 let mut filter = Self::new();
96 filter.load_word_dict("dict/dict.txt")?;
97 Ok(filter)
98 }
99
100 pub fn update_noise_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
102 self.noise = Regex::new(pattern)?;
103 Ok(())
104 }
105
106 pub fn add_word(&mut self, word: &str) {
108 let mut patterns = self.engine.get_patterns().to_vec();
109 Self::extend_patterns_with_word_variants(&mut patterns, &[word]);
110 self.engine.rebuild(&patterns);
111 for variant in Self::word_match_variants(word) {
112 self.variant_detector.add_word(&variant);
113 }
114 self.clear_cache();
115 }
116
117 pub fn add_words(&mut self, words: &[&str]) {
119 let mut patterns = self.engine.get_patterns().to_vec();
120 Self::extend_patterns_with_word_variants(&mut patterns, words);
121
122 self.engine.rebuild(&patterns);
123 for word in words {
124 for variant in Self::word_match_variants(word) {
125 self.variant_detector.add_word(&variant);
126 }
127 }
128 self.clear_cache();
129 }
130
131 pub fn current_algorithm(&self) -> MatchAlgorithm {
133 self.engine.current_algorithm()
134 }
135
136 pub fn del_word(&mut self, word: &str) {
138 let word_set: HashSet<_> = Self::word_match_variants(word).into_iter().collect();
139 let patterns: Vec<_> = self.engine.get_patterns().iter().filter(|w| !word_set.contains(*w)).cloned().collect();
140
141 self.engine.rebuild(&patterns);
142 self.clear_cache();
143 }
144
145 pub fn del_words(&mut self, words: &[&str]) {
147 let word_set: HashSet<String> = words.iter().flat_map(|word| Self::word_match_variants(word)).collect();
148 let patterns: Vec<_> = self.engine.get_patterns().iter().filter(|w| !word_set.contains(*w)).cloned().collect();
149
150 self.engine.rebuild(&patterns);
151 self.clear_cache();
152 }
153
154 pub fn load_word_dict<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
156 let file = File::open(path)?;
157 self.load(BufReader::new(file))
158 }
159
160 pub fn load<R: BufRead>(&mut self, reader: R) -> io::Result<()> {
162 let words: Vec<_> = reader.lines().collect::<Result<_, _>>()?;
163 self.add_words(&words.iter().map(|s| s.as_str()).collect::<Vec<_>>());
164 Ok(())
165 }
166
167 #[cfg(feature = "net")]
169 pub fn load_net_word_dict(&mut self, url: &str) -> io::Result<()> {
170 let response = self.http_client.get(url).send().map_err(io::Error::other)?;
171
172 if !response.status().is_success() {
173 return Err(io::Error::other(format!("HTTP request failed: {}", response.status())));
174 }
175
176 let reader = BufReader::new(response);
177 self.load(reader)
178 }
179
180 pub fn find_in(&self, text: &str) -> (bool, String) {
182 let clean_text = self.remove_noise(text);
183
184 if let Some(word) = self.engine.find_first(&clean_text) {
186 return (true, word);
187 }
188
189 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
191
192 if let Some(word) = self.variant_detector.detect(&clean_text, &patterns).first() {
193 return (true, word.to_string());
194 }
195
196 (false, String::new())
197 }
198
199 pub fn replace(&self, text: &str, replacement: char) -> String {
201 let clean_text = self.remove_noise(text);
202 let repl = replacement.to_string();
203
204 let mut positions = self.engine.find_matches_with_positions(&clean_text);
208 positions.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
209 let mut result = String::with_capacity(clean_text.len());
210 let mut cursor = 0usize;
211 for m in &positions {
212 if m.start < cursor {
213 continue; }
215 result.push_str(&clean_text[cursor..m.start]);
216 result.push_str(&repl.repeat(clean_text[m.start..m.end].chars().count()));
217 cursor = m.end;
218 }
219 result.push_str(&clean_text[cursor..]);
220
221 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
225 let variants = self.variant_detector.detect(&result, &patterns);
226 for variant in variants {
227 let repl_str = repl.repeat(variant.chars().count());
228 result = result.replace(variant, &repl_str);
229 }
230
231 result
232 }
233
234 pub fn filter(&self, text: &str) -> String {
236 let clean_text = self.remove_noise(text);
237
238 let mut result = self.engine.replace_all(&clean_text, "");
240
241 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
243 let variants = self.variant_detector.detect(&result, &patterns);
244
245 for variant in variants {
246 result = result.replace(variant, "");
247 }
248
249 result
250 }
251
252 pub fn validate(&self, text: &str) -> (bool, String) {
254 self.find_in(text)
255 }
256
257 pub fn remove_noise(&self, text: &str) -> String {
259 self.noise.replace_all(text, "").to_string()
260 }
261
262 pub fn get_noise_pattern(&self) -> &Regex {
264 &self.noise
265 }
266}
267
268impl Filter {
269 pub fn find_all(&self, text: &str) -> Vec<String> {
271 let clean_text = self.remove_noise(text);
272
273 if let Some(cached_result) = self.check_cache(&clean_text) {
275 return cached_result;
276 }
277
278 #[cfg(feature = "parallel")]
279 let results = if clean_text.len() > 1000 {
280 self.find_all_parallel(&clean_text) } else {
282 self.find_all_sequential(&clean_text) };
284 #[cfg(not(feature = "parallel"))]
285 let results = self.find_all_sequential(&clean_text);
286
287 self.cache_result(&clean_text, &results);
289
290 results
291 }
292
293 #[cfg(feature = "parallel")]
295 fn find_all_parallel(&self, text: &str) -> Vec<String> {
296 let chunk_size = std::cmp::max(text.len() / rayon::current_num_threads(), 100);
297 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
298
299 let max_pattern_len = patterns.iter().map(|p| p.chars().count()).max().unwrap_or(0);
301 let overlap = max_pattern_len.min(chunk_size);
302
303 let chars: Vec<char> = text.chars().collect();
305 let engine_results: Vec<String> = if chars.len() <= chunk_size {
306 self.engine.find_all(text)
307 } else {
308 let step = chunk_size;
309 chars
310 .windows(chunk_size + overlap)
311 .step_by(step)
312 .collect::<Vec<_>>()
313 .par_iter()
314 .flat_map(|window| {
315 let chunk_text: String = window.iter().collect();
316 self.engine.find_all(&chunk_text)
317 })
318 .collect()
319 };
320
321 let variant_results: Vec<String> = text
323 .split_whitespace()
324 .collect::<Vec<_>>()
325 .par_iter()
326 .map(|segment| self.variant_detector.detect(segment, &patterns))
327 .flatten()
328 .map(|s| s.to_string())
329 .collect();
330
331 let mut results = engine_results;
333 results.extend(variant_results);
334 self.deduplicate_and_sort(results)
335 }
336
337 fn find_all_sequential(&self, text: &str) -> Vec<String> {
339 let mut results = self.engine.find_all(text);
340 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
341
342 results.extend(self.variant_detector.detect(text, &patterns).into_iter().map(|s| s.to_string()));
344
345 self.deduplicate_and_sort(results)
346 }
347
348 fn deduplicate_and_sort(&self, mut results: Vec<String>) -> Vec<String> {
350 results.sort_unstable();
351 results.dedup();
352 results
353 }
354
355 pub fn find_all_batch(&self, texts: &[&str]) -> Vec<Vec<String>> {
357 #[cfg(feature = "parallel")]
358 {
359 texts.par_iter().map(|text| self.find_all(text)).collect()
360 }
361 #[cfg(not(feature = "parallel"))]
362 {
363 texts.iter().map(|text| self.find_all(text)).collect()
364 }
365 }
366
367 pub fn find_all_layered(&self, text: &str) -> Vec<String> {
369 let clean_text = self.remove_noise(text);
370 let mut results = Vec::new();
371 let mut remaining_text = clean_text.clone();
372
373 let mut sorted_patterns = self.engine.get_patterns().to_vec();
375 sorted_patterns.sort_by_key(|b| std::cmp::Reverse(b.len()));
376
377 for pattern in &sorted_patterns {
379 if remaining_text.contains(pattern) {
380 results.push(pattern.clone());
381 remaining_text = remaining_text.replace(pattern, " ");
383 }
384 }
385
386 let patterns: Vec<_> = sorted_patterns.iter().map(|s| s.as_str()).collect();
388 results.extend(self.variant_detector.detect(&remaining_text, &patterns).into_iter().map(|s| s.to_string()));
389
390 self.deduplicate_and_sort(results)
391 }
392
393 pub fn find_all_streaming<R: BufRead>(&self, reader: R) -> io::Result<Vec<String>> {
395 let mut all_results = Vec::new();
396
397 for line in reader.lines() {
398 let line = line?;
399 let results = self.find_all(&line);
400 all_results.extend(results);
401 }
402
403 Ok(self.deduplicate_and_sort(all_results))
404 }
405}
406
407impl Default for Filter {
408 fn default() -> Self {
409 Self::new()
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use std::io::Cursor;
417 #[test]
418 fn test_integration() {
419 let mut filter = Filter::new();
420 filter.add_words(&["赌博", "色情"]);
421
422 assert_eq!(filter.find_in("含有赌博"), (true, "赌博".to_string()));
424
425 assert_eq!(filter.find_in("含有 dubo"), (true, "赌博".to_string()));
427
428 assert_eq!(filter.replace("赌博 色情", '*'), "** **");
430
431 assert_eq!(filter.filter("赌博内容"), "内容");
433 }
434
435 #[test]
436 fn test_noise_handling() {
437 let mut filter = Filter::new();
438 filter.add_word("赌博");
439
440 assert_eq!(filter.remove_noise("赌 博"), "赌 博");
442
443 assert_eq!(filter.remove_noise("赌@#$博"), "赌博");
445 }
446
447 #[test]
448 fn test_replace_vs_filter() {
449 let mut filter = Filter::new();
450 filter.add_words(&["赌博", "色情"]);
451
452 let text = "这里有赌博和色情内容";
453
454 assert_eq!(filter.replace(text, '*'), "这里有**和**内容");
456
457 assert_eq!(filter.filter(text), "这里有和内容");
459 }
460
461 #[test]
462 fn test_replace_single_pass_rebuild() {
463 let mut filter = Filter::new();
466 filter.add_words(&["赌博", "色情"]);
467 assert_eq!(filter.replace("前缀赌博中间色情后缀", '*'), "前缀**中间**后缀");
468 }
469
470 #[test]
471 fn test_variant_detection() {
472 let mut filter = Filter::new();
473 filter.add_word("测试");
474
475 assert_eq!(filter.find_in("ceshi"), (true, "测试".to_string()));
476 }
477
478 #[test]
479 fn test_algorithm_switch_one() {
480 let mut small = Filter::new();
482 small.add_words(&["a", "b", "c"]);
483 assert!(matches!(small.engine.current_algorithm(), MatchAlgorithm::WuManber));
484
485 let words: Vec<_> = (0..150).map(|i| format!("word{i}")).collect();
487 let mut medium = Filter::new();
488 medium.add_words(&words.iter().map(|s| s.as_str()).collect::<Vec<_>>());
489 println!("Medium current_algorithm: {:?}", medium.engine.current_algorithm());
490 assert!(matches!(medium.engine.current_algorithm(), MatchAlgorithm::AhoCorasick));
491 }
492
493 #[test]
494 fn test_io_operations() -> io::Result<()> {
495 let mut filter = Filter::new();
496 let cursor = Cursor::new("word1\nword2\nword3");
497 filter.load(cursor)?;
498
499 assert_eq!(filter.find_in("word2"), (true, "word2".to_string()));
500 Ok(())
501 }
502
503 #[test]
504 fn test_space_folded_word_matches_both_forms() {
505 let mut filter = Filter::new();
506 filter.add_word("A 级");
507
508 assert_eq!(filter.find_in("含有 A 级 内容"), (true, "A 级".to_string()));
509 assert_eq!(filter.find_in("含有 A级 内容"), (true, "A级".to_string()));
510
511 let results = filter.find_all("A 级 和 A级");
512 assert!(results.contains(&"A 级".to_string()));
513 assert!(results.contains(&"A级".to_string()));
514 }
515
516 #[test]
517 fn test_loaded_space_word_adds_folded_match() -> io::Result<()> {
518 let mut filter = Filter::new();
519 filter.load(Cursor::new("A 级\n3 级片"))?;
520
521 assert_eq!(filter.find_in("这里有 A级 内容"), (true, "A级".to_string()));
522 assert_eq!(filter.find_in("这里有 3级片 内容"), (true, "3级片".to_string()));
523 Ok(())
524 }
525
526 #[test]
527 fn test_delete_space_word_removes_both_forms() {
528 let mut filter = Filter::new();
529 filter.add_words(&["A 级", "B 级"]);
530
531 filter.del_word("A 级");
532
533 assert_eq!(filter.find_in("含有 A 级 内容"), (false, String::new()));
534 assert_eq!(filter.find_in("含有 A级 内容"), (false, String::new()));
535 assert_eq!(filter.find_in("含有 B级 内容"), (true, "B级".to_string()));
536 }
537
538 #[test]
539 fn test_algorithm_recommendation() {
540 assert_eq!(MultiPatternEngine::recommend_algorithm(50), MatchAlgorithm::WuManber);
541 assert_eq!(MultiPatternEngine::recommend_algorithm(150), MatchAlgorithm::AhoCorasick);
542 assert_eq!(MultiPatternEngine::recommend_algorithm(15000), MatchAlgorithm::Regex);
543 }
544
545 #[test]
546 fn test_algorithm_switch() {
547 let mut small = Filter::new();
549 small.add_words(&["a", "b", "c"]);
550 println!("Small (3 words): {:?}", small.current_algorithm());
551 assert!(matches!(small.current_algorithm(), MatchAlgorithm::WuManber));
552
553 let words: Vec<_> = (0..150).map(|i| format!("word{i}")).collect();
555 let word_refs: Vec<&str> = words.iter().map(|s| s.as_str()).collect();
556
557 let mut medium = Filter::new();
558 medium.add_words(&word_refs);
559
560 println!("Medium (150 words): {:?}", medium.current_algorithm());
561 println!("Pattern count: {}", medium.engine.get_patterns().len());
562
563 let recommended = MultiPatternEngine::recommend_algorithm(150);
565 println!("Recommended algorithm for 150 words: {recommended:?}");
566
567 assert!(matches!(medium.current_algorithm(), MatchAlgorithm::AhoCorasick));
568 }
569
570 #[test]
571 fn test_cache_invalidation_on_add_word() {
572 let mut filter = Filter::new();
573 filter.add_word("赌博");
574
575 let results1 = filter.find_all("这里有赌博");
577 assert!(results1.contains(&"赌博".to_string()));
578
579 filter.add_word("色情");
581
582 let results2 = filter.find_all("这里有赌博和色情");
584 assert!(results2.contains(&"赌博".to_string()));
585 assert!(results2.contains(&"色情".to_string()));
586 }
587
588 #[test]
589 fn test_cache_invalidation_on_del_word() {
590 let mut filter = Filter::new();
591 filter.add_words(&["赌博", "色情"]);
592
593 let results1 = filter.find_all("这里有赌博和色情");
595 assert!(results1.contains(&"赌博".to_string()));
596 assert!(results1.contains(&"色情".to_string()));
597
598 filter.del_word("赌博");
600
601 let results2 = filter.find_all("这里有赌博和色情");
603 assert!(!results2.contains(&"赌博".to_string()));
604 assert!(results2.contains(&"色情".to_string()));
605 }
606
607 #[test]
608 fn test_mutex_poison_recovery() {
609 use std::sync::Arc;
610
611 let filter = Arc::new(Filter::new());
612 let filter_clone = Arc::clone(&filter);
613
614 let handle = std::thread::spawn(move || {
616 let _guard = filter_clone.cache.lock().unwrap();
617 panic!("intentional panic to poison mutex");
618 });
619 let _ = handle.join();
620
621 let results = filter.find_all("test");
623 assert!(results.is_empty());
624 }
625
626 #[test]
627 fn test_parallel_search_cross_boundary() {
628 let mut filter = Filter::new();
629 filter.add_word("赌博");
630
631 let prefix: String = "安全文字".repeat(200); let text = format!("{prefix}这里有赌博内容");
635
636 let results = filter.find_all(&text);
637 assert!(results.contains(&"赌博".to_string()));
638 }
639
640 #[test]
641 fn test_parallel_search_no_duplicates() {
642 let mut filter = Filter::new();
643 filter.add_word("赌博");
644
645 let prefix: String = "安全".repeat(300); let text = format!("{prefix}赌博{prefix}");
648
649 let results = filter.find_all(&text);
650 let count = results.iter().filter(|w| *w == "赌博").count();
651 assert_eq!(count, 1, "expected exactly 1 match, got {count}");
652 }
653
654 #[test]
657 fn test_find_all_batch() {
658 let mut filter = Filter::new();
659 filter.add_words(&["赌博", "色情"]);
660
661 let texts = vec!["含有赌博", "含有色情", "正常内容"];
662 let results = filter.find_all_batch(&texts);
663
664 assert_eq!(results.len(), 3);
665 assert!(results[0].contains(&"赌博".to_string()));
666 assert!(results[1].contains(&"色情".to_string()));
667 assert!(results[2].is_empty());
668 }
669
670 #[test]
671 fn test_find_all_batch_empty() {
672 let filter = Filter::new();
673 let results = filter.find_all_batch(&[]);
674 assert!(results.is_empty());
675 }
676
677 #[test]
678 fn test_find_all_layered_prefers_longest() {
679 let mut filter = Filter::new();
680 filter.add_words(&["赌", "赌博", "赌博机"]);
681
682 let results = filter.find_all_layered("这里有赌博机");
684 assert!(results.contains(&"赌博机".to_string()));
685 assert!(!results.contains(&"赌".to_string()));
686 assert!(!results.contains(&"赌博".to_string()));
687 }
688
689 #[test]
690 fn test_find_all_streaming_multiline() {
691 let mut filter = Filter::new();
692 filter.add_words(&["赌博", "色情"]);
693
694 let input = "第一行含有赌博\n第二行含有色情\n第三行正常";
695 let cursor = std::io::Cursor::new(input);
696 let results = filter.find_all_streaming(cursor).unwrap();
697
698 assert!(results.contains(&"赌博".to_string()));
699 assert!(results.contains(&"色情".to_string()));
700 assert_eq!(results.len(), 2);
701 }
702
703 #[test]
706 fn test_cache_hit_returns_consistent_results() {
707 let mut filter = Filter::new();
708 filter.add_words(&["赌博", "色情"]);
709
710 let r1 = filter.find_all("含有赌博和色情内容");
712 let r2 = filter.find_all("含有赌博和色情内容");
713 assert_eq!(r1, r2);
714 }
715
716 #[test]
717 fn test_cache_clear() {
718 let mut filter = Filter::new();
719 filter.add_word("赌博");
720
721 filter.find_all("含有赌博"); filter.clear_cache();
723
724 let results = filter.find_all("含有赌博");
726 assert!(results.contains(&"赌博".to_string()));
727 }
728
729 #[test]
732 fn test_empty_text() {
733 let mut filter = Filter::new();
734 filter.add_word("赌博");
735
736 assert_eq!(filter.find_in(""), (false, String::new()));
737 assert!(filter.find_all("").is_empty());
738 assert_eq!(filter.replace("", '*'), "");
739 assert_eq!(filter.filter(""), "");
740 }
741
742 #[test]
743 fn test_empty_dictionary() {
744 let filter = Filter::new();
745
746 assert_eq!(filter.find_in("任何文本"), (false, String::new()));
747 assert!(filter.find_all("任何文本").is_empty());
748 assert_eq!(filter.replace("任何文本", '*'), "任何文本");
749 assert_eq!(filter.filter("任何文本"), "任何文本");
750 }
751
752 #[test]
753 fn test_unicode_emoji_does_not_interfere() {
754 let mut filter = Filter::new();
755 filter.add_word("赌博");
756
757 let (found, word) = filter.find_in("🎉 赌博 🎰");
759 assert!(found);
760 assert_eq!(word, "赌博");
761 }
762
763 #[test]
764 fn test_very_long_text() {
765 let mut filter = Filter::new();
766 filter.add_word("赌博");
767
768 let long_text = "正常".repeat(100_000) + "赌博";
770 let results = filter.find_all(&long_text);
771 assert!(results.contains(&"赌博".to_string()));
772 }
773
774 #[test]
775 fn test_cjk_extension_b_chars() {
776 let mut filter = Filter::new();
777 filter.add_word("赌博");
778
779 let text = "含有赌博内容 𠀀𠀁";
781 let (found, _) = filter.find_in(text);
782 assert!(found);
783 }
784}