homoglyph-detect 0.1.0

Detect Cyrillic/Greek lookalike chars masquerading as ASCII. For prompt-injection and phishing defense. Zero deps.
Documentation
use homoglyph_detect::{ascii_equivalent, find_homoglyphs, has_homoglyphs, normalize_to_ascii};

#[test]
fn cyrillic_a_in_claude() {
    let s = "cl\u{0430}ude";
    assert!(has_homoglyphs(s));
    let f = find_homoglyphs(s);
    assert_eq!(f.len(), 1);
    assert_eq!(f[0].ascii_equivalent, 'a');
}

#[test]
fn normalizes_to_ascii() {
    assert_eq!(normalize_to_ascii("cl\u{0430}ude"), "claude");
    assert_eq!(normalize_to_ascii("g\u{043E}\u{043E}gle"), "google");
}

#[test]
fn fullwidth_chars_map() {
    // U+FF21 = 'A' (fullwidth A)
    assert_eq!(ascii_equivalent('\u{FF21}'), Some('A'));
    assert_eq!(ascii_equivalent('\u{FF41}'), Some('a'));
    assert_eq!(normalize_to_ascii("\u{FF41}\u{FF42}\u{FF43}"), "abc");
}

#[test]
fn pure_ascii_passes_through() {
    assert!(!has_homoglyphs("hello world"));
    assert_eq!(normalize_to_ascii("hello"), "hello");
}

#[test]
fn unknown_unicode_passes_through() {
    // Random Japanese — not a known lookalike.
    assert!(!has_homoglyphs("日本語"));
    assert_eq!(normalize_to_ascii("日本語"), "日本語");
}

#[test]
fn finding_records_byte_pos() {
    let s = "x\u{0430}y";
    let f = find_homoglyphs(s);
    assert_eq!(f[0].byte_pos, 1);
}