Skip to main content

rinova_proxy_sdk/
fetch.rs

1use 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}