1#![allow(clippy::struct_excessive_bools)]
2
3use std::fmt;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
6use serde_json::Value;
7
8fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
9where
10 D: Deserializer<'de>,
11 T: Deserialize<'de> + Default,
12{
13 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
14}
15
16#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
18#[non_exhaustive]
19pub enum HostSecurity {
20 #[default]
22 Same,
23 Tls,
25 None,
27 Reality,
29 Other(String),
31}
32
33impl HostSecurity {
34 pub fn as_str(&self) -> &str {
36 match self {
37 Self::Same => "same",
38 Self::Tls => "tls",
39 Self::None => "none",
40 Self::Reality => "reality",
41 Self::Other(value) => value,
42 }
43 }
44}
45
46impl Serialize for HostSecurity {
47 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48 where
49 S: Serializer,
50 {
51 serializer.serialize_str(self.as_str())
52 }
53}
54
55impl<'de> Deserialize<'de> for HostSecurity {
56 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57 where
58 D: Deserializer<'de>,
59 {
60 let value = String::deserialize(deserializer)?;
61 Ok(match value.as_str() {
62 "" | "same" => Self::Same,
63 "tls" => Self::Tls,
64 "none" => Self::None,
65 "reality" => Self::Reality,
66 _ => Self::Other(value),
67 })
68 }
69}
70
71#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
73#[non_exhaustive]
74pub enum MihomoIpVersion {
75 #[default]
77 Inherit,
78 Dual,
80 Ipv4,
82 Ipv6,
84 Ipv4Prefer,
86 Ipv6Prefer,
88 Other(String),
90}
91
92impl MihomoIpVersion {
93 pub fn as_str(&self) -> &str {
95 match self {
96 Self::Inherit => "",
97 Self::Dual => "dual",
98 Self::Ipv4 => "ipv4",
99 Self::Ipv6 => "ipv6",
100 Self::Ipv4Prefer => "ipv4-prefer",
101 Self::Ipv6Prefer => "ipv6-prefer",
102 Self::Other(value) => value,
103 }
104 }
105}
106
107impl Serialize for MihomoIpVersion {
108 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
109 where
110 S: Serializer,
111 {
112 serializer.serialize_str(self.as_str())
113 }
114}
115
116impl<'de> Deserialize<'de> for MihomoIpVersion {
117 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118 where
119 D: Deserializer<'de>,
120 {
121 let value = String::deserialize(deserializer)?;
122 Ok(match value.as_str() {
123 "" => Self::Inherit,
124 "dual" => Self::Dual,
125 "ipv4" => Self::Ipv4,
126 "ipv6" => Self::Ipv6,
127 "ipv4-prefer" => Self::Ipv4Prefer,
128 "ipv6-prefer" => Self::Ipv6Prefer,
129 _ => Self::Other(value),
130 })
131 }
132}
133
134#[derive(Clone, Debug, Eq, Hash, PartialEq)]
136#[non_exhaustive]
137pub enum SubscriptionFormat {
138 Raw,
140 Json,
142 Clash,
144 Other(String),
146}
147
148impl SubscriptionFormat {
149 pub fn as_str(&self) -> &str {
151 match self {
152 Self::Raw => "raw",
153 Self::Json => "json",
154 Self::Clash => "clash",
155 Self::Other(value) => value,
156 }
157 }
158}
159
160impl Serialize for SubscriptionFormat {
161 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162 where
163 S: Serializer,
164 {
165 serializer.serialize_str(self.as_str())
166 }
167}
168
169impl<'de> Deserialize<'de> for SubscriptionFormat {
170 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171 where
172 D: Deserializer<'de>,
173 {
174 let value = String::deserialize(deserializer)?;
175 Ok(match value.as_str() {
176 "raw" => Self::Raw,
177 "json" => Self::Json,
178 "clash" => Self::Clash,
179 _ => Self::Other(value),
180 })
181 }
182}
183
184#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
186pub struct VlessRoute(Option<u16>);
187
188impl VlessRoute {
189 pub const fn new(port: u16) -> Self {
191 Self(Some(port))
192 }
193
194 pub const fn port(self) -> Option<u16> {
196 self.0
197 }
198}
199
200impl From<u16> for VlessRoute {
201 fn from(port: u16) -> Self {
202 Self::new(port)
203 }
204}
205
206impl Serialize for VlessRoute {
207 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208 where
209 S: Serializer,
210 {
211 match self.0 {
212 Some(port) => serializer.serialize_str(&port.to_string()),
213 None => serializer.serialize_str(""),
214 }
215 }
216}
217
218impl<'de> Deserialize<'de> for VlessRoute {
219 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
220 where
221 D: Deserializer<'de>,
222 {
223 struct Visitor;
224
225 impl de::Visitor<'_> for Visitor {
226 type Value = VlessRoute;
227
228 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229 formatter.write_str("an empty string or a port from 0 through 65535")
230 }
231
232 fn visit_none<E>(self) -> Result<Self::Value, E> {
233 Ok(VlessRoute::default())
234 }
235
236 fn visit_unit<E>(self) -> Result<Self::Value, E> {
237 Ok(VlessRoute::default())
238 }
239
240 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
241 where
242 E: de::Error,
243 {
244 u16::try_from(value)
245 .map(VlessRoute::new)
246 .map_err(|_| E::custom("VLESS route is outside the port range"))
247 }
248
249 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
250 where
251 E: de::Error,
252 {
253 u16::try_from(value)
254 .map(VlessRoute::new)
255 .map_err(|_| E::custom("VLESS route is outside the port range"))
256 }
257
258 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
259 where
260 E: de::Error,
261 {
262 if value.is_empty() {
263 return Ok(VlessRoute::default());
264 }
265 value.parse::<u16>().map(VlessRoute::new).map_err(E::custom)
266 }
267 }
268
269 deserializer.deserialize_any(Visitor)
270 }
271}
272
273#[derive(Clone, Default, Eq, PartialEq)]
279pub struct HostJsonOverride(String);
280
281impl HostJsonOverride {
282 pub fn from_value(value: &Value) -> serde_json::Result<Self> {
288 serde_json::to_string(value).map(Self)
289 }
290
291 pub const fn empty() -> Self {
293 Self(String::new())
294 }
295
296 pub fn as_raw(&self) -> &str {
298 &self.0
299 }
300
301 pub fn is_empty(&self) -> bool {
303 self.0.is_empty()
304 }
305
306 pub fn value(&self) -> serde_json::Result<Option<Value>> {
312 if self.is_empty() {
313 Ok(None)
314 } else {
315 serde_json::from_str(&self.0).map(Some)
316 }
317 }
318}
319
320impl fmt::Debug for HostJsonOverride {
321 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
322 formatter
323 .debug_tuple("HostJsonOverride")
324 .field(&if self.is_empty() {
325 "[EMPTY]"
326 } else {
327 "[CONFIGURED]"
328 })
329 .finish()
330 }
331}
332
333impl Serialize for HostJsonOverride {
334 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
335 where
336 S: Serializer,
337 {
338 serializer.serialize_str(&self.0)
339 }
340}
341
342impl<'de> Deserialize<'de> for HostJsonOverride {
343 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
344 where
345 D: Deserializer<'de>,
346 {
347 Option::<String>::deserialize(deserializer).map(|value| Self(value.unwrap_or_default()))
348 }
349}
350
351#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
353#[serde(default, rename_all = "camelCase")]
354pub struct HostOptions {
355 pub sort_order: i64,
357 pub remark: String,
359 pub server_description: String,
361 pub is_disabled: bool,
363 pub is_hidden: bool,
365 #[serde(deserialize_with = "deserialize_null_default")]
367 pub tags: Vec<String>,
368 pub port: u16,
370 pub security: HostSecurity,
372 pub sni: String,
374 pub host_header: String,
376 pub path: String,
378 #[serde(deserialize_with = "deserialize_null_default")]
380 pub alpn: Vec<String>,
381 pub fingerprint: String,
383 pub override_sni_from_address: bool,
385 pub keep_sni_blank: bool,
387 #[serde(deserialize_with = "deserialize_null_default")]
389 pub pinned_peer_cert_sha256: Vec<String>,
390 pub verify_peer_cert_by_name: String,
392 pub allow_insecure: bool,
394 pub ech_config_list: String,
396 pub mux_params: HostJsonOverride,
398 pub sockopt_params: HostJsonOverride,
400 pub final_mask: HostJsonOverride,
402 pub vless_route: VlessRoute,
404 #[serde(deserialize_with = "deserialize_null_default")]
406 pub exclude_from_sub_types: Vec<SubscriptionFormat>,
407 #[serde(deserialize_with = "deserialize_null_default")]
409 pub node_guids: Vec<String>,
410 #[serde(rename = "mihomoIpVersion")]
412 pub mihomo_ip_version: MihomoIpVersion,
413 pub mihomo_x25519: bool,
415 pub shuffle_host: bool,
417}
418
419#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
423#[serde(default, rename_all = "camelCase")]
424pub struct HostGroup {
425 pub group_id: String,
427 #[serde(deserialize_with = "deserialize_null_default")]
429 pub inbound_ids: Vec<i64>,
430 #[serde(deserialize_with = "deserialize_null_default")]
432 pub hosts: Vec<String>,
433 #[serde(flatten)]
435 pub options: HostOptions,
436}
437
438impl HostGroup {
439 pub fn new(inbound_ids: Vec<i64>, remark: impl Into<String>) -> Self {
441 Self {
442 inbound_ids,
443 options: HostOptions {
444 remark: remark.into(),
445 ..HostOptions::default()
446 },
447 ..Self::default()
448 }
449 }
450}
451
452#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
454#[serde(default, rename_all = "camelCase")]
455pub struct HostRow {
456 pub id: i64,
458 pub group_id: String,
460 pub inbound_id: i64,
462 pub address: String,
464 #[serde(flatten)]
466 pub options: HostOptions,
467 pub created_at: i64,
469 pub updated_at: i64,
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use serde_json::json;
477
478 #[test]
479 fn extensible_host_enums_round_trip_known_and_future_values() {
480 for value in [
481 HostSecurity::Same,
482 HostSecurity::Tls,
483 HostSecurity::None,
484 HostSecurity::Reality,
485 HostSecurity::Other("future-security".into()),
486 ] {
487 let wire = value.as_str().to_owned();
488 assert_eq!(serde_json::to_value(&value).unwrap(), wire);
489 assert_eq!(
490 serde_json::from_value::<HostSecurity>(json!(wire)).unwrap(),
491 value
492 );
493 }
494 assert_eq!(
495 serde_json::from_value::<HostSecurity>(json!("")).unwrap(),
496 HostSecurity::Same
497 );
498
499 for value in [
500 MihomoIpVersion::Inherit,
501 MihomoIpVersion::Dual,
502 MihomoIpVersion::Ipv4,
503 MihomoIpVersion::Ipv6,
504 MihomoIpVersion::Ipv4Prefer,
505 MihomoIpVersion::Ipv6Prefer,
506 MihomoIpVersion::Other("future-ip-mode".into()),
507 ] {
508 let wire = value.as_str().to_owned();
509 assert_eq!(serde_json::to_value(&value).unwrap(), wire);
510 assert_eq!(
511 serde_json::from_value::<MihomoIpVersion>(json!(wire)).unwrap(),
512 value
513 );
514 }
515
516 for value in [
517 SubscriptionFormat::Raw,
518 SubscriptionFormat::Json,
519 SubscriptionFormat::Clash,
520 SubscriptionFormat::Other("future-format".into()),
521 ] {
522 let wire = value.as_str().to_owned();
523 assert_eq!(serde_json::to_value(&value).unwrap(), wire);
524 assert_eq!(
525 serde_json::from_value::<SubscriptionFormat>(json!(wire)).unwrap(),
526 value
527 );
528 }
529 }
530
531 #[test]
532 fn vless_route_accepts_every_upstream_representation() {
533 let configured = VlessRoute::new(8443);
534 assert_eq!(configured.port(), Some(8443));
535 assert_eq!(VlessRoute::from(443).port(), Some(443));
536 assert_eq!(serde_json::to_value(configured).unwrap(), "8443");
537 assert_eq!(serde_json::to_value(VlessRoute::default()).unwrap(), "");
538 assert_eq!(
539 serde_json::from_value::<VlessRoute>(json!(2053))
540 .unwrap()
541 .port(),
542 Some(2053)
543 );
544 assert_eq!(
545 serde_json::from_value::<VlessRoute>(json!("2087"))
546 .unwrap()
547 .port(),
548 Some(2087)
549 );
550 assert_eq!(
551 serde_json::from_value::<VlessRoute>(Value::Null)
552 .unwrap()
553 .port(),
554 None
555 );
556 assert!(serde_json::from_value::<VlessRoute>(json!(-1)).is_err());
557 assert!(serde_json::from_value::<VlessRoute>(json!(70_000)).is_err());
558 assert!(serde_json::from_value::<VlessRoute>(json!("invalid")).is_err());
559 }
560
561 #[test]
562 fn nested_json_override_preserves_data_but_redacts_debug() {
563 let value = json!({"password": "host-json-secret"});
564 let configured = HostJsonOverride::from_value(&value).unwrap();
565 assert!(!configured.is_empty());
566 assert_eq!(configured.value().unwrap(), Some(value));
567 assert!(configured.as_raw().contains("host-json-secret"));
568 assert_eq!(
569 format!("{configured:?}"),
570 "HostJsonOverride(\"[CONFIGURED]\")"
571 );
572
573 let empty = HostJsonOverride::empty();
574 assert!(empty.is_empty());
575 assert_eq!(empty.value().unwrap(), None);
576 assert_eq!(format!("{empty:?}"), "HostJsonOverride(\"[EMPTY]\")");
577 assert_eq!(
578 serde_json::from_value::<HostJsonOverride>(Value::Null).unwrap(),
579 empty
580 );
581 let invalid: HostJsonOverride = serde_json::from_value(json!("not-json")).unwrap();
582 assert!(invalid.value().is_err());
583 }
584
585 #[test]
586 fn host_group_constructor_sets_only_safe_defaults() {
587 let group = HostGroup::new(vec![7, 9], "production");
588 assert_eq!(group.inbound_ids, [7, 9]);
589 assert_eq!(group.options.remark, "production");
590 assert_eq!(group.options.security, HostSecurity::Same);
591 assert!(group.group_id.is_empty());
592 assert!(group.hosts.is_empty());
593 }
594}