/* Copyright © Triad National Security, LLC, and others. */

#define _GNU_SOURCE
#include "config.h"

#include <grp.h>
#include <pwd.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include "all.h"


/** External variables **/

/* Path to host temporary directory. Set during command line processing. */
char *host_tmp = NULL;

/* Username of invoking user. Set during early initialization. */
char *username = NULL;


/** Constants **/

/* Maximum string buffer size for getpwuid_r(3) and friends. */
size_t PW_STRINGS_MAX = 16384;

/* “No ID” sentinel values. Can’t use zero because that’s root. This appears
   to match POSIX, for chown(2) at least [1].
   [1]: https://www.man7.org/linux/man-pages/man3/chown.3p.html */
uid_t UID_NONE = (unsigned)-1;
gid_t GID_NONE = (unsigned)-1;


/** Private interface **/

void *getxxxid_real(uid_t uid, gid_t gid);


/** Functions **/

#undef execvpe
noreturn execvpe_ch(const char *path, char *const argv[], char *const envp[])
{
   execvpe(path, argv, envp);  // only returns on error
   if (errno == ENOEXEC)
      ERROR (errno, "can't exec: bad format: wrong architecture?: %s", path);
   else
      ERROR (errno, "can't exec: %s", path);
   exit(EXIT_ERR_CMD);
}
#define execvpe FN_BLOCKED

/** Retrieve group file entry.

    @param gid    Group to look up.

    @return Pointer to newly allocated `struct group`, or NULL if the UID
    cannot be found.

    On error, exit fatally.

    We provide this wrapper because (1) getpwuid(3) provides awkward
    diagnostics, (2) getpwuid_r(3) is even harder to call, and (3) getpwuid(3)
    is not thread-safe. See issue #2129.

    This function uses getgrgid_r(3) and is thus thread-safe. */
struct group *getgrgid_ch(gid_t gid)
{
   return getxxxid_real(UID_NONE, gid);
}

/** Retrieve password file entry.

    @param uid    User to look up.

    @return Pointer to newly allocated `struct passwd`, or NULL if the UID
    cannot be found.

    On error, exit fatally.

    We provide this wrapper because (1) getpwuid(3) provides awkward
    diagnostics, (2) getpwuid_r(3) is even harder to call, and (3) getpwuid(3)
    is not thread-safe. See issue #2129.

    This function uses getpwuid_r(3) and is thus thread-safe. */
struct passwd *getpwuid_ch(uid_t uid)
{
   return getxxxid_real(uid, GID_NONE);
}

/** Retrieve password or group file entry.

    @param uid     User ID to look up, or @c UID_NONE to look up group ID.

    @param gid     Group ID to look up, or @c GID_NONE to look up user ID.

    @param getx_r  Function to call to loop said ID, either getpwuid_r(3) or
                   getgrgid_r(3) depending on the type of @p id.

    @return Pointer to filled-in @p ret, or NULL if @p id not found.

    @note Exactly one of @p uid and @p gid must be valid, i.e. not @c
    {UID,GID}_NONE.

    The point of this function is that getpwuid_r(3) and getgrgid_r(3) have
    nearly identical drama to call them, which this function encapsulates, but
    to do so in C, it offers a really bizarre API. */
void *getxxxid_real(uid_t uid, gid_t gid)
{
   size_t ret_sz;
   void *ret;
   size_t strings_sz = 16;  // arbitrary starting buffer size
   void *strings = malloc_ch(strings_sz, false);

   // Allocate return buffer of correct size.
   if      (uid != UID_NONE && gid == GID_NONE)
      ret_sz = sizeof(struct passwd);
   else if (gid != GID_NONE && uid == UID_NONE)
      ret_sz = sizeof(struct group);
   else
      T__(false);  // invalid uid/gid combinarion
   ret = malloc_ch(ret_sz, true);

   while (strings_sz <= PW_STRINGS_MAX) {
      int errno_;
      void *found;

      // getpwuid_r(3) and getgrgid_r(3) have remarkably difficult APIs [1].
      // Enjoy a 30-line comment on how to call it correctly. Specifically:
      //
      //   1. The “found” parameter (“result” in the man pages) ends up either
      //      NULL or pointing to something you already have because you
      //      passed it in.
      //
      //   2. You can’t reliably tell how large the strings buffer need to be
      //      ahead of time, even by asking sysconf(3). Hence our loop. The
      //      initial value is sized to have a reasonably high probability of
      //      looping. We could instead size it very large and thus avoid
      //      realloc(3) most of the time, but then that code path becomes
      //      rarely used and thus buggy.
      //
      //   3. Discriminating between “successful lookup but not found” and
      //      “error during lookup” is awkward. Neither “found” nor the return
      //      value (which is a copy of “errno” AFAICT):
      //
      //        a. “found” is NULL in either condition.
      //
      //        b. While the man page states that “if no matching password
      //           record was found, these functions return 0”, there are
      //           reports that this is not true [2, e.g.], and I’ve seen
      //           ENOENT myself with getpwuid(3), which I’m pretty sure calls
      //           getpwuid_r(3) to do the actual work (that’s #2129).
      //
      //      We do what seems to be the convention which is to treat some
      //      errno values as a successful negative result [3, e.g.] and
      //      others as actual error.
      //
      // [1]: https://www.man7.org/linux/man-pages/man3/getpwuid.3.html
      // [2]: https://stackoverflow.com/questions/16666587
      // [3]: https://github.com/ruby/ruby/blob/81562f94/process.c#L5934-L5950
      if (uid != UID_NONE) {
         #undef getpwuid_r
         errno_ = getpwuid_r(uid, ret, strings, strings_sz,
                             (struct passwd **)&found);
         #define getpwuid_r FN_BLOCKED
      } else {
         #undef getgrgid_r
         errno_ = getgrgid_r(gid, ret, strings, strings_sz,
                             (struct group **)&found);
         #define getgruid_r FN_BLOCKED
      }
      if (found)         // success
         return ret;
      switch (errno_) {
         case 0:
         case ENOENT:
         case ESRCH:
         case EBADF:
         case EPERM:      // all of these indicate not found
            return NULL;
            break;
         case ERANGE:     // string buffer too small
            strings_sz *= 2;
            strings = realloc_ch(strings, strings_sz, false);
         case EINTR:      // syscall interrupted, just try again
            break;
         default:         // something actually went wrong
            Tfe (0, "bug: failure looking up UID %u or GID %u", uid, gid);
            break;
      }
   }
   Tf_ (0, "bug: user data too large for UID %u or GID %u", uid, gid);
}

