From c66ee73b458fe2584df6beafd57e708c7e1ccfe9 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 23 Oct 2026 00:37:14 +0800 Subject: [PATCH] Fix domain MRS pattern generation --- scripts/gfwlist_parser.py | 17 +++++-- scripts/tests/test_mrs_domain_patterns.py | 58 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 scripts/tests/test_mrs_domain_patterns.py diff --git a/scripts/gfwlist_parser.py b/scripts/gfwlist_parser.py index a410883..ab89e4b 100644 --- a/scripts/gfwlist_parser.py +++ b/scripts/gfwlist_parser.py @@ -214,6 +214,15 @@ def parse_list_rule_line(line): def is_domain_rule_type(rule_type): return rule_type in ('DOMAIN', 'DOMAIN-SUFFIX', 'DOMAIN-KEYWORD', 'DOMAIN-REGEX') +def format_mrs_domain_rule(rule_type, rule_value): + """Translate supported Clash domain rules to MRS domain patterns.""" + if rule_type == 'DOMAIN': + return rule_value + if rule_type == 'DOMAIN-SUFFIX': + # MRS uses +. to match both the suffix and its root domain. + return f'+.{rule_value}' + return f'{rule_type},{rule_value}' + def is_ip_rule_type(rule_type): return rule_type in ('IP-CIDR', 'IP-CIDR6', 'IP-SUFFIX', 'IP-ASN') @@ -257,7 +266,7 @@ def split_yaml_file(filepath): unique_domains = sorted(set(domains)) domain_yaml = "payload:\n" for rule_type, d in unique_domains: - domain_yaml += f" - {rule_type},{d}\n" + domain_yaml += f" - {format_mrs_domain_rule(rule_type, d)}\n" write_file(f"{base_name}_domain.yaml", domain_yaml) if ips: unique_ips = sorted(set(ips)) @@ -290,7 +299,7 @@ def split_list_file(filepath): unique_domains = sorted(set(domains)) domain_text = "" for rule_type, d in unique_domains: - domain_text += f"{rule_type},{d}\n" + domain_text += f"{format_mrs_domain_rule(rule_type, d)}\n" write_file(f"{base_name}_domain.list", domain_text) if ips: unique_ips = sorted(set(ips)) @@ -343,7 +352,7 @@ def generate_clash_provider_yaml(domain_list, ip_list, filename, title="payload" if unique_domains: domain_yaml = f"{title}:\n" for domain in unique_domains: - domain_yaml += f" - DOMAIN-SUFFIX,{domain}\n" + domain_yaml += f" - {format_mrs_domain_rule('DOMAIN-SUFFIX', domain)}\n" write_file(f"{base_name}_domain.yaml", domain_yaml) # IP 规则 - YAML 格式 @@ -372,7 +381,7 @@ def generate_clash_ruleset_list(domain_list, ip_list, filename, title="GFWList") if unique_domains: domain_text = "" for domain in unique_domains: - domain_text += f"DOMAIN-SUFFIX,{domain}\n" + domain_text += f"{format_mrs_domain_rule('DOMAIN-SUFFIX', domain)}\n" write_file(f"{base_name}_domain.list", domain_text) # IP 规则 - text 格式 diff --git a/scripts/tests/test_mrs_domain_patterns.py b/scripts/tests/test_mrs_domain_patterns.py new file mode 100644 index 0000000..e201cbd --- /dev/null +++ b/scripts/tests/test_mrs_domain_patterns.py @@ -0,0 +1,58 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).resolve().parents[1] / "gfwlist_parser.py" +SPEC = importlib.util.spec_from_file_location("gfwlist_parser", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +PARSER = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PARSER) + + +class MrsDomainPatternTests(unittest.TestCase): + def test_generated_clash_rules_are_preserved_and_mrs_inputs_are_patterns(self): + domain = "synthetic.example.invalid" + ip = "192.0.2.1" + + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + provider = root / "provider.yaml" + ruleset = root / "ruleset.list" + + PARSER.generate_clash_provider_yaml([domain], [ip], str(provider)) + PARSER.generate_clash_ruleset_list([domain], [ip], str(ruleset)) + PARSER.split_yaml_file(str(provider)) + PARSER.split_list_file(str(ruleset)) + + self.assertIn(f"DOMAIN-SUFFIX,{domain}", provider.read_text(encoding="utf-8")) + self.assertIn(f"DOMAIN-SUFFIX,{domain}\n", ruleset.read_text(encoding="utf-8")) + self.assertEqual( + (root / "provider_domain.yaml").read_text(encoding="utf-8"), + f"payload:\n - +.{domain}\n", + ) + self.assertEqual( + (root / "ruleset_domain.list").read_text(encoding="utf-8"), + f"+.{domain}\n", + ) + self.assertEqual( + (root / "provider_ip.yaml").read_text(encoding="utf-8"), + f"payload:\n - {ip}/32\n", + ) + self.assertEqual( + (root / "ruleset_ip.list").read_text(encoding="utf-8"), + f"{ip}/32\n", + ) + + def test_exact_and_suffix_rule_mappings(self): + domain = "synthetic.example.invalid" + self.assertEqual(PARSER.format_mrs_domain_rule("DOMAIN", domain), domain) + self.assertEqual( + PARSER.format_mrs_domain_rule("DOMAIN-SUFFIX", domain), + f"+.{domain}", + ) + + +if __name__ == "__main__": + unittest.main()