branch: master
tool_writeout.c
25842 bytesRaw
/***************************************************************************
 *                                  _   _ ____  _
 *  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_writeout.h"
#include "tool_writeout_json.h"

struct httpmap {
  const char *str;
  int num;
};

static const struct httpmap http_version[] = {
  { "0",   CURL_HTTP_VERSION_NONE },
  { "1",   CURL_HTTP_VERSION_1_0 },
  { "1.1", CURL_HTTP_VERSION_1_1 },
  { "2",   CURL_HTTP_VERSION_2 },
  { "3",   CURL_HTTP_VERSION_3 },
  { NULL, 0 } /* end of list */
};

static int writeTime(FILE *stream, const struct writeoutvar *wovar,
                     struct per_transfer *per, CURLcode per_result,
                     bool use_json)
{
  bool valid = false;
  curl_off_t us = 0;

  (void)per;
  (void)per_result;
  DEBUGASSERT(wovar->writefunc == writeTime);

  if(wovar->ci) {
    if(!curl_easy_getinfo(per->curl, wovar->ci, &us))
      valid = true;
  }
  else {
    DEBUGASSERT(0);
  }

  if(valid) {
    curl_off_t secs = us / 1000000;
    us %= 1000000;

    if(use_json)
      curl_mfprintf(stream, "\"%s\":", wovar->name);

    curl_mfprintf(stream, "%" CURL_FORMAT_CURL_OFF_TU
                  ".%06" CURL_FORMAT_CURL_OFF_TU, secs, us);
  }
  else {
    if(use_json)
      curl_mfprintf(stream, "\"%s\":null", wovar->name);
  }

  return 1; /* return 1 if anything was written */
}

static int urlpart(struct per_transfer *per, writeoutid vid,
                   const char **contentp)
{
  CURLU *uh = curl_url();
  int rc = 0;
  if(uh) {
    CURLUPart cpart = CURLUPART_HOST;
    char *part = NULL;
    char *url = NULL;

    if(vid >= VAR_INPUT_URLESCHEME) {
      if(curl_easy_getinfo(per->curl, CURLINFO_EFFECTIVE_URL, &url))
        rc = 5;
    }
    else
      url = per->url;

    if(!rc) {
      switch(vid) {
      case VAR_INPUT_URLSCHEME:
      case VAR_INPUT_URLESCHEME:
        cpart = CURLUPART_SCHEME;
        break;
      case VAR_INPUT_URLUSER:
      case VAR_INPUT_URLEUSER:
        cpart = CURLUPART_USER;
        break;
      case VAR_INPUT_URLPASSWORD:
      case VAR_INPUT_URLEPASSWORD:
        cpart = CURLUPART_PASSWORD;
        break;
      case VAR_INPUT_URLOPTIONS:
      case VAR_INPUT_URLEOPTIONS:
        cpart = CURLUPART_OPTIONS;
        break;
      case VAR_INPUT_URLHOST:
      case VAR_INPUT_URLEHOST:
        cpart = CURLUPART_HOST;
        break;
      case VAR_INPUT_URLPORT:
      case VAR_INPUT_URLEPORT:
        cpart = CURLUPART_PORT;
        break;
      case VAR_INPUT_URLPATH:
      case VAR_INPUT_URLEPATH:
        cpart = CURLUPART_PATH;
        break;
      case VAR_INPUT_URLQUERY:
      case VAR_INPUT_URLEQUERY:
        cpart = CURLUPART_QUERY;
        break;
      case VAR_INPUT_URLFRAGMENT:
      case VAR_INPUT_URLEFRAGMENT:
        cpart = CURLUPART_FRAGMENT;
        break;
      case VAR_INPUT_URLZONEID:
      case VAR_INPUT_URLEZONEID:
        cpart = CURLUPART_ZONEID;
        break;
      default:
        /* not implemented */
        rc = 4;
        break;
      }
    }
    if(!rc && curl_url_set(uh, CURLUPART_URL, url,
                           CURLU_GUESS_SCHEME | CURLU_NON_SUPPORT_SCHEME))
      rc = 2;

    if(!rc && curl_url_get(uh, cpart, &part, CURLU_DEFAULT_PORT))
      rc = 3;

    if(!rc && part)
      *contentp = part;
    curl_url_cleanup(uh);
  }
  else
    return 1;
  return rc;
}

