/* ========================================================================== */
/* Copyright (c) 2018-2021 Michael Baeuerle
 *
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, and/or sell copies of the Software, and to permit persons
 * to whom the Software is furnished to do so, provided that the above
 * copyright notice(s) and this permission notice appear in all copies of
 * the Software and that both the above copyright notice(s) and this
 * permission notice appear in supporting documentation.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
 * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
 * SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
 * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
 * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Except as contained in this notice, the name of a copyright holder
 * shall not be used in advertising or otherwise to promote the sale, use
 * or other dealings in this Software without prior written authorization
 * of the copyright holder.
 *
 * SPDX-License-Identifier: ICU
 */

/* C99 */
#include <ctype.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Local */
#include "package_name.h"


/* ========================================================================== */
/* Constants */

/*! Utility name suffix */
#define CL_USUFFIX  "-mhp"

/* Workaround for HP-UX: SIZE_MAX not defined by stdint.h */
#ifndef SIZE_MAX
#  define SIZE_MAX  (size_t) 0xFFFFFFFFUL
#endif  /* SIZE_MAX */


/* ========================================================================== */
/*! \brief Print error message
 *
 * \param[in] s  Error message
 */

static void print_error(const char *s)
{
   const char*  n = cl_hp_package_name();

   fprintf(stderr, "%s%s: %s\n", n, CL_USUFFIX, s);
   free((void *) n);

   return;
}


/* ========================================================================== */
/*! \brief Print program version information */

static void print_version(void)
{
   const char*  n = cl_hp_package_name();
   const char*  v = cl_hp_package_version();

   printf("%s%s %s\n", n, CL_USUFFIX, v);
   printf("%s\n", "Header parser for libcanlock"
                  " (to extract and unfold header field)."
   );
   free((void *) n);

   return;
}


/* ========================================================================== */
/*! \brief Print help */

static void print_help(void)
{
   const char*  n = cl_hp_package_name();

   printf("Usage: %s%s %s\n", n, CL_USUFFIX, "[options]\n"
          "Options:\n"
          "   -f field         Header field name to extract\n"
          "   -h               Print this help message to stdout and exit\n"
          "                    (must be the only option)\n"
          "   -v               Print version information to stdout and exit\n"
          "                    (must be the only option)\n"
          "(See manual page for details)"
   );
   free((void *) n);

   return;
}


/* ========================================================================== */
/*! \brief Strip message body (if present)
 *
 * Data is read from stdin.
 *
 * \return
 * - Pointer to buffer on success
 * - NULL on error
 */

static char *strip_body()
{
   char *buf = NULL;
   char *p;
   size_t len = 2048;
   size_t i = 0;
   int  c;

   while (1)
   {
      /* Allocate more memory for buffer if required */
      if ((len <= i) || NULL == buf)
      {
         if (SIZE_MAX / (size_t) 2 < len)
         {
            print_error("Data size not supported (too large)");
            free((void *) buf);
            buf = NULL;
            break;
         }
         p = (char *) realloc(buf, len * (size_t) 2);
         if (NULL == p)
         {
            print_error("Out of memory");
            free((void *) buf);
            buf = NULL;
            break;
         }
         else
         {
            buf = p;
            len *= (size_t) 2;
         }
      }

      /* Copy input data from stdin to buffer */
      c = fgetc(stdin);
      if (EOF == c)
      {
         print_error("EOF before end of header");
         free((void *) buf);
         buf = NULL;
         break;
      }
      else
      {
         /*
          * Workaround: field_parser() cannot handle NUL control characters.
          * They are no longer allowed and occur only in obsolete or invalid
          * syntax.
          */
         if (!c)
         {
            /* Map NUL to SUB */
            c = 0x1A;
         }
         buf[i++] = c;
      }

      /* Check for end of header */
      if ((size_t) 4 <= i)
      {
         if (!strncmp(&buf[i - (size_t) 4], "\r\n\r\n", (size_t) 4))
         {
            /* Append string termination */
            buf[i] = 0;
            break;
         }
      }
   }

   return (buf);
}


/* ========================================================================== */
/*! \brief Message header parser
 *
 * \param[in] buf  Pointer to header
 * \param[in] fn   Header field name
 *
 * \attention
 * Both parameters are not allowed to be \c NULL .
 *
 * \note
 * If there are multiple header fields with the name \e fn (this is forbidden
 * by RFC8315 for "Cancel-Lock" and "Cancel-Key" header fields), the first one
 * is used.
 *
 * \return
 * - 0 on success
 * - -1 on error
 */

