sub_converter/
api.rs

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
9/// Automatically detect the format of input content
10pub 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}