static void certinfo(struct per_transfer *per)
{
  if(!per->certinfo) {
    const struct curl_certinfo *certinfo;
    CURLcode result = curl_easy_getinfo(per->curl, CURLINFO_CERTINFO,
                                        &certinfo);
    per->certinfo = (!result && certinfo) ? certinfo : NULL;
  }
}

static int writeString(FILE *stream, const struct writeoutvar *wovar,
                       struct per_transfer *per, CURLcode per_result,
                       bool use_json)
{
  bool valid = false;
  const char *strinfo = NULL;
  const char *freestr = NULL;
  struct dynbuf buf;
  curlx_dyn_init(&buf, 256 * 1024);

  DEBUGASSERT(wovar->writefunc == writeString);

  if(wovar->ci) {
    if(wovar->ci == CURLINFO_HTTP_VERSION) {
      long version = 0;
      if(!curl_easy_getinfo(per->curl, CURLINFO_HTTP_VERSION, &version)) {
        const struct httpmap *m = &http_version[0];
        while(m->str) {
          if(m->num == version) {
            strinfo = m->str;
            valid = true;
            break;
          }
          m++;
        }
      }
    }
    else {
      if(!curl_easy_getinfo(per->curl, wovar->ci, &strinfo) && strinfo)
        valid = true;
    }
  }
  else {
    switch(wovar->id) {
    case VAR_CERT:
      certinfo(per);
      if(per->certinfo) {
        int i;
        bool error = FALSE;
        for(i = 0; (i < per->certinfo->num_of_certs) && !error; i++) {
          struct curl_slist *slist;

          for(slist = per->certinfo->certinfo[i]; slist; slist = slist->next) {
            size_t len;
            if(curl_strnequal(slist->data, "cert:", 5)) {
              if(curlx_dyn_add(&buf, &slist->data[5])) {
                error = TRUE;
                break;
              }
            }
            else {
              if(curlx_dyn_add(&buf, slist->data)) {
                error = TRUE;
                break;
              }
            }
            len = curlx_dyn_len(&buf);
            if(len) {
              char *ptr = curlx_dyn_ptr(&buf);
              if(ptr[len - 1] != '\n') {
                /* add a newline to make things look better */
                if(curlx_dyn_addn(&buf, "\n", 1)) {
                  error = TRUE;
                  break;
                }
              }
            }
          }
        }
        if(!error) {
          strinfo = curlx_dyn_ptr(&buf);
          if(!strinfo)
            /* maybe not a TLS protocol */
            strinfo = "";
          valid = true;
        }
      }
      else
        strinfo = ""; /* no cert info */
      break;
    case VAR_ERRORMSG:
      if(per_result) {
        strinfo = (per->errorbuffer[0]) ? per->errorbuffer :
          curl_easy_strerror(per_result);
        valid = true;
      }
      break;
    case VAR_EFFECTIVE_FILENAME:
      if(per->outs.filename) {
        strinfo = per->outs.filename;
        valid = true;
      }
      break;
    case VAR_INPUT_URL:
      if(per->url) {
        strinfo = per->url;
        valid = true;
      }
      break;
    case VAR_INPUT_URLSCHEME:
    case VAR_INPUT_URLUSER:
    case VAR_INPUT_URLPASSWORD:
    case VAR_INPUT_URLOPTIONS:
    case VAR_INPUT_URLHOST:
    case VAR_INPUT_URLPORT:
    case VAR_INPUT_URLPATH:
    case VAR_INPUT_URLQUERY:
    case VAR_INPUT_URLFRAGMENT:
    case VAR_INPUT_URLZONEID:
    case VAR_INPUT_URLESCHEME:
    case VAR_INPUT_URLEUSER:
    case VAR_INPUT_URLEPASSWORD:
    case VAR_INPUT_URLEOPTIONS:
    case VAR_INPUT_URLEHOST:
    case VAR_INPUT_URLEPORT:
    case VAR_INPUT_URLEPATH:
    case VAR_INPUT_URLEQUERY:
    case VAR_INPUT_URLEFRAGMENT:
    case VAR_INPUT_URLEZONEID:
      if(per->url) {
        if(!urlpart(per, wovar->id, &strinfo)) {
          freestr = strinfo;
          valid = true;
        }
      }
      break;
    default:
      DEBUGASSERT(0);
      break;
    }
  }

  DEBUGASSERT(!valid || strinfo);
  if(valid && strinfo) {
    if(use_json) {
      curl_mfprintf(stream, "\"%s\":", wovar->name);
      jsonWriteString(stream, strinfo, FALSE);
    }
    else
      fputs(strinfo, stream);
  }
  else {
    if(use_json)
      curl_mfprintf(stream, "\"%s\":null", wovar->name);
  }
  curl_free((char *)CURL_UNCONST(freestr));

  curlx_dyn_free(&buf);
  return 1; /* return 1 if anything was written */
}

