#!/usr/bin/env python3
"""
Patch ELF .so/.node files that require GLIBCXX_3.4.30 to use GLIBCXX_3.4.29
instead. Required for AlmaLinux 9 / RHEL 9 which ships GCC 11 (max GLIBCXX_3.4.29).

Both the version string AND the vna_hash in .gnu.version_r are updated so the
dynamic linker finds the version correctly at runtime.
"""
import sys
import struct


def elf_hash(name: str) -> int:
    """ELF version symbol hash (not GNU hash — used in .gnu.version_r)."""
    h = 0
    for c in name:
        h = ((h << 4) + ord(c)) & 0xFFFFFFFF
        g = h & 0xF0000000
        if g:
            h ^= g >> 24
        h &= (~g) & 0xFFFFFFFF
    return h


def patch_file(path: str) -> bool:
    with open(path, 'rb') as f:
        data = bytearray(f.read())

    old_ver = b'GLIBCXX_3.4.30'
    new_ver = b'GLIBCXX_3.4.11'

    if old_ver not in data:
        print(f'  skip (no GLIBCXX_3.4.30): {path}')
        return False

    # ----- ELF header -----
    if data[:4] != b'\x7fELF':
        print(f'  skip (not ELF): {path}')
        return False

    ei_class = data[4]  # 1=32-bit, 2=64-bit
    ei_data  = data[5]  # 1=LE, 2=BE
    endian   = '<' if ei_data == 1 else '>'

    if ei_class == 2:
        # 64-bit ELF
        e_shoff_fmt = f'{endian}Q'
        ehdr_size   = 64
        shdr_size   = 64
        shdr_fmt    = f'{endian}IIQQQQIIQQ'  # 64-byte shdr
        (e_shoff,)  = struct.unpack_from(f'{endian}Q', data, 40)
        (e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 58)
        shdr_type_off = 4
        shdr_off_off  = 24   # sh_offset within shdr
        shdr_link_off = 48   # sh_link within shdr
    else:
        # 32-bit ELF
        (e_shoff,)  = struct.unpack_from(f'{endian}I', data, 32)
        (e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 46)
        shdr_type_off = 4
        shdr_off_off  = 16
        shdr_link_off = 28

    SHT_GNU_verneed = 0x6FFFFFFE

    patched = False
    # Target GLIBCXX_3.4.11 — that's where condition_variable::wait actually lives
    # in AlmaLinux 9's libstdc++.so.6 (same byte length: 14 chars each)
    old_hash = elf_hash('GLIBCXX_3.4.30')
    new_hash = elf_hash('GLIBCXX_3.4.11')
    old_hash_bytes = struct.pack(f'{endian}I', old_hash)
    new_hash_bytes = struct.pack(f'{endian}I', new_hash)

    for i in range(e_shnum):
        shdr_base = e_shoff + i * e_shentsize
        sh_type   = struct.unpack_from(f'{endian}I', data, shdr_base + shdr_type_off)[0]

        if sh_type != SHT_GNU_verneed:
            continue

        # sh_offset and sh_link (index of associated string table section)
        if ei_class == 2:
            sh_offset = struct.unpack_from(f'{endian}Q', data, shdr_base + 24)[0]
            sh_size   = struct.unpack_from(f'{endian}Q', data, shdr_base + 32)[0]
            sh_link   = struct.unpack_from(f'{endian}I', data, shdr_base + 40)[0]
            sh_info   = struct.unpack_from(f'{endian}I', data, shdr_base + 44)[0]
        else:
            sh_offset = struct.unpack_from(f'{endian}I', data, shdr_base + 16)[0]
            sh_size   = struct.unpack_from(f'{endian}I', data, shdr_base + 20)[0]
            sh_link   = struct.unpack_from(f'{endian}I', data, shdr_base + 24)[0]
            sh_info   = struct.unpack_from(f'{endian}I', data, shdr_base + 28)[0]

        # String table section referenced by sh_link
        strtab_shdr = e_shoff + sh_link * e_shentsize
        if ei_class == 2:
            strtab_off = struct.unpack_from(f'{endian}Q', data, strtab_shdr + 24)[0]
        else:
            strtab_off = struct.unpack_from(f'{endian}I', data, strtab_shdr + 16)[0]

        # Walk Elf64_Verneed / Elf64_Vernaux entries
        # Elf64_Verneed: vn_version(2)+vn_cnt(2)+vn_file(4)+vn_aux(4)+vn_next(4) = 16 bytes
        # Elf64_Vernaux: vna_hash(4)+vna_flags(2)+vna_other(2)+vna_name(4)+vna_next(4) = 16 bytes
        vneed_pos = sh_offset
        for _ in range(sh_info):  # sh_info = number of Verneed entries
            vn_cnt  = struct.unpack_from(f'{endian}H', data, vneed_pos + 2)[0]
            vn_aux  = struct.unpack_from(f'{endian}I', data, vneed_pos + 8)[0]   # +8 (after vn_file)
            vn_next = struct.unpack_from(f'{endian}I', data, vneed_pos + 12)[0]

            vaux_pos = vneed_pos + vn_aux
            for _ in range(vn_cnt):
                vna_hash  = struct.unpack_from(f'{endian}I', data, vaux_pos)[0]
                vna_name  = struct.unpack_from(f'{endian}I', data, vaux_pos + 8)[0]
                vna_next  = struct.unpack_from(f'{endian}I', data, vaux_pos + 12)[0]

                # Read the version name from the string table
                name_off  = strtab_off + vna_name
                name_end  = data.index(b'\x00', name_off)
                ver_name  = bytes(data[name_off:name_end])

                if ver_name == old_ver:
                    # Patch hash
                    struct.pack_into(f'{endian}I', data, vaux_pos, new_hash)
                    # Patch string (same byte length — safe in-place)
                    data[name_off:name_off + len(old_ver)] = new_ver
                    patched = True

                if vna_next == 0:
                    break
                vaux_pos += vna_next

            if vn_next == 0:
                break
            vneed_pos += vn_next

    if patched:
        with open(path, 'wb') as f:
            f.write(data)
        print(f'  patched (hash+string): {path}')
        return True
    else:
        print(f'  skip (GLIBCXX_3.4.30 not in verneed): {path}')
        return False


if __name__ == '__main__':
    for path in sys.argv[1:]:
        patch_file(path)
