/***************************************************************************
 *                                  _   _ ____  _
 *  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 "curlx.h"

#include "tool_cfgable.h"
#include "tool_getparam.h"
#include "tool_helpers.h"
#include "tool_findfile.h"
#include "tool_msgs.h"
#include "tool_parsecfg.h"
#include "dynbuf.h"
#include "curl_base64.h"
#include "tool_paramhlp.h"
#include "tool_writeout_json.h"
#include "var.h"

#include "memdebug.h" /* keep this as LAST include */

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

static char *Memdup(const char *data, size_t len)
{
  char *p = malloc(len + 1);
  if(!p)
    return NULL;
  if(len)
    memcpy(p, data, len);
  p[len] = 0;
  return p;
}

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

static const struct tool_var *varcontent(struct GlobalConfig *global,
                                         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)

static ParameterError varfunc(struct GlobalConfig *global,
                              char *c, /* content */
                              size_t clen, /* content length */
                              char *f, /* functions */
                              size_t flen, /* function string length */
                              struct curlx_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(*c && 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(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 {
      /* unsupported function */
      errorf(global, "unknown variable function in '%.*s'",
             (int)flen, finput);
      err = PARAM_EXPAND_ERROR;
      break;
    }
    if(alloc)
      free(c);

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

ParameterError varexpand(struct GlobalConfig *global,
                         const char *line, struct curlx_dynbuf *out,
                         bool *replaced)
{
  CURLcode result;
  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;
      char *funcp;
      char *clp = strstr(envp, "}}");
      size_t prefix;

      if(!clp) {
        /* uneven braces */
        warnf(global, "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(global, "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(global, "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 curlx_dynbuf buf;
          const struct tool_var *v = varcontent(global, name, nlen);
          if(v) {
            value = (char *)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(global, 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. */
            char *nb = memchr(value, '\0', vlen);
            if(nb) {
              errorf(global, "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(struct GlobalConfig *global,
                                  const char *name,
                                  size_t nlen,
                                  const char *content,
                                  size_t clen,
                                  bool contalloc)
{
  struct tool_var *p;
  const struct tool_var *check = varcontent(global, name, nlen);
  DEBUGASSERT(nlen);
  if(check)
    notef(global, "Overwriting variable '%s'", check->name);

  p = calloc(1, sizeof(struct tool_var) + nlen);
  if(p) {
    memcpy(p->name, name, nlen);

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

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

#define MAX_FILENAME 10000

ParameterError setvariable(struct GlobalConfig *global,
                           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(global, "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(global, "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 == '[') {
    /* is there a byte range specified? [num-num] */
    if(ISDIGIT(line[1])) {
      char *endp;
      if(curlx_strtoofft(&line[1], &endp, 10, &startoffset) || (*endp != '-'))
        return PARAM_VAR_SYNTAX;
      else {
        char *p = endp + 1; /* pass the '-' */
        if(*p != ']') {
          if(curlx_strtoofft(p, &endp, 10, &endoffset) || (*endp != ']'))
            return PARAM_VAR_SYNTAX;
          line = &endp[1];  /* pass the ']' */
        }
        else
          line = &p[1]; /* pass the ']' */
      }
      if(startoffset > endoffset)
        return PARAM_VAR_SYNTAX;
    }
  }
  if(content)
    ;
  else if(*line == '@') {
    /* read from file or stdin */
    FILE *file;
    bool use_stdin;
    struct dynbuf fname;
    line++;

    Curl_dyn_init(&fname, MAX_FILENAME);

    use_stdin = !strcmp(line, "-");
    if(use_stdin)
      file = stdin;
    else {
      file = fopen(line, "rb");
      if(!file) {
        errorf(global, "Failed to open %s: %s", line,
               strerror(errno));
        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;
    }
    Curl_dyn_free(&fname);
    if(!use_stdin && file)
      fclose(file);
    if(err)
      return err;
  }
  else if(*line == '=') {
    line++;
    clen = strlen(line);
    /* this is the exact content */
    content = (char *)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(global, "Bad --variable syntax, skipping: %s", input);
    return PARAM_OK;
  }
  err = addvariable(global, name, nlen, content, clen, contalloc);
  if(err) {
    if(contalloc)
      free(content);
  }
  return err;
}
