LCOV - code coverage report
Current view: top level - src - parse_arguments.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 88.3 % 281 248
Test Date: 2026-10-20 14:27:43 Functions: 100.0 % 4 4
Branches: 85.8 % 219 188

             Branch data     Line data    Source code
       1                 :             : #include "precizer.h"
       2                 :             : #include <stdarg.h>
       3                 :             : #include <sysexits.h>
       4                 :             : 
       5                 :             : /**
       6                 :             :  * @brief Parse command-line arguments and populate global runtime configuration
       7                 :             :  */
       8                 :             : 
       9                 :             : /* Program documentation text used by argp */
      10                 :             : static char doc[] =
      11                 :             :         "\n" BOLD APP_NAME RESET ": data integrity verification for file systems of any scale\n\n"
      12                 :             :         BOLD APP_NAME RESET " is a lightweight, high-performance CLI tool written in pure C. It’s designed for file integrity verification and comparison, making it especially useful for validating synchronization results. The program traverses directory trees and builds a database of files and their checksums for fast, repeatable comparisons.\n"
      13                 :             :         "\n"
      14                 :             :         "Built for embedded systems and large-scale clustered environments, " BOLD APP_NAME RESET " detects synchronization drift by comparing files and checksums across sources. It can also analyze historical changes by comparing databases captured from the same source at different points in time.\n"
      15                 :             :         "\n"
      16                 :             :         "With love for Ukraine\n"
      17                 :             :         "\vSIMPLE EXAMPLE\n"
      18                 :             :         "\n"
      19                 :             :         "Use this workflow to verify that two mounted directory trees are equivalent.\n"
      20                 :             :         "\n"
      21                 :             :         "Assume the source trees are mounted at " YELLOW "/mnt1" RESET " and " YELLOW "/mnt2" RESET ".\n"
      22                 :             :         "\n"
      23                 :             :         "1. On machine " YELLOW "'host1'" RESET ", run:\n"
      24                 :             :         "\n"
      25                 :             :         "   " BOLDGREEN "$ " APP_NAME " --progress /mnt1" RESET "\n"
      26                 :             :         "\n"
      27                 :             :         "This traversal scans " YELLOW "/mnt1" RESET " recursively and creates " YELLOW "host1.db" RESET " in the current directory.\n"
      28                 :             :         "The " BOLD "--progress" RESET " option reports processed data volume and file count.\n"
      29                 :             :         "\n"
      30                 :             :         "2. On machine " YELLOW "'host2'" RESET ", run:\n"
      31                 :             :         "\n"
      32                 :             :         "   " BOLDGREEN "$ " APP_NAME " --progress /mnt2" RESET "\n"
      33                 :             :         "\n"
      34                 :             :         "This creates " YELLOW "host2.db" RESET " in the current directory.\n"
      35                 :             :         "\n"
      36                 :             :         "3. Copy " YELLOW "host1.db" RESET " and " YELLOW "host2.db" RESET " to one machine, then run:\n"
      37                 :             :         "\n"
      38                 :             :         "   " BOLDGREEN "$ " APP_NAME " --compare host1.db host2.db" RESET "\n"
      39                 :             :         "\n"
      40                 :             :         "Output reports:\n"
      41                 :             :         "\n"
      42                 :             :         "* Files present on " YELLOW "'host1'" RESET " but missing on " YELLOW "'host2'" RESET ", and vice versa.\n"
      43                 :             :         "* Files present on both hosts whose SHA512 checksums do not match.\n"
      44                 :             :         "\n"
      45                 :             :         "Database paths are stored as relative paths only.\n"
      46                 :             :         "For example, " YELLOW "/mnt1/abc/def/aaa.txt" RESET " is stored as " YELLOW "abc/def/aaa.txt" RESET ". "
      47                 :             :         "The same relative-path rule applies to " YELLOW "/mnt2/abc/def/aaa.txt" RESET ", enabling direct cross-source comparison.\n"
      48                 :             :         "\n"
      49                 :             :         "See the project README for additional technical details.";
      50                 :             : 
      51                 :             : /* Positional-argument description for argp */
      52                 :             : static char args_doc[] = "[PATH...]";
      53                 :             : 
      54                 :             : static bool information_mode_requested = false;
      55                 :             : 
      56                 :             : /* Supported command-line options for argp */
      57                 :             : const struct argp_option options[] = {
      58                 :             :         { 0,0,0,0,"Locked Checksum Protection:",3},
      59                 :             :         {"lock-checksum",'k',"PCRE2_REGEXP",0,"Relative path to be treated as immutable archival data. PCRE2 regular expressions can be used to "
      60                 :             :          "select files or directories whose checksums are written once to the database and never updated "
      61                 :             :          "again. If no matching files exist in the database yet, their entries and checksums will still be "
      62                 :             :          "created normally when they are scanned for the first time. All paths for the regular expression "
      63                 :             :          "must be specified as relative, the same way as for the "
      64                 :             :          BOLD "--ignore" RESET " option. For these entries, the "
      65                 :             :          BOLD "--update" RESET " option will not recalculate checksums; any difference in file size, timestamps, or content will "
      66                 :             :          "always be reported as data corruption, and no replacement checksum will be generated. Multiple "
      67                 :             :          "regular expressions can be specified using multiple "
      68                 :             :          BOLD "--lock-checksum" RESET " options.\n"
      69                 :             :          "Example:\n"
      70                 :             :          BOLD APP_NAME " --update --lock-checksum=\"^archive/2077/.*\" /mnt/storage" RESET,0},
      71                 :             :         {"rehash-locked",'r',0,0,"Force a full SHA512 rehash for every file that is already stored "
      72                 :             :          "in the database and protected by any "
      73                 :             :          BOLD "--lock-checksum" RESET " pattern. "
      74                 :             :          "This option must always be used together with "
      75                 :             :          BOLD "--lock-checksum" RESET "; it has no effect otherwise. During an update pass, "
      76                 :             :          "every locked entry found on disk is read again, its checksum is recomputed, and the "
      77                 :             :          "result is compared with the value stored in the database. This provides an extra "
      78                 :             :          "validation layer for immutable archives at the cost of additional I/O and CPU time, "
      79                 :             :          "ensuring that frozen records are validated even when file size or timestamps alone are "
      80                 :             :          "not sufficient indicators. "
      81                 :             :          "The option purposefully ignores "
      82                 :             :          BOLD "--watch-timestamps" RESET " — timestamp tracking can be on or off, the rehash will "
      83                 :             :          "still take place. If the recomputed checksum and file size match the database entry, "
      84                 :             :          "the record stays consistent but differing ctime/mtime values in the database are "
      85                 :             :          "synchronized with the on-disk timestamps. If the checksum differs, the file is "
      86                 :             :          "reported as corrupted just like any other locked entry.\n"
      87                 :             :          "Example:\n"
      88                 :             :          BOLD APP_NAME " --update --lock-checksum=\"^archive/2024/.*\" --rehash-locked /mnt/storage" RESET,0},
      89                 :             :         { 0,0,0,0,"Build database options:",2},
      90                 :             :         { 0,0,0,0,"Path Filtering and Ignore Policy:",4},
      91                 :             :         {"ignore",'e',"PCRE2_REGEXP",0,"Relative path to ignore. PCRE2 regular expressions could be used to specify "
      92                 :             :          "a pattern to ignore files or directories. Attention! All paths for the regular expression must be specified as relative. To understand what a relative path looks like, just run traverses without the "
      93                 :             :          BOLD "--ignore" RESET " option and look how the terminal will display "
      94                 :             :          "relative paths that are written to the database.\n"
      95                 :             :          "Example:\n"
      96                 :             :          BOLD APP_NAME " --ignore=\"^diff2/1/.*\" tests/fixtures/diffs" RESET "\n"
      97                 :             :          "In this example, the starting path for the traversing "
      98                 :             :          "is ./tests/fixtures/diffs and the relative path to ignore will "
      99                 :             :          "be ./tests/fixtures/diffs/diff2/1/ and all subdirectories (/.*).\n"
     100                 :             :          "Multiple regular expressions for ignore could be specified using many "
     101                 :             :          BOLD "--ignore" RESET " options at once.\n"
     102                 :             :          "Example:\n"
     103                 :             :          BOLD APP_NAME " --ignore=\"diff2/1/.*\" --ignore=\"diff2/2/.*\" tests/fixtures/diffs" RESET "\n"
     104                 :             :          "With " BOLD "--compare" RESET ", ignored paths are treated as out of scope for reported differences and equality summaries.",4 },
     105                 :             :         {"include",'i',"PCRE2_REGEXP",0,"Relative path to be included. PCRE2 regular expressions. Include these relative paths even if they were excluded via the " BOLD "--ignore" RESET " option. Multiple regular expressions could be specified. With " BOLD "--compare" RESET ", included paths are restored back into the reported comparison scope.",4 },
     106                 :             :         {"db-drop-ignored",'C',0,0,"The database is protected from accidental changes by default. The option " BOLD "--db-drop-ignored" RESET " must be specified additionally in order to remove from the database mention of files that matches the regular expression passed through the " BOLD "--ignore=PCRE2_REGEXP" RESET " option(s).",3},
     107                 :             :         {"db-clean-ignored",'C',0,OPTION_ALIAS | OPTION_HIDDEN,0,3}, // This legacy can be removed in 2036 (10-year Long-Term Support)
     108                 :             :         {"db-drop-inaccessible",'X',0,0,"Allow dropping database records for files that are inaccessible due to permission errors. By default, such paths are reported as \"inaccessible\" and their DB records are kept to avoid accidental loss when permissions change. This option is effective only with " BOLD "--update" RESET ".\n"
     109                 :             :          "Example:\n"
     110                 :             :          BOLD APP_NAME " --update --db-drop-inaccessible /mnt/storage" RESET,2},
     111                 :             :         {"drop-inaccessible",'X',0,OPTION_ALIAS | OPTION_HIDDEN,0,0}, // This legacy can be removed in 2036 (10-year Long-Term Support)
     112                 :             :         {"watch-timestamps",'T',0,0,"Also recalculate checksums when metadata change time (ctime) or allocated block count changes. By default, regular files are rehashed when their size or content modification time (mtime) changes.",0},
     113                 :             :         {"maxdepth",'m',"NUMBER",0,"Recursion depth limit. The depth of the traversal, numbered from 0 to N, where a file could be found. Representing the maximum of the starting point (from root) of the traversal. The root itself is numbered 0. " BOLD "--maxdepth=0" RESET " completely disable recursion.",0},
     114                 :             :         {"dry-run",'n',"MODE",OPTION_ARG_OPTIONAL,"Perform a trial run with no changes made. The option will not affect " BOLD "--compare" RESET ". "
     115                 :             :          "Supported mode: " BOLD "--dry-run=with-checksums" RESET " (read files and calculate checksums during dry run).",0},
     116                 :             :         {"one-file-system",'x',0,0,"This option prevents directory traversal from descending into directories that have a different device number than the file from which the descent began.",0 },
     117                 :             :         {"start-device-only",'o',0,OPTION_ALIAS | OPTION_HIDDEN,0,0}, // This legacy can be removed in 2036 (10-year Long-Term Support)
     118                 :             :         {"force",'f',0,0,"Use this option only in case when the PATHs that were written into the database as a result of the last scanning really need to be renewed. Warning! If this option will be used in incorrect way, information about files and their checksums against the database would be lost.",0},
     119                 :             :         {"update",'u',0,0,"Updates the database to reflect file system changes (new, modified and deleted files). Must be used with the same initial PATH that was used when creating the database, as existing records will be replaced with data from the specified location. This option modifies database consistency. Use with caution, especially in automated scripts, as incorrect usage may lead to loss of file checksums and metadata.",0 },
     120                 :             :         {"database",'d',"FILE",0,"Database filename. Defaults to ${HOST}.db, where HOST is the local hostname.",0 },
     121                 :             :         {"path",'P',"PATH",0,"Filesystem root to scan. May be repeated and combined with positional PATH arguments.",0},
     122                 :             :         {"check-level",'l',"FULL|QUICK",0,"Select database validation level: 'quick' for basic structure check, 'full' (default) for comprehensive integrity verification.",0 },
     123                 :             :         { 0,0,0,0,"Compare databases options:",1},
     124                 :             :         {"compare",'c',0,0,"Compare two databases from different sources. Requires two additional arguments specifying paths to database files, e.g.:\n" BOLD APP_NAME " --compare database1.db database2.db" RESET "\n"
     125                 :             :          "When combined with " BOLD "--ignore/--include" RESET ", the reported differences and equality summaries are limited to the filtered relative-path scope.",0 },
     126                 :             :         {"compare-filter",'F',"checksum-mismatch|first-source|second-source",0,
     127                 :             :          "Filter output categories for " BOLD "--compare" RESET ". "
     128                 :             :          "Supported values: "
     129                 :             :          BOLD "checksum-mismatch" RESET ", "
     130                 :             :          BOLD "first-source" RESET ", "
     131                 :             :          BOLD "second-source" RESET ". "
     132                 :             :          "The option can be specified multiple times in any combination.",0},
     133                 :             :         { 0,0,0,0,"Visualizations options:",-1},
     134                 :             :         {"silent",'s',0,0,"Don't produce any output. With " BOLD "--compare" RESET ", only paths with differences are shown, and category headings remain visible when multiple compare categories are active.",0 },
     135                 :             :         {"quiet-ignored",'q',0,0,"Suppress per-file log lines for paths filtered by " BOLD "--ignore/--include" RESET ". This helps keep program logs free of extra messages once ignore regular expressions are tuned and stable in use. Other warnings and errors remain visible.",0 },
     136                 :             :         {"verbose",'v',0,0,"Produce verbose output.",0 },
     137                 :             :         {"progress",'p',0,0,"Enabling this option displays progress information but requires an initial count of files and the space they occupy to estimate execution time. The program first traverses all specified directories, counting files, folders, and symlinks before proceeding with file analysis. This initial traversal may take a significant amount of time. It is strongly recommended not to use this option when calling the program from a script.",0 },
     138                 :             :         {"color",'S',"auto|always|never",0,"Choose when to apply color and text styling (-S means style). Use auto for terminal output, always to preserve styling in files and pipes, or never to disable it. The default is auto",-1},
     139                 :             :         {"config",'g',"FILE",0,"Read settings from FILE. Help, usage, and version requests ignore FILE. Otherwise, cannot be combined with other CLI arguments. With no CLI arguments, read " APP_NAME ".conf in the current directory if present",-1},
     140                 :             :         {"help",'h',0,0,"Give this help list",-1 },
     141                 :             :         {0,'?',0,OPTION_ALIAS,0,-1 },
     142                 :             :         {"usage",'z',0,0,"Give a short usage message",-1 },
     143                 :             :         {"version",'V',0,0,"Print program version",-1 },
     144                 :             :         {0}
     145                 :             : };
     146                 :             : 
     147                 :             : /**
     148                 :             :  * @brief Convert an argp parse error code into human-readable text
     149                 :             :  * @param parse_error Error code returned by argp_parse
     150                 :             :  * @return Pointer to a static descriptive string
     151                 :             :  */
     152                 :         132 : static const char *argp_error_to_text(const error_t parse_error)
     153                 :             : {
     154         [ +  + ]:         132 :         if(parse_error == EX_USAGE)
     155                 :             :         {
     156                 :          72 :                 return("command line usage error");
     157                 :             :         }
     158                 :             : 
     159         [ -  + ]:          60 :         if(parse_error == ARGP_ERR_UNKNOWN)
     160                 :             :         {
     161                 :           0 :                 return("unknown argument parsing error");
     162                 :             :         }
     163                 :             : 
     164                 :          60 :         const char *message = strerror(parse_error);
     165                 :             : 
     166   [ +  -  -  + ]:          60 :         if(message == NULL || message[0] == '\0')
     167                 :             :         {
     168                 :           0 :                 return("unknown error");
     169                 :             :         }
     170                 :             : 
     171                 :          60 :         return(message);
     172                 :             : }
     173                 :             : 
     174                 :             : /**
     175                 :             :  * @brief Report an option error with its configuration line when present
     176                 :             :  * @param state Parser or diagnostic state supplying the program name and error stream
     177                 :             :  * @param error_number Error number to append as system text, or zero
     178                 :             :  * @param format Printf-style diagnostic format without the ERROR: prefix
     179                 :             :  * @param ... Values referenced by the diagnostic format
     180                 :             :  */
     181                 :             : __attribute__((format(printf,3,4)))
     182                 :          67 : static void report_option_error(
     183                 :             :         const struct argp_state *state,
     184                 :             :         int                     error_number,
     185                 :             :         const char              *format,
     186                 :             :         ...)
     187                 :             : {
     188                 :          67 :         char *message = NULL;
     189                 :             :         va_list args;
     190                 :          67 :         va_start(args,format);
     191                 :          67 :         int message_length = vasprintf(&message,format,args);
     192                 :          67 :         va_end(args);
     193                 :             : 
     194                 :          67 :         const char *message_text = "Cannot format option diagnostic";
     195         [ +  - ]:          67 :         if(message_length >= 0)
     196                 :             :         {
     197                 :          67 :                 message_text = message;
     198                 :             :         } else {
     199                 :           0 :                 error_number = errno;
     200                 :             :         }
     201                 :             : 
     202         [ +  + ]:          67 :         if(config->config_file_error_line > 0)
     203                 :             :         {
     204                 :             :                 /* File errors retain the same program name and stream as CLI errors */
     205                 :          35 :                 argp_failure(state,0,error_number,"ERROR: Line %d in config file: %s",config->config_file_error_line,message_text);
     206                 :             :         } else {
     207                 :          32 :                 argp_failure(state,0,error_number,"ERROR: %s",message_text);
     208                 :             :         }
     209                 :             : 
     210         [ +  - ]:          67 :         if(message_length >= 0)
     211                 :             :         {
     212                 :          67 :                 free(message);
     213                 :             :         }
     214                 :          67 : }
     215                 :             : 
     216                 :             : /**
     217                 :             :  * @brief Apply a named option or handle an argp parser event
     218                 :             :  * @param key Current option key or argp event key
     219                 :             :  * @param arg Optional option value; file flags use NULL for true and "false" for false
     220                 :             :  * @param state CLI state with ArgumentParseContext in input, or a diagnostic state for file options
     221                 :             :  * @return EX_OK on success, EX_USAGE for invalid usage, or errno-style error code
     222                 :             :  */
     223                 :        8285 : error_t parse_opt(
     224                 :             :         int               key,
     225                 :             :         char              *arg,
     226                 :             :         struct argp_state *state)
     227                 :             : {
     228                 :             :         /* Points to the unparsed suffix after strtol() reads the number */
     229                 :        8285 :         char *number_suffix = NULL;
     230                 :             : 
     231                 :             :         /* Parsed --maxdepth value before range validation and conversion to short int */
     232                 :        8285 :         long int argument_value = -1;
     233                 :             : 
     234                 :             :         /* Both CLI options and configuration records reach this handler.
     235                 :             :            config_file_error_line is -1 for CLI calls. While applying a file entry,
     236                 :             :            read_config_entry() sets it to the current line for error reporting and
     237                 :             :            resets it afterward. A positive line also identifies a file value here.
     238                 :             : 
     239                 :             :            Before calling parse_opt(), read_config_entry() recognizes boolean
     240                 :             :            spellings without regard to letter case and converts them as follows:
     241                 :             :              true / yes / on / y / 1  -> arg is NULL, just like a CLI flag;
     242                 :             :              false / no / off / n / 0 -> arg points to the normalized string "false".
     243                 :             :            Thus, "0", "no", and the other false spellings never reach this comparison.
     244                 :             : 
     245                 :             :            The || conditions are evaluated from left to right:
     246                 :             :              1. A CLI call enables the flag regardless of arg. Optional arguments,
     247                 :             :                 such as --dry-run=MODE, are still validated in their switch branch.
     248                 :             :              2. A configuration call with arg == NULL enables the flag.
     249                 :             :              3. Otherwise, the normalized string "false" disables the flag.
     250                 :             :            Short-circuit evaluation prevents strcmp() from receiving a NULL pointer.
     251                 :             : 
     252                 :             :            dry-run also accepts the named mode "with-checksums", which reaches this
     253                 :             :            handler unchanged and enables the flag. Its branch validates the mode
     254                 :             :            and updates the checksum setting. In CLI, --dry-run=false still reaches
     255                 :             :            that validation and is rejected as an unsupported mode.
     256                 :             : 
     257                 :             :            Flag branches below assign this boolean to Config fields, so a later
     258                 :             :            false record can turn off a previously enabled flag. Options with ordinary
     259                 :             :            string or numeric arguments do not use this value */
     260   [ +  +  +  +  :        8285 :         const bool value = config->config_file_error_line < 0 || arg == NULL || strcmp(arg,"false") != 0;
                   +  + ]
     261                 :             : 
     262                 :             :         /* Reject argp-marked -- before accepting operands or checking required paths.
     263                 :             :            Error and cleanup events must not repeat the diagnostic */
     264   [ +  +  +  +  :        8285 :         if(state->quoted != 0 && (key == ARGP_KEY_ARG || key == ARGP_KEY_NO_ARGS || key == ARGP_KEY_END))
             +  +  -  + ]
     265                 :             :         {
     266                 :          34 :                 argp_failure(state,0,0,"ERROR: Unsupported use of --");
     267                 :          34 :                 return(EX_USAGE);
     268                 :             :         }
     269                 :             : 
     270   [ +  +  +  +  :        8251 :         switch(key)
          +  +  +  +  +  
          +  +  +  +  +  
          +  +  +  +  +  
          +  +  +  +  +  
             +  +  +  +  
                      + ]
     271                 :             :         {
     272                 :         528 :                 case 'd':
     273                 :             :                 {
     274                 :             :                         // Store full path to the DB file
     275         [ -  + ]:         528 :                         if(CRITICAL & m_copy_string(conf(db_primary_file_path),arg))
     276                 :             :                         {
     277                 :           0 :                                 report_option_error(state,ENOMEM,"Memory allocation for db_file_path failed");
     278                 :           0 :                                 return(ENOMEM);
     279                 :             :                         }
     280                 :             : 
     281                 :             :                         // Store only the DB file basename
     282                 :         528 :                         char *tmp = strdup(arg);
     283                 :             : 
     284         [ -  + ]:         528 :                         if(tmp == NULL)
     285                 :             :                         {
     286                 :           0 :                                 (void)m_del(conf(db_primary_file_path));
     287                 :           0 :                                 report_option_error(state,ENOMEM,"Memory allocation for db_file_name failed");
     288                 :           0 :                                 return(ENOMEM);
     289                 :             :                         }
     290                 :             : 
     291                 :         528 :                         const char *db_file_basename = basename(tmp);
     292                 :             : 
     293         [ -  + ]:         528 :                         if(db_file_basename == NULL)
     294                 :             :                         {
     295                 :           0 :                                 free(tmp);
     296                 :           0 :                                 (void)m_del(conf(db_primary_file_path));
     297                 :           0 :                                 report_option_error(state,0,"Failed to determine database base name from path '%s'",arg);
     298                 :           0 :                                 return(EINVAL);
     299                 :             :                         }
     300                 :             : 
     301         [ -  + ]:         528 :                         if(CRITICAL & m_copy_string(conf(db_file_name),db_file_basename))
     302                 :             :                         {
     303                 :           0 :                                 free(tmp);
     304                 :           0 :                                 (void)m_del(conf(db_primary_file_path));
     305                 :           0 :                                 report_option_error(state,ENOMEM,"Memory allocation for db_file_name failed");
     306                 :           0 :                                 return(ENOMEM);
     307                 :             :                         }
     308                 :             : 
     309                 :         528 :                         free(tmp);
     310                 :         528 :                         break;
     311                 :             :                 }
     312                 :         141 :                 case 'e':
     313                 :         141 :                         (void)add_string_to_array(&config->ignore,arg);
     314                 :         141 :                         break;
     315                 :         162 :                 case 'n':
     316                 :         162 :                         config->dry_run = value;
     317                 :             : 
     318                 :             :                         /* Each file value selects the complete dry-run mode */
     319         [ +  + ]:         162 :                         if(config->config_file_error_line > 0)
     320                 :             :                         {
     321                 :          57 :                                 config->dry_run_with_checksums = false;
     322                 :             :                         }
     323                 :             : 
     324   [ +  +  +  + ]:         162 :                         if(value == true && arg != NULL)
     325                 :             :                         {
     326         [ +  + ]:          23 :                                 if(0 == strcasecmp(arg,"with-checksums"))
     327                 :             :                                 {
     328                 :          18 :                                         config->dry_run_with_checksums = true;
     329                 :             : 
     330                 :             :                                 } else {
     331                 :           5 :                                         report_option_error(state,0,"Unsupported --dry-run mode '%s'. Supported mode: with-checksums. See --help for more information",arg);
     332                 :           5 :                                         return(EINVAL);
     333                 :             :                                 }
     334                 :             :                         }
     335                 :         157 :                         break;
     336                 :          65 :                 case 'i':
     337                 :          65 :                         (void)add_string_to_array(&config->include,arg);
     338                 :             :                         // Track that at least one --include pattern was provided
     339                 :          65 :                         config->include_specified = true;
     340                 :          65 :                         break;
     341                 :         128 :                 case 'k':
     342                 :         128 :                         (void)add_string_to_array(&config->lock_checksum,arg);
     343                 :         128 :                         break;
     344                 :          43 :                 case 'r':
     345                 :          43 :                         config->rehash_locked = value;
     346                 :          43 :                         break;
     347                 :         158 :                 case 'c':
     348                 :         158 :                         config->compare = value;
     349                 :         158 :                         break;
     350                 :         137 :                 case 'F':
     351   [ +  -  +  + ]:         137 :                         if(arg != NULL && 0 == strcmp(arg,"checksum-mismatch"))
     352                 :             :                         {
     353                 :          43 :                                 config->compare_filter |= CF_CHECKSUM_MISMATCH;
     354   [ +  -  +  + ]:          94 :                         } else if(arg != NULL && 0 == strcmp(arg,"first-source")){
     355                 :          37 :                                 config->compare_filter |= CF_FIRST_SOURCE;
     356   [ +  -  +  + ]:          57 :                         } else if(arg != NULL && 0 == strcmp(arg,"second-source")){
     357                 :          36 :                                 config->compare_filter |= CF_SECOND_SOURCE;
     358                 :             :                         } else {
     359                 :          21 :                                 report_option_error(state,0,"Unsupported --compare-filter value '%s'. Supported values: checksum-mismatch, first-source, second-source. See --help for more information",arg);
     360                 :          21 :                                 return(EINVAL);
     361                 :             :                         }
     362                 :         116 :                         break;
     363                 :          32 :                 case 'o': // This legacy can be removed in 2036 (10-year Long-Term Support)
     364                 :             :                 case 'x':
     365                 :          32 :                         config->one_file_system = value;
     366                 :          32 :                         break;
     367                 :          52 :                 case 'C':
     368                 :          52 :                         config->db_drop_ignored = value;
     369                 :          52 :                         break;
     370                 :          51 :                 case 'X':
     371                 :          51 :                         config->db_drop_inaccessible = value;
     372                 :          51 :                         break;
     373                 :          22 :                 case 'm':
     374                 :          22 :                         argument_value = strtol(arg,&number_suffix,10);
     375                 :             : 
     376                 :             :                         // Accept only non-negative integer values that fit into short int
     377                 :             :                         // and reject strings with trailing non-numeric characters
     378   [ +  +  +  +  :          22 :                         if(number_suffix != arg && argument_value >= 0 && argument_value <= 32767 && *number_suffix == '\0')
             +  +  +  - ]
     379                 :             :                         {
     380                 :          12 :                                 config->maxdepth = (short int)argument_value;
     381                 :             :                         } else {
     382                 :          10 :                                 report_option_error(state,0,"Wrong --maxdepth (-m) value. Should be an integer from 0 to 32767. See --help for more information");
     383                 :          10 :                                 return(EINVAL);
     384                 :             :                         }
     385                 :          12 :                         break;
     386                 :         134 :                 case 'p':
     387                 :         134 :                         config->progress = value;
     388                 :             :                         /*
     389                 :             :                          * Progress output can push important warning lines out of the visible terminal area.
     390                 :             :                          * Set final replay together with progress before traversal starts so warnings can be shown again near exit
     391                 :             :                          */
     392                 :         134 :                         config->show_remembered_messages_at_exit = value;
     393                 :         134 :                         break;
     394                 :          52 :                 case 'T':
     395                 :          52 :                         config->watch_timestamps = value;
     396                 :          52 :                         break;
     397                 :         242 :                 case 'u':
     398                 :         242 :                         config->update = value;
     399                 :         242 :                         break;
     400                 :          28 :                 case 'f':
     401                 :          28 :                         config->force = value;
     402                 :          28 :                         break;
     403                 :         345 :                 case 'l':
     404         [ +  + ]:         345 :                         if(0 == strcasecmp(arg,"QUICK"))
     405                 :             :                         {
     406                 :         341 :                                 config->db_check_level = QUICK;
     407         [ +  + ]:           4 :                         } else if(0 == strcasecmp(arg,"FULL")){
     408                 :           2 :                                 config->db_check_level = FULL;
     409                 :             :                         } else {
     410                 :           2 :                                 report_option_error(state,0,"Unsupported --check-level value '%s'. Supported values: FULL or QUICK",arg);
     411                 :           2 :                                 return(EINVAL);
     412                 :             :                         }
     413                 :         343 :                         break;
     414                 :         112 :                 case 's':
     415                 :             :                         // Set global logger mode
     416         [ +  + ]:         112 :                         if(value == true)
     417                 :             :                         {
     418                 :          99 :                                 rational_logger_mode = SILENT;
     419         [ +  + ]:          13 :                         } else if(rational_logger_mode == SILENT){
     420                 :             :                                 /* Restore the startup mode only when disabling the active output mode */
     421                 :           1 :                                 rational_logger_mode = REGULAR;
     422                 :             : 
     423                 :             : #ifdef TESTITALL_TEST_HOOKS
     424                 :           1 :                                 const char *testing = getenv("TESTING");
     425                 :             : 
     426   [ +  -  +  - ]:           1 :                                 if(testing != NULL && strcasecmp(testing,"true") == 0)
     427                 :             :                                 {
     428                 :           1 :                                         rational_logger_mode = TESTING;
     429                 :             :                                 }
     430                 :             : #endif
     431                 :             :                         }
     432                 :         112 :                         break;
     433                 :          28 :                 case 'q':
     434                 :          28 :                         config->quiet_ignored = value;
     435                 :          28 :                         break;
     436                 :          45 :                 case 'v':
     437                 :          45 :                         config->verbose = value;
     438                 :             : 
     439                 :             :                         // Set global logger mode
     440         [ +  + ]:          45 :                         if(value == true)
     441                 :             :                         {
     442                 :          31 :                                 rational_logger_mode = VERBOSE;
     443         [ +  + ]:          14 :                         } else if(rational_logger_mode == VERBOSE){
     444                 :             :                                 /* Restore the startup mode only when disabling the active output mode */
     445                 :           7 :                                 rational_logger_mode = REGULAR;
     446                 :             : 
     447                 :             : #ifdef TESTITALL_TEST_HOOKS
     448                 :           7 :                                 const char *testing = getenv("TESTING");
     449                 :             : 
     450   [ +  -  +  - ]:           7 :                                 if(testing != NULL && strcasecmp(testing,"true") == 0)
     451                 :             :                                 {
     452                 :           7 :                                         rational_logger_mode = TESTING;
     453                 :             :                                 }
     454                 :             : #endif
     455                 :             :                         }
     456                 :          45 :                         break;
     457                 :          59 :                 case 'S':
     458         [ +  + ]:          59 :                         if(0 == strcmp(arg,"auto"))
     459                 :             :                         {
     460                 :          13 :                                 rational_color_mode = COLOR_MODE_AUTO;
     461         [ +  + ]:          46 :                         } else if(0 == strcmp(arg,"always")){
     462                 :          16 :                                 rational_color_mode = COLOR_MODE_ALWAYS;
     463         [ +  + ]:          30 :                         } else if(0 == strcmp(arg,"never")){
     464                 :          14 :                                 rational_color_mode = COLOR_MODE_NEVER;
     465                 :             :                         } else {
     466                 :          16 :                                 report_option_error(state,0,"Unsupported --color value '%s'. Supported values: auto, always, or never",arg);
     467                 :          16 :                                 return(EINVAL);
     468                 :             :                         }
     469                 :          43 :                         break;
     470                 :          56 :                 case 'h':
     471                 :             :                 case '?':
     472                 :          56 :                         information_mode_requested = true;
     473                 :          56 :                         argp_state_help(state,state->out_stream,ARGP_HELP_STD_HELP & ~(ARGP_HELP_EXIT_OK | ARGP_HELP_EXIT_ERR));
     474                 :          56 :                         break;
     475                 :          61 :                 case 'V':
     476                 :          61 :                         information_mode_requested = true;
     477                 :          61 :                         break;
     478                 :          34 :                 case 'z':
     479                 :          34 :                         information_mode_requested = true;
     480                 :          34 :                         argp_state_help(state,state->out_stream,ARGP_HELP_USAGE);
     481                 :          34 :                         break;
     482                 :         214 :                 case 'g':
     483                 :             :                         /* Source selection and file reading precede this main pass */
     484                 :         214 :                         break;
     485                 :         305 :                 case ARGP_KEY_NO_ARGS:
     486                 :             :                         /* Help, usage, and version requests do not require paths */
     487         [ +  + ]:         305 :                         if(information_mode_requested == true)
     488                 :             :                         {
     489                 :         121 :                                 break;
     490                 :             :                         }
     491                 :             : 
     492                 :             :                         /* Argp sends this event after parsing options if no positional
     493                 :             :                            CLI arguments were processed. For example, --path=/mnt/data stores
     494                 :             :                            a root in Config.roots without supplying a positional argument.
     495                 :             :                            Configuration entries can also supply paths before this pass.
     496                 :             :                            Continue when paths for the selected mode are already present;
     497                 :             :                            ARGP_KEY_END checks that comparison has exactly two database paths.
     498                 :             :                            Scan roots cannot satisfy the comparison path requirement */
     499   [ +  +  +  + ]:         184 :                         if((config->compare == false && config->roots.length != 0) ||
     500   [ +  +  +  + ]:          46 :                            (config->compare == true && config->db_file_paths.length != 0))
     501                 :             :                         {
     502                 :             :                                 break;
     503                 :             :                         }
     504                 :             : 
     505                 :             :                         /* A bare invocation displays usage only when the optional file has
     506                 :             :                            no settings. A false flag still counts as a configuration entry */
     507   [ +  +  +  + ]:          18 :                         if(state->argc == 1 && ((const ArgumentParseContext *)state->input)->config_has_entries == false)
     508                 :             :                         {
     509                 :           6 :                                 information_mode_requested = true;
     510                 :           6 :                                 argp_usage(state);
     511                 :           6 :                                 break;
     512                 :             :                         }
     513                 :             : 
     514                 :             :                         /* Options were supplied, but the paths required for this mode are missing */
     515         [ +  + ]:          12 :                         if(config->compare == false)
     516                 :             :                         {
     517                 :             :                                 /* Explain how to supply scan roots in the selected settings source */
     518                 :          10 :                                 const char *path_hint = "Specify a directory as a positional argument or with --path=PATH";
     519         [ +  + ]:          10 :                                 if(config->config_file_path != NULL)
     520                 :             :                                 {
     521                 :           8 :                                         path_hint = "Add 'path = /path/to/directory' to the configuration file";
     522                 :             :                                 }
     523                 :          10 :                                 argp_failure(state,0,0,"ERROR: At least one scan path is required. %s",path_hint);
     524                 :             :                         }
     525                 :          12 :                         argp_usage(state);
     526                 :          12 :                         return(EX_USAGE);
     527                 :             :                         break;
     528                 :        1087 :                 case 'P':
     529                 :             :                 case ARGP_KEY_ARG:
     530                 :             :                 {
     531                 :             :                         /* Status returned by the path append operation.
     532                 :             :                            Assigned in either branch below before the shared result check */
     533                 :             :                         Return status;
     534                 :             : 
     535                 :             :                         /*
     536                 :             :                          * This branch receives a --path value or one positional argument.
     537                 :             :                          * Both cases share the empty-path check and append to Config.roots
     538                 :             :                          * when handling scan roots.
     539                 :             :                          * Argp normally processes options before positional arguments, so
     540                 :             :                          * `--compare` has selected the intended mode when a positional
     541                 :             :                          * argument arrives.
     542                 :             :                          * The configuration reader also uses ARGP_KEY_ARG for each compare
     543                 :             :                          * entry after enabling comparison and rejecting an empty path.
     544                 :             :                          *
     545                 :             :                          * In compare mode, positional arguments are database file paths.
     546                 :             :                          * Named paths and positional arguments in scan mode are filesystem
     547                 :             :                          * roots. Named roots are cleared before comparison
     548                 :             :                          */
     549   [ +  +  +  + ]:        1087 :                         if(key == ARGP_KEY_ARG && config->compare == true)
     550                 :             :                         {
     551                 :         339 :                                 status = m_string_array_append(conf(db_file_paths),char,arg);
     552                 :             :                         } else {
     553         [ +  + ]:         748 :                                 if(arg[0] == '\0')
     554                 :             :                                 {
     555                 :          13 :                                         report_option_error(state,0,"Filesystem root path must not be empty. See --help for more information");
     556                 :          13 :                                         return(EINVAL);
     557                 :             :                                 }
     558                 :             : 
     559                 :         735 :                                 status = m_string_array_append(conf(roots),char,arg);
     560                 :             :                         }
     561                 :             : 
     562         [ -  + ]:        1074 :                         if((TRIUMPH & status) == 0)
     563                 :             :                         {
     564                 :             :                                 /* Paths can come from CLI or a configuration file. The shared error
     565                 :             :                                    handler includes the line number for paths supplied by the file */
     566         [ #  # ]:           0 :                                 if(key == 'P')
     567                 :             :                                 {
     568                 :           0 :                                         report_option_error(state,ENOMEM,"Memory allocation for --path failed");
     569                 :             :                                 } else {
     570                 :           0 :                                         report_option_error(state,ENOMEM,"Memory allocation for positional argument failed");
     571                 :             :                                 }
     572                 :           0 :                                 return(ENOMEM);
     573                 :             :                         }
     574                 :        1074 :                         break;
     575                 :             :                 }
     576                 :         903 :                 case ARGP_KEY_END:
     577         [ +  + ]:         903 :                         if(information_mode_requested == true)
     578                 :             :                         {
     579                 :         137 :                                 break;
     580                 :             :                         }
     581                 :             : 
     582   [ +  +  +  + ]:         766 :                         if(config->compare_filter != CF_NONE_SPECIFIED && config->compare == false)
     583                 :             :                         {
     584                 :          16 :                                 argp_failure(state,0,0,"ERROR: --compare-filter can only be used together with --compare. See --help for more information");
     585                 :          16 :                                 return(EX_USAGE);
     586                 :             :                         }
     587                 :             : 
     588         [ +  + ]:         750 :                         if(config->compare == true)
     589                 :             :                         {
     590         [ +  + ]:         158 :                                 if(config->db_file_paths.length < 2)
     591                 :             :                                 {
     592                 :           6 :                                         argp_failure(state,0,0,"ERROR: Too few arguments. --compare require two arguments with paths to database files. See --help for more information");
     593                 :           6 :                                         return(EX_USAGE);
     594         [ +  + ]:         152 :                                 } else if(config->db_file_paths.length > 2){
     595                 :           4 :                                         argp_failure(state,0,0,"ERROR: Too many arguments. --compare require just two arguments with paths to database files. See --help for more information");
     596                 :           4 :                                         return(EX_USAGE);
     597                 :             :                                 }
     598                 :             :                         }
     599                 :             : 
     600   [ +  +  -  + ]:         740 :                         if(config->db_drop_inaccessible == true && config->update == false)
     601                 :             :                         {
     602                 :           0 :                                 argp_failure(state,0,0,"WARNING: --db-drop-inaccessible has no effect without --update; records for inaccessible paths will be kept in the database");
     603                 :             :                         }
     604                 :             : 
     605   [ +  +  -  + ]:         740 :                         if(config->rehash_locked == true && config->lock_checksum == NULL)
     606                 :             :                         {
     607                 :           0 :                                 argp_failure(state,0,0,"WARNING: --rehash-locked has no effect without --lock-checksum");
     608                 :             :                         }
     609                 :         740 :                         break;
     610                 :        3027 :                 default:
     611                 :        3027 :                         return(ARGP_ERR_UNKNOWN);
     612                 :             :         }
     613                 :             : 
     614                 :        5119 :         return(EX_OK);
     615                 :             : }
     616                 :             : 
     617                 :             : /* argp parser definition */
     618                 :             : static struct argp argp = {
     619                 :             :         options,parse_opt,args_doc,doc,0,0,0
     620                 :             : };
     621                 :             : 
     622                 :             : /**
     623                 :             :  * @brief Select and parse one settings source, then finalize configuration state
     624                 :             :  *
     625                 :             :  * In normal scan mode, --path values and positional arguments become roots. In
     626                 :             :  * `--compare` mode, the two positional arguments become database paths and
     627                 :             :  * their file names are used in user-facing messages
     628                 :             :  *
     629                 :             :  * init_config() must initialize the configuration and global output settings
     630                 :             :  * before this function applies settings. A preliminary argp pass selects a
     631                 :             :  * file or CLI-only parsing. File settings are applied before the main argp
     632                 :             :  * pass performs the shared final checks; file and CLI settings are never mixed
     633                 :             :  *
     634                 :             :  * For example, `precizer src tests` creates two traversal roots, while
     635                 :             :  * `precizer --compare first.db second.db` creates two compare database paths
     636                 :             :  *
     637                 :             :  * @param argc Number of CLI arguments
     638                 :             :  * @param argv CLI argument vector
     639                 :             :  * @return SUCCESS for normal mode, INFO|HALTED for informational mode, or failure flags on errors
     640                 :             :  */
     641                 :        1158 : Return parse_arguments(
     642                 :             :         const int argc,
     643                 :             :         char      *argv[])
     644                 :             : {
     645                 :             :         /* Status returned by this function through provide()
     646                 :             :            Default value assumes successful completion */
     647                 :        1158 :         Return status = SUCCESS;
     648                 :             : 
     649                 :             :         /* Return errors without exiting and leave informational options
     650                 :             :            to the application's handlers */
     651                 :        1158 :         unsigned int parse_flags = ARGP_NO_EXIT | ARGP_NO_HELP;
     652                 :             : 
     653                 :             :         /* Reset informational-mode and source-selection state for this invocation */
     654                 :        1158 :         information_mode_requested = false;
     655                 :        1158 :         config->config_file_path = NULL;
     656                 :             : 
     657                 :             :         /* Local source-selection state shared by both argp passes and file reading.
     658                 :             :            The preliminary pass stores the program name and error stream in its
     659                 :             :            embedded diagnostic_state for diagnostics between passes */
     660                 :        1158 :         ArgumentParseContext context = {0};
     661                 :             : 
     662                 :             :         /*
     663                 :             :          * Stage 1: Preliminary CLI parsing and source selection
     664                 :             :          *
     665                 :             :          * Identify the config path, other arguments, and information requests to
     666                 :             :          * choose a settings source without applying working options
     667                 :             :          */
     668                 :             : 
     669                 :             :         /* Copy the parser descriptor; the option table and help text remain shared.
     670                 :             :            Changing the callback below affects only this copy, so the main parser
     671                 :             :            keeps parse_opt() as its handler */
     672                 :        1158 :         struct argp preliminary_argp = argp;
     673                 :             : 
     674                 :             :         /* Collect the config path and record other arguments and information requests.
     675                 :             :            This callback does not apply working options such as --progress or --database */
     676                 :        1158 :         preliminary_argp.parser = parse_source_option;
     677                 :             : 
     678                 :             :         /* Scan CLI arguments without printing diagnostics or exiting on errors.
     679                 :             :            The callback updates context and config->config_file_path. The return code lets
     680                 :             :            source selection discard incomplete results; the main pass reports CLI
     681                 :             :            errors and handles help, usage, and version output */
     682                 :        1158 :         error_t preliminary_error = argp_parse(&preliminary_argp,argc,argv,ARGP_SILENT,0,&context);
     683                 :             : 
     684                 :             :         /* A conflict stops before reading. Information requests and preliminary
     685                 :             :            syntax errors select no file; output stays in the main CLI parser */
     686                 :        1158 :         run(select_settings_source(argc,preliminary_error,&context));
     687                 :             : 
     688                 :             :         /*
     689                 :             :          * Stage 2: Configuration file parsing
     690                 :             :          *
     691                 :             :          * Read a file only when source selection succeeded and selected a path.
     692                 :             :          * config_required makes a missing explicitly selected file an error, while
     693                 :             :          * a missing default file is allowed. Reading applies settings to Config and
     694                 :             :          * records recognized entries in config_has_entries, including false flags,
     695                 :             :          * so the main pass can distinguish an empty file from incomplete settings
     696                 :             :          */
     697                 :             : 
     698   [ +  +  +  + ]:        1158 :         if(SUCCESS == status && config->config_file_path != NULL)
     699                 :             :         {
     700                 :         185 :                 run(read_config_file(config->config_file_path,context.config_required,&context.config_has_entries,&context.diagnostic_state));
     701                 :             :         }
     702                 :             : 
     703         [ +  + ]:        1158 :         if(CRITICAL & status)
     704                 :             :         {
     705                 :         149 :                 provide(status);
     706                 :             :         }
     707                 :             : 
     708                 :             :         /*
     709                 :             :          * Stage 3: Main CLI parsing and final validation
     710                 :             :          *
     711                 :             :          * Route parser events through parse_opt() to apply CLI settings when CLI
     712                 :             :          * is the selected source, or finalize settings already read from the file.
     713                 :             :          * Report CLI errors and handle help, usage, and version requests here.
     714                 :             :          * File settings and CLI working options are never combined
     715                 :             :          */
     716                 :             : 
     717                 :        1009 :         error_t parse_error = argp_parse(&argp,argc,argv,parse_flags,0,&context);
     718                 :             : 
     719                 :             :         /* Even if a transient preliminary error does not recur, it must prevent
     720                 :             :            the operation from starting after source selection was abandoned */
     721   [ +  +  -  + ]:        1009 :         if(parse_error == EX_OK && preliminary_error != EX_OK)
     722                 :             :         {
     723                 :           0 :                 parse_error = preliminary_error;
     724                 :             :         }
     725                 :             : 
     726         [ +  + ]:        1009 :         if(parse_error != EX_OK)
     727                 :             :         {
     728                 :         132 :                 status = FAILURE;
     729         [ +  + ]:         877 :         } else if(information_mode_requested == true){
     730                 :         137 :                 provide(INFO | HALTED);
     731                 :             :         }
     732                 :             : 
     733                 :             :         /* Discard scan roots before comparison path handling and diagnostics */
     734   [ +  +  +  + ]:         872 :         if((SUCCESS & status) && config->compare == true)
     735                 :             :         {
     736                 :         148 :                 run(m_array_del(conf(roots)));
     737                 :             :         }
     738                 :             : 
     739                 :             :         /*
     740                 :             :          * Normal scan mode stores named and positional paths as traversal roots.
     741                 :             :          * Normalize those roots after argp has finished collecting them, so later
     742                 :             :          * code can compare paths without treating `dir` and `dir/` as different
     743                 :             :          * user input
     744                 :             :          */
     745   [ +  +  +  +  :         872 :         if((SUCCESS & status) && config->compare == false && config->roots.length != 0)
                   +  - ]
     746                 :             :         {
     747   [ +  +  +  + ]:        1800 :                 m_string_array_foreach(conf(roots),root)
     748                 :             :                 {
     749                 :         616 :                         run(remove_trailing_slash(root));
     750                 :             : 
     751         [ -  + ]:         616 :                         if((SUCCESS & status) == 0)
     752                 :             :                         {
     753                 :           0 :                                 break;
     754                 :             :                         }
     755                 :             :                 }
     756                 :             :         }
     757                 :             : 
     758                 :             :         /*
     759                 :             :          * Compare mode stores positional arguments as database file paths instead
     760                 :             :          * of traversal roots. Normalize each database path, then keep its basename
     761                 :             :          * separately so compare output can show friendly source names like
     762                 :             :          * `first.db` and `second.db`
     763                 :             :          */
     764   [ +  +  +  + ]:         872 :         if((SUCCESS & status) && config->compare == true)
     765                 :             :         {
     766         [ +  - ]:         148 :                 if(config->db_file_paths.length != 0)
     767                 :             :                 {
     768   [ +  +  +  + ]:         592 :                         m_string_array_foreach(conf(db_file_paths),db_file_path)
     769                 :             :                         {
     770                 :         296 :                                 run(remove_trailing_slash(db_file_path));
     771                 :             : 
     772         [ -  + ]:         296 :                                 if((SUCCESS & status) == 0)
     773                 :             :                                 {
     774                 :           0 :                                         break;
     775                 :             :                                 }
     776                 :             : 
     777                 :         296 :                                 const char *db_file_path_text = m_text(db_file_path);
     778                 :             : 
     779                 :             :                                 // Duplicate the path because basename may modify the buffer
     780                 :         296 :                                 char *tmp = strdup(db_file_path_text);
     781                 :             : 
     782         [ -  + ]:         296 :                                 if(tmp == NULL)
     783                 :             :                                 {
     784                 :           0 :                                         report("Failed to duplicate string: %s",db_file_path_text);
     785                 :           0 :                                         status = FAILURE;
     786                 :           0 :                                         break;
     787                 :             :                                 }
     788                 :             : 
     789                 :             :                                 // Resolve basename and handle NULL result
     790                 :         296 :                                 const char *db_file_basename = basename(tmp);
     791                 :             : 
     792         [ -  + ]:         296 :                                 if(db_file_basename == NULL)
     793                 :             :                                 {
     794                 :           0 :                                         report("basename failed for path: %s",tmp);
     795                 :           0 :                                         free(tmp);
     796                 :           0 :                                         status = FAILURE;
     797                 :           0 :                                         break;
     798                 :             :                                 }
     799                 :             : 
     800                 :         296 :                                 status = add_string_to_array(&config->db_file_names,db_file_basename);
     801                 :         296 :                                 free(tmp);
     802                 :             : 
     803         [ -  + ]:         296 :                                 if((SUCCESS & status) == 0)
     804                 :             :                                 {
     805                 :           0 :                                         break;
     806                 :             :                                 }
     807                 :             :                         }
     808                 :             :                 }
     809                 :             :         }
     810                 :             : 
     811         [ +  + ]:         872 :         if(parse_error != 0)
     812                 :             :         {
     813                 :         132 :                 slog(ERROR,"Argument parsing failed with code %d (%s)\n",parse_error,argp_error_to_text(parse_error));
     814                 :         132 :                 status = FAILURE;
     815                 :             :         }
     816                 :             : 
     817         [ +  + ]:         872 :         if(CRITICAL & status)
     818                 :             :         {
     819                 :         132 :                 provide(status);
     820                 :             :         }
     821                 :             : 
     822                 :             : #ifdef TESTITALL_TEST_HOOKS
     823                 :             :         /* Show key-value diagnostics consumed by test templates */
     824                 :         740 :         show_testing_diagnostics();
     825                 :             : #endif
     826                 :             : 
     827                 :             :         /* Show compact configuration summary for verbose user output */
     828                 :         740 :         show_verbose_diagnostics();
     829                 :             : 
     830                 :         740 :         slog(TRACE,"Argument parsing is finished\n");
     831                 :             : 
     832                 :         740 :         provide(status);
     833                 :             : }
        

Generated by: LCOV version 2.0-1