/*!A cross-platform build utility based on Lua
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright (C) 2015-present, Xmake Open Source Community.
 *
 * @author      ruki
 * @file        deplibs.c
 *
 */

/* //////////////////////////////////////////////////////////////////////////////////////
 * trace
 */
#define TB_TRACE_MODULE_NAME "deplibs_elf"
#define TB_TRACE_MODULE_DEBUG (0)

/* //////////////////////////////////////////////////////////////////////////////////////
 * includes
 */
#include "prefix.h"

/* //////////////////////////////////////////////////////////////////////////////////////
 * private implementation
 */

static tb_bool_t xm_binutils_elf_check_path(tb_char_t const* path, tb_char_t const* name, tb_char_t* output, tb_size_t output_size) {
    tb_char_t fullpath[TB_PATH_MAXN];
    tb_snprintf(fullpath, sizeof(fullpath), "%s/%s", path, name);
    if (tb_file_info(fullpath, tb_null)) {
        if (output) {
            tb_strlcpy(output, fullpath, output_size);
        }
        return tb_true;
    }
    return tb_false;
}

static tb_void_t xm_binutils_elf_resolve_path(tb_char_t const* name, tb_char_t const* rpath, tb_char_t const* binary_dir, tb_char_t* output, tb_size_t output_size) {
    
    // absolute path?
    if (tb_path_is_absolute(name)) {
        tb_strlcpy(output, name, output_size);
        return;
    }

    // try rpath/runpath
    if (rpath && binary_dir) {
        tb_char_t const* p = rpath;
        tb_char_t const* e = tb_null;
        while (*p) {
            e = tb_strchr(p, ':');
            tb_size_t n = e ? (tb_size_t)(e - p) : tb_strlen(p);
            if (n > 0) {
                tb_char_t path[TB_PATH_MAXN];
                tb_char_t expanded_path[TB_PATH_MAXN];
                tb_strncpy(path, p, n);
                path[n] = '\0';

                // replace $ORIGIN
                tb_char_t* origin = tb_strstr(path, "$ORIGIN");
                if (origin) {
                    tb_long_t len = tb_snprintf(expanded_path, sizeof(expanded_path), "%.*s%s%s", (tb_int_t)(origin - path), path, binary_dir, origin + 7);
                    if (len >= 0) {
                        if (xm_binutils_elf_check_path(expanded_path, name, output, output_size)) {
                            return;
                        }
                    }
                } else {
                    if (xm_binutils_elf_check_path(path, name, output, output_size)) {
                        return;
                    }
                }
            }
            if (e) p = e + 1;
            else break;
        }
    }

    // try system paths
    static tb_char_t const* s_sys_paths[] = {
        "/lib",
        "/usr/lib",
        "/lib64",
        "/usr/lib64",
        "/usr/local/lib",
        "/usr/local/lib64",
        "/lib/x86_64-linux-gnu", // Debian/Ubuntu
        "/usr/lib/x86_64-linux-gnu",
        "/lib/i386-linux-gnu",
        "/usr/lib/i386-linux-gnu",
        "/lib/aarch64-linux-gnu",
        "/usr/lib/aarch64-linux-gnu",
        "/lib/arm-linux-gnueabihf",
        "/usr/lib/arm-linux-gnueabihf",
        tb_null
    };

    for (tb_char_t const** sys_path = s_sys_paths; *sys_path; sys_path++) {
        if (xm_binutils_elf_check_path(*sys_path, name, output, output_size)) {
            return;
        }
    }

    // not found, return original name
    tb_strlcpy(output, name, output_size);
}

