/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/
#include "tool_setup.h"

#include "tool_cfgable.h"
#include "tool_getparam.h"
#include "tool_msgs.h"
#include "tool_paramhlp.h"
#include "tool_writeout_json.h"
#include "var.h"

#define MAX_EXPAND_CONTENT 10000000
#define MAX_VAR_LEN        128 /* max length of a name */

/* free everything */
void varcleanup(void)
{
  struct tool_var *list = global->variables;
  while(list) {
    struct tool_var *t = list;
    list = list->next;
    curlx_free(CURL_UNCONST(t->content));
    curlx_free(t);
  }
}

static const struct tool_var *varcontent(const char *name, size_t nlen)
{
  struct tool_var *list = global->variables;
  while(list) {
    if((strlen(list->name) == nlen) && !strncmp(name, list->name, nlen)) {
      return list;
    }
    list = list->next;
  }
  return NULL;
}

#define ENDOFFUNC(x) (((x) == '}') || ((x) == ':'))
#define FUNCMATCH(ptr, name, len)                   \
  (!strncmp(ptr, name, len) && ENDOFFUNC((ptr)[len]))

#define FUNC_TRIM      "trim"
#define FUNC_TRIM_LEN  (sizeof(FUNC_TRIM) - 1)
#define FUNC_JSON      "json"
#define FUNC_JSON_LEN  (sizeof(FUNC_JSON) - 1)
#define FUNC_URL       "url"
#define FUNC_URL_LEN   (sizeof(FUNC_URL) - 1)
#define FUNC_B64       "b64"
#define FUNC_B64_LEN   (sizeof(FUNC_B64) - 1)
#define FUNC_64DEC     "64dec" /* base64 decode */
#define FUNC_64DEC_LEN (sizeof(FUNC_64DEC) - 1)

static ParameterError varfunc(char *c, /* content */
                              size_t clen, /* content length */
                              const char *f, /* functions */
                              size_t flen, /* function string length */
                              struct dynbuf *out)
{
  bool alloc = FALSE;
  ParameterError err = PARAM_OK;
  const char *finput = f;

  /* The functions are independent and runs left to right */
  while(*f && !err) {
    if(*f == '}')
      /* end of functions */
      break;
    /* On entry, this is known to be a colon already. In subsequent laps, it
       is also known to be a colon since that is part of the FUNCMATCH()
       checks */
    f++;
    if(FUNCMATCH(f, FUNC_TRIM, FUNC_TRIM_LEN)) {
      size_t len = clen;
      f += FUNC_TRIM_LEN;
      if(clen) {
        /* skip leading white space, including CRLF */
        while(ISSPACE(*c)) {
          c++;
          len--;
        }
        while(len && ISSPACE(c[len - 1]))
          len--;
      }
      /* put it in the output */
      curlx_dyn_reset(out);
      if(curlx_dyn_addn(out, c, len)) {
        err = PARAM_NO_MEM;
        break;
      }
    }
    else if(FUNCMATCH(f, FUNC_JSON, FUNC_JSON_LEN)) {
      f += FUNC_JSON_LEN;
      curlx_dyn_reset(out);
      if(clen) {
        if(jsonquoted(c, clen, out, FALSE)) {
          err = PARAM_NO_MEM;
          break;
        }
      }
    }
    else if(FUNCMATCH(f, FUNC_URL, FUNC_URL_LEN)) {
      f += FUNC_URL_LEN;
      curlx_dyn_reset(out);
      if(clen) {
        char *enc = curl_easy_escape(NULL, c, (int)clen);
        if(!enc) {
          err = PARAM_NO_MEM;
          break;
        }

        /* put it in the output */
        if(curlx_dyn_add(out, enc))
          err = PARAM_NO_MEM;
        curl_free(enc);
        if(err)
          break;
      }
    }
    else if(FUNCMATCH(f, FUNC_B64, FUNC_B64_LEN)) {
      f += FUNC_B64_LEN;
      curlx_dyn_reset(out);
      if(clen) {
        char *enc;
        size_t elen;
        CURLcode result = curlx_base64_encode((uint8_t *)c, clen, &enc, &elen);
        if(result) {
          err = PARAM_NO_MEM;
          break;
        }

        /* put it in the output */
        if(curlx_dyn_addn(out, enc, elen))
          err = PARAM_NO_MEM;
        curl_free(enc);
        if(err)
          break;
      }
    }
    else if(FUNCMATCH(f, FUNC_64DEC, FUNC_64DEC_LEN)) {
      f += FUNC_64DEC_LEN;
      curlx_dyn_reset(out);
      if(clen) {
        unsigned char *enc;
        size_t elen;
        CURLcode result = curlx_base64_decode(c, &enc, &elen);
        /* put it in the output */
        if(result) {
          if(curlx_dyn_add(out, "[64dec-fail]"))
            err = PARAM_NO_MEM;
        }
        else {
          if(curlx_dyn_addn(out, enc, elen))
            err = PARAM_NO_MEM;
          curl_free(enc);
        }
        if(err)
          break;
      }
    }
    else {
      /* unsupported function */
      errorf("unknown variable function in '%.*s'", (int)flen, finput);
      err = PARAM_EXPAND_ERROR;
      break;
    }
    if(alloc)
      curlx_free(c);

    clen = curlx_dyn_len(out);
    c = curlx_memdup0(curlx_dyn_ptr(out), clen);
    if(!c) {
      err = PARAM_NO_MEM;
      break;
    }
    alloc = TRUE;
  }
  if(alloc)
    curlx_free(c);
  if(err)
    curlx_dyn_free(out);
  return err;
}