static int writeLong(FILE *stream, const struct writeoutvar *wovar,
                     struct per_transfer *per, CURLcode per_result,
                     bool use_json)
{
  bool valid = false;
  long longinfo = 0;

  DEBUGASSERT(wovar->writefunc == writeLong);

  if(wovar->ci) {
    if(!curl_easy_getinfo(per->curl, wovar->ci, &longinfo))
      valid = true;
  }
  else {
    switch(wovar->id) {
    case VAR_NUM_RETRY:
      longinfo = per->num_retries;
      valid = true;
      break;
    case VAR_NUM_CERTS:
      certinfo(per);
      longinfo = per->certinfo ? per->certinfo->num_of_certs : 0;
      valid = true;
      break;
    case VAR_NUM_HEADERS:
      longinfo = per->num_headers;
      valid = true;
      break;
    case VAR_EXITCODE:
      longinfo = (long)per_result;
      valid = true;
      break;
    default:
      DEBUGASSERT(0);
      break;
    }
  }

  if(valid) {
    if(use_json)
      curl_mfprintf(stream, "\"%s\":%ld", wovar->name, longinfo);
    else {
      if(wovar->id == VAR_HTTP_CODE || wovar->id == VAR_HTTP_CODE_PROXY)
        curl_mfprintf(stream, "%03ld", longinfo);
      else
        curl_mfprintf(stream, "%ld", longinfo);
    }
  }
  else {
    if(use_json)
      curl_mfprintf(stream, "\"%s\":null", wovar->name);
  }

  return 1; /* return 1 if anything was written */
}

static int writeOffset(FILE *stream, const struct writeoutvar *wovar,
                       struct per_transfer *per, CURLcode per_result,
                       bool use_json)
{
  bool valid = false;
  curl_off_t offinfo = 0;

  (void)per;
  (void)per_result;
  DEBUGASSERT(wovar->writefunc == writeOffset);

  if(wovar->ci) {
    if(!curl_easy_getinfo(per->curl, wovar->ci, &offinfo))
      valid = true;
  }
  else {
    switch(wovar->id) {
    case VAR_URLNUM:
      if(per->urlnum <= INT_MAX) {
        offinfo = per->urlnum;
        valid = true;
      }
      break;
    default:
      DEBUGASSERT(0);
    }
  }

  if(valid) {
    if(use_json)
      curl_mfprintf(stream, "\"%s\":", wovar->name);

    curl_mfprintf(stream, "%" CURL_FORMAT_CURL_OFF_T, offinfo);
  }
  else {
    if(use_json)
      curl_mfprintf(stream, "\"%s\":null", wovar->name);
  }

  return 1; /* return 1 if anything was written */
}

/* The designated write function should be the same as the CURLINFO return type
   with exceptions special cased in the respective function. For example,
   http_version uses CURLINFO_HTTP_VERSION which returns the version as a long,
   however it is output as a string and therefore is handled in writeString.

   Yes: "http_version": "1.1"
   No:  "http_version": 1.1

   Variable names MUST be in alphabetical order.
   */
