#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>

#ifdef __APPLE__
  #include <mach/mach.h>
  #include <mach-o/dyld_images.h>
  #include <crt_externs.h>
#else
  #include <sys/wait.h>
  #include <sys/personality.h>
#endif

#include "frida-gumjs.h"

#include "config.h"

#include "entry.h"
#include "instrument.h"
#include "intercept.h"
#include "js.h"
#include "lib.h"
#include "module.h"
#include "output.h"
#include "persistent.h"
#include "prefetch.h"
#include "ranges.h"
#include "seccomp.h"
#include "stalker.h"
#include "stats.h"
#include "util.h"

#define PROC_MAX 65536

#ifdef __APPLE__
extern mach_port_t mach_task_self();
extern GumAddress  gum_darwin_find_entrypoint(mach_port_t task);
#elif defined(__ANDROID__)
typedef struct {

  void (**preinit_array)(void);
  void (**init_array)(void);
  void (**fini_array)(void);

} structors_array_t;

extern void __libc_init(void *raw_args, void (*onexit)(void) __unused,
                        int (*slingshot)(int, char **, char **),
                        structors_array_t const *const structors);
#else
extern int __libc_start_main(int (*main)(int, char **, char **), int argc,
                             char **ubp_av, void (*init)(void),
                             void (*fini)(void), void (*rtld_fini)(void),
                             void(*stack_end));
#endif

typedef int (*main_fn_t)(int argc, char **argv, char **envp);

static main_fn_t main_fn = NULL;

#ifdef __APPLE__
static void on_main_os(int argc, char **argv, char **envp) {

  UNUSED_PARAMETER(argc);
  UNUSED_PARAMETER(argv);
  UNUSED_PARAMETER(envp);

}

#else
static void on_main_os(int argc, char **argv, char **envp) {

  UNUSED_PARAMETER(argc);
  /* Personality doesn't affect the current process, it only takes effect on
   * evec */
  int persona = personality(ADDR_NO_RANDOMIZE);
  if (persona == -1) { FWARNF("Failed to set ADDR_NO_RANDOMIZE: %d", errno); }
  if ((persona & ADDR_NO_RANDOMIZE) == 0) { execvpe(argv[0], argv, envp); }

  GumInterceptor *interceptor = gum_interceptor_obtain();

  gum_interceptor_begin_transaction(interceptor);
  #if defined(__ANDROID__)
  gum_interceptor_revert(interceptor, __libc_init);
  #else
  gum_interceptor_revert(interceptor, __libc_start_main);
  #endif
  gum_interceptor_end_transaction(interceptor);
  gum_interceptor_flush(interceptor);

}

#endif

static void embedded_init(void) {

  static gboolean initialized = false;
  if (!initialized) {

    gum_init_embedded();
    initialized = true;

  }

}

static void afl_print_cmdline(void) {

#if defined(__linux__)
  char  *buffer = g_malloc0(PROC_MAX);
  gchar *fname = g_strdup_printf("/proc/%d/cmdline", getppid());
  int    fd = open(fname, O_RDONLY);

  if (fd < 0) {

    FWARNF("Failed to open /proc/self/cmdline, errno: (%d)", errno);
    return;

  }

  ssize_t bytes_read = read(fd, buffer, PROC_MAX - 1);
  if (bytes_read < 0) {

    FFATAL("Failed to read /proc/self/cmdline, errno: (%d)", errno);

  }

  int idx = 0;

  FVERBOSE("Command Line");

  for (ssize_t i = 0; i < bytes_read; i++) {

    if (i == 0 || buffer[i - 1] == '\0') {

      FVERBOSE("\targv[%d] = %s", idx++, &buffer[i]);

    }

  }

  close(fd);
  g_free(fname);
  g_free(buffer);
#elif defined(__APPLE__)
  int    idx;
  char **argv = *_NSGetArgv();
  int    nargv = *_NSGetArgc();

  for (idx = 0; idx < nargv; idx++) {

    FVERBOSE("\targv[%d] = %s", idx, argv[idx]);

  }

#endif

}

static void afl_print_env(void) {

  char  *buffer = g_malloc0(PROC_MAX);
  gchar *fname = g_strdup_printf("/proc/%d/environ", getppid());
  int    fd = open(fname, O_RDONLY);

  if (fd < 0) {

    FWARNF("Failed to open /proc/self/cmdline, errno: (%d)", errno);
    return;

  }

  ssize_t bytes_read = read(fd, buffer, PROC_MAX - 1);
  if (bytes_read < 0) {

    FFATAL("Failed to read /proc/self/cmdline, errno: (%d)", errno);

  }

  int idx = 0;

  FVERBOSE("ENVIRONMENT");
  for (ssize_t i = 0; i < bytes_read; i++) {

    if (i == 0 || buffer[i - 1] == '\0') {

      FVERBOSE("\t%3d: %s", idx++, &buffer[i]);

    }

  }

  close(fd);
  g_free(fname);
  g_free(buffer);

}

