/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) 1998 - 2018, 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.haxx.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.
 *
 ***************************************************************************/
#include "tool_setup.h"

#include "mime.h"
#include "strcase.h"

#define ENABLE_CURLX_PRINTF
/* use our own printf() functions */
#include "curlx.h"

#include "tool_cfgable.h"
#include "tool_convert.h"
#include "tool_msgs.h"
#include "tool_binmode.h"
#include "tool_getparam.h"
#include "tool_paramhlp.h"
#include "tool_formparse.h"

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

/* Stdin parameters. */
typedef struct {
  char *data;  /* Memory data. */
  curl_off_t origin;  /* File read origin offset. */
  curl_off_t size; /* Data size. */
  curl_off_t curpos; /* Current read position. */
}  standard_input;


/*
 * helper function to get a word from form param
 * after call get_parm_word, str either point to string end
 * or point to any of end chars.
 */
static char *get_param_word(char **str, char **end_pos, char endchar)
{
  char *ptr = *str;
  char *word_begin = NULL;
  char *ptr2;
  char *escape = NULL;

  /* the first non-space char is here */
  word_begin = ptr;
  if(*ptr == '"') {
    ++ptr;
    while(*ptr) {
      if(*ptr == '\\') {
        if(ptr[1] == '\\' || ptr[1] == '"') {
          /* remember the first escape position */
          if(!escape)
            escape = ptr;
          /* skip escape of back-slash or double-quote */
          ptr += 2;
          continue;
        }
      }
      if(*ptr == '"') {
        *end_pos = ptr;
        if(escape) {
          /* has escape, we restore the unescaped string here */
          ptr = ptr2 = escape;
          do {
            if(*ptr == '\\' && (ptr[1] == '\\' || ptr[1] == '"'))
              ++ptr;
            *ptr2++ = *ptr++;
          }
          while(ptr < *end_pos);
          *end_pos = ptr2;
        }
        while(*ptr && *ptr != ';' && *ptr != endchar)
          ++ptr;
        *str = ptr;
        return word_begin + 1;
      }
      ++ptr;
    }
    /* end quote is missing, treat it as non-quoted. */
    ptr = word_begin;
  }

  while(*ptr && *ptr != ';' && *ptr != endchar)
    ++ptr;
  *str = *end_pos = ptr;
  return word_begin;
}

/* Append slist item and return -1 if failed. */
static int slist_append(struct curl_slist **plist, const char *data)
{
  struct curl_slist *s = curl_slist_append(*plist, data);

  if(!s)
    return -1;

  *plist = s;
  return 0;
}

/* Read headers from a file and append to list. */
static int read_field_headers(struct OperationConfig *config,
                              const char *filename, FILE *fp,
                              struct curl_slist **pheaders)
{
  size_t hdrlen = 0;
  size_t pos = 0;
  bool incomment = FALSE;
  int lineno = 1;
  char hdrbuf[999]; /* Max. header length + 1. */

  for(;;) {
    int c = getc(fp);
    if(c == EOF || (!pos && !ISSPACE(c))) {
      /* Strip and flush the current header. */
      while(hdrlen && ISSPACE(hdrbuf[hdrlen - 1]))
        hdrlen--;
      if(hdrlen) {
        hdrbuf[hdrlen] = '\0';
        if(slist_append(pheaders, hdrbuf)) {
          fprintf(config->global->errors,
                  "Out of memory for field headers!\n");
          return -1;
        }
        hdrlen = 0;
      }
    }

    switch(c) {
    case EOF:
      if(ferror(fp)) {
        fprintf(config->global->errors,
                "Header file %s read error: %s\n", filename, strerror(errno));
        return -1;
      }
      return 0;    /* Done. */
    case '\r':
      continue;    /* Ignore. */
    case '\n':
      pos = 0;
      incomment = FALSE;
      lineno++;
      continue;
    case '#':
      if(!pos)
        incomment = TRUE;
      break;
    }

    pos++;
    if(!incomment) {
      if(hdrlen == sizeof(hdrbuf) - 1) {
        warnf(config->global, "File %s line %d: header too long (truncated)\n",
              filename, lineno);
        c = ' ';
      }
      if(hdrlen <= sizeof(hdrbuf) - 1)
        hdrbuf[hdrlen++] = (char) c;
    }
  }
  /* NOTREACHED */
}

