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

#define _GNU_SOURCE
#include "config.h"

#include <ctype.h>
#include <regex.h>
#include <string.h>

#include "all.h"


/** Functions **/

/* Serialize the null-terminated vector of arguments argv and return the
   result as a newly allocated string. The purpose is to provide a
   human-readable reconstruction of a command line where each argument can
   also be recovered byte-for-byte; see ch-run(1) for details. */
char *argv_to_string(char **argv)
{
   char *s = NULL;

   for (size_t i = 0; argv[i] != NULL; i++) {
      char *argv_;
      bool quote_p = false;

      // Max length is escape every char plus two quotes and terminating zero.
      // Initialize to zeroes so we don’t have to terminate string later.
      argv_ = malloc_zeroed(2 * strlen(argv[i]) + 3, false);

      // Copy to new string, escaping as we go. Note lots of fall-through. I'm
      // not sure where this list of shell meta-characters came from; I just
      // had it on hand already from when we were deciding on the image
      // reference transformation for filesystem paths.
      for (size_t ji = 0, jo = 0; argv[i][ji] != 0; ji++) {
         char c = argv[i][ji];
         if (isspace(c) || !isascii(c) || !isprint(c))
            quote_p = true;
         switch (c) {
         case '!':   // history expansion
         case '"':   // string delimiter
         case '$':   // variable expansion
         case '\\':  // escape character
         case '`':   // output expansion
            argv_[jo++] = '\\';
         case '#':   // comment
         case '%':   // job ID
         case '&':   // job control
         case '\'':  // string delimiter
         case '(':   // subshell grouping
         case ')':   // subshell grouping
         case '*':   // globbing
         case ';':   // command separator
         case '<':   // redirect
         case '=':   // globbing
         case '>':   // redirect
         case '?':   // globbing
         case '[':   // globbing
         case ']':   // globbing
         case '^':   // command “quick substitution”
         case '{':   // command grouping
         case '|':   // pipe
         case '}':   // command grouping
         case '~':   // home directory expansion
            quote_p = true;
         default:
            argv_[jo++] = c;
            break;
         }
      }

      s = cats(5, s, i == 0 ? "" : " ",
               quote_p ? "\"" : "", argv_, quote_p ? "\"" : "");
   }

   return s;
}

/* Return a snprintf(3)-formatted string in a newly allocated buffer of
   appropriate length. Exit on error.

   This function formats the string twice: Once to figure out how long the
   formatted string is, and again to actually format the string. I’m not aware
   of a better way to compute string length. (musl does it the same way; glibc
   was too complicated for my patience in figuring it out.)

   An alternative would be to allocate a small buffer, try that, and if it’s
   too small re-allocate and format again. For strings that fit, this would
   save a formatting cycle at the cost of wasted memory and more code paths.
   That didn’t seem like the right trade-off, esp. since short strings should
   be the fastest to format. */
char *asprintf_ch(const char *fmt, ...)
{
   va_list ap;
   char *str;

   va_start(ap, fmt);
   str = vasprintf_ch(fmt, ap);
   va_end(ap);

   return str;
}

/* Return bool b as a string. */
const char *bool_to_string(bool b)
{
   return (b ? "yes" : "no");
}

/* Iterate through buffer “buf” of size “s” consisting of null-terminated
   strings and return the number of strings in it. Key assumptions:

      1. The buffer has been initialized to zero, i.e. all bytes that have not
         been explicitly set are null.

      2. All strings have been appended to the buffer in full without
         truncation, including their null terminator.

      3. The buffer contains no empty strings.

   These assumptions are consistent with the construction of the “warnings”
   shared memory buffer, which is the main justification for this function.
   Note that under these assumptions, the final byte in the buffer is
   guaranteed to be null. */
int buf_strings_count(char *buf, size_t size)
{
   int count = 0;

   if (buf[0] != '\0') {
      for (size_t i = 0; i < size; i++)
         if (buf[i] == '\0') {                     // found string terminator
            count++;
            if (i < size - 1 && buf[i+1] == '\0')  // two term. in a row; done
               break;
         }
   }

   return count;
}

/* Concatenate strings a and b into a newly-allocated buffer and return a
   pointer to this buffer. */
char *cat(const char *a, const char *b)
{
   return cats(2, a, b);
}

/* Concatenate argc strings into a newly allocated buffer and return a pointer
   to this buffer. If argc is zero, return the empty string. NULL pointers are
   treated as empty strings. */
char *cats(size_t argc, ...)
{
   char *ret, *next;
   size_t ret_len;
   char **argv;
   size_t *argv_lens;
   va_list ap;

   argv = malloc_ch(argc * sizeof(char *), true);
   argv_lens = malloc_ch(argc * sizeof(size_t), false);

   // compute buffer size and convert NULLs to empty string
   va_start(ap, argc);
   ret_len = 1;  // for terminator
   for (int i = 0; i < argc; i++)
   {
      char *arg = va_arg(ap, char *);
      if (arg == NULL) {
         argv[i] = "";
         argv_lens[i] = 0;
      } else {
         argv[i] = arg;
         argv_lens[i] = strlen(arg);
      }
      ret_len += argv_lens[i];
   }
   va_end(ap);

   // copy strings
   ret = malloc_ch(ret_len, false);
   next = ret;
   for (int i = 0; i < argc; i++) {
      memcpy(next, argv[i], argv_lens[i]);
      next += argv_lens[i];
   }
   ret[ret_len-1] = '\0';

   return ret;
}

