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;
7
8/// Automatically detect the format of input content
9pub fn detect_format(content: &str) -> Result<InputFormat> {
10    let trimmed = content.trim();
11
12    // Try to detect Clash YAML format
13    if is_clash_format(trimmed) {
14        return Ok(InputFormat::Clash);
15    }
16
17    // Try to detect SingBox JSON format
18    if is_singbox_format(trimmed) {
19        return Ok(InputFormat::SingBox);
20    }
21
22    // Try to detect URI list format
23    if is_uri_list_format(trimmed) {
24        return Ok(InputFormat::UriList);
25    }
26
27    Err(Error::ValidationError {
28        reason: "Cannot auto-detect input format, please specify manually".into(),
29    })
30}
31
32fn is_clash_format(content: &str) -> bool {
33    content.contains("proxies:")
34}
35
36fn is_singbox_format(content: &str) -> bool {
37    if !content.starts_with('{') || !content.contains("outbounds") {
38        return false;
39    }
40    // Try to parse as JSON to confirm
41    serde_json::from_str::<serde_json::Value>(content).is_ok()
42}
43
44fn is_uri_list_format(content: &str) -> bool {
45    let lines: Vec<&str> = content
46        .lines()
47        .map(|l| l.trim())
48        .filter(|l| !l.is_empty())
49        .collect();
50
51    !lines.is_empty() && lines.iter().all(|line| line.contains("://"))
52}
53
54#[derive(Debug, Clone, Copy)]
55pub enum InputFormat {
56    Clash,
57    SingBox,
58    UriList,
59}
60
61#[derive(Debug, Clone, Copy)]
62pub enum OutputFormat {
63    Clash,
64    SingBox,
65}
66
67#[derive(Debug, Clone)]
68pub struct InputItem {
69    pub format: InputFormat,
70    pub content: String,
71}
72
73pub fn convert(inputs: Vec<InputItem>, template: Template) -> Result<String> {
74    let mut groups = Vec::with_capacity(inputs.len());
75
76    for (index, item) in inputs.into_iter().enumerate() {
77        let parser: Box<dyn Parser> = match item.format {
78            InputFormat::Clash => Box::new(ClashParser),
79            InputFormat::SingBox => Box::new(SingBoxParser),
80            InputFormat::UriList => Box::new(UriListParser),
81        };
82
83        let nodes = parser
84            .parse(&item.content)
85            .map_err(|e| crate::error::Error::ParseError {
86                detail: format!("Input {}: {}", index + 1, e),
87            })?;
88        groups.push(nodes);
89    }
90
91    let subscription = merge_subscriptions(groups);
92
93    match template.target() {
94        OutputFormat::Clash => ClashEmitter.emit(subscription, template),
95        OutputFormat::SingBox => SingBoxEmitter.emit(subscription, template),
96    }
97}