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

#define _GNU_SOURCE
#include "config.h"

#include <fnmatch.h>
#include <string.h>
#include <unistd.h>

#include "all.h"


/** Macros **/


/** Function prototypes (private) **/

char *environ_get(char **env, const char *name, int *index);
char *environ_expand(char **env, const char *value);


/** Functions **/

/** Get a container environment variable.

      @param  c     Container configuration whose environment to search.

      @param  name  Name of variable to look up.

      @returns String value of environment variable @p name in @p c, or @c
               NULL if not set. This is a pointer to the container’s actual
               environment buffer and thus should not be modified. */
char *cenv_get(struct container *c, const char *name)
{
   return environ_get(c->environ, name, NULL);
}

/** Set variable in container environment.

      @param c      Container configuration to change.

      @param name   Name of variable.

      @param value  New value for variable.

    If @p name already exists in the container environment, its value is replaced. Otherwise, it is added to the environment.

    If @p c->env_expand, then further expand variables in @p value marked with
    @c $, as described in man page. */
void cenv_set(struct container *c, const char *name, const char *value)
{
   int i;
   char *line;

   T__(strchr(name, '=') == NULL);
   line = cats(3, name, "=",
               c->env_expand ? environ_expand(c->environ, value) : value);

   DEBUG("environment: %s", line);
   (void)environ_get(c->environ, name, &i);
   if (i != -1)
      c->environ[i] = line;
   else
      list_append((void **)&(c->environ), &line, sizeof(line));
}

/** Set multiple variables in a container environment.

      @param c     Container to modify.

      @param vars  List of variables to set. */
void cenvs_set(struct container *c, const struct env_var *vars)
{
   for (size_t i = 0; vars[i].name != NULL; i++)
      cenv_set(c, vars[i].name, vars[i].value);
}

/** Unset variable in container environment.

      @param name  Name of variable to unset.

    If @p name is not in the container environment, do nothing. */
void cenv_unset(struct container *c, const char *name)
{
   int victim_i;
   int last_i = list_count(c->environ, sizeof(c->environ[0])) - 1;
   T__ (name != NULL);

   (void)environ_get(c->environ, name, &victim_i);
   if (victim_i == -1)
      DEBUG("environment: unset: not set, ignoring: %s", name);
   else {
      DEBUG("environment: unset %s", name);
      // swap with last non-null line to avoid gap
      c->environ[victim_i] = c->environ[last_i];
      c->environ[last_i] = NULL;
   }
}

/** Remove variables matching a glob from the container environment.

      @param c    Container to modify.

      @param glob  All variables whose names match this glob will be removed
                   from the container environment. */
void cenvs_unset(struct container *c, const char *glob)
{
   char **new_environ;

   T__ (c->environ != NULL);
   new_environ = list_new(list_count(c->environ, sizeof(c->environ[0])),
                          sizeof(c->environ[0]));

   // Copy into new environment the variables whose names *don’t* match.
   for (int i_old = 0, i_new = 0; c->environ[i_old] != NULL; i_old++) {
      char *name, *value;
      int matchp;
      split(&name, &value, c->environ[i_old], '=');
      T__ (value != NULL);                         // entries must have equals
      matchp = fnmatch(glob, name, FNM_EXTMATCH);  // extglobs if available
      if (matchp == 0) {
         DEBUG("environment: unset %s", name);
      } else {
         T__ (matchp == FNM_NOMATCH);
         *(value - 1) = '=';  // rejoin line
         new_environ[i_new++] = c->environ[i_old];
      }
   }

   // Note: It is legitimate to reassign the environ(7) global, if we ever
   // want to do that. See: http://man7.org/linux/man-pages/man3/exec.3p.html
   c->environ = new_environ;
}

/* Read the file listing environment variables at path, with records separated
   by delim, and return a corresponding list of struct env_var. Reads the
   entire file one time without seeking. If there is a problem reading the
   file, or with any individual variable, exit with error.

   The purpose of delim is to allow both newline- and zero-delimited files. We
   did consider using a heuristic to choose the file’s delimiter, but there
   seemed to be two problems. First, every heuristic we considered had flaws.
   Second, use of a heuristic would require reading the file twice or seeking.
   We don’t want to demand non-seekable files (e.g., pipes), and if we read
   the file into a buffer before parsing, we’d need our own getdelim(3). See
   issue #1124 for further discussion. */