static int get_param_part(struct OperationConfig *config, char endchar,
                          char **str, char **pdata, char **ptype,
                          char **pfilename, char **pencoder,
                          struct curl_slist **pheaders)
{
  char *p = *str;
  char *type = NULL;
  char *filename = NULL;
  char *encoder = NULL;
  char *endpos;
  char *tp;
  char sep;
  char type_major[128] = "";
  char type_minor[128] = "";
  char *endct = NULL;
  struct curl_slist *headers = NULL;

  if(ptype)
    *ptype = NULL;
  if(pfilename)
    *pfilename = NULL;
  if(pheaders)
    *pheaders = NULL;
  if(pencoder)
    *pencoder = NULL;
  while(ISSPACE(*p))
    p++;
  tp = p;
  *pdata = get_param_word(&p, &endpos, endchar);
  /* If not quoted, strip trailing spaces. */
  if(*pdata == tp)
    while(endpos > *pdata && ISSPACE(endpos[-1]))
      endpos--;
  sep = *p;
  *endpos = '\0';
  while(sep == ';') {
    while(ISSPACE(*++p))
      ;

    if(!endct && checkprefix("type=", p)) {
      for(p += 5; ISSPACE(*p); p++)
        ;
      /* set type pointer */
      type = p;

      /* verify that this is a fine type specifier */
      if(2 != sscanf(type, "%127[^/ ]/%127[^;, \n]", type_major, type_minor)) {
        warnf(config->global, "Illegally formatted content-type field!\n");
        curl_slist_free_all(headers);
        return -1; /* illegal content-type syntax! */
      }

      /* now point beyond the content-type specifier */
      p = type + strlen(type_major) + strlen(type_minor) + 1;
      for(endct = p; *p && *p != ';' && *p != endchar; p++)
        if(!ISSPACE(*p))
          endct = p + 1;
      sep = *p;
    }
    else if(checkprefix("filename=", p)) {
      if(endct) {
        *endct = '\0';
        endct = NULL;
      }
      for(p += 9; ISSPACE(*p); p++)
        ;
      tp = p;
      filename = get_param_word(&p, &endpos, endchar);
      /* If not quoted, strip trailing spaces. */
      if(filename == tp)
        while(endpos > filename && ISSPACE(endpos[-1]))
          endpos--;
      sep = *p;
      *endpos = '\0';
    }
    else if(checkprefix("headers=", p)) {
      if(endct) {
        *endct = '\0';
        endct = NULL;
      }
      p += 8;
      if(*p == '@' || *p == '<') {
        char *hdrfile;
        FILE *fp;
        /* Read headers from a file. */

        do {
          p++;
        } while(ISSPACE(*p));
        tp = p;
        hdrfile = get_param_word(&p, &endpos, endchar);
        /* If not quoted, strip trailing spaces. */
        if(hdrfile == tp)
          while(endpos > hdrfile && ISSPACE(endpos[-1]))
            endpos--;
        sep = *p;
        *endpos = '\0';
        /* TODO: maybe special fopen for VMS? */
        fp = fopen(hdrfile, FOPEN_READTEXT);
        if(!fp)
          warnf(config->global, "Cannot read from %s: %s\n", hdrfile,
                strerror(errno));
        else {
          int i = read_field_headers(config, hdrfile, fp, &headers);

          fclose(fp);
          if(i) {
            curl_slist_free_all(headers);
            return -1;
          }
        }
      }
      else {
        char *hdr;

        while(ISSPACE(*p))
          p++;
        tp = p;
        hdr = get_param_word(&p, &endpos, endchar);
        /* If not quoted, strip trailing spaces. */
        if(hdr == tp)
          while(endpos > hdr && ISSPACE(endpos[-1]))
            endpos--;
        sep = *p;
        *endpos = '\0';
        if(slist_append(&headers, hdr)) {
          fprintf(config->global->errors, "Out of memory for field header!\n");
          curl_slist_free_all(headers);
          return -1;
        }
      }
    }
    else if(checkprefix("encoder=", p)) {
      if(endct) {
        *endct = '\0';
        endct = NULL;
      }
      for(p += 8; ISSPACE(*p); p++)
        ;
      tp = p;
      encoder = get_param_word(&p, &endpos, endchar);
      /* If not quoted, strip trailing spaces. */
      if(encoder == tp)
        while(endpos > encoder && ISSPACE(endpos[-1]))
          endpos--;
      sep = *p;
      *endpos = '\0';
    }
    else if(endct) {
      /* This is part of content type. */
      for(endct = p; *p && *p != ';' && *p != endchar; p++)
        if(!ISSPACE(*p))
          endct = p + 1;
      sep = *p;
    }
    else {
      /* unknown prefix, skip to next block */
      char *unknown = get_param_word(&p, &endpos, endchar);

      sep = *p;
      *endpos = '\0';
      if(*unknown)
        warnf(config->global, "skip unknown form field: %s\n", unknown);
    }
  }

