libsubconverter/utils/useragent.rs
1//! User agent matching utilities
2//!
3//! This module provides functionality for parsing and matching user agent strings
4//! to determine target formats and settings.
5
6use crate::models::SubconverterTarget;
7
8/// User agent profile structure
9pub struct UAProfile {
10 /// Beginning of user agent string to match
11 pub head: String,
12 /// Version string to look for
13 pub version_match: String,
14 /// Target version to compare with
15 pub version_target: String,
16 /// Target format to use
17 pub target: SubconverterTarget,
18 /// Whether to use new field names in Clash
19 /// None means indeterminate (equivalent to tribool indeterminate state)
20 pub clash_new_name: Option<bool>,
21 /// Surge version
22 pub surge_ver: i32,
23}
24
25impl UAProfile {
26 /// Create a new UAProfile
27 pub fn new(
28 head: &str,
29 version_match: &str,
30 version_target: &str,
31 target: SubconverterTarget,
32 clash_new_name: Option<bool>,
33 surge_ver: i32,
34 ) -> Self {
35 UAProfile {
36 head: head.to_string(),
37 version_match: version_match.to_string(),
38 version_target: version_target.to_string(),
39 target,
40 clash_new_name,
41 surge_ver,
42 }
43 }
44}
45
46/// Compare two version strings to check if source version is greater than or equal to target version
47///
48/// # Arguments
49///
50/// * `src_ver` - Source version string, like "1.2.3"
51/// * `target_ver` - Target version string to compare with
52///
53/// # Returns
54///
55/// `true` if the source version is greater than or equal to the target version
56pub fn ver_greater_equal(src_ver: &str, target_ver: &str) -> bool {
57 // Create iterators for both version strings, splitting by dots
58 let src_parts = src_ver.split('.').collect::<Vec<&str>>();
59 let target_parts = target_ver.split('.').collect::<Vec<&str>>();
60
61 // Compare each part of the version
62 let min_len = src_parts.len().min(target_parts.len());
63 for i in 0..min_len {
64 // Parse to integers, default to 0 if parsing fails
65 let src_part = src_parts[i].parse::<i32>().unwrap_or(0);
66 let target_part = target_parts[i].parse::<i32>().unwrap_or(0);
67
68 // If source part is greater, return true
69 if src_part > target_part {
70 return true;
71 }
72 // If source part is less, return false
73 else if src_part < target_part {
74 return false;
75 }
76 // If equal, continue to next part
77 }
78
79 // If all parts were equal, check if source has more parts than target
80 // For example, 1.2.3 is greater than 1.2
81 if src_parts.len() >= target_parts.len() {
82 return true;
83 }
84
85 false
86}
87
88/// Match user agent string to determine target format and settings
89///
90/// # Arguments
91///
92/// * `user_agent` - User agent string to match
93/// * `target` - Output parameter for target format
94/// * `clash_new_name` - Output parameter for Clash new name setting (None = indeterminate)
95/// * `surge_ver` - Output parameter for Surge version
96///
97/// # Returns
98///
99/// Updates the target, clash_new_name, and surge_ver parameters based on matching
100pub fn match_user_agent(
101 user_agent: &str,
102 target: &mut SubconverterTarget,
103 clash_new_name: &mut Option<bool>,
104 surge_ver: &mut i32,
105) {
106 // Define user agent profiles to match C++ UAMatchList
107 let ua_profiles = vec![
108 // ClashForAndroid profiles
109 UAProfile::new(
110 "clashforandroid",
111 "\\/([0-9.]+)",
112 "2.0",
113 SubconverterTarget::Clash,
114 Some(true), // True
115 -1,
116 ),
117 UAProfile::new(
118 "clashforandroid",
119 "\\/([0-9.]+)r",
120 "",
121 SubconverterTarget::ClashR,
122 Some(false), // False
123 -1,
124 ),
125 UAProfile::new(
126 "clashforandroid",
127 "",
128 "",
129 SubconverterTarget::Clash,
130 Some(false), // False
131 -1,
132 ),
133 // ClashForWindows profiles
134 UAProfile::new(
135 "clashforwindows",
136 "\\/([0-9.]+)",
137 "0.11",
138 SubconverterTarget::Clash,
139 Some(true), // True
140 -1,
141 ),
142 UAProfile::new(
143 "clashforwindows",
144 "",
145 "",
146 SubconverterTarget::Clash,
147 Some(false), // False
148 -1,
149 ),
150 // Clash Verge
151 UAProfile::new(
152 "clash-verge",
153 "",
154 "",
155 SubconverterTarget::Clash,
156 Some(true), // True
157 -1,
158 ),
159 // ClashX Pro
160 UAProfile::new(
161 "clashx pro",
162 "",
163 "",
164 SubconverterTarget::Clash,
165 Some(true), // True
166 -1,
167 ),
168 // ClashX
169 UAProfile::new(
170 "clashx",
171 "\\/([0-9.]+)",
172 "0.13",
173 SubconverterTarget::Clash,
174 Some(true), // True
175 -1,
176 ),
177 // Generic Clash
178 UAProfile::new(
179 "clash",
180 "",
181 "",
182 SubconverterTarget::Clash,
183 Some(true), // True
184 -1,
185 ),
186 // Kitsunebi
187 UAProfile::new(
188 "kitsunebi",
189 "",
190 "",
191 SubconverterTarget::V2Ray,
192 None, // Indeterminate
193 -1,
194 ),
195 // Loon
196 UAProfile::new(
197 "loon",
198 "",
199 "",
200 SubconverterTarget::Loon,
201 None, // Indeterminate
202 -1,
203 ),
204 // Pharos
205 UAProfile::new(
206 "pharos",
207 "",
208 "",
209 SubconverterTarget::Mixed,
210 None, // Indeterminate
211 -1,
212 ),
213 // Potatso
214 UAProfile::new(
215 "potatso",
216 "",
217 "",
218 SubconverterTarget::Mixed,
219 None, // Indeterminate
220 -1,
221 ),
222 // Quantumult X
223 UAProfile::new(
224 "quantumult%20x",
225 "",
226 "",
227 SubconverterTarget::QuantumultX,
228 None, // Indeterminate
229 -1,
230 ),
231 // Quantumult
232 UAProfile::new(
233 "quantumult",
234 "",
235 "",
236 SubconverterTarget::Quantumult,
237 None, // Indeterminate
238 -1,
239 ),
240 // Qv2ray
241 UAProfile::new(
242 "qv2ray",
243 "",
244 "",
245 SubconverterTarget::V2Ray,
246 None, // Indeterminate
247 -1,
248 ),
249 // Shadowrocket
250 UAProfile::new(
251 "shadowrocket",
252 "",
253 "",
254 SubconverterTarget::Mixed, // In original C++ it's "mixed"
255 None, // Indeterminate
256 -1,
257 ),
258 // Surfboard
259 UAProfile::new(
260 "surfboard",
261 "",
262 "",
263 SubconverterTarget::Surfboard,
264 None, // Indeterminate
265 -1,
266 ),
267 // Surge Mac x86
268 UAProfile::new(
269 "surge",
270 "\\/([0-9.]+).*x86",
271 "906",
272 SubconverterTarget::Surge(4),
273 Some(false), // False
274 4,
275 ),
276 UAProfile::new(
277 "surge",
278 "\\/([0-9.]+).*x86",
279 "368",
280 SubconverterTarget::Surge(3),
281 Some(false), // False
282 3,
283 ),
284 // Surge iOS
285 UAProfile::new(
286 "surge",
287 "\\/([0-9.]+)",
288 "1419",
289 SubconverterTarget::Surge(4),
290 Some(false), // False
291 4,
292 ),
293 UAProfile::new(
294 "surge",
295 "\\/([0-9.]+)",
296 "900",
297 SubconverterTarget::Surge(3),
298 Some(false), // False
299 3,
300 ),
301 // Fallback for any Surge version
302 UAProfile::new(
303 "surge",
304 "",
305 "",
306 SubconverterTarget::Surge(2),
307 Some(false), // False
308 2,
309 ),
310 // Trojan-Qt5
311 UAProfile::new(
312 "trojan-qt5",
313 "",
314 "",
315 SubconverterTarget::Trojan,
316 None, // Indeterminate
317 -1,
318 ),
319 // V2rayU
320 UAProfile::new(
321 "v2rayu",
322 "",
323 "",
324 SubconverterTarget::V2Ray,
325 None, // Indeterminate
326 -1,
327 ),
328 // V2RayX
329 UAProfile::new(
330 "v2rayx",
331 "",
332 "",
333 SubconverterTarget::V2Ray,
334 None, // Indeterminate
335 -1,
336 ),
337 // SingBox (not in original C++ list but keep it)
338 UAProfile::new(
339 "sing-box",
340 "",
341 "",
342 SubconverterTarget::SingBox,
343 None, // Indeterminate
344 -1,
345 ),
346 ];
347
348 // Convert the user agent to lowercase for case-insensitive matching
349 let user_agent_lower = user_agent.to_lowercase();
350
351 for profile in ua_profiles {
352 if user_agent_lower.contains(&profile.head) {
353 // If a version string is specified, check if it matches and is greater than or equal to target version
354 if !profile.version_match.is_empty() && !profile.version_target.is_empty() {
355 if let Some(version_pos) = user_agent_lower.find(&profile.version_match) {
356 let version_begin = version_pos + profile.version_match.len();
357 if let Some(version_end) = user_agent_lower[version_begin..].find(' ') {
358 let version =
359 &user_agent_lower[version_begin..(version_begin + version_end)];
360 if ver_greater_equal(version, &profile.version_target) {
361 *target = profile.target;
362 *clash_new_name = profile.clash_new_name;
363 *surge_ver = profile.surge_ver;
364 return;
365 }
366 } else {
367 let version = &user_agent_lower[version_begin..];
368 if ver_greater_equal(version, &profile.version_target) {
369 *target = profile.target;
370 *clash_new_name = profile.clash_new_name;
371 *surge_ver = profile.surge_ver;
372 return;
373 }
374 }
375 }
376 } else {
377 // If no version string specified, just match the head
378 *target = profile.target;
379 *clash_new_name = profile.clash_new_name;
380 *surge_ver = profile.surge_ver;
381 return;
382 }
383 }
384 }
385}