this module implements the rfcs 3490 idna and 3491 nameprep idna section 3 1 idna section 5 this assumes query strings so allowunassigned is true map map to nothing normalize prohibit check bidi there is a randal char in the string must perform further tests 1 the characters in section 5 8 must be prohibited this is table c 8 which was already checked 2 if a string contains any randalcat character the string must not contain any lcat character 3 if a string contains any randalcat character a randalcat character must be the first character of the string and a randalcat character must be the last character of the string step 1 try ascii skip to step 3 usestd3asciirules is false so skip to step 8 step 2 nameprep step 3 usestd3asciirules is false step 4 try ascii skip to step 8 step 5 check ace prefix step 6 encode with punycode step 7 prepend ace prefix step 8 check size protection from https github compythoncpythonissues98433 https datatracker ietf orgdochtmlrfc5894section6 doesn t specify a label size limit prior to nameprep but having one makes practical sense this leaves ample room for nameprep to remove nothing characters per https www rfceditor orgrfcrfc3454section3 1 while still preventing us from wasting time decoding a big thing that ll just hit the actual 63 length limit in step 6 step 1 check for ascii step 2 perform nameprep it doesn t say this but apparently it should be ascii now step 3 check for ace prefix step 4 remove ace prefix step 5 decode using punycode step 6 apply toascii step 7 compare the result of step 6 with the one of step 3 label2 will already be in lower case step 8 return the result of step 5 codec apis idna is quite clear that implementations must be strict ascii name fast path join with u002e idna allows decoding to operate on unicode strings too xxx obviously wrong see 3232 fast path idna is quite clear that implementations must be strict keep potentially unfinished label until the next call join with u002e idna allows decoding to operate on unicode strings too must be ascii string keep potentially unfinished label until the next call encodings module api this module implements the rfcs 3490 idna and 3491 nameprep idna section 3 1 idna section 5 this assumes query strings so allowunassigned is true map map to nothing normalize prohibit check bidi there is a randal char in the string must perform further tests 1 the characters in section 5 8 must be prohibited this is table c 8 which was already checked 2 if a string contains any randalcat character the string must not contain any lcat character 3 if a string contains any randalcat character a randalcat character must be the first character of the string and a randalcat character must be the last character of the string step 1 try ascii skip to step 3 usestd3asciirules is false so skip to step 8 step 2 nameprep step 3 usestd3asciirules is false step 4 try ascii skip to step 8 step 5 check ace prefix step 6 encode with punycode step 7 prepend ace prefix step 8 check size protection from https github com python cpython issues 98433 https datatracker ietf org doc html rfc5894 section 6 doesn t specify a label size limit prior to nameprep but having one makes practical sense this leaves ample room for nameprep to remove nothing characters per https www rfc editor org rfc rfc3454 section 3 1 while still preventing us from wasting time decoding a big thing that ll just hit the actual 63 length limit in step 6 step 1 check for ascii step 2 perform nameprep it doesn t say this but apparently it should be ascii now step 3 check for ace prefix step 4 remove ace prefix step 5 decode using punycode step 6 apply toascii step 7 compare the result of step 6 with the one of step 3 label2 will already be in lower case step 8 return the result of step 5 codec apis idna is quite clear that implementations must be strict ascii name fast path join with u 002e idna allows decoding to operate on unicode strings too xxx obviously wrong see 3232 fast path idna is quite clear that implementations must be strict keep potentially unfinished label until the next call join with u 002e idna allows decoding to operate on unicode strings too must be ascii string keep potentially unfinished label until the next call encodings module api
import stringprep, re, codecs
from unicodedata import ucd_3_2_0 as unicodedata


dots = re.compile("[\u002E\u3002\uFF0E\uFF61]")


ace_prefix = b"xn--"
sace_prefix = "xn--"


def nameprep(label):
    
    newlabel = []
    for c in label:
        if stringprep.in_table_b1(c):
            
            continue
        newlabel.append(stringprep.map_table_b2(c))
    label = "".join(newlabel)

    
    label = unicodedata.normalize("NFKC", label)

    
    for c in label:
        if stringprep.in_table_c12(c) or \
           stringprep.in_table_c22(c) or \
           stringprep.in_table_c3(c) or \
           stringprep.in_table_c4(c) or \
           stringprep.in_table_c5(c) or \
           stringprep.in_table_c6(c) or \
           stringprep.in_table_c7(c) or \
           stringprep.in_table_c8(c) or \
           stringprep.in_table_c9(c):
            raise UnicodeError("Invalid character %r" % c)

    
    RandAL = [stringprep.in_table_d1(x) for x in label]
    if any(RandAL):
        
        
        
        
        
        
        if any(stringprep.in_table_d2(x) for x in label):
            raise UnicodeError("Violation of BIDI requirement 2")
        
        
        
        
        if not RandAL[0] or not RandAL[-1]:
            raise UnicodeError("Violation of BIDI requirement 3")

    return label

