/*      $NetBSD: load.c,v 1.10 2024/08/18 20:47:24 christos Exp $       */


/**
*  \file load.c
*
*  This file contains the routines that deal with processing text strings
*  for options, either from a NUL-terminated string passed in or from an
*  rc/ini file.
*
* @addtogroup autoopts
* @{
*/
/*
*  This file is part of AutoOpts, a companion to AutoGen.
*  AutoOpts is free software.
*  AutoOpts is Copyright (C) 1992-2018 by Bruce Korb - all rights reserved
*
*  AutoOpts is available under any one of two licenses.  The license
*  in use must be one of these two and the choice is under the control
*  of the user of the license.
*
*   The GNU Lesser General Public License, version 3 or later
*      See the files "COPYING.lgplv3" and "COPYING.gplv3"
*
*   The Modified Berkeley Software Distribution License
*      See the file "COPYING.mbsd"
*
*  These files have the following sha256 sums:
*
*  8584710e9b04216a394078dc156b781d0b47e1729104d666658aecef8ee32e95  COPYING.gplv3
*  4379e7444a0e2ce2b12dd6f5a52a27a4d02d39d247901d3285c88cf0d37f477b  COPYING.lgplv3
*  13aa749a5b0a454917a944ed8fffc530b784f5ead522b1aacaf4ec8aa55a6239  COPYING.mbsd
*/

static bool
get_realpath(char * buf, size_t b_sz)
{
#if defined(HAVE_CANONICALIZE_FILE_NAME)
   {
       size_t name_len;

       char * pz = canonicalize_file_name(buf);
       if (pz == NULL)
           return false;

       name_len = strlen(pz);
       if (name_len >= (size_t)b_sz) {
           free(pz);
           return false;
       }

       memcpy(buf, pz, name_len + 1);
       free(pz);
   }

#elif defined(HAVE_REALPATH)
   {
       size_t name_len;
       char z[PATH_MAX+1];

       if (realpath(buf, z) == NULL)
           return false;

       name_len = strlen(z);
       if (name_len >= b_sz)
           return false;

       memcpy(buf, z, name_len + 1);
   }
#endif
   return true;
}

/*=export_func  optionMakePath
* private:
*
* what:  translate and construct a path
* arg:   + char *       + p_buf     + The result buffer +
* arg:   + int          + b_sz      + The size of this buffer +
* arg:   + char const * + fname     + The input name +
* arg:   + char const * + prg_path  + The full path of the current program +
*
* ret-type: bool
* ret-desc: true if the name was handled, otherwise false.
*           If the name does not start with ``$'', then it is handled
*           simply by copying the input name to the output buffer and
*           resolving the name with either
*           @code{canonicalize_file_name(3GLIBC)} or @code{realpath(3C)}.
*
* doc:
*
*  This routine will copy the @code{pzName} input name into the
*  @code{pzBuf} output buffer, not exceeding @code{bufSize} bytes.  If the
*  first character of the input name is a @code{'$'} character, then there
*  is special handling:
*  @*
*  @code{$$} is replaced with the directory name of the @code{pzProgPath},
*  searching @code{$PATH} if necessary.
*  @*
*  @code{$@} is replaced with the AutoGen package data installation directory
*  (aka @code{pkgdatadir}).
*  @*
*  @code{$NAME} is replaced by the contents of the @code{NAME} environment
*  variable.  If not found, the search fails.
*
*  Please note: both @code{$$} and @code{$NAME} must be at the start of the
*     @code{pzName} string and must either be the entire string or be followed
*     by the @code{'/'} (backslash on windows) character.
*
* err:  @code{false} is returned if:
*       @*
*       @bullet{} The input name exceeds @code{bufSize} bytes.
*       @*
*       @bullet{} @code{$$}, @code{$@@} or @code{$NAME} is not the full string
*                 and the next character is not '/'.
*       @*
*       @bullet{} libopts was built without PKGDATADIR defined and @code{$@@}
*                 was specified.
*       @*
*       @bullet{} @code{NAME} is not a known environment variable
*       @*
*       @bullet{} @code{canonicalize_file_name} or @code{realpath} return
*                 errors (cannot resolve the resulting path).
=*/
bool
optionMakePath(char * p_buf, int b_sz, char const * fname, char const * prg_path)
{
   {
       size_t len = strlen(fname);

       if (((size_t)b_sz <= len) || (len == 0))
           return false;
   }

   /*
    *  IF not an environment variable, just copy the data
    */
   if (*fname != '$') {
       char   const * src = fname;
       char * dst = p_buf;
       int    ct  = b_sz;

       for (;;) {
           if ( (*(dst++) = *(src++)) == NUL)
               break;
           if (--ct <= 0)
               return false;
       }
   }

   /*
    *  IF the name starts with "$$", then it must be "$$" or
    *  it must start with "$$/".  In either event, replace the "$$"
    *  with the path to the executable and append a "/" character.
    */
   else switch (fname[1]) {
   case NUL:
       return false;

   case '$':
       if (! add_prog_path(p_buf, b_sz, fname, prg_path))
           return false;
       break;

   case '@':
       if (program_pkgdatadir[0] == NUL)
           return false;

       if (snprintf(p_buf, (size_t)b_sz, "%s%s",
                    program_pkgdatadir, fname + 2) >= b_sz)
           return false;
       break;

   default:
       if (! add_env_val(p_buf, b_sz, fname))
           return false;
   }

   return get_realpath(p_buf, b_sz);
}