static int field_parser(char *buf, const char *fn)
{
   int  res = -1;
   size_t buf_len, len;
   char *ubuf = NULL;
   char *ufn = NULL;
   size_t i;
   char *p;
   char *start = NULL;
   char *end = NULL;

   /* Create uppercase version of header */
   buf_len = strlen(buf);
   ubuf = (char *) malloc(buf_len + (size_t) 1);
   if (NULL == ubuf)  { print_error("Out of memory"); }
   else
   {
      for (i = 0; i < buf_len; ++i)
      {
         ubuf[i] = toupper((int) (unsigned char) buf[i]);
      }
      ubuf[buf_len] = 0;
   }
   /* Create uppercase version of field name */
   len = strlen(fn);
   ufn = (char *) malloc(len + (size_t) 2);
   if (NULL == ufn)  { print_error("Out of memory"); }
   else
   {
      for (i = 0; i < len; ++i)
      {
         ufn[i] = toupper((int) (unsigned char) fn[i]);
      }
      /* Append colon to field name ("aaa" must not match field "aaab") */
      ufn[len++] = ':';
      ufn[len] = 0;
   }
   /* Search for header field (case insensitive) */
   if (NULL != ubuf && NULL != ufn)
   {
      len = strlen(ufn);

      if (!len)  { print_error("Empty header field name"); }
      else
      {
         i = 0;
         while (res)
         {
            p = strstr(&ubuf[i], ufn);
            if (NULL == p)
            {
               /* Not found */
               break;
            }
            else
            {
               /* Check for position (at start of line) */
               i = (size_t) (p - ubuf);
               if (!i || *(p - 1) == '\n')
               {
                  /* Found */
                  start = &buf[i];
                  res = 0;
               }
               else
               {
                  /* Not found ("Bar:" must not match "Foo-Bar:") */
                  if ((len >= buf_len) || (i >= buf_len - len))
                  {
                     break;
                  }
                  i += len;
               }
            }
         }
      }
   }
   if (res)
   {
      print_error("Header field name not found");
   }
   free((void *) ufn);
   free((void *) ubuf);

   /* Search for end of header field */
   if (!res)
   {
      i = 0;
      while (NULL == end)
      {
         p = strchr(&start[i], (int) '\n');
         if (NULL != p)
         {
            /* Check for folding */
            if (' ' == p[1] || '\t' == p[1])
            {
               /* Folding point detected */
               i = (size_t) (p - start) + (size_t) 1;
               continue;
            }
            else if ((size_t) (p - start) > (size_t) 2)
            {
               end = p - 1;
               /* Terminate after header field body */
               *end = 0;
               break;
            }
         }
      }
   }

   /* Unfold header field */
   if (!res)
   {
      while (NULL != (p = strchr(start, (int) '\r')))
      {
         memmove((void *) p, (void *) &p[1], strlen(&p[1]) + (size_t) 1);
      }
      while (NULL != (p = strchr(start, (int) '\n')))
      {
         memmove((void *) p, (void *) &p[1], strlen(&p[1]) + (size_t) 1);
      }
      /* Print unfolded header field to stdout */
      printf("%s", start);
   }

   return (res);
}


/* ========================================================================== */
/*! \brief Execute request
 *
 * \param[in] opt        Pointer to option string
 * \param[in] opt_value  Pointer to option value string
 *
 * \attention
 * The caller must ensure that \e opt and \e opt_val are not \c NULL .
 *
 * \return
 * - 0 on success
 * - -1 on error
 */

static int exec_request(const char *opt, const char *opt_value)
{
   int res = 0;
   enum { UK, FIELD } op = UK;
   char *buf = NULL;

   if (NULL == opt)  { return (-1); }
   if (NULL == opt_value)  { return (-1); }

   if (!strcmp(opt, "-f")) { op = FIELD; }
   switch (op)
   {
      case FIELD:
         /* Read data from stdin and strip message body */
         buf = strip_body();
         if (NULL == buf)  { res = -1; }
         else
         {
            /* Extract requested header field and unfold it */
            res = field_parser(buf, opt_value);
            free((void *) buf);
         }
         break;
      case UK:
      default:
         print_error("Unknown operation (bug)");
         res = -1;
         break;
   }

   return (res);
}


/* ========================================================================== */
/*! \brief Header parser utility
 *
 * An RFC5322 header (or complete message) in canonical form (with CRLF line
 * breaks) is read from stdin.
 *
 * \param[in] argc  Number of command line arguments
 * \param[in] argv  Array containing command line argument strings
 *
 * \return
 * - \c EXIT_SUCCESS on success
 * - \c EXIT_FAILURE on error
 */

int main(int argc, char **argv)
{
   int rv;
   enum { OK, ERROR, ABORT } rv2 = ERROR;
   unsigned int i;
   char option = 0;  /* Flag indicating option value will follow */
   const char *opt = NULL;
   const char *opt_value = NULL;

   if (argc)
   {
      for (i = 1; i < (unsigned int) argc; i++)
      {
         if (option)
         {
            /* Process value of former option */
            opt_value = argv[i];
            if ('-' == opt_value[0])
            {
               print_error("Option value missing");
               break;
            }
            /* Accept option value */
            rv2 = OK;
            break;
         }
         if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--version"))
         {
            /* Print version information and report success */
            print_version();
            rv2 = ABORT;
            break;
         }
         else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
         {
            /* Print help and report success */
            print_help();
            rv2 = ABORT;
            break;
         }
         else if (!strcmp(argv[i], "-f"))
         {
            /* Option value must follow */
            opt = argv[i];
            option = 1;
         }
         else if ('-' == argv[i][0])
         {
            /* Unknown option */
            print_error("Unknown option");
            break;
         }
      }
      if (ERROR == rv2)
      {
         print_error("Use '-h' or read the man page for help");
      }
   }

   /* Check options */
   switch (rv2)
   {
      case OK:
      {
         /* Check option value */
         if (NULL == opt_value)
         {
            print_error("Option value missing (bug)");
            rv = EXIT_FAILURE;
            break;
         }
         /* Execute requested operation */
         if (exec_request(opt, opt_value)) { rv = EXIT_FAILURE; }
         else { rv = EXIT_SUCCESS; }
         break;
      }
      case ABORT:
      {
         /* Nothing more to do, but no error */
         rv = EXIT_SUCCESS;
         break;
      }
      case ERROR:
      default:
      {
         /* Error */
         rv = EXIT_FAILURE;
         break;
      }
   }

   exit(rv);
}


/* EOF */
