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 64 : static Return compose_sql(
14 : char **sql,
15 : const char *db_path,
16 : int db_num)
17 : {
18 : /// The status that will be passed to return() before exiting.
19 : /// By default, the function worked without errors.
20 64 : Return status = SUCCESS;
21 :
22 64 : 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 64 : 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 64 : static Return db_attach(
42 : int db_A,
43 : int db_B)
44 : {
45 : /// The status that will be passed to return() before exiting.
46 : /// By default, the function worked without errors.
47 64 : Return status = SUCCESS;
48 :
49 64 : char *select_sql = NULL;
50 :
51 64 : run(compose_sql(&select_sql,config->db_file_paths[db_A],db_B));
52 :
53 64 : if(SUCCESS == status)
54 : {
55 64 : int rc = sqlite3_exec(config->db,select_sql,NULL,NULL,NULL);
56 :
57 64 : 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 64 : free(select_sql);
65 :
66 64 : 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 64 : static Return db_detach(const char *db_alias)
76 : {
77 : /// The status that will be passed to return() before exiting.
78 : /// By default, the function worked without errors.
79 64 : Return status = SUCCESS;
80 :
81 64 : sqlite3_stmt *stmt = NULL;
82 :
83 64 : const char *sql = "DETACH DATABASE ?1;";
84 :
85 64 : int rc = sqlite3_prepare_v2(config->db,sql,-1,&stmt,NULL);
86 :
87 64 : 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 64 : if(SUCCESS == status)
94 : {
95 64 : rc = sqlite3_bind_text(stmt,1,db_alias,-1,SQLITE_STATIC);
96 :
97 64 : 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 64 : if(SUCCESS == status)
105 : {
106 64 : rc = sqlite3_step(stmt);
107 :
108 64 : 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 64 : if(stmt != NULL)
116 : {
117 64 : sqlite3_finalize(stmt);
118 : }
119 :
120 64 : 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[out] the_databases_are_equal Flag indicating whether the databases are equal.
132 : * @param[in] db_A Index of the first database in the configuration array.
133 : * @param[in] db_B Index of the second database in the configuration array.
134 : * @return Return structure indicating the operation status.
135 : */
136 64 : static Return db_changes(
137 : const char *compare_sql,
138 : bool *files_the_same,
139 : bool *the_databases_are_equal,
140 : int db_A,
141 : int db_B)
142 : {
143 : /// The status that will be passed to return() before exiting.
144 : /// By default, the function worked without errors.
145 64 : Return status = SUCCESS;
146 :
147 64 : bool first_iteration = true;
148 :
149 64 : sqlite3_stmt *select_stmt = NULL;
150 :
151 64 : int rc = sqlite3_prepare_v2(config->db,compare_sql,-1,&select_stmt,NULL);
152 :
153 64 : if(SQLITE_OK != rc)
154 : {
155 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
156 0 : status = FAILURE;
157 : }
158 :
159 88 : while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
160 : {
161 24 : *the_databases_are_equal = false;
162 24 : *files_the_same = false;
163 :
164 : // Interrupt the loop smoothly
165 : // Interrupt when Ctrl+C
166 24 : if(global_interrupt_flag == true)
167 : {
168 0 : break;
169 : }
170 :
171 24 : if(first_iteration == true)
172 : {
173 24 : first_iteration = false;
174 24 : 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]);
175 : }
176 :
177 24 : const unsigned char *relative_path = NULL;
178 24 : relative_path = sqlite3_column_text(select_stmt,0);
179 :
180 24 : if(relative_path != NULL)
181 : {
182 24 : slog(EVERY|UNDECOR,"%s\n",relative_path);
183 : } else {
184 0 : slog(ERROR,"General database error!\n");
185 0 : status = FAILURE;
186 0 : break;
187 : }
188 : }
189 :
190 64 : if(SQLITE_DONE != rc)
191 : {
192 0 : log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
193 0 : status = FAILURE;
194 : }
195 :
196 64 : if(select_stmt != NULL)
197 : {
198 64 : rc = sqlite3_finalize(select_stmt);
199 :
200 64 : if(SQLITE_OK != rc)
201 : {
202 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize SQLite statement");
203 0 : status = FAILURE;
204 : } else {
205 64 : select_stmt = NULL;
206 : }
207 : }
208 :
209 64 : provide(status);
210 : }
211 :
212 : /**
213 : * @brief Compare two databases
214 : * @details Compares content of two databases specified in Config structure
215 : * Checks for file existence, missing files and SHA512 checksums
216 : * @return Return enum indicating operation status
217 : */
218 192 : Return db_compare(void)
219 : {
220 : /// The status that will be passed to return() before exiting.
221 : /// By default, the function worked without errors.
222 192 : Return status = SUCCESS;
223 :
224 192 : bool attached_db1 = false;
225 192 : bool attached_db2 = false;
226 :
227 : /* Interrupt the function smoothly */
228 : /* Interrupt when Ctrl+C */
229 192 : if(global_interrupt_flag == true)
230 : {
231 0 : provide(status);
232 : }
233 :
234 : /* Skip if comparison mode is not enabled */
235 192 : if(config->compare != true)
236 : {
237 156 : slog(TRACE,"Database comparison mode is not enabled. Skipping comparison\n");
238 156 : provide(status);
239 : }
240 :
241 36 : slog(EVERY,"The comparison of %s and %s databases is starting…\n",
242 : config->db_file_names[0],
243 : config->db_file_names[1]);
244 :
245 : /* Validate database paths */
246 102 : for(int i = 0; config->db_file_paths[i]; i++)
247 : {
248 70 : if(NOT_FOUND == file_availability(config->db_file_paths[i],SHOULD_BE_A_FILE))
249 : {
250 0 : slog(ERROR,"The database file %s is either inaccessible or not a valid file\n",
251 : config->db_file_paths[i]);
252 0 : status = FAILURE;
253 0 : break;
254 : }
255 :
256 70 : if(SUCCESS == status)
257 : {
258 : /*
259 : * Validate the integrity of the database file
260 : */
261 70 : status = db_test(config->db_file_paths[i]);
262 :
263 70 : if(SUCCESS != status)
264 : {
265 4 : break;
266 : }
267 : }
268 : }
269 :
270 : /* Attach databases */
271 36 : if(SUCCESS == status)
272 : {
273 : // Attach the database 1
274 32 : status = db_attach(0,1);
275 :
276 32 : if(SUCCESS == status)
277 : {
278 32 : attached_db1 = true;
279 : }
280 : }
281 :
282 36 : if(SUCCESS == status)
283 : {
284 : // Attach the database 2
285 32 : status = db_attach(1,2);
286 :
287 32 : if(SUCCESS == status)
288 : {
289 32 : attached_db2 = true;
290 : }
291 : }
292 :
293 : /* SQL queries for comparison */
294 36 : const char *compare_A_sql = "SELECT a.relative_path "
295 : "FROM db2.files AS a "
296 : "LEFT JOIN db1.files AS b on b.relative_path = a.relative_path "
297 : "WHERE b.relative_path IS NULL "
298 : "ORDER BY a.relative_path ASC;";
299 :
300 36 : const char *compare_B_sql = "SELECT a.relative_path "
301 : "FROM db1.files AS a "
302 : "LEFT join db2.files AS b on b.relative_path = a.relative_path "
303 : "WHERE b.relative_path IS NULL "
304 : "ORDER BY a.relative_path ASC;";
305 :
306 36 : bool files_the_same = true;
307 36 : bool the_databases_are_equal = true;
308 :
309 : /* Compare files existence between databases */
310 36 : run(db_changes(compare_A_sql,
311 : &files_the_same,
312 : &the_databases_are_equal,
313 : 0,
314 : 1));
315 :
316 36 : run(db_changes(compare_B_sql,
317 : &files_the_same,
318 : &the_databases_are_equal,
319 : 1,
320 : 0));
321 :
322 : #if 0
323 : // Old multiPATH solutions
324 : const char *compare_checksums = "select a.relative_path from db2.files a inner join db1.files b"
325 : " on b.relative_path = a.relative_path "
326 : " and b.sha512 != a.sha512"
327 : " order by a.relative_path asc;";
328 :
329 : const char *compare_checksums = "SELECT p.path,f1.relative_path "
330 : "FROM db1.files AS f1 "
331 : "JOIN db1.paths AS p ON f1.path_prefix_index = p.ID "
332 : "JOIN db2.files AS f2 ON f1.relative_path = f2.relative_path "
333 : "JOIN db2.paths AS p2 ON f2.path_prefix_index = p2.ID "
334 : "WHERE f1.sha512 <> f2.sha512 AND p.path = p2.path "
335 : "ORDER BY p.path,f1.relative_path ASC;";
336 : #else
337 : // One PATH solution
338 36 : const char *compare_checksums = "SELECT a.relative_path "
339 : "FROM db2.files AS a "
340 : "INNER JOIN db1.files b on b.relative_path = a.relative_path and b.sha512 != a.sha512 "
341 : "ORDER BY a.relative_path ASC;";
342 : #endif
343 :
344 : /* Compare SHA512 checksums */
345 36 : sqlite3_stmt *select_stmt = NULL;
346 36 : bool first_iteration = true;
347 36 : bool checksums = true;
348 :
349 36 : if(SUCCESS == status)
350 : {
351 32 : int rc = sqlite3_prepare_v2(config->db,
352 : compare_checksums,
353 : -1,
354 : &select_stmt,
355 : NULL);
356 :
357 32 : if(SQLITE_OK != rc)
358 : {
359 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
360 0 : status = FAILURE;
361 : }
362 : }
363 :
364 36 : if(SUCCESS == status)
365 : {
366 : int rc;
367 :
368 64 : while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
369 : {
370 32 : the_databases_are_equal = false;
371 32 : checksums = false;
372 :
373 : // Interrupt the loop smoothly
374 : // Interrupt when Ctrl+C
375 32 : if(global_interrupt_flag == true)
376 : {
377 0 : break;
378 : }
379 :
380 32 : if(first_iteration == true)
381 : {
382 16 : first_iteration = false;
383 16 : slog(EVERY,BOLD "The SHA512 checksums of these files do not match between %s and %s" RESET "\n",
384 : config->db_file_names[0],
385 : config->db_file_names[1]);
386 : }
387 :
388 : #if 0
389 : const unsigned char *relative_path = NULL;
390 : const unsigned char *path_prefix = NULL;
391 : path_prefix = sqlite3_column_text(select_stmt,0);
392 : relative_path = sqlite3_column_text(select_stmt,1);
393 : #endif
394 :
395 32 : const unsigned char *relative_path = sqlite3_column_text(select_stmt,0);
396 :
397 32 : if(relative_path != NULL)
398 : {
399 32 : slog(EVERY|UNDECOR,"%s\n",relative_path);
400 : } else {
401 0 : slog(ERROR,"General database error!\n");
402 0 : status = FAILURE;
403 0 : break;
404 : }
405 : }
406 :
407 32 : if(SQLITE_DONE != rc)
408 : {
409 0 : log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
410 0 : status = FAILURE;
411 : }
412 : }
413 :
414 : /* Cleanup */
415 36 : if(attached_db2 == true)
416 : {
417 32 : call(db_finalize(config->db,"db2",&select_stmt));
418 : }
419 :
420 36 : if(attached_db1 == true)
421 : {
422 32 : sqlite3_stmt *no_stmt = NULL;
423 32 : call(db_finalize(config->db,"db1",&no_stmt));
424 : }
425 :
426 : /* Detach databases in attach order */
427 36 : if(attached_db1 == true)
428 : {
429 32 : call(db_detach("db1"));
430 : }
431 :
432 36 : if(attached_db2 == true)
433 : {
434 32 : call(db_detach("db2"));
435 : }
436 :
437 : /* Output results */
438 36 : if(SUCCESS == status)
439 : {
440 32 : if(files_the_same == true && checksums == true)
441 : {
442 12 : slog(EVERY,BOLD "All files are identical against %s and %s" RESET "\n",
443 : config->db_file_names[0],
444 : config->db_file_names[1]);
445 : }
446 :
447 32 : if(checksums == true)
448 : {
449 16 : slog(EVERY,BOLD "All SHA512 checksums of files are identical against %s and %s" RESET "\n",
450 : config->db_file_names[0],
451 : config->db_file_names[1]);
452 : }
453 :
454 32 : if(the_databases_are_equal == true)
455 : {
456 12 : slog(EVERY,BOLD "The databases %s and %s are absolutely equal" RESET "\n",
457 : config->db_file_names[0],
458 : config->db_file_names[1]);
459 : }
460 : }
461 :
462 36 : slog(EVERY,"Comparison of %s and %s databases is complete\n",
463 : config->db_file_names[0],
464 : config->db_file_names[1]);
465 :
466 36 : provide(status);
467 : }
|