1use crate::engine::MatchAlgorithm;
11use crate::{engine::MultiPatternEngine, variant::VariantDetector};
12use lru::LruCache;
13#[cfg(feature = "parallel")]
14use rayon::prelude::*;
15use regex::Regex;
16use std::collections::HashSet;
17use std::num::NonZero;
18use std::sync::{Arc, Mutex};
19use std::{
20 fs::File,
21 io::{self, BufRead, BufReader},
22 path::Path,
23};
24
25pub struct Filter {
27 engine: MultiPatternEngine, variant_detector: VariantDetector, noise: Regex, cache: Arc<Mutex<LruCache<String, Vec<String>>>>,
31 #[cfg(feature = "net")]
32 http_client: reqwest::blocking::Client, }
34
35#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct Match {
41 pub word: String,
43 pub is_variant: bool,
45}
46
47impl std::fmt::Debug for Filter {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("Filter")
50 .field("engine", &self.engine)
51 .field("variant_detector", &self.variant_detector)
52 .field("noise", &self.noise)
53 .field("cache", &"<LruCache>")
54 .finish()
55 }
56}
57
58impl Filter {
59 pub fn new() -> Self {
71 Self {
72 engine: MultiPatternEngine::new(None, &[]),
73 variant_detector: VariantDetector::new(),
74 noise: Regex::new(r"[^\w\s\u4e00-\u9fff]").unwrap(),
75 cache: Arc::new(Mutex::new(LruCache::new(NonZero::new(1000).unwrap()))), #[cfg(feature = "net")]
77 http_client: reqwest::blocking::Client::builder()
78 .timeout(std::time::Duration::from_secs(5))
79 .build()
80 .unwrap(),
81 }
82 }
83
84 fn check_cache(&self, text: &str) -> Option<Vec<String>> {
85 self.cache.lock().unwrap_or_else(|e| e.into_inner()).get(text).cloned()
86 }
87
88 fn cache_result(&self, text: &str, results: &[String]) {
89 self.cache.lock().unwrap_or_else(|e| e.into_inner()).put(text.to_string(), results.to_vec());
90 }
91
92 fn word_match_variants(word: &str) -> Vec<String> {
93 let mut variants = vec![word.to_string()];
94 if word.chars().any(char::is_whitespace) {
95 let folded: String = word.chars().filter(|c| !c.is_whitespace()).collect();
96 if !folded.is_empty() && folded != word {
97 variants.push(folded);
98 }
99 }
100 variants
101 }
102
103 fn extend_patterns_with_word_variants(patterns: &mut Vec<String>, words: &[&str]) {
104 let mut seen: HashSet<String> = patterns.iter().cloned().collect();
105 for word in words {
106 for variant in Self::word_match_variants(word) {
107 if seen.insert(variant.clone()) {
108 patterns.push(variant);
109 }
110 }
111 }
112 }
113
114 pub fn clear_cache(&self) {
116 self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
117 }
118
119 pub fn with_algorithm(algorithm: MatchAlgorithm) -> Self {
121 Self { engine: MultiPatternEngine::new(Some(algorithm), &[]), ..Self::new() }
122 }
123
124 pub fn with_default_dict() -> io::Result<Self> {
126 let mut filter = Self::new();
127 filter.load_word_dict("dict/dict.txt")?;
128 Ok(filter)
129 }
130
131 pub fn update_noise_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
149 self.noise = Regex::new(pattern)?;
150 Ok(())
151 }
152
153 pub fn add_word(&mut self, word: &str) {
155 let mut patterns = self.engine.get_patterns().to_vec();
156 Self::extend_patterns_with_word_variants(&mut patterns, &[word]);
157 self.engine.rebuild(&patterns);
158 for variant in Self::word_match_variants(word) {
159 self.variant_detector.add_word(&variant);
160 }
161 self.clear_cache();
162 }
163
164 pub fn add_words(&mut self, words: &[&str]) {
176 let mut patterns = self.engine.get_patterns().to_vec();
177 Self::extend_patterns_with_word_variants(&mut patterns, words);
178
179 self.engine.rebuild(&patterns);
180 for word in words {
181 for variant in Self::word_match_variants(word) {
182 self.variant_detector.add_word(&variant);
183 }
184 }
185 self.clear_cache();
186 }
187
188 #[must_use]
190 pub fn current_algorithm(&self) -> MatchAlgorithm {
191 self.engine.current_algorithm()
192 }
193
194 pub fn del_word(&mut self, word: &str) {
196 let word_set: HashSet<_> = Self::word_match_variants(word).into_iter().collect();
197 let patterns: Vec<_> = self.engine.get_patterns().iter().filter(|w| !word_set.contains(*w)).cloned().collect();
198
199 self.engine.rebuild(&patterns);
200 self.clear_cache();
201 }
202
203 pub fn del_words(&mut self, words: &[&str]) {
205 let word_set: HashSet<String> = words.iter().flat_map(|word| Self::word_match_variants(word)).collect();
206 let patterns: Vec<_> = self.engine.get_patterns().iter().filter(|w| !word_set.contains(*w)).cloned().collect();
207
208 self.engine.rebuild(&patterns);
209 self.clear_cache();
210 }
211
212 pub fn load_word_dict<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
214 let file = File::open(path)?;
215 self.load(BufReader::new(file))
216 }
217
218 pub fn load<R: BufRead>(&mut self, reader: R) -> io::Result<()> {
234 let words: Vec<_> = reader.lines().collect::<Result<_, _>>()?;
235 self.add_words(&words.iter().map(|s| s.as_str()).collect::<Vec<_>>());
236 Ok(())
237 }
238
239 #[cfg(feature = "net")]
241 pub fn load_net_word_dict(&mut self, url: &str) -> io::Result<()> {
242 let response = self.http_client.get(url).send().map_err(io::Error::other)?;
243
244 if !response.status().is_success() {
245 return Err(io::Error::other(format!("HTTP request failed: {}", response.status())));
246 }
247
248 let reader = BufReader::new(response);
249 self.load(reader)
250 }
251
252 #[must_use]
279 pub fn find_first_match(&self, text: &str) -> Option<Match> {
280 let clean_text = self.remove_noise(text);
281
282 if let Some(word) = self.engine.find_first(&clean_text) {
284 return Some(Match { word, is_variant: false });
285 }
286
287 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
289
290 if let Some(word) = self.variant_detector.detect(&clean_text, &patterns).first() {
291 return Some(Match { word: word.to_string(), is_variant: true });
292 }
293
294 None
295 }
296
297 #[must_use]
313 pub fn find_in(&self, text: &str) -> (bool, String) {
314 match self.find_first_match(text) {
315 Some(m) => (true, m.word),
316 None => (false, String::new()),
317 }
318 }
319
320 #[must_use]
334 pub fn replace(&self, text: &str, replacement: char) -> String {
335 let clean_text = self.remove_noise(text);
336 let repl = replacement.to_string();
337
338 let mut positions = self.engine.find_matches_with_positions(&clean_text);
342 positions.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
343 let mut result = String::with_capacity(clean_text.len());
344 let mut cursor = 0usize;
345 for m in &positions {
346 if m.start < cursor {
347 continue; }
349 result.push_str(&clean_text[cursor..m.start]);
350 result.push_str(&repl.repeat(clean_text[m.start..m.end].chars().count()));
351 cursor = m.end;
352 }
353 result.push_str(&clean_text[cursor..]);
354
355 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
359 let variants = self.variant_detector.detect(&result, &patterns);
360 for variant in variants {
361 let repl_str = repl.repeat(variant.chars().count());
362 result = result.replace(variant, &repl_str);
363 }
364
365 result
366 }
367
368 #[must_use]
380 pub fn filter(&self, text: &str) -> String {
381 let clean_text = self.remove_noise(text);
382
383 let mut result = self.engine.replace_all(&clean_text, "");
385
386 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
388 let variants = self.variant_detector.detect(&result, &patterns);
389
390 for variant in variants {
391 result = result.replace(variant, "");
392 }
393
394 result
395 }
396
397 #[must_use]
413 pub fn validate(&self, text: &str) -> (bool, String) {
414 self.find_in(text)
415 }
416
417 #[must_use]
419 pub fn remove_noise(&self, text: &str) -> String {
420 self.noise.replace_all(text, "").to_string()
421 }
422
423 #[must_use]
425 pub fn get_noise_pattern(&self) -> &Regex {
426 &self.noise
427 }
428}
429
430impl Filter {
431 #[must_use]
447 pub fn find_all(&self, text: &str) -> Vec<String> {
448 let clean_text = self.remove_noise(text);
449
450 if let Some(cached_result) = self.check_cache(&clean_text) {
452 return cached_result;
453 }
454
455 #[cfg(feature = "parallel")]
456 let results = if clean_text.len() > 1000 {
457 self.find_all_parallel(&clean_text) } else {
459 self.find_all_sequential(&clean_text) };
461 #[cfg(not(feature = "parallel"))]
462 let results = self.find_all_sequential(&clean_text);
463
464 self.cache_result(&clean_text, &results);
466
467 results
468 }
469
470 #[cfg(feature = "parallel")]
472 fn find_all_parallel(&self, text: &str) -> Vec<String> {
473 let chunk_size = std::cmp::max(text.len() / rayon::current_num_threads(), 100);
474 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
475
476 let max_pattern_len = patterns.iter().map(|p| p.chars().count()).max().unwrap_or(0);
478 let overlap = max_pattern_len.min(chunk_size);
479
480 let chars: Vec<char> = text.chars().collect();
482 let engine_results: Vec<String> = if chars.len() <= chunk_size {
483 self.engine.find_all(text)
484 } else {
485 let step = chunk_size;
486 chars
487 .windows(chunk_size + overlap)
488 .step_by(step)
489 .collect::<Vec<_>>()
490 .par_iter()
491 .flat_map(|window| {
492 let chunk_text: String = window.iter().collect();
493 self.engine.find_all(&chunk_text)
494 })
495 .collect()
496 };
497
498 let variant_results: Vec<String> = text
500 .split_whitespace()
501 .collect::<Vec<_>>()
502 .par_iter()
503 .map(|segment| self.variant_detector.detect(segment, &patterns))
504 .flatten()
505 .map(|s| s.to_string())
506 .collect();
507
508 let mut results = engine_results;
510 results.extend(variant_results);
511 self.deduplicate_and_sort(results)
512 }
513
514 fn find_all_sequential(&self, text: &str) -> Vec<String> {
516 let mut results = self.engine.find_all(text);
517 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
518
519 results.extend(self.variant_detector.detect(text, &patterns).into_iter().map(|s| s.to_string()));
521
522 self.deduplicate_and_sort(results)
523 }
524
525 fn deduplicate_and_sort(&self, mut results: Vec<String>) -> Vec<String> {
527 results.sort_unstable();
528 results.dedup();
529 results
530 }
531
532 #[must_use]
550 pub fn find_all_batch(&self, texts: &[&str]) -> Vec<Vec<String>> {
551 #[cfg(feature = "parallel")]
552 {
553 texts.par_iter().map(|text| self.find_all(text)).collect()
554 }
555 #[cfg(not(feature = "parallel"))]
556 {
557 texts.iter().map(|text| self.find_all(text)).collect()
558 }
559 }
560
561 #[must_use]
578 pub fn find_all_layered(&self, text: &str) -> Vec<String> {
579 let clean_text = self.remove_noise(text);
580 let mut results = Vec::new();
581 let mut remaining_text = clean_text.clone();
582
583 let mut sorted_patterns = self.engine.get_patterns().to_vec();
585 sorted_patterns.sort_by_key(|b| std::cmp::Reverse(b.len()));
586
587 for pattern in &sorted_patterns {
589 if remaining_text.contains(pattern) {
590 results.push(pattern.clone());
591 remaining_text = remaining_text.replace(pattern, " ");
593 }
594 }
595
596 let patterns: Vec<_> = sorted_patterns.iter().map(|s| s.as_str()).collect();
598 results.extend(self.variant_detector.detect(&remaining_text, &patterns).into_iter().map(|s| s.to_string()));
599
600 self.deduplicate_and_sort(results)
601 }
602
603 pub fn find_all_streaming<R: BufRead>(&self, reader: R) -> io::Result<Vec<String>> {
622 let mut all_results = Vec::new();
623
624 for line in reader.lines() {
625 let line = line?;
626 let results = self.find_all(&line);
627 all_results.extend(results);
628 }
629
630 Ok(self.deduplicate_and_sort(all_results))
631 }
632}
633
634impl Default for Filter {
635 fn default() -> Self {
636 Self::new()
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use std::io::Cursor;
644 #[test]
645 fn test_integration() {
646 let mut filter = Filter::new();
647 filter.add_words(&["赌博", "色情"]);
648
649 assert_eq!(filter.find_in("含有赌博"), (true, "赌博".to_string()));
651
652 assert_eq!(filter.find_in("含有 dubo"), (true, "赌博".to_string()));
654
655 assert_eq!(filter.replace("赌博 色情", '*'), "** **");
657
658 assert_eq!(filter.filter("赌博内容"), "内容");
660 }
661
662 #[test]
663 fn test_noise_handling() {
664 let mut filter = Filter::new();
665 filter.add_word("赌博");
666
667 assert_eq!(filter.remove_noise("赌 博"), "赌 博");
669
670 assert_eq!(filter.remove_noise("赌@#$博"), "赌博");
672 }
673
674 #[test]
675 fn test_replace_vs_filter() {
676 let mut filter = Filter::new();
677 filter.add_words(&["赌博", "色情"]);
678
679 let text = "这里有赌博和色情内容";
680
681 assert_eq!(filter.replace(text, '*'), "这里有**和**内容");
683
684 assert_eq!(filter.filter(text), "这里有和内容");
686 }
687
688 #[test]
689 fn test_replace_single_pass_rebuild() {
690 let mut filter = Filter::new();
693 filter.add_words(&["赌博", "色情"]);
694 assert_eq!(filter.replace("前缀赌博中间色情后缀", '*'), "前缀**中间**后缀");
695 }
696
697 #[test]
698 fn test_variant_detection() {
699 let mut filter = Filter::new();
700 filter.add_word("测试");
701
702 assert_eq!(filter.find_in("ceshi"), (true, "测试".to_string()));
703 }
704
705 #[test]
706 fn test_find_first_match_exact() {
707 let mut filter = Filter::new();
708 filter.add_words(&["赌博", "色情"]);
709
710 assert_eq!(filter.find_first_match("含有赌博"), Some(Match { word: "赌博".to_string(), is_variant: false }));
711 assert_eq!(filter.find_first_match("正常文本"), None);
712 }
713
714 #[test]
715 fn test_find_first_match_variant() {
716 let mut filter = Filter::new();
717 filter.add_word("赌博");
718
719 assert_eq!(filter.find_first_match("含有 dubo"), Some(Match { word: "赌博".to_string(), is_variant: true }));
721 }
722
723 #[test]
724 fn test_find_first_match_prefers_exact_over_variant() {
725 let mut filter = Filter::new();
726 filter.add_word("赌博");
727
728 assert_eq!(filter.find_first_match("赌博 dubo"), Some(Match { word: "赌博".to_string(), is_variant: false }));
730 }
731
732 #[test]
733 fn test_algorithm_switch_one() {
734 let mut small = Filter::new();
736 small.add_words(&["a", "b", "c"]);
737 assert!(matches!(small.engine.current_algorithm(), MatchAlgorithm::WuManber));
738
739 let words: Vec<_> = (0..150).map(|i| format!("word{i}")).collect();
741 let mut medium = Filter::new();
742 medium.add_words(&words.iter().map(|s| s.as_str()).collect::<Vec<_>>());
743 println!("Medium current_algorithm: {:?}", medium.engine.current_algorithm());
744 assert!(matches!(medium.engine.current_algorithm(), MatchAlgorithm::AhoCorasick));
745 }
746
747 #[test]
748 fn test_io_operations() -> io::Result<()> {
749 let mut filter = Filter::new();
750 let cursor = Cursor::new("word1\nword2\nword3");
751 filter.load(cursor)?;
752
753 assert_eq!(filter.find_in("word2"), (true, "word2".to_string()));
754 Ok(())
755 }
756
757 #[test]
758 fn test_space_folded_word_matches_both_forms() {
759 let mut filter = Filter::new();
760 filter.add_word("A 级");
761
762 assert_eq!(filter.find_in("含有 A 级 内容"), (true, "A 级".to_string()));
763 assert_eq!(filter.find_in("含有 A级 内容"), (true, "A级".to_string()));
764
765 let results = filter.find_all("A 级 和 A级");
766 assert!(results.contains(&"A 级".to_string()));
767 assert!(results.contains(&"A级".to_string()));
768 }
769
770 #[test]
771 fn test_loaded_space_word_adds_folded_match() -> io::Result<()> {
772 let mut filter = Filter::new();
773 filter.load(Cursor::new("A 级\n3 级片"))?;
774
775 assert_eq!(filter.find_in("这里有 A级 内容"), (true, "A级".to_string()));
776 assert_eq!(filter.find_in("这里有 3级片 内容"), (true, "3级片".to_string()));
777 Ok(())
778 }
779
780 #[test]
781 fn test_delete_space_word_removes_both_forms() {
782 let mut filter = Filter::new();
783 filter.add_words(&["A 级", "B 级"]);
784
785 filter.del_word("A 级");
786
787 assert_eq!(filter.find_in("含有 A 级 内容"), (false, String::new()));
788 assert_eq!(filter.find_in("含有 A级 内容"), (false, String::new()));
789 assert_eq!(filter.find_in("含有 B级 内容"), (true, "B级".to_string()));
790 }
791
792 #[test]
793 fn test_algorithm_recommendation() {
794 assert_eq!(MultiPatternEngine::recommend_algorithm(50), MatchAlgorithm::WuManber);
795 assert_eq!(MultiPatternEngine::recommend_algorithm(150), MatchAlgorithm::AhoCorasick);
796 assert_eq!(MultiPatternEngine::recommend_algorithm(15000), MatchAlgorithm::Regex);
797 }
798
799 #[test]
800 fn test_algorithm_switch() {
801 let mut small = Filter::new();
803 small.add_words(&["a", "b", "c"]);
804 println!("Small (3 words): {:?}", small.current_algorithm());
805 assert!(matches!(small.current_algorithm(), MatchAlgorithm::WuManber));
806
807 let words: Vec<_> = (0..150).map(|i| format!("word{i}")).collect();
809 let word_refs: Vec<&str> = words.iter().map(|s| s.as_str()).collect();
810
811 let mut medium = Filter::new();
812 medium.add_words(&word_refs);
813
814 println!("Medium (150 words): {:?}", medium.current_algorithm());
815 println!("Pattern count: {}", medium.engine.get_patterns().len());
816
817 let recommended = MultiPatternEngine::recommend_algorithm(150);
819 println!("Recommended algorithm for 150 words: {recommended:?}");
820
821 assert!(matches!(medium.current_algorithm(), MatchAlgorithm::AhoCorasick));
822 }
823
824 #[test]
825 fn test_cache_invalidation_on_add_word() {
826 let mut filter = Filter::new();
827 filter.add_word("赌博");
828
829 let results1 = filter.find_all("这里有赌博");
831 assert!(results1.contains(&"赌博".to_string()));
832
833 filter.add_word("色情");
835
836 let results2 = filter.find_all("这里有赌博和色情");
838 assert!(results2.contains(&"赌博".to_string()));
839 assert!(results2.contains(&"色情".to_string()));
840 }
841
842 #[test]
843 fn test_cache_invalidation_on_del_word() {
844 let mut filter = Filter::new();
845 filter.add_words(&["赌博", "色情"]);
846
847 let results1 = filter.find_all("这里有赌博和色情");
849 assert!(results1.contains(&"赌博".to_string()));
850 assert!(results1.contains(&"色情".to_string()));
851
852 filter.del_word("赌博");
854
855 let results2 = filter.find_all("这里有赌博和色情");
857 assert!(!results2.contains(&"赌博".to_string()));
858 assert!(results2.contains(&"色情".to_string()));
859 }
860
861 #[test]
862 fn test_mutex_poison_recovery() {
863 use std::sync::Arc;
864
865 let filter = Arc::new(Filter::new());
866 let filter_clone = Arc::clone(&filter);
867
868 let handle = std::thread::spawn(move || {
870 let _guard = filter_clone.cache.lock().unwrap();
871 panic!("intentional panic to poison mutex");
872 });
873 let _ = handle.join();
874
875 let results = filter.find_all("test");
877 assert!(results.is_empty());
878 }
879
880 #[test]
881 fn test_parallel_search_cross_boundary() {
882 let mut filter = Filter::new();
883 filter.add_word("赌博");
884
885 let prefix: String = "安全文字".repeat(200); let text = format!("{prefix}这里有赌博内容");
889
890 let results = filter.find_all(&text);
891 assert!(results.contains(&"赌博".to_string()));
892 }
893
894 #[test]
895 fn test_parallel_search_no_duplicates() {
896 let mut filter = Filter::new();
897 filter.add_word("赌博");
898
899 let prefix: String = "安全".repeat(300); let text = format!("{prefix}赌博{prefix}");
902
903 let results = filter.find_all(&text);
904 let count = results.iter().filter(|w| *w == "赌博").count();
905 assert_eq!(count, 1, "expected exactly 1 match, got {count}");
906 }
907
908 #[test]
911 fn test_find_all_batch() {
912 let mut filter = Filter::new();
913 filter.add_words(&["赌博", "色情"]);
914
915 let texts = vec!["含有赌博", "含有色情", "正常内容"];
916 let results = filter.find_all_batch(&texts);
917
918 assert_eq!(results.len(), 3);
919 assert!(results[0].contains(&"赌博".to_string()));
920 assert!(results[1].contains(&"色情".to_string()));
921 assert!(results[2].is_empty());
922 }
923
924 #[test]
925 fn test_find_all_batch_empty() {
926 let filter = Filter::new();
927 let results = filter.find_all_batch(&[]);
928 assert!(results.is_empty());
929 }
930
931 #[test]
932 fn test_find_all_layered_prefers_longest() {
933 let mut filter = Filter::new();
934 filter.add_words(&["赌", "赌博", "赌博机"]);
935
936 let results = filter.find_all_layered("这里有赌博机");
938 assert!(results.contains(&"赌博机".to_string()));
939 assert!(!results.contains(&"赌".to_string()));
940 assert!(!results.contains(&"赌博".to_string()));
941 }
942
943 #[test]
944 fn test_find_all_streaming_multiline() {
945 let mut filter = Filter::new();
946 filter.add_words(&["赌博", "色情"]);
947
948 let input = "第一行含有赌博\n第二行含有色情\n第三行正常";
949 let cursor = std::io::Cursor::new(input);
950 let results = filter.find_all_streaming(cursor).unwrap();
951
952 assert!(results.contains(&"赌博".to_string()));
953 assert!(results.contains(&"色情".to_string()));
954 assert_eq!(results.len(), 2);
955 }
956
957 #[test]
960 fn test_cache_hit_returns_consistent_results() {
961 let mut filter = Filter::new();
962 filter.add_words(&["赌博", "色情"]);
963
964 let r1 = filter.find_all("含有赌博和色情内容");
966 let r2 = filter.find_all("含有赌博和色情内容");
967 assert_eq!(r1, r2);
968 }
969
970 #[test]
971 fn test_cache_clear() {
972 let mut filter = Filter::new();
973 filter.add_word("赌博");
974
975 let _ = filter.find_all("含有赌博"); filter.clear_cache();
977
978 let results = filter.find_all("含有赌博");
980 assert!(results.contains(&"赌博".to_string()));
981 }
982
983 #[test]
986 fn test_empty_text() {
987 let mut filter = Filter::new();
988 filter.add_word("赌博");
989
990 assert_eq!(filter.find_in(""), (false, String::new()));
991 assert!(filter.find_all("").is_empty());
992 assert_eq!(filter.replace("", '*'), "");
993 assert_eq!(filter.filter(""), "");
994 }
995
996 #[test]
997 fn test_empty_dictionary() {
998 let filter = Filter::new();
999
1000 assert_eq!(filter.find_in("任何文本"), (false, String::new()));
1001 assert!(filter.find_all("任何文本").is_empty());
1002 assert_eq!(filter.replace("任何文本", '*'), "任何文本");
1003 assert_eq!(filter.filter("任何文本"), "任何文本");
1004 }
1005
1006 #[test]
1007 fn test_unicode_emoji_does_not_interfere() {
1008 let mut filter = Filter::new();
1009 filter.add_word("赌博");
1010
1011 let (found, word) = filter.find_in("🎉 赌博 🎰");
1013 assert!(found);
1014 assert_eq!(word, "赌博");
1015 }
1016
1017 #[test]
1018 fn test_very_long_text() {
1019 let mut filter = Filter::new();
1020 filter.add_word("赌博");
1021
1022 let long_text = "正常".repeat(100_000) + "赌博";
1024 let results = filter.find_all(&long_text);
1025 assert!(results.contains(&"赌博".to_string()));
1026 }
1027
1028 #[test]
1029 fn test_cjk_extension_b_chars() {
1030 let mut filter = Filter::new();
1031 filter.add_word("赌博");
1032
1033 let text = "含有赌博内容 𠀀𠀁";
1035 let (found, _) = filter.find_in(text);
1036 assert!(found);
1037 }
1038}