/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8                               :vi │
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2021 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/atomic.h"
#include "libc/calls/calls.h"
#include "libc/calls/metalfile.internal.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/cosmo.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/fmt/libgen.h"
#include "libc/intrin/getenv.h"
#include "libc/intrin/strace.h"
#include "libc/limits.h"
#include "libc/macros.h"
#include "libc/nt/runtime.h"
#include "libc/runtime/runtime.h"
#include "libc/serialize.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/at.h"
#include "libc/sysv/consts/ok.h"

#ifdef __x86_64__
__static_yoink("_init_program_executable_name");
#endif

#define CTL_KERN                   1
#define KERN_PROC                  14
#define KERN_PROC_PATHNAME_FREEBSD 12
#define KERN_PROC_PATHNAME_NETBSD  5

#define DevFd() (IsBsd() ? "/dev/fd/" : IsLinux() ? "/proc/self/fd/" : 0)
#define StrlenDevFd()                      \
  ((IsBsd()     ? sizeof("/dev/fd/")       \
    : IsLinux() ? sizeof("/proc/self/fd/") \
                : 0) -                     \
   1)

static struct {
  atomic_uint once;
  union {
    char buf[PATH_MAX];
    char16_t buf16[PATH_MAX / 2];
  } u;
} g_prog;

static inline int IsAlpha(int c) {
  return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
}

static inline int AllNumDot(const char *s) {
  while (true) {
    switch (*s++) {
      default:
        return 0;
      case 0:
        return 1;
      case '0':
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
      case '.':; /* continue */
    }
  }
}

// old loaders do not pass __program_executable_name, so we need to
// check for them when we use KERN_PROC_PATHNAME et al.
static int OldApeLoader(char *s) {
  char *b;
  return !strcmp(s, "/usr/bin/ape") ||
         (!strncmp((b = basename(s)), ".ape-", 5) && AllNumDot(b + 5));
}

static int CopyWithCwd(const char *q, char *p, char *e) {
  char c;
  if (*q != '/') {
    if (q[0] == '.' && q[1] == '/')
      q += 2;
    int got = __getcwd(p, e - p - 1 /* '/' */);
    if (got != -1) {
      p += got - 1;
      *p++ = '/';
    }
  }
  while ((c = *q++)) {
    if (p + 1 /* nul */ < e) {
      *p++ = c;
    } else {
      return 0;
    }
  }
  *p = 0;
  return 1;
}

// if q exists then turn it into an absolute path.
static int TryPath(const char *q) {
  if (!q)
    return 0;
  if (!CopyWithCwd(q, g_prog.u.buf, g_prog.u.buf + sizeof(g_prog.u.buf)))
    return 0;
  return !sys_faccessat(AT_FDCWD, g_prog.u.buf, F_OK, 0);
}

// if the loader passed a relative path, prepend cwd to it.
// called early in init.
void __init_program_executable_name(void) {
  if (__program_executable_name && *__program_executable_name != '/' &&
      CopyWithCwd(__program_executable_name, g_prog.u.buf,
                  g_prog.u.buf + sizeof(g_prog.u.buf)))
    __program_executable_name = g_prog.u.buf;
}

static inline void InitProgramExecutableNameImpl(void) {
  size_t n;
  ssize_t got;
  char c, *q, *b;

  if (IsWindows()) {
    int n = GetModuleFileName(0, g_prog.u.buf16, ARRAYLEN(g_prog.u.buf16));
    for (int i = 0; i < n; ++i) {
      // turn c:\foo\bar into c:/foo/bar
      if (g_prog.u.buf16[i] == '\\') {
        g_prog.u.buf16[i] = '/';
      }
    }
    if (IsAlpha(g_prog.u.buf16[0]) &&  //
        g_prog.u.buf16[1] == ':' &&    //
        g_prog.u.buf16[2] == '/') {
      // turn c:/... into /c/...
      g_prog.u.buf16[1] = g_prog.u.buf16[0];
      g_prog.u.buf16[0] = '/';
      g_prog.u.buf16[2] = '/';
    }
    tprecode16to8(g_prog.u.buf, sizeof(g_prog.u.buf), g_prog.u.buf16);
    goto UseBuf;
  }
  if (IsMetal()) {
    __program_executable_name = APE_COM_NAME;
    return;
  }

  // see if the loader passed us a path.
  if (__program_executable_name) {
    if (issetugid()) {
      /* we are running as a set-id interpreter script. this is highly unusual.
         it means either someone installed their ape loader set-id, or they are
         running a system that supports secure set-id interpreter scripts via a
         /dev/fd/ path. check for the latter and allow that. otherwise, use the
         empty string to obviate the TOCTOU problem between loader and binary.
       */
      if (!(b = DevFd()) ||
          0 != strncmp(b, __program_executable_name, (n = StrlenDevFd())) ||
          !__program_executable_name[n] ||
          __program_executable_name[n] == '.' ||
          strchr(__program_executable_name + n, '/')) {
        goto UseEmpty;
      }
    }
    return;
  }

  b = g_prog.u.buf;
  n = sizeof(g_prog.u.buf) - 1;
  if (IsFreebsd() || IsNetbsd()) {
    int cmd[4];
    cmd[0] = CTL_KERN;
    cmd[1] = KERN_PROC;
    if (IsFreebsd()) {
      cmd[2] = KERN_PROC_PATHNAME_FREEBSD;
    } else {
      cmd[2] = KERN_PROC_PATHNAME_NETBSD;
    }
    cmd[3] = -1;  // current process
    if (sysctl(cmd, ARRAYLEN(cmd), b, &n, 0, 0) != -1) {
      if (!OldApeLoader(b)) {
        goto UseBuf;
      }
    }
  }
  if (IsLinux()) {
    if ((got = sys_readlinkat(AT_FDCWD, "/proc/self/exe", b, n)) > 0 ||
        (got = sys_readlinkat(AT_FDCWD, "/proc/curproc/file", b, n)) > 0) {
      b[got] = 0;
      if (!OldApeLoader(b)) {
        goto UseBuf;
      }
    }
  }

  // don't trust argv or envp if set-id.
  if (issetugid())
    goto UseEmpty;

  // try argv[0], then then $_.
  if (TryPath(__argv[0]) || TryPath(__getenv(__envp, "_").s))
    goto UseBuf;

  // give up and just copy argv[0] into it
  if ((q = __argv[0])) {
    char *p = g_prog.u.buf;
    char *e = p + sizeof(g_prog.u.buf);
    while ((c = *q++)) {
      if (p + 1 < e) {
        *p++ = c;
      }
    }
    *p = 0;
    goto UseBuf;
  }

UseEmpty:
  // if we don't even have that then empty the string
  g_prog.u.buf[0] = 0;

UseBuf:
  __program_executable_name = g_prog.u.buf;
}

static void InitProgramExecutableName(void) {
  int e = errno;
  InitProgramExecutableNameImpl();
  errno = e;
}

/**
 * Returns absolute path of program.
 */
char *GetProgramExecutableName(void) {
  cosmo_once(&g_prog.once, InitProgramExecutableName);
  STRACE("GetProgramExecutableName() → %#s", __program_executable_name);
  return __program_executable_name;
}