/**
* convert a leading "$$" into a path to the executable.
*/
static bool
add_prog_path(char * buf, int b_sz, char const * fname, char const * prg_path)
{
   char const *   path;
   char const *   pz;
   int     skip = 2;
   size_t  fname_len;
   size_t  dir_len;  //!< length of the directory portion of the path to the exe

   switch (fname[2]) {
   case DIRCH:
       skip = 3;
   case NUL:
       break;
   default:
       return false;
   }

   /*
    *  See if the path is included in the program name.
    *  If it is, we're done.  Otherwise, we have to hunt
    *  for the program using "pathfind".
    */
   if (strchr(prg_path, DIRCH) != NULL)
       path = prg_path;
   else {
       path = pathfind(getenv("PATH"), prg_path, "rx");

       if (path == NULL)
           return false;
   }

   pz = strrchr(path, DIRCH);

   /*
    *  IF we cannot find a directory name separator,
    *  THEN we do not have a path name to our executable file.
    */
   if (pz == NULL)
       return false;

   fname    += skip;
   fname_len = strlen(fname) + 1; // + NUL byte
   dir_len   = (pz - path) + 1;   // + dir sep character

   /*
    *  Concatenate the file name to the end of the executable path.
    *  The result may be either a file or a directory.
    */
   if (dir_len + fname_len > (unsigned)b_sz)
       return false;

   memcpy(buf, path, dir_len);
   memcpy(buf + dir_len, fname, fname_len);

   /*
    *  If the "path" path was gotten from "pathfind()", then it was
    *  allocated and we need to deallocate it.
    */
   if (path != prg_path)
       AGFREE(path);
   return true;
}

/**
* Add an environment variable value.
*/
static bool
add_env_val(char * buf, int buf_sz, char const * name)
{
   char * dir_part = buf;

   for (;;) {
       int ch = (int)*++name;
       if (! IS_VALUE_NAME_CHAR(ch))
           break;
       *(dir_part++) = (char)ch;
   }

   if (dir_part == buf)
       return false;

   *dir_part = NUL;

   dir_part = getenv(buf);

   /*
    *  Environment value not found -- skip the home list entry
    */
   if (dir_part == NULL)
       return false;

   {
       size_t dir_len = strlen(dir_part);
       size_t nm_len  = strlen(name) + 1;

       if (dir_len + nm_len >= (unsigned)buf_sz)
           return false;
       memcpy(buf, dir_part, dir_len);
       memcpy(buf + dir_len, name, nm_len);
   }

   return true;
}

