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 : 140 : 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 : 140 : Return status = SUCCESS;
21 : :
22 [ - + ]: 140 : if(asprintf(sql,"ATTACH DATABASE '%s' as db%d;",db_path,db_num) == -1)
23 : : {
24 : 0 : status = FAILURE;
25 : 0 : report("Memory allocation failed for SQL query string");
26 : : }
27 : :
28 : 140 : provide(status);
29 : : }
30 : :
31 : : /**
32 : : * @brief Attaches a secondary database to the primary database connection.
33 : : *
34 : : * This function attaches a secondary database (specified by its index in the configuration)
35 : : * to the primary SQLite database connection using the ATTACH DATABASE command.
36 : : *
37 : : * @param[in] db_A Index of the database path in the configuration array.
38 : : * @param[in] db_B Database number (1 or 2) to be used in the ATTACH DATABASE command.
39 : : * @return Return structure indicating the operation status.
40 : : */
41 : 140 : static Return db_attach(
42 : : int db_A,
43 : : int db_B)
44 : : {
45 : : /* Status returned by this function through provide()
46 : : Default value assumes successful completion */
47 : 140 : Return status = SUCCESS;
48 : :
49 : 140 : char *select_sql = NULL;
50 : :
51 [ + - - + ]: 140 : run(compose_sql(&select_sql,config->db_file_paths[db_A],db_B));
52 : :
53 [ + - ]: 140 : if(SUCCESS == status)
54 : : {
55 : 140 : int rc = sqlite3_exec(config->db,select_sql,NULL,NULL,NULL);
56 : :
57 [ - + ]: 140 : if(rc!= SQLITE_OK)
58 : : {
59 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't execute");
60 : 0 : status = FAILURE;
61 : : }
62 : : }
63 : :
64 : 140 : free(select_sql);
65 : :
66 : 140 : provide(status);
67 : : }
68 : :
69 : : /**
70 : : * @brief Detach database by alias
71 : : *
72 : : * @param[in] db_alias Attached database alias name
73 : : * @return Return status code
74 : : */
75 : 140 : static Return db_detach(const char *db_alias)
76 : : {
77 : : /* Status returned by this function through provide()
78 : : Default value assumes successful completion */
79 : 140 : Return status = SUCCESS;
80 : :
81 : 140 : sqlite3_stmt *stmt = NULL;
82 : :
83 : 140 : const char *sql = "DETACH DATABASE ?1;";
84 : :
85 : 140 : int rc = sqlite3_prepare_v2(config->db,sql,-1,&stmt,NULL);
86 : :
87 [ - + ]: 140 : if(SQLITE_OK != rc)
88 : : {
89 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to prepare detach statement");
90 : 0 : status = FAILURE;
91 : : }
92 : :
93 [ + - ]: 140 : if(SUCCESS == status)
94 : : {
95 : 140 : rc = sqlite3_bind_text(stmt,1,db_alias,-1,SQLITE_STATIC);
96 : :
97 [ - + ]: 140 : if(SQLITE_OK != rc)
98 : : {
99 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to bind database alias in detach");
100 : 0 : status = FAILURE;
101 : : }
102 : : }
103 : :
104 [ + - ]: 140 : if(SUCCESS == status)
105 : : {
106 : 140 : rc = sqlite3_step(stmt);
107 : :
108 [ - + ]: 140 : if(SQLITE_DONE != rc)
109 : : {
110 : 0 : log_sqlite_error(config->db,rc,NULL,"Detach statement didn't return DONE");
111 : 0 : status = FAILURE;
112 : : }
113 : : }
114 : :
115 [ + - ]: 140 : if(stmt != NULL)
116 : : {
117 : 140 : sqlite3_finalize(stmt);
118 : : }
119 : :
120 : 140 : provide(status);
121 : : }
122 : :
123 : : /**
124 : : * @brief Compares changes between two databases.
125 : : *
126 : : * This function executes a provided SQL query to compare differences between two databases.
127 : : * It identifies files that exist in one database but not the other, updating flags to reflect the comparison results.
128 : : *
129 : : * @param[in] compare_sql SQL query string for comparison.
130 : : * @param[out] files_the_same Flag indicating whether files are identical between the databases.
131 : : * @param[in] db_A Index of the first database in the configuration array.
132 : : * @param[in] db_B Index of the second database in the configuration array.
133 : : * @return Return structure indicating the operation status.
134 : : */
135 : 116 : static Return db_changes(
136 : : const char *compare_sql,
137 : : bool *files_the_same,
138 : : int db_A,
139 : : int db_B)
140 : : {
141 : : /* Status returned by this function through provide()
142 : : Default value assumes successful completion */
143 : 116 : Return status = SUCCESS;
144 : :
145 : 116 : bool first_iteration = true;
146 : :
147 : 116 : sqlite3_stmt *select_stmt = NULL;
148 : :
149 : 116 : int rc = sqlite3_prepare_v2(config->db,compare_sql,-1,&select_stmt,NULL);
150 : :
151 [ - + ]: 116 : if(SQLITE_OK != rc)
152 : : {
153 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
154 : 0 : status = FAILURE;
155 : : }
156 : :
157 [ + + ]: 160 : while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
158 : : {
159 : 44 : *files_the_same = false;
160 : :
161 : : // Interrupt the loop smoothly
162 : : // Interrupt when Ctrl+C
163 [ - + ]: 44 : if(global_interrupt_flag == true)
164 : : {
165 : 0 : break;
166 : : }
167 : :
168 [ + - ]: 44 : if(first_iteration == true)
169 : : {
170 : 44 : first_iteration = false;
171 : 44 : slog(EVERY,BOLD "These files are no longer in the %s but still exist in the %s" RESET "\n",config->db_file_names[db_A],config->db_file_names[db_B]);
172 : : }
173 : :
174 : 44 : const unsigned char *relative_path = NULL;
175 : 44 : relative_path = sqlite3_column_text(select_stmt,0);
176 : :
177 [ + - ]: 44 : if(relative_path != NULL)
178 : : {
179 : 44 : slog(EVERY|UNDECOR,"%s\n",relative_path);
180 : : } else {
181 : 0 : slog(ERROR,"General database error!\n");
182 : 0 : status = FAILURE;
183 : 0 : break;
184 : : }
185 : : }
186 : :
187 [ - + ]: 116 : if(SQLITE_DONE != rc)
188 : : {
189 : 0 : log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
190 : 0 : status = FAILURE;
191 : : }
192 : :
193 [ + - ]: 116 : if(select_stmt != NULL)
194 : : {
195 : 116 : rc = sqlite3_finalize(select_stmt);
196 : :
197 [ - + ]: 116 : if(SQLITE_OK != rc)
198 : : {
199 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize SQLite statement");
200 : 0 : status = FAILURE;
201 : : } else {
202 : 116 : select_stmt = NULL;
203 : : }
204 : : }
205 : :
206 : 116 : provide(status);
207 : : }
208 : :
209 : : /**
210 : : * @brief Compare two databases
211 : : * @details Compares content of two databases specified in Config structure
212 : : * Checks for file existence, missing files and SHA512 checksums
213 : : * @return Return enum indicating operation status
214 : : */
215 : 311 : Return db_compare(void)
216 : : {
217 : : /* Status returned by this function through provide()
218 : : Default value assumes successful completion */
219 : 311 : Return status = SUCCESS;
220 : :
221 : 311 : bool attached_db1 = false;
222 : 311 : bool attached_db2 = false;
223 : :
224 : : /* Interrupt the function smoothly */
225 : : /* Interrupt when Ctrl+C */
226 [ - + ]: 311 : if(global_interrupt_flag == true)
227 : : {
228 : 0 : provide(status);
229 : : }
230 : :
231 : : /* Skip if comparison mode is not enabled */
232 [ + + ]: 311 : if(config->compare != true)
233 : : {
234 : 235 : slog(TRACE,"Database comparison mode is not enabled. Skipping comparison\n");
235 : 235 : provide(status);
236 : : }
237 : :
238 : 76 : slog(EVERY,"The comparison of %s and %s databases is starting…\n",
239 : : config->db_file_names[0],
240 : : config->db_file_names[1]);
241 : :
242 : : /* Validate database paths */
243 [ + + ]: 220 : for(int i = 0; config->db_file_paths[i]; i++)
244 : : {
245 [ - + ]: 150 : if(NOT_FOUND == file_availability(config->db_file_paths[i],SHOULD_BE_A_FILE))
246 : : {
247 : 0 : slog(ERROR,"The database file %s is either inaccessible or not a valid file\n",
248 : : config->db_file_paths[i]);
249 : 0 : status = FAILURE;
250 : 0 : break;
251 : : }
252 : :
253 [ + - ]: 150 : if(SUCCESS == status)
254 : : {
255 : : /*
256 : : * Validate the integrity of the database file
257 : : */
258 : 150 : status = db_test(config->db_file_paths[i]);
259 : :
260 [ + + ]: 150 : if(SUCCESS != status)
261 : : {
262 : 6 : break;
263 : : }
264 : : }
265 : : }
266 : :
267 : : /* Attach databases */
268 [ + + ]: 76 : if(SUCCESS == status)
269 : : {
270 : : // Attach the database 1
271 : 70 : status = db_attach(0,1);
272 : :
273 [ + - ]: 70 : if(SUCCESS == status)
274 : : {
275 : 70 : attached_db1 = true;
276 : : }
277 : : }
278 : :
279 [ + + ]: 76 : if(SUCCESS == status)
280 : : {
281 : : // Attach the database 2
282 : 70 : status = db_attach(1,2);
283 : :
284 [ + - ]: 70 : if(SUCCESS == status)
285 : : {
286 : 70 : attached_db2 = true;
287 : : }
288 : : }
289 : :
290 : : /* SQL queries for comparison */
291 : 76 : const char *compare_A_sql = "SELECT a.relative_path "
292 : : "FROM db2.files AS a "
293 : : "LEFT JOIN db1.files AS b on b.relative_path = a.relative_path "
294 : : "WHERE b.relative_path IS NULL "
295 : : "ORDER BY a.relative_path ASC;";
296 : :
297 : 76 : const char *compare_B_sql = "SELECT a.relative_path "
298 : : "FROM db1.files AS a "
299 : : "LEFT join db2.files AS b on b.relative_path = a.relative_path "
300 : : "WHERE b.relative_path IS NULL "
301 : : "ORDER BY a.relative_path ASC;";
302 : :
303 : : // True when user provided at least one --compare-filter option.
304 : : // False means default compare mode: all three categories are enabled.
305 : 152 : const bool filter_specified = config->compare_filter_checksum_mismatch == true
306 [ + + ]: 60 : || config->compare_filter_second_source_only == true
307 [ + + + + ]: 136 : || config->compare_filter_first_source_only == true;
308 : :
309 : : // Enables "first-source-only" category:
310 : : // show paths that exist in db2 but are missing in db1.
311 : : // This category is active either explicitly by filter or by default mode.
312 : 152 : const bool check_first_source_only = config->compare_filter_first_source_only == true
313 [ + + + + ]: 76 : || filter_specified == false;
314 : :
315 : : // Enables "second-source-only" category:
316 : : // show paths that exist in db1 but are missing in db2.
317 : : // This category is active either explicitly by filter or by default mode.
318 : 152 : const bool check_second_source_only = config->compare_filter_second_source_only == true
319 [ + + + + ]: 76 : || filter_specified == false;
320 : :
321 : : // Enables checksum verification category for common relative paths.
322 : : // Active either explicitly by checksum filter or by default mode.
323 : 152 : const bool verify_checksum_consistency = config->compare_filter_checksum_mismatch == true
324 [ + + + + ]: 76 : || filter_specified == false;
325 : :
326 : 76 : bool files_the_same = true;
327 : :
328 : : /* Compare files existence between databases */
329 [ + + ]: 76 : if(check_first_source_only == true)
330 : : {
331 [ + + - + ]: 64 : run(db_changes(compare_B_sql,
332 : : &files_the_same,
333 : : 1,
334 : : 0));
335 : : }
336 : :
337 [ + + ]: 76 : if(check_second_source_only == true)
338 : : {
339 [ + + - + ]: 64 : run(db_changes(compare_A_sql,
340 : : &files_the_same,
341 : : 0,
342 : : 1));
343 : : }
344 : :
345 : : #if 0
346 : : // Old multiPATH solutions
347 : : const char *compare_checksums = "select a.relative_path from db2.files a inner join db1.files b"
348 : : " on b.relative_path = a.relative_path "
349 : : " and b.sha512 != a.sha512"
350 : : " order by a.relative_path asc;";
351 : :
352 : : const char *compare_checksums = "SELECT p.path,f1.relative_path "
353 : : "FROM db1.files AS f1 "
354 : : "JOIN db1.paths AS p ON f1.path_prefix_index = p.ID "
355 : : "JOIN db2.files AS f2 ON f1.relative_path = f2.relative_path "
356 : : "JOIN db2.paths AS p2 ON f2.path_prefix_index = p2.ID "
357 : : "WHERE f1.sha512 <> f2.sha512 AND p.path = p2.path "
358 : : "ORDER BY p.path,f1.relative_path ASC;";
359 : : #else
360 : : // One PATH solution
361 : 76 : const char *compare_checksums = "SELECT a.relative_path "
362 : : "FROM db2.files AS a "
363 : : "INNER JOIN db1.files b on b.relative_path = a.relative_path and b.sha512 != a.sha512 "
364 : : "ORDER BY a.relative_path ASC;";
365 : : #endif
366 : :
367 : : /* Compare SHA512 checksums */
368 : 76 : sqlite3_stmt *select_stmt = NULL;
369 : 76 : bool first_iteration = true;
370 : 76 : bool all_checksums_match = true;
371 : :
372 [ + + ]: 76 : if(verify_checksum_consistency == true)
373 : : {
374 [ + + ]: 64 : if(SUCCESS == status)
375 : : {
376 : 58 : int rc = sqlite3_prepare_v2(config->db,
377 : : compare_checksums,
378 : : -1,
379 : : &select_stmt,
380 : : NULL);
381 : :
382 [ - + ]: 58 : if(SQLITE_OK != rc)
383 : : {
384 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
385 : 0 : status = FAILURE;
386 : : }
387 : :
388 [ + - ]: 58 : if(SUCCESS == status)
389 : : {
390 [ + + ]: 100 : while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
391 : : {
392 : 42 : all_checksums_match = false;
393 : :
394 : : // Interrupt the loop smoothly
395 : : // Interrupt when Ctrl+C
396 [ - + ]: 42 : if(global_interrupt_flag == true)
397 : : {
398 : 0 : break;
399 : : }
400 : :
401 [ + + ]: 42 : if(first_iteration == true)
402 : : {
403 : 26 : first_iteration = false;
404 : 26 : slog(EVERY,BOLD "The SHA512 checksums of these files do not match between %s and %s" RESET "\n",
405 : : config->db_file_names[0],
406 : : config->db_file_names[1]);
407 : : }
408 : :
409 : : #if 0
410 : : const unsigned char *relative_path = NULL;
411 : : const unsigned char *path_prefix = NULL;
412 : : path_prefix = sqlite3_column_text(select_stmt,0);
413 : : relative_path = sqlite3_column_text(select_stmt,1);
414 : : #endif
415 : :
416 : 42 : const unsigned char *relative_path = sqlite3_column_text(select_stmt,0);
417 : :
418 [ + - ]: 42 : if(relative_path != NULL)
419 : : {
420 : 42 : slog(EVERY|UNDECOR,"%s\n",relative_path);
421 : : } else {
422 : 0 : slog(ERROR,"General database error!\n");
423 : 0 : status = FAILURE;
424 : 0 : break;
425 : : }
426 : : }
427 : :
428 [ - + ]: 58 : if(SQLITE_DONE != rc)
429 : : {
430 : 0 : log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
431 : 0 : status = FAILURE;
432 : : }
433 : : }
434 : : }
435 : : }
436 : :
437 : : /* Cleanup */
438 [ + + ]: 76 : if(attached_db2 == true)
439 : : {
440 [ - + - + ]: 70 : call(db_finalize(config->db,"db2",&select_stmt));
441 : : }
442 : :
443 [ + + ]: 76 : if(attached_db1 == true)
444 : : {
445 : 70 : sqlite3_stmt *no_stmt = NULL;
446 [ - + - + ]: 70 : call(db_finalize(config->db,"db1",&no_stmt));
447 : :
448 : : /* Detach databases in attach order */
449 [ - + - + ]: 70 : call(db_detach("db1"));
450 : : }
451 : :
452 [ + + ]: 76 : if(attached_db2 == true)
453 : : {
454 [ - + - + ]: 70 : call(db_detach("db2"));
455 : : }
456 : :
457 : : /* Output results */
458 [ + + ]: 76 : if(SUCCESS == status)
459 : : {
460 [ + + + + ]: 70 : if((check_first_source_only == true || check_second_source_only == true)
461 [ + + ]: 66 : && files_the_same == true
462 [ + + ]: 34 : && all_checksums_match == true)
463 : : {
464 : 32 : slog(EVERY,BOLD "All files are identical against %s and %s" RESET "\n",
465 : : config->db_file_names[0],
466 : : config->db_file_names[1]);
467 : : }
468 : :
469 [ + + + + ]: 70 : if(verify_checksum_consistency == true && all_checksums_match == true)
470 : : {
471 : 32 : slog(EVERY,BOLD "All SHA512 checksums of files are identical against %s and %s" RESET "\n",
472 : : config->db_file_names[0],
473 : : config->db_file_names[1]);
474 : : }
475 : :
476 [ + + ]: 70 : if(check_first_source_only == true
477 [ + + ]: 58 : && check_second_source_only == true
478 [ + + ]: 50 : && verify_checksum_consistency == true
479 [ + + ]: 46 : && files_the_same == true
480 [ + + ]: 24 : && all_checksums_match == true)
481 : : {
482 : 22 : slog(EVERY,BOLD "The databases %s and %s are absolutely equal" RESET "\n",
483 : : config->db_file_names[0],
484 : : config->db_file_names[1]);
485 : : }
486 : : }
487 : :
488 : 76 : slog(EVERY,"Comparison of %s and %s databases is complete\n",
489 : : config->db_file_names[0],
490 : : config->db_file_names[1]);
491 : :
492 : 76 : provide(status);
493 : : }
|