import re from .net_hostname_validate_types import HostnameValidation ALLOWED = re.compile(r"[A-Za-z0-9.-]*") DIGITS = re.compile(r"[0-9]+") def validate_hostname(text: str) -> HostnameValidation: """Check a host name against the RFC 1123 / RFC 952 rules and normalise it: lower-case, one trailing dot (a fully qualified name) removed. Checks run in a fixed order and the first failure is the reason: empty, characters, total length, then each label in turn, then the top-level label. """ if not isinstance(text, str): return HostnameValidation(valid=False, hostname=None, reason="not text", labels=0) name = text[:-1] if text.endswith(".") else text labels = [] if len(name) == 0 else name.split(".") def fail(reason: str) -> HostnameValidation: return HostnameValidation(valid=False, hostname=None, reason=reason, labels=len(labels)) if len(name) == 0: return fail("empty") # Characters first: once the text is known to be ASCII, every language # counts its length the same way. if not ALLOWED.fullmatch(name): return fail("character other than a letter, digit, hyphen or dot") if len(name) > 253: return fail("longer than 253 characters") for label in labels: if len(label) == 0: return fail("empty label") if len(label) > 63: return fail("label longer than 63 characters") if label.startswith("-") or label.endswith("-"): return fail("label starts or ends with a hyphen") # RFC 3696 section 2: a top-level domain is never all-numeric, which keeps # 1.2.3.4 an address rather than a name. if DIGITS.fullmatch(labels[-1]): return fail("top-level label is all numeric") return HostnameValidation(valid=True, hostname=name.lower(), reason=None, labels=len(labels))