static const struct writeoutvar variables[] = {
  { "certs", VAR_CERT, CURLINFO_NONE, writeString },
  { "conn_id", VAR_CONN_ID, CURLINFO_CONN_ID, writeOffset },
  { "content_type", VAR_CONTENT_TYPE, CURLINFO_CONTENT_TYPE, writeString },
  { "errormsg", VAR_ERRORMSG, CURLINFO_NONE, writeString },
  { "exitcode", VAR_EXITCODE, CURLINFO_NONE, writeLong },
  { "filename_effective", VAR_EFFECTIVE_FILENAME, CURLINFO_NONE, writeString },
  { "ftp_entry_path", VAR_FTP_ENTRY_PATH, CURLINFO_FTP_ENTRY_PATH,
    writeString },
  { "header_json", VAR_HEADER_JSON, CURLINFO_NONE, NULL },
  { "http_code", VAR_HTTP_CODE, CURLINFO_RESPONSE_CODE, writeLong },
  { "http_connect", VAR_HTTP_CODE_PROXY, CURLINFO_HTTP_CONNECTCODE,
    writeLong },
  { "http_version", VAR_HTTP_VERSION, CURLINFO_HTTP_VERSION, writeString },
  { "json", VAR_JSON, CURLINFO_NONE, NULL },
  { "local_ip", VAR_LOCAL_IP, CURLINFO_LOCAL_IP, writeString },
  { "local_port", VAR_LOCAL_PORT, CURLINFO_LOCAL_PORT, writeLong },
  { "method", VAR_EFFECTIVE_METHOD, CURLINFO_EFFECTIVE_METHOD, writeString },
  { "num_certs", VAR_NUM_CERTS, CURLINFO_NONE, writeLong },
  { "num_connects", VAR_NUM_CONNECTS, CURLINFO_NUM_CONNECTS, writeLong },
  { "num_headers", VAR_NUM_HEADERS, CURLINFO_NONE, writeLong },
  { "num_redirects", VAR_REDIRECT_COUNT, CURLINFO_REDIRECT_COUNT, writeLong },
  { "num_retries", VAR_NUM_RETRY, CURLINFO_NONE, writeLong },
  { "onerror", VAR_ONERROR, CURLINFO_NONE, NULL },
  { "proxy_ssl_verify_result", VAR_PROXY_SSL_VERIFY_RESULT,
    CURLINFO_PROXY_SSL_VERIFYRESULT, writeLong },
  { "proxy_used", VAR_PROXY_USED, CURLINFO_USED_PROXY, writeLong },
  { "redirect_url", VAR_REDIRECT_URL, CURLINFO_REDIRECT_URL, writeString },
  { "referer", VAR_REFERER, CURLINFO_REFERER, writeString },
  { "remote_ip", VAR_PRIMARY_IP, CURLINFO_PRIMARY_IP, writeString },
  { "remote_port", VAR_PRIMARY_PORT, CURLINFO_PRIMARY_PORT, writeLong },
  { "response_code", VAR_HTTP_CODE, CURLINFO_RESPONSE_CODE, writeLong },
  { "scheme", VAR_SCHEME, CURLINFO_SCHEME, writeString },
  { "size_download", VAR_SIZE_DOWNLOAD, CURLINFO_SIZE_DOWNLOAD_T,
    writeOffset },
  { "size_header", VAR_HEADER_SIZE, CURLINFO_HEADER_SIZE, writeLong },
  { "size_request", VAR_REQUEST_SIZE, CURLINFO_REQUEST_SIZE, writeLong },
  { "size_upload", VAR_SIZE_UPLOAD, CURLINFO_SIZE_UPLOAD_T, writeOffset },
  { "speed_download", VAR_SPEED_DOWNLOAD, CURLINFO_SPEED_DOWNLOAD_T,
    writeOffset },
  { "speed_upload", VAR_SPEED_UPLOAD, CURLINFO_SPEED_UPLOAD_T, writeOffset },
  { "ssl_verify_result", VAR_SSL_VERIFY_RESULT, CURLINFO_SSL_VERIFYRESULT,
    writeLong },
  { "stderr", VAR_STDERR, CURLINFO_NONE, NULL },
  { "stdout", VAR_STDOUT, CURLINFO_NONE, NULL },
  { "time_appconnect", VAR_APPCONNECT_TIME, CURLINFO_APPCONNECT_TIME_T,
    writeTime },
  { "time_connect", VAR_CONNECT_TIME, CURLINFO_CONNECT_TIME_T, writeTime },
  { "time_namelookup", VAR_NAMELOOKUP_TIME, CURLINFO_NAMELOOKUP_TIME_T,
    writeTime },
  { "time_posttransfer", VAR_POSTTRANSFER_TIME, CURLINFO_POSTTRANSFER_TIME_T,
    writeTime },
  { "time_pretransfer", VAR_PRETRANSFER_TIME, CURLINFO_PRETRANSFER_TIME_T,
    writeTime },
  { "time_queue", VAR_QUEUE_TIME, CURLINFO_QUEUE_TIME_T, writeTime },
  { "time_redirect", VAR_REDIRECT_TIME, CURLINFO_REDIRECT_TIME_T, writeTime },
  { "time_starttransfer", VAR_STARTTRANSFER_TIME,
    CURLINFO_STARTTRANSFER_TIME_T, writeTime },
  { "time_total", VAR_TOTAL_TIME, CURLINFO_TOTAL_TIME_T, writeTime },
  { "tls_earlydata", VAR_TLS_EARLYDATA_SENT, CURLINFO_EARLYDATA_SENT_T,
    writeOffset },
  { "url", VAR_INPUT_URL, CURLINFO_NONE, writeString },
  { "url.fragment", VAR_INPUT_URLFRAGMENT, CURLINFO_NONE, writeString },
  { "url.host", VAR_INPUT_URLHOST, CURLINFO_NONE, writeString },
  { "url.options", VAR_INPUT_URLOPTIONS, CURLINFO_NONE, writeString },
  { "url.password", VAR_INPUT_URLPASSWORD, CURLINFO_NONE, writeString },
  { "url.path", VAR_INPUT_URLPATH, CURLINFO_NONE, writeString },
  { "url.port", VAR_INPUT_URLPORT, CURLINFO_NONE, writeString },
  { "url.query", VAR_INPUT_URLQUERY, CURLINFO_NONE, writeString },
  { "url.scheme", VAR_INPUT_URLSCHEME, CURLINFO_NONE, writeString },
  { "url.user", VAR_INPUT_URLUSER, CURLINFO_NONE, writeString },
  { "url.zoneid", VAR_INPUT_URLZONEID, CURLINFO_NONE, writeString },
  { "url_effective", VAR_EFFECTIVE_URL, CURLINFO_EFFECTIVE_URL, writeString },
  { "urle.fragment", VAR_INPUT_URLEFRAGMENT, CURLINFO_NONE, writeString },
  { "urle.host", VAR_INPUT_URLEHOST, CURLINFO_NONE, writeString },
  { "urle.options", VAR_INPUT_URLEOPTIONS, CURLINFO_NONE, writeString },
  { "urle.password", VAR_INPUT_URLEPASSWORD, CURLINFO_NONE, writeString },
  { "urle.path", VAR_INPUT_URLEPATH, CURLINFO_NONE, writeString },
  { "urle.port", VAR_INPUT_URLEPORT, CURLINFO_NONE, writeString },
  { "urle.query", VAR_INPUT_URLEQUERY, CURLINFO_NONE, writeString },
  { "urle.scheme", VAR_INPUT_URLESCHEME, CURLINFO_NONE, writeString },
  { "urle.user", VAR_INPUT_URLEUSER, CURLINFO_NONE, writeString },
  { "urle.zoneid", VAR_INPUT_URLEZONEID, CURLINFO_NONE, writeString },
  { "urlnum", VAR_URLNUM, CURLINFO_NONE, writeOffset },
  { "xfer_id", VAR_EASY_ID, CURLINFO_XFER_ID, writeOffset }
};

