#!/bin/bash

#############################################################################
##
## Copyright (C) 2017 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is the build configuration utility of the Qt Toolkit.
##
## $QT_BEGIN_LICENSE:LGPL$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and The Qt Company. For licensing terms
## and conditions see https://www.qt.io/terms-conditions. For further
## information use the contact form at https://www.qt.io/contact-us.
##
## GNU Lesser General Public License Usage
## Alternatively, this file may be used under the terms of the GNU Lesser
## General Public License version 3 as published by the Free Software
## Foundation and appearing in the file LICENSE.LGPL3 included in the
## packaging of this file. Please review the following information to
## ensure the GNU Lesser General Public License version 3 requirements
## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 2.0 or (at your option) the GNU General
## Public license version 3 or any later version approved by the KDE Free
## Qt Foundation. The licenses are as published by the Free Software
## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
## included in the packaging of this file. Please review the following
## information to ensure the GNU General Public License requirements will
## be met: https://www.gnu.org/licenses/gpl-2.0.html and
## https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################

script_argument_prefix="-Wobjc_namespace,--"

required_arguments="target suffix original_ld"
optional_arguments="exclude_list exclude_regex slient"

for argument in $required_arguments $optional_arguments; do
    declare "$argument="
done

declare -i silent=0
declare -a linker_arguments

for i in "$@"; do
    case $1 in
        $script_argument_prefix*)
            declare "${1#$script_argument_prefix}"
            ;;
        -o)
            if [ -n "$2" ]; then
                target="$2"
            fi
            linker_arguments+=("$1")
            ;;
        *)
            linker_arguments+=("$1")
            ;;
    esac
    shift
done

get_entry() {
    local map=$1 key=$2
    local i="${map}_map_${2}"
    printf '%s' "${!i}"
}

error() {
    echo "$0: error: $*" >&2
    exit 1
}

for argument in $required_arguments; do
    if [ -z "${!argument}" ]; then
        error "missing argument --${argument}"
    fi
done

# Normalize suffix so we can use it as a bash variable
suffix=${suffix//[-. ]/_}

link_binary() {
    (PS4=; test $silent -ne 1 && set -x; $original_ld "${linker_arguments[@]}" "$@") 2>&1 || exit 1
}

sanitize_address() {
    local address="$1"
    address=${address#0x} # Remove hex prefix
    address=${address: ${#address} < 8 ? 0 : -8} # Limit to 32-bit
    echo "0x$address"
}

read_binary() {
    local address=$1
    local length=$2

    dd if="$target" bs=1 iseek=$address count=$length 2>|/dev/null
}

read_32bit_value() {
    local address=$1
    read_binary $address 4 | xxd -p | dd conv=swab 2>/dev/null | rev
}

inspect_binary() {
    inspect_mode="$1"

    echo -n "🔎  Inspecting binary '$target', "
    if [ ! -f "$target" ]; then
        echo "target does not exist!"
        exit 1
    fi

    read -a mach_header <<< "$(otool -h "$target" -v | tail -n 1)"
    if [ "${mach_header[1]}" != "X86_64" ]; then
        echo "binary is not 64-bit, only 64-bit binaries are supported!"
        exit 1
    fi

    classnames_section="__objc_classname"
    classnames=$(otool -v -s __TEXT $classnames_section "$target" | tail -n +3)
    while read -a classname; do
        address=$(sanitize_address ${classname[0]})
        name=${classname[1]}

        declare "address_to_classname_map_$address=$name"
        declare "classname_to_address_map_$name=$address"
    done <<< "$classnames"

    extra_classnames_file="$(mktemp -t ${classnames_section}_additions).S"

    if [ "$inspect_mode" == "inject_classnames" ]; then
        echo "class names have not been namespaced, adding suffix '$suffix'..."
        printf ".section __TEXT,$classnames_section,cstring_literals,no_dead_strip\n" > $extra_classnames_file
    elif [ "$inspect_mode" == "patch_classes" ]; then
        echo "found namespaced class names, updating class entries..."
    fi

    classes=$(otool -o "$target" | grep class_ro_t)
    while read -a class; do
        address="$(sanitize_address ${class[1]})"

        class_flags="0x$(read_32bit_value $address)"
        if [ -z "$class_flags" ]; then
            echo " 💥  failed to read class flags for class at $address"
            continue
        fi

        is_metaclass=$(($class_flags & 0x1))

        name_offset=$(($address + 24))
        classname_address="0x$(read_32bit_value $name_offset)"
        if [ -z "$classname_address" ]; then
            echo " 💥  failed to read class name address for class at $address"
            continue
        fi

        classname=$(get_entry address_to_classname $classname_address)
        if [ -z "$classname" ]; then
            echo " 💥  failed to resolve class name for address '$classname_address'"
            continue
        fi

        if [[ $exclude_list =~ $classname || $classname =~ $exclude_regex ]]; then
            if [ $is_metaclass -eq 1 ]; then
                class_type="meta class"
            else
                class_type="class"
            fi
            echo " 🚽  skipping excluded $class_type '$classname'"
            continue
        fi

        newclassname="${classname}_${suffix}"

        if [ "$inspect_mode" == "inject_classnames" ]; then
            if [ $is_metaclass -eq 1 ]; then
                continue
            fi

            echo " 💉  injecting $classnames_section entry '$newclassname' for '$classname'"
            printf ".asciz \"$newclassname\"\n" >> $extra_classnames_file

        elif [ "$inspect_mode" == "patch_classes" ]; then
            newclassname_address=$(get_entry classname_to_address ${newclassname})
            if [ -z "$newclassname_address" ]; then
                echo " 💥  failed to resolve class name address for class '$newclassname'"
                continue
            fi

            if [ $is_metaclass -eq 1 ]; then
                class_type="meta"
            else
                class_type="class"
            fi

            echo " 🔨  patching class_ro_t at $address ($class_type) from $classname_address ($classname) to $newclassname_address ($newclassname)"
            echo ${newclassname_address: -8} | rev | dd conv=swab 2>/dev/null | xxd -p -r -seek $name_offset -l 4 - "$target"
        fi
    done <<< "$classes"
}

echo "🔩  Linking binary using '$original_ld'..."
link_binary

inspect_binary inject_classnames

echo "🔩  Re-linking binary with extra __objc_classname section..."
link_binary $extra_classnames_file

inspect_binary patch_classes

