use crate::models::{ ComponentId, Profile, ProfileInput, ProfileItem, ProfileItemType, Protocol, ProxyProtocol, Target, TargetInput, TargetKind, }; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ValidationError { pub field: String, pub message: String, } pub type ValidationResult = Result>; fn error(field: impl Into, message: impl Into) -> ValidationError { ValidationError { field: field.into(), message: message.into(), } } fn clean(value: &str) -> String { value.trim().to_string() } fn slug(value: &str, fallback: &str) -> String { let mut output = String::new(); let mut previous_dash = false; for ch in value.trim().to_lowercase().chars() { if ch.is_ascii_alphanumeric() { output.push(ch); previous_dash = false; } else if !previous_dash { output.push('-'); previous_dash = true; } } let output = output.trim_matches('-').to_string(); if output.is_empty() { fallback.to_string() } else { output } } fn process_name(value: &str) -> String { let base = value.trim().rsplit(['\\', '/']).next().unwrap_or("").trim(); base.strip_suffix(".exe") .or_else(|| base.strip_suffix(".EXE")) .unwrap_or(base) .trim() .to_string() } pub fn parse_protocol(value: &str) -> Result { match value.trim().to_ascii_uppercase().as_str() { "TCP" => Ok(Protocol::Tcp), "UDP" => Ok(Protocol::Udp), _ => Err(error( "protocols", format!("Неподдерживаемый протокол: {value}"), )), } } pub fn parse_profile_item_type(value: &str) -> Result { match value.trim().to_ascii_lowercase().as_str() { "process" => Ok(ProfileItemType::Process), "folder" => Ok(ProfileItemType::Folder), "exe" => Ok(ProfileItemType::Exe), _ => Err(error( "items.type", format!("Неподдерживаемый тип элемента: {value}"), )), } } pub fn parse_target_kind(value: &str) -> Result { match value.trim().to_ascii_lowercase().as_str() { "local" => Ok(TargetKind::Local), "external" => Ok(TargetKind::External), _ => Err(error("kind", format!("Неподдерживаемый тип цели: {value}"))), } } pub fn parse_proxy_protocol(value: &str) -> Result { match value.trim().to_ascii_lowercase().as_str() { "socks5" => Ok(ProxyProtocol::Socks5), "http" => Ok(ProxyProtocol::Http), _ => Err(error( "protocol", format!("Неподдерживаемый протокол прокси: {value}"), )), } } pub fn parse_component_id(value: &str) -> Result { match value.trim().to_ascii_lowercase().as_str() { "control-app" | "controlapp" => Ok(ComponentId::ControlApp), "proxyfier" => Ok(ComponentId::Proxyfier), "singbox" | "sing-box" => Ok(ComponentId::Singbox), _ => Err(error( "requires_component", format!("Неподдерживаемый компонент: {value}"), )), } } pub fn normalize_profile(input: ProfileInput) -> ValidationResult { let mut errors = Vec::new(); let name = clean(&input.name); if name.is_empty() { errors.push(error("name", "Укажите название профиля")); } let target_id = clean(&input.target_id); if target_id.is_empty() { errors.push(error("target_id", "Укажите цель профиля")); } let mut protocols = Vec::new(); for value in input.protocols { match parse_protocol(&value) { Ok(protocol) if !protocols.contains(&protocol) => protocols.push(protocol), Ok(_) => {} Err(err) => errors.push(err), } } if protocols.is_empty() { errors.push(error("protocols", "Выберите хотя бы один протокол")); } let mut items = Vec::new(); for raw_item in input.items { let item_type = match parse_profile_item_type(&raw_item.item_type) { Ok(item_type) => item_type, Err(err) => { errors.push(err); continue; } }; let value = match item_type { ProfileItemType::Process => process_name(&raw_item.value), ProfileItemType::Folder | ProfileItemType::Exe => clean(&raw_item.value), }; if value.is_empty() { errors.push(error("items.value", "Укажите значение элемента профиля")); continue; } let recursive = matches!(item_type, ProfileItemType::Folder) && raw_item.recursive.unwrap_or(true); items.push(ProfileItem { item_type, value, recursive, }); } if !errors.is_empty() { return Err(errors); } Ok(Profile { id: slug(input.id.as_deref().unwrap_or(&name), "profile"), name, enabled: input.enabled, target_id, protocols, items, }) } pub fn normalize_target(input: TargetInput) -> ValidationResult { let mut errors = Vec::new(); let name = clean(&input.name); let host = clean(&input.host); if name.is_empty() { errors.push(error("name", "Укажите название цели")); } if host.is_empty() { errors.push(error("host", "Укажите хост цели")); } if input.port == 0 || input.port > u16::MAX as u32 { errors.push(error("port", "Порт цели должен быть от 1 до 65535")); } let kind = parse_target_kind(&input.kind).unwrap_or_else(|err| { errors.push(err); TargetKind::External }); let protocol = parse_proxy_protocol(&input.protocol).unwrap_or_else(|err| { errors.push(err); ProxyProtocol::Socks5 }); let requires_component = match input.requires_component { Some(value) if !value.trim().is_empty() => match parse_component_id(&value) { Ok(component) => Some(component), Err(err) => { errors.push(err); None } }, _ => None, }; if !errors.is_empty() { return Err(errors); } Ok(Target { id: slug(input.id.as_deref().unwrap_or(&name), "target"), name, kind, protocol, host, port: input.port as u16, requires_component, }) }