1pub mod builder;
25pub mod error;
26pub mod fetch;
27pub mod i18n;
28pub mod parser;
29pub mod server;
30pub mod types;
31pub mod utils;
32
33pub use builder::{build_config, to_yaml};
34pub use error::{ProxyError, Result};
35pub use fetch::{deduplicate_names, fetch_subscription};
36pub use i18n::{get_lang, t, t_simple, t_with_fallback, Lang};
37pub use parser::{parse_lines, parse_uri};
38pub use server::{start_server, ServerHandle, ServerOptions};
39pub use types::{
40 ClashConfig, ConvertOptions, ConvertResult, ProxyGroup, ProxyNode, ProxyType, RuleMode,
41};
42
43use builder::{build_config as build, to_yaml as serialize_yaml};
44use fetch::{deduplicate_names as dedup, fetch_subscription as fetch};
45use i18n::t as translate;
46use parser::parse_lines as parse;
47
48pub async fn convert(url: &str, opts: Option<ConvertOptions>) -> Result<ConvertResult> {
50 let opts = opts.unwrap_or_default();
51 let lines = fetch(url).await?;
52 convert_from_parsed_lines(lines, opts, "err_no_nodes_subscription")
53}
54
55pub fn convert_from_lines(lines: &[impl AsRef<str>], opts: Option<ConvertOptions>) -> Result<ConvertResult> {
57 let lines: Vec<String> = lines.iter().map(|l| l.as_ref().to_string()).collect();
58 convert_from_parsed_lines(lines, opts.unwrap_or_default(), "err_no_nodes_input")
59}
60
61fn convert_from_parsed_lines(
62 lines: Vec<String>,
63 opts: ConvertOptions,
64 empty_err_key: &str,
65) -> Result<ConvertResult> {
66 let mut nodes = parse(&lines);
67
68 if nodes.is_empty() {
69 return Err(ProxyError::msg(translate(empty_err_key, &[])));
70 }
71
72 if opts.deduplicate {
73 dedup(&mut nodes);
74 }
75
76 let config = build(&nodes, opts.rules);
77 let yaml = serialize_yaml(&config)?;
78
79 Ok(ConvertResult {
80 config,
81 yaml,
82 nodes,
83 })
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn convert_from_lines_returns_yaml_config_nodes() {
92 let lines = vec![
93 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#US-01".to_string(),
94 "trojan://pass@sg.example.com:443?security=tls&sni=sg.example.com#SG-01".to_string(),
95 ];
96 let result = convert_from_lines(&lines, None).unwrap();
97 assert_eq!(result.nodes.len(), 2);
98 assert!(result.yaml.contains("proxies:"));
99 assert!(result.yaml.contains("US-01"));
100 }
101
102 #[test]
103 fn convert_from_lines_deduplicates_by_default() {
104 let lines = vec![
105 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@a.com:443#Node".to_string(),
106 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@b.com:443#Node".to_string(),
107 ];
108 let result = convert_from_lines(&lines, None).unwrap();
109 assert_eq!(result.nodes[0].name, "Node");
110 assert_eq!(result.nodes[1].name, "Node-2");
111 }
112
113 #[test]
114 fn convert_from_lines_supports_external_rules() {
115 let lines = vec![
116 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#US-01".to_string(),
117 ];
118 let result = convert_from_lines(
119 &lines,
120 Some(ConvertOptions {
121 rules: RuleMode::External,
122 deduplicate: true,
123 }),
124 )
125 .unwrap();
126 assert!(result.config.rules[0].starts_with("RULE-SET"));
127 }
128
129 #[test]
130 fn convert_from_lines_throws_on_empty() {
131 assert!(convert_from_lines(&[] as &[String], None).is_err());
132 }
133
134 #[test]
135 fn convert_from_lines_yaml_contains_groups_and_rules() {
136 let lines = vec![
137 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#US-01".to_string(),
138 "trojan://pass@sg.example.com:443?security=tls&sni=sg.example.com#SG-01".to_string(),
139 ];
140 let result = convert_from_lines(&lines, None).unwrap();
141 assert!(result.yaml.contains("proxy-groups:"));
142 assert!(result.yaml.contains("rules:"));
143 assert!(result.yaml.contains("SG-01"));
144 }
145
146 #[test]
147 fn convert_from_lines_skips_dedup_when_false() {
148 let lines = vec![
149 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@a.com:443#Node".to_string(),
150 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@b.com:443#Node".to_string(),
151 ];
152 let result = convert_from_lines(
153 &lines,
154 Some(ConvertOptions {
155 rules: RuleMode::Builtin,
156 deduplicate: false,
157 }),
158 )
159 .unwrap();
160 assert_eq!(result.nodes[0].name, "Node");
161 assert_eq!(result.nodes[1].name, "Node");
162 }
163
164 #[test]
165 fn convert_from_lines_defaults_to_builtin_rules() {
166 let lines = vec![
167 "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#US-01".to_string(),
168 ];
169 let result = convert_from_lines(&lines, None).unwrap();
170 assert!(result.config.rules[0].starts_with("DOMAIN-SUFFIX"));
171 }
172
173 #[test]
174 fn convert_from_lines_throws_when_all_unparseable() {
175 assert!(convert_from_lines(&["invalid://line".to_string()], None).is_err());
176 }
177
178 #[test]
179 fn parse_uri_and_build_config_to_yaml() {
180 let node = parse_uri("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@host:8388#Test").unwrap();
181 let config = build_config(&[node], RuleMode::Builtin);
182 let yaml = to_yaml(&config).unwrap();
183 assert!(yaml.contains("Test"));
184 }
185}