/**
* Trim leading and trailing white space.
* If we are cooking the text and the text is quoted, then "cook"
* the string.  To cook, the string must be quoted.
*
* @param[in,out] txt  the input and output string
* @param[in]     mode the handling mode (cooking method)
*/
static void
munge_str(char * txt, tOptionLoadMode mode)
{
   char * end;

   if (mode == OPTION_LOAD_KEEP)
       return;

   if (IS_WHITESPACE_CHAR(*txt)) {
       char * src = SPN_WHITESPACE_CHARS(txt+1);
       size_t l   = strlen(src) + 1;
       memmove(txt, src, l);
       end = txt + l - 1;

   } else
       end = txt + strlen(txt);

   end  = SPN_WHITESPACE_BACK(txt, end);
   *end = NUL;

   if (mode == OPTION_LOAD_UNCOOKED)
       return;

   switch (*txt) {
   default: return;
   case '"':
   case '\'': break;
   }

   switch (end[-1]) {
   default: return;
   case '"':
   case '\'': break;
   }

   (void)ao_string_cook(txt, NULL);
}

static char *
assemble_arg_val(char * txt, tOptionLoadMode mode)
{
   char * end = strpbrk(txt, ARG_BREAK_STR);
   int    space_break;

   /*
    *  Not having an argument to a configurable name is okay.
    */
   if (end == NULL)
       return txt + strlen(txt);

   /*
    *  If we are keeping all whitespace, then the  modevalue starts with the
    *  character that follows the end of the configurable name, regardless
    *  of which character caused it.
    */
   if (mode == OPTION_LOAD_KEEP) {
       *(end++) = NUL;
       return end;
   }

   /*
    *  If the name ended on a white space character, remember that
    *  because we'll have to skip over an immediately following ':' or '='
    *  (and the white space following *that*).
    */
   space_break = IS_WHITESPACE_CHAR(*end);
   *(end++) = NUL;

   end = SPN_WHITESPACE_CHARS(end);
   if (space_break && ((*end == ':') || (*end == '=')))
       end = SPN_WHITESPACE_CHARS(end+1);

   return end;
}

static char *
trim_quotes(char * arg)
{
   switch (*arg) {
   case '"':
   case '\'':
       ao_string_cook(arg, NULL);
   }
   return arg;
}

/**
* See if the option is to be processed in the current scan direction
* (-1 or +1).
*/
static bool
direction_ok(opt_state_mask_t f, int dir)
{
   if (dir == 0)
       return true;

   switch (f & (OPTST_IMM|OPTST_DISABLE_IMM)) {
   case 0:
       /*
        *  The selected option has no immediate action.
        *  THEREFORE, if the direction is PRESETTING
        *  THEN we skip this option.
        */
       if (PRESETTING(dir))
           return false;
       break;

   case OPTST_IMM:
       if (PRESETTING(dir)) {
           /*
            *  We are in the presetting direction with an option we handle
            *  immediately for enablement, but normally for disablement.
            *  Therefore, skip if disabled.
            */
           if ((f & OPTST_DISABLED) == 0)
               return false;
       } else {
           /*
            *  We are in the processing direction with an option we handle
            *  immediately for enablement, but normally for disablement.
            *  Therefore, skip if NOT disabled.
            */
           if ((f & OPTST_DISABLED) != 0)
               return false;
       }
       break;

   case OPTST_DISABLE_IMM:
       if (PRESETTING(dir)) {
           /*
            *  We are in the presetting direction with an option we handle
            *  immediately for disablement, but normally for handling.
            *  Therefore, skip if NOT disabled.
            */
           if ((f & OPTST_DISABLED) != 0)
               return false;
       } else {
           /*
            *  We are in the processing direction with an option we handle
            *  immediately for disablement, but normally for handling.
            *  Therefore, skip if disabled.
            */
           if ((f & OPTST_DISABLED) == 0)
               return false;
       }
       break;

   case OPTST_IMM|OPTST_DISABLE_IMM:
       /*
        *  The selected option is always for immediate action.
        *  THEREFORE, if the direction is PROCESSING
        *  THEN we skip this option.
        */
       if (PROCESSING(dir))
           return false;
       break;
   }
   return true;
}

