use llm_shield_core::{
async_trait, Entity, Error, Result, RiskFactor, ScanResult, Scanner, ScannerType, Severity,
Vault,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageConfig {
pub allowed_languages: Vec<String>,
pub blocked_languages: Vec<String>,
pub min_confidence: f32,
pub min_length: usize,
}
impl Default for LanguageConfig {
fn default() -> Self {
Self {
allowed_languages: vec!["en".to_string()], blocked_languages: Vec::new(),
min_confidence: 0.7,
min_length: 10,
}
}
}
#[derive(Debug, Clone)]
struct LanguageDetection {
code: String,
name: String,
confidence: f32,
}
pub struct Language {
config: LanguageConfig,
}
impl Language {
pub fn new(config: LanguageConfig) -> Result<Self> {
if !(0.0..=1.0).contains(&config.min_confidence) {
return Err(Error::config("min_confidence must be between 0.0 and 1.0"));
}
Ok(Self { config })
}
pub fn default_config() -> Result<Self> {
Self::new(LanguageConfig::default())
}
pub fn allow_all() -> Result<Self> {
Self::new(LanguageConfig {
allowed_languages: Vec::new(),
blocked_languages: Vec::new(),
min_confidence: 0.7,
min_length: 10,
})
}
fn detect_language(&self, text: &str) -> LanguageDetection {
let mut latin_chars = 0;
let mut cyrillic_chars = 0;
let mut arabic_chars = 0;
let mut cjk_chars = 0;
let mut devanagari_chars = 0;
let mut total_alpha = 0;
for ch in text.chars() {
if ch.is_alphabetic() {
total_alpha += 1;
if ch.is_ascii_alphabetic() || matches!(ch, 'À'..='ÿ') {
latin_chars += 1;
}
else if matches!(ch, 'А'..='я' | 'Ё' | 'ё') {
cyrillic_chars += 1;
}
else if matches!(ch, '\u{0600}'..='\u{06FF}') {
arabic_chars += 1;
}
else if matches!(ch, '\u{4E00}'..='\u{9FFF}' | '\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}' | '\u{AC00}'..='\u{D7AF}') {
cjk_chars += 1;
}
else if matches!(ch, '\u{0900}'..='\u{097F}') {
devanagari_chars += 1;
}
}
}
if total_alpha == 0 {
return LanguageDetection {
code: "unknown".to_string(),
name: "Unknown".to_string(),
confidence: 0.0,
};
}
let confidence;
let (code, name) = if cyrillic_chars > total_alpha / 2 {
confidence = cyrillic_chars as f32 / total_alpha as f32;
("ru", "Russian")
} else if arabic_chars > total_alpha / 2 {
confidence = arabic_chars as f32 / total_alpha as f32;
("ar", "Arabic")
} else if cjk_chars > total_alpha / 3 {
confidence = cjk_chars as f32 / total_alpha as f32;
("zh", "Chinese")
} else if devanagari_chars > total_alpha / 2 {
confidence = devanagari_chars as f32 / total_alpha as f32;
("hi", "Hindi")
} else if latin_chars > total_alpha / 2 {
confidence = latin_chars as f32 / total_alpha as f32;
self.detect_latin_language(text)
} else {
confidence = 0.3;
("unknown", "Unknown")
};
LanguageDetection {
code: code.to_string(),
name: name.to_string(),
confidence,
}
}
fn detect_latin_language(&self, text: &str) -> (&str, &str) {
let text_lower = text.to_lowercase();
let spanish_indicators = ["el ", "la ", "los ", "las ", "es ", "de ", "que ", "y ", "un ", "una ", "estar ", "hola ", "cómo ", "qué "];
let french_indicators = ["le ", "la ", "les ", "de ", "un ", "une ", "est ", "et ", "dans ", "pour ", "que ", "qui ", "avec ", "être "];
let german_indicators = ["der ", "die ", "das ", "den ", "dem ", "des ", "und ", "ist ", "ein ", "eine ", "nicht ", "ich ", "sie ", "wir "];
let portuguese_indicators = ["o ", "a ", "os ", "as ", "de ", "do ", "da ", "em ", "um ", "uma ", "que ", "não ", "para ", "com "];
let italian_indicators = ["il ", "lo ", "la ", "i ", "gli ", "le ", "di ", "da ", "in ", "con ", "è ", "che ", "per ", "un ", "una "];
let mut spanish_count = 0;
let mut french_count = 0;
let mut german_count = 0;
let mut portuguese_count = 0;
let mut italian_count = 0;
for indicator in &spanish_indicators {
if text_lower.contains(indicator) {
spanish_count += 1;
}
}
for indicator in &french_indicators {
if text_lower.contains(indicator) {
french_count += 1;
}
}
for indicator in &german_indicators {
if text_lower.contains(indicator) {
german_count += 1;
}
}
for indicator in &portuguese_indicators {
if text_lower.contains(indicator) {
portuguese_count += 1;
}
}
for indicator in &italian_indicators {
if text_lower.contains(indicator) {
italian_count += 1;
}
}
let max_count = *[spanish_count, french_count, german_count, portuguese_count, italian_count]
.iter()
.max()
.unwrap_or(&0);
if max_count >= 2 {
if spanish_count == max_count {
("es", "Spanish")
} else if french_count == max_count {
("fr", "French")
} else if german_count == max_count {
("de", "German")
} else if portuguese_count == max_count {
("pt", "Portuguese")
} else if italian_count == max_count {
("it", "Italian")
} else {
("en", "English") }
} else {
("en", "English") }
}
fn is_language_allowed(&self, detected_language: &str) -> bool {
if !self.config.blocked_languages.is_empty() {
if self.config.blocked_languages.contains(&detected_language.to_string()) {
return false;
}
}
if self.config.allowed_languages.is_empty() {
return true;
}
self.config.allowed_languages.contains(&detected_language.to_string())
}
}
#[async_trait]
impl Scanner for Language {
fn name(&self) -> &str {
"Language"
}
async fn scan(&self, input: &str, _vault: &Vault) -> Result<ScanResult> {
if input.len() < self.config.min_length {
return Ok(ScanResult::pass(input.to_string()));
}
let detection = self.detect_language(input);
if detection.confidence < self.config.min_confidence {
return Ok(ScanResult::new(input.to_string(), true, detection.confidence * 0.5)
.with_metadata("detected_language", "unknown")
.with_metadata("confidence", "low"));
}
let is_allowed = self.is_language_allowed(&detection.code);
if is_allowed {
return Ok(ScanResult::pass(input.to_string())
.with_metadata("detected_language", &detection.code)
.with_metadata("language_name", &detection.name)
.with_metadata("confidence", detection.confidence.to_string()));
}
let mut metadata = HashMap::new();
metadata.insert("detected_language".to_string(), detection.code.clone());
metadata.insert("language_name".to_string(), detection.name.clone());
metadata.insert("confidence".to_string(), detection.confidence.to_string());
let entity = Entity {
entity_type: "disallowed_language".to_string(),
text: format!("{} ({})", detection.name, detection.code),
start: 0,
end: input.len(),
confidence: detection.confidence,
metadata,
};
let description = format!("Text in disallowed language: {} ({})", detection.name, detection.code);
let risk_factor = RiskFactor::new(
"disallowed_language",
&description,
Severity::High,
1.0,
);
Ok(ScanResult::new(input.to_string(), false, 1.0)
.with_entity(entity)
.with_risk_factor(risk_factor)
.with_metadata("detected_language", &detection.code)
.with_metadata("language_name", &detection.name))
}
fn scanner_type(&self) -> ScannerType {
ScannerType::Input
}
fn description(&self) -> &str {
"Detects and validates the language of input text"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_language_english() {
let scanner = Language::default_config().unwrap();
let vault = Vault::new();
let english_text = "Hello world, this is a test of the language detection system.";
let result = scanner.scan(english_text, &vault).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.metadata.get("detected_language").unwrap(), "en");
}
#[tokio::test]
async fn test_language_spanish_blocked() {
let config = LanguageConfig {
allowed_languages: vec!["en".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let spanish_text = "Hola mundo, ¿cómo estás? Esto es una prueba del sistema de detección de idioma.";
let result = scanner.scan(spanish_text, &vault).await.unwrap();
assert!(!result.is_valid);
assert_eq!(result.entities[0].metadata.get("detected_language").unwrap(), "es");
}
#[tokio::test]
async fn test_language_spanish_allowed() {
let config = LanguageConfig {
allowed_languages: vec!["en".to_string(), "es".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let spanish_text = "Hola mundo, ¿cómo estás? Esto es una prueba del sistema.";
let result = scanner.scan(spanish_text, &vault).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.metadata.get("detected_language").unwrap(), "es");
}
#[tokio::test]
async fn test_language_french() {
let config = LanguageConfig {
allowed_languages: vec!["fr".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let french_text = "Bonjour le monde, c'est un test du système de détection de langue.";
let result = scanner.scan(french_text, &vault).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.metadata.get("detected_language").unwrap(), "fr");
}
#[tokio::test]
async fn test_language_russian() {
let config = LanguageConfig {
allowed_languages: vec!["ru".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let russian_text = "Привет мир, это тест системы обнаружения языка.";
let result = scanner.scan(russian_text, &vault).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.metadata.get("detected_language").unwrap(), "ru");
}
#[tokio::test]
async fn test_language_arabic() {
let config = LanguageConfig {
allowed_languages: vec!["ar".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let arabic_text = "مرحبا بالعالم، هذا اختبار لنظام كشف اللغة.";
let result = scanner.scan(arabic_text, &vault).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.metadata.get("detected_language").unwrap(), "ar");
}
#[tokio::test]
async fn test_language_allow_all() {
let scanner = Language::allow_all().unwrap();
let vault = Vault::new();
let spanish_text = "Hola mundo";
let result = scanner.scan(spanish_text, &vault).await.unwrap();
assert!(result.is_valid);
}
#[tokio::test]
async fn test_language_blocked_list() {
let config = LanguageConfig {
allowed_languages: Vec::new(), blocked_languages: vec!["es".to_string()], ..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let spanish_text = "Hola mundo, esto es español.";
let result = scanner.scan(spanish_text, &vault).await.unwrap();
assert!(!result.is_valid);
}
#[tokio::test]
async fn test_language_short_text() {
let scanner = Language::default_config().unwrap();
let vault = Vault::new();
let short_text = "Hi";
let result = scanner.scan(short_text, &vault).await.unwrap();
assert!(result.is_valid);
}
#[tokio::test]
async fn test_language_german() {
let config = LanguageConfig {
allowed_languages: vec!["de".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let german_text = "Hallo Welt, das ist ein Test des Spracherkennungssystems.";
let result = scanner.scan(german_text, &vault).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.metadata.get("detected_language").unwrap(), "de");
}
#[tokio::test]
async fn test_language_mixed_scripts() {
let config = LanguageConfig {
allowed_languages: vec!["en".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let mixed_text = "Hello world! This is a test with some números 123.";
let result = scanner.scan(mixed_text, &vault).await.unwrap();
assert!(result.is_valid);
}
#[tokio::test]
async fn test_language_portuguese() {
let config = LanguageConfig {
allowed_languages: vec!["pt".to_string()],
..Default::default()
};
let scanner = Language::new(config).unwrap();
let vault = Vault::new();
let portuguese_text = "Olá mundo, este é um teste do sistema de detecção de língua.";
let result = scanner.scan(portuguese_text, &vault).await.unwrap();
assert!(result.is_valid);
assert_eq!(result.metadata.get("detected_language").unwrap(), "pt");
}
}