Files
ProxyWarden/src-tauri/src/models.rs
T
dokril efda8eb98f
CI / Windows baseline (push) Canceled after 0s
Release v2.0.0
2026-09-10 20:59:52 +03:00

366 lines
9.5 KiB
Rust

use percent_encoding::percent_decode_str;
use serde::{Deserialize, Serialize};
use url::Url;
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_HOST: &str = "127.0.0.1";
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_PORT: u16 = 1080;
pub const DEFAULT_LOCAL_SINGBOX_SERVICE_NAME: &str = "ProxyWardenSingBox";
pub const DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT: &str =
r"C:\Program Files\ProxyWarden\components\sing-box";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Protocol {
Tcp,
Udp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ProfileItemType {
Process,
Folder,
Exe,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TargetKind {
Local,
External,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProxyProtocol {
Socks5,
Http,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ComponentId {
ControlApp,
Proxyfier,
Singbox,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ComponentState {
Installed,
Missing,
Stopped,
Running,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileItemInput {
#[serde(rename = "type")]
pub item_type: String,
pub value: String,
#[serde(default)]
pub recursive: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileInput {
pub id: Option<String>,
pub name: String,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_target_id")]
pub target_id: String,
#[serde(default = "default_protocols")]
pub protocols: Vec<String>,
#[serde(default)]
pub items: Vec<ProfileItemInput>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileItem {
#[serde(rename = "type")]
pub item_type: ProfileItemType,
pub value: String,
pub recursive: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Profile {
pub id: String,
pub name: String,
pub enabled: bool,
pub target_id: String,
pub protocols: Vec<Protocol>,
pub items: Vec<ProfileItem>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetInput {
pub id: Option<String>,
pub name: String,
#[serde(default = "default_target_kind")]
pub kind: String,
#[serde(default = "default_proxy_protocol")]
pub protocol: String,
pub host: String,
pub port: u32,
#[serde(default)]
pub requires_component: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Target {
pub id: String,
pub name: String,
pub kind: TargetKind,
pub protocol: ProxyProtocol,
pub host: String,
pub port: u16,
pub requires_component: Option<ComponentId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ComponentStatus {
pub id: ComponentId,
pub name: String,
pub state: ComponentState,
pub installed: bool,
pub running: bool,
pub version: Option<String>,
pub path: Option<String>,
#[serde(default)]
pub service_name: Option<String>,
#[serde(default)]
pub service_status: Option<String>,
#[serde(default)]
pub problems: Vec<String>,
#[serde(default)]
pub actions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalSingBoxConfig {
#[serde(default)]
pub subscription_url: Option<String>,
#[serde(default)]
pub device_hwid: Option<String>,
#[serde(default)]
pub selected_server_tag: Option<String>,
#[serde(default)]
pub selected_server_id: Option<String>,
#[serde(default = "default_local_singbox_listen_host")]
pub listen_host: String,
#[serde(default = "default_local_singbox_listen_port")]
pub listen_port: u16,
#[serde(default = "default_local_singbox_service_name")]
pub service_name: String,
#[serde(default = "default_local_singbox_install_root", skip_serializing)]
pub install_root: String,
#[serde(default)]
pub updated_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StorageMigrationOutcome {
InitializedEmpty,
AdoptedWithoutLegacyImport,
ImportedLegacyConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StorageMeta {
pub storage_schema_version: u32,
pub outcome: StorageMigrationOutcome,
pub migration_id: String,
pub completed_at_epoch_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ComponentLayoutMeta {
pub component_layout_version: u32,
pub verified_at_epoch_seconds: u64,
}
impl LocalSingBoxConfig {
pub fn subscription_display_url(&self) -> Option<String> {
self.subscription_url
.as_deref()
.map(redact_subscription_url)
}
pub fn normalize_percent_encoded_tags(&mut self) {
if let Some(selected_server_tag) = self.selected_server_tag.as_mut() {
*selected_server_tag = decode_percent_encoded_utf8(selected_server_tag);
}
}
}
impl Default for LocalSingBoxConfig {
fn default() -> Self {
Self {
subscription_url: None,
device_hwid: None,
selected_server_tag: None,
selected_server_id: None,
listen_host: default_local_singbox_listen_host(),
listen_port: default_local_singbox_listen_port(),
service_name: default_local_singbox_service_name(),
install_root: default_local_singbox_install_root(),
updated_at: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SubscriptionCache {
pub config: serde_json::Value,
#[serde(default)]
pub servers: Vec<SubscriptionServer>,
#[serde(default)]
pub user_info: serde_json::Map<String, serde_json::Value>,
pub fetched_at: String,
}
impl SubscriptionCache {
pub fn normalize_percent_encoded_tags(&mut self) {
for server in &mut self.servers {
server.tag = decode_percent_encoded_utf8(&server.tag);
server.ensure_id();
}
// Outbound bytes define stable identity. Decode display labels only.
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubscriptionServer {
#[serde(default)]
pub id: String,
pub tag: String,
#[serde(rename = "type")]
pub server_type: String,
pub server: String,
pub server_port: u16,
}
impl SubscriptionServer {
pub fn ensure_id(&mut self) {
if self.id.trim().is_empty() {
self.id = subscription_server_id(
&self.server_type,
&self.tag,
&self.server,
self.server_port,
);
}
}
}
pub fn subscription_server_id(
server_type: &str,
tag: &str,
server: &str,
server_port: u16,
) -> String {
format!(
"{}|{}|{}|{}",
server_type.trim().to_ascii_lowercase(),
tag.trim(),
server.trim().to_ascii_lowercase(),
server_port
)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityEntry {
pub id: String,
pub at: String,
pub level: ActivityLevel,
pub title: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActivityLevel {
Info,
Warning,
Error,
Success,
}
fn default_enabled() -> bool {
true
}
fn default_target_id() -> String {
"local-singbox".to_string()
}
fn default_protocols() -> Vec<String> {
vec!["TCP".to_string(), "UDP".to_string()]
}
fn default_target_kind() -> String {
"external".to_string()
}
fn default_proxy_protocol() -> String {
"socks5".to_string()
}
fn default_local_singbox_listen_host() -> String {
DEFAULT_LOCAL_SINGBOX_LISTEN_HOST.to_string()
}
fn default_local_singbox_listen_port() -> u16 {
DEFAULT_LOCAL_SINGBOX_LISTEN_PORT
}
fn default_local_singbox_service_name() -> String {
DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string()
}
fn default_local_singbox_install_root() -> String {
DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT.to_string()
}
pub fn redact_subscription_url(raw_url: &str) -> String {
let trimmed = raw_url.trim();
if trimmed.is_empty() {
return String::new();
}
let Ok(parsed) = Url::parse(trimmed) else {
return "***".to_string();
};
let host = parsed.host_str().unwrap_or("subscription");
let host = if host.contains(':') && !host.starts_with('[') {
format!("[{host}]")
} else {
host.to_string()
};
let port = parsed
.port()
.map(|port| format!(":{port}"))
.unwrap_or_default();
format!("{}://{}{}/...", parsed.scheme(), host, port)
}
pub fn decode_percent_encoded_utf8(value: &str) -> String {
percent_decode_str(value)
.decode_utf8()
.map(|decoded| decoded.into_owned())
.unwrap_or_else(|_| value.to_string())
}