/**
*  Load an option from a block of text.  The text must start with the
*  configurable/option name and be followed by its associated value.
*  That value may be processed in any of several ways.  See "tOptionLoadMode"
*  in autoopts.h.
*
* @param[in,out] opts       program options descriptor
* @param[in,out] opt_state  option processing state
* @param[in,out] line       source line with long option name in it
* @param[in]     direction  current processing direction (preset or not)
* @param[in]     load_mode  option loading mode (OPTION_LOAD_*)
*/
static void
load_opt_line(tOptions * opts, tOptState * opt_state, char * line,
             tDirection direction, tOptionLoadMode load_mode )
{
   /*
    * When parsing a stored line, we only look at the characters after
    * a hyphen.  Long names must always be at least two characters and
    * short options are always exactly one character long.
    */
   line = SPN_LOAD_LINE_SKIP_CHARS(line);

   {
       char * arg = assemble_arg_val(line, load_mode);

       if (IS_OPTION_NAME_CHAR(line[1])) {

           if (! SUCCESSFUL(opt_find_long(opts, line, opt_state)))
               return;

       } else if (! SUCCESSFUL(opt_find_short(opts, *line, opt_state)))
           return;

       if ((! CALLED(direction)) && (opt_state->flags & OPTST_NO_INIT))
           return;

       opt_state->pzOptArg = trim_quotes(arg);
   }

   if (! direction_ok(opt_state->flags, direction))
       return;

   /*
    *  Fix up the args.
    */
   if (OPTST_GET_ARGTYPE(opt_state->pOD->fOptState) == OPARG_TYPE_NONE) {
       if (*opt_state->pzOptArg != NUL)
           return;
       opt_state->pzOptArg = NULL;

   } else if (opt_state->pOD->fOptState & OPTST_ARG_OPTIONAL) {
       if (*opt_state->pzOptArg == NUL)
            opt_state->pzOptArg = NULL;
       else {
           AGDUPSTR(opt_state->pzOptArg, opt_state->pzOptArg, "opt arg");
           opt_state->flags |= OPTST_ALLOC_ARG;
       }

   } else {
       if (*opt_state->pzOptArg == NUL)
            opt_state->pzOptArg = zNil;
       else {
           AGDUPSTR(opt_state->pzOptArg, opt_state->pzOptArg, "opt arg");
           opt_state->flags |= OPTST_ALLOC_ARG;
       }
   }

   {
       tOptionLoadMode sv = option_load_mode;
       option_load_mode = load_mode;
       handle_opt(opts, opt_state);
       option_load_mode = sv;
   }
}

/*=export_func  optionLoadLine
*
* what:  process a string for an option name and value
*
* arg:   tOptions *,   opts,  program options descriptor
* arg:   char const *, line,  NUL-terminated text
*
* doc:
*
*  This is a client program callable routine for setting options from, for
*  example, the contents of a file that they read in.  Only one option may
*  appear in the text.  It will be treated as a normal (non-preset) option.
*
*  When passed a pointer to the option struct and a string, it will find
*  the option named by the first token on the string and set the option
*  argument to the remainder of the string.  The caller must NUL terminate
*  the string.  The caller need not skip over any introductory hyphens.
*  Any embedded new lines will be included in the option
*  argument.  If the input looks like one or more quoted strings, then the
*  input will be "cooked".  The "cooking" is identical to the string
*  formation used in AutoGen definition files (@pxref{basic expression}),
*  except that you may not use backquotes.
*
* err:   Invalid options are silently ignored.  Invalid option arguments
*        will cause a warning to print, but the function should return.
=*/
void
optionLoadLine(tOptions * opts, char const * line)
{
   tOptState st = OPTSTATE_INITIALIZER(SET);
   char *    pz;
   proc_state_mask_t sv_flags = opts->fOptSet;
   opts->fOptSet &= ~OPTPROC_ERRSTOP;
   AGDUPSTR(pz, line, "opt line");
   load_opt_line(opts, &st, pz, DIRECTION_CALLED, OPTION_LOAD_COOKED);
   AGFREE(pz);
   opts->fOptSet = sv_flags;
}
/** @}
*
* Local Variables:
* mode: C
* c-file-style: "stroustrup"
* indent-tabs-mode: nil
* End:
* end of autoopts/load.c */