static tb_bool_t xm_binutils_elf_deplibs_32(tb_stream_ref_t istream, tb_hize_t base_offset, lua_State *lua) {
    tb_assert_and_check_return_val(istream && lua, tb_false);

    // read ELF header
    xm_elf32_header_t header;
    if (!xm_binutils_elf_read_header_32(istream, base_offset, &header)) {
        return tb_false;
    }

    // find program interpreter (PT_INTERP) and push
    tb_char_t name[256];
    tb_size_t result_count = 0;
    if (xm_binutils_elf_find_interp_32(istream, base_offset, &header, name, sizeof(name))) {
        lua_pushinteger(lua, result_count + 1);
        lua_pushstring(lua, name);
        lua_settable(lua, -3);
        result_count++;
    }

    // build dynamic/string table context
    xm_elf_context_t ctx;
    if (!xm_binutils_elf_get_context_32(istream, base_offset, &ctx)) {
        return tb_true;
    }

    // scan .dynamic entries: collect NEEDED and read RPATH/RUNPATH
    tb_uint32_t count = (tb_uint32_t)(ctx.dynamic_size / sizeof(xm_elf32_dynamic_t));
    if (!tb_stream_seek(istream, base_offset + ctx.dynamic_offset)) {
        return tb_false;
    }

    tb_char_t rpath[8192] = {0};
    tb_char_t runpath[8192] = {0};
    tb_vector_ref_t needed_libs = tb_vector_init(0, tb_element_str(tb_true));
    if (needed_libs) {
        for (tb_uint32_t i = 0; i < count; i++) {
            xm_elf32_dynamic_t dyn;
            if (!tb_stream_bread(istream, (tb_byte_t*)&dyn, sizeof(dyn))) {
                break;
            }

            if (dyn.d_tag == XM_ELF_DT_NULL) {
                break;
            }

            if (dyn.d_tag == XM_ELF_DT_NEEDED || dyn.d_tag == XM_ELF_DT_SONAME || dyn.d_tag == XM_ELF_DT_AUXILIARY || dyn.d_tag == XM_ELF_DT_FILTER) {
                tb_char_t name[256];
                if (xm_binutils_read_string(istream, base_offset + ctx.strtab_offset + dyn.d_un.d_val, name, sizeof(name)) && name[0]) {
                    tb_vector_insert_tail(needed_libs, name);
                }
            } else if (dyn.d_tag == XM_ELF_DT_RPATH) {
                xm_binutils_read_string(istream, base_offset + ctx.strtab_offset + dyn.d_un.d_val, rpath, sizeof(rpath));
            } else if (dyn.d_tag == XM_ELF_DT_RUNPATH) {
                xm_binutils_read_string(istream, base_offset + ctx.strtab_offset + dyn.d_un.d_val, runpath, sizeof(runpath));
            }
        }

        tb_char_t const* binary_path = tb_null;
        tb_char_t binary_dir[TB_PATH_MAXN] = {0};
        if (tb_stream_ctrl(istream, TB_STREAM_CTRL_GET_PATH, &binary_path) && binary_path) {
            tb_path_directory(binary_path, binary_dir, sizeof(binary_dir));
        }

        // resolve $ORIGIN and rpath/runpath, push full paths
        tb_for_all (tb_char_t const*, name, needed_libs) {
            tb_char_t fullpath[TB_PATH_MAXN];
            xm_binutils_elf_resolve_path(name, runpath[0]? runpath : rpath, binary_dir[0]? binary_dir : tb_null, fullpath, sizeof(fullpath));
            lua_pushinteger(lua, result_count + 1);
            lua_pushstring(lua, fullpath);
            lua_settable(lua, -3);
            result_count++;
        }
        tb_vector_exit(needed_libs);
    }

    return tb_true;
}