ParameterError varexpand(const char *line, struct dynbuf *out, bool *replaced)
{
  CURLcode result;
  const char *envp;
  bool added = FALSE;
  const char *input = line;
  *replaced = FALSE;
  curlx_dyn_init(out, MAX_EXPAND_CONTENT);
  do {
    envp = strstr(line, "{{");
    if((envp > line) && envp[-1] == '\\') {
      /* preceding backslash, we want this verbatim */

      /* insert the text up to this point, minus the backslash */
      result = curlx_dyn_addn(out, line, envp - line - 1);
      if(result)
        return PARAM_NO_MEM;

      /* output '{{' then continue from here */
      result = curlx_dyn_addn(out, "{{", 2);
      if(result)
        return PARAM_NO_MEM;
      line = &envp[2];
    }
    else if(envp) {
      char name[MAX_VAR_LEN];
      size_t nlen;
      size_t i;
      const char *funcp;
      const char *clp = strstr(envp, "}}");
      size_t prefix;

      if(!clp) {
        /* uneven braces */
        warnf("missing close '}}' in '%s'", input);
        break;
      }

      prefix = 2;
      envp += 2; /* move over the {{ */

      /* if there is a function, it ends the name with a colon */
      funcp = memchr(envp, ':', clp - envp);
      if(funcp)
        nlen = funcp - envp;
      else
        nlen = clp - envp;
      if(!nlen || (nlen >= sizeof(name))) {
        warnf("bad variable name length '%s'", input);
        /* insert the text as-is since this is not an env variable */
        result = curlx_dyn_addn(out, line, clp - line + prefix);
        if(result)
          return PARAM_NO_MEM;
      }
      else {
        /* insert the text up to this point */
        result = curlx_dyn_addn(out, line, envp - prefix - line);
        if(result)
          return PARAM_NO_MEM;

        /* copy the name to separate buffer */
        memcpy(name, envp, nlen);
        name[nlen] = 0;

        /* verify that the name looks sensible */
        for(i = 0; (i < nlen) && (ISALNUM(name[i]) || (name[i] == '_')); i++)
          ;
        if(i != nlen) {
          warnf("bad variable name: %s", name);
          /* insert the text as-is since this is not an env variable */
          result = curlx_dyn_addn(out, envp - prefix, clp - envp + prefix + 2);
          if(result)
            return PARAM_NO_MEM;
        }
        else {
          char *value;
          size_t vlen = 0;
          struct dynbuf buf;
          const struct tool_var *v = varcontent(name, nlen);
          if(v) {
            value = (char *)CURL_UNCONST(v->content);
            vlen = v->clen;
          }
          else
            value = NULL;

          curlx_dyn_init(&buf, MAX_EXPAND_CONTENT);
          if(funcp) {
            /* apply the list of functions on the value */
            size_t flen = clp - funcp;
            ParameterError err = varfunc(value, vlen, funcp, flen, &buf);
            if(err)
              return err;
            value = curlx_dyn_ptr(&buf);
            vlen = curlx_dyn_len(&buf);
          }

          if(value && vlen > 0) {
            /* A variable might contain null bytes. Such bytes cannot be shown
               using normal means, this is an error. */
            const char *nb = memchr(value, '\0', vlen);
            if(nb) {
              errorf("variable contains null byte");
              return PARAM_EXPAND_ERROR;
            }
          }
          /* insert the value */
          result = curlx_dyn_addn(out, value, vlen);
          curlx_dyn_free(&buf);
          if(result)
            return PARAM_NO_MEM;

          added = true;
        }
      }
      line = &clp[2];
    }

  } while(envp);
  if(added && *line) {
    /* add the "suffix" as well */
    result = curlx_dyn_add(out, line);
    if(result)
      return PARAM_NO_MEM;
  }
  *replaced = added;
  if(!added)
    curlx_dyn_free(out);
  return PARAM_OK;
}

