Branch data Line data Source code
1 : : #include "precizer.h"
2 : : #include <errno.h>
3 : :
4 : : /**
5 : : * @brief Check whether an unavailable file must be reported as a lock violation
6 : : *
7 : : * @param[in] relative_path Relative path descriptor from the database
8 : : * @param[in] access_status Current unavailable status for the path
9 : : *
10 : : * @return Return status code:
11 : : * - SUCCESS|YES: A checksum-locked unavailable violation was reported
12 : : * - SUCCESS|NO: No checksum-locked unavailable violation was reported
13 : : * - FAILURE|YES: A checksum-locked unavailable violation was detected, but reporting failed
14 : : * - FAILURE|NO: Lock-checksum evaluation or reporting failed
15 : : */
16 : 50 : static Return db_check_locked_unavailable_violation(
17 : : const memory *relative_path,
18 : : const FileAccessStatus access_status)
19 : : {
20 : : /* This function was reviewed line by line by a human and is not AI-generated
21 : : Any change to this function requires separate explicit approval */
22 : :
23 : : /* Status returned by this function through provide()
24 : : Default value assumes successful completion */
25 : 50 : Return status = SUCCESS;
26 : :
27 : 50 : bool locked_unavailable_violation = false;
28 : :
29 : : /*
30 : : * YES means the unavailable path is checksum-locked, so it must be reported as a violation.
31 : : * NO means the path is not checksum-locked, so this unavailable state is not a lock violation
32 : : */
33 : 50 : if(ask(path_check_locked_checksum(relative_path)))
34 : : {
35 : 12 : status = show_locked_checksum_unavailable_violation(relative_path,
36 : : access_status,
37 : : NULL,
38 : : NULL);
39 : :
40 : 12 : locked_unavailable_violation = true;
41 : : }
42 : :
43 [ + + ]: 50 : if(locked_unavailable_violation == true)
44 : : {
45 : 12 : status |= YES;
46 : :
47 : : } else {
48 : 38 : status |= NO;
49 : : }
50 : :
51 : 50 : provide(status);
52 : : }
53 : :
54 : : /**
55 : : * @brief Keep checksum-locked rows even when --db-drop-ignored matched them
56 : : *
57 : : * The function checks the locked path relative to an already opened root
58 : : * directory. An unavailable locked path is reported and kept
59 : : *
60 : : * @param[in] root_directory_fd Open descriptor for the root directory. Used
61 : : * as the base for the relative access check
62 : : * @param[in] relative_path Relative path descriptor from the database
63 : : * @param[out] locked_unavailable_violation_out Set to true when this call reported a locked unavailable path
64 : : *
65 : : * @return Return status code:
66 : : * - SUCCESS|YES: The row must stay in the DB
67 : : * - SUCCESS|NO: The row does not need checksum-lock preservation
68 : : * - FAILURE|NO: Validation, lock-checksum evaluation, or reporting failed
69 : : */
70 : 66 : static Return db_preserve_locked_ignored_record(
71 : : const int root_directory_fd,
72 : : const memory *relative_path,
73 : : bool *locked_unavailable_violation_out)
74 : : {
75 : : /* This changed function requires a new line-by-line human review before it is considered trusted */
76 : :
77 : : /* Status returned by this function through provide()
78 : : Default value assumes successful completion */
79 : 66 : Return status = SUCCESS;
80 : :
81 [ - + ]: 66 : if(locked_unavailable_violation_out == NULL)
82 : : {
83 : 0 : provide(FAILURE | NO);
84 : : }
85 : :
86 : 66 : *locked_unavailable_violation_out = false;
87 : :
88 : 66 : bool keep_locked_record = false;
89 : :
90 : : /*
91 : : * YES means the ignored DB row is checksum-locked and must be checked for preservation.
92 : : * NO means the row is not checksum-locked, so this helper does not preserve it
93 : : */
94 : 66 : if(ask(path_check_locked_checksum(relative_path)))
95 : : {
96 : : /*
97 : : * Check the path relative to the open root directory without
98 : : * constructing an absolute path
99 : : */
100 : 16 : FileAccessStatus access_status = file_check_access(
101 : : root_directory_fd,
102 : : relative_path,
103 : : R_OK);
104 : :
105 [ + - + + ]: 16 : if(SUCCESS == status && access_status != FILE_ACCESS_ALLOWED)
106 : : {
107 : : /*
108 : : * YES means the unavailable checksum-locked row was reported as a violation.
109 : : * NO means no violation was reported for this unavailable row
110 : : */
111 : 6 : if(ask(db_check_locked_unavailable_violation(relative_path,access_status)))
112 : : {
113 : 6 : *locked_unavailable_violation_out = true;
114 : : }
115 : :
116 : : }
117 : :
118 [ + - ]: 16 : if(TRIUMPH & status)
119 : : {
120 : 16 : keep_locked_record = true;
121 : : }
122 : : }
123 : :
124 [ + + ]: 66 : if(keep_locked_record == true)
125 : : {
126 : 16 : status |= YES;
127 : :
128 : : } else {
129 : 50 : status |= NO;
130 : : }
131 : :
132 : 66 : provide(status);
133 : : }
134 : :
135 : : /**
136 : : * @brief Remove stale file records from the primary database after an update
137 : : *
138 : : * @details
139 : : * The function walks through file records stored in the primary database and
140 : : * decides whether each row should remain there
141 : : *
142 : : * One root prefix is retrieved from the `paths` table and opened once. Every
143 : : * file row is checked relative to that same directory descriptor. The current
144 : : * cleanup path therefore requires the database to describe a single root and
145 : : * does not distinguish file rows belonging to different stored roots
146 : : *
147 : : * A row can be removed when the file is no longer present on disk, when it is
148 : : * inaccessible or its access check fails and `--db-drop-inaccessible` is active,
149 : : * or when the path is ignored and `--db-drop-ignored` is enabled. Ignored rows
150 : : * are otherwise kept outside missing-file cleanup, except checksum-locked rows
151 : : * that must still report unavailable protected files
152 : : *
153 : : * If the root cannot be opened for any reason, its cleanup is skipped without
154 : : * changing file rows or returning an error. This prevents a temporary root
155 : : * access problem from being mistaken for missing or inaccessible files
156 : : *
157 : : * Checksum-locked paths are file paths matched by a `--lock-checksum` pattern.
158 : : * Their stored checksums serve as protected integrity references, so cleanup
159 : : * must not silently remove their database rows when the files cannot be verified
160 : : * If a locked file is missing or unavailable, the function keeps the database
161 : : * row, reports the violation, and returns a warning instead of deleting it
162 : : * A NULL `files.relative_path` value violates the database contract and stops
163 : : * cleanup with `FAILURE`
164 : : *
165 : : * In `--compare` mode, or when `--update` is not active, the function exits
166 : : * without performing cleanup
167 : : * In `--dry-run` mode it evaluates the same cleanup decisions without deleting
168 : : * rows from the database
169 : : *
170 : : * @return Return status code:
171 : : * - SUCCESS: Cleanup completed without lock-checksum warnings
172 : : * - WARNING: One or more unavailable checksum-locked paths were found
173 : : * - FAILURE: Cleanup was interrupted by an internal error
174 : : */
175 : 441 : Return db_delete_missing_metadata(void)
176 : : {
177 : : /* Status returned by this function through provide()
178 : : Default value assumes successful completion */
179 : 441 : Return status = SUCCESS;
180 : :
181 : : /* Interrupt the function smoothly */
182 : : /* Interrupt when Ctrl+C */
183 [ - + ]: 441 : if(global_interrupt_flag == true)
184 : : {
185 : 0 : provide(status);
186 : : }
187 : :
188 : : /* Skip in comparison mode */
189 [ + + ]: 441 : if(config->compare == true)
190 : : {
191 : 112 : slog(TRACE,"Comparison mode is enabled. The primary database does not require cleanup\n");
192 : 112 : provide(status);
193 : : }
194 : :
195 : : /* Update mode should be enabled */
196 [ + + ]: 329 : if(config->update == true)
197 : : {
198 : 128 : slog(EVERY,"Searching for files that no longer exist on the file system…\n");
199 : :
200 : : } else {
201 : : // Don't do anything
202 : 201 : provide(status);
203 : : }
204 : :
205 [ + + + - ]: 128 : if(config->dry_run == true && config->db_primary_file_exists == true)
206 : : {
207 : 11 : slog(TRACE,"Dry-run mode is enabled. The primary database will remain unchanged\n");
208 : : }
209 : :
210 : : // Print deletion banners only once per run
211 : 128 : bool first_iteration = true;
212 : :
213 : : // Tracks whether cleanup reported an unavailable checksum-locked path
214 : 128 : bool locked_unavailable_violation_detected = false;
215 : :
216 : : // Stores the root directory path retrieved from the database
217 : 128 : m_create(char,root_path,MEMORY_STRING);
218 : :
219 : : // Stores the relative path of the file record currently being processed
220 : 128 : m_create(char,relative_path,MEMORY_STRING);
221 : :
222 : : // Holds the prepared SQLite statement used to iterate over file records
223 : 128 : sqlite3_stmt *select_stmt = NULL;
224 : :
225 : : // Stores the result code returned by the most recent SQLite operation
226 : 128 : int rc = 0;
227 : :
228 : : // Identifies the open root directory used as the base for relative access checks
229 : 128 : int root_directory_fd = -1;
230 : :
231 : : // Stores whether the database root was opened for directory-relative checks
232 : 128 : FileAccessStatus root_access_status = FILE_ACCESS_ERROR;
233 : :
234 : : /*
235 : : * Load the shared root path and open it once before processing file rows.
236 : : * Each file record stores only a relative path, so access checks use this
237 : : * directory descriptor. If the root cannot be opened, the open helper has
238 : : * already reported and remembered the warning, and cleanup safely stops
239 : : * without treating that root-level access problem as a technical failure
240 : : */
241 : 128 : run(db_retrieve_root_path(root_path));
242 : :
243 [ + - ]: 128 : if(SUCCESS == status)
244 : : {
245 : 128 : root_access_status = directory_open_root(root_path,&root_directory_fd);
246 : : }
247 : :
248 : : /*
249 : : * Without an open root, file rows cannot be classified safely.
250 : : * Release local memory and keep the current status when the root was not
251 : : * opened or no descriptor was returned for directory-relative checks
252 : : */
253 [ + + - + ]: 128 : if(root_access_status != FILE_ACCESS_ALLOWED || root_directory_fd < 0)
254 : : {
255 : 6 : call(m_del(relative_path));
256 : 6 : call(m_del(root_path));
257 : :
258 : 6 : provide(status);
259 : : }
260 : :
261 [ + - ]: 122 : if(SUCCESS == status)
262 : : {
263 : : #if 0 // Disabled multi-root path index implementation
264 : : const char *select_sql = "SELECT files.ID,paths.prefix,files.relative_path FROM files LEFT JOIN paths ON files.root_path_index = paths.ID;";
265 : : #endif
266 : 122 : const char *select_sql = "SELECT ID,relative_path FROM files;";
267 : :
268 : 122 : rc = sqlite3_prepare_v2(config->db,select_sql,-1,&select_stmt,NULL);
269 : :
270 [ - + ]: 122 : if(SQLITE_OK != rc)
271 : : {
272 : 0 : log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement");
273 : 0 : status = FAILURE;
274 : : }
275 : : }
276 : :
277 : : /*
278 : : * Iterate over every file record selected from the files table
279 : : */
280 [ + - + + ]: 1606 : while(SUCCESS == status && SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
281 : : {
282 : : /* Interrupt the loop smoothly */
283 : : /* Interrupt when Ctrl+C */
284 [ - + ]: 1484 : if(global_interrupt_flag == true)
285 : : {
286 : 0 : break;
287 : : }
288 : :
289 : 1484 : sqlite_int64 ID = sqlite3_column_int64(select_stmt,0);
290 : :
291 : 1484 : const unsigned char *db_relative_path = sqlite3_column_text(select_stmt,1);
292 : :
293 : : // Marks paths excluded by the current --ignore/--include scope
294 : 1484 : bool ignored = false;
295 : :
296 : : // Marks deletions triggered by --db-drop-ignored
297 : 1484 : bool drop_ignored = false;
298 : :
299 : : // Marks deletions triggered by unavailable access when --db-drop-inaccessible is active
300 : 1484 : bool inaccessible = false;
301 : :
302 : : // Marks deletions triggered by a missing path on disk
303 : 1484 : bool file_not_found = false;
304 : :
305 : : // Aggregates whether the current record should be deleted for any reason
306 : 1484 : bool should_delete = false;
307 : :
308 : : /*
309 : : * files.relative_path is required by the database contract to be non-NULL.
310 : : * This is the single boundary check before the helper chain below relies on that invariant
311 : : */
312 [ - + ]: 1484 : if(db_relative_path == NULL)
313 : : {
314 : 0 : slog(ERROR,"The files table returned a NULL relative path\n");
315 : 0 : status = FAILURE;
316 : 0 : break;
317 : : }
318 : :
319 : : /*
320 : : * sqlite3_column_bytes() returns the relative path length without the
321 : : * terminating NUL byte, so add one byte to copy a complete C string
322 : : */
323 : 1484 : run(m_copy_fixed_string(relative_path,(size_t)sqlite3_column_bytes(select_stmt,1) + 1U,db_relative_path));
324 : :
325 [ - + ]: 1484 : if(SUCCESS != status)
326 : : {
327 : 0 : break;
328 : : }
329 : :
330 : 1484 : const char *runtime_relative_path = m_text(relative_path);
331 : :
332 : : /*
333 : : * Apply the current include and ignore scope to database rows before
334 : : * checking filesystem availability. Ignored rows stay outside normal
335 : : * missing-file cleanup unless --db-drop-ignored explicitly allows removing them
336 : : */
337 [ + + ]: 1484 : if(config->ignore != NULL)
338 : : {
339 : 474 : status = match_include_ignore(relative_path,NULL,&ignored);
340 : :
341 [ - + ]: 474 : if(SUCCESS != status)
342 : : {
343 : 0 : break;
344 : : }
345 : : }
346 : :
347 [ + + ]: 1484 : if(ignored == true)
348 : : {
349 [ + + ]: 82 : if(config->db_drop_ignored == true)
350 : : {
351 : 66 : drop_ignored = true;
352 : :
353 : : } else {
354 : :
355 : : /*
356 : : * YES means the ignored row is checksum-locked and still needs
357 : : * an availability check. NO means normal ignore protection keeps
358 : : * the row without touching the filesystem
359 : : */
360 : 16 : if(ask(path_check_locked_checksum(relative_path)))
361 : : {
362 : 0 : FileAccessStatus access_status = file_check_access(root_directory_fd,relative_path,R_OK);
363 : :
364 [ # # ]: 0 : if(access_status != FILE_ACCESS_ALLOWED)
365 : : {
366 : : /*
367 : : * YES means the unavailable ignored row was reported as a checksum-lock violation.
368 : : * NO is not expected after the positive lock-check above, but keeps status handling explicit
369 : : */
370 : 0 : if(ask(db_check_locked_unavailable_violation(relative_path,access_status)))
371 : : {
372 : 0 : locked_unavailable_violation_detected = true;
373 : : }
374 : :
375 [ # # ]: 0 : if(SUCCESS != status)
376 : : {
377 : 0 : break;
378 : : }
379 : : }
380 : : }
381 : :
382 [ - + ]: 16 : if(SUCCESS != status)
383 : : {
384 : 0 : break;
385 : : }
386 : :
387 : 1402 : continue;
388 : : }
389 : : }
390 : :
391 [ + + ]: 1468 : if(drop_ignored == true)
392 : : {
393 : 66 : bool locked_unavailable_violation = false;
394 : :
395 : : /*
396 : : * YES means the ignored DB row must be kept because it is checksum-locked.
397 : : * NO means checksum-lock protection does not apply here, so the ignored row may be deleted
398 : : */
399 : 66 : if(ask(db_preserve_locked_ignored_record(root_directory_fd,relative_path,&locked_unavailable_violation)))
400 : : {
401 [ + + ]: 16 : if(locked_unavailable_violation == true)
402 : : {
403 : 6 : locked_unavailable_violation_detected = true;
404 : : }
405 : :
406 : 16 : continue;
407 : : }
408 : :
409 [ - + ]: 50 : if(SUCCESS != status)
410 : : {
411 : 0 : break;
412 : : }
413 : :
414 : 50 : should_delete = true;
415 : :
416 : : } else {
417 : :
418 : : /*
419 : : * This changed access-check block requires a new line-by-line human
420 : : * review before it is considered trusted
421 : : *
422 : : * Check the path relative to the open root directory without
423 : : * constructing an absolute path
424 : : */
425 : 1402 : FileAccessStatus access_status = file_check_access(root_directory_fd,relative_path,R_OK);
426 : :
427 [ + + ]: 1402 : if(access_status == FILE_ACCESS_ALLOWED)
428 : : {
429 : 1358 : continue;
430 : :
431 [ + + + + ]: 44 : } else if(access_status == FILE_ACCESS_DENIED || access_status == FILE_ACCESS_ERROR){
432 : :
433 : : /*
434 : : * YES means the unavailable DB row is checksum-locked and was reported as a violation.
435 : : * NO means no checksum-lock violation was reported, so normal inaccessible handling continues
436 : : */
437 : 14 : if(ask(db_check_locked_unavailable_violation(relative_path,access_status)))
438 : : {
439 : 4 : locked_unavailable_violation_detected = true;
440 : 4 : continue;
441 : : }
442 : :
443 [ - + ]: 10 : if(SUCCESS != status)
444 : : {
445 : 0 : break;
446 : : }
447 : :
448 [ + + ]: 10 : if(config->db_drop_inaccessible == true)
449 : : {
450 : 4 : inaccessible = true;
451 : 4 : should_delete = true;
452 : :
453 : : } else {
454 : 6 : slog(EVERY|UNDECOR,"kept inaccessible %s\n",runtime_relative_path);
455 : 6 : continue;
456 : : }
457 : :
458 [ + - ]: 30 : } else if(access_status == FILE_NOT_FOUND){
459 : :
460 : : /*
461 : : * YES means the missing DB row is checksum-locked and was reported as a violation.
462 : : * NO means no checksum-lock violation was reported, so normal missing-file handling continues
463 : : */
464 : 30 : if(ask(db_check_locked_unavailable_violation(relative_path,access_status)))
465 : : {
466 : 2 : locked_unavailable_violation_detected = true;
467 : 2 : continue;
468 : : }
469 : :
470 [ - + ]: 28 : if(SUCCESS != status)
471 : : {
472 : 0 : break;
473 : : }
474 : :
475 : 28 : file_not_found = true;
476 : 28 : should_delete = true;
477 : :
478 : : }
479 : : }
480 : :
481 [ + - ]: 82 : if(should_delete == true)
482 : : {
483 [ + + ]: 82 : if(first_iteration == true)
484 : : {
485 : 38 : first_iteration = false;
486 : :
487 [ + + ]: 38 : if(config->ignore != NULL)
488 : : {
489 [ + + ]: 22 : if(config->db_drop_ignored == false)
490 : : {
491 : 6 : slog(EVERY,"If the information about ignored files should be removed from the database the " BOLD "--db-drop-ignored" RESET " option must be specified. This is special protection against accidental deletion of information from the database\n");
492 : : } else {
493 : 16 : slog(TRACE,"The " BOLD "--db-drop-ignored" RESET " option has been used, so the information about ignored files will be removed against the database %s\n",confstr(db_file_name));
494 : : }
495 : : }
496 : :
497 [ + + ]: 38 : if(config->db_drop_inaccessible)
498 : : {
499 : 2 : slog(EVERY,BOLD "Dropping DB records for missing, inaccessible, or ignored paths in %s:" RESET "\n",confstr(db_file_name));
500 : : } else {
501 : 36 : slog(EVERY,BOLD "Dropping DB records for missing or ignored paths in %s:" RESET "\n",confstr(db_file_name));
502 : : }
503 : : }
504 : :
505 : 82 : status = db_delete_the_record_by_id(&ID);
506 : :
507 [ - + ]: 82 : if(SUCCESS != status)
508 : : {
509 : 0 : break;
510 : : }
511 : :
512 [ + + ]: 82 : if(drop_ignored == true)
513 : : {
514 : 50 : slog(EVERY|UNDECOR|REMEMBER,"drop ignored %s\n",runtime_relative_path);
515 : :
516 [ + + ]: 32 : } else if(inaccessible == true){
517 : 4 : slog(EVERY|UNDECOR|REMEMBER,"drop due to inaccessible %s\n",runtime_relative_path);
518 : :
519 [ + - ]: 28 : } else if(file_not_found == true){
520 : 28 : slog(EVERY|UNDECOR,"no longer exists %s\n",runtime_relative_path);
521 : : }
522 : : }
523 : : }
524 : :
525 [ + - - + ]: 122 : if(SUCCESS == status && SQLITE_DONE != rc)
526 : : {
527 [ # # ]: 0 : if(global_interrupt_flag == false)
528 : : {
529 : 0 : log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
530 : 0 : status = FAILURE;
531 : : }
532 : : }
533 : :
534 : 122 : rc = sqlite3_finalize(select_stmt);
535 : :
536 [ + - - + ]: 122 : if(SUCCESS == status && SQLITE_OK != rc)
537 : : {
538 : 0 : log_sqlite_error(config->db,rc,NULL,"Failed to finalize select statement");
539 : 0 : status = FAILURE;
540 : : }
541 : :
542 [ - + ]: 122 : if(close(root_directory_fd) != 0)
543 : : {
544 : 0 : slog(ERROR,"Failed to close root directory descriptor: %s\n",strerror(errno));
545 : 0 : status = FAILURE;
546 : : }
547 : :
548 [ + - + - ]: 122 : if(SUCCESS == status && global_interrupt_flag == false)
549 : : {
550 : 122 : slog(EVERY,"Missing file search finished\n");
551 : : }
552 : :
553 : 122 : call(m_del(relative_path));
554 : 122 : call(m_del(root_path));
555 : :
556 [ + + ]: 122 : if(locked_unavailable_violation_detected == true)
557 : : {
558 : 12 : slog(EVERY,BOLD "Warning! Data corruption detected for checksum-locked file!" RESET "\n");
559 : :
560 [ + - ]: 12 : if(SUCCESS == status)
561 : : {
562 : 12 : status = WARNING;
563 : : }
564 : : }
565 : :
566 : 122 : provide(status);
567 : : }
|