#define MAX_WRITEOUT_NAME_LENGTH 24

/* return the position after %time{} */
static const char *outtime(const char *ptr, /* %time{ ... */
                           FILE *stream)
{
  const char *end;
  ptr += 6;
  end = strchr(ptr, '}');
  if(end) {
    struct dynbuf format;
    char output[256]; /* max output time length */
#ifdef HAVE_GETTIMEOFDAY
    struct timeval cnow;
#else
    struct curltime cnow;
#endif
    time_t secs;
    unsigned int usecs;
    size_t i;
    size_t vlen;
    CURLcode result = CURLE_OK;

#ifdef HAVE_GETTIMEOFDAY
    gettimeofday(&cnow, NULL);
#else
    cnow.tv_sec = time(NULL);
    cnow.tv_usec = 0;
#endif
    secs = cnow.tv_sec;
    usecs = (unsigned int)cnow.tv_usec;
#ifdef DEBUGBUILD
    {
      const char *timestr = getenv("CURL_TIME");
      if(timestr) {
        curl_off_t val;
        curlx_str_number(&timestr, &val, TIME_T_MAX);
        secs = (time_t)val;
        usecs = (unsigned int)(val % 1000000);
      }
    }
#endif
    vlen = end - ptr;
    curlx_dyn_init(&format, 1024);

    /* insert sub-seconds for %f */
    /* insert +0000 for %z because it is otherwise not portable */
    /* insert UTC for %Z because it is otherwise not portable */
    for(i = 0; !result && i < vlen; i++) {
      if((i < vlen - 1) && ptr[i] == '%' &&
         ((ptr[i + 1] == 'f') || ((ptr[i + 1] | 0x20) == 'z'))) {
        if(ptr[i + 1] == 'f')
          result = curlx_dyn_addf(&format, "%06u", usecs);
        else if(ptr[i + 1] == 'Z')
          result = curlx_dyn_addn(&format, "UTC", 3);
        else
          result = curlx_dyn_addn(&format, "+0000", 5);
        i++;
      }
      else
        result = curlx_dyn_addn(&format, &ptr[i], 1);
    }
    if(!result) {
      struct tm utc;
      result = curlx_gmtime(secs, &utc);
#ifdef CURL_HAVE_DIAG /* includes llvm/clang, but not affected as of v22.1.0 */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
      if(curlx_dyn_len(&format) && !result &&
         strftime(output, sizeof(output), curlx_dyn_ptr(&format), &utc))
        fputs(output, stream);
#ifdef CURL_HAVE_DIAG
#pragma GCC diagnostic pop
#endif
      curlx_dyn_free(&format);
    }
    ptr = end + 1;
  }
  else
    fputs("%time{", stream);
  return ptr;
}

