Skip to main content

sil_confusable/
detector.rs

1use crate::unicode_map::to_ascii_equivalent;
2
3pub fn detect_confusables(input: &str) -> Vec<String> {
4    let mut flags = vec![];
5
6    let normalized = to_ascii_equivalent(input);
7
8    if normalized != input {
9        flags.push("VISUAL_MISMATCH_DETECTED".to_string());
10    }
11
12    if has_cross_script_mix(input) {
13        flags.push("CROSS_SCRIPT_MIXING".to_string());
14    }
15
16    if looks_like_common_target(input) {
17        flags.push("POTENTIAL_IMPERSONATION".to_string());
18    }
19
20    flags
21}
22
23fn has_cross_script_mix(input: &str) -> bool {
24    let mut has_latin = false;
25    let mut has_cyrillic = false;
26    let mut has_greek = false;
27
28    for c in input.chars() {
29        let code = c as u32;
30        match code {
31            0x0041..=0x005A | 0x0061..=0x007A => has_latin = true,
32            0x0400..=0x04FF => has_cyrillic = true,
33            0x0370..=0x03FF => has_greek = true,
34            _ => {}
35        }
36    }
37
38    (has_latin && has_cyrillic) || (has_latin && has_greek) || (has_cyrillic && has_greek)
39}
40
41fn looks_like_common_target(input: &str) -> bool {
42    let normalized = to_ascii_equivalent(input).to_lowercase();
43    let targets = [
44        "paypal",
45        "google",
46        "facebook",
47        "amazon",
48        "apple",
49        "microsoft",
50        "netflix",
51        "github",
52        "gmail",
53        "whatsapp",
54    ];
55    targets.contains(&normalized.as_str())
56}