1use crate::engine::MatchAlgorithm;
2use crate::{engine::MultiPatternEngine, variant::VariantDetector};
3use lru::LruCache;
4use rayon::prelude::*;
5use regex::Regex;
6use std::num::NonZero;
7use std::sync::{Arc, Mutex};
8use std::{
9 fs::File,
10 io::{self, BufRead, BufReader},
11 path::Path,
12};
13
14pub struct Filter {
16 engine: MultiPatternEngine, variant_detector: VariantDetector, noise: Regex, cache: Arc<Mutex<LruCache<String, Vec<String>>>>,
20 #[cfg(feature = "net")]
21 http_client: reqwest::blocking::Client, }
23
24impl std::fmt::Debug for Filter {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("Filter")
27 .field("engine", &self.engine)
28 .field("variant_detector", &self.variant_detector)
29 .field("noise", &self.noise)
30 .field("cache", &"<LruCache>")
31 .finish()
32 }
33}
34
35impl Filter {
36 pub fn new() -> Self {
38 Self {
39 engine: MultiPatternEngine::new(None, &[]),
40 variant_detector: VariantDetector::new(),
41 noise: Regex::new(r"[^\w\s\u4e00-\u9fff]").unwrap(),
42 cache: Arc::new(Mutex::new(LruCache::new(NonZero::new(1000).unwrap()))), #[cfg(feature = "net")]
44 http_client: reqwest::blocking::Client::builder()
45 .timeout(std::time::Duration::from_secs(5))
46 .build()
47 .unwrap(),
48 }
49 }
50
51 fn check_cache(&self, text: &str) -> Option<Vec<String>> {
52 self.cache.lock().unwrap_or_else(|e| e.into_inner()).get(text).cloned()
53 }
54
55 fn cache_result(&self, text: &str, results: &[String]) {
56 self.cache.lock().unwrap_or_else(|e| e.into_inner()).put(text.to_string(), results.to_vec());
57 }
58
59 pub fn clear_cache(&self) {
61 self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
62 }
63
64 pub fn with_algorithm(algorithm: MatchAlgorithm) -> Self {
66 Self { engine: MultiPatternEngine::new(Some(algorithm), &[]), ..Self::new() }
67 }
68
69 pub fn with_default_dict() -> io::Result<Self> {
71 let mut filter = Self::new();
72 filter.load_word_dict("dict/dict.txt")?;
73 Ok(filter)
74 }
75
76 pub fn update_noise_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
78 self.noise = Regex::new(pattern)?;
79 Ok(())
80 }
81
82 pub fn add_word(&mut self, word: &str) {
84 let patterns = {
85 let mut p = self.engine.get_patterns().to_vec();
86 p.push(word.to_string());
87 p
88 };
89 self.engine.rebuild(&patterns);
90 self.variant_detector.add_word(word);
91 self.clear_cache();
92 }
93
94 pub fn add_words(&mut self, words: &[&str]) {
96 let mut patterns = self.engine.get_patterns().to_vec();
97 patterns.extend(words.iter().map(|s| s.to_string()));
98
99 self.engine.rebuild(&patterns);
100 for word in words {
101 self.variant_detector.add_word(word);
102 }
103 self.clear_cache();
104 }
105
106 pub fn current_algorithm(&self) -> MatchAlgorithm {
108 self.engine.current_algorithm()
109 }
110
111 pub fn del_word(&mut self, word: &str) {
113 let patterns: Vec<_> = self.engine.get_patterns().iter().filter(|&w| w != word).cloned().collect();
114
115 self.engine.rebuild(&patterns);
116 self.clear_cache();
117 }
118
119 pub fn del_words(&mut self, words: &[&str]) {
121 let word_set: std::collections::HashSet<_> = words.iter().collect();
122 let patterns: Vec<_> =
123 self.engine.get_patterns().iter().filter(|w| !word_set.contains(&w.as_str())).cloned().collect();
124
125 self.engine.rebuild(&patterns);
126 self.clear_cache();
127 }
128
129 pub fn load_word_dict<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
131 let file = File::open(path)?;
132 self.load(BufReader::new(file))
133 }
134
135 pub fn load<R: BufRead>(&mut self, reader: R) -> io::Result<()> {
137 let words: Vec<_> = reader.lines().collect::<Result<_, _>>()?;
138 self.add_words(&words.iter().map(|s| s.as_str()).collect::<Vec<_>>());
139 Ok(())
140 }
141
142 #[cfg(feature = "net")]
144 pub fn load_net_word_dict(&mut self, url: &str) -> io::Result<()> {
145 let response = self.http_client.get(url).send().map_err(io::Error::other)?;
146
147 if !response.status().is_success() {
148 return Err(io::Error::other(format!("HTTP request failed: {}", response.status())));
149 }
150
151 let reader = BufReader::new(response);
152 self.load(reader)
153 }
154
155 pub fn find_in(&self, text: &str) -> (bool, String) {
157 let clean_text = self.remove_noise(text);
158
159 if let Some(word) = self.engine.find_first(&clean_text) {
161 return (true, word);
162 }
163
164 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
166
167 if let Some(word) = self.variant_detector.detect(&clean_text, &patterns).first() {
168 return (true, word.to_string());
169 }
170
171 (false, String::new())
172 }
173
174 pub fn replace(&self, text: &str, replacement: char) -> String {
176 let clean_text = self.remove_noise(text);
177
178 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
180 let variants = self.variant_detector.detect(&clean_text, &patterns);
181
182 let mut result = clean_text;
183
184 for pattern in self.engine.get_patterns() {
186 let repl_str = replacement.to_string().repeat(pattern.chars().count());
187 result = result.replace(pattern, &repl_str);
188 }
189
190 for variant in variants {
192 let repl_str = replacement.to_string().repeat(variant.chars().count());
193 result = result.replace(variant, &repl_str);
194 }
195
196 result
197 }
198
199 pub fn filter(&self, text: &str) -> String {
201 let clean_text = self.remove_noise(text);
202
203 let mut result = self.engine.replace_all(&clean_text, "");
205
206 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
208 let variants = self.variant_detector.detect(&result, &patterns);
209
210 for variant in variants {
211 result = result.replace(variant, "");
212 }
213
214 result
215 }
216
217 pub fn validate(&self, text: &str) -> (bool, String) {
219 self.find_in(text)
220 }
221
222 pub fn remove_noise(&self, text: &str) -> String {
224 self.noise.replace_all(text, "").to_string()
225 }
226
227 pub fn get_noise_pattern(&self) -> &Regex {
229 &self.noise
230 }
231}
232
233impl Filter {
234 pub fn find_all(&self, text: &str) -> Vec<String> {
236 let clean_text = self.remove_noise(text);
237
238 if let Some(cached_result) = self.check_cache(&clean_text) {
240 return cached_result;
241 }
242
243 let results = if clean_text.len() > 1000 {
244 self.find_all_parallel(&clean_text)
246 } else {
247 self.find_all_sequential(&clean_text)
249 };
250
251 self.cache_result(&clean_text, &results);
253
254 results
255 }
256
257 fn find_all_parallel(&self, text: &str) -> Vec<String> {
259 let chunk_size = std::cmp::max(text.len() / rayon::current_num_threads(), 100);
260 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
261
262 let max_pattern_len = patterns.iter().map(|p| p.chars().count()).max().unwrap_or(0);
264 let overlap = max_pattern_len.min(chunk_size);
265
266 let chars: Vec<char> = text.chars().collect();
268 let engine_results: Vec<String> = if chars.len() <= chunk_size {
269 self.engine.find_all(text)
270 } else {
271 let step = chunk_size;
272 chars
273 .windows(chunk_size + overlap)
274 .step_by(step)
275 .collect::<Vec<_>>()
276 .par_iter()
277 .flat_map(|window| {
278 let chunk_text: String = window.iter().collect();
279 self.engine.find_all(&chunk_text)
280 })
281 .collect()
282 };
283
284 let variant_results: Vec<String> = text
286 .split_whitespace()
287 .collect::<Vec<_>>()
288 .par_iter()
289 .map(|segment| self.variant_detector.detect(segment, &patterns))
290 .flatten()
291 .map(|s| s.to_string())
292 .collect();
293
294 let mut results = engine_results;
296 results.extend(variant_results);
297 self.deduplicate_and_sort(results)
298 }
299
300 fn find_all_sequential(&self, text: &str) -> Vec<String> {
302 let mut results = self.engine.find_all(text);
303 let patterns: Vec<_> = self.engine.get_patterns().iter().map(|s| s.as_str()).collect();
304
305 results.extend(self.variant_detector.detect(text, &patterns).into_iter().map(|s| s.to_string()));
307
308 self.deduplicate_and_sort(results)
309 }
310
311 fn deduplicate_and_sort(&self, mut results: Vec<String>) -> Vec<String> {
313 results.sort_unstable();
314 results.dedup();
315 results
316 }
317
318 pub fn find_all_batch(&self, texts: &[&str]) -> Vec<Vec<String>> {
320 texts.par_iter().map(|text| self.find_all(text)).collect()
321 }
322
323 pub fn find_all_layered(&self, text: &str) -> Vec<String> {
325 let clean_text = self.remove_noise(text);
326 let mut results = Vec::new();
327 let mut remaining_text = clean_text.clone();
328
329 let mut sorted_patterns = self.engine.get_patterns().to_vec();
331 sorted_patterns.sort_by_key(|b| std::cmp::Reverse(b.len()));
332
333 for pattern in &sorted_patterns {
335 if remaining_text.contains(pattern) {
336 results.push(pattern.clone());
337 remaining_text = remaining_text.replace(pattern, " ");
339 }
340 }
341
342 let patterns: Vec<_> = sorted_patterns.iter().map(|s| s.as_str()).collect();
344 results.extend(self.variant_detector.detect(&remaining_text, &patterns).into_iter().map(|s| s.to_string()));
345
346 self.deduplicate_and_sort(results)
347 }
348
349 pub fn find_all_streaming<R: BufRead>(&self, reader: R) -> io::Result<Vec<String>> {
351 let mut all_results = Vec::new();
352
353 for line in reader.lines() {
354 let line = line?;
355 let results = self.find_all(&line);
356 all_results.extend(results);
357 }
358
359 Ok(self.deduplicate_and_sort(all_results))
360 }
361}
362
363impl Default for Filter {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use std::io::Cursor;
373 #[test]
374 fn test_integration() {
375 let mut filter = Filter::new();
376 filter.add_words(&["赌博", "色情"]);
377
378 assert_eq!(filter.find_in("含有赌博"), (true, "赌博".to_string()));
380
381 assert_eq!(filter.find_in("含有 dubo"), (true, "赌博".to_string()));
383
384 assert_eq!(filter.replace("赌博 色情", '*'), "** **");
386
387 assert_eq!(filter.filter("赌博内容"), "内容");
389 }
390
391 #[test]
392 fn test_noise_handling() {
393 let mut filter = Filter::new();
394 filter.add_word("赌博");
395
396 assert_eq!(filter.remove_noise("赌 博"), "赌 博");
398
399 assert_eq!(filter.remove_noise("赌@#$博"), "赌博");
401 }
402
403 #[test]
404 fn test_replace_vs_filter() {
405 let mut filter = Filter::new();
406 filter.add_words(&["赌博", "色情"]);
407
408 let text = "这里有赌博和色情内容";
409
410 assert_eq!(filter.replace(text, '*'), "这里有**和**内容");
412
413 assert_eq!(filter.filter(text), "这里有和内容");
415 }
416
417 #[test]
418 fn test_variant_detection() {
419 let mut filter = Filter::new();
420 filter.add_word("测试");
421
422 assert_eq!(filter.find_in("ceshi"), (true, "测试".to_string()));
423 }
424
425 #[test]
426 fn test_algorithm_switch_one() {
427 let mut small = Filter::new();
429 small.add_words(&["a", "b", "c"]);
430 assert!(matches!(small.engine.current_algorithm(), MatchAlgorithm::WuManber));
431
432 let words: Vec<_> = (0..150).map(|i| format!("word{i}")).collect();
434 let mut medium = Filter::new();
435 medium.add_words(&words.iter().map(|s| s.as_str()).collect::<Vec<_>>());
436 println!("Medium current_algorithm: {:?}", medium.engine.current_algorithm());
437 assert!(matches!(medium.engine.current_algorithm(), MatchAlgorithm::AhoCorasick));
438 }
439
440 #[test]
441 fn test_io_operations() -> io::Result<()> {
442 let mut filter = Filter::new();
443 let cursor = Cursor::new("word1\nword2\nword3");
444 filter.load(cursor)?;
445
446 assert_eq!(filter.find_in("word2"), (true, "word2".to_string()));
447 Ok(())
448 }
449
450 #[test]
451 fn test_algorithm_recommendation() {
452 assert_eq!(MultiPatternEngine::recommend_algorithm(50), MatchAlgorithm::WuManber);
453 assert_eq!(MultiPatternEngine::recommend_algorithm(150), MatchAlgorithm::AhoCorasick);
454 assert_eq!(MultiPatternEngine::recommend_algorithm(15000), MatchAlgorithm::Regex);
455 }
456
457 #[test]
458 fn test_algorithm_switch() {
459 let mut small = Filter::new();
461 small.add_words(&["a", "b", "c"]);
462 println!("Small (3 words): {:?}", small.current_algorithm());
463 assert!(matches!(small.current_algorithm(), MatchAlgorithm::WuManber));
464
465 let words: Vec<_> = (0..150).map(|i| format!("word{i}")).collect();
467 let word_refs: Vec<&str> = words.iter().map(|s| s.as_str()).collect();
468
469 let mut medium = Filter::new();
470 medium.add_words(&word_refs);
471
472 println!("Medium (150 words): {:?}", medium.current_algorithm());
473 println!("Pattern count: {}", medium.engine.get_patterns().len());
474
475 let recommended = MultiPatternEngine::recommend_algorithm(150);
477 println!("Recommended algorithm for 150 words: {recommended:?}");
478
479 assert!(matches!(medium.current_algorithm(), MatchAlgorithm::AhoCorasick));
480 }
481
482 #[test]
483 fn test_cache_invalidation_on_add_word() {
484 let mut filter = Filter::new();
485 filter.add_word("赌博");
486
487 let results1 = filter.find_all("这里有赌博");
489 assert!(results1.contains(&"赌博".to_string()));
490
491 filter.add_word("色情");
493
494 let results2 = filter.find_all("这里有赌博和色情");
496 assert!(results2.contains(&"赌博".to_string()));
497 assert!(results2.contains(&"色情".to_string()));
498 }
499
500 #[test]
501 fn test_cache_invalidation_on_del_word() {
502 let mut filter = Filter::new();
503 filter.add_words(&["赌博", "色情"]);
504
505 let results1 = filter.find_all("这里有赌博和色情");
507 assert!(results1.contains(&"赌博".to_string()));
508 assert!(results1.contains(&"色情".to_string()));
509
510 filter.del_word("赌博");
512
513 let results2 = filter.find_all("这里有赌博和色情");
515 assert!(!results2.contains(&"赌博".to_string()));
516 assert!(results2.contains(&"色情".to_string()));
517 }
518
519 #[test]
520 fn test_mutex_poison_recovery() {
521 use std::sync::Arc;
522
523 let filter = Arc::new(Filter::new());
524 let filter_clone = Arc::clone(&filter);
525
526 let handle = std::thread::spawn(move || {
528 let _guard = filter_clone.cache.lock().unwrap();
529 panic!("intentional panic to poison mutex");
530 });
531 let _ = handle.join();
532
533 let results = filter.find_all("test");
535 assert!(results.is_empty());
536 }
537
538 #[test]
539 fn test_parallel_search_cross_boundary() {
540 let mut filter = Filter::new();
541 filter.add_word("赌博");
542
543 let prefix: String = "安全文字".repeat(200); let text = format!("{prefix}这里有赌博内容");
547
548 let results = filter.find_all(&text);
549 assert!(results.contains(&"赌博".to_string()));
550 }
551
552 #[test]
553 fn test_parallel_search_no_duplicates() {
554 let mut filter = Filter::new();
555 filter.add_word("赌博");
556
557 let prefix: String = "安全".repeat(300); let text = format!("{prefix}赌博{prefix}");
560
561 let results = filter.find_all(&text);
562 let count = results.iter().filter(|w| *w == "赌博").count();
563 assert_eq!(count, 1, "expected exactly 1 match, got {count}");
564 }
565
566 #[test]
569 fn test_find_all_batch() {
570 let mut filter = Filter::new();
571 filter.add_words(&["赌博", "色情"]);
572
573 let texts = vec!["含有赌博", "含有色情", "正常内容"];
574 let results = filter.find_all_batch(&texts);
575
576 assert_eq!(results.len(), 3);
577 assert!(results[0].contains(&"赌博".to_string()));
578 assert!(results[1].contains(&"色情".to_string()));
579 assert!(results[2].is_empty());
580 }
581
582 #[test]
583 fn test_find_all_batch_empty() {
584 let filter = Filter::new();
585 let results = filter.find_all_batch(&[]);
586 assert!(results.is_empty());
587 }
588
589 #[test]
590 fn test_find_all_layered_prefers_longest() {
591 let mut filter = Filter::new();
592 filter.add_words(&["赌", "赌博", "赌博机"]);
593
594 let results = filter.find_all_layered("这里有赌博机");
596 assert!(results.contains(&"赌博机".to_string()));
597 assert!(!results.contains(&"赌".to_string()));
598 assert!(!results.contains(&"赌博".to_string()));
599 }
600
601 #[test]
602 fn test_find_all_streaming_multiline() {
603 let mut filter = Filter::new();
604 filter.add_words(&["赌博", "色情"]);
605
606 let input = "第一行含有赌博\n第二行含有色情\n第三行正常";
607 let cursor = std::io::Cursor::new(input);
608 let results = filter.find_all_streaming(cursor).unwrap();
609
610 assert!(results.contains(&"赌博".to_string()));
611 assert!(results.contains(&"色情".to_string()));
612 assert_eq!(results.len(), 2);
613 }
614
615 #[test]
618 fn test_cache_hit_returns_consistent_results() {
619 let mut filter = Filter::new();
620 filter.add_words(&["赌博", "色情"]);
621
622 let r1 = filter.find_all("含有赌博和色情内容");
624 let r2 = filter.find_all("含有赌博和色情内容");
625 assert_eq!(r1, r2);
626 }
627
628 #[test]
629 fn test_cache_clear() {
630 let mut filter = Filter::new();
631 filter.add_word("赌博");
632
633 filter.find_all("含有赌博"); filter.clear_cache();
635
636 let results = filter.find_all("含有赌博");
638 assert!(results.contains(&"赌博".to_string()));
639 }
640
641 #[test]
644 fn test_empty_text() {
645 let mut filter = Filter::new();
646 filter.add_word("赌博");
647
648 assert_eq!(filter.find_in(""), (false, String::new()));
649 assert!(filter.find_all("").is_empty());
650 assert_eq!(filter.replace("", '*'), "");
651 assert_eq!(filter.filter(""), "");
652 }
653
654 #[test]
655 fn test_empty_dictionary() {
656 let filter = Filter::new();
657
658 assert_eq!(filter.find_in("任何文本"), (false, String::new()));
659 assert!(filter.find_all("任何文本").is_empty());
660 assert_eq!(filter.replace("任何文本", '*'), "任何文本");
661 assert_eq!(filter.filter("任何文本"), "任何文本");
662 }
663
664 #[test]
665 fn test_unicode_emoji_does_not_interfere() {
666 let mut filter = Filter::new();
667 filter.add_word("赌博");
668
669 let (found, word) = filter.find_in("🎉 赌博 🎰");
671 assert!(found);
672 assert_eq!(word, "赌博");
673 }
674
675 #[test]
676 fn test_very_long_text() {
677 let mut filter = Filter::new();
678 filter.add_word("赌博");
679
680 let long_text = "正常".repeat(100_000) + "赌博";
682 let results = filter.find_all(&long_text);
683 assert!(results.contains(&"赌博".to_string()));
684 }
685
686 #[test]
687 fn test_cjk_extension_b_chars() {
688 let mut filter = Filter::new();
689 filter.add_word("赌博");
690
691 let text = "含有赌博内容 𠀀𠀁";
693 let (found, _) = filter.find_in(text);
694 assert!(found);
695 }
696}