224 lines
6.7 KiB
Rust
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,
|
|
})
|
|
}
|