(self, rule: str)
| 102 | return all(self._valid_hostname(h, rule, False) for h in hostnames) |
| 103 | |
| 104 | def _valid_adblock(self, rule: str) -> bool: |
| 105 | # 1. Parse Modifiers (Simplified parser) |
| 106 | has_limit_mod = False |
| 107 | if '$' in rule: |
| 108 | try: |
| 109 | base, opts = rule.split('$', 1) |
| 110 | options = opts.split(',') |
| 111 | for opt in options: |
| 112 | name = opt.split('=', 1)[0].strip() |
| 113 | if name not in SUPPORTED_MODIFIERS: |
| 114 | logger.debug(f"Unsupported modifier {name}: {rule}") |
| 115 | return False |
| 116 | if name in ANY_PATTERN_MODIFIER: |
| 117 | has_limit_mod = True |
| 118 | except ValueError: |
| 119 | return False |
| 120 | else: |
| 121 | base = rule |
| 122 | |
| 123 | trimmed_pattern = base.strip() |
| 124 | |
| 125 | # 2. Length Check |
| 126 | if len(trimmed_pattern) < 5: |
| 127 | logger.debug(f"Rule too short: {rule}") |
| 128 | return False |
| 129 | |
| 130 | # 3. Char Check (Skip regex rules /.../) |
| 131 | if not (trimmed_pattern.startswith('/') and trimmed_pattern.endswith('/')): |
| 132 | to_test = trimmed_pattern.lstrip(':/') # Handle :// |
| 133 | if not RE_VALID_DOMAIN_CHARS.match(to_test): |
| 134 | logger.debug(f"Invalid chars in rule: {rule}") |
| 135 | return False |
| 136 | |
| 137 | # 4. Domain Validation |
| 138 | # Check standard ||domain^ pattern |
| 139 | if trimmed_pattern.startswith(DOMAIN_PREFIX) and DOMAIN_SEPARATOR in trimmed_pattern: |
| 140 | sep_idx = trimmed_pattern.find(DOMAIN_SEPARATOR) |
| 141 | wild_idx = trimmed_pattern.find(WILDCARD) |
| 142 | |
| 143 | # Invalid: ||example.org^test* |
| 144 | if wild_idx != -1 and wild_idx > sep_idx: |
| 145 | return False |
| 146 | |
| 147 | domain_part = trimmed_pattern[len(DOMAIN_PREFIX):sep_idx] |
| 148 | |
| 149 | # Handle wildcards |
| 150 | if wild_idx != -1: |
| 151 | # Logic: If ||*.example.com^, check example.com |
| 152 | if domain_part.startswith(WILDCARD_PART): |
| 153 | clean_domain = domain_part.replace(WILDCARD_PART, "") |
| 154 | if '.' not in clean_domain: |
| 155 | return self._valid_hostname(clean_domain, rule, has_limit_mod) |
| 156 | return True |
| 157 | return True |
| 158 | |
| 159 | if not self._valid_hostname(domain_part, rule, has_limit_mod): |
| 160 | return False |
| 161 |
no test coverage detected