/** Run a command, wait for it to exit, then return. If the command fails,
    exit with a fatal error. Similar to Python’s @c subprocess.run().

    @param cmd    Command to run. If a path contains a slash, run that file
                  directly; otherwise, search in @c $PATH; i.e. the same
                  behavior as @c execlp(3).

    @param pathx  Colon-separated string suitable for @c $PATH to append to
                  the existing @c $PATH (e.g. “@c /foo:/bar”)., or @c NULL if
                  none. Neither the child nor the current process” @c $PATH is
                  modified. May not begin/end with colon.

    @param args2  Null-terminated array of arguments for the command, like
                  @c execv(3).

    @param args1  Null-terminated variadic arguments for the command, like
                  @c execl(3). If not used, caller must provide @c NULL.

    @warning
    Both @p args1 and @p args2 can be non-NULL simultaneously. In this case,
    @p args1 precedes @p args2 on the @p cmd command line, which is the
    opposite order from the function call.

    Design decisions:

    #. Manual @c fork(2) / @c exec(2) rather than @c posix_spawn(3) for better
       error reporting.

    #. @c fork(2) rather than @c vfork(2) or @c clone(2) because I didn’t feel
       like thinking hard enough to figure out whether one of the latter two
       would be better.

    #. @c fork(2) rather than @c fork_ch() because we don’t want garbage
       collection overhead. */
void run_wait(char *cmd, char *pathx, char **args2, ...)
{
   pid_t child;

   T__ (pathx && pathx[0] != '\0');
   T__ (pathx[0] != ':' && pathx[strlen(pathx)-1] != ':');

#undef fork
   Tfe (0 <= (child = fork()), "can't fork: %s", cmd);
#define fork FN_BLOCKED
   if (child) {  // parent
      int ws;
      DEBUG("run_wait: waiting ...");
      T_e (-1 != waitpid(child, &ws, 0));
      if (WIFEXITED(ws)) {
         Zf_ (WEXITSTATUS(ws), "%s: failed with %d", cmd, WEXITSTATUS(ws));
      } else {
         // no WUNTRACED so assume WIFSIGNALED
         Tf_ (0, "%s: killed by signal %d", cmd, WTERMSIG(ws));
      }
   } else {      // child
      char **args, **env_saved;
      char *arg, *path;
      va_list ap;

      // append pathx for execvpe_ch()
      env_saved = list_copy(environ, sizeof(environ[0]));
      path = cats(3, getenv("PATH"), (pathx == NULL ? "" : ":"), pathx);
      setenv("PATH", path, true);
      DEBUG("run_wait: PATH=%s", path);

      // prepare command arguments
      args = NULL;
      list_append((void **)&args, &cmd, sizeof(cmd));
      va_start(ap, args2);
      while ((arg = va_arg(ap, char *)))
         list_append((void **)&args, &arg, sizeof(arg));
      va_end(ap);
      list_cat((void **)&args, args2, sizeof(char *));

      // replace self with command
      T__ (streq(cmd, args[0]));
      DEBUG("run_wait: $ %s", argv_to_string(args));
      execvpe_ch(cmd, args, env_saved);
   }
}

/* Set the username global by looking up EUID in the password database. Logic
   must match ch.user(). Formerly, we used $USER, but that’s not reliably set.
   See #1162. This approach does require that EUID *have* a corresponding
   username. */
void username_set(void)
{
   struct passwd *pw = getpwuid_ch(geteuid());
   username = pw->pw_name;
}

/* Report the version number. */
void version(void)
{
   fprintf(stderr, "%s\n", VERSION);
}
