/* -*- coding: utf-8 -*-
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
*/

/**
 * This file implements a shared library. This library can be pre-loaded by
 * the dynamic linker of the Operating System (OS). It implements a few function
 * related to process creation. By pre-load this library the executed process
 * uses these functions instead of those from the standard library.
 *
 * The idea here is to inject a logic before call the real methods. The logic is
 * to dump the call into a file. To call the real method this library is doing
 * the job of the dynamic linker.
 *
 * The only input for the log writing is about the destination directory.
 * This is passed as environment variable.
 */

// NOLINTNEXTLINE
#include "config.h"

#include <dlfcn.h>
#include <pthread.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#if defined HAVE_POSIX_SPAWN || defined HAVE_POSIX_SPAWNP
#include <spawn.h>
#endif

#if defined HAVE_NSGETENVIRON
#include <crt_externs.h>
#else
extern char **environ;
#endif

#define ENV_OUTPUT "INTERCEPT_BUILD_TARGET_DIR"
#ifdef APPLE
#define ENV_FLAT "DYLD_FORCE_FLAT_NAMESPACE"
#define ENV_PRELOAD "DYLD_INSERT_LIBRARIES"
#define ENV_SIZE 3
#else
#define ENV_PRELOAD "LD_PRELOAD"
#define ENV_SIZE 2
#endif

#define DLSYM(TYPE_, VAR_, SYMBOL_)                                            \
  union {                                                                      \
    void *from;                                                                \
    TYPE_ to;                                                                  \
  } cast;                                                                      \
  if (0 == (cast.from = dlsym(RTLD_NEXT, SYMBOL_))) {                          \
    perror("bear: dlsym");                                                     \
    exit(EXIT_FAILURE);                                                        \
  }                                                                            \
  TYPE_ const VAR_ = cast.to;

typedef char const *bear_env_t[ENV_SIZE];

static int bear_capture_env_t(bear_env_t *env);
static int bear_reset_env_t(bear_env_t *env);
static void bear_release_env_t(bear_env_t *env);
static char const **bear_update_environment(char *const envp[],
                                            bear_env_t *env);
static char const **bear_update_environ(char const **in, char const *key,
                                        char const *value);
static char **bear_get_environment();
static void bear_report_call(char const *fun, char const *const argv[]);
static char const **bear_strings_build(char const *arg, va_list *ap);
static char const **bear_strings_copy(char const **const in);
static char const **bear_strings_append(char const **in, char const *e);
static size_t bear_strings_length(char const *const *in);
static void bear_strings_release(char const **);

static bear_env_t env_names = {ENV_OUTPUT, ENV_PRELOAD
#ifdef ENV_FLAT
                               ,
                               ENV_FLAT
#endif
};

static bear_env_t initial_env = {0, 0
#ifdef ENV_FLAT
                                 ,
                                 0
#endif
};

static int initialized = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

static void on_load(void) __attribute__((constructor));
static void on_unload(void) __attribute__((destructor));

#ifdef HAVE_EXECVE
static int call_execve(const char *path, char *const argv[],
                       char *const envp[]);
#endif
#ifdef HAVE_EXECVP
static int call_execvp(const char *file, char *const argv[]);
#endif
#ifdef HAVE_EXECVPE
static int call_execvpe(const char *file, char *const argv[],
                        char *const envp[]);
#endif
#ifdef HAVE_EXECVP2
static int call_execvP(const char *file, const char *search_path,
                       char *const argv[]);
#endif
#ifdef HAVE_EXECT
static int call_exect(const char *path, char *const argv[], char *const envp[]);
#endif
#ifdef HAVE_POSIX_SPAWN
static int call_posix_spawn(pid_t *restrict pid, const char *restrict path,
                            const posix_spawn_file_actions_t *file_actions,
                            const posix_spawnattr_t *restrict attrp,
                            char *const argv[restrict],
                            char *const envp[restrict]);
#endif
#ifdef HAVE_POSIX_SPAWNP
static int call_posix_spawnp(pid_t *restrict pid, const char *restrict file,
                             const posix_spawn_file_actions_t *file_actions,
                             const posix_spawnattr_t *restrict attrp,
                             char *const argv[restrict],
                             char *const envp[restrict]);
#endif

/* Initialization method to Captures the relevant environment variables.
 */

static void on_load(void) {
  pthread_mutex_lock(&mutex);
  if (!initialized)
    initialized = bear_capture_env_t(&initial_env);
  pthread_mutex_unlock(&mutex);
}

static void on_unload(void) {
  pthread_mutex_lock(&mutex);
  bear_release_env_t(&initial_env);
  initialized = 0;
  pthread_mutex_unlock(&mutex);
}

/* These are the methods we are try to hijack.
 */