struct env_var *env_file_read(const char *path, int delim)
{
   struct env_var *vars;
   FILE *fp;

   Tfe (fp = fopen(path, "r"), "can't open: %s", path);

   vars = list_new(0, sizeof(struct env_var));
   for (size_t line_no = 1; true; line_no++)
   {
      struct env_var var;
      char *line;
      errno = 0;
      line = getdelim_ch(fp, delim);
      if (line == NULL) // EOF
         break;
      if (line[0] == '\0') // skip blank lines
         continue;
      var = env_parse(line, path, line_no);
      list_append((void **)&vars, &var, sizeof(var));
   }

   Zfe (fclose(fp), "can't close: %s", path);
   return vars;
}

/* Parse the environment variable in line and return it as a struct env_var.
   Exit with error on syntax error; if path is non-NULL, attribute the problem
   to that path at line_no. Note: Trailing whitespace such as newline is
   *included* in the value. */
struct env_var env_parse(const char *line, const char *path, size_t lineno)
{
   char *name, *value, *where;

   if (path == NULL)
      where = strdup_ch(line);
   else
      where = asprintf_ch("%s:%zu", path, lineno);

   // Split line into variable name and value.
   split(&name, &value, line, '=');
   Tf_ (value != NULL, "can't parse variable: no delimiter: %s", where);
   Tf_ (name[0] != 0, "can't parse variable: empty name: %s", where);

   // Strip leading and trailing single quotes from value, if both present.
   if (   strlen(value) >= 2
       && value[0] == '\''
       && value[strlen(value) - 1] == '\'') {
      value[strlen(value) - 1] = 0;
      value++;
   }

   return (struct env_var){ name, value };
}

/** Get the value of a variable in an @c environ(7)-style array.

      @param  env[in]     @c NULL-terminated array of strings having the form
                          @c name=value, i.e. variable name as a sequence of
                          one or more bytes, equals character (@c 0x3d),
                          variable value as a sequence of zero or more bytes,
                          terminating zero byte.

      @param  name[in]    Name of variable to search for.

      @param  index[out]  Array index of variable @p name, or -1 if not found.
                          Ignored if a null pointer.

    @returns the value of variable @p name, or @c NULL if not found. This is a
             pointer into @p env and thus should not be modified. */
char *environ_get(char **env, const char *name, int *index)
{
   size_t name_len = strlen(name);

   for (int i = 0; env[i] != NULL; i++) {
      char *v;

      T__ (v = strchr(env[i], '='));        // address of first ‘=’
      v++;                                  // skip ‘=’

      if (name_len != (v - env[i] - 1))     // lengths different ⇒ no match
         continue;

      if (streqn(env[i], name, name_len)) {  // found
         if (index != NULL)
            *index = i;
         return v;
      }
   }

   // not found
   if (index != NULL)
      *index = -1;
   return NULL;
}

/** Expand shell-style variable notation in a string.

      @param env    List of environment variable names and values in the same
                    format at @c environ(7). This is the variables used for
                    expansion.

      @param value  String to expand.

    @returns the expanded string.

    See @c ch-run(1) for the details of how expansion works. Notably,
    expansions within colon-separated lists (e.g. @c $PATH) have special
    treatment. */
char *environ_expand(char **env, const char *value)
{
   char *vwk;                   // modifiable copy of value
   char *vwk_cur;               // current location in vwk
   char *vout = strdup_ch("");  // output (expanded) string
   bool first_out = false;      // true after 1st output element written
   vwk = strdup_ch(value);
   vwk_cur = vwk;

   while (true) {                          // loop executes ≥ once
      char *elem = strsep(&vwk_cur, ":");  // NULL ⇒ no more elements
      if (elem == NULL)
         break;
      if (elem[0] == '$' && elem[1] != 0) {  // looks like $VARIABLE
         elem = environ_get(env, elem + 1, NULL);
         if (elem != NULL && elem[0] == '\0')  // if set but empty ...
            elem = NULL;                       // ... convert to unset
      }
      if (elem != NULL) {  // empty -> omit from output list
         vout = cats(3, vout, first_out ? ":" : "", elem);
         first_out = true;
      }
   }

   return vout;
}