static void separator(const char *sep, size_t seplen, FILE *stream)
{
  while(seplen) {
    if(*sep == '\\') {
      switch(sep[1]) {
      case 'r':
        fputc('\r', stream);
        break;
      case 'n':
        fputc('\n', stream);
        break;
      case 't':
        fputc('\t', stream);
        break;
      case '}':
        fputc('}', stream);
        break;
      case '\0':
        break;
      default:
        /* unknown, output this */
        fputc(sep[0], stream);
        fputc(sep[1], stream);
        break;
      }
      sep += 2;
      seplen -= 2;
    }
    else {
      fputc(*sep, stream);
      sep++;
      seplen--;
    }
  }
}

static void output_header(struct per_transfer *per,
                          FILE *stream,
                          const char **pptr)
{
  const char *ptr = *pptr;
  const char *end;
  end = strchr(ptr, '}');
  do {
    if(!end || (end[-1] != '\\'))
      break;
    end = strchr(&end[1], '}');
  } while(end);
  if(end) {
    char hname[256]; /* holds the longest header field name */
    struct curl_header *header;
    const char *instr;
    const char *sep = NULL;
    size_t seplen = 0;
    size_t vlen = end - ptr;
    instr = memchr(ptr, ':', vlen);
    if(instr) {
      /* instructions follow */
      if(!strncmp(&instr[1], "all:", 4)) {
        sep = &instr[5];
        seplen = end - sep;
        vlen -= (seplen + 5);
      }
    }
    if(vlen < sizeof(hname)) {
      memcpy(hname, ptr, vlen);
      hname[vlen] = 0;
      if(sep) {
        /* get headers from all requests */
        int reqno = 0;
        size_t indno = 0;
        bool output = FALSE;
        do {
          if(CURLHE_OK == curl_easy_header(per->curl, hname, indno,
                                           CURLH_HEADER, reqno,
                                           &header)) {
            if(output)
              /* output separator */
              separator(sep, seplen, stream);
            fputs(header->value, stream);
            output = TRUE;
          }
          else
            break;
          if((header->index + 1) < header->amount)
            indno++;
          else {
            ++reqno;
            indno = 0;
          }
        } while(1);
      }
      else {
        if(CURLHE_OK == curl_easy_header(per->curl, hname, 0,
                                         CURLH_HEADER, -1, &header))
          fputs(header->value, stream);
      }
    }
    ptr = end + 1;
  }
  else
    fputs("%header{", stream);
  *pptr = ptr;
}

static int matchvar(const void *m1, const void *m2)
{
  const struct writeoutvar *v1 = m1;
  const struct writeoutvar *v2 = m2;

  return strcmp(v1->name, v2->name);
}

