Port of the JavaScript Validator class. Performs strict validation and cleans up invalid rules + preceding comments.
| 59 | # --- Logic Ported from JavaScript (Validator) --- |
| 60 | |
| 61 | class RuleValidator: |
| 62 | """ |
| 63 | Port of the JavaScript Validator class. |
| 64 | Performs strict validation and cleans up invalid rules + preceding comments. |
| 65 | """ |
| 66 | def __init__(self, allow_ip: bool = False): |
| 67 | self.allow_ip = allow_ip |
| 68 | self._prev_removed = False |
| 69 | |
| 70 | @staticmethod |
| 71 | def is_comment(line: str) -> bool: |
| 72 | return line.lstrip().startswith(('!', '#')) |
| 73 | |
| 74 | def _valid_hostname(self, hostname: str, rule_text: str, has_limit_mod: bool) -> bool: |
| 75 | """Checks if hostname is valid. Imitates tldts logic with standard lib.""" |
| 76 | hostname = hostname.strip().lower() |
| 77 | |
| 78 | if not hostname: |
| 79 | return False |
| 80 | |
| 81 | # IP Check |
| 82 | is_ip = bool(re.match(r"^\d{1,3}(\.\d{1,3}){3}$", hostname)) |
| 83 | if is_ip and not self.allow_ip: |
| 84 | logger.debug(f"Invalid hostname (IP not allowed): {hostname} in {rule_text}") |
| 85 | return False |
| 86 | |
| 87 | # Public Suffix Check (Heuristic approximation for stdlib) |
| 88 | if '.' not in hostname and not is_ip and not has_limit_mod and hostname != "localhost": |
| 89 | logger.debug(f"Matching whole TLD/Suffix not allowed: {hostname} in {rule_text}") |
| 90 | return False |
| 91 | |
| 92 | return True |
| 93 | |
| 94 | def _valid_etc_hosts(self, rule: str) -> bool: |
| 95 | parts = rule.split() |
| 96 | # Remove comments/IP |
| 97 | hosts = [p for p in parts if not p.startswith(('#', '!'))] |
| 98 | if len(hosts) < 2: |
| 99 | return False |
| 100 | |
| 101 | hostnames = hosts[1:] # 0 is IP |
| 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: |