  /* Terminate content type. */
  if(endct)
    *endct = '\0';

  if(ptype)
    *ptype = type;
  else if(type)
    warnf(config->global, "Field content type not allowed here: %s\n", type);

  if(pfilename)
    *pfilename = filename;
  else if(filename)
    warnf(config->global,
          "Field file name not allowed here: %s\n", filename);

  if(pencoder)
    *pencoder = encoder;
  else if(encoder)
    warnf(config->global,
          "Field encoder not allowed here: %s\n", encoder);

  if(pheaders)
    *pheaders = headers;
  else if(headers) {
    warnf(config->global,
          "Field headers not allowed here: %s\n", headers->data);
    curl_slist_free_all(headers);
  }

  *str = p;
  return sep & 0xFF;
}


/* Mime part callbacks for stdin. */
static size_t stdin_read(char *buffer, size_t size, size_t nitems, void *arg)
{
  standard_input *sip = (standard_input *) arg;
  curl_off_t bytesleft;
  (void) size;  /* Always 1: ignored. */

  if(sip->curpos >= sip->size)
    return 0;  /* At eof. */
  bytesleft = sip->size - sip->curpos;
  if((curl_off_t) nitems > bytesleft)
    nitems = (size_t) bytesleft;
  if(sip->data) {
    /* Return data from memory. */
    memcpy(buffer, sip->data + (size_t) sip->curpos, nitems);
  }
  else {
    /* Read from stdin. */
    nitems = fread(buffer, 1, nitems, stdin);
  }
  sip->curpos += nitems;
  return nitems;
}

static int stdin_seek(void *instream, curl_off_t offset, int whence)
{
  standard_input *sip = (standard_input *) instream;

  switch(whence) {
  case SEEK_CUR:
    offset += sip->curpos;
    break;
  case SEEK_END:
    offset += sip->size;
    break;
  }
  if(offset < 0)
    return CURL_SEEKFUNC_CANTSEEK;
  if(!sip->data) {
    if(fseek(stdin, (long) (offset + sip->origin), SEEK_SET))
      return CURL_SEEKFUNC_CANTSEEK;
  }
  sip->curpos = offset;
  return CURL_SEEKFUNC_OK;
}

static void stdin_free(void *ptr)
{
  standard_input *sip = (standard_input *) ptr;

  Curl_safefree(sip->data);
  free(sip);
}

/* Set a part's data from a file, taking care about the pseudo filename "-" as
 * a shortcut to read stdin: if so, use a callback to read OUR stdin (to
 * workaround Windows DLL file handle caveat).
 * If stdin is a regular file opened in binary mode, save current offset as
 * origin for rewind and do not buffer data. Else read to EOF and keep in
 * memory. In all cases, compute the stdin data size.
 */