void afl_frida_config(void) {

  FOKF(cRED "**********************");
  FOKF(cRED "* " cYEL "******************" cRED " *");
  FOKF(cRED "* " cYEL "* " cGRN "**************" cYEL " *" cRED " *");
  FOKF(cRED "* " cYEL "* " cGRN "* FRIDA MODE *" cYEL " *" cRED " *");
  FOKF(cRED "* " cYEL "* " cGRN "**************" cYEL " *" cRED " *");
  FOKF(cRED "* " cYEL "******************" cRED " *");
  FOKF(cRED "**********************");
  afl_print_cmdline();
  afl_print_env();

  /* Configure */
  entry_config();
  instrument_config();
  js_config();
  lib_config();
  module_config();
  output_config();
  persistent_config();
  prefetch_config();
  ranges_config();
  seccomp_config();
  stalker_config();
  stats_config();

  js_start();

  output_init();
  embedded_init();
  entry_init();
  instrument_init();
  lib_init();
  module_init();
  persistent_init();
  prefetch_init();
  seccomp_init();
  stalker_init();
  ranges_init();
  stats_init();

}

void afl_frida_run(void) {

  stalker_start();
  entry_start();

}

__attribute__((visibility("default"))) void afl_frida_start(void) {

  afl_frida_config();
  afl_frida_run();

}

typedef void *(*entry_func_t)(size_t a1, size_t a2, size_t a3, size_t a4,
                              size_t a5, size_t a6);

static void *on_entry(size_t a1, size_t a2, size_t a3, size_t a4, size_t a5,
                      size_t a6) {

  intercept_unhook(GSIZE_TO_POINTER(entry_point));
  afl_frida_run();
  entry_func_t entry = (entry_func_t)entry_point;
  return entry(a1, a2, a3, a4, a5, a6);

}

static int on_main(int argc, char **argv, char **envp) {

  int ret;

  on_main_os(argc, argv, envp);

  intercept_unhook_self();

  afl_frida_config();

  if (entry_point == 0) {

    afl_frida_run();

  } else {

    intercept_hook(GSIZE_TO_POINTER(entry_point), on_entry, NULL);

  }

  if (js_main_hook != NULL) {

    ret = js_main_hook(argc, argv, envp);

  } else {

    ret = main_fn(argc, argv, envp);

  }

  return ret;

}

#if defined(EMBEDDED)
extern int main(int argc, char **argv, char **envp);

static void intercept_main(void) {

  main_fn = main;
  intercept_hook(main, on_main, NULL);

}

#elif defined(__APPLE__)
static void intercept_main(void) {

  mach_port_t task = mach_task_self();
  FVERBOSE("Task Id: %u", task);
  GumAddress entry = gum_darwin_find_entrypoint(task);
  FVERBOSE("Entry Point: 0x%016" G_GINT64_MODIFIER "x", entry);
  void *main = GSIZE_TO_POINTER(entry);
  main_fn = main;
  intercept_hook(main, on_main, NULL);

}

#elif defined(__ANDROID__)
static void on_libc_init(void *raw_args, void (*onexit)(void) __unused,
                         int (*slingshot)(int, char **, char **),
                         structors_array_t const *const structors) {

  main_fn = slingshot;
  intercept_unhook_self();
  intercept_hook(slingshot, on_main, NULL);
  return __libc_init(raw_args, onexit, slingshot, structors);

}

static void intercept_main(void) {

  intercept_hook(__libc_init, on_libc_init, NULL);

}

#else
static int on_libc_start_main(int (*main)(int, char **, char **), int argc,
                              char **ubp_av, void (*init)(void),
                              void (*fini)(void), void (*rtld_fini)(void),
                              void(*stack_end)) {

  main_fn = main;
  intercept_unhook_self();
  intercept_hook(main, on_main, NULL);
  return __libc_start_main(main, argc, ubp_av, init, fini, rtld_fini,
                           stack_end);

}

static void intercept_main(void) {

  intercept_hook(__libc_start_main, on_libc_start_main, NULL);

}

#endif

__attribute__((constructor)) static void init(void) {

  embedded_init();

  intercept_main();

}