def ToASCII(label):
    try:
        
        label = label.encode("ascii")
    except UnicodeError:
        pass
    else:
        
        
        if 0 < len(label) < 64:
            return label
        raise UnicodeError("label empty or too long")

    
    label = nameprep(label)

    
    
    try:
        label = label.encode("ascii")
    except UnicodeError:
        pass
    else:
        
        if 0 < len(label) < 64:
            return label
        raise UnicodeError("label empty or too long")

    
    if label.startswith(sace_prefix):
        raise UnicodeError("Label starts with ACE prefix")

    
    label = label.encode("punycode")

    
    label = ace_prefix + label

    
    if 0 < len(label) < 64:
        return label
    raise UnicodeError("label empty or too long")

def ToUnicode(label):
    if len(label) > 1024:
        
        
        
        
        
        
        
        
        raise UnicodeError("label way too long")
    
    if isinstance(label, bytes):
        pure_ascii = True
    else:
        try:
            label = label.encode("ascii")
            pure_ascii = True
        except UnicodeError:
            pure_ascii = False
    if not pure_ascii:
        
        label = nameprep(label)
        
        try:
            label = label.encode("ascii")
        except UnicodeError:
            raise UnicodeError("Invalid character in IDN label")
    
    if not label.startswith(ace_prefix):
        return str(label, "ascii")

    
    label1 = label[len(ace_prefix):]

    
    result = label1.decode("punycode")

    
    label2 = ToASCII(result)

    
    
    if str(label, "ascii").lower() != str(label2, "ascii"):
        raise UnicodeError("IDNA does not round-trip", label, label2)

    
    return result



class Codec(codecs.Codec):
    def encode(self, input, errors='strict'):

        if errors != 'strict':
            
            raise UnicodeError("unsupported error handling "+errors)

        if not input:
            return b'', 0

        try:
            result = input.encode('ascii')
        except UnicodeEncodeError:
            pass
        else:
            
            labels = result.split(b'.')
            for label in labels[:-1]:
                if not (0 < len(label) < 64):
                    raise UnicodeError("label empty or too long")
            if len(labels[-1]) >= 64:
                raise UnicodeError("label too long")
            return result, len(input)

        result = bytearray()
        labels = dots.split(input)
        if labels and not labels[-1]:
            trailing_dot = b'.'
            del labels[-1]
        else:
            trailing_dot = b''
        for label in labels:
            if result:
                
                result.extend(b'.')
            result.extend(ToASCII(label))
        return bytes(result+trailing_dot), len(input)

    def decode(self, input, errors='strict'):

        if errors != 'strict':
            raise UnicodeError("Unsupported error handling "+errors)

        if not input:
            return "", 0

        
        if not isinstance(input, bytes):
            
            input = bytes(input)

        if ace_prefix not in input:
            
            try:
                return input.decode('ascii'), len(input)
            except UnicodeDecodeError:
                pass

        labels = input.split(b".")

        if labels and len(labels[-1]) == 0:
            trailing_dot = '.'
            del labels[-1]
        else:
            trailing_dot = ''

        result = []
        for label in labels:
            result.append(ToUnicode(label))

        return ".".join(result)+trailing_dot, len(input)

class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
    def _buffer_encode(self, input, errors, final):
        if errors != 'strict':
            
            raise UnicodeError("unsupported error handling "+errors)

        if not input:
            return (b'', 0)

        labels = dots.split(input)
        trailing_dot = b''
        if labels:
            if not labels[-1]:
                trailing_dot = b'.'
                del labels[-1]
            elif not final:
                
                del labels[-1]
                if labels:
                    trailing_dot = b'.'

        result = bytearray()
        size = 0
        for label in labels:
            if size:
                
                result.extend(b'.')
                size += 1
            result.extend(ToASCII(label))
            size += len(label)

        result += trailing_dot
        size += len(trailing_dot)
        return (bytes(result), size)

class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
    def _buffer_decode(self, input, errors, final):
        if errors != 'strict':
            raise UnicodeError("Unsupported error handling "+errors)

        if not input:
            return ("", 0)

        
        if isinstance(input, str):
            labels = dots.split(input)
        else:
            
            input = str(input, "ascii")
            labels = input.split(".")

        trailing_dot = ''
        if labels:
            if not labels[-1]:
                trailing_dot = '.'
                del labels[-1]
            elif not final:
                
                del labels[-1]
                if labels:
                    trailing_dot = '.'

        result = []
        size = 0
        for label in labels:
            result.append(ToUnicode(label))
            if size:
                size += 1
            size += len(label)

        result = ".".join(result) + trailing_dot
        size += len(trailing_dot)
        return (result, size)

class StreamWriter(Codec,codecs.StreamWriter):
    pass

class StreamReader(Codec,codecs.StreamReader):
    pass



def getregentry():
    return codecs.CodecInfo(
        name='idna',
        encode=Codec().encode,
        decode=Codec().decode,
        incrementalencoder=IncrementalEncoder,
        incrementaldecoder=IncrementalDecoder,
        streamwriter=StreamWriter,
        streamreader=StreamReader,
    )