/** Pack an @c environ(7) style environment into a buffer.

      @param[in]  env      Null-terminated list of equals-separate environment
                           variable strings.

      @param[out] byte_ct  Size of the returned buffer in bytes.

    @returns a newly-allocated buffer containing the variable strings in @p
    env separated by zero bytes (@c '\0'). This is the same format as @c
    /proc/<pid>/environ. */
char *environ_serialize(char **env, size_t *byte_ct)
{
   char *buf = NULL;

   *byte_ct = 0;
   for (size_t i = 0; env[i] != NULL; i++) {
      size_t line_byte_ct = strlen(env[i]) + 1;
      buf = realloc_ch(buf, *byte_ct + line_byte_ct, false);
      memcpy(buf + *byte_ct, env[i], line_byte_ct);
      *byte_ct += line_byte_ct;
   }

   return buf;
}

/** Unpack a buffer of environment variables.

      @param buf      Buffer containing a zero-byte (@c '\0') delimited
                      sequence of equals-separated variables (same format as
                      @c /proc/<pid>/environ).

      @param byte_ct  Length of buffer in bytes.

   @returns a @c environ(7) style null-terminated array of strings. Both the
   array of pointers and the strings they point to are newly allocated (i.e.,
   it’s a deep copy); thus, @p buf need not remain valid after the call.

   @note The last byte in @p env must be @c '\0', terminating the last
   variable string. */
char **environ_unserialize(char *buf, size_t byte_ct)
{
   char **env = NULL;
   char *new = malloc_ch(byte_ct, false);
   char *next;
   int var_ct;

   memcpy(new, buf, byte_ct);

   // Allocate env to the right size.
   var_ct = 0;
   for (int i = 0; i < byte_ct; i++)
      var_ct += (new[i] == '\0');
   env = list_new(var_ct, sizeof(char *));

   next = new;
   for (int i = 0; i < var_ct; i++) {
      env[i] = next;
      next += strlen(next) + 1;
   }
   T__ (new[byte_ct - 1] == '\0');
   T__ (next == new + byte_ct);

   return env;
}

/* Set the environment variables listed in d. */
void envs_hook_set(struct container *c, void *d)
{
   struct env_var *vars = d;
   cenvs_set(c, vars);
}

/* Set the environment variables specified in file d. */
void envs_hook_set_file(struct container *c, void *d)
{
   struct env_file *ef = d;
   cenvs_set(c, env_file_read(ef->path, ef->delim));
}

/* Unset the environment variables matching glob d. */
void envs_hook_unset(struct container *c, void *d)
{
   cenvs_unset(c, (char *)d);
}

/** @c getenv(3) with a default value.

      @param name  Name of environment variable to fetch.

      @param df    Value to return if @p name is not set.

    @returns the value of environment variable @p name if set, otherwise @p
    default. Note: if @p name is set to the empty string, that is the return
    value, i.e. the distinction between unset and set but empty matters. */
char *getenv_df(const char *name, char *df)
{
   char *ret = getenv(name);
   if (!ret)
      ret = df;
   return ret;
}

/** Get the first environment variable in a prioritized list that is set.

      @param names[in]   Zero-terminated array of variable names in descending
                         priority order.

      @param name[out]   Name of the first variable in @p names that is set,
                         or @c NULL if none were.

      @param value[out]  Value of the first set variable, or @c NULL.

    @returns true if one of the variables was set, false otherwise. */
bool getenv_first(char **array, char **name, char **value)
{
   for (int i = 0; array[i] != NULL; i++) {
      *name = array[i];
      *value = getenv(*name);
      if (*value != NULL)
         return true;
   }

   *name = NULL;
   *value = NULL;
   return false;
}
