Files
harbor-net/apps/windows-client/src-tauri/src/validation.rs

224 lines
6.7 KiB
Rust

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,
})
}