/* Return a string describing the given errno_ in a nerdly fashion, i.e.,
   numeric value and also C constant name if available. */
char *errno_nerd_str(int errno_)
{
   char *ret;

   ret = asprintf_ch("%d", errno_);
#ifdef HAVE_STRERRORNAME_NP
   ret = cats(3, ret, " ", strerrorname_np(errno_));
#endif

   return ret;
}

/** Return a copy of @p s with all instances of @p old replaced with @p new. */
char *replace_char(const char *s, char old, char new)
{
   char *ret = strdup_ch(s);

   for (int i = 0; ret[i] != '\0'; i++)
      if (ret[i] == old)
         ret[i] = new;

   return ret;
}

/** Split a string on the first occurrence of a delimiter byte.

    @param a[out]   The part of @p str before the first occurrence of @p del,
                    or all of @p str if @p del is not present.

    @param b[out]   The part of @p str after the first occurrence of @p del,
                    or @c NULL if @p del is not present.

    @param str[in]  String to split. May not be @c NULL. Can be the same
                    buffer as @p a or @p b, if its value isn’t needed after
                    the call.

    @param del[in]  Delimiter to split on.

    @p a and @p b point into a newly allocated buffer, and @p str is left
    unchanged. This is the *same* buffer, so the parts can be re-joined by
    setting @c *(*b-1) to @p del. The point here is to provide an easier
    wrapper for @c strsep(3). */
void split(char **a, char **b, const char *str, char del)
{
   char delstr[2] = { del, 0 };
   T__ (str != NULL);
   *b = strdup_ch(str);
   *a = strsep(b, delstr);
}

/* Return a copy of s in a newly allocated, pointerless buffer. Cannot fail.

   Note: Unlike strdup(3), strdup_ch() is only needed if you need to actually
   modify the copy. It should not be used to simplify memory management.

   Implemented in terms of a memory copy so we don’t need to care about which
   strdup(3) is being used (libc or libgc). */
char *strdup_ch(const char *s)
{
   char *dst;
   size_t buf_sz = strlen(s) + 1;

   dst = malloc_ch(buf_sz, false);
   memcpy(dst, s, buf_sz);
   return dst;
}

/** Test equality of two strings.

      @param a,b  Strings to compare. May not be @c NULL.

    @returns True if @p a and @p b are equal, false otherwise.

    TL;DR: I got burned by @c strcmp(3)’s confusing inverted return value one
    too many times */
bool streq(const char *a, const char *b)
{
   T__ (a && b);

#undef strcmp
   return !strcmp(a, b);
#define strcmp strcmp_error
}

/** Test equality of first @p n bytes of two strings.

      @param a,b  Strings to compare. May not be @c NULL.

      @param n    Maximum number of bytes to compare.

    @returns True if @a and @p are equal through the first @p n bytes, false otherwise. If no bytes are compared, i.e. @p n is zero, return true.

    This is equivalent to @c streq() if either input’s length is less than or equal to @n bytes. */
bool streqn(const char *a, const char *b, size_t n)
{
   T__ (a && b);

#undef strncmp
   return !strncmp(a, b, n);
#define strncmp strncmp_error
}

/** Test if a string matches a regular expression pattern.

      @param str      String to test against the pattern.

      @param pattern  POSIX extended regular expression pattern.

    @returns true if @p str matches @p pattern, false otherwise.

    Uses POSIX extended regular expressions (REG_EXTENDED). Note the pattern is
    compiled for each call. */
bool strregex(const char *str, const char *pattern)
{
   regex_t regex;
   bool ret;

   T__ (pattern && str);

   T_e (0 == regcomp(&regex, pattern, REG_EXTENDED | REG_NOSUB));
   ret = (regexec(&regex, str, 0, NULL, 0) == 0);
   regfree(&regex);

   return ret;
}

/* Append null-terminated string “str” to the memory buffer “offset” bytes
   after from the address pointed to by “addr”. Buffer length is “size” bytes.
   Return the number of bytes written. If there isn’t enough room for the
   string, do nothing and return zero. */
size_t string_append(char *addr, char *str, size_t size, size_t offset)
{
   size_t written = strlen(str) + 1;

   if (size > (offset + written - 1))  // there is space
      memcpy(addr + offset, str, written);

   return written;
}

/* Like asprintf_ch(), but takes and consumes a va_list pointer. */
char *vasprintf_ch(const char *fmt, va_list ap)
{
   va_list ap2;
   int str_len;
   char *str; // = malloc_ch(1024, false);

   va_copy(ap2, ap);

   T_e (0 <= (str_len = vsnprintf(NULL, 0, fmt, ap)));
   str = malloc_ch(str_len + 1, false);
   T_e (str_len == vsnprintf(str, str_len + 1, fmt, ap2));

   va_end(ap2);

   return str;
}