#ifdef HAVE_EXECVE
int execve(const char *path, char *const argv[], char *const envp[]) {
  bear_report_call(__func__, (char const *const *)argv);
  return call_execve(path, argv, envp);
}
#endif

#ifdef HAVE_EXECV
#ifndef HAVE_EXECVE
#error can not implement execv without execve
#endif
int execv(const char *path, char *const argv[]) {
  bear_report_call(__func__, (char const *const *)argv);
  char *const *envp = bear_get_environment();
  return call_execve(path, argv, envp);
}
#endif

#ifdef HAVE_EXECVPE
int execvpe(const char *file, char *const argv[], char *const envp[]) {
  bear_report_call(__func__, (char const *const *)argv);
  return call_execvpe(file, argv, envp);
}
#endif

#ifdef HAVE_EXECVP
int execvp(const char *file, char *const argv[]) {
  bear_report_call(__func__, (char const *const *)argv);
  return call_execvp(file, argv);
}
#endif

#ifdef HAVE_EXECVP2
int execvP(const char *file, const char *search_path, char *const argv[]) {
  bear_report_call(__func__, (char const *const *)argv);
  return call_execvP(file, search_path, argv);
}
#endif

#ifdef HAVE_EXECT
int exect(const char *path, char *const argv[], char *const envp[]) {
  bear_report_call(__func__, (char const *const *)argv);
  return call_exect(path, argv, envp);
}
#endif

#ifdef HAVE_EXECL
#ifndef HAVE_EXECVE
#error can not implement execl without execve
#endif
int execl(const char *path, const char *arg, ...) {
  va_list args;
  va_start(args, arg);
  char const **argv = bear_strings_build(arg, &args);
  va_end(args);

  bear_report_call(__func__, (char const *const *)argv);
  char *const *envp = bear_get_environment();
  int const result = call_execve(path, (char *const *)argv, envp);

  bear_strings_release(argv);
  return result;
}
#endif

#ifdef HAVE_EXECLP
#ifndef HAVE_EXECVP
#error can not implement execlp without execvp
#endif
int execlp(const char *file, const char *arg, ...) {
  va_list args;
  va_start(args, arg);
  char const **argv = bear_strings_build(arg, &args);
  va_end(args);

  bear_report_call(__func__, (char const *const *)argv);
  int const result = call_execvp(file, (char *const *)argv);

  bear_strings_release(argv);
  return result;
}
#endif

#ifdef HAVE_EXECLE
#ifndef HAVE_EXECVE
#error can not implement execle without execve
#endif
// int execle(const char *path, const char *arg, ..., char * const envp[]);
int execle(const char *path, const char *arg, ...) {
  va_list args;
  va_start(args, arg);
  char const **argv = bear_strings_build(arg, &args);
  char const **envp = va_arg(args, char const **);
  va_end(args);

  bear_report_call(__func__, (char const *const *)argv);
  int const result =
      call_execve(path, (char *const *)argv, (char *const *)envp);

  bear_strings_release(argv);
  return result;
}
#endif

#ifdef HAVE_POSIX_SPAWN
int posix_spawn(pid_t *restrict pid, const char *restrict path,
                const posix_spawn_file_actions_t *file_actions,
                const posix_spawnattr_t *restrict attrp,
                char *const argv[restrict], char *const envp[restrict]) {
  bear_report_call(__func__, (char const *const *)argv);
  return call_posix_spawn(pid, path, file_actions, attrp, argv, envp);
}
#endif

#ifdef HAVE_POSIX_SPAWNP
int posix_spawnp(pid_t *restrict pid, const char *restrict file,
                 const posix_spawn_file_actions_t *file_actions,
                 const posix_spawnattr_t *restrict attrp,
                 char *const argv[restrict], char *const envp[restrict]) {
  bear_report_call(__func__, (char const *const *)argv);
  return call_posix_spawnp(pid, file, file_actions, attrp, argv, envp);
}
#endif

/* These are the methods which forward the call to the standard implementation.
 */

#ifdef HAVE_EXECVE
static int call_execve(const char *path, char *const argv[],
                       char *const envp[]) {
  typedef int (*func)(const char *, char *const *, char *const *);

  DLSYM(func, fp, "execve");

  char const **const menvp = bear_update_environment(envp, &initial_env);
  int const result = (*fp)(path, argv, (char *const *)menvp);
  bear_strings_release(menvp);
  return result;
}
#endif

#ifdef HAVE_EXECVPE
static int call_execvpe(const char *file, char *const argv[],
                        char *const envp[]) {
  typedef int (*func)(const char *, char *const *, char *const *);

  DLSYM(func, fp, "execvpe");

  char const **const menvp = bear_update_environment(envp, &initial_env);
  int const result = (*fp)(file, argv, (char *const *)menvp);
  bear_strings_release(menvp);
  return result;
}
#endif

