LCOV - code coverage report
Current view: top level - src - parse_configfile.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.6 % 85 83
Test Date: 2026-10-20 14:27:43 Functions: 100.0 % 2 2
Branches: 97.7 % 86 84

             Branch data     Line data    Source code
       1                 :             : #include "precizer.h"
       2                 :             : #include <sysexits.h>
       3                 :             : 
       4                 :             : /* Shared CLI option table */
       5                 :             : extern const struct argp_option options[];
       6                 :             : 
       7                 :             : /**
       8                 :             :  * @brief Share record handling results with the file reader through inih's user pointer
       9                 :             :  *
      10                 :             :  * One local instance belongs to each read_config_file() call. has_entries
      11                 :             :  * records whether a recognized, permitted key was encountered, including a
      12                 :             :  * false flag that disables an option. status records handler failure, so the
      13                 :             :  * reader does not add a generic syntax error after a diagnostic was already
      14                 :             :  * reported. state supplies the program name and error stream for diagnostics.
      15                 :             :  * The settings themselves are stored in Config
      16                 :             :  */
      17                 :             : typedef struct {
      18                 :             :         bool has_entries;
      19                 :             :         Return status;
      20                 :             :         struct argp_state *state;
      21                 :             : } ConfigFileReadContext;
      22                 :             : 
      23                 :             : /**
      24                 :             :  * @brief Resolve a configuration key and apply its value through parse_opt
      25                 :             :  *
      26                 :             :  * Strip one matching pair of outer quotes before interpreting the value.
      27                 :             :  * Interior spaces and backslashes are preserved after inih handles comments
      28                 :             :  *
      29                 :             :  * @param user Configuration read context
      30                 :             :  * @param section Ignored section name supplied by inih
      31                 :             :  * @param name Exact long option name, or NULL for a section header
      32                 :             :  * @param value Parsed value, valid only during this call
      33                 :             :  * @param line Original physical line number
      34                 :             :  * @return Nonzero when a setting is applied or a section is ignored, or zero after an error
      35                 :             :  */
      36                 :         981 : static int read_config_entry(
      37                 :             :         void       *user,
      38                 :             :         const char *section __attribute__((unused)),
      39                 :             :         const char *name,
      40                 :             :         const char *value,
      41                 :             :         int        line)
      42                 :             : {
      43                 :         981 :         ConfigFileReadContext *context = user;
      44                 :             : 
      45                 :             :         /* Section headers only organize the file; all keys share the same settings */
      46         [ +  + ]:         981 :         if(name == NULL)
      47                 :             :         {
      48                 :          10 :                 return(1);
      49                 :             :         }
      50                 :             : 
      51                 :             :         /* Help, usage, version, and configuration file selection are available only
      52                 :             :            through CLI. Reject these keys before parsing their values, so even
      53                 :             :            help = false is an error. The lookup below accepts only exact long names
      54                 :             :            without leading dashes; short forms such as h, V, and -h are rejected
      55                 :             :            as unknown keys */
      56   [ +  +  +  + ]:         971 :         if(strcmp(name,"help") == 0 || strcmp(name,"usage") == 0 ||
      57   [ +  +  +  + ]:         969 :            strcmp(name,"version") == 0 || strcmp(name,"config") == 0)
      58                 :             :         {
      59                 :           4 :                 argp_failure(context->state,0,0,"ERROR: Line %d in config file: Config key '%s' is only available in CLI",line,name);
      60                 :           4 :                 context->status = FAILURE;
      61                 :           4 :                 return(0);
      62                 :             :         }
      63                 :             : 
      64                 :             :         /* For an option entry, argp_option.name is the long CLI name, key selects
      65                 :             :            its parse_opt() branch, and arg names the expected value (NULL for a flag).
      66                 :             :            This pointer refers to an existing options[] entry without copying it.
      67                 :             :            Keep the most recent non-alias entry so an alias can use its primary
      68                 :             :            option's value requirements and handler key */
      69                 :         967 :         const struct argp_option *option = NULL;
      70                 :             : 
      71                 :             :         /* Find this configuration key in the shared CLI table. Stop at the zero
      72                 :             :            terminator, not at the first entry without a name: help group headings
      73                 :             :            and aliases that provide only a short option also have NULL names.
      74                 :             :            Compare the supplied key with each long name exactly, including case.
      75                 :             :            On a match, convert flag values and apply the setting, then return.
      76                 :             :            Reaching the end without a match produces the unknown-key error below */
      77         [ +  + ]:        4228 :         for(size_t i = 0; options[i].name != NULL || options[i].key != 0 ||
      78   [ +  +  +  +  :       20932 :             options[i].doc != NULL || options[i].group != 0; i++)
                   -  + ]
      79                 :             :         {
      80                 :             :                 /* Aliases inherit the argument requirements of their primary option.
      81                 :             :                    Match the alias's own name below while retaining the primary entry */
      82         [ +  + ]:       17671 :                 if((options[i].flags & OPTION_ALIAS) == 0)
      83                 :             :                 {
      84                 :       15379 :                         option = &options[i];
      85                 :             :                 }
      86                 :             : 
      87   [ +  +  +  + ]:       17671 :                 if(options[i].name != NULL && strcmp(name,options[i].name) == 0)
      88                 :             :                 {
      89                 :             :                         /* A false flag still counts as an explicit setting */
      90                 :         962 :                         context->has_entries = true;
      91                 :             : 
      92                 :             :                         /* Whether parse_opt() needs the prepared value as its argument.
      93                 :             :                            Remains true for options with values and for false flags, which
      94                 :             :                            receive the normalized string "false". Becomes false for true
      95                 :             :                            flags, which receive NULL just like flags supplied through CLI */
      96                 :         962 :                         bool takes_value = true;
      97                 :             : 
      98                 :             :                         /* Number of value bytes to copy, excluding the terminating null byte.
      99                 :             :                            Initially includes any outer quotes; reduced by two when they are removed.
     100                 :             :                            inih has already removed surrounding whitespace and comments.
     101                 :             :                            Only a leading single or double quote starts a quoted value;
     102                 :             :                            otherwise the pointer and length retain the entire value */
     103                 :         962 :                         size_t value_length = strlen(value);
     104                 :             : 
     105   [ +  +  +  + ]:         962 :                         if(value[0] == '"' || value[0] == '\'')
     106                 :             :                         {
     107                 :             :                                 /* A quoted value needs at least two characters, and its final
     108                 :             :                                    character must match the opening quote. A lone opening quote
     109                 :             :                                    or a different final character is an error on this physical line */
     110   [ +  +  +  + ]:          32 :                                 if(value_length < 2 || value[value_length - 1] != value[0])
     111                 :             :                                 {
     112                 :             :                                         /* Report the key and line, then stop reading. Recording the failure
     113                 :             :                                            prevents the file reader from adding a generic syntax error */
     114                 :           4 :                                         argp_failure(context->state,0,0,"ERROR: Line %d in config file: Unclosed quoted value for '%s'",line,name);
     115                 :             : 
     116                 :           4 :                                         context->status = FAILURE;
     117                 :             : 
     118                 :           4 :                                         return(0);
     119                 :             :                                 }
     120                 :             : 
     121                 :             :                                 /* Skip the opening quote and exclude both quotes from the copy length.
     122                 :             :                                    The original text is not modified. Only this outer pair is removed;
     123                 :             :                                    inner quotes, spaces, and backslashes remain literal characters.
     124                 :             :                                    An empty pair produces a zero-length value for normal option validation */
     125                 :          28 :                                 value++;
     126                 :             : 
     127                 :          28 :                                 value_length -= 2;
     128                 :             :                         }
     129                 :             : 
     130                 :             :                         /* Argp accepts mutable arguments, so copy the selected bytes into a local
     131                 :             :                            buffer. inih bounds the input with INI_MAX_LINE, so the value fits.
     132                 :             :                            Add the terminating null byte explicitly: memcpy() copies only the
     133                 :             :                            selected contents, excluding any closing quote and original terminator */
     134                 :             :                         char value_buffer[INI_MAX_LINE];
     135                 :             : 
     136                 :         958 :                         memcpy(value_buffer,value,value_length);
     137                 :             : 
     138                 :         958 :                         value_buffer[value_length] = '\0';
     139                 :             : 
     140                 :             :                         /* Interpret booleans and other option values from this prepared copy,
     141                 :             :                            so quoted and unquoted spellings reach the same validation logic */
     142                 :         958 :                         value = value_buffer;
     143                 :             : 
     144                 :             :                         /* Flags accept boolean values, except compare, whose file entries
     145                 :             :                            contain database paths. dry-run also accepts a named mode,
     146                 :             :                            which is validated by parse_opt() when it is not a boolean */
     147   [ +  +  +  +  :         958 :                         if((option->arg == NULL && option->key != 'c') || option->key == 'n')
                   +  + ]
     148                 :             :                         {
     149                 :             :                                 /* Normalize false spellings for the shared option handler */
     150   [ +  +  +  + ]:         408 :                                 if(strcasecmp(value,"false") == 0 || strcasecmp(value,"no") == 0 ||
     151   [ +  +  +  + ]:         334 :                                         strcasecmp(value,"off") == 0 || strcasecmp(value,"n") == 0 ||
     152         [ +  + ]:         285 :                                         strcmp(value,"0") == 0)
     153                 :             :                                 {
     154                 :         149 :                                         strcpy(value_buffer,"false");
     155                 :             : 
     156   [ +  +  +  + ]:         259 :                                 } else if(strcasecmp(value,"true") == 0 || strcasecmp(value,"yes") == 0 ||
     157   [ +  +  +  + ]:          60 :                                         strcasecmp(value,"on") == 0 || strcasecmp(value,"y") == 0 ||
     158         [ +  + ]:          36 :                                         strcmp(value,"1") == 0){
     159                 :             :                                         /* A true flag reaches parse_opt() without a value, as in CLI */
     160                 :         237 :                                         takes_value = false;
     161                 :             : 
     162         [ +  + ]:          22 :                                 } else if(option->key != 'n'){
     163                 :             : 
     164                 :           6 :                                         argp_failure(context->state,0,0,"ERROR: Line %d in config file: Unsupported --%s value '%s'. Supported values: true/yes/on/y/1 or false/no/off/n/0",line,name,value);
     165                 :           6 :                                         context->status = FAILURE;
     166                 :           6 :                                         return(0);
     167                 :             :                                 }
     168                 :             :                         }
     169                 :             : 
     170                 :         952 :                         char *argument = NULL;
     171                 :             : 
     172         [ +  + ]:         952 :                         if(takes_value == true)
     173                 :             :                         {
     174                 :         715 :                                 argument = value_buffer;
     175                 :             :                         }
     176                 :             : 
     177                 :             :                         /* Each compare entry enables comparison and supplies one database
     178                 :             :                            path through the existing positional-argument handler */
     179                 :         952 :                         int key = option->key;
     180                 :             : 
     181         [ +  + ]:         952 :                         if(key == 'c')
     182                 :             :                         {
     183         [ +  + ]:          85 :                                 if(argument[0] == '\0')
     184                 :             :                                 {
     185                 :           4 :                                         argp_failure(context->state,0,0,"ERROR: Line %d in config file: Database path for 'compare' must not be empty",line);
     186                 :           4 :                                         context->status = FAILURE;
     187                 :           4 :                                         return(0);
     188                 :             :                                 }
     189                 :             : 
     190                 :          81 :                                 config->compare = true;
     191                 :          81 :                                 key = ARGP_KEY_ARG;
     192                 :             :                         }
     193                 :             : 
     194                 :             :                         /* Make the line available to shared option diagnostics only while this
     195                 :             :                            entry is being applied. Reset it even when parse_opt() returns an
     196                 :             :                            error, so later CLI processing cannot inherit a file line */
     197                 :         948 :                         config->config_file_error_line = line;
     198                 :             : 
     199                 :         948 :                         error_t parse_error = parse_opt(key,argument,context->state);
     200                 :             : 
     201                 :         948 :                         config->config_file_error_line = -1;
     202                 :             : 
     203         [ +  + ]:         948 :                         if(parse_error != EX_OK)
     204                 :             :                         {
     205                 :          35 :                                 context->status = FAILURE;
     206                 :          35 :                                 return(0);
     207                 :             :                         }
     208                 :             : 
     209                 :         913 :                         return(1);
     210                 :             :                 }
     211                 :             :         }
     212                 :             : 
     213                 :           5 :         argp_failure(context->state,0,0,"ERROR: Line %d in config file: Unknown config key '%s'",line,name);
     214                 :           5 :         context->status = FAILURE;
     215                 :           5 :         return(0);
     216                 :             : }
     217                 :             : 
     218                 :             : /**
     219                 :             :  * @brief Read configuration with inih and apply options through parse_opt
     220                 :             :  *
     221                 :             :  * Config must be initialized before reading. Recognized records are applied
     222                 :             :  * in file order regardless of section. Section headers are ignored, and
     223                 :             :  * reading stops at the first syntax, name, or value error.
     224                 :             :  * Required parameters and option combinations are checked during finalization
     225                 :             :  *
     226                 :             :  * @param path Configuration file path, relative to the working directory or absolute
     227                 :             :  * @param required Whether a missing file is an error; other open errors always fail
     228                 :             :  * @param has_entries Set when at least one recognized, permitted record is read
     229                 :             :  * @param state Diagnostic state with the program name and error stream
     230                 :             :  * @return SUCCESS on completion or an optional missing file, otherwise FAILURE
     231                 :             :  */
     232                 :         276 : Return read_config_file(
     233                 :             :         const char        *path,
     234                 :             :         bool              required,
     235                 :             :         bool              *has_entries,
     236                 :             :         struct argp_state *state)
     237                 :             : {
     238                 :             :         /* Status returned by this function through deliver()
     239                 :             :            Default value assumes successful completion */
     240                 :         276 :         Return status = SUCCESS;
     241                 :             : 
     242                 :             :         /* Output flag for whether a recognized, permitted setting key was read.
     243                 :             :            Blank lines, comments, and section headers do not count; false flags do.
     244                 :             :            This distinguishes a file without settings from one that leaves defaults
     245                 :             :            unchanged. Start with no entries, including when an optional file is absent.
     246                 :             :            The return status reports errors separately; this flag does not mean success */
     247                 :         276 :         *has_entries = false;
     248                 :             : 
     249                 :         276 :         FILE *file = fopen(path,"rb");
     250                 :             : 
     251         [ +  + ]:         276 :         if(file == NULL)
     252                 :             :         {
     253   [ +  +  +  + ]:          14 :                 if(required || errno != ENOENT)
     254                 :             :                 {
     255                 :          11 :                         argp_failure(state,0,errno,"ERROR: Cannot open config file '%s'",path);
     256                 :          11 :                         status = FAILURE;
     257                 :             :                 }
     258                 :             : 
     259                 :             :         } else {
     260                 :             : 
     261                 :             :                 /* inih reads the open file and calls read_config_entry() for each record.
     262                 :             :                    The callback applies settings and records any error it reports.
     263                 :             :                    Copy its status and setting-presence flag back after parsing.
     264                 :             :                    If the callback succeeded, check for a stream read error first, then
     265                 :             :                    a parser error, using the returned line number for syntax diagnostics.
     266                 :             :                    The status checks prevent a second message for an already reported error */
     267                 :         262 :                 ConfigFileReadContext context = {.status = SUCCESS,.state = state};
     268                 :             : 
     269                 :         262 :                 int parse_error = ini_parse_file(file,read_config_entry,&context);
     270                 :             : 
     271                 :         262 :                 status = context.status;
     272                 :             : 
     273                 :         262 :                 *has_entries = context.has_entries;
     274                 :             : 
     275   [ +  +  +  + ]:         262 :                 if(SUCCESS == status && ferror(file))
     276                 :             :                 {
     277                 :           7 :                         argp_failure(state,0,errno,"ERROR: Cannot read config file '%s'",path);
     278                 :           7 :                         status = FAILURE;
     279                 :             :                 }
     280                 :             : 
     281   [ +  +  +  + ]:         262 :                 if(SUCCESS == status && parse_error != 0)
     282                 :             :                 {
     283                 :           4 :                         argp_failure(state,0,0,"ERROR: Line %d in config file: Invalid config syntax",parse_error);
     284                 :           4 :                         status = FAILURE;
     285                 :             :                 }
     286                 :             : 
     287                 :             :                 /* Closing remains mandatory after a parse or read error */
     288                 :         262 :                 int close_error = fclose(file);
     289                 :             : 
     290   [ +  +  -  + ]:         262 :                 if(SUCCESS == status && close_error != 0)
     291                 :             :                 {
     292                 :           0 :                         argp_failure(state,0,errno,"ERROR: Cannot close config file '%s'",path);
     293                 :           0 :                         status = FAILURE;
     294                 :             :                 }
     295                 :             :         }
     296                 :             : 
     297                 :         276 :         deliver(status);
     298                 :             : }
        

Generated by: LCOV version 2.0-1