rinova_proxy_sdk/
fetch.rs1use crate::error::{ProxyError, Result};
2use crate::i18n::t;
3use crate::utils::decode_base64_utf8;
4
5const USER_AGENT: &str = "@rinova/proxy-sdk/1.0.0";
6
7pub async fn fetch_subscription(url: &str) -> Result<Vec<String>> {
8 let client = reqwest::Client::builder()
9 .timeout(std::time::Duration::from_secs(15))
10 .build()?;
11
12 let resp = client
13 .get(url)
14 .header("User-Agent", USER_AGENT)
15 .send()
16 .await?
17 .error_for_status()?
18 .text()
19 .await?;
20
21 let mut raw = resp.trim().to_string();
22
23 if raw.contains('%') {
24 if let Ok(decoded) = urlencoding::decode(&raw) {
25 raw = decoded.into_owned();
26 }
27 }
28
29 let decoded = decode_base64_utf8(&raw).unwrap_or(raw);
30
31 let lines: Vec<String> = decoded
32 .lines()
33 .map(str::trim)
34 .filter(|line| !line.is_empty() && !line.starts_with('#'))
35 .map(str::to_string)
36 .collect();
37
38 if lines.is_empty() {
39 return Err(ProxyError::msg(t("err_empty_subscription", &[])));
40 }
41
42 Ok(lines)
43}
44
45pub fn deduplicate_names(nodes: &mut [crate::types::ProxyNode]) {
46 let mut seen = std::collections::HashMap::<String, u32>::new();
47 for node in nodes.iter_mut() {
48 let base = strip_numeric_suffix(&node.name).to_string();
49 let count = seen.entry(base.clone()).or_insert(0);
50 *count += 1;
51 if *count > 1 {
52 node.name = format!("{base}-{}", *count);
53 }
54 }
55}
56
57fn strip_numeric_suffix(name: &str) -> &str {
58 if let Some(idx) = name.rfind('-') {
59 let suffix = &name[idx + 1..];
60 if !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()) {
61 return &name[..idx];
62 }
63 }
64 name
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use crate::types::{ProxyNode, ProxyType};
71
72 #[test]
73 fn deduplicate_preserves_unique_names() {
74 let mut nodes = vec![
75 ProxyNode::new("日本-01".into(), ProxyType::Ss, "a.com".into(), 443),
76 ProxyNode::new("新加坡-01".into(), ProxyType::Ss, "b.com".into(), 443),
77 ];
78 deduplicate_names(&mut nodes);
79 assert_eq!(nodes[0].name, "日本-01");
80 assert_eq!(nodes[1].name, "新加坡-01");
81 }
82
83 #[test]
84 fn pad_base64_roundtrip() {
85 let raw = "ss://line1\nss://line2";
86 use base64::Engine;
87 let encoded = base64::engine::general_purpose::STANDARD.encode(raw);
88 let decoded = decode_base64_utf8(&encoded).unwrap();
89 assert_eq!(decoded, raw);
90 }
91}