static tb_bool_t xm_binutils_elf_deplibs_64(tb_stream_ref_t istream, tb_hize_t base_offset, lua_State *lua) {
    tb_assert_and_check_return_val(istream && lua, tb_false);

    // read ELF header
    xm_elf64_header_t header;
    if (!xm_binutils_elf_read_header_64(istream, base_offset, &header)) {
        return tb_false;
    }

    // find program interpreter (PT_INTERP) and push
    tb_char_t name[256];
    tb_size_t result_count = 0;
    if (xm_binutils_elf_find_interp_64(istream, base_offset, &header, name, sizeof(name))) {
        lua_pushinteger(lua, result_count + 1);
        lua_pushstring(lua, name);
        lua_settable(lua, -3);
        result_count++;
    }

    // build dynamic/string table context
    xm_elf_context_t ctx;
    if (!xm_binutils_elf_get_context_64(istream, base_offset, &ctx)) {
        return tb_true;
    }

    // read dynamic entries
    tb_uint32_t count = (tb_uint32_t)(ctx.dynamic_size / sizeof(xm_elf64_dynamic_t));
    if (!tb_stream_seek(istream, base_offset + ctx.dynamic_offset)) {
        return tb_false;
    }

    tb_char_t rpath[8192] = {0};
    tb_char_t runpath[8192] = {0};
    tb_vector_ref_t needed_libs = tb_vector_init(0, tb_element_str(tb_true));
    if (needed_libs) {
        for (tb_uint32_t i = 0; i < count; i++) {
            xm_elf64_dynamic_t dyn;
            if (!tb_stream_bread(istream, (tb_byte_t*)&dyn, sizeof(dyn))) {
                break;
            }

            if (dyn.d_tag == XM_ELF_DT_NULL) {
                break;
            }

            if (dyn.d_tag == XM_ELF_DT_NEEDED || dyn.d_tag == XM_ELF_DT_SONAME || dyn.d_tag == XM_ELF_DT_AUXILIARY || dyn.d_tag == XM_ELF_DT_FILTER) {
                 tb_char_t name[256];
                 if (xm_binutils_read_string(istream, base_offset + ctx.strtab_offset + (tb_uint32_t)dyn.d_un.d_val, name, sizeof(name)) && name[0]) {
                     tb_vector_insert_tail(needed_libs, name);
                 }
            } else if (dyn.d_tag == XM_ELF_DT_RPATH) {
                 xm_binutils_read_string(istream, base_offset + ctx.strtab_offset + (tb_uint32_t)dyn.d_un.d_val, rpath, sizeof(rpath));
            } else if (dyn.d_tag == XM_ELF_DT_RUNPATH) {
                 xm_binutils_read_string(istream, base_offset + ctx.strtab_offset + (tb_uint32_t)dyn.d_un.d_val, runpath, sizeof(runpath));
            }
        }

        // get binary directory
        tb_char_t const* binary_path = tb_null;
        tb_char_t binary_dir[TB_PATH_MAXN] = {0};
        if (tb_stream_ctrl(istream, TB_STREAM_CTRL_GET_PATH, &binary_path) && binary_path) {
            tb_path_directory(binary_path, binary_dir, sizeof(binary_dir));
        }

        // resolve and push paths
        tb_for_all (tb_char_t const*, name, needed_libs) {
            tb_char_t fullpath[TB_PATH_MAXN];
            xm_binutils_elf_resolve_path(name, runpath[0]? runpath : rpath, binary_dir[0]? binary_dir : tb_null, fullpath, sizeof(fullpath));
            lua_pushinteger(lua, result_count + 1);
            lua_pushstring(lua, fullpath);
            lua_settable(lua, -3);
            result_count++;
        }
        tb_vector_exit(needed_libs);
    }

    return tb_true;
}

/* //////////////////////////////////////////////////////////////////////////////////////
 * implementation
 */
tb_bool_t xm_binutils_elf_deplibs(tb_stream_ref_t istream, tb_hize_t base_offset, lua_State *lua) {
    tb_assert_and_check_return_val(istream && lua, tb_false);

    // read and check ELF magic
    tb_uint8_t magic[4];
    if (!tb_stream_seek(istream, base_offset)) {
        return tb_false;
    }
    if (!tb_stream_bread(istream, magic, 4)) {
        return tb_false;
    }
    if (magic[0] != 0x7f || magic[1] != 'E' || magic[2] != 'L' || magic[3] != 'F') {
        return tb_false;
    }

    // check ELF class (32-bit or 64-bit)
    tb_uint8_t elf_class;
    if (!tb_stream_seek(istream, base_offset + 4)) {
        return tb_false;
    }
    if (!tb_stream_bread(istream, (tb_byte_t*)&elf_class, 1)) {
        return tb_false;
    }

    if (elf_class == 1) {
        return xm_binutils_elf_deplibs_32(istream, base_offset, lua);
    } else if (elf_class == 2) {
        return xm_binutils_elf_deplibs_64(istream, base_offset, lua);
    }

    return tb_false;
}