/*
 * Created in a way that is not revealing how variables are actually stored so
 * that we can improve this if we want better performance when managing many
 * at a later point.
 */
static ParameterError addvariable(const char *name,
                                  size_t nlen,
                                  const char *content,
                                  size_t clen,
                                  bool contalloc)
{
  struct tool_var *p;
  const struct tool_var *check = varcontent(name, nlen);
  DEBUGASSERT(nlen);
  if(check)
    notef("Overwriting variable '%s'", check->name);

  p = curlx_calloc(1, sizeof(struct tool_var) + nlen);
  if(p) {
    memcpy(p->name, name, nlen);
    /* the null termination byte is already present from above */

    p->content = contalloc ? content : curlx_memdup0(content, clen);
    if(p->content) {
      p->clen = clen;

      p->next = global->variables;
      global->variables = p;
      return PARAM_OK;
    }
    curlx_free(p);
  }
  return PARAM_NO_MEM;
}

ParameterError setvariable(const char *input)
{
  const char *name;
  size_t nlen;
  char *content = NULL;
  size_t clen = 0;
  bool contalloc = FALSE;
  const char *line = input;
  ParameterError err = PARAM_OK;
  bool import = FALSE;
  char *ge = NULL;
  char buf[MAX_VAR_LEN];
  curl_off_t startoffset = 0;
  curl_off_t endoffset = CURL_OFF_T_MAX;

  if(*input == '%') {
    import = TRUE;
    line++;
  }
  name = line;
  while(*line && (ISALNUM(*line) || (*line == '_')))
    line++;
  nlen = line - name;
  if(!nlen || (nlen >= MAX_VAR_LEN)) {
    warnf("Bad variable name length (%zd), skipping", nlen);
    return PARAM_OK;
  }
  if(import) {
    /* this does not use curl_getenv() because we want "" support for blank
       content */
    if(*line) {
      /* if there is a default action, we need to copy the name */
      memcpy(buf, name, nlen);
      buf[nlen] = 0;
      name = buf;
    }
    ge = getenv(name);
    if(!*line && !ge) {
      /* no assign, no variable, fail */
      errorf("Variable '%s' import fail, not set", name);
      return PARAM_EXPAND_ERROR;
    }
    else if(ge) {
      /* there is a value to use */
      content = ge;
      clen = strlen(ge);
    }
  }
  if(*line == '[' && ISDIGIT(line[1])) {
    /* is there a byte range specified? [num-num] */
    line++;
    if(curlx_str_number(&line, &startoffset, CURL_OFF_T_MAX) ||
       curlx_str_single(&line, '-'))
      return PARAM_VAR_SYNTAX;
    if(curlx_str_single(&line, ']')) {
      if(curlx_str_number(&line, &endoffset, CURL_OFF_T_MAX) ||
         curlx_str_single(&line, ']'))
        return PARAM_VAR_SYNTAX;
    }
    if(startoffset > endoffset)
      return PARAM_VAR_SYNTAX;
  }
  if(content)
    ;
  else if(*line == '@') {
    /* read from file or stdin */
    FILE *file;
    bool use_stdin;
    line++;

    use_stdin = !strcmp(line, "-");
    if(use_stdin)
      file = stdin;
    else {
      file = curlx_fopen(line, "rb");
      if(!file) {
        char errbuf[STRERROR_LEN];
        errorf("Failed to open %s: %s", line,
               curlx_strerror(errno, errbuf, sizeof(errbuf)));
        err = PARAM_READ_ERROR;
      }
    }
    if(!err) {
      err = file2memory_range(&content, &clen, file, startoffset, endoffset);
      /* in case of out of memory, this should fail the entire operation */
      if(clen)
        contalloc = TRUE;
    }
    if(!use_stdin && file)
      curlx_fclose(file);
    if(err)
      return err;
  }
  else if(*line == '=') {
    line++;
    clen = strlen(line);
    /* this is the exact content */
    content = (char *)CURL_UNCONST(line);
    if(startoffset || (endoffset != CURL_OFF_T_MAX)) {
      if(startoffset >= (curl_off_t)clen)
        clen = 0;
      else {
        /* make the end offset no larger than the last byte */
        if(endoffset >= (curl_off_t)clen)
          endoffset = clen - 1;
        clen = (size_t)(endoffset - startoffset) + 1;
        content += startoffset;
      }
    }
  }
  else {
    warnf("Bad --variable syntax, skipping: %s", input);
    return PARAM_OK;
  }
  err = addvariable(name, nlen, content, clen, contalloc);
  if(err) {
    if(contalloc)
      curlx_free(content);
  }
  return err;
}
