Branch data Line data Source code
1 : : #include "precizer.h"
2 : : #include <sysexits.h>
3 : :
4 : : /**
5 : : * @brief Parse command-line arguments and populate global runtime configuration
6 : : */
7 : :
8 : : /* Program documentation text used by argp */
9 : : static char doc[] =
10 : : "\nVerify file checksums at scale\n\n"
11 : : BOLD APP_NAME RESET " is a lightweight and blazing-fast CLI application designed for file integrity verification and comparison, making it particularly useful for checking synchronization results. The program recursively traverses directories, generating a database of files and their checksums for quick and efficient comparisons.\n"
12 : : "\n"
13 : : "Built for both embedded platforms and large-scale clustered mainframes, " BOLD APP_NAME RESET " helps detect synchronization errors by comparing files and their checksums across different sources. It can also be used to analyze historical changes by comparing databases generated at different points in time from the same source.\n"
14 : : "\n"
15 : : "With love for Ukraine\n"
16 : : "\vSIMPLE EXAMPLE\n"
17 : : "\n"
18 : : "Use this workflow to verify that two mounted directory trees are equivalent.\n"
19 : : "\n"
20 : : "Assume the source trees are mounted at " YELLOW "/mnt1" RESET " and " YELLOW "/mnt2" RESET ".\n"
21 : : "\n"
22 : : "1. On machine " YELLOW "'host1'" RESET ", run:\n"
23 : : "\n"
24 : : " " BOLDGREEN "$ " APP_NAME " --progress /mnt1" RESET "\n"
25 : : "\n"
26 : : "This traversal scans " YELLOW "/mnt1" RESET " recursively and creates " YELLOW "host1.db" RESET " in the current directory.\n"
27 : : "The " BOLD "--progress" RESET " option reports processed data volume and file count.\n"
28 : : "\n"
29 : : "2. On machine " YELLOW "'host2'" RESET ", run:\n"
30 : : "\n"
31 : : " " BOLDGREEN "$ " APP_NAME " --progress /mnt2" RESET "\n"
32 : : "\n"
33 : : "This creates " YELLOW "host2.db" RESET " in the current directory.\n"
34 : : "\n"
35 : : "3. Copy " YELLOW "host1.db" RESET " and " YELLOW "host2.db" RESET " to one machine, then run:\n"
36 : : "\n"
37 : : " " BOLDGREEN "$ " APP_NAME " --compare host1.db host2.db" RESET "\n"
38 : : "\n"
39 : : "Output reports:\n"
40 : : "\n"
41 : : "* Files present on " YELLOW "'host1'" RESET " but missing on " YELLOW "'host2'" RESET ", and vice versa.\n"
42 : : "* Files present on both hosts whose SHA512 checksums do not match.\n"
43 : : "\n"
44 : : "Database paths are stored as relative paths only.\n"
45 : : "For example, " YELLOW "/mnt1/abc/def/aaa.txt" RESET " is stored as " YELLOW "abc/def/aaa.txt" RESET ". "
46 : : "The same relative-path rule applies to " YELLOW "/mnt2/abc/def/aaa.txt" RESET ", enabling direct cross-source comparison.\n"
47 : : "\n"
48 : : "See the project README for additional technical details.";
49 : :
50 : : /* Positional-argument description for argp */
51 : : static char args_doc[] = "PATH";
52 : :
53 : : static bool information_mode_requested = false;
54 : :
55 : : /* Supported command-line options for argp */
56 : : static struct argp_option options[] = {
57 : : { 0,0,0,0,"Locked Checksum Protection:",3},
58 : : {"lock-checksum",'k',"PCRE2_REGEXP",0,"Relative path to be treated as immutable archival data. PCRE2 regular expressions can be used to "
59 : : "select files or directories whose checksums are written once to the database and never updated "
60 : : "again. If no matching files exist in the database yet, their entries and checksums will still be "
61 : : "created normally when they are scanned for the first time. All paths for the regular expression "
62 : : "must be specified as relative, the same way as for the "
63 : : BOLD "--ignore" RESET " option. For these entries, the "
64 : : BOLD "--update" RESET " option will not recalculate checksums; any difference in file size, timestamps, or content will "
65 : : "always be reported as data corruption instead of a reason to generate a new checksum. Multiple "
66 : : "regular expressions can be specified using multiple "
67 : : BOLD "--lock-checksum" RESET " options.\n"
68 : : "Example:\n"
69 : : BOLD APP_NAME " --update --lock-checksum=\"^archive/2077/.*\" /mnt/storage" RESET "\n",0},
70 : : {"rehash-locked",'r',0,0,"Force a full SHA512 rehash for every file that is already stored "
71 : : "in the database and protected by any "
72 : : BOLD "--lock-checksum" RESET " pattern. "
73 : : "This option must always be used together with "
74 : : BOLD "--lock-checksum" RESET "; it has no effect otherwise. During an update pass, "
75 : : "every locked entry found on disk is read again, its checksum is recomputed, and the "
76 : : "result is compared with the value stored in the database. This provides an extra "
77 : : "validation layer for immutable archives at the cost of additional I/O and CPU time, "
78 : : "ensuring that frozen records are validated even when file size or timestamps alone are "
79 : : "not sufficient indicators. "
80 : : "The option purposefully ignores "
81 : : BOLD "--watch-timestamps" RESET " — timestamp tracking can be on or off, the rehash will "
82 : : "still take place. If the recomputed checksum and file size match the database entry, "
83 : : "the record stays consistent but differing ctime/mtime values in the database are "
84 : : "synchronized with the on-disk timestamps. If the checksum differs, the file is "
85 : : "reported as corrupted just like any other locked entry.\n"
86 : : "Example:\n"
87 : : BOLD APP_NAME " --update --lock-checksum=\"^archive/2024/.*\" --rehash-locked /mnt/storage" RESET "\n",0},
88 : : { 0,0,0,0,"Build database options:",2},
89 : : { 0,0,0,0,"Path Filtering and Ignore Policy:",4},
90 : : {"ignore",'e',"PCRE2_REGEXP",0,"Relative path to ignore. PCRE2 regular expressions could be used to specify "
91 : : "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 "
92 : : BOLD "--ignore" RESET " option and look how the terminal will display "
93 : : "relative paths that are written to the database.\n"
94 : : "Example:\n"
95 : : BOLD APP_NAME " --ignore=\"^diff2/1/.*\" tests/fixtures/diffs" RESET "\n"
96 : : "In this example, the starting path for the traversing "
97 : : "is ./tests/fixtures/diffs and the relative path to ignore will "
98 : : "be ./tests/fixtures/diffs/diff2/1/ and all subdirectories (/.*).\n"
99 : : "Multiple regular expressions for ignore could be specified using many "
100 : : BOLD "--ignore" RESET " options at once.\n"
101 : : "Example:\n"
102 : : BOLD APP_NAME " --ignore=\"diff2/1/.*\" --ignore=\"diff2/2/.*\" tests/fixtures/diffs" RESET "\n",4 },
103 : : {"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.\n",4 },
104 : : {"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).\n",3},
105 : : {"db-clean-ignored",'C',0,OPTION_ALIAS | OPTION_HIDDEN,0,3}, // This legacy can be removed in 2036 (10-year Long-Term Support)
106 : : {"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"
107 : : "Example:\n"
108 : : BOLD APP_NAME " --update --db-drop-inaccessible /mnt/storage" RESET "\n",2},
109 : : {"drop-inaccessible",'X',0,OPTION_ALIAS | OPTION_HIDDEN,0,0}, // This legacy can be removed in 2036 (10-year Long-Term Support)
110 : : {"watch-timestamps",'T',0,0,"Consider file metadata changes (creation and modification timestamps) in addition to file size when detecting changes. By default, only file size changes trigger rescanning. When this option is enabled, any changes to file timestamps or size will cause the file to be rescanned and its checksum updated in the primary database.\n",0},
111 : : {"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.\n",0},
112 : : {"dry-run",'n',"MODE",OPTION_ARG_OPTIONAL,"Perform a trial run with no changes made. The option will not affect " BOLD "--compare" RESET ". "
113 : : "Supported mode: " BOLD "--dry-run=with-checksums" RESET " (read files and calculate checksums during dry run).\n",0},
114 : : {"start-device-only",'o',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.\n",0 },
115 : : {"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.\n",0},
116 : : {"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.\n",0 },
117 : : {"database",'d',"FILE",0,"Database filename. Defaults to ${HOST}.db, where HOST is the local hostname.\n",0 },
118 : : {"check-level",'l',"FULL|QUICK",0,"Select database validation level: 'quick' for basic structure check, 'full' (default) for comprehensive integrity verification.\n",0 },
119 : : { 0,0,0,0,"Compare databases options:",1},
120 : : {"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",0 },
121 : : {"compare-filter",'F',"checksum-mismatch|first-source-only|second-source-only",0,
122 : : "Filter output categories for " BOLD "--compare" RESET ". "
123 : : "Supported values: "
124 : : BOLD "checksum-mismatch" RESET ", "
125 : : BOLD "first-source-only" RESET ", "
126 : : BOLD "second-source-only" RESET ". "
127 : : "The option can be specified multiple times in any combination.\n",0},
128 : : { 0,0,0,0,"Visualizations options:\n",-1},
129 : : {"silent",'s',0,0,"Don't produce any output. The option will not affect " BOLD "--compare" RESET,0 },
130 : : {"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.\n",0 },
131 : : {"verbose",'v',0,0,"Produce verbose output.",0 },
132 : : {"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 },
133 : : {"help",'h',0,0,"Give this help list",-1 },
134 : : {"help",'?',0,OPTION_ALIAS | OPTION_HIDDEN,0,-1 },
135 : : {"usage",'z',0,0,"Give a short usage message",-1 },
136 : : {"version",'V',0,0,"Print program version",-1 },
137 : : {0}
138 : : };
139 : :
140 : : /**
141 : : * @brief Convert an argp parse error code into human-readable text
142 : : * @param parse_error Error code returned by argp_parse
143 : : * @return Pointer to a static descriptive string
144 : : */
145 : 22 : static const char *argp_error_to_text(const error_t parse_error)
146 : : {
147 [ + + ]: 22 : if(parse_error == EX_USAGE)
148 : : {
149 : 14 : return("command line usage error");
150 : : }
151 : :
152 [ - + ]: 8 : if(parse_error == ARGP_ERR_UNKNOWN)
153 : : {
154 : 0 : return("unknown argument parsing error");
155 : : }
156 : :
157 : 8 : const char *message = strerror(parse_error);
158 : :
159 [ + - - + ]: 8 : if(message == NULL || message[0] == '\0')
160 : : {
161 : 0 : return("unknown error");
162 : : }
163 : :
164 : 8 : return(message);
165 : : }
166 : :
167 : : /**
168 : : * @brief Handle a single argp parser event
169 : : * @param key Current option key or argp event key
170 : : * @param arg Optional value for the current option
171 : : * @param state Current argp parser state
172 : : * @return EX_OK on success, EX_USAGE for invalid usage, or errno-style error code
173 : : */
174 : 2695 : static error_t parse_opt(
175 : : int key,
176 : : char *arg,
177 : : struct argp_state *state)
178 : : {
179 : 2695 : char *ptr = NULL;
180 : 2695 : long int argument_value = -1;
181 : :
182 [ + + + + : 2695 : switch(key)
+ + + + +
+ + + + +
+ + + + -
+ + + + +
+ + + ]
183 : : {
184 : 231 : case 'd':
185 : : {
186 : : // Store full path to the DB file
187 [ - + ]: 231 : if(CRITICAL & copy_literal(conf(db_primary_file_path),arg))
188 : : {
189 : 0 : argp_failure(state,0,ENOMEM,"ERROR: Memory allocation for db_file_path failed");
190 : 0 : return(ENOMEM);
191 : : }
192 : :
193 : : // Store only the DB file basename
194 : 231 : char *tmp = strdup(arg);
195 : :
196 [ - + ]: 231 : if(tmp == NULL)
197 : : {
198 : 0 : (void)del(conf(db_primary_file_path));
199 : 0 : argp_failure(state,0,ENOMEM,"ERROR: Memory allocation for db_file_name failed");
200 : 0 : return(ENOMEM);
201 : : }
202 : :
203 : 231 : const char *db_file_basename = basename(tmp);
204 : :
205 [ - + ]: 231 : if(db_file_basename == NULL)
206 : : {
207 : 0 : free(tmp);
208 : 0 : (void)del(conf(db_primary_file_path));
209 : 0 : argp_failure(state,0,0,"ERROR: Failed to determine database base name from path '%s'",arg);
210 : 0 : return(EINVAL);
211 : : }
212 : :
213 [ - + ]: 231 : if(CRITICAL & copy_literal(conf(db_file_name),db_file_basename))
214 : : {
215 : 0 : free(tmp);
216 : 0 : (void)del(conf(db_primary_file_path));
217 : 0 : argp_failure(state,0,ENOMEM,"ERROR: Memory allocation for db_file_name failed");
218 : 0 : return(ENOMEM);
219 : : }
220 : :
221 : 231 : free(tmp);
222 : 231 : break;
223 : : }
224 : 36 : case 'e':
225 : 36 : (void)add_string_to_array(&config->ignore,arg);
226 : 36 : break;
227 : 18 : case 'n':
228 : 18 : config->dry_run = true;
229 : :
230 [ + + ]: 18 : if(arg != NULL)
231 : : {
232 [ + + ]: 4 : if(0 == strcasecmp(arg,"with-checksums"))
233 : : {
234 : 2 : config->dry_run_with_checksums = true;
235 : :
236 : : } else {
237 : 2 : argp_failure(state,0,0,"ERROR: Unsupported --dry-run mode '%s'. Supported mode: with-checksums. See --help for more information",arg);
238 : 2 : return(EINVAL);
239 : : }
240 : : }
241 : 16 : break;
242 : 16 : case 'i':
243 : 16 : (void)add_string_to_array(&config->include,arg);
244 : : // Track that at least one --include pattern was provided
245 : 16 : config->include_specified = true;
246 : 16 : break;
247 : 48 : case 'k':
248 : 48 : (void)add_string_to_array(&config->lock_checksum,arg);
249 : 48 : break;
250 : 10 : case 'r':
251 : 10 : config->rehash_locked = true;
252 : 10 : break;
253 : 88 : case 'c':
254 : 88 : config->compare = true;
255 : 88 : break;
256 : 74 : case 'F':
257 [ + - + + ]: 74 : if(arg != NULL && 0 == strcmp(arg,"checksum-mismatch"))
258 : : {
259 : 24 : config->compare_filter_checksum_mismatch = true;
260 [ + - + + ]: 50 : } else if(arg != NULL && 0 == strcmp(arg,"first-source-only")){
261 : 24 : config->compare_filter_first_source_only = true;
262 [ + - + + ]: 26 : } else if(arg != NULL && 0 == strcmp(arg,"second-source-only")){
263 : 24 : config->compare_filter_second_source_only = true;
264 : : } else {
265 [ + - ]: 2 : argp_failure(state,0,0,"ERROR: Unsupported --compare-filter value '%s'. Supported values: checksum-mismatch, first-source-only, second-source-only. See --help for more information",arg == NULL ? "" : arg);
266 : 2 : return(EINVAL);
267 : : }
268 : 72 : break;
269 : 2 : case 'o':
270 : 2 : config->start_device_only = true;
271 : 2 : break;
272 : 12 : case 'C':
273 : 12 : config->db_drop_ignored = true;
274 : 12 : break;
275 : 2 : case 'X':
276 : 2 : config->db_drop_inaccessible = true;
277 : 2 : break;
278 : 4 : case 'm':
279 : 4 : argument_value = strtol(arg,&ptr,10);
280 : :
281 : : // Accept only non-negative integer values that fit into short int
282 : : // and reject strings with trailing non-numeric characters
283 [ + - + - : 4 : if(argument_value >= 0 && argument_value <= 32767 && *ptr == '\0')
+ - ]
284 : : {
285 : 4 : config->maxdepth = (short int)argument_value;
286 : : } else {
287 : 0 : argp_failure(state,0,0,"ERROR: Wrong --maxdepth (-m) value. Should be an integer from 0 to 32767. See --help for more information");
288 : 0 : return(EINVAL);
289 : : }
290 : 4 : break;
291 : 61 : case 'p':
292 : 61 : config->progress = true;
293 : 61 : break;
294 : 26 : case 'T':
295 : 26 : config->watch_timestamps = true;
296 : 26 : break;
297 : 128 : case 'u':
298 : 128 : config->update = true;
299 : 128 : break;
300 : 2 : case 'f':
301 : 2 : config->force = true;
302 : 2 : break;
303 : 2 : case 'l':
304 [ + - ]: 2 : if(0 == strncasecmp(arg,"QUICK",sizeof("QUICK")))
305 : : {
306 : 2 : config->db_check_level = QUICK;
307 [ # # ]: 0 : } else if(0 == strncasecmp(arg,"FULL",sizeof("FULL"))){
308 : 0 : config->db_check_level = FULL;
309 : : } else {
310 : 0 : argp_failure(state,0,0,"ERROR: Unsupported --check-level value '%s'. Supported values: FULL or QUICK",arg);
311 : 0 : return(EINVAL);
312 : : }
313 : 2 : break;
314 : 32 : case 's':
315 : : // Set global logger mode
316 : 32 : rational_logger_mode = SILENT;
317 : 32 : break;
318 : 0 : case 'q':
319 : 0 : config->quiet_ignored = true;
320 : 0 : break;
321 : 6 : case 'v':
322 : : // Set global logger mode
323 : 6 : rational_logger_mode = VERBOSE;
324 : 6 : config->verbose = true;
325 : 6 : break;
326 : 10 : case 'h':
327 : : case '?':
328 : 10 : information_mode_requested = true;
329 : 10 : about();
330 : 10 : argp_state_help(state,state->out_stream,ARGP_HELP_STD_HELP & ~(ARGP_HELP_EXIT_OK | ARGP_HELP_EXIT_ERR));
331 : 10 : break;
332 : 6 : case 'V':
333 : 6 : information_mode_requested = true;
334 : 6 : about();
335 : 6 : break;
336 : 12 : case 'z':
337 : 12 : information_mode_requested = true;
338 : 12 : about();
339 : 12 : argp_state_help(state,state->out_stream,ARGP_HELP_USAGE);
340 : 12 : break;
341 : 20 : case ARGP_KEY_NO_ARGS:
342 [ + + ]: 20 : if(information_mode_requested == true)
343 : : {
344 : 18 : break;
345 : : }
346 : :
347 [ + - ]: 2 : if(state->argc == 1)
348 : : {
349 : 2 : information_mode_requested = true;
350 : 2 : about();
351 : 2 : argp_usage(state);
352 : 2 : break;
353 : : }
354 : :
355 : 0 : argp_usage(state);
356 : 0 : return(EX_USAGE);
357 : : break;
358 : 349 : case ARGP_KEY_ARG:
359 : 349 : config->paths = &state->argv[state->next - 1];
360 : 349 : state->next = state->argc;
361 : 349 : break;
362 : 369 : case ARGP_KEY_END:
363 [ + + ]: 369 : if(information_mode_requested == true)
364 : : {
365 : 30 : break;
366 : : }
367 : :
368 : 678 : const bool compare_filter_specified = config->compare_filter_checksum_mismatch == true
369 [ + + ]: 315 : || config->compare_filter_first_source_only == true
370 [ + + + + ]: 654 : || config->compare_filter_second_source_only == true;
371 : :
372 [ + + + + ]: 339 : if(compare_filter_specified == true && config->compare == false)
373 : : {
374 : 14 : argp_failure(state,0,0,"ERROR: --compare-filter can only be used together with --compare. See --help for more information");
375 : 14 : return(EX_USAGE);
376 : : }
377 : :
378 [ + + ]: 325 : if(config->compare == true)
379 : : {
380 [ - + ]: 76 : if(state->arg_num < 2)
381 : : {
382 : 0 : argp_failure(state,0,0,"ERROR: Too few arguments\n--compare require two arguments with paths to database files. See --help for more information");
383 : 0 : return(EX_USAGE);
384 [ - + ]: 76 : } else if(state->arg_num > 2){
385 : 0 : argp_failure(state,0,0,"ERROR: Too many arguments\n--compare require just two arguments with paths to database files. See --help for more information");
386 : 0 : return(EX_USAGE);
387 : : }
388 [ - + ]: 249 : } else if(state->arg_num > 1){
389 : 0 : slog(TRACE,"Caution: multiple PATH arguments received. Multipath mode activated. It’s important to note that when comparison mode is enabled, the ORDER of the paths must be identical for the database comparison to work correctly. Number of paths: %d\n",state->arg_num);
390 : : }
391 : :
392 [ + + - + ]: 325 : if(config->db_drop_inaccessible == true && config->update == false)
393 : : {
394 : 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");
395 : : }
396 : :
397 [ + + - + ]: 325 : if(config->rehash_locked == true && config->lock_checksum == NULL)
398 : : {
399 : 0 : argp_failure(state,0,0,"WARNING: --rehash-locked has no effect without --lock-checksum");
400 : : }
401 : 325 : break;
402 : 1131 : default:
403 : 1131 : return(ARGP_ERR_UNKNOWN);
404 : : }
405 : :
406 : 1546 : return(EX_OK);
407 : : }
408 : :
409 : : /* argp parser definition */
410 : : static struct argp argp = {
411 : : options,parse_opt,args_doc,doc,0,0,0
412 : : };
413 : :
414 : : /**
415 : : * @brief Parse command-line arguments and finalize parse-related configuration state
416 : : * @param argc Number of CLI arguments
417 : : * @param argv CLI argument vector
418 : : * @return SUCCESS for normal mode, INFO for informational mode, or failure flags on errors
419 : : */
420 : 377 : Return parse_arguments(
421 : : const int argc,
422 : : char *argv[])
423 : : {
424 : : /* Status returned by this function through provide()
425 : : Default value assumes successful completion */
426 : 377 : Return status = SUCCESS;
427 : 377 : unsigned int parse_flags = ARGP_NO_EXIT | ARGP_NO_HELP;
428 : :
429 : 377 : information_mode_requested = false;
430 : :
431 : : /* Parse arguments and route each parser event through parse_opt */
432 : 377 : error_t parse_error = argp_parse(&argp,argc,argv,parse_flags,0,0);
433 : :
434 [ + + ]: 377 : if(parse_error != EX_OK)
435 : : {
436 : 22 : status = FAILURE;
437 [ + + ]: 355 : } else if(information_mode_requested == true){
438 : 30 : provide(INFO);
439 : : }
440 : :
441 [ + + + - ]: 347 : if((SUCCESS & status) && config->paths != NULL)
442 : : {
443 [ + + ]: 726 : for(int i = 0; config->paths[i]; i++)
444 : : {
445 : : // Normalize input path by removing trailing slash
446 : 401 : remove_trailing_slash(config->paths[i]);
447 : : }
448 : : }
449 : :
450 [ + + + + ]: 347 : if((SUCCESS & status) && config->compare == true)
451 : : {
452 [ + - ]: 76 : if(config->paths != NULL)
453 : : {
454 : : // Reuse parsed positional arguments as database file paths
455 : 76 : config->db_file_paths = config->paths;
456 : :
457 [ + + + - ]: 228 : for(int i = 0; config->db_file_paths[i] && (SUCCESS & status); i++)
458 : : {
459 : : // Duplicate the path because basename may modify the buffer
460 : 152 : char *tmp = strdup(config->db_file_paths[i]);
461 : :
462 [ - + ]: 152 : if(tmp == NULL)
463 : : {
464 : 0 : report("Failed to duplicate string: %s",config->db_file_paths[i]);
465 : 0 : status = FAILURE;
466 : 0 : break;
467 : : }
468 : :
469 : : // Resolve basename and handle NULL result
470 : 152 : const char *db_file_basename = basename(tmp);
471 : :
472 [ - + ]: 152 : if(db_file_basename == NULL)
473 : : {
474 : 0 : report("basename failed for path: %s",tmp);
475 : 0 : free(tmp);
476 : 0 : status = FAILURE;
477 : 0 : break;
478 : : }
479 : :
480 : 152 : status = add_string_to_array(&config->db_file_names,db_file_basename);
481 : 152 : free(tmp);
482 : :
483 [ - + ]: 152 : if((SUCCESS & status) == false)
484 : : {
485 : 0 : break;
486 : : }
487 : : }
488 : : }
489 : : }
490 : :
491 [ + + ]: 347 : if(parse_error != 0)
492 : : {
493 : 22 : slog(ERROR,"Argument parsing failed with code %d (%s)\n",parse_error,argp_error_to_text(parse_error));
494 : 22 : status = FAILURE;
495 : : }
496 : :
497 [ + + ]: 347 : if(CRITICAL & status)
498 : : {
499 : 22 : provide(status);
500 : : }
501 : :
502 : : /* Testing-mode diagnostics */
503 : : {
504 : 325 : slog(TESTING,"rational_logger_mode=%s\n",rational_reconvert(rational_logger_mode));
505 : :
506 [ + - ]: 325 : if(config->paths != NULL)
507 : : {
508 : 325 : slog(TESTING,"argument:paths=");
509 : :
510 [ + + ]: 726 : for(int i = 0; config->paths[i]; i++)
511 : : {
512 [ + + ]: 401 : slog(TESTING|UNDECOR,i == 0 ? "%s" : ", %s",config->paths[i]);
513 : : }
514 : 325 : slog(TESTING|UNDECOR,"\n");
515 : : }
516 : :
517 : : // String descriptor length includes '\0'; >1 means there is actual content
518 [ + + ]: 325 : if(conf(db_primary_file_path)->length > 1)
519 : : {
520 : 231 : slog(TESTING,"argument:database=%s\n",confstr(db_primary_file_path));
521 : : }
522 : :
523 : : // String descriptor length includes '\0'; >1 means there is actual content
524 [ + + ]: 325 : if(conf(db_file_name)->length > 1)
525 : : {
526 : 231 : slog(TESTING,"argument:db_file_name=%s\n",confstr(db_file_name));
527 : : }
528 : :
529 [ + + ]: 325 : if(config->db_file_paths != NULL)
530 : : {
531 : 76 : slog(TESTING,"argument:db_file_paths=");
532 : :
533 [ + + ]: 228 : for(int i = 0; config->db_file_paths[i]; i++)
534 : : {
535 [ + + ]: 152 : slog(TESTING|UNDECOR,i == 0 ? "%s" : ", %s",config->db_file_paths[i]);
536 : : }
537 : 76 : slog(TESTING|UNDECOR,"\n");
538 : : }
539 : :
540 [ + + ]: 325 : if(config->db_file_names != NULL)
541 : : {
542 : 76 : slog(TESTING,"argument:db_file_names=");
543 : :
544 [ + + ]: 228 : for(int i = 0; config->db_file_names[i]; i++)
545 : : {
546 [ + + ]: 152 : slog(TESTING|UNDECOR,i == 0 ? "%s" : ", %s",config->db_file_names[i]);
547 : : }
548 : 76 : slog(TESTING|UNDECOR,"\n");
549 : : }
550 : :
551 [ + + ]: 325 : if(config->ignore != NULL)
552 : : {
553 : 32 : slog(TESTING,"argument:ignore=");
554 : :
555 : : // Print string-array contents
556 [ + + ]: 68 : for(int i = 0; config->ignore[i] != NULL; ++i)
557 : : {
558 [ + + ]: 36 : slog(TESTING|UNDECOR,i == 0 ? "%s" : ", %s",config->ignore[i]);
559 : : }
560 : 32 : slog(TESTING|UNDECOR,"\n");
561 : : }
562 : :
563 [ + + ]: 325 : if(config->include != NULL)
564 : : {
565 : 12 : slog(TESTING,"argument:include=");
566 : :
567 : : // Print string-array contents
568 [ + + ]: 28 : for(int i = 0; config->include[i] != NULL; ++i)
569 : : {
570 [ + + ]: 16 : slog(TESTING|UNDECOR,i == 0 ? "%s" : ", %s",config->include[i]);
571 : : }
572 : 12 : slog(TESTING|UNDECOR,"\n");
573 : : }
574 : :
575 [ + + ]: 325 : if(config->lock_checksum != NULL)
576 : : {
577 : 40 : slog(TESTING,"argument:lock-checksum=");
578 : :
579 : : // Print string-array contents
580 [ + + ]: 88 : for(int i = 0; config->lock_checksum[i] != NULL; ++i)
581 : : {
582 [ + + ]: 48 : slog(TESTING|UNDECOR,i == 0 ? "%s" : ", %s",config->lock_checksum[i]);
583 : : }
584 : 40 : slog(TESTING|UNDECOR,"\n");
585 : : }
586 : :
587 [ + + ]: 325 : if(config->db_check_level != FULL)
588 : : {
589 [ + - ]: 2 : slog(TESTING,"argument:check-level=%s\n",config->db_check_level == QUICK ? "QUICK" : "FULL");
590 : : }
591 : :
592 [ + + ]: 325 : if(config->maxdepth > 0)
593 : : {
594 : 2 : slog(TESTING,"argument:maxdepth=%d\n",config->maxdepth);
595 : : }
596 : :
597 [ + + ]: 325 : if(config->verbose)
598 : : {
599 [ + - ]: 6 : slog(TESTING,"argument:verbose=%s\n",config->verbose ? "yes" : "no");
600 : : }
601 : :
602 [ - + ]: 325 : if(config->quiet_ignored)
603 : : {
604 [ # # ]: 0 : slog(TESTING,"argument:quiet-ignored=%s\n",config->quiet_ignored ? "yes" : "no");
605 : : }
606 : :
607 [ + + ]: 325 : if(config->watch_timestamps)
608 : : {
609 [ + - ]: 26 : slog(TESTING,"argument:watch-timestamps=%s\n",config->watch_timestamps ? "yes" : "no");
610 : : }
611 : :
612 [ + + ]: 325 : if(config->rehash_locked == true)
613 : : {
614 [ + - ]: 10 : slog(TESTING,"argument:rehash-locked=%s\n",config->rehash_locked ? "yes" : "no");
615 : : }
616 : :
617 [ + + ]: 325 : if(config->force)
618 : : {
619 [ + - ]: 2 : slog(TESTING,"argument:force=%s\n",config->force ? "yes" : "no");
620 : : }
621 : :
622 [ + + ]: 325 : if(config->update)
623 : : {
624 [ + - ]: 128 : slog(TESTING,"argument:update=%s\n",config->update ? "yes" : "no");
625 : : }
626 : :
627 [ + + ]: 325 : if(config->progress)
628 : : {
629 [ + - ]: 61 : slog(TESTING,"argument:progress=%s\n",config->progress ? "yes" : "no");
630 : : }
631 : :
632 [ + + ]: 325 : if(config->compare)
633 : : {
634 [ + - ]: 76 : slog(TESTING,"argument:compare=%s\n",config->compare ? "yes" : "no");
635 : : }
636 : :
637 [ + + ]: 325 : if(config->db_drop_ignored)
638 : : {
639 [ + - ]: 12 : slog(TESTING,"argument:db-drop-ignored=%s\n",config->db_drop_ignored ? "yes" : "no");
640 : : }
641 : :
642 [ + + ]: 325 : if(config->db_drop_inaccessible)
643 : : {
644 [ + - ]: 2 : slog(TESTING,"argument:db-drop-inaccessible=%s\n",config->db_drop_inaccessible ? "yes" : "no");
645 : : }
646 : :
647 [ + + ]: 325 : if(config->dry_run)
648 : : {
649 [ + + ]: 16 : slog(TESTING,"argument:dry-run=%s\n",config->dry_run_with_checksums ? "with-checksums" : "yes");
650 : : }
651 : :
652 [ + + ]: 325 : if(config->start_device_only)
653 : : {
654 [ + - ]: 2 : slog(TESTING,"argument:start-device-only=%s\n",config->start_device_only ? "yes" : "no");
655 : : }
656 : :
657 : : }
658 : :
659 : : /* Verbose-mode diagnostics */
660 : : {
661 : 325 : slog(VERBOSE,"Configuration: ");
662 : 325 : slog(VERBOSE|UNDECOR,"rational_logger_mode=%s\n",rational_reconvert(rational_logger_mode));
663 : :
664 [ + - ]: 325 : if(config->paths != NULL)
665 : : {
666 : 325 : slog(VERBOSE|UNDECOR,"paths=");
667 : :
668 [ + + ]: 726 : for(int i = 0; config->paths[i]; i++)
669 : : {
670 [ + + ]: 401 : slog(VERBOSE|UNDECOR,i == 0 ? "%s" : ", %s",config->paths[i]);
671 : : }
672 : 325 : slog(VERBOSE|UNDECOR,"; ");
673 : : }
674 : :
675 : : // String descriptor length includes '\0'; >1 means there is actual content
676 [ + + ]: 325 : if(conf(db_primary_file_path)->length > 1)
677 : : {
678 : 231 : slog(VERBOSE|UNDECOR,"database=%s; ",confstr(db_primary_file_path));
679 : : }
680 : :
681 : : // String descriptor length includes '\0'; >1 means there is actual content
682 [ + + ]: 325 : if(conf(db_file_name)->length > 1)
683 : : {
684 : 231 : slog(VERBOSE|UNDECOR,"db_file_name=%s; ",confstr(db_file_name));
685 : : }
686 : :
687 [ + + ]: 325 : if(config->db_file_paths != NULL)
688 : : {
689 : 76 : slog(VERBOSE|UNDECOR,"db_file_paths=");
690 : :
691 [ + + ]: 228 : for(int i = 0; config->db_file_paths[i]; i++)
692 : : {
693 [ + + ]: 152 : slog(VERBOSE|UNDECOR,i == 0 ? "%s" : ", %s",config->db_file_paths[i]);
694 : : }
695 : 76 : slog(VERBOSE|UNDECOR,"; ");
696 : : }
697 : :
698 [ + + ]: 325 : if(config->db_file_names != NULL)
699 : : {
700 : 76 : slog(VERBOSE|UNDECOR,"db_file_names=");
701 : :
702 [ + + ]: 228 : for(int i = 0; config->db_file_names[i]; i++)
703 : : {
704 [ + + ]: 152 : slog(VERBOSE|UNDECOR,i == 0 ? "%s" : ", %s",config->db_file_names[i]);
705 : : }
706 : 76 : slog(VERBOSE|UNDECOR,"; ");
707 : : }
708 : :
709 [ + + ]: 325 : if(config->ignore != NULL)
710 : : {
711 : 32 : slog(VERBOSE|UNDECOR,"ignore=");
712 : :
713 : : // Print string-array contents
714 [ + + ]: 68 : for(int i = 0; config->ignore[i] != NULL; ++i)
715 : : {
716 [ + + ]: 36 : slog(VERBOSE|UNDECOR,i == 0 ? "%s" : ", %s",config->ignore[i]);
717 : : }
718 : 32 : slog(VERBOSE|UNDECOR,"; ");
719 : : }
720 : :
721 [ + + ]: 325 : if(config->include != NULL)
722 : : {
723 : 12 : slog(VERBOSE|UNDECOR,"include=");
724 : :
725 : : // Print string-array contents
726 [ + + ]: 28 : for(int i = 0; config->include[i] != NULL; ++i)
727 : : {
728 [ + + ]: 16 : slog(VERBOSE|UNDECOR,i == 0 ? "%s" : ", %s",config->include[i]);
729 : : }
730 : 12 : slog(VERBOSE|UNDECOR,"; ");
731 : : }
732 : :
733 [ + + ]: 325 : if(config->lock_checksum != NULL)
734 : : {
735 : 40 : slog(VERBOSE|UNDECOR,"lock-checksum=");
736 : :
737 : : // Print string-array contents
738 [ + + ]: 88 : for(int i = 0; config->lock_checksum[i] != NULL; ++i)
739 : : {
740 [ + + ]: 48 : slog(VERBOSE|UNDECOR,i == 0 ? "%s" : ", %s",config->lock_checksum[i]);
741 : : }
742 : 40 : slog(VERBOSE|UNDECOR,"; ");
743 : : }
744 : :
745 : 325 : const char *dry_run_mode = "no";
746 : :
747 [ + + ]: 325 : if(config->dry_run == true)
748 : : {
749 [ + + ]: 16 : if(config->dry_run_with_checksums == true)
750 : : {
751 : 2 : dry_run_mode = "with-checksums";
752 : : } else {
753 : 14 : dry_run_mode = "yes";
754 : : }
755 : : }
756 : :
757 [ + + + + : 325 : slog(VERBOSE|UNDECOR,"verbose=%s; maxdepth=%d; silent=no; quiet-ignored=%s; force=%s; update=%s; watch-timestamps=%s; rehash-locked=%s; progress=%s; compare=%s, db-drop-ignored=%s, db-drop-inaccessible=%s, dry-run=%s, start-device-only=%s, check-level=%s, rational_logger_mode=%s",
+ + + + +
+ + + + +
+ + + + +
+ - + +
+ ]
758 : : config->verbose ? "yes" : "no",
759 : : config->maxdepth,
760 : : config->quiet_ignored ? "yes" : "no",
761 : : config->force ? "yes" : "no",
762 : : config->update ? "yes" : "no",
763 : : config->watch_timestamps ? "yes" : "no",
764 : : config->rehash_locked ? "yes" : "no",
765 : : config->progress ? "yes" : "no",
766 : : config->compare ? "yes" : "no",
767 : : config->db_drop_ignored ? "yes" : "no",
768 : : config->db_drop_inaccessible ? "yes" : "no",
769 : : dry_run_mode,
770 : : config->start_device_only ? "yes" : "no",
771 : : config->db_check_level == QUICK ? "QUICK" : "FULL",
772 : : rational_reconvert(rational_logger_mode));
773 : 325 : slog(VERBOSE|UNDECOR,"\n");
774 : : }
775 : :
776 : 325 : slog(TRACE,"Argument parsing is complete\n");
777 : :
778 : 325 : provide(status);
779 : : }
|