1use crate::emit::{Emitter, clash::ClashEmitter, sing_box::SingBoxEmitter};
2use crate::error::{Error, Result};
3use crate::merge::merge_subscriptions;
4use crate::parse::uri::UriListParser;
5use crate::parse::{Parser, clash::ClashParser, sing_box::SingBoxParser};
6use crate::template::Template;
7use base64::Engine;
8
9pub fn detect_format(content: &str) -> Result<InputFormat> {
11 let trimmed = content.trim();
12
13 if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(trimmed)
14 && let Ok(decoded_str) = String::from_utf8(decoded)
15 && is_uri_list_format(&decoded_str)
16 {
17 return Ok(InputFormat::UriListBase64);
18 }
19
20 let json_like = trimmed.trim_start().starts_with('{');
21 if json_like {
22 if let Ok(v) = serde_json::from_str::<serde_json::Value>(trimmed) {
23 if v.get("proxies").is_some() {
24 return Ok(InputFormat::ClashJson);
25 }
26 if v.get("outbounds").is_some() {
27 return Ok(InputFormat::SingBoxJson);
28 }
29 }
30 } else {
31 if trimmed.contains("proxies:") {
32 return Ok(InputFormat::ClashYaml);
33 }
34 if trimmed.contains("outbounds:") {
35 return Ok(InputFormat::SingBoxYaml);
36 }
37 }
38
39 if is_uri_list_format(trimmed) {
40 return Ok(InputFormat::UriList);
41 }
42
43 Err(Error::ValidationError {
44 reason: "Cannot auto-detect input format, please specify manually".into(),
45 })
46}
47
48fn is_uri_list_format(content: &str) -> bool {
49 let lines: Vec<&str> = content
50 .lines()
51 .map(|l| l.trim())
52 .filter(|l| !l.is_empty())
53 .collect();
54
55 !lines.is_empty() && lines.iter().all(|line| line.contains("://"))
56}
57
58#[derive(Debug, Clone, Copy)]
59pub enum InputFormat {
60 Auto,
61 ClashYaml,
62 ClashJson,
63 SingBoxYaml,
64 SingBoxJson,
65 UriList,
66 UriListBase64,
67}
68
69#[derive(Debug, Clone, Copy)]
70pub enum OutputFormat {
71 ClashYaml,
72 ClashJson,
73 SingBoxYaml,
74 SingBoxJson,
75}
76
77#[derive(Debug, Clone)]
78pub struct InputItem {
79 pub format: InputFormat,
80 pub content: String,
81}
82
83pub fn convert(inputs: Vec<InputItem>, template: Template) -> Result<String> {
84 let mut groups = Vec::with_capacity(inputs.len());
85
86 for (index, item) in inputs.into_iter().enumerate() {
87 let actual_content = match item.format {
88 InputFormat::UriListBase64 => {
89 let bytes = base64::engine::general_purpose::STANDARD
90 .decode(item.content.trim())
91 .map_err(|e| Error::ParseError {
92 detail: format!("base64 decode: {e}"),
93 })?;
94 String::from_utf8(bytes).map_err(|e| Error::ParseError {
95 detail: format!("utf8: {e}"),
96 })?
97 }
98 _ => item.content,
99 };
100
101 let parser: Box<dyn Parser> = match item.format {
102 InputFormat::ClashYaml | InputFormat::ClashJson => Box::new(ClashParser),
103 InputFormat::SingBoxYaml | InputFormat::SingBoxJson => Box::new(SingBoxParser),
104 InputFormat::UriList | InputFormat::UriListBase64 | InputFormat::Auto => {
105 Box::new(UriListParser)
106 }
107 };
108
109 let nodes = parser
110 .parse(&actual_content)
111 .map_err(|e| crate::error::Error::ParseError {
112 detail: format!("Input {}: {}", index + 1, e),
113 })?;
114 groups.push(nodes);
115 }
116
117 let subscription = merge_subscriptions(groups);
118
119 match template.target() {
120 OutputFormat::ClashYaml | OutputFormat::ClashJson => {
121 ClashEmitter.emit(subscription, template)
122 }
123 OutputFormat::SingBoxYaml | OutputFormat::SingBoxJson => {
124 SingBoxEmitter.emit(subscription, template)
125 }
126 }
127}