wwww

import re

class DomainError(ValueError):
    pass

_LABEL_RE_ASCII = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$", re.IGNORECASE)

def _to_idna(label: str) -> str:
    """
    Convert a potentially-unicode label to ASCII using IDNA.
    Empty labels are returned as-is (used to skip optional parts).
    """
    if label is None:
        return ""
    label = label.strip()
    if not label:
        return ""
    try:
        return label.encode("idna").decode("ascii")
    except Exception as e:
        raise DomainError(f"Invalid internationalized label {label!r}: {e}") from e

def _validate_ascii_label(label: str, *, part_name: str) -> None:
    if not (1 <= len(label) <= 63):
        raise DomainError(f"{part_name} label length must be 1..63, got {len(label)} for {label!r}")
    if not _LABEL_RE_ASCII.match(label):
        raise DomainError(
            f"{part_name} label {label!r} must be alphanumeric or hyphen, "
            "not start/end with hyphen."
        )

def create_fqdn(subdomain: str | None, sld: str, tld: str, *, trailing_dot: bool = False) -> str:
    """
    Build a validated FQDN from subdomain, SLD, and TLD.

    Args:
        subdomain: e.g., "www", "api", "a.b" or None / "" for no subdomain.
                   Can include multiple dot-separated labels.
        sld: second-level domain (e.g., "example").
        tld: top-level domain (e.g., "com" or "co.uk" — multi-label TLDs supported).
        trailing_dot: append a final dot (.) for absolute FQDN (useful for DNS zones).

    Returns:
        The ASCII (IDNA/punycode) FQDN in lowercase.

    Raises:
        DomainError on any validation problem.
    """
    # Normalize to IDNA ASCII (handles Unicode inputs)
    sub_ascii = _to_idna(subdomain)
    sld_ascii = _to_idna(sld)
    tld_ascii = _to_idna(tld)

    if not sld_ascii:
        raise DomainError("SLD is required.")
    if not tld_ascii:
        raise DomainError("TLD is required.")

    # Split into labels
    sub_labels = [l for l in sub_ascii.split(".") if l] if sub_ascii else []
    sld_labels = [sld_ascii]
    tld_labels = [l for l in tld_ascii.split(".") if l]

    # Validate each ASCII label (RFC 1035/1123 style)
    for lbl in sub_labels:
        _validate_ascii_label(lbl, part_name="Subdomain")
    for lbl in sld_labels:
        _validate_ascii_label(lbl, part_name="SLD")
    for lbl in tld_labels:
        _validate_ascii_label(lbl, part_name="TLD")

    labels = [*sub_labels, *sld_labels, *tld_labels]
    fqdn = ".".join(labels).lower()

    if len(fqdn) > 253:
        # RFC says 255 octets incl. trailing dot; here we enforce 253 without it
        raise DomainError(f"FQDN too long ({len(fqdn)} characters, max 253): {fqdn!r}")

    if trailing_dot:
        fqdn += "."

    return fqdn


# ------------------
# Example usages
# ------------------
if __name__ == "__main__":
    print(create_fqdn("blog", "example", "com")) # blog.example.com
    print(create_fqdn("a.b", "example", "co.uk")) # a.b.example.co.uk
    print(create_fqdn("", "bücher", "de")) # xn--bcher-kva.de
    print(create_fqdn("服务", "例子", "中国", trailing_dot=True)) # xn--serv-1s1u.xn--fsqu00a.xn--fiqs8s.

Comments

Popular posts from this blog

Crypto Market Update – August 7, 2025

ce1