Skip to main content

sensitive_rs/engine/
wumanber.rs

1use alloc::{string::String, string::ToString, sync::Arc, vec::Vec};
2use core::hash::{Hash, Hasher};
3use hashbrown::{HashMap, HashSet};
4#[cfg(feature = "parallel")]
5use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
6use smallvec::SmallVec;
7
8#[cfg(feature = "std")]
9use std::hash::DefaultHasher;
10
11/// Compact FNV-1a hasher used on `no_std` targets where `std::hash::DefaultHasher`
12/// (SipHash) is unavailable. Quality is sufficient for the shift/hash tables.
13#[cfg(not(feature = "std"))]
14struct Fnv1a(u64);
15
16#[cfg(not(feature = "std"))]
17impl Fnv1a {
18    const OFFSET: u64 = 0xcbf29ce484222325;
19    const PRIME: u64 = 0x100000001b3;
20    fn new() -> Self {
21        Self(Self::OFFSET)
22    }
23}
24
25#[cfg(not(feature = "std"))]
26impl Hasher for Fnv1a {
27    fn write(&mut self, bytes: &[u8]) {
28        let mut h = self.0;
29        for &b in bytes {
30            h ^= u64::from(b);
31            h = h.wrapping_mul(Self::PRIME);
32        }
33        self.0 = h;
34    }
35    fn finish(&self) -> u64 {
36        self.0
37    }
38}
39
40/// Space handling policy
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum SpaceHandling {
43    /// Strict match (default): Spaces must match exactly
44    Strict,
45    /// Ignore spaces: Ignore all whitespace characters when matching
46    IgnoreSpaces,
47    /// Normalized spaces: Treat multiple consecutive spaces as a single space
48    NormalizeSpaces,
49}
50
51/// High-performance Wu-Manber multi-pattern matching algorithm
52/// Optimized for Chinese text processing with parallel table building and memory efficiency
53#[derive(Debug, Clone)]
54pub struct WuManber {
55    patterns: Vec<Arc<String>>,
56    original_patterns: Vec<String>,
57    pattern_set: HashSet<Arc<String>>, // For quick lookup
58    min_len: usize,
59    block_size: usize,
60    shift_table: HashMap<u64, usize>,
61    hash_table: HashMap<u64, SmallVec<[usize; 4]>>,
62    space_handling: SpaceHandling,
63}
64
65impl WuManber {
66    /// Create new instance optimized for Chinese characters
67    pub fn new_chinese(patterns: Vec<String>) -> Self {
68        Self::new_with_space_handling(patterns, SpaceHandling::Strict)
69    }
70
71    /// Create new instance with custom block size
72    pub fn new(patterns: Vec<String>, block_size: usize) -> Self {
73        Self::new_with_block_size_and_space_handling(patterns, block_size, SpaceHandling::Strict)
74    }
75
76    /// Create new instance with parallel table building
77    pub fn new_parallel(patterns: Vec<String>, block_size: usize) -> Self {
78        Self::new_parallel_with_space_handling(patterns, block_size, SpaceHandling::Strict)
79    }
80
81    /// Create new instance with space handling strategy
82    pub fn new_with_space_handling(patterns: Vec<String>, space_handling: SpaceHandling) -> Self {
83        let block_size = Self::calculate_optimal_block_size(&patterns, space_handling);
84        Self::new_with_block_size_and_space_handling(patterns, block_size, space_handling)
85    }
86
87    /// Create new instance with custom block size and space handling
88    pub fn new_with_block_size_and_space_handling(
89        patterns: Vec<String>,
90        block_size: usize,
91        space_handling: SpaceHandling,
92    ) -> Self {
93        Self::build_instance(patterns, block_size, false, space_handling)
94    }
95
96    /// Create new instance with parallel table building and space handling
97    pub fn new_parallel_with_space_handling(
98        patterns: Vec<String>,
99        block_size: usize,
100        space_handling: SpaceHandling,
101    ) -> Self {
102        Self::build_instance(patterns, block_size, true, space_handling)
103    }
104
105    /// Calculate optimal block size based on patterns and space handling
106    fn calculate_optimal_block_size(patterns: &[String], space_handling: SpaceHandling) -> usize {
107        if patterns.is_empty() {
108            return 2;
109        }
110
111        let processed_patterns: Vec<String> =
112            patterns.iter().map(|p| Self::preprocess_pattern(p, space_handling)).filter(|p| !p.is_empty()).collect();
113
114        let min_len = processed_patterns.iter().map(|p| p.chars().count()).min().unwrap_or(2);
115
116        // Dynamically adjust block_size to ensure it doesn't exceed the minimum mode length
117        match min_len {
118            1 => 1,
119            2..=3 => 2,
120            4..=10 => 3,
121            _ => (min_len / 2).clamp(2, 4),
122        }
123    }
124
125    /// Preprocess pattern based on space handling strategy
126    fn preprocess_pattern(pattern: &str, space_handling: SpaceHandling) -> String {
127        match space_handling {
128            SpaceHandling::Strict => pattern.to_string(),
129            SpaceHandling::IgnoreSpaces => pattern.chars().filter(|c| !c.is_whitespace()).collect(),
130            SpaceHandling::NormalizeSpaces => {
131                let mut result = String::new();
132                let mut prev_was_space = false;
133
134                for ch in pattern.chars() {
135                    if ch.is_whitespace() {
136                        if !prev_was_space {
137                            result.push(' ');
138                            prev_was_space = true;
139                        }
140                    } else {
141                        result.push(ch);
142                        prev_was_space = false;
143                    }
144                }
145
146                result
147            }
148        }
149    }
150
151    /// Preprocess text based on space handling strategy
152    fn preprocess_text(&self, text: &str) -> String {
153        Self::preprocess_pattern(text, self.space_handling)
154    }
155
156    /// Create empty instance
157    fn empty() -> Self {
158        WuManber {
159            patterns: Vec::new(),
160            original_patterns: Vec::new(),
161            pattern_set: HashSet::new(),
162            min_len: 0,
163            block_size: 2,
164            shift_table: HashMap::new(),
165            hash_table: HashMap::new(),
166            space_handling: SpaceHandling::Strict,
167        }
168    }
169
170    /// Internal instance builder with Arc optimization and space handling
171    fn build_instance(patterns: Vec<String>, block_size: usize, parallel: bool, space_handling: SpaceHandling) -> Self {
172        if patterns.is_empty() {
173            return Self::empty();
174        }
175
176        // Save the original mode
177        let original_patterns = patterns.clone();
178
179        // Pretreatment mode
180        let processed_patterns: Vec<String> =
181            patterns.iter().map(|p| Self::preprocess_pattern(p, space_handling)).filter(|p| !p.is_empty()).collect();
182
183        if processed_patterns.is_empty() {
184            return Self::empty();
185        }
186
187        let min_len = processed_patterns.iter().map(|p| p.chars().count()).min().unwrap_or(1);
188
189        // Make sure that the block_size does not exceed the minimum mode length
190        let safe_block_size = block_size.min(min_len);
191
192        let patterns_arc: Vec<Arc<String>> = processed_patterns.into_iter().map(Arc::new).collect();
193        // Build pattern_set for quick lookups
194        let pattern_set: HashSet<Arc<String>> = patterns_arc.iter().cloned().collect();
195        let mut instance = WuManber {
196            patterns: patterns_arc,
197            original_patterns,
198            pattern_set,
199            min_len,
200            block_size: safe_block_size,
201            shift_table: HashMap::new(),
202            hash_table: HashMap::new(),
203            space_handling,
204        };
205
206        // Parallel table building only when the `parallel` feature is enabled;
207        // otherwise fall back to the sequential builder.
208        #[cfg(feature = "parallel")]
209        {
210            if parallel {
211                instance.build_tables_parallel();
212                return instance;
213            }
214        }
215        #[cfg(not(feature = "parallel"))]
216        let _ = parallel;
217
218        instance.build_tables();
219
220        instance
221    }
222
223    /// Build shift and hash tables sequentially with memory optimization
224    fn build_tables(&mut self) {
225        self.build_shift_table();
226        self.build_hash_table();
227    }
228
229    /// Build shift and hash tables in parallel with memory optimization
230    #[cfg(feature = "parallel")]
231    fn build_tables_parallel(&mut self) {
232        self.build_shift_table_parallel();
233        self.build_hash_table_parallel();
234    }
235
236    /// Build shift table sequentially with optimized character handling
237    fn build_shift_table(&mut self) {
238        for pattern in self.patterns.iter() {
239            let chars: Vec<char> = pattern.chars().collect();
240            let char_count = chars.len();
241
242            // Prevent spillage: Ensure that the pattern length is greater than or equal to block_size
243            if char_count < self.block_size {
244                continue;
245            }
246
247            for i in 0..=(char_count - self.block_size) {
248                let block = self.extract_block_optimized(&chars, i);
249                let hash = Self::calculate_hash_fast(&block);
250                let shift = char_count - i - self.block_size;
251
252                self.shift_table.entry(hash).and_modify(|v| *v = (*v).min(shift)).or_insert(shift);
253            }
254        }
255    }
256
257    /// Build hash table sequentially with memory optimization
258    fn build_hash_table(&mut self) {
259        for (pattern_idx, pattern) in self.patterns.iter().enumerate() {
260            let chars: Vec<char> = pattern.chars().collect();
261            let char_count = chars.len();
262
263            if char_count >= self.block_size {
264                let start_pos = char_count - self.block_size;
265                let block = self.extract_block_optimized(&chars, start_pos);
266                let hash = Self::calculate_hash_fast(&block);
267
268                self.hash_table.entry(hash).or_default().push(pattern_idx);
269            }
270        }
271    }
272
273    /// Build shift table in parallel with proper iterator handling
274    #[cfg(feature = "parallel")]
275    fn build_shift_table_parallel(&mut self) {
276        let block_size = self.block_size;
277
278        let shift_entries: Vec<(u64, usize)> = self
279            .patterns
280            .par_iter()
281            .flat_map(|pattern| {
282                let chars: Vec<char> = pattern.chars().collect();
283                let char_count = chars.len();
284
285                if char_count < block_size {
286                    return Vec::new();
287                }
288
289                (0..=(char_count - block_size))
290                    .map(move |i| {
291                        let block = chars[i..i + block_size].iter().collect::<String>();
292                        let hash = Self::calculate_hash_fast(&block);
293                        let shift = char_count - i - block_size;
294                        (hash, shift)
295                    })
296                    .collect::<Vec<_>>()
297            })
298            .collect();
299
300        for (hash, shift) in shift_entries {
301            self.shift_table.entry(hash).and_modify(|v| *v = (*v).min(shift)).or_insert(shift);
302        }
303    }
304
305    /// Build hash table in parallel with memory optimization
306    #[cfg(feature = "parallel")]
307    fn build_hash_table_parallel(&mut self) {
308        let block_size = self.block_size;
309
310        let hash_entries: Vec<(u64, usize)> = self
311            .patterns
312            .par_iter()
313            .enumerate()
314            .filter_map(|(pattern_idx, pattern)| {
315                let chars: Vec<char> = pattern.chars().collect();
316                let char_count = chars.len();
317
318                if char_count >= block_size {
319                    let start_pos = char_count - block_size;
320                    let block = chars[start_pos..start_pos + block_size].iter().collect::<String>();
321                    let hash = Self::calculate_hash_fast(&block);
322                    Some((hash, pattern_idx))
323                } else {
324                    None
325                }
326            })
327            .collect();
328
329        for (hash, pattern_idx) in hash_entries {
330            self.hash_table.entry(hash).or_insert_with(SmallVec::new).push(pattern_idx);
331        }
332    }
333
334    /// Extract block from character array with reduced allocations
335    #[inline]
336    fn extract_block_optimized(&self, chars: &[char], start: usize) -> String {
337        chars[start..start + self.block_size].iter().collect()
338    }
339
340    /// Optimized hash calculation with better performance
341    #[inline]
342    fn calculate_hash_fast(s: &str) -> u64 {
343        // `DefaultHasher` (SipHash) under std; a compact FNV-1a fallback on `no_std`.
344        #[cfg(feature = "std")]
345        let mut hasher = DefaultHasher::new();
346        #[cfg(not(feature = "std"))]
347        let mut hasher = Fnv1a::new();
348        s.hash(&mut hasher);
349        hasher.finish()
350    }
351
352    /// Search for first match using Wu-Manber algorithm with space handling
353    pub fn search(&self, text: &str) -> Option<Arc<String>> {
354        if self.patterns.is_empty() || text.is_empty() {
355            return None;
356        }
357
358        if self.space_handling != SpaceHandling::Strict {
359            return self.search_with_preprocessing(text);
360        }
361
362        let chars: Vec<char> = text.chars().collect();
363        let text_len = chars.len();
364
365        if text_len < self.min_len {
366            return None;
367        }
368
369        let mut pos = self.min_len - 1;
370
371        while pos < text_len {
372            if pos + 1 < self.block_size {
373                pos += 1;
374                continue;
375            }
376
377            let block_start = pos + 1 - self.block_size;
378            let block = self.extract_block_optimized(&chars, block_start);
379            let hash = Self::calculate_hash_fast(&block);
380
381            if let Some(&shift) = self.shift_table.get(&hash) {
382                if shift == 0 {
383                    if let Some(pattern_indices) = self.hash_table.get(&hash)
384                        && let Some(result) = self.verify_matches_arc(&chars, pos, pattern_indices)
385                    {
386                        return Some(result);
387                    }
388
389                    let prev_block_start = block_start.saturating_sub(1);
390                    if prev_block_start < block_start {
391                        let prev_block = self.extract_block_optimized(&chars, prev_block_start);
392                        let prev_hash = Self::calculate_hash_fast(&prev_block);
393                        if let Some(found) = self.hash_table.get(&prev_hash).and_then(|prev_pattern_indices| {
394                            self.verify_matches_arc(&chars, pos - 1, prev_pattern_indices)
395                        }) {
396                            return Some(found);
397                        }
398                    }
399
400                    pos += 1;
401                } else {
402                    pos += shift;
403                }
404            } else {
405                // pos += self.min_len;
406                pos += self.min_len.saturating_sub(self.block_size).saturating_add(1);
407            }
408        }
409
410        None
411    }
412
413    /// Search with preprocessing for non-strict space handling
414    fn search_with_preprocessing(&self, text: &str) -> Option<Arc<String>> {
415        for (i, original_pattern) in self.original_patterns.iter().enumerate() {
416            let processed_text = self.preprocess_text(text);
417            let processed_pattern = Self::preprocess_pattern(original_pattern, self.space_handling);
418
419            if processed_text.contains(&processed_pattern) {
420                return self.patterns.get(i).cloned();
421            }
422        }
423        None
424    }
425
426    /// Verify potential matches and return Arc pattern
427    fn verify_matches_arc(&self, chars: &[char], pos: usize, pattern_indices: &[usize]) -> Option<Arc<String>> {
428        for &pattern_idx in pattern_indices {
429            if let Some(pattern) = self.patterns.get(pattern_idx) {
430                let pattern_chars: Vec<char> = pattern.chars().collect();
431                let pattern_len = pattern_chars.len();
432
433                if pos + 1 >= pattern_len {
434                    let start = pos + 1 - pattern_len;
435                    if chars[start..start + pattern_len] == pattern_chars[..] {
436                        return Some(pattern.clone());
437                    }
438                }
439            }
440        }
441        None
442    }
443
444    /// Legacy search method for string compatibility
445    pub fn search_string(&self, text: &str) -> Option<String> {
446        if self.space_handling != SpaceHandling::Strict {
447            // For non-strict matches, use simplified logic to ensure correctness
448            for original_pattern in &self.original_patterns {
449                let processed_text = self.preprocess_text(text);
450                let processed_pattern = Self::preprocess_pattern(original_pattern, self.space_handling);
451                if processed_text.contains(&processed_pattern) {
452                    return Some(original_pattern.clone());
453                }
454            }
455            return None;
456        }
457
458        self.search(text).map(|arc| (*arc).clone())
459    }
460
461    /// Find all matches in text (deduplicated pattern names).
462    pub fn search_all(&self, text: &str) -> Vec<Arc<String>> {
463        if self.patterns.is_empty() || text.is_empty() {
464            return Vec::new();
465        }
466        if self.space_handling != SpaceHandling::Strict {
467            return self.search_all_with_preprocessing(text);
468        }
469
470        let chars: Vec<char> = text.chars().collect();
471        let mut results = Vec::new();
472        let mut found_indices = HashSet::new();
473        self.scan_core(&chars, |pattern_idx, _start, _len| {
474            if found_indices.insert(pattern_idx) {
475                if let Some(pattern) = self.patterns.get(pattern_idx) {
476                    results.push(pattern.clone());
477                }
478            }
479        });
480        results
481    }
482
483    /// Core table-driven scan (Strict mode). Walks the text with the shift/hash
484    /// tables — the same logic `search_all` used to inline — and for every
485    /// verified occurrence (including overlaps) calls
486    /// `on_match(pattern_idx, char_start, char_len)`. The caller decides the
487    /// collect strategy, so `search_all` (dedup by pattern) and `find_matches`
488    /// (collect every position) share one scan implementation instead of two.
489    fn scan_core(&self, chars: &[char], mut on_match: impl FnMut(usize, usize, usize)) {
490        let text_len = chars.len();
491        if text_len < self.min_len {
492            return;
493        }
494        let mut pos = self.min_len - 1;
495        while pos < text_len {
496            if pos + 1 < self.block_size {
497                pos += 1;
498                continue;
499            }
500
501            let block_start = pos + 1 - self.block_size;
502            let block = self.extract_block_optimized(chars, block_start);
503            let hash = Self::calculate_hash_fast(&block);
504
505            if let Some(&shift) = self.shift_table.get(&hash) {
506                if shift == 0 {
507                    if let Some(pattern_indices) = self.hash_table.get(&hash) {
508                        self.verify_and_emit(chars, pos, pattern_indices, &mut on_match);
509                    }
510                    // Overlap handling: re-check the previous block.
511                    let prev_block_start = block_start.saturating_sub(1);
512                    if prev_block_start < block_start {
513                        let prev_block = self.extract_block_optimized(chars, prev_block_start);
514                        let prev_hash = Self::calculate_hash_fast(&prev_block);
515                        if let Some(prev_pattern_indices) = self.hash_table.get(&prev_hash) {
516                            self.verify_and_emit(chars, pos - 1, prev_pattern_indices, &mut on_match);
517                        }
518                    }
519                    pos += 1;
520                } else {
521                    pos += shift;
522                }
523            } else {
524                pos += self.min_len.saturating_sub(self.block_size).saturating_add(1);
525            }
526        }
527    }
528
529    /// Verify candidate patterns whose suffix block sits at `pos` (match ends at
530    /// `pos`, so start = pos+1-len) and emit each that actually matches. No
531    /// dedup — that is the caller's responsibility.
532    fn verify_and_emit(
533        &self,
534        chars: &[char],
535        pos: usize,
536        pattern_indices: &[usize],
537        on_match: &mut impl FnMut(usize, usize, usize),
538    ) {
539        for &pattern_idx in pattern_indices {
540            if let Some(pattern) = self.patterns.get(pattern_idx) {
541                let pattern_chars: Vec<char> = pattern.chars().collect();
542                let pattern_len = pattern_chars.len();
543                if pos + 1 >= pattern_len {
544                    let start = pos + 1 - pattern_len;
545                    if chars[start..start + pattern_len] == pattern_chars[..] {
546                        on_match(pattern_idx, start, pattern_len);
547                    }
548                }
549            }
550        }
551    }
552
553    /// Search all with preprocessing for non-strict space handling
554    fn search_all_with_preprocessing(&self, text: &str) -> Vec<Arc<String>> {
555        let mut results = Vec::new();
556        let mut found_indices = HashSet::new();
557        let processed_text = self.preprocess_text(text);
558
559        for (i, original_pattern) in self.original_patterns.iter().enumerate() {
560            let processed_pattern = Self::preprocess_pattern(original_pattern, self.space_handling);
561
562            if processed_text.contains(&processed_pattern) && !found_indices.contains(&i) {
563                found_indices.insert(i);
564                if i < self.patterns.len() {
565                    results.push(self.patterns[i].clone());
566                }
567            }
568        }
569
570        results
571    }
572
573    /// Find all matches returning strings for compatibility
574    pub fn search_all_strings(&self, text: &str) -> Vec<String> {
575        self.search_all(text).iter().map(|arc| (**arc).clone()).collect()
576    }
577
578    /// Replace all matches with `replacement`, one char per matched character.
579    /// Overlapping patterns resolve leftmost-longest (e.g. with both "赌博" and
580    /// "赌博机" in the dictionary, "赌博机" wins and becomes "***").
581    pub fn replace_all(&self, text: &str, replacement: char) -> String {
582        let repl = replacement.to_string();
583        self.rebuild_matches(text, |start, end| {
584            let char_count = text[start..end].chars().count();
585            repl.repeat(char_count)
586        })
587    }
588
589    /// Remove all matches from text (leftmost-longest on overlap).
590    pub fn remove_all(&self, text: &str) -> String {
591        self.rebuild_matches(text, |_, _| String::new())
592    }
593
594    /// Single-pass rebuild: locate every match via the table-driven
595    /// `find_matches`, order them leftmost-longest, then walk the text once,
596    /// copying non-match spans verbatim and substituting match spans. Spans
597    /// overlapped by an earlier (longer-leftmost) match are skipped.
598    fn rebuild_matches(&self, text: &str, make_replacement: impl Fn(usize, usize) -> String) -> String {
599        let mut matches = self.find_matches(text);
600        // start asc; for equal start, longer span first (so it wins on overlap)
601        matches.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
602
603        let mut out = String::with_capacity(text.len());
604        let mut cursor = 0usize;
605        for m in matches {
606            if m.start < cursor {
607                continue; // covered by a previously kept span
608            }
609            out.push_str(&text[cursor..m.start]);
610            out.push_str(&make_replacement(m.start, m.end));
611            cursor = m.end;
612        }
613        out.push_str(&text[cursor..]);
614        out
615    }
616
617    /// Find all match positions with byte offsets.
618    ///
619    /// Strict mode uses the shift/hash tables via `scan_core` (shared with
620    /// `search_all`); char indices are translated to byte offsets with an O(n)
621    /// `char_indices` lookup so `Match` keeps its byte-offset contract. Non-strict
622    /// space handling falls back to a byte-level brute-force scan
623    /// (`Filter`/`MultiPatternEngine` always use Strict).
624    pub fn find_matches(&self, text: &str) -> Vec<Match> {
625        if self.space_handling != SpaceHandling::Strict || self.patterns.is_empty() || text.is_empty() {
626            return self.find_matches_bruteforce(text);
627        }
628
629        let chars: Vec<char> = text.chars().collect();
630        // char index -> byte offset (O(n) once). A trailing sentinel = text.len()
631        // lets a match ending at the last char resolve to the end of the text.
632        let mut char_to_byte: Vec<usize> = text.char_indices().map(|(b, _)| b).collect();
633        char_to_byte.push(text.len());
634
635        let mut matches = Vec::new();
636        let mut seen = HashSet::new();
637        self.scan_core(&chars, |_pattern_idx, char_start, char_len| {
638            let char_end = char_start + char_len;
639            // scan_core's overlap re-check can emit the same (pattern, position)
640            // twice; dedup by char span so each occurrence is reported once.
641            if seen.insert((char_start, char_end)) {
642                matches.push(Match { start: char_to_byte[char_start], end: char_to_byte[char_end] });
643            }
644        });
645        matches.sort_by_key(|m| m.start);
646        matches
647    }
648
649    /// Byte-level brute-force fallback used for non-strict space handling.
650    fn find_matches_bruteforce(&self, text: &str) -> Vec<Match> {
651        let mut matches = Vec::new();
652        for pattern in &self.original_patterns {
653            let mut start = 0;
654            while let Some(pos) = text[start..].find(pattern) {
655                let absolute_start = start + pos;
656                let absolute_end = absolute_start + pattern.len();
657                matches.push(Match { start: absolute_start, end: absolute_end });
658                start = absolute_start + text[absolute_start..].chars().next().map_or(1, char::len_utf8);
659            }
660        }
661        matches.sort_by_key(|m| m.start);
662        matches
663    }
664
665    /// Get patterns reference (returns Arc slice)
666    pub fn patterns(&self) -> &[Arc<String>] {
667        &self.patterns
668    }
669
670    /// Get original patterns as strings
671    pub fn patterns_strings(&self) -> Vec<String> {
672        self.original_patterns.clone()
673    }
674
675    /// Check if pattern exists (使用 pattern_set 提供 O(1) 查找)
676    pub fn contains_pattern(&self, pattern: &str) -> bool {
677        let target = Arc::new(pattern.to_string());
678        self.pattern_set.contains(&target)
679    }
680
681    /// Get space handling strategy
682    pub fn space_handling(&self) -> SpaceHandling {
683        self.space_handling
684    }
685
686    /// Get memory usage statistics
687    pub fn memory_stats(&self) -> WuManberMemoryStats {
688        let patterns_memory = self.patterns.iter().map(|p| size_of::<Arc<String>>() + p.len()).sum::<usize>();
689
690        let shift_table_memory = self.shift_table.len() * (size_of::<u64>() + size_of::<usize>());
691        let hash_table_memory = self
692            .hash_table
693            .iter()
694            .map(|(_k, v)| size_of::<u64>() + size_of::<SmallVec<[usize; 4]>>() + v.len() * size_of::<usize>())
695            .sum::<usize>();
696
697        let pattern_set_memory = self.pattern_set.len() * size_of::<Arc<String>>();
698
699        let total_memory = patterns_memory + shift_table_memory + hash_table_memory + pattern_set_memory;
700
701        WuManberMemoryStats {
702            total_patterns: self.patterns.len(),
703            patterns_memory,
704            shift_table_memory,
705            hash_table_memory,
706            total_memory,
707        }
708    }
709
710    /// Get statistics
711    pub fn stats(&self) -> WuManberStats {
712        WuManberStats {
713            pattern_count: self.patterns.len(),
714            min_length: self.min_len,
715            block_size: self.block_size,
716            shift_table_size: self.shift_table.len(),
717            hash_table_size: self.hash_table.len(),
718        }
719    }
720}
721
722/// Match result with byte positions
723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724pub struct Match {
725    pub start: usize,
726    pub end: usize,
727}
728
729/// Wu-Manber statistics
730#[derive(Debug, Clone)]
731pub struct WuManberStats {
732    pub pattern_count: usize,
733    pub min_length: usize,
734    pub block_size: usize,
735    pub shift_table_size: usize,
736    pub hash_table_size: usize,
737}
738
739/// Wu-Manber memory usage statistics
740#[derive(Debug, Clone)]
741pub struct WuManberMemoryStats {
742    pub total_patterns: usize,
743    pub patterns_memory: usize,
744    pub shift_table_memory: usize,
745    pub hash_table_memory: usize,
746    pub total_memory: usize,
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752
753    #[test]
754    fn test_basic_matching() {
755        let wm =
756            WuManber::new_chinese(vec!["色情".to_string(), "赌博".to_string(), "诈骗".to_string(), "扯蛋".to_string()]);
757
758        assert_eq!(wm.search_string("正常内容"), None);
759        assert_eq!(wm.search_string("测试含有赌博内容"), Some("赌博".to_string()));
760        assert_eq!(wm.search_string("测试还有色情赌博图片"), Some("色情".to_string()));
761        assert_eq!(wm.search_string("测试有色情赌博图片"), Some("色情".to_string()));
762        assert_eq!(wm.search_string("还有诈骗色情测试"), Some("诈骗".to_string()));
763    }
764
765    #[test]
766    fn test_varied_length() {
767        let wm = WuManber::new(vec!["赌".to_string(), "赌博".to_string(), "赌博机".to_string()], 1);
768
769        assert_eq!(wm.search_string("赌"), Some("赌".to_string()));
770        assert_eq!(wm.search_string("赌博"), Some("赌".to_string())); // 会匹配最先找到的
771        assert_eq!(wm.search_string("赌博机"), Some("赌".to_string())); // 会匹配最先找到的
772    }
773
774    #[test]
775    fn test_space_handling_strategies() {
776        let patterns = vec!["hello world".to_string(), "test  pattern".to_string()];
777
778        // 严格匹配
779        let wm_strict = WuManber::new_with_space_handling(patterns.clone(), SpaceHandling::Strict);
780        assert_eq!(wm_strict.search_string("hello world"), Some("hello world".to_string()));
781        assert_eq!(wm_strict.search_string("helloworld"), None);
782
783        // 忽略空格
784        let wm_ignore = WuManber::new_with_space_handling(patterns.clone(), SpaceHandling::IgnoreSpaces);
785        assert_eq!(wm_ignore.search_string("hello world"), Some("hello world".to_string()));
786        assert_eq!(wm_ignore.search_string("helloworld"), Some("hello world".to_string()));
787
788        // 标准化空格
789        let wm_normalize = WuManber::new_with_space_handling(patterns.clone(), SpaceHandling::NormalizeSpaces);
790        assert_eq!(wm_normalize.search_string("test  pattern"), Some("test  pattern".to_string()));
791        assert_eq!(wm_normalize.search_string("test   pattern"), Some("test  pattern".to_string()));
792    }
793
794    #[test]
795    fn test_mixed_patterns() {
796        let mixed_patterns =
797            vec!["关键词 2500".to_string(), "关键词 3000".to_string(), "test".to_string(), "hello world".to_string()];
798
799        let wm = WuManber::new_chinese(mixed_patterns);
800
801        assert_eq!(wm.search_string("包含关键词 2500 的文本"), Some("关键词 2500".to_string()));
802        assert_eq!(wm.search_string("包含关键词 3000 的文本"), Some("关键词 3000".to_string()));
803        assert_eq!(wm.search_string("this is test"), Some("test".to_string()));
804        assert_eq!(wm.search_string("say hello world"), Some("hello world".to_string()));
805    }
806
807    #[test]
808    fn test_complex_patterns_with_spaces() {
809        let complex_patterns = vec![
810            "Hello World".to_string(),
811            "你好 世界".to_string(),
812            "multiple   spaces".to_string(),
813            "tab\there".to_string(),
814            "new\nline".to_string(),
815        ];
816
817        let wm = WuManber::new_chinese(complex_patterns);
818
819        assert_eq!(wm.search_string("Say Hello World"), Some("Hello World".to_string()));
820        assert_eq!(wm.search_string("说你好 世界"), Some("你好 世界".to_string()));
821        assert_eq!(wm.search_string("has multiple   spaces here"), Some("multiple   spaces".to_string()));
822        assert_eq!(wm.search_string("tab\there you go"), Some("tab\there".to_string()));
823        assert_eq!(wm.search_string("new\nline break"), Some("new\nline".to_string()));
824    }
825
826    #[test]
827    fn test_ignore_spaces_functionality() {
828        let patterns = vec!["关键词 2500".to_string(), "hello world".to_string()];
829        let wm = WuManber::new_with_space_handling(patterns, SpaceHandling::IgnoreSpaces);
830
831        // 这些都应该匹配
832        assert_eq!(wm.search_string("关键词 2500"), Some("关键词 2500".to_string()));
833        assert_eq!(wm.search_string("关键词 2500"), Some("关键词 2500".to_string()));
834        assert_eq!(wm.search_string("关键词  2500"), Some("关键词 2500".to_string()));
835        assert_eq!(wm.search_string("helloworld"), Some("hello world".to_string()));
836        assert_eq!(wm.search_string("hello world"), Some("hello world".to_string()));
837    }
838
839    #[test]
840    fn test_parallel_performance() {
841        let patterns: Vec<String> = (0..5000).map(|i| format!("关键词 {i}")).collect();
842
843        let wm_seq = WuManber::new(patterns.clone(), 2);
844        let wm_par = WuManber::new_parallel(patterns, 2);
845
846        let text = "这里包含关键词 2500 和关键词 3000";
847
848        let result_seq = wm_seq.search_all_strings(text);
849        let result_par = wm_par.search_all_strings(text);
850
851        assert_eq!(result_seq.len(), result_par.len());
852        assert!(result_seq.contains(&"关键词 2500".to_string()));
853        assert!(result_seq.contains(&"关键词 3000".to_string()));
854
855        for item in &result_seq {
856            assert!(result_par.contains(item));
857        }
858    }
859
860    #[test]
861    fn test_search_all_functionality() {
862        let wm = WuManber::new_chinese(vec!["苹果".to_string(), "香蕉".to_string(), "橙子".to_string()]);
863
864        let text = "我喜欢吃苹果、香蕉和橙子";
865        let results = wm.search_all_strings(text);
866
867        assert_eq!(results.len(), 3);
868        assert!(results.contains(&"苹果".to_string()));
869        assert!(results.contains(&"香蕉".to_string()));
870        assert!(results.contains(&"橙子".to_string()));
871    }
872
873    #[test]
874    fn test_find_matches_multibyte_positions() {
875        // Regression: find_matches used to panic on multi-byte text because the
876        // scan cursor advanced by 1 byte instead of 1 character.
877        let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
878
879        let text = "含有赌博内容";
880        let matches = wm.find_matches(text);
881        assert_eq!(matches.len(), 1);
882        assert_eq!(matches[0].start, 6); // "含有" = 6 bytes
883        assert_eq!(matches[0].end, 12); // "赌博" = 6 bytes
884        assert_eq!(&text[matches[0].start..matches[0].end], "赌博");
885    }
886
887    #[test]
888    fn test_find_matches_multiple_multibyte_occurrences() {
889        // Two non-overlapping occurrences must both be reported without panicking.
890        let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
891        let text = "赌博和赌博";
892        let matches = wm.find_matches(text);
893        assert_eq!(matches.len(), 2);
894        for m in &matches {
895            assert_eq!(&text[m.start..m.end], "赌博");
896        }
897    }
898
899    #[test]
900    fn test_find_matches_mixed_ascii_and_multibyte() {
901        // Mixed-script text with multiple matches must not panic.
902        let wm = WuManber::new_chinese(vec!["ab".to_string(), "赌博".to_string()]);
903        let text = "x 赌博 y 赌博 z";
904        let matches = wm.find_matches(text);
905        assert_eq!(matches.len(), 2);
906        assert!(matches.iter().all(|m| &text[m.start..m.end] == "赌博"));
907    }
908
909    #[test]
910    fn test_find_matches_overlapping_patterns() {
911        // Overlapping patterns: "赌博" ⊂ "赌博机" — both match at start=0, but
912        // each occurrence must be reported exactly once (scan_core's overlap
913        // re-check must not double-count the shorter pattern).
914        let wm = WuManber::new_chinese(vec!["赌博".to_string(), "赌博机".to_string()]);
915        let text = "赌博机";
916        let matches = wm.find_matches(text);
917        assert_eq!(matches.len(), 2, "got {matches:?}");
918        assert!(matches.iter().any(|m| &text[m.start..m.end] == "赌博"));
919        assert!(matches.iter().any(|m| &text[m.start..m.end] == "赌博机"));
920        // No duplicate spans.
921        let mut spans: Vec<_> = matches.iter().map(|m| (m.start, m.end)).collect();
922        spans.sort();
923        let before = spans.len();
924        spans.dedup();
925        assert_eq!(spans.len(), before);
926    }
927
928    #[test]
929    fn test_find_matches_pattern_at_text_end() {
930        // Pattern ending exactly at the text boundary exercises the
931        // char_to_byte sentinel (char_end == chars.len() -> text.len()).
932        let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
933        let text = "前缀赌博";
934        let matches = wm.find_matches(text);
935        assert_eq!(matches.len(), 1);
936        assert_eq!(&text[matches[0].start..matches[0].end], "赌博");
937        assert_eq!(matches[0].end, text.len());
938    }
939
940    #[test]
941    fn test_replace_all_overlapping_leftmost_longest() {
942        // 赌 ⊂ 赌博 ⊂ 赌博机:leftmost-longest replaces the whole "赌博机" (3 chars).
943        let wm = WuManber::new_chinese(vec!["赌".to_string(), "赌博".to_string(), "赌博机".to_string()]);
944        assert_eq!(wm.replace_all("赌博机", '*'), "***");
945        assert_eq!(wm.remove_all("赌博机"), "");
946    }
947
948    #[test]
949    fn test_replace_all_multiple_distinct() {
950        let wm = WuManber::new_chinese(vec!["赌博".to_string(), "色情".to_string()]);
951        // Each match replaced by one char per matched character.
952        assert_eq!(wm.replace_all("赌博和色情", '*'), "**和**");
953    }
954
955    #[test]
956    fn test_replace_all_no_match() {
957        let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
958        assert_eq!(wm.replace_all("正常内容", '*'), "正常内容");
959        assert_eq!(wm.remove_all("正常内容"), "正常内容");
960    }
961}