Functional Weave
Code in Rust

net.hostname-validate@1.0.0

impl/python.py

1,856 bytes · the Python implementation · view raw

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))