"""
Perform queries specific to the musl libc.
"""

from __future__ import annotations

import pwndbg.aglib.elf
import pwndbg.aglib.memory
import pwndbg.aglib.symbol
import pwndbg.aglib.typeinfo
import pwndbg.lib.cache

from . import util
from .dispatch import LibcType
from .dispatch import LibcURLs


def type() -> LibcType:
    return LibcType.MUSL


@pwndbg.lib.cache.cache_until("start", "objfile")
def version(libc_filepath: str) -> tuple[int, ...]:
    # __libc_version is an internal symbol in musl, added in version v1.1.21
    # https://elixir.bootlin.com/musl/v1.1.21/source/src/internal/version.c#L4
    addr = pwndbg.aglib.symbol.lookup_symbol_addr("__libc_version", objfile_endswith=libc_filepath)
    if addr is not None:
        ver = pwndbg.aglib.memory.string(addr)
        return util.version_parse(ver)

    # The version string is simply not embedded into older versions of musl afaict.
    return (-1, -1)


@pwndbg.lib.cache.cache_until("start", "objfile")
def has_internal_symbols(libc_filepath: str) -> bool:
    # c_messages is an internal global variable in musl. Has existed since the
    # first release i.e. version v0.5.0 (2011). (elixir doesn't have 0.5.0 on hand)
    # https://elixir.bootlin.com/musl/v0.5.9/source/src/locale/langinfo.c#L24
    return (
        pwndbg.aglib.symbol.lookup_symbol("c_messages", objfile_endswith=libc_filepath) is not None
    )


@pwndbg.lib.cache.cache_until("start", "objfile")
def has_debug_info() -> bool:
    # Available since the first release (0.5.0). (elixir doesn't have 0.5.0 on hand)
    # https://elixir.bootlin.com/musl/v0.5.9/source/include/bits/pthread.h#L1
    return pwndbg.aglib.typeinfo.load("struct __ptcb") is not None


def verify_libc_candidate(mapping_name: str) -> bool:
    # First check __freadahead which is an exported symbol in musl, and bionic but not in glibc
    # It was introduced in v0.9.2 (year 2012)
    # https://elixir.bootlin.com/musl/v0.9.2/source/src/stdio/ext2.c#L3
    if (
        util.has_exported_symbols(mapping_name)
        and pwndbg.aglib.symbol.lookup_symbol("__freadahead", objfile_endswith=mapping_name) is None
    ):
        return False

    # Then do a consistent but more expensive (?) check:
    # Check if the string "/tmp/tmpnam_XXXX" is in the .rodata of the binary.
    # Added in musl version v1.1.2 (year 2014) (is present until at least v1.2.5).
    # https://elixir.bootlin.com/musl/v1.1.2/source/src/stdio/tmpnam.c#L15
    rodata: tuple[int, int, bytes] | None = pwndbg.aglib.elf.section_by_name(
        mapping_name, ".rodata", try_local_path=True
    )
    if rodata is None:
        return False
    _, _, data = rodata
    return b"/tmp/tmpnam_XXXX" in data


def verify_ld_candidate(mapping_name: str) -> bool:
    # For musl, ld and libc are the same mapping.
    # On some distributions it is named libc, on some it's ld.
    return verify_libc_candidate(mapping_name)


def urls(ver: tuple[int, ...] | None) -> LibcURLs:
    assert ver is not None
    if ver[0] == -1:
        # Version not available, use dummy values.
        return LibcURLs(
            versioned_readable_source="https://elixir.bootlin.com/musl/latest/source",
            versioned_compressed_source="https://musl.libc.org/releases/musl-<major>.<minor>.<patch>.tar.gz",
            homepage="https://musl.libc.org/",
            git="git://git.musl-libc.org/musl",
        )

    ver_str = ".".join(map(str, ver))
    return LibcURLs(
        versioned_readable_source=f"https://elixir.bootlin.com/musl/v{ver_str}/source",
        versioned_compressed_source=f"https://musl.libc.org/releases/musl-{ver_str}.tar.gz",
        homepage="https://musl.libc.org/",
        git="git://git.musl-libc.org/musl",
    )


def libc_same_as_ld() -> bool:
    return True