static CURLcode file_or_stdin(curl_mimepart *part, const char *file)
{
  standard_input *sip = NULL;
  int fd = -1;
  CURLcode result = CURLE_OK;
  struct_stat sbuf;

  if(strcmp(file, "-"))
    return curl_mime_filedata(part, file);

  sip = (standard_input *) calloc(1, sizeof(*sip));
  if(!sip)
    return CURLE_OUT_OF_MEMORY;

  set_binmode(stdin);

  /* If stdin is a regular file, do not buffer data but read it when needed. */
  fd = fileno(stdin);
  sip->origin = ftell(stdin);
  if(fd >= 0 && sip->origin >= 0 && !fstat(fd, &sbuf) &&
#ifdef __VMS
     sbuf.st_fab_rfm != FAB$C_VAR && sbuf.st_fab_rfm != FAB$C_VFC &&
#endif
     S_ISREG(sbuf.st_mode)) {
    sip->size = sbuf.st_size - sip->origin;
    if(sip->size < 0)
      sip->size = 0;
  }
  else {  /* Not suitable for direct use, buffer stdin data. */
    size_t stdinsize = 0;

    sip->origin = 0;
    if(file2memory(&sip->data, &stdinsize, stdin) != PARAM_OK)
      result = CURLE_OUT_OF_MEMORY;
    else {
      if(!stdinsize)
        sip->data = NULL;  /* Has been freed if no data. */
      sip->size = stdinsize;
      if(ferror(stdin))
        result = CURLE_READ_ERROR;
    }
  }

  /* Set remote file name. */
  if(!result)
    result = curl_mime_filename(part, file);

  /* Set part's data from callback. */
  if(!result)
    result = curl_mime_data_cb(part, sip->size,
                               stdin_read, stdin_seek, stdin_free, sip);
  if(result)
    stdin_free(sip);
  return result;
}


/***************************************************************************
 *
 * formparse()
 *
 * Reads a 'name=value' parameter and builds the appropriate linked list.
 *
 * Specify files to upload with 'name=@filename', or 'name=@"filename"'
 * in case the filename contain ',' or ';'. Supports specified
 * given Content-Type of the files. Such as ';type=<content-type>'.
 *
 * If literal_value is set, any initial '@' or '<' in the value string
 * loses its special meaning, as does any embedded ';type='.
 *
 * You may specify more than one file for a single name (field). Specify
 * multiple files by writing it like:
 *
 * 'name=@filename,filename2,filename3'
 *
 * or use double-quotes quote the filename:
 *
 * 'name=@"filename","filename2","filename3"'
 *
 * If you want content-types specified for each too, write them like:
 *
 * 'name=@filename;type=image/gif,filename2,filename3'
 *
 * If you want custom headers added for a single part, write them in a separate
 * file and do like this:
 *
 * 'name=foo;headers=@headerfile' or why not
 * 'name=@filemame;headers=@headerfile'
 *
 * To upload a file, but to fake the file name that will be included in the
 * formpost, do like this:
 *
 * 'name=@filename;filename=/dev/null' or quote the faked filename like:
 * 'name=@filename;filename="play, play, and play.txt"'
 *
 * If filename/path contains ',' or ';', it must be quoted by double-quotes,
 * else curl will fail to figure out the correct filename. if the filename
 * tobe quoted contains '"' or '\', '"' and '\' must be escaped by backslash.
 *
 * This function uses curl_formadd to fulfill it's job. Is heavily based on
 * the old curl_formparse code.
 *
 ***************************************************************************/

