libsubconverter/parser/
infoparser.rs

1use std::time::{Duration, UNIX_EPOCH};
2
3use crate::models::{Proxy, RegexMatchConfigs};
4use crate::utils::base64::url_safe_base64_decode;
5use crate::utils::system::safe_system_time;
6use crate::utils::url::get_url_arg;
7use regex::Regex;
8
9/// Converts a string representing data size with units (B, KB, MB, etc.) to bytes
10pub fn stream_to_int(stream: &str) -> u64 {
11    if stream.is_empty() {
12        return 0;
13    }
14
15    let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
16    let mut index = units.len() - 1;
17
18    loop {
19        if stream.ends_with(units[index]) {
20            let value_str = stream.trim_end_matches(units[index]);
21            let base_value = value_str.parse::<f64>().unwrap_or(0.0);
22            return (base_value * (1024_f64.powi(index as i32))) as u64;
23        }
24
25        if index == 0 {
26            break;
27        }
28        index -= 1;
29    }
30
31    // If no unit is found, try parsing as a raw number
32    stream.parse::<u64>().unwrap_or(0)
33}
34
35/// Converts a percentage string (e.g., "50%") to a decimal value (0.5)
36fn percent_to_double(percent: &str) -> f64 {
37    if percent.ends_with('%') {
38        let value_str = &percent[..percent.len() - 1];
39        return value_str.parse::<f64>().unwrap_or(0.0) / 100.0;
40    }
41    0.0
42}
43
44/// Converts a date string to a timestamp
45pub fn date_string_to_timestamp(date: &str) -> u64 {
46    let now = safe_system_time()
47        .duration_since(UNIX_EPOCH)
48        .unwrap_or_else(|_| Duration::from_secs(0))
49        .as_secs();
50
51    if date.starts_with("left=") {
52        let mut seconds_left = 0;
53        let time_str = &date[5..];
54
55        if time_str.ends_with('d') {
56            let days = time_str[..time_str.len() - 1].parse::<f64>().unwrap_or(0.0);
57            seconds_left = (days * 86400.0) as u64;
58        }
59
60        return now + seconds_left;
61    } else {
62        let parts: Vec<&str> = date.split(':').collect();
63        if parts.len() != 6 {
64            return 0;
65        }
66
67        // This is a simplified version - Rust's time handling is different
68        // In a full implementation, you would use chrono crate for better date handling
69        let _year = parts[0].parse::<i32>().unwrap_or(1900);
70        let _month = parts[1].parse::<u32>().unwrap_or(1);
71        let _day = parts[2].parse::<u32>().unwrap_or(1);
72        let _hour = parts[3].parse::<u32>().unwrap_or(0);
73        let _minute = parts[4].parse::<u32>().unwrap_or(0);
74        let _second = parts[5].parse::<u32>().unwrap_or(0);
75
76        // This is a placeholder - in practice use chrono::NaiveDate::from_ymd_opt and related functions
77        // Return current time as fallback
78        return now;
79    }
80}
81
82/// Extracts subscription info from HTTP headers
83pub fn get_sub_info_from_header(header: &str) -> Option<String> {
84    let re = Regex::new(r"(?i)^Subscription-UserInfo: (.*?)$").ok()?;
85
86    if let Some(captures) = re.captures(header) {
87        if let Some(matched) = captures.get(1) {
88            let ret_str = matched.as_str().trim();
89            if !ret_str.is_empty() {
90                return Some(ret_str.to_string());
91            }
92        }
93    }
94
95    None
96}
97
98/// Extracts subscription info from a collection of proxy nodes
99pub fn get_sub_info_from_nodes(
100    nodes: &[Proxy],
101    stream_rules: &RegexMatchConfigs,
102    time_rules: &RegexMatchConfigs,
103) -> Option<String> {
104    let mut stream_info = String::new();
105    let mut time_info = String::new();
106
107    for node in nodes {
108        let remarks = &node.remark;
109
110        // Extract stream info if not already found
111        if stream_info.is_empty() {
112            for rule in stream_rules {
113                let re = Regex::new(&rule._match).ok()?;
114                if re.is_match(remarks) {
115                    let new_remark = re.replace(remarks, &rule.replace).to_string();
116                    if new_remark != *remarks {
117                        stream_info = new_remark;
118                        break;
119                    }
120                }
121            }
122        }
123
124        // Extract time info if not already found
125        if time_info.is_empty() {
126            for rule in time_rules {
127                let re = Regex::new(&rule._match).ok()?;
128                if re.is_match(remarks) {
129                    let new_remark = re.replace(remarks, &rule.replace).to_string();
130                    if new_remark != *remarks {
131                        time_info = new_remark;
132                        break;
133                    }
134                }
135            }
136        }
137
138        if !stream_info.is_empty() && !time_info.is_empty() {
139            break;
140        }
141    }
142
143    if stream_info.is_empty() && time_info.is_empty() {
144        return None;
145    }
146
147    // Calculate stream usage
148    let mut total: u64 = 0;
149    let mut used: u64 = 0;
150
151    let total_str = get_url_arg(&stream_info, "total");
152    let left_str = get_url_arg(&stream_info, "left");
153    let used_str = get_url_arg(&stream_info, "used");
154
155    if total_str.contains('%') {
156        if !used_str.is_empty() {
157            used = stream_to_int(&used_str);
158            let percentage = percent_to_double(&total_str);
159            if percentage > 0.0 {
160                total = (used as f64 / (1.0 - percentage)) as u64;
161            }
162        } else if !left_str.is_empty() {
163            let left = stream_to_int(&left_str);
164            let percentage = percent_to_double(&total_str);
165            if percentage > 0.0 {
166                total = (left as f64 / percentage) as u64;
167                if left > total {
168                    used = 0;
169                } else {
170                    used = total - left;
171                }
172            }
173        }
174    } else {
175        total = stream_to_int(&total_str);
176        if !used_str.is_empty() {
177            used = stream_to_int(&used_str);
178        } else if !left_str.is_empty() {
179            let left = stream_to_int(&left_str);
180            if left > total {
181                used = 0;
182            } else {
183                used = total - left;
184            }
185        }
186    }
187
188    let mut result = format!("upload=0; download={}; total={};", used, total);
189
190    // Calculate expire time
191    let expire = date_string_to_timestamp(&time_info);
192    if expire > 0 {
193        result.push_str(&format!(" expire={};", expire));
194    }
195
196    Some(result)
197}
198
199/// Extracts subscription info from an SSD-format subscription
200pub fn get_sub_info_from_ssd(sub: &str) -> Option<String> {
201    if !sub.starts_with("ssd://") {
202        return None;
203    }
204
205    let decoded = url_safe_base64_decode(&sub[6..]);
206
207    // Parse JSON
208    let json: serde_json::Value = match serde_json::from_str(&decoded) {
209        Ok(val) => val,
210        Err(_) => return None,
211    };
212
213    let used_str = json.get("traffic_used")?.as_str()?;
214    let total_str = json.get("traffic_total")?.as_str()?;
215
216    // 1 GB = 1024^3 bytes
217    let gb_to_bytes = 1024u64.pow(3);
218    let used = used_str.parse::<f64>().unwrap_or(0.0) * gb_to_bytes as f64;
219    let total = total_str.parse::<f64>().unwrap_or(0.0) * gb_to_bytes as f64;
220
221    let mut result = format!(
222        "upload=0; download={}; total={};",
223        used as u64, total as u64
224    );
225
226    if let Some(expire_str) = json.get("expiry").and_then(|v| v.as_str()) {
227        // Convert expiry format using regex
228        let re = Regex::new(r"(\d+)-(\d+)-(\d+) (.*)").unwrap();
229        let formatted_date = re.replace(expire_str, "$1:$2:$3:$4").to_string();
230
231        let expire = date_string_to_timestamp(&formatted_date);
232        if expire > 0 {
233            result.push_str(&format!(" expire={};", expire));
234        }
235    }
236
237    Some(result)
238}