use crate::protocol::mcp::Tool;
use crate::scanner::finding::{Evidence, Finding, FindingLocation, Severity};
pub struct UnicodeHiddenDetector;
#[derive(Debug, Clone)]
pub struct SuspiciousChar {
#[allow(dead_code)] pub char: char,
pub codepoint: u32,
pub position: usize,
pub category: UnicodeCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnicodeCategory {
ZeroWidth,
Bidirectional,
Tag,
Homoglyph,
Combining,
PrivateUse,
DeprecatedFormat,
}
impl std::fmt::Display for UnicodeCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnicodeCategory::ZeroWidth => write!(f, "zero-width"),
UnicodeCategory::Bidirectional => write!(f, "bidirectional-control"),
UnicodeCategory::Tag => write!(f, "tag-character"),
UnicodeCategory::Homoglyph => write!(f, "homoglyph"),
UnicodeCategory::Combining => write!(f, "combining"),
UnicodeCategory::PrivateUse => write!(f, "private-use"),
UnicodeCategory::DeprecatedFormat => write!(f, "deprecated-format"),
}
}
}
impl UnicodeHiddenDetector {
pub fn new() -> Self {
Self
}
pub fn check_tools(&self, tools: &[Tool]) -> Vec<Finding> {
let mut findings = Vec::new();
for tool in tools {
let name_suspicious = self.detect_suspicious_unicode(&tool.name);
if !name_suspicious.is_empty() {
findings.push(self.create_finding(
&tool.name,
"name",
&tool.name,
&name_suspicious,
));
}
if let Some(ref desc) = tool.description {
let desc_suspicious = self.detect_suspicious_unicode(desc);
if !desc_suspicious.is_empty() {
findings.push(self.create_finding(
&tool.name,
"description",
desc,
&desc_suspicious,
));
}
}
if let Some(schema_findings) = self.check_schema(&tool.name, &tool.input_schema) {
findings.extend(schema_findings);
}
}
findings
}
pub fn detect_suspicious_unicode(&self, text: &str) -> Vec<SuspiciousChar> {
let mut suspicious = Vec::new();
for (pos, ch) in text.char_indices() {
if let Some(category) = self.classify_char(ch, text, pos) {
suspicious.push(SuspiciousChar {
char: ch,
codepoint: ch as u32,
position: pos,
category,
});
}
}
suspicious
}
fn classify_char(&self, ch: char, text: &str, pos: usize) -> Option<UnicodeCategory> {
let cp = ch as u32;
if matches!(
cp,
0x200B | 0x200C | 0x200D | 0xFEFF | 0x2060 | 0x2061 | 0x2062 | 0x2063 | 0x2064 | 0x180E ) {
return Some(UnicodeCategory::ZeroWidth);
}
if matches!(
cp,
0x200E | 0x200F | 0x202A | 0x202B | 0x202C | 0x202D | 0x202E | 0x2066 | 0x2067 | 0x2068 | 0x2069 ) {
return Some(UnicodeCategory::Bidirectional);
}
if (0xE0000..=0xE007F).contains(&cp) || (0xE0100..=0xE01EF).contains(&cp) {
return Some(UnicodeCategory::Tag);
}
if self.is_homoglyph(ch) {
return Some(UnicodeCategory::Homoglyph);
}
if (0x0300..=0x036F).contains(&cp) || (0x1AB0..=0x1AFF).contains(&cp) || (0x1DC0..=0x1DFF).contains(&cp) || (0x20D0..=0x20FF).contains(&cp) || (0xFE20..=0xFE2F).contains(&cp)
{
if self.has_excessive_combining(text, pos) {
return Some(UnicodeCategory::Combining);
}
}
if (0xE000..=0xF8FF).contains(&cp) || (0xF0000..=0xFFFFD).contains(&cp) || (0x100000..=0x10FFFD).contains(&cp)
{
return Some(UnicodeCategory::PrivateUse);
}
if (0x206A..=0x206F).contains(&cp) {
return Some(UnicodeCategory::DeprecatedFormat);
}
None
}
fn is_homoglyph(&self, ch: char) -> bool {
matches!(
ch,
'а' | 'е' | 'о' | 'р' | 'с' | 'у' | 'х' | 'А' | 'В' | 'Е' | 'К' | 'М' | 'Н' | 'О' | 'Р' | 'С' | 'Т' | 'У' | 'Х' | 'Α' | 'Β' | 'Ε' | 'Ζ' | 'Η' | 'Ι' | 'Κ' | 'Μ' | 'Ν' | 'Ο' | 'Ρ' | 'Τ' | 'Υ' | 'Χ' |
'A'..='Z' | 'a'..='z' | '0'..='9' |
'𝐀'..='𝐙' | '𝐚'..='𝐳' | '𝑨'..='𝒁' | '𝒂'..='𝒛' | '𝗔'..='𝗭' | '𝗮'..='𝘇' | '𝘈'..='𝘡' | '𝘢'..='𝘻' | 'ı' | 'ȷ' | 'ɑ' | 'ɡ' | 'ℓ' | '℮' | 'ⅰ'..='ⅻ' | 'Ⅰ'..='Ⅻ' )
}
fn has_excessive_combining(&self, text: &str, pos: usize) -> bool {
let chars: Vec<char> = text.chars().collect();
let char_pos = text[..pos].chars().count();
let mut combining_count = 0;
for ch in chars.iter().skip(char_pos) {
let cp = *ch as u32;
if (0x0300..=0x036F).contains(&cp)
|| (0x1AB0..=0x1AFF).contains(&cp)
|| (0x1DC0..=0x1DFF).contains(&cp)
|| (0x20D0..=0x20FF).contains(&cp)
|| (0xFE20..=0xFE2F).contains(&cp)
{
combining_count += 1;
} else {
break;
}
}
combining_count > 2
}
fn check_schema(&self, tool_name: &str, schema: &serde_json::Value) -> Option<Vec<Finding>> {
let mut findings = Vec::new();
self.check_schema_recursive(tool_name, schema, &mut findings, "");
if findings.is_empty() {
None
} else {
Some(findings)
}
}
fn check_schema_recursive(
&self,
tool_name: &str,
value: &serde_json::Value,
findings: &mut Vec<Finding>,
path: &str,
) {
match value {
serde_json::Value::String(s) => {
let suspicious = self.detect_suspicious_unicode(s);
if !suspicious.is_empty() {
findings.push(self.create_finding(
tool_name,
&format!("schema{}", path),
s,
&suspicious,
));
}
}
serde_json::Value::Array(arr) => {
for (i, item) in arr.iter().enumerate() {
self.check_schema_recursive(
tool_name,
item,
findings,
&format!("{}[{}]", path, i),
);
}
}
serde_json::Value::Object(obj) => {
for (key, val) in obj {
let key_suspicious = self.detect_suspicious_unicode(key);
if !key_suspicious.is_empty() {
findings.push(self.create_finding(
tool_name,
&format!("schema{}.key", path),
key,
&key_suspicious,
));
}
self.check_schema_recursive(
tool_name,
val,
findings,
&format!("{}.{}", path, key),
);
}
}
_ => {}
}
}
fn create_finding(
&self,
tool_name: &str,
field: &str,
text: &str,
suspicious: &[SuspiciousChar],
) -> Finding {
let categories: Vec<_> = suspicious
.iter()
.map(|s| s.category)
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
let severity = self.assess_severity(&categories);
let char_details: Vec<String> = suspicious
.iter()
.take(5)
.map(|s| {
format!(
"U+{:04X} ({}) at pos {}",
s.codepoint, s.category, s.position
)
})
.collect();
let truncated_text = if text.len() > 50 {
format!("{}...", &text[..50])
} else {
text.to_string()
};
Finding::new(
"MCP-SEC-044",
severity,
"Unicode Hidden Instructions Detected",
format!(
"Tool '{}' {} contains {} suspicious Unicode character(s) that could hide malicious instructions.",
tool_name,
field,
suspicious.len()
),
)
.with_location(FindingLocation::tool(tool_name).with_context(field))
.with_evidence(Evidence::observation(
format!("Detected: {}", char_details.join(", ")),
format!("In text: \"{}\"", truncated_text),
))
.with_remediation(
"Remove or replace suspicious Unicode characters. Use ASCII-only text for tool \
descriptions and names. Implement Unicode normalization and validation.",
)
.with_cwe("116")
}
fn assess_severity(&self, categories: &[UnicodeCategory]) -> Severity {
if categories.contains(&UnicodeCategory::Bidirectional) {
return Severity::Critical;
}
if categories.contains(&UnicodeCategory::ZeroWidth) {
return Severity::High;
}
if categories.contains(&UnicodeCategory::Tag) {
return Severity::High;
}
if categories.contains(&UnicodeCategory::Homoglyph) {
return Severity::High;
}
if categories.contains(&UnicodeCategory::PrivateUse) {
return Severity::Medium;
}
Severity::Medium
}
}
impl Default for UnicodeHiddenDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_tool(name: &str, description: Option<&str>) -> Tool {
Tool {
name: name.to_string(),
description: description.map(|s| s.to_string()),
input_schema: json!({"type": "object"}),
}
}
#[test]
fn detect_zero_width_space() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{200B}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::ZeroWidth);
}
#[test]
fn detect_rtl_override() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{202E}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Bidirectional);
}
#[test]
fn detect_cyrillic_homoglyph() {
let detector = UnicodeHiddenDetector::new();
let text = "hеllo";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Homoglyph);
}
#[test]
fn detect_tag_characters() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{E0001}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Tag);
}
#[test]
fn clean_text_no_detection() {
let detector = UnicodeHiddenDetector::new();
let text = "This is a normal tool description with no hidden characters.";
let suspicious = detector.detect_suspicious_unicode(text);
assert!(suspicious.is_empty());
}
#[test]
fn check_tool_with_hidden_chars() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool(
"test\u{200B}tool",
Some("A tool with hidden\u{202E}text"),
)];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 2); }
#[test]
fn severity_assessment() {
let detector = UnicodeHiddenDetector::new();
assert_eq!(
detector.assess_severity(&[UnicodeCategory::Bidirectional]),
Severity::Critical
);
assert_eq!(
detector.assess_severity(&[UnicodeCategory::ZeroWidth]),
Severity::High
);
assert_eq!(
detector.assess_severity(&[UnicodeCategory::PrivateUse]),
Severity::Medium
);
}
#[test]
fn fullwidth_detection() {
let detector = UnicodeHiddenDetector::new();
let text = "exec\u{FF45}cute";
let suspicious = detector.detect_suspicious_unicode(text);
assert!(!suspicious.is_empty());
assert_eq!(suspicious[0].category, UnicodeCategory::Homoglyph);
}
#[test]
fn test_detector_creation() {
let detector = UnicodeHiddenDetector::new();
let detector_default = UnicodeHiddenDetector;
let text = "normal text";
assert!(detector.detect_suspicious_unicode(text).is_empty());
assert!(detector_default.detect_suspicious_unicode(text).is_empty());
}
#[test]
fn test_check_tools_with_safe_tools() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![
make_tool("safe_tool", Some("This is a safe description")),
make_tool("another_tool", Some("No hidden characters here")),
make_tool("third_tool", None),
];
let findings = detector.check_tools(&tools);
assert!(findings.is_empty());
}
#[test]
fn test_detect_ltr_mark() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{200E}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Bidirectional);
assert_eq!(suspicious[0].codepoint, 0x200E);
}
#[test]
fn test_detect_rtl_mark() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{200F}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Bidirectional);
assert_eq!(suspicious[0].codepoint, 0x200F);
}
#[test]
fn test_detect_ltr_embedding() {
let detector = UnicodeHiddenDetector::new();
let text = "test\u{202A}content";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Bidirectional);
}
#[test]
fn test_detect_rtl_embedding() {
let detector = UnicodeHiddenDetector::new();
let text = "test\u{202B}content";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Bidirectional);
}
#[test]
fn test_detect_zero_width_non_joiner() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{200C}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::ZeroWidth);
assert_eq!(suspicious[0].codepoint, 0x200C);
}
#[test]
fn test_detect_zero_width_joiner() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{200D}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::ZeroWidth);
assert_eq!(suspicious[0].codepoint, 0x200D);
}
#[test]
fn test_detect_byte_order_mark() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{FEFF}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::ZeroWidth);
}
#[test]
fn test_detect_greek_homoglyphs() {
let detector = UnicodeHiddenDetector::new();
let text = "helloΑworld";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Homoglyph);
}
#[test]
fn test_detect_mathematical_symbols() {
let detector = UnicodeHiddenDetector::new();
let text = "hello𝐀world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Homoglyph);
}
#[test]
fn test_detect_word_joiner() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{2060}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::ZeroWidth);
}
#[test]
fn test_multiple_tools_mixed_findings() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![
make_tool("safe_tool", Some("Normal description")),
make_tool("bad\u{200B}tool", Some("Also\u{202E}bad")),
make_tool("another_safe", Some("Clean text")),
];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 2); assert!(findings
.iter()
.all(|f| f.location.identifier == "bad\u{200B}tool"));
}
#[test]
fn test_nested_unicode_in_description() {
let detector = UnicodeHiddenDetector::new();
let text = "start\u{200B}middle\u{202E}end\u{200C}finish";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 3);
assert_eq!(suspicious[0].category, UnicodeCategory::ZeroWidth);
assert_eq!(suspicious[1].category, UnicodeCategory::Bidirectional);
assert_eq!(suspicious[2].category, UnicodeCategory::ZeroWidth);
}
#[test]
fn test_unicode_in_input_schema_string() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![Tool {
name: "test_tool".to_string(),
description: None,
input_schema: json!({
"type": "object",
"properties": {
"field": {
"type": "string",
"description": "Field with\u{200B}hidden char"
}
}
}),
}];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert!(findings[0].description.contains("test_tool"));
}
#[test]
fn test_unicode_in_schema_keys() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![Tool {
name: "test_tool".to_string(),
description: None,
input_schema: json!({
"type": "object",
"properties": {
"bad\u{200B}key": {
"type": "string"
}
}
}),
}];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert!(findings[0].description.contains("test_tool"));
}
#[test]
fn test_empty_string() {
let detector = UnicodeHiddenDetector::new();
let text = "";
let suspicious = detector.detect_suspicious_unicode(text);
assert!(suspicious.is_empty());
}
#[test]
fn test_ascii_only_text() {
let detector = UnicodeHiddenDetector::new();
let text = "The quick brown fox jumps over the lazy dog. 1234567890!@#$%^&*()";
let suspicious = detector.detect_suspicious_unicode(text);
assert!(suspicious.is_empty());
}
#[test]
fn test_finding_severity_critical_for_bidirectional() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("text\u{202E}rtl"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].severity, Severity::Critical);
}
#[test]
fn test_finding_severity_high_for_zero_width() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("text\u{200B}space"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].severity, Severity::High);
}
#[test]
fn test_finding_severity_high_for_tag() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("text\u{E0001}tag"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].severity, Severity::High);
}
#[test]
fn test_finding_severity_high_for_homoglyph() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("tеxt"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].severity, Severity::High);
}
#[test]
fn test_finding_location_for_tool_name() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("bad\u{200B}tool", None)];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].location.identifier, "bad\u{200B}tool");
assert!(findings[0]
.location
.context
.as_ref()
.unwrap()
.contains("name"));
}
#[test]
fn test_finding_location_for_description() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("bad\u{200B}desc"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].location.identifier, "tool");
assert!(findings[0]
.location
.context
.as_ref()
.unwrap()
.contains("description"));
}
#[test]
fn test_detect_private_use_area() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{E000}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::PrivateUse);
assert_eq!(suspicious[0].codepoint, 0xE000);
}
#[test]
fn test_detect_deprecated_format() {
let detector = UnicodeHiddenDetector::new();
let text = "hello\u{206A}world";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::DeprecatedFormat);
}
#[test]
fn test_detect_excessive_combining() {
let detector = UnicodeHiddenDetector::new();
let text = "e\u{0301}\u{0302}\u{0303}\u{0304}";
let suspicious = detector.detect_suspicious_unicode(text);
assert!(!suspicious.is_empty());
assert_eq!(suspicious[0].category, UnicodeCategory::Combining);
}
#[test]
fn test_normal_combining_not_detected() {
let detector = UnicodeHiddenDetector::new();
let text = "café";
let suspicious = detector.detect_suspicious_unicode(text);
assert!(suspicious.is_empty() || suspicious.len() < 2);
}
#[test]
fn test_schema_nested_objects() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![Tool {
name: "test_tool".to_string(),
description: None,
input_schema: json!({
"type": "object",
"properties": {
"outer": {
"type": "object",
"properties": {
"inner": {
"type": "string",
"default": "value\u{200B}hidden"
}
}
}
}
}),
}];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert!(findings[0]
.location
.context
.as_ref()
.unwrap()
.contains("schema"));
}
#[test]
fn test_schema_with_arrays() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![Tool {
name: "test_tool".to_string(),
description: None,
input_schema: json!({
"type": "array",
"items": [
"clean",
"bad\u{200B}item",
"also_clean"
]
}),
}];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
}
#[test]
fn test_position_tracking() {
let detector = UnicodeHiddenDetector::new();
let text = "abc\u{200B}def\u{202E}ghi";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 2);
assert_eq!(suspicious[0].position, 3); assert_eq!(suspicious[1].position, 9); }
#[test]
fn test_finding_evidence_details() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("test\u{200B}text"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert!(!findings[0].evidence.is_empty());
assert!(findings[0].evidence[0].data.contains("U+200B"));
}
#[test]
fn test_finding_remediation_present() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("test\u{200B}text"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert!(!findings[0].remediation.is_empty());
assert!(findings[0].remediation.contains("ASCII"));
}
#[test]
fn test_finding_cwe_set() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("test\u{200B}text"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert!(findings[0].references.iter().any(|r| r.id.contains("116")));
}
#[test]
fn test_finding_rule_id() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("tool", Some("test\u{200B}text"))];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].rule_id, "MCP-SEC-044");
}
#[test]
fn test_unicode_category_display() {
assert_eq!(format!("{}", UnicodeCategory::ZeroWidth), "zero-width");
assert_eq!(
format!("{}", UnicodeCategory::Bidirectional),
"bidirectional-control"
);
assert_eq!(format!("{}", UnicodeCategory::Tag), "tag-character");
assert_eq!(format!("{}", UnicodeCategory::Homoglyph), "homoglyph");
assert_eq!(format!("{}", UnicodeCategory::Combining), "combining");
assert_eq!(format!("{}", UnicodeCategory::PrivateUse), "private-use");
assert_eq!(
format!("{}", UnicodeCategory::DeprecatedFormat),
"deprecated-format"
);
}
#[test]
fn test_multiple_categories_severity() {
let detector = UnicodeHiddenDetector::new();
assert_eq!(
detector.assess_severity(&[
UnicodeCategory::ZeroWidth,
UnicodeCategory::Bidirectional,
UnicodeCategory::PrivateUse
]),
Severity::Critical
);
assert_eq!(
detector.assess_severity(&[UnicodeCategory::ZeroWidth, UnicodeCategory::PrivateUse]),
Severity::High
);
}
#[test]
fn test_tool_name_only_unicode() {
let detector = UnicodeHiddenDetector::new();
let tools = vec![make_tool("bad\u{200B}name", None)];
let findings = detector.check_tools(&tools);
assert_eq!(findings.len(), 1);
assert!(findings[0]
.location
.context
.as_ref()
.unwrap()
.contains("name"));
}
#[test]
fn test_dotless_homoglyphs() {
let detector = UnicodeHiddenDetector::new();
let text = "admın";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Homoglyph);
}
#[test]
fn test_roman_numeral_homoglyphs() {
let detector = UnicodeHiddenDetector::new();
let text = "testⅠdata";
let suspicious = detector.detect_suspicious_unicode(text);
assert_eq!(suspicious.len(), 1);
assert_eq!(suspicious[0].category, UnicodeCategory::Homoglyph);
}
}