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
8pub fn detect_format(content: &str) -> Result<InputFormat> {
10 let trimmed = content.trim();
11
12 if is_clash_format(trimmed) {
14 return Ok(InputFormat::Clash);
15 }
16
17 if is_singbox_format(trimmed) {
19 return Ok(InputFormat::SingBox);
20 }
21
22 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 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}