int formparse(struct OperationConfig *config,
              const char *input,
              curl_mime **mimepost,
              curl_mime **mimecurrent,
              bool literal_value)
{
  /* input MUST be a string in the format 'name=contents' and we'll
     build a linked list with the info */
  char *name = NULL;
  char *contents = NULL;
  char *contp;
  char *data;
  char *type = NULL;
  char *filename = NULL;
  char *encoder = NULL;
  struct curl_slist *headers = NULL;
  curl_mimepart *part = NULL;
  CURLcode res;

  /* Allocate the main mime structure if needed. */
  if(!*mimepost) {
    *mimepost = curl_mime_init(config->easy);
    if(!*mimepost) {
      warnf(config->global, "curl_mime_init failed!\n");
      return 1;
    }
    *mimecurrent = *mimepost;
  }

  /* Make a copy we can overwrite. */
  contents = strdup(input);
  if(!contents) {
    fprintf(config->global->errors, "out of memory\n");
    return 2;
  }

  /* Scan for the end of the name. */
  contp = strchr(contents, '=');
  if(contp) {
    int sep = '\0';
    if(contp > contents)
      name = contents;
    *contp++ = '\0';

    if(*contp == '(' && !literal_value) {
      curl_mime *subparts;

      /* Starting a multipart. */
      sep = get_param_part(config, '\0',
                           &contp, &data, &type, NULL, NULL, &headers);
      if(sep < 0) {
        Curl_safefree(contents);
        return 3;
      }
      subparts = curl_mime_init(config->easy);
      if(!subparts) {
        warnf(config->global, "curl_mime_init failed!\n");
        curl_slist_free_all(headers);
        Curl_safefree(contents);
        return 4;
      }
      part = curl_mime_addpart(*mimecurrent);
      if(!part) {
        warnf(config->global, "curl_mime_addpart failed!\n");
        curl_mime_free(subparts);
        curl_slist_free_all(headers);
        Curl_safefree(contents);
        return 5;
      }
      if(curl_mime_subparts(part, subparts)) {
        warnf(config->global, "curl_mime_subparts failed!\n");
        curl_mime_free(subparts);
        curl_slist_free_all(headers);
        Curl_safefree(contents);
        return 6;
      }
      *mimecurrent = subparts;
      if(curl_mime_headers(part, headers, 1)) {
        warnf(config->global, "curl_mime_headers failed!\n");
        curl_slist_free_all(headers);
        Curl_safefree(contents);
        return 7;
      }
      if(curl_mime_type(part, type)) {
        warnf(config->global, "curl_mime_type failed!\n");
        Curl_safefree(contents);
        return 8;
      }
    }
    else if(!name && !strcmp(contp, ")") && !literal_value) {
      /* Ending a mutipart. */
      if(*mimecurrent == *mimepost) {
        warnf(config->global, "no multipart to terminate!\n");
        Curl_safefree(contents);
        return 9;
        }
      *mimecurrent = (*mimecurrent)->parent->parent;
    }
    else if('@' == contp[0] && !literal_value) {

      /* we use the @-letter to indicate file name(s) */

      curl_mime *subparts = NULL;

      do {
        /* since this was a file, it may have a content-type specifier
           at the end too, or a filename. Or both. */
        ++contp;
        sep = get_param_part(config, ',', &contp,
                             &data, &type, &filename, &encoder, &headers);
        if(sep < 0) {
          if(subparts != *mimecurrent)
            curl_mime_free(subparts);
          Curl_safefree(contents);
          return 10;
        }

        /* now contp point to comma or string end.
           If more files to come, make sure we have multiparts. */
        if(!subparts) {
          if(sep != ',')    /* If there is a single file. */
            subparts = *mimecurrent;
          else {
            subparts = curl_mime_init(config->easy);
            if(!subparts) {
              warnf(config->global, "curl_mime_init failed!\n");
              curl_slist_free_all(headers);
              Curl_safefree(contents);
              return 11;
            }
          }
        }

        /* Allocate a part for that file. */
        part = curl_mime_addpart(subparts);
        if(!part) {
          warnf(config->global, "curl_mime_addpart failed!\n");
          if(subparts != *mimecurrent)
            curl_mime_free(subparts);
          curl_slist_free_all(headers);
          Curl_safefree(contents);
          return 12;
        }

        /* Set part headers. */
        if(curl_mime_headers(part, headers, 1)) {
          warnf(config->global, "curl_mime_headers failed!\n");
          if(subparts != *mimecurrent)
            curl_mime_free(subparts);
          curl_slist_free_all(headers);
          Curl_safefree(contents);
          return 13;
        }

        /* Setup file in part. */
        res = file_or_stdin(part, data);
        if(res) {
          warnf(config->global, "setting file %s  failed!\n", data);
          if(res != CURLE_READ_ERROR) {
            if(subparts != *mimecurrent)
              curl_mime_free(subparts);
            Curl_safefree(contents);
            return 14;
          }
        }
        if(filename && curl_mime_filename(part, filename)) {
          warnf(config->global, "curl_mime_filename failed!\n");
          if(subparts != *mimecurrent)
            curl_mime_free(subparts);
          Curl_safefree(contents);
          return 15;
        }
        if(curl_mime_type(part, type)) {
          warnf(config->global, "curl_mime_type failed!\n");
          if(subparts != *mimecurrent)
            curl_mime_free(subparts);
          Curl_safefree(contents);
          return 16;
        }
        if(curl_mime_encoder(part, encoder)) {
          warnf(config->global, "curl_mime_encoder failed!\n");
          if(subparts != *mimecurrent)
            curl_mime_free(subparts);
          Curl_safefree(contents);
          return 17;
        }

        /* *contp could be '\0', so we just check with the delimiter */
      } while(sep); /* loop if there's another file name */

      /* now we add the multiple files section */
      if(subparts != *mimecurrent) {
        part = curl_mime_addpart(*mimecurrent);
        if(!part) {
          warnf(config->global, "curl_mime_addpart failed!\n");
          curl_mime_free(subparts);
          Curl_safefree(contents);
          return 18;
        }
        if(curl_mime_subparts(part, subparts)) {
          warnf(config->global, "curl_mime_subparts failed!\n");
          curl_mime_free(subparts);
          Curl_safefree(contents);
          return 19;
        }
      }
    }
    else {
        /* Allocate a mime part. */
        part = curl_mime_addpart(*mimecurrent);
        if(!part) {
          warnf(config->global, "curl_mime_addpart failed!\n");
          Curl_safefree(contents);
          return 20;
        }

      if(*contp == '<' && !literal_value) {
        ++contp;
        sep = get_param_part(config, '\0', &contp,
                             &data, &type, NULL, &encoder, &headers);
        if(sep < 0) {
          Curl_safefree(contents);
          return 21;
        }

        /* Set part headers. */
        if(curl_mime_headers(part, headers, 1)) {
          warnf(config->global, "curl_mime_headers failed!\n");
          curl_slist_free_all(headers);
          Curl_safefree(contents);
          return 22;
        }

        /* Setup file in part. */
        res = file_or_stdin(part, data);
        if(res) {
          warnf(config->global, "setting file %s failed!\n", data);
          if(res != CURLE_READ_ERROR) {
            Curl_safefree(contents);
            return 23;
          }
        }
      }
      else {
        if(literal_value)
          data = contp;
        else {
          sep = get_param_part(config, '\0', &contp,
                               &data, &type, &filename, &encoder, &headers);
          if(sep < 0) {
            Curl_safefree(contents);
            return 24;
          }
        }

        /* Set part headers. */
        if(curl_mime_headers(part, headers, 1)) {
          warnf(config->global, "curl_mime_headers failed!\n");
          curl_slist_free_all(headers);
          Curl_safefree(contents);
          return 25;
        }

#ifdef CURL_DOES_CONVERSIONS
        if(convert_to_network(data, strlen(data))) {
          warnf(config->global, "curl_formadd failed!\n");
          Curl_safefree(contents);
          return 26;
        }
#endif

        if(curl_mime_data(part, data, CURL_ZERO_TERMINATED)) {
          warnf(config->global, "curl_mime_data failed!\n");
          Curl_safefree(contents);
          return 27;
        }
      }

      if(curl_mime_filename(part, filename)) {
        warnf(config->global, "curl_mime_filename failed!\n");
        Curl_safefree(contents);
        return 28;
      }
      if(curl_mime_type(part, type)) {
        warnf(config->global, "curl_mime_type failed!\n");
        Curl_safefree(contents);
        return 29;
      }
      if(curl_mime_encoder(part, encoder)) {
        warnf(config->global, "curl_mime_encoder failed!\n");
        Curl_safefree(contents);
        return 30;
      }

      if(sep) {
        *contp = (char) sep;
        warnf(config->global,
              "garbage at end of field specification: %s\n", contp);
      }
    }

    /* Set part name. */
    if(name && curl_mime_name(part, name)) {
      warnf(config->global, "curl_mime_name failed!\n");
      Curl_safefree(contents);
      return 31;
    }
  }
  else {
    warnf(config->global, "Illegally formatted input field!\n");
    Curl_safefree(contents);
    return 32;
  }
  Curl_safefree(contents);
  return 0;
}
