use keyhog_scanner::testing::unicode_hardening::{
contains_evasion, detect_unicode_attacks, normalize_homoglyphs, EvasionKind,
};
use std::borrow::Cow;
fn assert_fullwidth_maps(fw: char, ascii: char) {
let input = format!("x{fw}y");
let out = normalize_homoglyphs(&input);
assert_eq!(
out.as_ref(),
format!("x{ascii}y"),
"U+{:04X} must normalize to '{ascii}'; got {out:?}",
fw as u32
);
}
fn assert_kept_and_not_evasion(c: char, label: &str) {
let s = format!("token_{c}_value");
let out = normalize_homoglyphs(&s);
assert!(
out.contains(c),
"{label} (U+{:04X}) must be kept, not stripped/replaced; got {out:?}",
c as u32
);
assert!(
!contains_evasion(&s),
"{label} (U+{:04X}) must NOT be flagged as evasion",
c as u32
);
}
#[test]
fn fullwidth_uppercase_letter_maps_to_ascii() {
assert_fullwidth_maps('\u{FF21}', 'A'); }
#[test]
fn fullwidth_lowercase_letter_maps_to_ascii() {
assert_fullwidth_maps('\u{FF47}', 'g'); }
#[test]
fn fullwidth_digit_maps_to_ascii() {
assert_fullwidth_maps('\u{FF10}', '0'); }
#[test]
fn fullwidth_underscore_maps_to_ascii() {
assert_fullwidth_maps('\u{FF3F}', '_'); }
#[test]
fn fullwidth_plus_maps_to_ascii() {
assert_fullwidth_maps('\u{FF0B}', '+');
}
#[test]
fn fullwidth_slash_maps_to_ascii() {
assert_fullwidth_maps('\u{FF0F}', '/');
}
#[test]
fn fullwidth_equals_maps_to_ascii() {
assert_fullwidth_maps('\u{FF1D}', '=');
}
#[test]
fn fullwidth_range_start_ff01_maps() {
assert_fullwidth_maps('\u{FF01}', '!'); }
#[test]
fn fullwidth_range_end_ff5e_maps() {
assert_fullwidth_maps('\u{FF5E}', '~'); }
#[test]
fn fullwidth_ghp_prefix_reassembles() {
let out = normalize_homoglyphs("\u{FF47}\u{FF48}\u{FF50}\u{FF3F}deadbeef");
assert_eq!(out.as_ref(), "ghp_deadbeef", "got {out:?}");
}
#[test]
fn fullwidth_aws_key_reassembles() {
let out = normalize_homoglyphs("\u{FF21}\u{FF2B}\u{FF29}\u{FF21}QYLPMN5HFIQR7BBB");
assert!(
out.contains("AKIA"),
"fullwidth AKIA must normalize; got {out:?}"
);
}
#[test]
fn fullwidth_letter_still_classified_fullwidth() {
let attacks = detect_unicode_attacks("ghp_\u{FF21}bc");
assert!(
attacks
.iter()
.any(|a| a.kind == EvasionKind::Fullwidth && a.char == '\u{FF21}'),
"fullwidth letter must still be reported as Fullwidth evasion; got {attacks:?}"
);
}
#[test]
fn fullwidth_letters_trigger_contains_evasion() {
assert!(contains_evasion("\u{FF21}\u{FF22}\u{FF23}"));
}
#[test]
fn halfwidth_katakana_ff61_is_kept() {
assert_kept_and_not_evasion('\u{FF61}', "halfwidth ideographic full stop");
}
#[test]
fn halfwidth_katakana_ka_ff76_is_kept() {
assert_kept_and_not_evasion('\u{FF76}', "halfwidth katakana KA");
}
#[test]
fn halfwidth_katakana_ff9f_is_kept() {
assert_kept_and_not_evasion(
'\u{FF9F}',
"halfwidth katakana semi-voiced mark (range edge)",
);
}
#[test]
fn halfwidth_hangul_filler_ffa0_is_stripped_as_evasion() {
let spliced = "token_\u{FFA0}_value";
assert!(
contains_evasion(spliced),
"the invisible halfwidth hangul filler U+FFA0 must be flagged as an evasion splice vector"
);
let normalized = normalize_homoglyphs(spliced);
assert!(
!normalized.contains('\u{FFA0}'),
"U+FFA0 must be stripped from the value scan path, got {normalized:?}"
);
}
#[test]
fn fullwidth_white_paren_ff5f_is_kept() {
assert_kept_and_not_evasion('\u{FF5F}', "fullwidth left white parenthesis");
}
#[test]
fn fullwidth_yen_sign_ffe5_is_kept() {
assert_kept_and_not_evasion('\u{FFE5}', "fullwidth yen sign");
}
#[test]
fn pure_katakana_text_is_not_evasion() {
assert!(
!contains_evasion("\u{FF76}\u{FF77}\u{FF78}"),
"halfwidth katakana run is not evasion"
);
}
#[test]
fn katakana_only_text_stays_borrowed() {
let out = normalize_homoglyphs("config_\u{FF76}\u{FF77}\u{FF78}_name");
assert!(
matches!(out, Cow::Borrowed(_)),
"CJK-only text must not allocate; got {out:?}"
);
}
#[test]
fn unassigned_ff00_is_kept() {
let out = normalize_homoglyphs("x\u{FF00}y");
assert!(
out.contains('\u{FF00}'),
"U+FF00 (below range) must be kept; got {out:?}"
);
}
#[test]
fn mixed_fullwidth_letter_and_katakana() {
let out = normalize_homoglyphs("\u{FF21}\u{FF76}");
assert_eq!(out.as_ref(), "A\u{FF76}", "got {out:?}");
}
#[test]
fn pure_ascii_stays_borrowed_and_identical() {
let out = normalize_homoglyphs("ghp_abcdef0123456789");
assert!(
matches!(out, Cow::Borrowed(_)),
"pure-ASCII must not allocate"
);
assert_eq!(out.as_ref(), "ghp_abcdef0123456789");
}