#![allow(clippy::struct_excessive_bools)]
use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum HostSecurity {
#[default]
Same,
Tls,
None,
Reality,
Other(String),
}
impl HostSecurity {
pub fn as_str(&self) -> &str {
match self {
Self::Same => "same",
Self::Tls => "tls",
Self::None => "none",
Self::Reality => "reality",
Self::Other(value) => value,
}
}
}
impl Serialize for HostSecurity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for HostSecurity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
"" | "same" => Self::Same,
"tls" => Self::Tls,
"none" => Self::None,
"reality" => Self::Reality,
_ => Self::Other(value),
})
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum MihomoIpVersion {
#[default]
Inherit,
Dual,
Ipv4,
Ipv6,
Ipv4Prefer,
Ipv6Prefer,
Other(String),
}
impl MihomoIpVersion {
pub fn as_str(&self) -> &str {
match self {
Self::Inherit => "",
Self::Dual => "dual",
Self::Ipv4 => "ipv4",
Self::Ipv6 => "ipv6",
Self::Ipv4Prefer => "ipv4-prefer",
Self::Ipv6Prefer => "ipv6-prefer",
Self::Other(value) => value,
}
}
}
impl Serialize for MihomoIpVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for MihomoIpVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
"" => Self::Inherit,
"dual" => Self::Dual,
"ipv4" => Self::Ipv4,
"ipv6" => Self::Ipv6,
"ipv4-prefer" => Self::Ipv4Prefer,
"ipv6-prefer" => Self::Ipv6Prefer,
_ => Self::Other(value),
})
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum SubscriptionFormat {
Raw,
Json,
Clash,
Other(String),
}
impl SubscriptionFormat {
pub fn as_str(&self) -> &str {
match self {
Self::Raw => "raw",
Self::Json => "json",
Self::Clash => "clash",
Self::Other(value) => value,
}
}
}
impl Serialize for SubscriptionFormat {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for SubscriptionFormat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
"raw" => Self::Raw,
"json" => Self::Json,
"clash" => Self::Clash,
_ => Self::Other(value),
})
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct VlessRoute(Option<u16>);
impl VlessRoute {
pub const fn new(port: u16) -> Self {
Self(Some(port))
}
pub const fn port(self) -> Option<u16> {
self.0
}
}
impl From<u16> for VlessRoute {
fn from(port: u16) -> Self {
Self::new(port)
}
}
impl Serialize for VlessRoute {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.0 {
Some(port) => serializer.serialize_str(&port.to_string()),
None => serializer.serialize_str(""),
}
}
}
impl<'de> Deserialize<'de> for VlessRoute {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl de::Visitor<'_> for Visitor {
type Value = VlessRoute;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an empty string or a port from 0 through 65535")
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(VlessRoute::default())
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(VlessRoute::default())
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
u16::try_from(value)
.map(VlessRoute::new)
.map_err(|_| E::custom("VLESS route is outside the port range"))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
u16::try_from(value)
.map(VlessRoute::new)
.map_err(|_| E::custom("VLESS route is outside the port range"))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if value.is_empty() {
return Ok(VlessRoute::default());
}
value.parse::<u16>().map(VlessRoute::new).map_err(E::custom)
}
}
deserializer.deserialize_any(Visitor)
}
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct HostJsonOverride(String);
impl HostJsonOverride {
pub fn from_value(value: &Value) -> serde_json::Result<Self> {
serde_json::to_string(value).map(Self)
}
pub const fn empty() -> Self {
Self(String::new())
}
pub fn as_raw(&self) -> &str {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn value(&self) -> serde_json::Result<Option<Value>> {
if self.is_empty() {
Ok(None)
} else {
serde_json::from_str(&self.0).map(Some)
}
}
}
impl fmt::Debug for HostJsonOverride {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("HostJsonOverride")
.field(&if self.is_empty() {
"[EMPTY]"
} else {
"[CONFIGURED]"
})
.finish()
}
}
impl Serialize for HostJsonOverride {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for HostJsonOverride {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).map(|value| Self(value.unwrap_or_default()))
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HostOptions {
pub sort_order: i64,
pub remark: String,
pub server_description: String,
pub is_disabled: bool,
pub is_hidden: bool,
#[serde(deserialize_with = "deserialize_null_default")]
pub tags: Vec<String>,
pub port: u16,
pub security: HostSecurity,
pub sni: String,
pub host_header: String,
pub path: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub alpn: Vec<String>,
pub fingerprint: String,
pub override_sni_from_address: bool,
pub keep_sni_blank: bool,
#[serde(deserialize_with = "deserialize_null_default")]
pub pinned_peer_cert_sha256: Vec<String>,
pub verify_peer_cert_by_name: String,
pub allow_insecure: bool,
pub ech_config_list: String,
pub mux_params: HostJsonOverride,
pub sockopt_params: HostJsonOverride,
pub final_mask: HostJsonOverride,
pub vless_route: VlessRoute,
#[serde(deserialize_with = "deserialize_null_default")]
pub exclude_from_sub_types: Vec<SubscriptionFormat>,
#[serde(deserialize_with = "deserialize_null_default")]
pub node_guids: Vec<String>,
#[serde(rename = "mihomoIpVersion")]
pub mihomo_ip_version: MihomoIpVersion,
pub mihomo_x25519: bool,
pub shuffle_host: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HostGroup {
pub group_id: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub inbound_ids: Vec<i64>,
#[serde(deserialize_with = "deserialize_null_default")]
pub hosts: Vec<String>,
#[serde(flatten)]
pub options: HostOptions,
}
impl HostGroup {
pub fn new(inbound_ids: Vec<i64>, remark: impl Into<String>) -> Self {
Self {
inbound_ids,
options: HostOptions {
remark: remark.into(),
..HostOptions::default()
},
..Self::default()
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HostRow {
pub id: i64,
pub group_id: String,
pub inbound_id: i64,
pub address: String,
#[serde(flatten)]
pub options: HostOptions,
pub created_at: i64,
pub updated_at: i64,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn extensible_host_enums_round_trip_known_and_future_values() {
for value in [
HostSecurity::Same,
HostSecurity::Tls,
HostSecurity::None,
HostSecurity::Reality,
HostSecurity::Other("future-security".into()),
] {
let wire = value.as_str().to_owned();
assert_eq!(serde_json::to_value(&value).unwrap(), wire);
assert_eq!(
serde_json::from_value::<HostSecurity>(json!(wire)).unwrap(),
value
);
}
assert_eq!(
serde_json::from_value::<HostSecurity>(json!("")).unwrap(),
HostSecurity::Same
);
for value in [
MihomoIpVersion::Inherit,
MihomoIpVersion::Dual,
MihomoIpVersion::Ipv4,
MihomoIpVersion::Ipv6,
MihomoIpVersion::Ipv4Prefer,
MihomoIpVersion::Ipv6Prefer,
MihomoIpVersion::Other("future-ip-mode".into()),
] {
let wire = value.as_str().to_owned();
assert_eq!(serde_json::to_value(&value).unwrap(), wire);
assert_eq!(
serde_json::from_value::<MihomoIpVersion>(json!(wire)).unwrap(),
value
);
}
for value in [
SubscriptionFormat::Raw,
SubscriptionFormat::Json,
SubscriptionFormat::Clash,
SubscriptionFormat::Other("future-format".into()),
] {
let wire = value.as_str().to_owned();
assert_eq!(serde_json::to_value(&value).unwrap(), wire);
assert_eq!(
serde_json::from_value::<SubscriptionFormat>(json!(wire)).unwrap(),
value
);
}
}
#[test]
fn vless_route_accepts_every_upstream_representation() {
let configured = VlessRoute::new(8443);
assert_eq!(configured.port(), Some(8443));
assert_eq!(VlessRoute::from(443).port(), Some(443));
assert_eq!(serde_json::to_value(configured).unwrap(), "8443");
assert_eq!(serde_json::to_value(VlessRoute::default()).unwrap(), "");
assert_eq!(
serde_json::from_value::<VlessRoute>(json!(2053))
.unwrap()
.port(),
Some(2053)
);
assert_eq!(
serde_json::from_value::<VlessRoute>(json!("2087"))
.unwrap()
.port(),
Some(2087)
);
assert_eq!(
serde_json::from_value::<VlessRoute>(Value::Null)
.unwrap()
.port(),
None
);
assert!(serde_json::from_value::<VlessRoute>(json!(-1)).is_err());
assert!(serde_json::from_value::<VlessRoute>(json!(70_000)).is_err());
assert!(serde_json::from_value::<VlessRoute>(json!("invalid")).is_err());
}
#[test]
fn nested_json_override_preserves_data_but_redacts_debug() {
let value = json!({"password": "host-json-secret"});
let configured = HostJsonOverride::from_value(&value).unwrap();
assert!(!configured.is_empty());
assert_eq!(configured.value().unwrap(), Some(value));
assert!(configured.as_raw().contains("host-json-secret"));
assert_eq!(
format!("{configured:?}"),
"HostJsonOverride(\"[CONFIGURED]\")"
);
let empty = HostJsonOverride::empty();
assert!(empty.is_empty());
assert_eq!(empty.value().unwrap(), None);
assert_eq!(format!("{empty:?}"), "HostJsonOverride(\"[EMPTY]\")");
assert_eq!(
serde_json::from_value::<HostJsonOverride>(Value::Null).unwrap(),
empty
);
let invalid: HostJsonOverride = serde_json::from_value(json!("not-json")).unwrap();
assert!(invalid.value().is_err());
}
#[test]
fn host_group_constructor_sets_only_safe_defaults() {
let group = HostGroup::new(vec![7, 9], "production");
assert_eq!(group.inbound_ids, [7, 9]);
assert_eq!(group.options.remark, "production");
assert_eq!(group.options.security, HostSecurity::Same);
assert!(group.group_id.is_empty());
assert!(group.hosts.is_empty());
}
}