#ifdef HAVE_EXECVP
static int call_execvp(const char *file, char *const argv[]) {
  typedef int (*func)(const char *file, char *const argv[]);

  DLSYM(func, fp, "execvp");

  bear_env_t current_env;
  bear_capture_env_t(&current_env);
  bear_reset_env_t(&initial_env);
  int const result = (*fp)(file, argv);
  bear_reset_env_t(&current_env);
  bear_release_env_t(&current_env);

  return result;
}
#endif

#ifdef HAVE_EXECVP2
static int call_execvP(const char *file, const char *search_path,
                       char *const argv[]) {
  typedef int (*func)(const char *, const char *, char *const *);

  DLSYM(func, fp, "execvP");

  bear_env_t current_env;
  bear_capture_env_t(&current_env);
  bear_reset_env_t(&initial_env);
  int const result = (*fp)(file, search_path, argv);
  bear_reset_env_t(&current_env);
  bear_release_env_t(&current_env);

  return result;
}
#endif

#ifdef HAVE_EXECT
static int call_exect(const char *path, char *const argv[],
                      char *const envp[]) {
  typedef int (*func)(const char *, char *const *, char *const *);

  DLSYM(func, fp, "exect");

  char const **const menvp = bear_update_environment(envp, &initial_env);
  int const result = (*fp)(path, argv, (char *const *)menvp);
  bear_strings_release(menvp);
  return result;
}
#endif

#ifdef HAVE_POSIX_SPAWN
static int call_posix_spawn(pid_t *restrict pid, const char *restrict path,
                            const posix_spawn_file_actions_t *file_actions,
                            const posix_spawnattr_t *restrict attrp,
                            char *const argv[restrict],
                            char *const envp[restrict]) {
  typedef int (*func)(pid_t *restrict, const char *restrict,
                      const posix_spawn_file_actions_t *,
                      const posix_spawnattr_t *restrict, char *const *restrict,
                      char *const *restrict);

  DLSYM(func, fp, "posix_spawn");

  char const **const menvp = bear_update_environment(envp, &initial_env);
  int const result =
      (*fp)(pid, path, file_actions, attrp, argv, (char *const *restrict)menvp);
  bear_strings_release(menvp);
  return result;
}
#endif

#ifdef HAVE_POSIX_SPAWNP
static int call_posix_spawnp(pid_t *restrict pid, const char *restrict file,
                             const posix_spawn_file_actions_t *file_actions,
                             const posix_spawnattr_t *restrict attrp,
                             char *const argv[restrict],
                             char *const envp[restrict]) {
  typedef int (*func)(pid_t *restrict, const char *restrict,
                      const posix_spawn_file_actions_t *,
                      const posix_spawnattr_t *restrict, char *const *restrict,
                      char *const *restrict);

  DLSYM(func, fp, "posix_spawnp");

  char const **const menvp = bear_update_environment(envp, &initial_env);
  int const result =
      (*fp)(pid, file, file_actions, attrp, argv, (char *const *restrict)menvp);
  bear_strings_release(menvp);
  return result;
}
#endif

/* this method is to write log about the process creation. */

static void bear_report_call(char const *fun, char const *const argv[]) {
  static int const GS = 0x1d;
  static int const RS = 0x1e;
  static int const US = 0x1f;

  if (!initialized)
    return;

  pthread_mutex_lock(&mutex);
  const char *cwd = getcwd(NULL, 0);
  if (0 == cwd) {
    perror("bear: getcwd");
    pthread_mutex_unlock(&mutex);
    exit(EXIT_FAILURE);
  }
  char const *const out_dir = initial_env[0];
  size_t const path_max_length = strlen(out_dir) + 32;
  char filename[path_max_length];
  if (-1 ==
      snprintf(filename, path_max_length, "%s/%d.cmd", out_dir, getpid())) {
    perror("bear: snprintf");
    pthread_mutex_unlock(&mutex);
    exit(EXIT_FAILURE);
  }
  FILE *fd = fopen(filename, "a+");
  if (0 == fd) {
    perror("bear: fopen");
    pthread_mutex_unlock(&mutex);
    exit(EXIT_FAILURE);
  }
  fprintf(fd, "%d%c", getpid(), RS);
  fprintf(fd, "%d%c", getppid(), RS);
  fprintf(fd, "%s%c", fun, RS);
  fprintf(fd, "%s%c", cwd, RS);
  size_t const argc = bear_strings_length(argv);
  for (size_t it = 0; it < argc; ++it) {
    fprintf(fd, "%s%c", argv[it], US);
  }
  fprintf(fd, "%c", GS);
  if (fclose(fd)) {
    perror("bear: fclose");
    pthread_mutex_unlock(&mutex);
    exit(EXIT_FAILURE);
  }
  free((void *)cwd);
  pthread_mutex_unlock(&mutex);
}