void ourWriteOut(struct OperationConfig *config, struct per_transfer *per,
                 CURLcode per_result)
{
  FILE *stream = stdout;
  const char *writeinfo = config->writeout;
  const char *ptr = writeinfo;
  bool done = FALSE;
  bool fclose_stream = FALSE;
  struct dynbuf name;

  if(!writeinfo)
    return;

  curlx_dyn_init(&name, MAX_WRITEOUT_NAME_LENGTH);
  while(ptr && *ptr && !done) {
    if('%' == *ptr && ptr[1]) {
      if('%' == ptr[1]) {
        /* an escaped %-letter */
        fputc('%', stream);
        ptr += 2;
      }
      else {
        /* this is meant as a variable to output */
        const char *end;
        size_t vlen;
        if('{' == ptr[1]) {
          const struct writeoutvar *wv = NULL;
          struct writeoutvar find = { 0 };
          end = strchr(ptr, '}');
          ptr += 2; /* pass the % and the { */
          if(!end) {
            fputs("%{", stream);
            continue;
          }
          vlen = end - ptr;

          curlx_dyn_reset(&name);
          if(!curlx_dyn_addn(&name, ptr, vlen)) {
            find.name = curlx_dyn_ptr(&name);
            wv = bsearch(&find,
                         variables, CURL_ARRAYSIZE(variables),
                         sizeof(variables[0]), matchvar);
          }
          else
            break;
          if(wv) {
            switch(wv->id) {
            case VAR_ONERROR:
              if(per_result == CURLE_OK)
                /* this is not error so skip the rest */
                done = TRUE;
              break;
            case VAR_STDOUT:
              if(fclose_stream)
                curlx_fclose(stream);
              fclose_stream = FALSE;
              stream = stdout;
              break;
            case VAR_STDERR:
              if(fclose_stream)
                curlx_fclose(stream);
              fclose_stream = FALSE;
              stream = tool_stderr;
              break;
            case VAR_JSON:
              ourWriteOutJSON(stream, variables,
                              CURL_ARRAYSIZE(variables),
                              per, per_result);
              break;
            case VAR_HEADER_JSON:
              headerJSON(stream, per);
              break;
            default:
              (void)wv->writefunc(stream, wv, per, per_result, false);
              break;
            }
          }
          else {
            curl_mfprintf(tool_stderr,
                          "curl: unknown --write-out variable: '%.*s'\n",
                          (int)vlen, ptr);
          }
          ptr = end + 1; /* pass the end */
        }
        else if(!strncmp("header{", &ptr[1], 7)) {
          ptr += 8;
          output_header(per, stream, &ptr);
        }
        else if(!strncmp("time{", &ptr[1], 5)) {
          ptr = outtime(ptr, stream);
        }
        else if(!strncmp("output{", &ptr[1], 7)) {
          bool append = FALSE;
          ptr += 8;
          if((ptr[0] == '>') && (ptr[1] == '>')) {
            append = TRUE;
            ptr += 2;
          }
          end = strchr(ptr, '}');
          if(end) {
            char fname[512]; /* holds the longest filename */
            size_t flen = end - ptr;
            if(flen < sizeof(fname)) {
              FILE *stream2;
              memcpy(fname, ptr, flen);
              fname[flen] = 0;
              stream2 = curlx_fopen(fname, append ? FOPEN_APPENDTEXT :
                                    FOPEN_WRITETEXT);
              if(stream2) {
                /* only change if the open worked */
                if(fclose_stream)
                  curlx_fclose(stream);
                stream = stream2;
                fclose_stream = TRUE;
              }
            }
            ptr = end + 1;
          }
          else
            fputs("%output{", stream);
        }
        else {
          /* illegal syntax, then output the characters that are used */
          fputc('%', stream);
          fputc(ptr[1], stream);
          ptr += 2;
        }
      }
    }
    else if('\\' == *ptr && ptr[1]) {
      switch(ptr[1]) {
      case 'r':
        fputc('\r', stream);
        break;
      case 'n':
        fputc('\n', stream);
        break;
      case 't':
        fputc('\t', stream);
        break;
      default:
        /* unknown, output this */
        fputc(*ptr, stream);
        fputc(ptr[1], stream);
        break;
      }
      ptr += 2;
    }
    else {
      fputc(*ptr, stream);
      ptr++;
    }
  }
  if(fclose_stream)
    curlx_fclose(stream);
  curlx_dyn_free(&name);
}