libsubconverter/utils/
matcher.rs

1use crate::models::{Proxy, ProxyType};
2use lazy_static::lazy_static;
3use regex::Regex;
4use std::collections::HashMap;
5use std::str::FromStr;
6
7lazy_static! {
8    static ref GROUPID_REGEX: Regex =
9        Regex::new(r"^!!(?:GROUPID|INSERT)=([\d\-+!,]+)(?:!!(.*))?$").unwrap();
10    static ref GROUP_REGEX: Regex = Regex::new(r"^!!(?:GROUP)=(.+?)(?:!!(.*))?$").unwrap();
11    static ref TYPE_REGEX: Regex = Regex::new(r"^!!(?:TYPE)=(.+?)(?:!!(.*))?$").unwrap();
12    static ref PORT_REGEX: Regex = Regex::new(r"^!!(?:PORT)=(.+?)(?:!!(.*))?$").unwrap();
13    static ref SERVER_REGEX: Regex = Regex::new(r"^!!(?:SERVER)=(.+?)(?:!!(.*))?$").unwrap();
14    static ref PROTOCOL_REGEX: Regex = Regex::new(r"^!!(?:PROTOCOL)=(.+?)(?:!!(.*))?$").unwrap();
15    static ref UDPSUPPORT_REGEX: Regex =
16        Regex::new(r"^!!(?:UDPSUPPORT)=(.+?)(?:!!(.*))?$").unwrap();
17    static ref SECURITY_REGEX: Regex = Regex::new(r"^!!(?:SECURITY)=(.+?)(?:!!(.*))?$").unwrap();
18    static ref REMARKS_REGEX: Regex = Regex::new(r"^!!(?:REMARKS)=(.+?)(?:!!(.*))?$").unwrap();
19    static ref PROXY_TYPES: HashMap<ProxyType, &'static str> = {
20        let mut m = HashMap::new();
21        m.insert(ProxyType::Shadowsocks, "SS");
22        m.insert(ProxyType::ShadowsocksR, "SSR");
23        m.insert(ProxyType::VMess, "VMESS");
24        m.insert(ProxyType::Trojan, "TROJAN");
25        m.insert(ProxyType::Snell, "SNELL");
26        m.insert(ProxyType::HTTP, "HTTP");
27        m.insert(ProxyType::HTTPS, "HTTPS");
28        m.insert(ProxyType::Socks5, "SOCKS5");
29        m.insert(ProxyType::WireGuard, "WIREGUARD");
30        m.insert(ProxyType::Hysteria, "HYSTERIA");
31        m.insert(ProxyType::Hysteria2, "HYSTERIA2");
32        m.insert(ProxyType::Unknown, "UNKNOWN");
33        m
34    };
35}
36
37/// Match a rule against a proxy node
38///
39/// This function evaluates complex rule strings that can match different
40/// aspects of a proxy node. Special rule formats begin with "!!" and can match
41/// against properties like group, type, port, etc.
42///
43/// Supported special rules:
44/// - !!GROUP=<group_pattern> - Matches node's group against pattern
45/// - !!GROUPID=<id_range> - Matches node's group ID against range
46/// - !!INSERT=<id_range> - Like GROUPID but negates direction
47/// - !!TYPE=<type_pattern> - Matches node's proxy type against pattern
48/// - !!PORT=<port_range> - Matches node's port against range
49/// - !!SERVER=<server_pattern> - Matches node's hostname against pattern
50/// - !!PROTOCOL=<protocol_pattern> - Matches node's protocol against pattern
51/// - !!UDPSUPPORT=<support_pattern> - Matches node's UDP support status
52/// - !!SECURITY=<security_pattern> - Matches node's security features
53/// - !!REMARKS=<remarks_pattern> - Matches node's remark against pattern
54///
55/// # Arguments
56/// * `rule` - The rule to match
57/// * `real_rule` - Output parameter that will contain the processed rule after
58///   special prefix handling
59/// * `node` - The proxy node to match against
60///
61/// # Returns
62/// * `true` if the rule matches the node
63/// * `false` otherwise
64pub fn apply_matcher(rule: &str, real_rule: &mut String, node: &Proxy) -> bool {
65    if rule.starts_with("!!GROUP=") {
66        if let Some(captures) = GROUP_REGEX.captures(rule) {
67            let target = captures.get(1).map_or("", |m| m.as_str());
68            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
69            return reg_find(&node.group, target);
70        }
71    } else if rule.starts_with("!!GROUPID=") || rule.starts_with("!!INSERT=") {
72        let dir = if rule.starts_with("!!INSERT=") { -1 } else { 1 };
73        if let Some(captures) = GROUPID_REGEX.captures(rule) {
74            let target = captures.get(1).map_or("", |m| m.as_str());
75            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
76            return match_range(target, dir * (node.group_id as i32));
77        }
78    } else if rule.starts_with("!!TYPE=") {
79        if let Some(captures) = TYPE_REGEX.captures(rule) {
80            let target = captures.get(1).map_or("", |m| m.as_str());
81            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
82            if node.proxy_type == ProxyType::Unknown {
83                return false;
84            }
85
86            let type_str = PROXY_TYPES.get(&node.proxy_type).unwrap_or(&"UNKNOWN");
87            return reg_match(type_str, target);
88        }
89    } else if rule.starts_with("!!PORT=") {
90        if let Some(captures) = PORT_REGEX.captures(rule) {
91            let target = captures.get(1).map_or("", |m| m.as_str());
92            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
93            return match_range(target, node.port as i32);
94        }
95    } else if rule.starts_with("!!SERVER=") {
96        if let Some(captures) = SERVER_REGEX.captures(rule) {
97            let target = captures.get(1).map_or("", |m| m.as_str());
98            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
99            return reg_find(&node.hostname, target);
100        }
101    } else if rule.starts_with("!!PROTOCOL=") {
102        if let Some(captures) = PROTOCOL_REGEX.captures(rule) {
103            let target = captures.get(1).map_or("", |m| m.as_str());
104            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
105            let protocol = match &node.protocol {
106                Some(proto) => proto,
107                None => return false,
108            };
109            return reg_find(protocol, target);
110        }
111    } else if rule.starts_with("!!UDPSUPPORT=") {
112        if let Some(captures) = UDPSUPPORT_REGEX.captures(rule) {
113            let target = captures.get(1).map_or("", |m| m.as_str());
114            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
115
116            match node.udp {
117                Some(true) => return reg_match("yes", target),
118                Some(false) => return reg_match("no", target),
119                None => return reg_match("undefined", target),
120            }
121        }
122    } else if rule.starts_with("!!SECURITY=") {
123        if let Some(captures) = SECURITY_REGEX.captures(rule) {
124            let target = captures.get(1).map_or("", |m| m.as_str());
125            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
126
127            // Build a string of security features
128            let mut features = String::new();
129
130            if node.tls_secure {
131                features.push_str("TLS,");
132            }
133
134            if let Some(true) = node.allow_insecure {
135                features.push_str("INSECURE,");
136            }
137
138            if let Some(true) = node.tls13 {
139                features.push_str("TLS13,");
140            }
141
142            if !features.is_empty() {
143                features.pop(); // Remove trailing comma
144            } else {
145                features.push_str("NONE");
146            }
147
148            return reg_find(&features, target);
149        }
150    } else if rule.starts_with("!!REMARKS=") {
151        if let Some(captures) = REMARKS_REGEX.captures(rule) {
152            let target = captures.get(1).map_or("", |m| m.as_str());
153            *real_rule = captures.get(2).map_or("", |m| m.as_str()).to_string();
154            return reg_find(&node.remark, target);
155        }
156    } else {
157        *real_rule = rule.to_string();
158    }
159
160    true
161}
162
163/// Match a number against a range specification
164///
165/// Range specification can include:
166/// * Single numbers: "1", "2"
167/// * Ranges: "1-10", "100-200"
168/// * Negation: "!1-10" (everything except 1-10)
169/// * Multiple ranges: "1-10,20-30,50"
170///
171/// # Arguments
172/// * `range` - The range specification string
173/// * `target` - The target number to match
174///
175/// # Returns
176/// * `true` if the target matches the range
177/// * `false` otherwise
178pub fn match_range(range: &str, target: i32) -> bool {
179    let mut negate = false;
180    let mut matched = false;
181
182    for range_part in range.split(',') {
183        let mut part = range_part.trim();
184
185        if part.starts_with('!') {
186            negate = true;
187            part = &part[1..];
188        }
189
190        if part.contains('-') {
191            let bounds: Vec<&str> = part.split('-').collect();
192            if bounds.len() == 2 {
193                let lower = bounds[0].parse::<i32>().unwrap_or(i32::MIN);
194                let upper = bounds[1].parse::<i32>().unwrap_or(i32::MAX);
195
196                if target >= lower && target <= upper {
197                    matched = true;
198                    break;
199                }
200            }
201        } else if let Ok(exact) = part.parse::<i32>() {
202            if target == exact {
203                matched = true;
204                break;
205            }
206        }
207    }
208
209    if negate {
210        !matched
211    } else {
212        matched
213    }
214}
215
216/// Check if a string matches a regular expression pattern
217///
218/// # Arguments
219/// * `text` - The text to search
220/// * `pattern` - The regex pattern to match
221///
222/// # Returns
223/// * `true` if the pattern is found in the text
224/// * `false` otherwise
225pub fn reg_find(text: &str, pattern: &str) -> bool {
226    if pattern.is_empty() {
227        return true;
228    }
229
230    match Regex::new(&format!("(?i){}", pattern)) {
231        Ok(re) => re.is_match(text),
232        Err(_) => false,
233    }
234}
235
236/// Check if a string fully matches a regular expression pattern
237///
238/// # Arguments
239/// * `text` - The text to match
240/// * `pattern` - The regex pattern to match
241///
242/// # Returns
243/// * `true` if the pattern fully matches the text
244/// * `false` otherwise
245pub fn reg_match(text: &str, pattern: &str) -> bool {
246    if pattern.is_empty() {
247        return true;
248    }
249
250    match Regex::new(&format!("(?i)^{}$", pattern)) {
251        Ok(re) => re.is_match(text),
252        Err(_) => false,
253    }
254}
255
256#[derive(Debug, Clone)]
257pub struct CompiledRange {
258    lower: i32,
259    upper: i32,
260}
261
262#[derive(Debug, Clone)]
263pub enum CompiledMatcher {
264    /// Match against group name (case-insensitive regex find)
265    Group(Regex),
266    /// Match against group ID range
267    GroupId {
268        ranges: Vec<CompiledRange>,
269        negate: bool,
270    },
271    /// Match against proxy type (case-insensitive regex match)
272    Type(Regex),
273    /// Match against port range
274    Port {
275        ranges: Vec<CompiledRange>,
276        negate: bool,
277    },
278    /// Match against server/hostname (case-insensitive regex find)
279    Server(Regex),
280    /// Match against protocol (case-insensitive regex find)
281    Protocol(Regex),
282    /// Match against UDP support (case-insensitive regex match: "yes", "no",
283    /// "undefined")
284    UdpSupport(Regex),
285    /// Match against security features (case-insensitive regex find: "TLS",
286    /// "INSECURE", "TLS13", "NONE")
287    Security(Regex),
288    /// Match against remark (case-insensitive regex find)
289    Remarks(Regex),
290    /// A plain regex rule (equivalent to !!REMARKS= but without the prefix)
291    Plain(Regex),
292    /// Rule that always matches (e.g., empty rule)
293    AlwaysTrue,
294    /// Rule that is invalid or cannot be compiled
295    Invalid,
296}
297
298#[derive(Debug, Clone)]
299pub struct CompiledRule {
300    pub matcher: CompiledMatcher,
301    pub sub_rule: Option<Box<CompiledRule>>, // For rules like !!GROUP=X!!Y
302}
303
304fn parse_range_string(range_str: &str) -> (Vec<CompiledRange>, bool) {
305    let mut negate = false;
306    let mut ranges = Vec::new();
307    let mut effective_range_str = range_str;
308
309    if let Some(stripped) = range_str.strip_prefix('!') {
310        negate = true;
311        effective_range_str = stripped;
312    }
313
314    for range_part in effective_range_str.split(',') {
315        let part = range_part.trim();
316        if part.is_empty() {
317            continue;
318        }
319
320        if part.contains('-') {
321            let bounds: Vec<&str> = part.split('-').collect();
322            if bounds.len() == 2 {
323                // Allow empty bounds to signify MIN/MAX
324                let lower = bounds[0].parse::<i32>().unwrap_or_else(|_| i32::MIN);
325                let upper = bounds[1].parse::<i32>().unwrap_or_else(|_| i32::MAX);
326                if lower <= upper {
327                    ranges.push(CompiledRange { lower, upper });
328                } // else: invalid range like 10-1, ignore
329            }
330        } else if let Ok(exact) = part.parse::<i32>() {
331            ranges.push(CompiledRange {
332                lower: exact,
333                upper: exact,
334            });
335        }
336        // Ignore parts that are not numbers or valid ranges
337    }
338    (ranges, negate)
339}
340
341/// Compiles a rule string into a `CompiledRule` structure.
342///
343/// This function parses the rule string, pre-compiles any regex patterns,
344/// and determines the type of match to perform.
345pub fn compile_rule(rule: &str) -> CompiledRule {
346    let mut sub_rule_str: Option<&str> = None;
347    let matcher = if let Some(captures) = GROUP_REGEX.captures(rule) {
348        sub_rule_str = captures.get(2).map(|m| m.as_str());
349        let target = captures.get(1).map_or("", |m| m.as_str());
350        Regex::new(&format!("(?i){}", target))
351            .map(CompiledMatcher::Group)
352            .unwrap_or(CompiledMatcher::Invalid)
353    } else if let Some(captures) = GROUPID_REGEX.captures(rule) {
354        sub_rule_str = captures.get(2).map(|m| m.as_str());
355        let target = captures.get(1).map_or("", |m| m.as_str());
356        let dir = if rule.starts_with("!!INSERT=") { -1 } else { 1 }; // Apply direction modifier conceptually later
357        let (ranges, negate) = parse_range_string(target);
358        // The 'dir' multiplier is handled during application, not compilation
359        CompiledMatcher::GroupId { ranges, negate }
360    } else if let Some(captures) = TYPE_REGEX.captures(rule) {
361        sub_rule_str = captures.get(2).map(|m| m.as_str());
362        let target = captures.get(1).map_or("", |m| m.as_str());
363        Regex::new(&format!("(?i)^{}$", target))
364            .map(CompiledMatcher::Type)
365            .unwrap_or(CompiledMatcher::Invalid)
366    } else if let Some(captures) = PORT_REGEX.captures(rule) {
367        sub_rule_str = captures.get(2).map(|m| m.as_str());
368        let target = captures.get(1).map_or("", |m| m.as_str());
369        let (ranges, negate) = parse_range_string(target);
370        CompiledMatcher::Port { ranges, negate }
371    } else if let Some(captures) = SERVER_REGEX.captures(rule) {
372        sub_rule_str = captures.get(2).map(|m| m.as_str());
373        let target = captures.get(1).map_or("", |m| m.as_str());
374        Regex::new(&format!("(?i){}", target))
375            .map(CompiledMatcher::Server)
376            .unwrap_or(CompiledMatcher::Invalid)
377    } else if let Some(captures) = PROTOCOL_REGEX.captures(rule) {
378        sub_rule_str = captures.get(2).map(|m| m.as_str());
379        let target = captures.get(1).map_or("", |m| m.as_str());
380        Regex::new(&format!("(?i){}", target))
381            .map(CompiledMatcher::Protocol)
382            .unwrap_or(CompiledMatcher::Invalid)
383    } else if let Some(captures) = UDPSUPPORT_REGEX.captures(rule) {
384        sub_rule_str = captures.get(2).map(|m| m.as_str());
385        let target = captures.get(1).map_or("", |m| m.as_str());
386        Regex::new(&format!("(?i)^{}$", target))
387            .map(CompiledMatcher::UdpSupport)
388            .unwrap_or(CompiledMatcher::Invalid)
389    } else if let Some(captures) = SECURITY_REGEX.captures(rule) {
390        sub_rule_str = captures.get(2).map(|m| m.as_str());
391        let target = captures.get(1).map_or("", |m| m.as_str());
392        Regex::new(&format!("(?i){}", target))
393            .map(CompiledMatcher::Security)
394            .unwrap_or(CompiledMatcher::Invalid)
395    } else if let Some(captures) = REMARKS_REGEX.captures(rule) {
396        sub_rule_str = captures.get(2).map(|m| m.as_str());
397        let target = captures.get(1).map_or("", |m| m.as_str());
398        Regex::new(&format!("(?i){}", target))
399            .map(CompiledMatcher::Remarks)
400            .unwrap_or(CompiledMatcher::Invalid)
401    } else {
402        // Treat as plain regex match against remark if no prefix
403        if rule.is_empty() {
404            CompiledMatcher::AlwaysTrue
405        } else {
406            Regex::new(&format!("(?i){}", rule))
407                .map(CompiledMatcher::Plain)
408                .unwrap_or(CompiledMatcher::Invalid)
409        }
410    };
411
412    let sub_rule = sub_rule_str
413        .filter(|s| !s.is_empty()) // Only compile non-empty sub-rules
414        .map(|s| Box::new(compile_rule(s)));
415
416    CompiledRule { matcher, sub_rule }
417}
418
419/// Applies a pre-compiled rule against a proxy node.
420///
421/// # Arguments
422/// * `compiled_rule` - The pre-compiled rule structure.
423/// * `node` - The proxy node to match against.
424///
425/// # Returns
426/// * `true` if the rule matches the node, `false` otherwise.
427pub fn apply_compiled_rule(compiled_rule: &CompiledRule, node: &Proxy) -> bool {
428    let primary_match = match &compiled_rule.matcher {
429        CompiledMatcher::Group(re) => re.is_match(&node.group),
430        CompiledMatcher::GroupId { ranges, negate } => {
431            // Determine direction based on original rule (though not stored in compiled,
432            // assume GroupId is +1, Insert is -1 conceptually)
433            // We assume compile_rule was called on the original string, so we don't store
434            // 'dir' Let's refine this: The compile function needs to know if
435            // it's GROUPID or INSERT. For now, let's assume we only compile
436            // GROUPID-like rules or handle INSERT elsewhere. A better approach
437            // might be to store the direction in the CompiledMatcher enum.
438            // Let's stick to the original match_range logic for simplicity for now.
439            // We need the original rule string to call match_range properly,
440            // which defeats the purpose of compiling.
441            // Let's reimplement match_range logic here based on compiled ranges.
442            let target = node.group_id as i32; // Assume dir = 1 for now
443            let mut matched = false;
444            for r in ranges {
445                if target >= r.lower && target <= r.upper {
446                    matched = true;
447                    break;
448                }
449            }
450            if *negate {
451                !matched
452            } else {
453                matched
454            }
455        }
456        CompiledMatcher::Type(re) => {
457            if node.proxy_type == ProxyType::Unknown {
458                false
459            } else {
460                let type_str = PROXY_TYPES.get(&node.proxy_type).unwrap_or(&"UNKNOWN");
461                re.is_match(type_str)
462            }
463        }
464        CompiledMatcher::Port { ranges, negate } => {
465            let target = node.port as i32;
466            let mut matched = false;
467            for r in ranges {
468                if target >= r.lower && target <= r.upper {
469                    matched = true;
470                    break;
471                }
472            }
473            if *negate {
474                !matched
475            } else {
476                matched
477            }
478        }
479        CompiledMatcher::Server(re) => re.is_match(&node.hostname),
480        CompiledMatcher::Protocol(re) => node.protocol.as_ref().map_or(false, |p| re.is_match(p)),
481        CompiledMatcher::UdpSupport(re) => {
482            let udp_str = match node.udp {
483                Some(true) => "yes",
484                Some(false) => "no",
485                None => "undefined",
486            };
487            re.is_match(udp_str)
488        }
489        CompiledMatcher::Security(re) => {
490            let mut features = String::new();
491            if node.tls_secure {
492                features.push_str("TLS,");
493            }
494            if let Some(true) = node.allow_insecure {
495                features.push_str("INSECURE,");
496            }
497            if let Some(true) = node.tls13 {
498                features.push_str("TLS13,");
499            }
500            if !features.is_empty() {
501                features.pop();
502            } else {
503                features.push_str("NONE");
504            }
505            re.is_match(&features)
506        }
507        CompiledMatcher::Remarks(re) | CompiledMatcher::Plain(re) => re.is_match(&node.remark),
508        CompiledMatcher::AlwaysTrue => true,
509        CompiledMatcher::Invalid => false, // Invalid rules never match
510    };
511
512    // If there's a sub-rule, the overall result is the logical AND of the primary
513    // match and the sub-rule match.
514    match &compiled_rule.sub_rule {
515        Some(sub) => primary_match && apply_compiled_rule(sub, node),
516        None => primary_match,
517    }
518}
519
520/// Applies a pre-compiled rule against a simple string (like a remark).
521/// Used for `RegexMatchConfig`.
522pub fn apply_compiled_rule_to_string(compiled_rule: &CompiledRule, text: &str) -> bool {
523    // Only Plain and Remarks matchers directly apply to a simple string.
524    // Other matchers implicitly require a Proxy node context.
525    let primary_match = match &compiled_rule.matcher {
526        CompiledMatcher::Plain(re) | CompiledMatcher::Remarks(re) => re.is_match(text),
527        CompiledMatcher::AlwaysTrue => true,
528        CompiledMatcher::Invalid => false,
529        // For other types, when applied to a string, they don't match
530        _ => false,
531    };
532
533    // Sub-rules don't make sense when matching against a single string,
534    // as the context (Proxy node) is lost. We only consider the primary match.
535    // If a sub-rule exists, it implies the original rule was complex (e.g.,
536    // !!GROUP=X!!Y) and shouldn't have been compiled for simple string matching
537    // context. However, to be safe, let's return false if a sub-rule exists in
538    // this context.
539    if compiled_rule.sub_rule.is_some() {
540        false
541    } else {
542        primary_match
543    }
544}
545
546/// Replaces text using a pre-compiled regex.
547///
548/// # Arguments
549/// * `text` - The input text.
550/// * `re` - The pre-compiled regex object.
551/// * `replacement` - The replacement string (can use capture groups like $1,
552///   $name).
553/// * `replace_all` - Whether to replace all occurrences or just the first.
554/// * `literal` - Whether the replacement string should be treated literally (no
555///   capture group expansion).
556///
557/// # Returns
558/// * The text with replacements made.
559pub fn replace_with_compiled_regex(
560    text: &str,
561    re: &Regex,
562    replacement: &str,
563    replace_all: bool,
564    literal: bool,
565) -> String {
566    let result = if replace_all {
567        if literal {
568            re.replace_all(text, regex::NoExpand(replacement))
569        } else {
570            re.replace_all(text, replacement)
571        }
572    } else {
573        // Find the first match for non-literal replacement
574        if literal {
575            re.replacen(text, 1, regex::NoExpand(replacement))
576        } else {
577            re.replacen(text, 1, replacement)
578        }
579    };
580    result.into_owned() // Convert Cow<str> to String
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::models::ProxyType;
587
588    // Helper function to create a test proxy
589    fn create_test_proxy() -> Proxy {
590        Proxy {
591            id: 1,
592            group_id: 2,
593            group: "TestGroup".to_string(),
594            remark: "TestRemark".to_string(),
595            hostname: "example.com".to_string(),
596            port: 8080,
597            proxy_type: ProxyType::Shadowsocks,
598            protocol: Some("origin".to_string()),
599            udp: Some(true),
600            tls_secure: true,
601            tls13: Some(true),
602            ..Default::default()
603        }
604    }
605
606    #[test]
607    fn test_match_range_simple() {
608        assert!(match_range("5", 5));
609        assert!(!match_range("5", 6));
610    }
611
612    #[test]
613    fn test_match_range_with_ranges() {
614        assert!(match_range("1-10", 5));
615        assert!(!match_range("1-10", 11));
616    }
617
618    #[test]
619    fn test_match_range_with_negation() {
620        assert!(!match_range("!5", 5));
621        assert!(match_range("!5", 6));
622        assert!(!match_range("!1-10", 5));
623        assert!(match_range("!1-10", 11));
624    }
625
626    #[test]
627    fn test_match_range_with_multiple() {
628        assert!(match_range("1-5,10-15", 3));
629        assert!(match_range("1-5,10-15", 12));
630        assert!(!match_range("1-5,10-15", 7));
631    }
632
633    #[test]
634    fn test_match_range_complex() {
635        assert!(match_range("!1-5,10,15-20", 12));
636        assert!(!match_range("!1-5,10,15-20", 10));
637        assert!(!match_range("!1-5,10,15-20", 3));
638        assert!(match_range("!1-5,10,15-20", 6));
639    }
640
641    #[test]
642    fn test_reg_find() {
643        assert!(reg_find("This is a test", "test"));
644        assert!(reg_find("This is a test", "TEST")); // Case insensitive
645        assert!(!reg_find("This is a test", "banana"));
646        assert!(reg_find("This is a test", "")); // Empty pattern always matches
647    }
648
649    #[test]
650    fn test_reg_match() {
651        assert!(reg_match("12345", r"^\d+$"));
652        assert!(!reg_match("12345a", r"^\d+$"));
653        assert!(reg_match("HELLO", r"(?i)hello"));
654    }
655
656    #[test]
657    fn test_apply_matcher_group() {
658        let node = create_test_proxy();
659        let mut real_rule = String::new();
660
661        assert!(apply_matcher("!!GROUP=TestGroup", &mut real_rule, &node));
662        assert_eq!(real_rule, "");
663
664        real_rule.clear();
665        assert!(!apply_matcher("!!GROUP=OtherGroup", &mut real_rule, &node));
666    }
667
668    #[test]
669    fn test_apply_matcher_type() {
670        let node = create_test_proxy();
671        let mut real_rule = String::new();
672
673        assert!(apply_matcher("!!TYPE=SS", &mut real_rule, &node));
674        assert_eq!(real_rule, "");
675
676        real_rule.clear();
677        assert!(!apply_matcher("!!TYPE=VMess", &mut real_rule, &node));
678    }
679
680    #[test]
681    fn test_apply_matcher_port() {
682        let node = create_test_proxy();
683        let mut real_rule = String::new();
684
685        assert!(apply_matcher("!!PORT=8080", &mut real_rule, &node));
686        assert_eq!(real_rule, "");
687
688        real_rule.clear();
689        assert!(apply_matcher("!!PORT=8000-9000", &mut real_rule, &node));
690
691        real_rule.clear();
692        assert!(!apply_matcher("!!PORT=443", &mut real_rule, &node));
693    }
694
695    #[test]
696    fn test_apply_matcher_server() {
697        let node = create_test_proxy();
698        let mut real_rule = String::new();
699
700        assert!(apply_matcher("!!SERVER=example", &mut real_rule, &node));
701        assert_eq!(real_rule, "");
702
703        real_rule.clear();
704        assert!(!apply_matcher("!!SERVER=google", &mut real_rule, &node));
705    }
706
707    #[test]
708    fn test_apply_matcher_protocol() {
709        let node = create_test_proxy();
710        let mut real_rule = String::new();
711
712        assert!(apply_matcher("!!PROTOCOL=origin", &mut real_rule, &node));
713        assert_eq!(real_rule, "");
714
715        real_rule.clear();
716        assert!(!apply_matcher(
717            "!!PROTOCOL=auth_sha1",
718            &mut real_rule,
719            &node
720        ));
721    }
722
723    #[test]
724    fn test_apply_matcher_udp_support() {
725        let node = create_test_proxy();
726        let mut real_rule = String::new();
727
728        assert!(apply_matcher("!!UDPSUPPORT=yes", &mut real_rule, &node));
729        assert_eq!(real_rule, "");
730
731        real_rule.clear();
732        assert!(!apply_matcher("!!UDPSUPPORT=no", &mut real_rule, &node));
733
734        // Test with undefined UDP support
735        let mut node_no_udp = node.clone();
736        node_no_udp.udp = None;
737
738        real_rule.clear();
739        assert!(apply_matcher(
740            "!!UDPSUPPORT=undefined",
741            &mut real_rule,
742            &node_no_udp
743        ));
744    }
745
746    #[test]
747    fn test_apply_matcher_security() {
748        let node = create_test_proxy();
749        let mut real_rule = String::new();
750
751        assert!(apply_matcher("!!SECURITY=TLS", &mut real_rule, &node));
752        assert_eq!(real_rule, "");
753
754        real_rule.clear();
755        assert!(apply_matcher("!!SECURITY=TLS13", &mut real_rule, &node));
756
757        real_rule.clear();
758        assert!(!apply_matcher("!!SECURITY=INSECURE", &mut real_rule, &node));
759
760        // Test with insecure allowed
761        let mut node_insecure = node.clone();
762        node_insecure.allow_insecure = Some(true);
763
764        real_rule.clear();
765        assert!(apply_matcher(
766            "!!SECURITY=INSECURE",
767            &mut real_rule,
768            &node_insecure
769        ));
770    }
771
772    #[test]
773    fn test_apply_matcher_remarks() {
774        let node = create_test_proxy();
775        let mut real_rule = String::new();
776
777        assert!(apply_matcher("!!REMARKS=Test", &mut real_rule, &node));
778        assert_eq!(real_rule, "");
779
780        real_rule.clear();
781        assert!(!apply_matcher("!!REMARKS=Premium", &mut real_rule, &node));
782    }
783
784    #[test]
785    fn test_apply_matcher_with_trailing_rule() {
786        let node = create_test_proxy();
787        let mut real_rule = String::new();
788
789        assert!(apply_matcher(
790            "!!GROUP=TestGroup!!.+",
791            &mut real_rule,
792            &node
793        ));
794        assert_eq!(real_rule, ".+");
795
796        // The trailing rule ".+" will be used with node.remark in the parent
797        // function
798    }
799
800    // Helper for creating test proxies
801    fn create_proxy_for_compile_test(
802        group: &str,
803        group_id: i32,
804        ptype: ProxyType,
805        port: u16,
806        hostname: &str,
807        protocol: Option<&str>,
808        udp: Option<bool>,
809        tls: bool,
810        insecure: Option<bool>,
811        tls13: Option<bool>,
812        remark: &str,
813    ) -> Proxy {
814        Proxy {
815            id: 1, // Not usually matched
816            group_id: group_id,
817            group: group.to_string(),
818            remark: remark.to_string(),
819            hostname: hostname.to_string(),
820            port,
821            proxy_type: ptype,
822            protocol: protocol.map(|s| s.to_string()),
823            udp,
824            tls_secure: tls,
825            allow_insecure: insecure,
826            tls13,
827            ..Default::default()
828        }
829    }
830
831    #[test]
832    fn test_compile_rule_plain_regex() {
833        let rule = compile_rule("some_remark_pattern");
834        assert!(matches!(rule.matcher, CompiledMatcher::Plain(_)));
835        assert!(rule.sub_rule.is_none());
836        let node = create_proxy_for_compile_test(
837            "G",
838            1,
839            ProxyType::HTTP,
840            80,
841            "h",
842            None,
843            None,
844            false,
845            None,
846            None,
847            "this matches some_remark_pattern",
848        );
849        assert!(apply_compiled_rule(&rule, &node));
850        assert!(apply_compiled_rule_to_string(
851            &rule,
852            "this matches some_remark_pattern"
853        ));
854        assert!(!apply_compiled_rule_to_string(&rule, "no match here"));
855    }
856
857    #[test]
858    fn test_compile_rule_group() {
859        let rule = compile_rule("!!GROUP=Test.*");
860        assert!(matches!(rule.matcher, CompiledMatcher::Group(_)));
861        assert!(rule.sub_rule.is_none());
862        let node = create_proxy_for_compile_test(
863            "TestGroup",
864            1,
865            ProxyType::HTTP,
866            80,
867            "h",
868            None,
869            None,
870            false,
871            None,
872            None,
873            "remark",
874        );
875        assert!(apply_compiled_rule(&rule, &node));
876        let node2 = create_proxy_for_compile_test(
877            "OtherGroup",
878            1,
879            ProxyType::HTTP,
880            80,
881            "h",
882            None,
883            None,
884            false,
885            None,
886            None,
887            "remark",
888        );
889        assert!(!apply_compiled_rule(&rule, &node2));
890    }
891
892    #[test]
893    fn test_compile_rule_group_with_subrule() {
894        let rule = compile_rule("!!GROUP=Test.*!!PORT=80");
895        assert!(matches!(rule.matcher, CompiledMatcher::Group(_)));
896        assert!(rule.sub_rule.is_some());
897        assert!(matches!(
898            rule.sub_rule.as_ref().unwrap().matcher,
899            CompiledMatcher::Port { .. }
900        ));
901
902        let node_match = create_proxy_for_compile_test(
903            "TestGroup",
904            1,
905            ProxyType::HTTP,
906            80,
907            "h",
908            None,
909            None,
910            false,
911            None,
912            None,
913            "remark",
914        );
915        let node_no_group = create_proxy_for_compile_test(
916            "OtherGroup",
917            1,
918            ProxyType::HTTP,
919            80,
920            "h",
921            None,
922            None,
923            false,
924            None,
925            None,
926            "remark",
927        );
928        let node_no_port = create_proxy_for_compile_test(
929            "TestGroup",
930            1,
931            ProxyType::HTTP,
932            81,
933            "h",
934            None,
935            None,
936            false,
937            None,
938            None,
939            "remark",
940        );
941
942        assert!(apply_compiled_rule(&rule, &node_match));
943        assert!(!apply_compiled_rule(&rule, &node_no_group));
944        assert!(!apply_compiled_rule(&rule, &node_no_port));
945    }
946
947    #[test]
948    fn test_compile_rule_groupid() {
949        // Note: Doesn't handle !!INSERT= direction multiplier directly in compiled
950        // struct yet
951        let rule = compile_rule("!!GROUPID=1-5,10");
952        assert!(matches!(rule.matcher, CompiledMatcher::GroupId { .. }));
953        let node = create_proxy_for_compile_test(
954            "G",
955            3,
956            ProxyType::HTTP,
957            80,
958            "h",
959            None,
960            None,
961            false,
962            None,
963            None,
964            "remark",
965        );
966        assert!(apply_compiled_rule(&rule, &node));
967        let node2 = create_proxy_for_compile_test(
968            "G",
969            7,
970            ProxyType::HTTP,
971            80,
972            "h",
973            None,
974            None,
975            false,
976            None,
977            None,
978            "remark",
979        );
980        assert!(!apply_compiled_rule(&rule, &node2));
981        let node3 = create_proxy_for_compile_test(
982            "G",
983            10,
984            ProxyType::HTTP,
985            80,
986            "h",
987            None,
988            None,
989            false,
990            None,
991            None,
992            "remark",
993        );
994        assert!(apply_compiled_rule(&rule, &node3));
995    }
996
997    #[test]
998    fn test_compile_rule_port_negated() {
999        let rule = compile_rule("!!PORT=!80,443");
1000        assert!(matches!(
1001            rule.matcher,
1002            CompiledMatcher::Port { negate: true, .. }
1003        ));
1004        let node_80 = create_proxy_for_compile_test(
1005            "G",
1006            1,
1007            ProxyType::HTTP,
1008            80,
1009            "h",
1010            None,
1011            None,
1012            false,
1013            None,
1014            None,
1015            "remark",
1016        );
1017        let node_443 = create_proxy_for_compile_test(
1018            "G",
1019            1,
1020            ProxyType::HTTPS,
1021            443,
1022            "h",
1023            None,
1024            None,
1025            false,
1026            None,
1027            None,
1028            "remark",
1029        );
1030        let node_other = create_proxy_for_compile_test(
1031            "G",
1032            1,
1033            ProxyType::Socks5,
1034            1080,
1035            "h",
1036            None,
1037            None,
1038            false,
1039            None,
1040            None,
1041            "remark",
1042        );
1043        assert!(!apply_compiled_rule(&rule, &node_80));
1044        assert!(!apply_compiled_rule(&rule, &node_443));
1045        assert!(apply_compiled_rule(&rule, &node_other));
1046    }
1047
1048    #[test]
1049    fn test_compile_rule_type() {
1050        let rule = compile_rule("!!TYPE=S(S|OCKS5)"); // Matches SS or SOCKS5
1051        assert!(matches!(rule.matcher, CompiledMatcher::Type(_)));
1052        let node_ss = create_proxy_for_compile_test(
1053            "G",
1054            1,
1055            ProxyType::Shadowsocks,
1056            80,
1057            "h",
1058            None,
1059            None,
1060            false,
1061            None,
1062            None,
1063            "remark",
1064        );
1065        let node_socks = create_proxy_for_compile_test(
1066            "G",
1067            1,
1068            ProxyType::Socks5,
1069            1080,
1070            "h",
1071            None,
1072            None,
1073            false,
1074            None,
1075            None,
1076            "remark",
1077        );
1078        let node_vmess = create_proxy_for_compile_test(
1079            "G",
1080            1,
1081            ProxyType::VMess,
1082            80,
1083            "h",
1084            None,
1085            None,
1086            false,
1087            None,
1088            None,
1089            "remark",
1090        );
1091        assert!(apply_compiled_rule(&rule, &node_ss));
1092        assert!(apply_compiled_rule(&rule, &node_socks));
1093        assert!(!apply_compiled_rule(&rule, &node_vmess));
1094    }
1095
1096    #[test]
1097    fn test_compile_rule_security() {
1098        let rule = compile_rule("!!SECURITY=TLS,TLS13");
1099        assert!(matches!(rule.matcher, CompiledMatcher::Security(_)));
1100
1101        let node_match = create_proxy_for_compile_test(
1102            "G",
1103            1,
1104            ProxyType::Trojan,
1105            443,
1106            "h",
1107            None,
1108            None,
1109            true,
1110            None,
1111            Some(true),
1112            "remark",
1113        );
1114        let node_no_tls13 = create_proxy_for_compile_test(
1115            "G",
1116            1,
1117            ProxyType::Trojan,
1118            443,
1119            "h",
1120            None,
1121            None,
1122            true,
1123            None,
1124            Some(false),
1125            "remark",
1126        );
1127        let node_insecure = create_proxy_for_compile_test(
1128            "G",
1129            1,
1130            ProxyType::Trojan,
1131            443,
1132            "h",
1133            None,
1134            None,
1135            true,
1136            Some(true),
1137            Some(true),
1138            "remark",
1139        ); // Also matches "TLS," part
1140
1141        assert!(apply_compiled_rule(&rule, &node_match)); // Matches "TLS,TLS13"
1142        assert!(apply_compiled_rule(&rule, &node_no_tls13)); // Matches "TLS" part
1143        assert!(apply_compiled_rule(&rule, &node_insecure)); // Matches "TLS,INSECURE,TLS13" -> contains "TLS,"
1144    }
1145}