Clarify active Windows client architecture
This commit is contained in:
222
apps/windows-client/src-tauri/src/validation.rs
Normal file
222
apps/windows-client/src-tauri/src/validation.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
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<T> = Result<T, Vec<ValidationError>>;
|
||||
|
||||
fn error(field: impl Into<String>, message: impl Into<String>) -> 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<Protocol, ValidationError> {
|
||||
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<ProfileItemType, ValidationError> {
|
||||
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<TargetKind, ValidationError> {
|
||||
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<ProxyProtocol, ValidationError> {
|
||||
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<ComponentId, ValidationError> {
|
||||
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<Profile> {
|
||||
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<Target> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user