# Buggy code
def parse_domain_name(buffer: typing.BinaryIO) -> str:
    """Unpack a domain name, handling any name compression encountered.

    Basically, each component of a domain name (called a "label") is prefixed with a
    length followed by the IDNA encoded label. The final component has a zero length
    with a null label for the DNS root. The tricky part is that labels are limited to 63
    bytes, and the upper two bits of the length are used as a flag for "name
    compression". For full details, see RFC1035, sections 3.1 and 4.1.4.

    This is distinct from "character-string" encoding; use `parse_string` for that.
    """
    labels = []
    compression_offset = None
    while buffer:
        length = unpack_stream(">B", buffer)[0]
        if length == 0:
            break
        # The two high bits of the length are a flag for DNS name compression
        length_flags = (length & 0xC0) >> 6
        # The 10 and 01 flags are reserved
        assert length_flags in (0, 0b11)
        if length_flags == 0b11:
            # Mask off the upper two bits, then concatenate the next byte from the
            # stream to get the offset.
            high_bits: int = length & 0x3F
            new_offset_data = bytearray(buffer.read(1))
            new_offset_data.insert(0, high_bits)
            new_offset = struct.unpack(">H", new_offset_data)[0]
            # I think it's technically possible to have multiple levels of name
            # compression, so make sure we don't lose the original place we need to go
            # back to.
            if compression_offset is None:
                compression_offset = buffer.tell()
            buffer.seek(new_offset)
        elif length_flags == 0:
            label = buffer.read(length)
            labels.append(label)
    if compression_offset is not None:
        buffer.seek(compression_offset)
    return ".".join(label.decode("idna") for label in labels)


# Traceback (UnicodeDecodeError)
$  atvremote --debug --mdns-debug scan
...
2021-01-09 22:15:43 ERROR: bad stuff
Traceback (most recent call last):
File "/home/postlund/pyatv_dev/pyatv/pyatv/support/mdns.py", line 337, in datagram_received
services = parse_services(DnsMessage().unpack(data))
File "/home/postlund/pyatv_dev/pyatv/pyatv/support/dns.py", line 291, in unpack
self.answers.extend(
File "/home/postlund/pyatv_dev/pyatv/pyatv/support/dns.py", line 292, in <genexpr>
DnsResource.unpack_read(buffer) for _ in range((header.ancount))
File "/home/postlund/pyatv_dev/pyatv/pyatv/support/dns.py", line 258, in unpack_read
rd = qtype.parse_rdata(buffer, rd_length)
File "/home/postlund/pyatv_dev/pyatv/pyatv/support/dns.py", line 173, in parse_rdata
return parse_domain_name(buffer)
File "/home/postlund/pyatv_dev/pyatv/pyatv/support/dns.py", line 108, in parse_domain_name
return ".".join(label.decode("idna") for label in labels)
File "/home/postlund/pyatv_dev/pyatv/pyatv/support/dns.py", line 108, in <genexpr>
return ".".join(label.decode("idna") for label in labels)
File "/usr/local/lib/python3.8/encodings/idna.py", line 214, in decode
result.append(ToUnicode(label))
File "/usr/local/lib/python3.8/encodings/idna.py", line 125, in ToUnicode
return str(label, "ascii")
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 in position 5: ordinal not in range(128)
2021-01-09 22:15:43 DEBUG: Failed to decode message (Msg=35ff840000030003000000000b5f6170706c6574762d7632045f746370056c6f63616c00000c80010b5f746f7563682d61626c65c018000c8001085f616972706c6179c018000c8001c028000c00010000000a00131032444632453735303736333943323639c028c00c000c00010000000a00131032444632453735303736333943323639c00cc03a000c00010000000a0013104170706c65c2a0545620283431363729c03a)
...

# Fixed code
def parse_domain_name(buffer: typing.BinaryIO) -> str:
    """Unpack a domain name, handling any name compression encountered.

    Basically, each component of a domain name (called a "label") is prefixed with a
    length followed by the encoded label. The final component has a zero length
    with a null label for the DNS root. The tricky part is that labels are limited to 63
    bytes, and the upper two bits of the length are used as a flag for "name
    compression". For full details, see RFC 1035, sections 3.1 and 4.1.4.

    If labels start with the "ASCII Compatible Encoding" prefix ("xn--"), they are
    decoded with IDNA. Otherwise each label is decoded as UTF-8, as that is what is used
    for DNS-SD and Apple doesn't seem to use IDNA anywhere in their mDNS/DNS-SD stack.

    This is distinct from "character-string" encoding; use `parse_string` for that.
    """
    labels = []
    compression_offset = None
    while buffer:
        length = unpack_stream(">B", buffer)[0]
        if length == 0:
            break
        # The two high bits of the length are a flag for DNS name compression
        length_flags = (length & 0xC0) >> 6
        # The 10 and 01 flags are reserved
        assert length_flags in (0, 0b11)
        if length_flags == 0b11:
            # Mask off the upper two bits, then concatenate the next byte from the
            # stream to get the offset.
            high_bits: int = length & 0x3F
            new_offset_data = bytearray(buffer.read(1))
            new_offset_data.insert(0, high_bits)
            new_offset = struct.unpack(">H", new_offset_data)[0]
            # I think it's technically possible to have multiple levels of name
            # compression, so make sure we don't lose the original place we need to go
            # back to.
            if compression_offset is None:
                compression_offset = buffer.tell()
            buffer.seek(new_offset)
        elif length_flags == 0:
            label = buffer.read(length)
            if label[:4] == b"xn--":
                decoded_label = label.decode("idna")
            else:
                decoded_label = label.decode("utf-8")
            labels.append(decoded_label)
    if compression_offset is not None:
        buffer.seek(compression_offset)
    return ".".join(labels)