Branch data Line data Source code
1 : : #include "precizer.h"
2 : :
3 : : /**
4 : : * @brief Composes an SQL ATTACH DATABASE query string.
5 : : *
6 : : * This function generates an SQL query string to attach a database with a specified path and number.
7 : : *
8 : : * @param[out] sql Pointer to a string that will hold the generated SQL query.
9 : : * @param[in] db_path Path to the database file to be attached.
10 : : * @param[in] db_num Database number (1 or 2) used in the query.
11 : : * @return Return structure indicating the operation status.
12 : : */
13 : 224 : static Return compose_sql(
14 : : char **sql,
15 : : const char *db_path,
16 : : int db_num)
17 : : {
18 : : /* Status returned by this function through provide()
19 : : Default value assumes successful completion */
20 : 224 : Return status = SUCCESS;
21 : 224 : create(char,wrapped_db_path);
22 : :
23 [ + - - + ]: 224 : run(db_sql_wrap_string(wrapped_db_path,db_path));
24 : :
25 [ + - - + ]: 224 : if(SUCCESS == status && asprintf(sql,"ATTACH DATABASE %s as db%d;",getcstring(wrapped_db_path),db_num) == -1)
26 : : {
27 : 0 : status = FAILURE;
28 : 0 : report("Memory allocation failed for SQL query string");
29 : : }
30 : :
31 : 224 : del(wrapped_db_path);
32 : :
33 : 224 : provide(status);
34 : : }
35 : :
36 : : /**
37 : : * @brief Attaches a secondary database to the primary database connection.
38 : : *
39 : : * This function attaches a secondary database (specified by its index in the configuration)
40 : : * to the primary SQLite database connection using the ATTACH DATABASE command.
41 : : *
42 : : * @param[in] db_A Index of the database path in the configuration array.
43 : : * @param[in] db_B Database number (1 or 2) to be used in the ATTACH DATABASE command.
44 : : * @return Return structure indicating the operation status.
45 : : */
46 : 224 : static Return db_attach(
47 : : int db_A,
48 : : int db_B)
49 : : {
50 : : /* Status returned by this function through provide()
51 : : Default value assumes successful completion */
52 : 224 : Return status = SUCCESS;
53 : :
54 : 224 : char *select_sql = NULL;
55 : :
56 [ + - - + ]: 224 : run(compose_sql(&select_sql,config->db_file_paths[db_A],db_B));
57 : :
58 [ + - ]: 224 : if(SUCCESS == status)
59 : : {
60 : 224 : int rc = sqlite3_exec(config->db,select_sql,NULL,NULL,NULL);
61 : :
62 [ - + ]: 224 : if(rc!= SQLITE_OK)
63 : : {
64 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't execute");
65 : 0 : status = FAILURE;
66 : : }
67 : : }
68 : :
69 : 224 : free(select_sql);
70 : :
71 : 224 : provide(status);
72 : : }
73 : :
74 : : /**
75 : : * @brief Detach database by alias
76 : : *
77 : : * @param[in] db_alias Attached database alias name
78 : : * @return Return status code
79 : : */
80 : 224 : static Return db_detach(const char *db_alias)
81 : : {
82 : : /* Status returned by this function through provide()
83 : : Default value assumes successful completion */
84 : 224 : Return status = SUCCESS;
85 : :
86 : 224 : sqlite3_stmt *stmt = NULL;
87 : :
88 : 224 : const char *sql = "DETACH DATABASE ?1;";
89 : :
90 : 224 : int rc = sqlite3_prepare_v2(config->db,sql,-1,&stmt,NULL);
91 : :
92 [ - + ]: 224 : if(SQLITE_OK != rc)
93 : : {
94 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to prepare detach statement");
95 : 0 : status = FAILURE;
96 : : }
97 : :
98 [ + - ]: 224 : if(SUCCESS == status)
99 : : {
100 : 224 : rc = sqlite3_bind_text(stmt,1,db_alias,-1,SQLITE_STATIC);
101 : :
102 [ - + ]: 224 : if(SQLITE_OK != rc)
103 : : {
104 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to bind database alias in detach");
105 : 0 : status = FAILURE;
106 : : }
107 : : }
108 : :
109 [ + - ]: 224 : if(SUCCESS == status)
110 : : {
111 : 224 : rc = sqlite3_step(stmt);
112 : :
113 [ - + ]: 224 : if(SQLITE_DONE != rc)
114 : : {
115 : 0 : log_sqlite_error(config->db,rc,NULL,"Detach statement didn't return DONE");
116 : 0 : status = FAILURE;
117 : : }
118 : : }
119 : :
120 [ + - ]: 224 : if(stmt != NULL)
121 : : {
122 : 224 : sqlite3_finalize(stmt);
123 : : }
124 : :
125 : 224 : provide(status);
126 : : }
127 : :
128 : : /**
129 : : * @brief Run one compare query and print only the paths that pass output filters
130 : : *
131 : : * Executes the supplied comparison query, applies the shared --include/--ignore
132 : : * decision before reporting each row, and reports only visible relative paths.
133 : : * Only visible paths contribute to category state, so compare summaries and
134 : : * equality messages are evaluated against the filtered scope. The category
135 : : * heading is emitted only before the first visible path and stays visible in
136 : : * `--silent` only when `show_headings_in_silent` enables it
137 : : *
138 : : * @param[in] compare_sql SQL query that returns relative paths for one compare category
139 : : * @param[out] differences_found Set to `true` after the first visible path is printed
140 : : * so hidden rows stay outside the reported comparison scope
141 : : * @param[in] show_headings_in_silent True to keep the category heading visible in `--silent`
142 : : * @param[in] heading_format Heading format string with two `%s` database-name slots
143 : : * @param[in] db_A_name First database name for the heading
144 : : * @param[in] db_B_name Second database name for the heading
145 : : * @return Return structure indicating the operation status
146 : : */
147 : 266 : static Return db_report_category(
148 : : const char *compare_sql,
149 : : bool *differences_found,
150 : : const bool show_headings_in_silent,
151 : : const char *heading_format,
152 : : const char *db_A_name,
153 : : const char *db_B_name)
154 : : {
155 : : /* Status returned by this function through provide()
156 : : Default value assumes successful completion */
157 : 266 : Return status = SUCCESS;
158 : :
159 : 266 : sqlite3_stmt *select_stmt = NULL;
160 : :
161 : 266 : int rc = sqlite3_prepare_v2(config->db,compare_sql,-1,&select_stmt,NULL);
162 : :
163 [ - + ]: 266 : if(SQLITE_OK != rc)
164 : : {
165 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
166 : 0 : status = FAILURE;
167 : : }
168 : :
169 [ + - ]: 266 : if(SUCCESS == status)
170 : : {
171 : 266 : bool first_visible_path = true;
172 : :
173 [ + + ]: 426 : while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
174 : : {
175 : : // Interrupt the loop smoothly
176 : : // Interrupt when Ctrl+C
177 [ - + ]: 160 : if(global_interrupt_flag == true)
178 : : {
179 : 0 : break;
180 : : }
181 : :
182 : 160 : const unsigned char *relative_path = sqlite3_column_text(select_stmt,0);
183 : :
184 [ - + ]: 160 : if(relative_path == NULL)
185 : : {
186 : 0 : rc = sqlite3_errcode(config->db);
187 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to read relative path from select result");
188 : 0 : status = FAILURE;
189 : 0 : break;
190 : : }
191 : :
192 : 160 : bool ignore = false;
193 : :
194 : 160 : status = match_include_ignore((const char *)relative_path,
195 : : NULL,
196 : : &ignore);
197 : :
198 [ - + ]: 160 : if(SUCCESS != status)
199 : : {
200 : 0 : break;
201 : : }
202 : :
203 [ + + ]: 160 : if(ignore == true)
204 : : {
205 : 32 : continue;
206 : : }
207 : :
208 [ + + ]: 128 : if(first_visible_path == true)
209 : : {
210 : 112 : first_visible_path = false;
211 : :
212 : : // Outside --silent the heading is always shown
213 : : // In --silent it stays only when multiple compare categories can mix together in one output
214 [ + + + + ]: 112 : if((rational_logger_mode & SILENT) == 0 || show_headings_in_silent == true)
215 : : {
216 : 106 : slog(EVERY|VISIBLE_IN_SILENT,heading_format,db_A_name,db_B_name);
217 : : }
218 : : }
219 : :
220 : 128 : *differences_found = true;
221 : 128 : slog(EVERY|UNDECOR|VISIBLE_IN_SILENT,"%s\n",relative_path);
222 : : }
223 : :
224 [ + - + - : 266 : if(SUCCESS == status && global_interrupt_flag == false && SQLITE_DONE != rc)
- + ]
225 : : {
226 : 0 : log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
227 : 0 : status = FAILURE;
228 : : }
229 : : }
230 : :
231 [ + - ]: 266 : if(select_stmt != NULL)
232 : : {
233 : 266 : rc = sqlite3_finalize(select_stmt);
234 : :
235 [ - + ]: 266 : if(SQLITE_OK != rc)
236 : : {
237 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize SQLite statement");
238 : 0 : status = FAILURE;
239 : : } else {
240 : 266 : select_stmt = NULL;
241 : : }
242 : : }
243 : :
244 : 266 : provide(status);
245 : : }
246 : :
247 : : /**
248 : : * @brief Compare two databases selected in the global config
249 : : *
250 : : * @details Attaches both databases, reports requested difference categories,
251 : : * and prints summary lines for missing paths and checksum mismatches. The
252 : : * comparison scope can be limited with `--compare-filter` and further narrowed
253 : : * by `--ignore` and `--include`; without filters the function checks
254 : : * first-source paths, second-source paths, and SHA512 mismatches
255 : : *
256 : : * @return Return status code
257 : : */
258 : 389 : Return db_compare(void)
259 : : {
260 : : /* Status returned by this function through provide()
261 : : Default value assumes successful completion */
262 : 389 : Return status = SUCCESS;
263 : :
264 : 389 : bool attached_db1 = false;
265 : 389 : bool attached_db2 = false;
266 : :
267 : : /* Interrupt the function smoothly */
268 : : /* Interrupt when Ctrl+C */
269 [ - + ]: 389 : if(global_interrupt_flag == true)
270 : : {
271 : 0 : provide(status);
272 : : }
273 : :
274 : : /* Skip if comparison mode is not enabled */
275 [ + + ]: 389 : if(config->compare != true)
276 : : {
277 : 271 : slog(TRACE,"Database comparison mode is not enabled. Skipping comparison\n");
278 : 271 : provide(status);
279 : : }
280 : :
281 : 118 : slog(EVERY,"The comparison of %s and %s databases is starting…\n",config->db_file_names[0],config->db_file_names[1]);
282 : :
283 : : /* Validate database paths */
284 [ + + ]: 346 : for(int i = 0; config->db_file_paths[i]; i++)
285 : : {
286 [ - + ]: 234 : if(NOT_FOUND == file_availability(config->db_file_paths[i],NULL,SHOULD_BE_A_FILE))
287 : : {
288 : 0 : slog(ERROR,"The database file %s is either inaccessible or not a valid file\n",
289 : : config->db_file_paths[i]);
290 : 0 : status = FAILURE;
291 : 0 : break;
292 : : }
293 : :
294 [ + - ]: 234 : if(SUCCESS == status)
295 : : {
296 : : /*
297 : : * Validate the integrity of the database file
298 : : */
299 : 234 : status = db_integrity_check(config->db_file_paths[i]);
300 : :
301 [ + + ]: 234 : if(SUCCESS != status)
302 : : {
303 : 6 : break;
304 : : }
305 : : }
306 : : }
307 : :
308 : : /* Attach databases */
309 [ + + ]: 118 : if(SUCCESS == status)
310 : : {
311 : : // Attach the database 1
312 : 112 : status = db_attach(0,1);
313 : :
314 [ + - ]: 112 : if(SUCCESS == status)
315 : : {
316 : 112 : attached_db1 = true;
317 : : }
318 : : }
319 : :
320 [ + + ]: 118 : if(SUCCESS == status)
321 : : {
322 : : // Attach the database 2
323 : 112 : status = db_attach(1,2);
324 : :
325 [ + - ]: 112 : if(SUCCESS == status)
326 : : {
327 : 112 : attached_db2 = true;
328 : : }
329 : : }
330 : :
331 : : /* SQL queries for comparison */
332 : 118 : const char *compare_A_sql = "SELECT a.relative_path "
333 : : "FROM db2.files AS a "
334 : : "LEFT JOIN db1.files AS b on b.relative_path = a.relative_path "
335 : : "WHERE b.relative_path IS NULL "
336 : : "ORDER BY a.relative_path ASC;";
337 : :
338 : 118 : const char *compare_B_sql = "SELECT a.relative_path "
339 : : "FROM db1.files AS a "
340 : : "LEFT join db2.files AS b on b.relative_path = a.relative_path "
341 : : "WHERE b.relative_path IS NULL "
342 : : "ORDER BY a.relative_path ASC;";
343 : :
344 : : // True when user provided at least one --compare-filter option.
345 : : // False means default compare mode: all three categories are enabled.
346 : 118 : const bool filter_specified = config->compare_filter != CF_NONE_SPECIFIED;
347 : :
348 : : // Enables "first-source" category:
349 : : // show paths that exist in db1 but are missing in db2.
350 : : // This category is active either explicitly by filter or by default mode.
351 : 236 : const bool check_first_source = (config->compare_filter & CF_FIRST_SOURCE)
352 [ + + + + ]: 118 : || filter_specified == false;
353 : :
354 : : // Enables "second-source" category:
355 : : // show paths that exist in db2 but are missing in db1.
356 : : // This category is active either explicitly by filter or by default mode.
357 : 236 : const bool check_second_source = (config->compare_filter & CF_SECOND_SOURCE)
358 [ + + + + ]: 118 : || filter_specified == false;
359 : :
360 : : // Enables checksum verification category for common relative paths.
361 : : // Active either explicitly by checksum filter or by default mode.
362 : 236 : const bool verify_checksum_consistency = (config->compare_filter & CF_CHECKSUM_MISMATCH)
363 [ + + + + ]: 118 : || filter_specified == false;
364 : :
365 : : // Counts enabled compare categories so silent mode can decide whether headings are needed
366 : 118 : unsigned int active_compare_categories = 0u;
367 : :
368 [ + + ]: 118 : if(check_first_source == true)
369 : : {
370 : 96 : active_compare_categories++;
371 : : }
372 : :
373 [ + + ]: 118 : if(check_second_source == true)
374 : : {
375 : 96 : active_compare_categories++;
376 : : }
377 : :
378 [ + + ]: 118 : if(verify_checksum_consistency == true)
379 : : {
380 : 92 : active_compare_categories++;
381 : : }
382 : :
383 : : // Keeps category headings visible only when silent output can mix multiple categories
384 : 118 : const bool show_headings_in_silent = active_compare_categories > 1u;
385 : :
386 : : // Comparison result flags grouped in one place for summary evaluation
387 : 118 : bool first_source_differences_found = false;
388 : 118 : bool second_source_differences_found = false;
389 : 118 : bool checksum_mismatches_found = false;
390 : :
391 : : /* Compare files existence between databases */
392 [ + + ]: 118 : if(check_first_source == true)
393 : : {
394 [ + + - + ]: 96 : run(db_report_category(compare_B_sql,
395 : : &first_source_differences_found,
396 : : show_headings_in_silent,
397 : : BOLD "These files are no longer in the %s but still exist in the %s" RESET "\n",
398 : : config->db_file_names[1],
399 : : config->db_file_names[0]));
400 : : }
401 : :
402 [ + + ]: 118 : if(check_second_source == true)
403 : : {
404 [ + + - + ]: 96 : run(db_report_category(compare_A_sql,
405 : : &second_source_differences_found,
406 : : show_headings_in_silent,
407 : : BOLD "These files are no longer in the %s but still exist in the %s" RESET "\n",
408 : : config->db_file_names[0],
409 : : config->db_file_names[1]));
410 : : }
411 : :
412 : : #if 0
413 : : // Old multiPATH solutions
414 : : const char *compare_checksums_sql = "select a.relative_path from db2.files a inner join db1.files b"
415 : : " on b.relative_path = a.relative_path "
416 : : " and b.sha512 is not a.sha512"
417 : : " order by a.relative_path asc;";
418 : :
419 : : const char *compare_checksums_sql = "SELECT p.path,f1.relative_path "
420 : : "FROM db1.files AS f1 "
421 : : "JOIN db1.paths AS p ON f1.path_prefix_index = p.ID "
422 : : "JOIN db2.files AS f2 ON f1.relative_path = f2.relative_path "
423 : : "JOIN db2.paths AS p2 ON f2.path_prefix_index = p2.ID "
424 : : "WHERE f1.sha512 IS NOT f2.sha512 AND p.path = p2.path "
425 : : "ORDER BY p.path,f1.relative_path ASC;";
426 : : #else
427 : : // One PATH solution
428 : 118 : const char *compare_checksums_sql = "SELECT a.relative_path "
429 : : "FROM db2.files AS a "
430 : : "INNER JOIN db1.files AS b ON b.relative_path = a.relative_path "
431 : : "WHERE b.sha512 IS NOT a.sha512 "
432 : : "ORDER BY a.relative_path ASC;";
433 : : #endif
434 : :
435 [ + + ]: 118 : if(verify_checksum_consistency == true)
436 : : {
437 [ + + - + ]: 92 : run(db_report_category(compare_checksums_sql,
438 : : &checksum_mismatches_found,
439 : : show_headings_in_silent,
440 : : BOLD "The SHA512 checksums of these files do not match between %s and %s" RESET "\n",
441 : : config->db_file_names[0],
442 : : config->db_file_names[1]));
443 : : }
444 : :
445 : : /* Cleanup */
446 [ + + ]: 118 : if(attached_db1 == true)
447 : : {
448 [ - + - + ]: 112 : call(db_detach("db1"));
449 : : }
450 : :
451 [ + + ]: 118 : if(attached_db2 == true)
452 : : {
453 [ - + - + ]: 112 : call(db_detach("db2"));
454 : : }
455 : :
456 : : /* Output results */
457 [ + + ]: 118 : if(SUCCESS == status)
458 : : {
459 : 112 : const bool full_compare_scope = check_first_source == true
460 [ + + ]: 90 : && check_second_source == true
461 [ + + + + ]: 202 : && verify_checksum_consistency == true;
462 : :
463 [ + + ]: 112 : if(full_compare_scope == true
464 [ + + ]: 70 : && first_source_differences_found == false
465 [ + + ]: 50 : && second_source_differences_found == false
466 [ + + ]: 36 : && checksum_mismatches_found == false)
467 : : {
468 : 28 : slog(EVERY,BOLD "All files are identical against %s and %s" RESET "\n",
469 : : config->db_file_names[0],
470 : : config->db_file_names[1]);
471 : :
472 [ + + ]: 84 : } else if(full_compare_scope == false){
473 : :
474 [ + + ]: 42 : if(check_first_source == true
475 [ + + ]: 20 : && first_source_differences_found == false)
476 : : {
477 : 8 : slog(EVERY,BOLD "No first-source differences found between %s and %s" RESET "\n",
478 : : config->db_file_names[0],
479 : : config->db_file_names[1]);
480 : : }
481 : :
482 [ + + ]: 42 : if(check_second_source == true
483 [ + + ]: 20 : && second_source_differences_found == false)
484 : : {
485 : 8 : slog(EVERY,BOLD "No second-source differences found between %s and %s" RESET "\n",
486 : : config->db_file_names[0],
487 : : config->db_file_names[1]);
488 : : }
489 : : }
490 : :
491 [ + + + + ]: 112 : if(verify_checksum_consistency == true && checksum_mismatches_found == false)
492 : : {
493 : 46 : slog(EVERY,BOLD "All SHA512 checksums of files are identical against %s and %s" RESET "\n",
494 : : config->db_file_names[0],
495 : : config->db_file_names[1]);
496 : : }
497 : :
498 [ + + ]: 112 : if(full_compare_scope == true
499 [ + + ]: 70 : && first_source_differences_found == false
500 [ + + ]: 50 : && second_source_differences_found == false
501 [ + + ]: 36 : && checksum_mismatches_found == false)
502 : : {
503 : 28 : slog(EVERY,BOLD "The databases %s and %s are absolutely equal" RESET "\n",
504 : : config->db_file_names[0],
505 : : config->db_file_names[1]);
506 : : }
507 : : }
508 : :
509 : 118 : slog(EVERY,"Comparison of %s and %s databases is complete\n",
510 : : config->db_file_names[0],
511 : : config->db_file_names[1]);
512 : :
513 : 118 : provide(status);
514 : : }
|