Skip to main content

rinova_proxy_sdk/
parser.rs

1use crate::error::{ProxyError, Result};
2use crate::i18n::t;
3use crate::types::{ProxyNode, ProxyType};
4use crate::utils::{decode_base64_utf8, safe_decode_uri};
5use indexmap::IndexMap;
6use serde_yaml::Value as YamlValue;
7
8fn parse_ss(uri: &str) -> Result<ProxyNode> {
9    let without_scheme = &uri[5..];
10
11    let hash_idx = without_scheme.rfind('#');
12    let raw_name = match hash_idx {
13        Some(idx) => safe_decode_uri(Some(&without_scheme[idx + 1..])),
14        None => String::new(),
15    };
16    let body = match hash_idx {
17        Some(idx) => &without_scheme[..idx],
18        None => without_scheme,
19    };
20
21    if let Some(at_idx) = body.find('@') {
22        let b64_part = &body[..at_idx];
23        let host_port = &body[at_idx + 1..];
24        let (server, port) = split_host_port(host_port).ok_or_else(|| {
25            ProxyError::msg(t("err_ss_address", &[("host", host_port)]))
26        })?;
27
28        let decoded = decode_base64_utf8(b64_part).map_err(|_| {
29            ProxyError::msg(t("err_ss_sip002_credentials", &[("raw", b64_part)]))
30        })?;
31
32        if let Some(at_in_decoded) = decoded.find('@') {
33            let cred = &decoded[..at_in_decoded];
34            let (method, password) = split_cred(cred).ok_or_else(|| {
35                ProxyError::msg(t(
36                    "err_ss_sip002_credentials",
37                    &[("raw", cred)],
38                ))
39            })?;
40            let mut node = ProxyNode::new(
41                if raw_name.is_empty() {
42                    server.clone()
43                } else {
44                    raw_name
45                },
46                ProxyType::Ss,
47                server,
48                port,
49            );
50            node.set_str("cipher", method);
51            node.set_str("password", password);
52            return Ok(node);
53        }
54
55        let colon_idx = decoded.find(':').ok_or_else(|| {
56            ProxyError::msg(t(
57                "err_ss_sip002_credentials",
58                &[("raw", &decoded)],
59            ))
60        })?;
61        let mut node = ProxyNode::new(
62            if raw_name.is_empty() {
63                server.clone()
64            } else {
65                raw_name
66            },
67            ProxyType::Ss,
68            server,
69            port,
70        );
71        node.set_str("cipher", &decoded[..colon_idx]);
72        node.set_str("password", &decoded[colon_idx + 1..]);
73        return Ok(node);
74    }
75
76    let decoded = decode_base64_utf8(body).map_err(|_| {
77        ProxyError::msg(t("err_ss_legacy_no_at", &[("raw", body)]))
78    })?;
79    let at_in_decoded = decoded.find('@').ok_or_else(|| {
80        ProxyError::msg(t("err_ss_legacy_no_at", &[("raw", &decoded)]))
81    })?;
82    let cred = &decoded[..at_in_decoded];
83    let host_port = &decoded[at_in_decoded + 1..];
84    let (server, port) = split_host_port(host_port).ok_or_else(|| {
85        ProxyError::msg(t("err_ss_legacy_address", &[("host", host_port)]))
86    })?;
87    let (method, password) = split_cred(cred).ok_or_else(|| {
88        ProxyError::msg(t("err_ss_legacy_no_method", &[("raw", cred)]))
89    })?;
90
91    let mut node = ProxyNode::new(
92        if raw_name.is_empty() {
93            server.clone()
94        } else {
95            raw_name
96        },
97        ProxyType::Ss,
98        server,
99        port,
100    );
101    node.set_str("cipher", method);
102    node.set_str("password", password);
103    Ok(node)
104}
105
106fn indexmap_to_yaml_mapping(map: IndexMap<String, YamlValue>) -> YamlValue {
107    let mut mapping = serde_yaml::Mapping::new();
108    for (k, v) in map {
109        mapping.insert(YamlValue::String(k), v);
110    }
111    YamlValue::Mapping(mapping)
112}
113
114fn split_host_port(host_port: &str) -> Option<(String, u16)> {
115    let (server, port_str) = host_port.rsplit_once(':')?;
116    let port = port_str.parse().ok()?;
117    if server.is_empty() {
118        return None;
119    }
120    Some((server.to_string(), port))
121}
122
123fn split_cred(cred: &str) -> Option<(String, String)> {
124    let colon_idx = cred.find(':')?;
125    let method = cred[..colon_idx].trim();
126    if method.is_empty() {
127        return None;
128    }
129    Some((method.to_string(), cred[colon_idx + 1..].to_string()))
130}
131
132fn vmess_field_str(obj: &serde_json::Value, key: &str) -> Option<String> {
133    match obj.get(key)? {
134        serde_json::Value::String(s) => Some(s.clone()),
135        serde_json::Value::Number(n) => Some(n.to_string()),
136        serde_json::Value::Bool(b) => Some(b.to_string()),
137        _ => None,
138    }
139}
140
141fn vmess_field_u16(obj: &serde_json::Value, key: &str) -> Option<u16> {
142    match obj.get(key)? {
143        serde_json::Value::Number(n) => n.as_u64().and_then(|v| u16::try_from(v).ok()),
144        serde_json::Value::String(s) => s.parse().ok(),
145        _ => None,
146    }
147}
148
149fn parse_vmess(uri: &str) -> Result<ProxyNode> {
150    let b64 = &uri[8..];
151    let json_str = decode_base64_utf8(b64).map_err(|_| ProxyError::msg(t("err_vmess_parse", &[])))?;
152    let obj: serde_json::Value =
153        serde_json::from_str(&json_str).map_err(|_| ProxyError::msg(t("err_vmess_parse", &[])))?;
154
155    let ps = vmess_field_str(&obj, "ps");
156    let remarks = vmess_field_str(&obj, "remarks");
157    let name = {
158        let raw = safe_decode_uri(ps.as_deref().or(remarks.as_deref()));
159        if raw.is_empty() {
160            "Unnamed".to_string()
161        } else {
162            raw
163        }
164    };
165    let ws_host = vmess_field_str(&obj, "host");
166    let server = vmess_field_str(&obj, "add")
167        .or_else(|| vmess_field_str(&obj, "host"))
168        .unwrap_or_default();
169    let port = vmess_field_u16(&obj, "port").unwrap_or(0);
170
171    if server.is_empty() || port == 0 {
172        return Err(ProxyError::msg(t("err_vmess_no_server", &[])));
173    }
174
175    let mut node = ProxyNode::new(name, ProxyType::Vmess, server, port);
176    if let Some(uuid) = vmess_field_str(&obj, "id") {
177        node.set_str("uuid", uuid);
178    }
179    node.set_u64(
180        "alterId",
181        vmess_field_u16(&obj, "aid").map(u64::from).unwrap_or(0),
182    );
183    node.set_str(
184        "cipher",
185        vmess_field_str(&obj, "scy").as_deref().unwrap_or("auto"),
186    );
187
188    let net = vmess_field_str(&obj, "net").unwrap_or_else(|| "tcp".to_string());
189    match net.as_str() {
190        "ws" => {
191            node.set_str("network", "ws");
192            let mut ws_opts = IndexMap::new();
193            ws_opts.insert(
194                "path".to_string(),
195                YamlValue::String(vmess_field_str(&obj, "path").unwrap_or_else(|| "/".to_string())),
196            );
197            if let Some(host) = ws_host.clone() {
198                let mut headers = IndexMap::new();
199                headers.insert("Host".to_string(), YamlValue::String(host));
200                ws_opts.insert("headers".to_string(), indexmap_to_yaml_mapping(headers));
201            }
202            node.set_value("ws-opts", indexmap_to_yaml_mapping(ws_opts));
203        }
204        "grpc" => {
205            node.set_str("network", "grpc");
206            let mut grpc_opts = IndexMap::new();
207            grpc_opts.insert(
208                "grpc-service-name".to_string(),
209                YamlValue::String(vmess_field_str(&obj, "serviceName").unwrap_or_default()),
210            );
211            node.set_value("grpc-opts", indexmap_to_yaml_mapping(grpc_opts));
212        }
213        "h2" => {
214            node.set_str("network", "h2");
215            let mut h2_opts = IndexMap::new();
216            h2_opts.insert(
217                "path".to_string(),
218                YamlValue::String(vmess_field_str(&obj, "path").unwrap_or_else(|| "/".to_string())),
219            );
220            if let Some(host) = ws_host.clone() {
221                h2_opts.insert("host".to_string(), YamlValue::String(host));
222            }
223            node.set_value("h2-opts", indexmap_to_yaml_mapping(h2_opts));
224        }
225        "quic" => {
226            node.set_str("network", "quic");
227            let mut quic_opts = IndexMap::new();
228            quic_opts.insert(
229                "security".to_string(),
230                YamlValue::String(
231                    vmess_field_str(&obj, "security").unwrap_or_else(|| "none".to_string()),
232                ),
233            );
234            quic_opts.insert(
235                "key".to_string(),
236                YamlValue::String(vmess_field_str(&obj, "key").unwrap_or_default()),
237            );
238            node.set_value("quic-opts", indexmap_to_yaml_mapping(quic_opts));
239        }
240        "kcp" => {
241            node.set_str("network", "kcp");
242            let mut kcp_opts = IndexMap::new();
243            kcp_opts.insert("mtu".to_string(), YamlValue::Number(1350.into()));
244            kcp_opts.insert("mptcp".to_string(), YamlValue::Bool(false));
245            node.set_value("kcp-opts", indexmap_to_yaml_mapping(kcp_opts));
246        }
247        _ => {}
248    }
249
250    let tls = vmess_field_str(&obj, "tls");
251    match tls.as_deref() {
252        Some("tls") => {
253            node.set_bool("tls", true);
254            if let Some(sni) = vmess_field_str(&obj, "sni") {
255                node.set_str("sni", sni);
256            }
257            if let Some(alpn) = vmess_field_str(&obj, "alpn") {
258                let values: Vec<YamlValue> = alpn
259                    .split(',')
260                    .map(|s| YamlValue::String(s.trim().to_string()))
261                    .collect();
262                node.set_value("alpn", YamlValue::Sequence(values));
263            }
264            if let Some(fp) = vmess_field_str(&obj, "fp") {
265                node.set_str("client-fingerprint", fp);
266            }
267            let skip = ["skip-cert-verify", "skip_cert_verify", "allow_insecure", "allowInsecure"]
268                .iter()
269                .filter_map(|key| vmess_field_str(&obj, key))
270                .any(|v| v == "true" || v == "1");
271            if skip {
272                node.set_bool("skip-cert-verify", true);
273            }
274        }
275        Some("none") => {
276            node.set_bool("tls", false);
277            node.set_bool("skip-cert-verify", true);
278        }
279        _ => {}
280    }
281
282    Ok(node)
283}
284
285fn parse_trojan(uri: &str) -> Result<ProxyNode> {
286    let parsed = url::Url::parse(uri)?;
287    let name = {
288        let raw = safe_decode_uri(parsed.fragment());
289        if raw.is_empty() {
290            parsed.host_str().unwrap_or_default().to_string()
291        } else {
292            raw
293        }
294    };
295    let server = parsed.host_str().unwrap_or_default().to_string();
296    let port = parsed.port().unwrap_or(443);
297    let password = if parsed.username().is_empty() {
298        parsed.password().unwrap_or_default().to_string()
299    } else {
300        parsed.username().to_string()
301    };
302
303    let mut node = ProxyNode::new(name, ProxyType::Trojan, server.clone(), port);
304    node.set_str("password", password);
305    node.set_str(
306        "sni",
307        parsed
308            .query_pairs()
309            .find(|(k, _)| k == "sni")
310            .map(|(_, v)| v.into_owned())
311            .unwrap_or(server),
312    );
313
314    let allow_insecure = parsed
315        .query_pairs()
316        .find(|(k, _)| k == "allowInsecure" || k == "allow_insecure")
317        .map(|(_, v)| v.into_owned());
318    if allow_insecure.as_deref() == Some("1") || allow_insecure.as_deref() == Some("true") {
319        node.set_bool("skip-cert-verify", true);
320    }
321
322    Ok(node)
323}
324
325fn parse_hysteria2(uri: &str) -> Result<ProxyNode> {
326    let parsed = url::Url::parse(uri)?;
327    let name = {
328        let raw = safe_decode_uri(parsed.fragment());
329        if raw.is_empty() {
330            parsed.host_str().unwrap_or_default().to_string()
331        } else {
332            raw
333        }
334    };
335    let server = parsed.host_str().unwrap_or_default().to_string();
336    let port = parsed.port().unwrap_or(443);
337    let password = if parsed.username().is_empty() {
338        parsed.password().unwrap_or_default().to_string()
339    } else {
340        parsed.username().to_string()
341    };
342
343    let mut node = ProxyNode::new(name, ProxyType::Hysteria2, server, port);
344    node.set_str("password", password);
345
346    if let Some(sni) = parsed
347        .query_pairs()
348        .find(|(k, _)| k == "sni" || k == "peer")
349        .map(|(_, v)| v.into_owned())
350    {
351        node.set_str("sni", sni);
352    }
353
354    let insecure = parsed
355        .query_pairs()
356        .find(|(k, _)| k == "insecure" || k == "allowInsecure" || k == "skip-cert-verify")
357        .map(|(_, v)| v.into_owned());
358    if insecure.as_deref() == Some("1") || insecure.as_deref() == Some("true") {
359        node.set_bool("skip-cert-verify", true);
360    }
361
362    if let Some(alpn) = parsed
363        .query_pairs()
364        .find(|(k, _)| k == "alpn")
365        .map(|(_, v)| v.into_owned())
366    {
367        let values: Vec<YamlValue> = alpn
368            .split(',')
369            .map(|s| YamlValue::String(s.trim().to_string()))
370            .collect();
371        node.set_value("alpn", YamlValue::Sequence(values));
372    }
373
374    if let Some(up) = parsed
375        .query_pairs()
376        .find(|(k, _)| k == "up" || k == "upload")
377        .and_then(|(_, v)| v.parse::<u64>().ok())
378    {
379        node.set_u64("up", up);
380    }
381
382    if let Some(down) = parsed
383        .query_pairs()
384        .find(|(k, _)| k == "down" || k == "download")
385        .and_then(|(_, v)| v.parse::<u64>().ok())
386    {
387        node.set_u64("down", down);
388    }
389
390    Ok(node)
391}
392
393pub fn parse_uri(uri: &str) -> Result<ProxyNode> {
394    if let Some(rest) = uri.strip_prefix("ss://") {
395        return parse_ss(&format!("ss://{rest}"));
396    }
397    if let Some(rest) = uri.strip_prefix("vmess://") {
398        return parse_vmess(&format!("vmess://{rest}"));
399    }
400    if uri.starts_with("trojan://") {
401        return parse_trojan(uri);
402    }
403    if uri.starts_with("hysteria2://") || uri.starts_with("hy2://") {
404        return parse_hysteria2(uri);
405    }
406
407    let prefix = uri.chars().take(10).collect::<String>();
408    Err(ProxyError::msg(t(
409        "err_unsupported_protocol",
410        &[("prefix", &prefix)],
411    )))
412}
413
414pub fn parse_lines(lines: &[impl AsRef<str>]) -> Vec<ProxyNode> {
415    let mut nodes = Vec::new();
416    for line in lines {
417        match parse_uri(line.as_ref()) {
418            Ok(node) => nodes.push(node),
419            Err(err) => {
420                eprintln!(
421                    "⚠️  {}",
422                    t("skip_node", &[("msg", &err.to_string())])
423                );
424            }
425        }
426    }
427    nodes
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::fetch::deduplicate_names;
434
435    #[test]
436    fn ss_sip002_standard() {
437        let uri = "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#美国-01";
438        let node = parse_uri(uri).unwrap();
439        assert_eq!(node.proxy_type, ProxyType::Ss);
440        assert_eq!(node.name, "美国-01");
441        assert_eq!(node.server, "us1.example.com");
442        assert_eq!(node.port, 8388);
443        assert_eq!(node.extra.get("cipher").and_then(|v| v.as_str()), Some("aes-256-gcm"));
444        assert_eq!(node.extra.get("password").and_then(|v| v.as_str()), Some("password"));
445    }
446
447    #[test]
448    fn ss_legacy_format() {
449        use base64::Engine;
450        let raw = "chacha20-ietf-poly1305:secret123@jp1.example.com:443";
451        let b64 = base64::engine::general_purpose::STANDARD.encode(raw);
452        let uri = format!("ss://{b64}#日本-01");
453        let node = parse_uri(&uri).unwrap();
454        assert_eq!(node.name, "日本-01");
455        assert_eq!(node.server, "jp1.example.com");
456        assert_eq!(node.port, 443);
457    }
458
459    #[test]
460    fn ss_fallback_name_to_server() {
461        let uri = "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@sg1.example.com:443";
462        let node = parse_uri(uri).unwrap();
463        assert_eq!(node.name, "sg1.example.com");
464    }
465
466    #[test]
467    fn ss_password_with_colons() {
468        use base64::Engine;
469        let raw = "2022-blake3-aes-256-gcm:long:password:with:colons";
470        let b64 = base64::engine::general_purpose::STANDARD.encode(raw);
471        let uri = format!("ss://{b64}@hk1.example.com:443#香港");
472        let node = parse_uri(&uri).unwrap();
473        assert_eq!(
474            node.extra.get("cipher").and_then(|v| v.as_str()),
475            Some("2022-blake3-aes-256-gcm")
476        );
477        assert_eq!(
478            node.extra.get("password").and_then(|v| v.as_str()),
479            Some("long:password:with:colons")
480        );
481    }
482
483    #[test]
484    fn vmess_aid_as_number() {
485        use base64::Engine;
486        let cfg = r#"{"ps":"JMS-test","port":"6191","id":"be599ec8-c3de-47d5-ac77-c57f24e13d47","aid":0,"net":"tcp","type":"none","tls":"none","add":"104.243.21.29"}"#;
487        let b64 = base64::engine::general_purpose::STANDARD.encode(cfg);
488        let node = parse_uri(&format!("vmess://{b64}")).unwrap();
489        assert_eq!(node.proxy_type, ProxyType::Vmess);
490        assert_eq!(node.server, "104.243.21.29");
491        assert_eq!(node.extra.get("alterId").and_then(|v| v.as_u64()), Some(0));
492    }
493
494    #[test]
495    fn vmess_ws_tls() {
496        use base64::Engine;
497        let cfg = serde_json::json!({
498            "v": "2",
499            "ps": "日本-01",
500            "add": "jp1.example.com",
501            "port": "443",
502            "id": "550e8400-e29b-41d4-a716-446655440000",
503            "aid": "0",
504            "net": "ws",
505            "host": "cloudflare.com",
506            "path": "/ws?ed=2048",
507            "tls": "tls",
508            "sni": "cloudflare.com"
509        });
510        let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
511        let node = parse_uri(&format!("vmess://{b64}")).unwrap();
512        assert_eq!(node.proxy_type, ProxyType::Vmess);
513        assert_eq!(node.extra.get("network").and_then(|v| v.as_str()), Some("ws"));
514        assert_eq!(node.extra.get("tls").and_then(|v| v.as_bool()), Some(true));
515    }
516
517    #[test]
518    fn trojan_standard() {
519        let uri = "trojan://my-password@sg1.example.com:443?security=tls&sni=sg1.example.com#新加坡-01";
520        let node = parse_uri(uri).unwrap();
521        assert_eq!(node.proxy_type, ProxyType::Trojan);
522        assert_eq!(node.name, "新加坡-01");
523        assert_eq!(node.extra.get("password").and_then(|v| v.as_str()), Some("my-password"));
524    }
525
526    #[test]
527    fn hysteria2_standard() {
528        let uri = "hysteria2://my-password@jp1.example.com:443?insecure=1&sni=jp1.example.com&alpn=h3#日本-Hy2";
529        let node = parse_uri(uri).unwrap();
530        assert_eq!(node.proxy_type, ProxyType::Hysteria2);
531        assert_eq!(node.name, "日本-Hy2");
532        assert_eq!(
533            node.extra.get("skip-cert-verify").and_then(|v| v.as_bool()),
534            Some(true)
535        );
536    }
537
538    #[test]
539    fn deduplicate_names_suffix() {
540        let mut nodes = vec![
541            ProxyNode::new("日本-01".into(), ProxyType::Ss, "a.com".into(), 443),
542            ProxyNode::new("日本-01".into(), ProxyType::Ss, "b.com".into(), 443),
543            ProxyNode::new("日本-01".into(), ProxyType::Ss, "c.com".into(), 443),
544        ];
545        deduplicate_names(&mut nodes);
546        assert_eq!(nodes[0].name, "日本-01");
547        assert_eq!(nodes[1].name, "日本-2");
548        assert_eq!(nodes[2].name, "日本-3");
549    }
550
551    #[test]
552    fn parse_lines_mixed_protocols() {
553        let lines = vec![
554            "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#美国-01".to_string(),
555            "trojan://pass@sg1.example.com:443?security=tls&sni=sg1.example.com#新加坡-01".to_string(),
556        ];
557        let nodes = parse_lines(&lines);
558        assert_eq!(nodes.len(), 2);
559    }
560
561    #[test]
562    fn ss_sip002_nonstandard_extension() {
563        use base64::Engine;
564        let full = "aes-256-gcm:RealPassword@extra.example.com:8443";
565        let b64 = base64::engine::general_purpose::STANDARD.encode(full);
566        let uri = format!("ss://{b64}@extra.example.com:8443#测试");
567        let node = parse_uri(&uri).unwrap();
568        assert_eq!(
569            node.extra.get("cipher").and_then(|v| v.as_str()),
570            Some("aes-256-gcm")
571        );
572        assert_eq!(
573            node.extra.get("password").and_then(|v| v.as_str()),
574            Some("RealPassword")
575        );
576        assert_eq!(node.server, "extra.example.com");
577        assert_eq!(node.port, 8443);
578    }
579
580    #[test]
581    fn vmess_grpc() {
582        use base64::Engine;
583        let cfg = serde_json::json!({
584            "v": "2",
585            "ps": "日本-01",
586            "add": "jp1.example.com",
587            "port": "443",
588            "id": "550e8400-e29b-41d4-a716-446655440000",
589            "aid": "0",
590            "net": "grpc",
591            "serviceName": "my-service",
592            "tls": "tls"
593        });
594        let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
595        let node = parse_uri(&format!("vmess://{b64}")).unwrap();
596        assert_eq!(node.extra.get("network").and_then(|v| v.as_str()), Some("grpc"));
597        let grpc_opts = node.extra.get("grpc-opts").unwrap();
598        let mapping = grpc_opts.as_mapping().unwrap();
599        let key = YamlValue::String("grpc-service-name".into());
600        assert_eq!(
601            mapping.get(&key).and_then(|v| v.as_str()),
602            Some("my-service")
603        );
604    }
605
606    #[test]
607    fn vmess_h2() {
608        use base64::Engine;
609        let cfg = serde_json::json!({
610            "v": "2",
611            "ps": "日本-01",
612            "add": "jp1.example.com",
613            "port": "443",
614            "id": "550e8400-e29b-41d4-a716-446655440000",
615            "aid": "0",
616            "net": "h2",
617            "path": "/api",
618            "host": "example.com",
619            "tls": "tls"
620        });
621        let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
622        let node = parse_uri(&format!("vmess://{b64}")).unwrap();
623        assert_eq!(node.extra.get("network").and_then(|v| v.as_str()), Some("h2"));
624        let h2_opts = node.extra.get("h2-opts").unwrap().as_mapping().unwrap();
625        assert_eq!(
626            h2_opts
627                .get(&YamlValue::String("path".into()))
628                .and_then(|v| v.as_str()),
629            Some("/api")
630        );
631        assert_eq!(
632            h2_opts
633                .get(&YamlValue::String("host".into()))
634                .and_then(|v| v.as_str()),
635            Some("example.com")
636        );
637    }
638
639    #[test]
640    fn vmess_tcp_no_tls() {
641        use base64::Engine;
642        let cfg = serde_json::json!({
643            "v": "2",
644            "ps": "日本-01",
645            "add": "jp1.example.com",
646            "port": "443",
647            "id": "550e8400-e29b-41d4-a716-446655440000",
648            "aid": "0",
649            "net": "tcp",
650            "tls": "none"
651        });
652        let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
653        let node = parse_uri(&format!("vmess://{b64}")).unwrap();
654        assert!(node.extra.get("network").is_none());
655        assert_eq!(node.extra.get("tls").and_then(|v| v.as_bool()), Some(false));
656    }
657
658    #[test]
659    fn vmess_remarks_host_alias() {
660        use base64::Engine;
661        let cfg = serde_json::json!({
662            "v": "2",
663            "remarks": "香港-01",
664            "add": "hk.example.com",
665            "port": "80",
666            "id": "uuid",
667            "aid": "0",
668            "net": "tcp"
669        });
670        let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
671        let node = parse_uri(&format!("vmess://{b64}")).unwrap();
672        assert_eq!(node.name, "香港-01");
673        assert_eq!(node.server, "hk.example.com");
674    }
675
676    #[test]
677    fn vmess_client_fingerprint_alpn() {
678        use base64::Engine;
679        let cfg = serde_json::json!({
680            "v": "2",
681            "ps": "日本-01",
682            "add": "jp1.example.com",
683            "port": "443",
684            "id": "550e8400-e29b-41d4-a716-446655440000",
685            "aid": "0",
686            "net": "ws",
687            "tls": "tls",
688            "fp": "chrome",
689            "alpn": "h2,http/1.1"
690        });
691        let b64 = base64::engine::general_purpose::STANDARD.encode(cfg.to_string());
692        let node = parse_uri(&format!("vmess://{b64}")).unwrap();
693        assert_eq!(
694            node.extra
695                .get("client-fingerprint")
696                .and_then(|v| v.as_str()),
697            Some("chrome")
698        );
699        let alpn = node.extra.get("alpn").unwrap().as_sequence().unwrap();
700        assert_eq!(alpn.len(), 2);
701        assert_eq!(alpn[0].as_str(), Some("h2"));
702        assert_eq!(alpn[1].as_str(), Some("http/1.1"));
703    }
704
705    #[test]
706    fn trojan_allow_insecure() {
707        let uri = "trojan://pass@us1.example.com:443?allowInsecure=1&sni=us1.example.com#美国";
708        let node = parse_uri(uri).unwrap();
709        assert_eq!(
710            node.extra.get("skip-cert-verify").and_then(|v| v.as_bool()),
711            Some(true)
712        );
713    }
714
715    #[test]
716    fn trojan_fallback_name_to_hostname() {
717        let uri = "trojan://pass@jp1.example.com:443";
718        let node = parse_uri(uri).unwrap();
719        assert_eq!(node.name, "jp1.example.com");
720    }
721
722    #[test]
723    fn hysteria2_hy2_prefix() {
724        let uri = "hy2://pass@sg1.example.com:8443?insecure=1&alpn=h3,h2#新加坡-Hy2";
725        let node = parse_uri(uri).unwrap();
726        assert_eq!(node.proxy_type, ProxyType::Hysteria2);
727        assert_eq!(node.server, "sg1.example.com");
728        assert_eq!(node.port, 8443);
729        let alpn = node.extra.get("alpn").unwrap().as_sequence().unwrap();
730        assert_eq!(alpn.len(), 2);
731        assert_eq!(alpn[0].as_str(), Some("h3"));
732        assert_eq!(alpn[1].as_str(), Some("h2"));
733    }
734
735    #[test]
736    fn hysteria2_up_down() {
737        let uri = "hysteria2://pass@us1.example.com:443?up=50&down=150&insecure=1#美国";
738        let node = parse_uri(uri).unwrap();
739        assert_eq!(node.extra.get("up").and_then(|v| v.as_u64()), Some(50));
740        assert_eq!(node.extra.get("down").and_then(|v| v.as_u64()), Some(150));
741    }
742
743    #[test]
744    fn hysteria2_fallback_name_to_hostname() {
745        let uri = "hysteria2://pass@hk1.example.com:443";
746        let node = parse_uri(uri).unwrap();
747        assert_eq!(node.name, "hk1.example.com");
748        assert!(node.extra.get("skip-cert-verify").is_none());
749    }
750
751    #[test]
752    fn parse_lines_skips_unparseable() {
753        let lines = vec![
754            "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@us1.example.com:8388#美国".to_string(),
755            "unknown://bad-line".to_string(),
756            "trojan://pass@sg.example.com:443?sni=sg.example.com#新加坡".to_string(),
757        ];
758        let nodes = parse_lines(&lines);
759        assert_eq!(nodes.len(), 2);
760        assert_eq!(nodes[0].proxy_type, ProxyType::Ss);
761        assert_eq!(nodes[1].proxy_type, ProxyType::Trojan);
762    }
763}