/* update environment assure that chilren processes will copy the desired
 * behaviour */

static int bear_capture_env_t(bear_env_t *env) {
  int status = 1;
  for (size_t it = 0; it < ENV_SIZE; ++it) {
    char const *const env_value = getenv(env_names[it]);
    char const *const env_copy = (env_value) ? strdup(env_value) : env_value;
    (*env)[it] = env_copy;
    status &= (env_copy) ? 1 : 0;
  }
  return status;
}

static int bear_reset_env_t(bear_env_t *env) {
  int status = 1;
  for (size_t it = 0; it < ENV_SIZE; ++it) {
    if ((*env)[it]) {
      setenv(env_names[it], (*env)[it], 1);
    } else {
      unsetenv(env_names[it]);
    }
  }
  return status;
}

static void bear_release_env_t(bear_env_t *env) {
  for (size_t it = 0; it < ENV_SIZE; ++it) {
    free((void *)(*env)[it]);
    (*env)[it] = 0;
  }
}

static char const **bear_update_environment(char *const envp[],
                                            bear_env_t *env) {
  char const **result = bear_strings_copy((char const **)envp);
  for (size_t it = 0; it < ENV_SIZE && (*env)[it]; ++it)
    result = bear_update_environ(result, env_names[it], (*env)[it]);
  return result;
}

static char const **bear_update_environ(char const *envs[], char const *key,
                                        char const *const value) {
  // find the key if it's there
  size_t const key_length = strlen(key);
  char const **it = envs;
  for (; (it) && (*it); ++it) {
    if ((0 == strncmp(*it, key, key_length)) && (strlen(*it) > key_length) &&
        ('=' == (*it)[key_length]))
      break;
  }
  // allocate a environment entry
  size_t const value_length = strlen(value);
  size_t const env_length = key_length + value_length + 2;
  char *env = malloc(env_length);
  if (0 == env) {
    perror("bear: malloc [in env_update]");
    exit(EXIT_FAILURE);
  }
  if (-1 == snprintf(env, env_length, "%s=%s", key, value)) {
    perror("bear: snprintf");
    exit(EXIT_FAILURE);
  }
  // replace or append the environment entry
  if (it && *it) {
    free((void *)*it);
    *it = env;
    return envs;
  }
  return bear_strings_append(envs, env);
}

static char **bear_get_environment() {
#if defined HAVE_NSGETENVIRON
  return *_NSGetEnviron();
#else
  return environ;
#endif
}

/* util methods to deal with string arrays. environment and process arguments
 * are both represented as string arrays. */

static char const **bear_strings_build(char const *const arg, va_list *args) {
  char const **result = 0;
  size_t size = 0;
  for (char const *it = arg; it; it = va_arg(*args, char const *)) {
    result = realloc(result, (size + 1) * sizeof(char const *));
    if (0 == result) {
      perror("bear: realloc");
      exit(EXIT_FAILURE);
    }
    char const *copy = strdup(it);
    if (0 == copy) {
      perror("bear: strdup");
      exit(EXIT_FAILURE);
    }
    result[size++] = copy;
  }
  result = realloc(result, (size + 1) * sizeof(char const *));
  if (0 == result) {
    perror("bear: realloc");
    exit(EXIT_FAILURE);
  }
  result[size++] = 0;

  return result;
}

static char const **bear_strings_copy(char const **const in) {
  size_t const size = bear_strings_length(in);

  char const **const result = malloc((size + 1) * sizeof(char const *));
  if (0 == result) {
    perror("bear: malloc");
    exit(EXIT_FAILURE);
  }

  char const **out_it = result;
  for (char const *const *in_it = in; (in_it) && (*in_it); ++in_it, ++out_it) {
    *out_it = strdup(*in_it);
    if (0 == *out_it) {
      perror("bear: strdup");
      exit(EXIT_FAILURE);
    }
  }
  *out_it = 0;
  return result;
}

static char const **bear_strings_append(char const **const in,
                                        char const *const e) {
  size_t size = bear_strings_length(in);
  char const **result = realloc(in, (size + 2) * sizeof(char const *));
  if (0 == result) {
    perror("bear: realloc");
    exit(EXIT_FAILURE);
  }
  result[size++] = e;
  result[size++] = 0;
  return result;
}

static size_t bear_strings_length(char const *const *const in) {
  size_t result = 0;
  for (char const *const *it = in; (it) && (*it); ++it)
    ++result;
  return result;
}

static void bear_strings_release(char const **in) {
  for (char const *const *it = in; (it) && (*it); ++it) {
    free((void *)*it);
  }
  free((void *)in);
}