Branch data Line data Source code
1 : : #include "precizer.h"
2 : : #include <errno.h>
3 : :
4 : : /**
5 : : * @brief Compare two FTS entries by filename
6 : : * @param first Pointer to first FTSENT structure
7 : : * @param second Pointer to second FTSENT structure
8 : : * @return Integer less than, equal to, or greater than zero if first is found,
9 : : * respectively, to be less than, to match, or be greater than second
10 : : */
11 : 7784 : static int compare_by_name(
12 : : #ifdef __CYGWIN__
13 : : const FTSENT * const *first,
14 : : const FTSENT * const *second)
15 : : #else
16 : : const FTSENT **first,
17 : : const FTSENT **second)
18 : : #endif
19 : : {
20 : 7784 : return strcmp((*first)->fts_name,(*second)->fts_name);
21 : : }
22 : :
23 : : /**
24 : : * @brief Traverse configured roots and process files for one pass
25 : : *
26 : : * The function is a no-op in compare mode. During normal scanning it walks each
27 : : * root in `config->roots` with a separate FTS stream. Paths written to the
28 : : * database are kept relative to the current root, so the same traversal logic
29 : : * works for one root or for several positional directory arguments
30 : : *
31 : : * `summary->stats_only_pass` controls how much work is done. When it is `true`,
32 : : * traversal only counts directories, files, symlinks, and allocated size, and
33 : : * that pass is skipped unless progress output needs those counters. When it is
34 : : * `false`, regular files can be hashed, compared with existing database rows,
35 : : * inserted, updated, or reported according to ignore/include and checksum
36 : : * locking settings
37 : : *
38 : : * For example, after `precizer src tests`, `config->roots` contains two roots.
39 : : * This function traverses `src` first, closes that FTS stream, then traverses
40 : : * `tests`
41 : : *
42 : : * @param summary Traversal state that is reset and populated by this call
43 : : * @return `SUCCESS` when traversal completes, `WARNING` when a non-fatal
44 : : * checksum-lock violation is reported, or `FAILURE` for hard traversal,
45 : : * memory, hashing, or database errors
46 : : */
47 : 948 : Return file_list(TraversalSummary *summary)
48 : : {
49 : : /* Status returned by this function through provide()
50 : : Default value assumes successful completion */
51 : 948 : Return status = SUCCESS;
52 : :
53 : : // Don't do anything
54 [ + + ]: 948 : if(config->compare == true)
55 : : {
56 : 224 : provide(status);
57 : : }
58 : :
59 : : #ifdef TESTITALL_TEST_HOOKS
60 : : {
61 : 724 : const char *skip = getenv("TESTITALL_TEST_ENV_SKIP_FILE_LIST");
62 : :
63 [ + + + + ]: 724 : if(skip != NULL && strcmp(skip,"1") == 0)
64 : : {
65 : 24 : provide(status);
66 : : }
67 : : }
68 : : #endif
69 : :
70 [ + + + + ]: 700 : if(config->progress == false && summary->stats_only_pass == true)
71 : : {
72 : : // Don't do anything
73 : 258 : provide(status);
74 : : }
75 : :
76 : : // Per-pass state used to control visible output and checksum-lock warnings
77 : :
78 : : // Print traversal/update banners only once
79 : 442 : bool first_iteration = true;
80 : :
81 : : // Signals integrity issues for locked files
82 : 442 : bool lock_checksum_violation_detected = false;
83 : :
84 : 442 : FTS *file_systems = NULL;
85 : 442 : FTSENT *p = NULL;
86 : :
87 : 442 : int fts_options = FTS_PHYSICAL;
88 : :
89 : : #ifdef FTS_NOCHDIR
90 : : /*
91 : : * Keep FTS from changing the process working directory during traversal.
92 : : * File operations use the opened root descriptor and root-relative paths,
93 : : * so traversal does not need process-wide working directory changes
94 : : */
95 : 442 : fts_options |= FTS_NOCHDIR;
96 : : #endif
97 : :
98 [ + + ]: 442 : if(config->start_device_only == true)
99 : : {
100 : 2 : fts_options |= FTS_XDEV;
101 : : }
102 : :
103 : : // Reset traversal counters and timing for this pass.
104 : 442 : summary->count_dirs = 0;
105 : :
106 : : // Number of regular files seen in this pass.
107 : 442 : summary->count_files = 0;
108 : :
109 : : // Number of symlinks seen in this pass.
110 : 442 : summary->count_symlnks = 0;
111 : :
112 : : // Sum of allocated bytes for encountered files.
113 : 442 : summary->total_allocated_bytes = 0;
114 : :
115 : : // Sum of bytes hashed by SHA512 during this pass.
116 : 442 : summary->total_hashed_bytes = 0;
117 : :
118 : : // Track whether any output was produced
119 : 442 : summary->at_least_one_file_was_shown = false;
120 : :
121 : : // Root path currently being traversed by this pass.
122 : 442 : summary->root = NULL;
123 : :
124 : : // Sum of per-file hashing elapsed time in nanoseconds.
125 : 442 : summary->total_hashing_elapsed_ns = 0LL;
126 : :
127 : : #if 0 // Disabled multi-root path index implementation
128 : : /**
129 : : * Path-prefix index reserved for the disabled per-file root mapping.
130 : : * Active traversal code currently builds each file path relative to the
131 : : * root being scanned and does not use this value
132 : : */
133 : : sqlite3_int64 runtime_root_path_index = -1;
134 : : #endif
135 : :
136 : : // Limit recursion to the depth determined in config->maxdepth
137 [ + + ]: 442 : if(config->maxdepth > -1)
138 : : {
139 : 4 : slog(EVERY,"Recursion depth limited to: %d\n",config->maxdepth);
140 : : }
141 : :
142 [ + + + - ]: 442 : if(summary->stats_only_pass == true && config->progress == true)
143 : : {
144 : 92 : slog(EVERY,"File system traversal initiated to calculate file count and storage usage\n");
145 : : }
146 : :
147 : 442 : bool continue_the_loop = true;
148 : :
149 : : // Allocate reusable buffers shared by entries in this traversal pass
150 : 442 : m_create(unsigned char,file_buffer);
151 : 442 : m_create(char,relative_path,MEMORY_STRING);
152 : :
153 [ + + ]: 442 : if(summary->stats_only_pass == false)
154 : : {
155 : 350 : status = m_resize(file_buffer,file_buffer_memory());
156 : :
157 [ - + ]: 350 : if(SUCCESS != status)
158 : : {
159 : 0 : provide(status);
160 : : }
161 : : #ifdef TESTITALL_TEST_HOOKS
162 : 350 : signal_wait_at_point(1U);
163 : : #endif
164 : : }
165 : :
166 : : // Open one FTS stream per traversal root
167 [ + + + + ]: 1318 : m_string_array_foreach(conf(roots),root)
168 : : {
169 : 444 : summary->root = root;
170 : :
171 : 444 : char *root_path_text = m_data(char,root);
172 : :
173 [ - + ]: 444 : if(root_path_text == NULL)
174 : : {
175 : 0 : slog(ERROR,"Unable to access root path descriptor\n");
176 : 0 : status = FAILURE;
177 : 4 : break;
178 : : }
179 : :
180 : : /*
181 : : * fts_open() expects a NULL-terminated array of root path strings, even
182 : : * when only one root is opened. file_list() processes configured roots
183 : : * one by one, so this short array keeps the current FTS stream scoped to
184 : : * the current root only
185 : : */
186 : 444 : char *root_argv[] = {
187 : : root_path_text,
188 : : NULL
189 : : };
190 : :
191 : : /*
192 : : * Descriptor for the current traversal root. Content-pass access checks
193 : : * use it as the base directory for relative paths
194 : : */
195 : 444 : int root_directory_fd = -1;
196 : :
197 : : /*
198 : : * The stats-only pass does not perform directory-relative access checks,
199 : : * so it can traverse the root path directly through FTS. The content pass
200 : : * needs a stable root descriptor because file and directory access checks
201 : : * are made with paths relative to this root. If the root cannot be
202 : : * opened, directory_open_root() reports and remembers the
203 : : * warning, and this loop skips only the unavailable root before
204 : : * continuing with the next configured traversal root
205 : : */
206 [ + + ]: 444 : if(summary->stats_only_pass == false)
207 : : {
208 : 352 : const FileAccessStatus root_access_status = directory_open_root(root,&root_directory_fd);
209 : :
210 [ + + ]: 352 : if(root_access_status != FILE_ACCESS_ALLOWED)
211 : : {
212 : 2 : continue;
213 : : }
214 : : }
215 : :
216 [ - + ]: 442 : if((file_systems = fts_open(root_argv,fts_options,compare_by_name)) == NULL)
217 : : {
218 : 0 : slog(ERROR,"fts_open() error\n");
219 : 0 : status = FAILURE;
220 : 0 : break;
221 : : }
222 : :
223 [ + + + - ]: 45028 : while((p = fts_read(file_systems)) != NULL && continue_the_loop == true)
224 : : {
225 : : /* Interrupt the loop smoothly */
226 : : /* Interrupt when Ctrl+C */
227 [ + + ]: 44593 : if(global_interrupt_flag == true)
228 : : {
229 : 4 : break;
230 : : }
231 : :
232 : : #if 0 // Disabled multi-root path index implementation
233 : : if(p->fts_level == FTS_ROOTLEVEL)
234 : : {
235 : : // If several paths were passed as arguments,
236 : : // then the counting of the path prefix index
237 : : // will start from zero
238 : : if(SUCCESS != (status = db_get_runtime_root_path_index(config,
239 : : root,
240 : : &runtime_root_path_index)))
241 : : {
242 : : continue_the_loop = false;
243 : : break;
244 : : }
245 : : }
246 : : #endif
247 : :
248 [ + + + + ]: 44589 : if(config->maxdepth > -1 && p->fts_level > config->maxdepth + 1)
249 : : {
250 [ + + ]: 42 : if(p->fts_info == FTS_D)
251 : : {
252 : 14 : (void)fts_set(file_systems,p,FTS_SKIP);
253 : : }
254 : :
255 : 42 : continue;
256 : : }
257 : :
258 [ + + + - : 44547 : switch(p->fts_info)
+ ]
259 : : {
260 : 19404 : case FTS_D:
261 : : {
262 : 19404 : run(path_build_relative(relative_path,p));
263 : :
264 [ - + ]: 19404 : if((TRIUMPH & status) == 0)
265 : : {
266 : 0 : continue_the_loop = false;
267 : 0 : break;
268 : : }
269 : :
270 : : // Captures files explicitly skipped or forced by regexp filters
271 : : // Ignored with --ignore= or admitted with --include=
272 : 19404 : bool ignore = false;
273 : :
274 : : // Included with --include=
275 : 19404 : bool include = false;
276 : :
277 : 19404 : status = match_include_ignore(relative_path,&include,&ignore);
278 : :
279 [ - + ]: 19404 : if((TRIUMPH & status) == 0)
280 : : {
281 : 0 : continue_the_loop = false;
282 : 0 : break;
283 : : }
284 : :
285 [ + + ]: 19404 : if(summary->stats_only_pass == false)
286 : : {
287 : 15030 : directory_show(relative_path,
288 : : &first_iteration,
289 : : summary,
290 : : ignore,
291 : : include);
292 : : }
293 : :
294 [ + + ]: 19404 : if(ignore == true)
295 : : {
296 : : /*
297 : : * Use FTS_SKIP for an ignored directory only when no --include
298 : : * or --lock-checksum patterns were provided
299 : : * When checksum locking is active, descendants of an ignored
300 : : * directory may still need integrity checks or rehashing
301 : : */
302 [ + + ]: 344 : if(config->include_specified == false
303 [ + + ]: 106 : && config->lock_checksum == NULL)
304 : : {
305 : 26 : (void)fts_set(file_systems,p,FTS_SKIP);
306 : : }
307 : 344 : break;
308 : : }
309 : :
310 [ + + ]: 19060 : if(summary->stats_only_pass == false)
311 : : {
312 : : // Check directory access and skip subtrees that cannot be entered
313 : 14778 : status = directory_access_verify(
314 : : file_systems,
315 : : p,
316 : : root_directory_fd,
317 : : relative_path,
318 : : &first_iteration,
319 : : summary);
320 : : }
321 : :
322 [ - + ]: 19060 : if((TRIUMPH & status) == 0)
323 : : {
324 : 0 : continue_the_loop = false;
325 : 0 : break;
326 : : }
327 : :
328 : 19060 : summary->count_dirs++;
329 : 19060 : break;
330 : : }
331 : 5366 : case FTS_F:
332 : : {
333 : 5366 : summary->total_allocated_bytes += blocks_to_bytes(p->fts_statp->st_blocks);
334 : 5366 : summary->count_files++;
335 : :
336 [ + + ]: 5366 : if(summary->stats_only_pass == true)
337 : : {
338 : 1168 : continue;
339 : : }
340 : :
341 : : // Keep per-file processing state on the stack and attach
342 : : // the DB row that will be filled for this path
343 : 4198 : File _file = {0};
344 : 4198 : File *file = &_file;
345 : 4198 : DBrow dbrow = {0};
346 : 4198 : file->db = &dbrow;
347 : :
348 : 4198 : run(path_build_relative(relative_path,p));
349 : :
350 [ - + ]: 4198 : if((TRIUMPH & status) == 0)
351 : : {
352 : 0 : continue_the_loop = false;
353 : 4195 : break;
354 : : }
355 : :
356 : : /* Load the database row, if any, for this root-relative file path */
357 : : #if 0 // Disabled multi-root path index implementation
358 : : run(db_read_file_data_from(file,&runtime_root_path_index,relative_path));
359 : : #else
360 : 4198 : run(db_read_file_data_from(file,relative_path));
361 : : #endif
362 : :
363 [ - + ]: 4198 : if(SUCCESS != status)
364 : : {
365 : 0 : continue_the_loop = false;
366 : 0 : break;
367 : : }
368 : :
369 : : /*
370 : : * Mutable row-existence flag for the current relative path.
371 : : * It starts with the state loaded before file processing and may become true after sha512sum() writes the first periodic checkpoint for a new file
372 : : */
373 : 4198 : bool path_known = file->db->relative_path_was_in_db_before_processing == true;
374 : :
375 : : /*
376 : : * YES means the current relative path is checksum-locked and the file must be marked as protected.
377 : : * NO means the path is not checksum-locked, so the file remains a regular tracked file
378 : : */
379 : 4198 : if(ask(path_check_locked_checksum(relative_path)))
380 : : {
381 : : // Mark this file as checksum-locked
382 : 286 : file->locked_checksum_file = true;
383 : : }
384 : :
385 [ - + ]: 4198 : if(SUCCESS != status)
386 : : {
387 : 0 : continue_the_loop = false;
388 : 0 : break;
389 : : }
390 : :
391 : : // Store the final --include/--ignore decision in the per-file state
392 : 4198 : status = match_include_ignore(relative_path,&file->include,&file->ignore);
393 : :
394 [ - + ]: 4198 : if(SUCCESS != status)
395 : : {
396 : 0 : continue_the_loop = false;
397 : 0 : break;
398 : : }
399 : :
400 : : // Ensure checksum-locked files stay visible even if matched by ignore
401 [ + + + + ]: 4198 : if(file->ignore == true && file->locked_checksum_file == true)
402 : : {
403 : 28 : file->ignore = false;
404 : : }
405 : :
406 : : // Determine read access for non-ignored paths
407 [ + + ]: 4198 : if(file->ignore == false)
408 : : {
409 : 4102 : FileAccessStatus access_status = file_check_access(root_directory_fd,relative_path,R_OK);
410 : 4102 : bool locked_unavailable_violation = false;
411 : :
412 [ + + ]: 4102 : if(path_known == true
413 [ + + ]: 1623 : && file->locked_checksum_file == true
414 [ + + ]: 145 : && access_status != FILE_ACCESS_ALLOWED)
415 : : {
416 : 8 : status = show_locked_checksum_unavailable_violation(relative_path,
417 : : access_status,
418 : : &first_iteration,
419 : : summary);
420 : :
421 [ - + ]: 8 : if(SUCCESS != status)
422 : : {
423 : 0 : continue_the_loop = false;
424 : 0 : break;
425 : : }
426 : :
427 : 8 : file->is_readable = false;
428 : 8 : locked_unavailable_violation = true;
429 : :
430 : : } else {
431 : 4094 : file->is_readable = (access_status == FILE_ACCESS_ALLOWED);
432 : : }
433 : :
434 [ + + ]: 4102 : if(locked_unavailable_violation == true)
435 : : {
436 : 8 : lock_checksum_violation_detected = true;
437 : 8 : break;
438 : : }
439 : : }
440 : :
441 : : // Copy the current metadata once for comparisons, hashing, logging,
442 : : // and database writes
443 : 4190 : run(stat_copy(p->fts_statp,&file->stat));
444 : :
445 : : // Validate whether logical size, allocated blocks, and ctime/mtime
446 : : // changed since the previous scan.
447 : : // Default value is:
448 : 4190 : file->db_record_vs_file_metadata_changes = NOT_EQUAL;
449 : :
450 [ + + ]: 4190 : if(path_known == true)
451 : : {
452 : : // Validate whether logical size, allocated blocks, and ctime/mtime
453 : : // changed since the previous scan.
454 : 1647 : file->db_record_vs_file_metadata_changes = file_compare_metadata_equivalence(&file->db->saved_stat,&file->stat);
455 : : }
456 : :
457 : 4190 : bool file_metadata_identical = file->db_record_vs_file_metadata_changes == IDENTICAL;
458 : 4190 : const bool has_saved_offset = file->db->saved_offset > 0;
459 : : // Indicates that the checksum-locked file has already been fully hashed and recorded
460 : 8380 : bool lock_checksum_ready = file->locked_checksum_file == true
461 [ + + ]: 278 : && path_known == true
462 [ + + + - ]: 4468 : && has_saved_offset == false;
463 : :
464 : : // Used to skip files whose metadata and checksum are already up to date
465 : 8380 : bool unchanged_and_complete = path_known == true
466 [ + + ]: 1647 : && file_metadata_identical == true
467 [ + + + + ]: 5837 : && has_saved_offset == false;
468 : :
469 [ + + + + : 4190 : if(unchanged_and_complete == true && !(config->rehash_locked == true && lock_checksum_ready == true))
+ + ]
470 : : {
471 : : // Relative path already in DB and doesn't require any change
472 : : break;
473 : : }
474 : :
475 : : // Derived flags to qualify the type of metadata change
476 : 2813 : bool size_changed = (file->db_record_vs_file_metadata_changes & SIZE_CHANGED) != 0;
477 : :
478 : 2813 : bool timestamps_changed = (file->db_record_vs_file_metadata_changes & (STATUS_CHANGED_TIME | MODIFICATION_TIME_CHANGED)) != 0;
479 : :
480 : 5626 : bool timestamps_only_changed = path_known == true
481 [ + + ]: 270 : && file_metadata_identical == false
482 [ + + ]: 232 : && config->watch_timestamps == false
483 [ + + ]: 178 : && size_changed == false
484 [ + + + - ]: 3083 : && has_saved_offset == false;
485 : :
486 : : // Decision whether to rehash the file contents using
487 : : // the SHA512 algorithm. Defaults to rehash.
488 : 2813 : file->rehash = true;
489 : :
490 [ + + ]: 2813 : if(timestamps_only_changed == true)
491 : : {
492 : : // ctime/mtime changed only: update DB without rehash
493 : 132 : file->rehash = false;
494 : : }
495 : :
496 [ + + + + ]: 2813 : if(lock_checksum_ready == true && config->rehash_locked == true)
497 : : {
498 : 49 : file->rehash = true;
499 : : }
500 : :
501 : : /* Handle files whose previous checksum pass stopped before completion */
502 : :
503 : : // Can we resume hashing from a previous partial state?
504 : 2813 : bool can_resume_partial_hash = has_saved_offset == true
505 [ + + + + ]: 2813 : && file_metadata_identical == true;
506 : :
507 : : // Indicates that a previous partial hash is now invalid and must restart
508 : 2813 : bool partial_hash_invalidated = has_saved_offset == true
509 [ + + + + ]: 2813 : && file_metadata_identical == false;
510 : :
511 [ + + ]: 2813 : if(can_resume_partial_hash == true)
512 : : {
513 : : // Continue hashing from the saved offset and SHA512 context
514 : : // Restore byte offset from where the previous pass stopped
515 : 5 : file->checksum_offset = file->db->saved_offset;
516 : : // Restore SHA512 state to continue from that offset
517 : 5 : memcpy(&file->mdContext,&file->db->saved_mdContext,sizeof(SHA512_Context));
518 : :
519 [ + + ]: 2808 : } else if(partial_hash_invalidated == true){
520 : : /* The previous partial hash is no longer valid because file metadata changed */
521 : : // Signal that hashing must restart from byte zero
522 : 2 : file->rehashing_from_the_beginning = true;
523 : : }
524 : :
525 : : // Marks zero-length files to avoid unnecessary hashing
526 [ + + ]: 2813 : if(file->stat.st_size == 0)
527 : : {
528 : 6 : file->zero_size_file = true;
529 : 6 : file->rehash = false;
530 : : }
531 : :
532 : : // Locked checksum files must not diverge once sealed
533 : 2813 : file->lock_checksum_violation = lock_checksum_ready == true
534 [ + + + + ]: 2864 : && (size_changed == true
535 [ + + ]: 51 : || (config->watch_timestamps == true
536 [ + + ]: 20 : && config->rehash_locked == false
537 [ + - ]: 2 : && timestamps_changed == true));
538 : :
539 : : // Timestamps drift on a locked file may be ignored depending on config
540 : 2813 : bool locked_timestamp_drift_only = lock_checksum_ready == true
541 [ + + ]: 61 : && config->watch_timestamps == false
542 [ + + ]: 41 : && config->rehash_locked == false
543 [ + - ]: 10 : && timestamps_changed == true
544 [ + + + + ]: 2874 : && size_changed == false;
545 : :
546 [ + + ]: 2813 : if(locked_timestamp_drift_only == true)
547 : : {
548 : 2 : break;
549 : : }
550 : :
551 : : // Hard hashing failure that should stop traversal for this pass
552 : 2811 : bool hash_failed = false;
553 : : // Controls whether the final outcome for this file should be shown
554 : 2811 : bool show_log = false;
555 : :
556 : 5622 : bool should_process = file->is_readable == true
557 [ + - ]: 2737 : && file->ignore == false
558 [ + + + + ]: 5548 : && file->lock_checksum_violation == false;
559 : :
560 [ + + ]: 2811 : if(should_process == true)
561 : : {
562 [ + + ]: 2725 : if(file->rehash == true)
563 : : {
564 [ + - ]: 2603 : if(SUCCESS == status)
565 : : {
566 : 2603 : status = sha512sum(root_directory_fd,
567 : : relative_path,
568 : : file_buffer,
569 : : summary,
570 : : file,
571 : : &path_known);
572 : : }
573 : :
574 [ + - ]: 2600 : if(TRIUMPH & status)
575 : : {
576 : : /* If the sha512sum has been interrupted smoothly when Ctrl+C */
577 [ + + + - ]: 2600 : if(file->checksum_offset > 0 && global_interrupt_flag == true)
578 : : {
579 : 2 : file->hash_interrupted = true;
580 : : }
581 : :
582 : : } else {
583 : 0 : continue_the_loop = false;
584 : 0 : hash_failed = true;
585 : : }
586 : :
587 : : } else {
588 : : // No rehash: reuse the digest stored in the DB
589 : 122 : memcpy(file->sha512,file->db->sha512,sizeof(file->sha512));
590 : : }
591 : :
592 [ + - ]: 2722 : if(hash_failed == false
593 [ + + ]: 2722 : && file->read_error == false
594 [ + + ]: 2719 : && config->rehash_locked == true
595 [ + - ]: 47 : && lock_checksum_ready == true
596 [ + - ]: 47 : && file->rehash == true
597 [ + - ]: 47 : && (TRIUMPH & status)
598 [ + - ]: 47 : && file->wrong_file_type == false
599 [ + - ]: 47 : && file->zero_size_file == false
600 [ + - ]: 47 : && file->checksum_offset == 0)
601 : : {
602 [ + + ]: 47 : if(memcmp(file->sha512,file->db->sha512,SHA512_DIGEST_LENGTH) != 0)
603 : : {
604 : : // Detects corruption when rehashing locked files
605 : 10 : file->locked_checksum_mismatch = true;
606 : : }
607 : : }
608 : : }
609 : :
610 [ + + ]: 2808 : if(file->is_readable != true
611 [ + - ]: 2734 : || file->ignore == true
612 [ + + ]: 2734 : || file->lock_checksum_violation == true
613 [ + - ]: 2722 : || hash_failed == true
614 [ + + ]: 2722 : || file->read_error == true
615 [ + + ]: 2719 : || file->locked_checksum_mismatch == true)
616 : : {
617 : 99 : show_log = true;
618 : :
619 : : /* When a checksum-locked file changed;
620 : : blocks rehash/DB update and flags corruption */
621 [ + + + + : 99 : if((file->lock_checksum_violation == true || file->locked_checksum_mismatch == true) && file->read_error == false)
+ - ]
622 : : {
623 : 22 : lock_checksum_violation_detected = true;
624 : : /*
625 : : * A checksum-locked content or metadata mismatch means protected data may be corrupted.
626 : : * Replay remembered warnings at exit even without --progress so this critical result remains visible after normal traversal output
627 : : */
628 : 22 : config->show_remembered_messages_at_exit = true;
629 : : }
630 : :
631 [ + + ]: 2709 : } else if(path_known == true){
632 : : /* Update in DB */
633 : 236 : show_log = true;
634 : :
635 : : /*
636 : : * True when this file was absent at the start of processing.
637 : : * A periodic checkpoint may already have inserted its row, so the final save must still be allowed and reported as a new file
638 : : */
639 : 236 : bool path_inserted_during_processing = file->db->relative_path_was_in_db_before_processing == false;
640 : :
641 : : /*
642 : : * Controls whether the final save may update an existing row.
643 : : * Regular files, forced locked-file rehashes, resumed partial hashes, and rows inserted during this pass are allowed to be saved
644 : : */
645 : 236 : bool allow_locked_update = path_inserted_during_processing == true
646 [ + + ]: 235 : || file->locked_checksum_file == false
647 [ - + ]: 37 : || config->rehash_locked == true
648 [ + + - - ]: 471 : || has_saved_offset == true;
649 : :
650 : : /*
651 : : * Final database-save decision for a known row.
652 : : * It is true when the row was created by a checkpoint, the hash advanced, a resumed hash completed, or the file metadata changed
653 : : */
654 : 236 : bool should_update_db = allow_locked_update == true
655 [ + - + + ]: 471 : && (path_inserted_during_processing == true
656 [ + - ]: 235 : || file->checksum_offset > file->db->saved_offset
657 [ + + - + ]: 235 : || (has_saved_offset == true && file->checksum_offset == 0)
658 [ + + ]: 229 : || file_metadata_identical == false);
659 : :
660 [ + + ]: 236 : if(should_update_db == true)
661 : : {
662 : : /* Save record in DB */
663 [ + - ]: 205 : if(TRIUMPH & status)
664 : : {
665 : 205 : status = db_save_file_record(relative_path,file,&path_known,true);
666 : :
667 [ - + ]: 205 : if((TRIUMPH & status) == 0)
668 : : {
669 : 0 : continue_the_loop = false;
670 : 0 : break;
671 : : }
672 : : }
673 : : }
674 : : } else {
675 : 2473 : show_log = true;
676 : :
677 : : /* Save record in DB */
678 [ + - ]: 2473 : if(TRIUMPH & status)
679 : : {
680 : 2473 : status = db_save_file_record(relative_path,file,&path_known,true);
681 : :
682 [ - + ]: 2473 : if((TRIUMPH & status) == 0)
683 : : {
684 : 0 : continue_the_loop = false;
685 : 0 : break;
686 : : }
687 : : }
688 : : }
689 : :
690 [ + - ]: 2808 : if(show_log == true)
691 : : {
692 : : // Print out of a file name and its changes
693 : 2808 : show_file(relative_path,
694 : : &first_iteration,
695 : : summary,
696 : : file);
697 : : }
698 : :
699 : 2808 : break;
700 : : }
701 : 378 : case FTS_SL:
702 : 378 : summary->count_symlnks++;
703 : 378 : break;
704 : 0 : case FTS_DNR:
705 : : case FTS_ERR:
706 : : case FTS_NS:
707 : : {
708 [ # # ]: 0 : if(summary->stats_only_pass == true)
709 : : {
710 : 0 : break;
711 : : }
712 : :
713 : 0 : run(path_build_relative(relative_path,p));
714 : :
715 [ # # ]: 0 : if((TRIUMPH & status) == 0)
716 : : {
717 : 0 : continue_the_loop = false;
718 : 0 : break;
719 : : }
720 : :
721 : 0 : const char *runtime_relative_path = m_text(relative_path);
722 : :
723 [ # # ]: 0 : if(p->fts_info == FTS_DNR)
724 : : {
725 : 0 : slog_show(EVERY|UNDECOR|REMEMBER,false,&first_iteration,summary,"inaccessible directory %s\n",runtime_relative_path);
726 : :
727 [ # # ]: 0 : } else if(p->fts_info == FTS_NS){
728 : :
729 : 0 : slog_show(EVERY|UNDECOR|REMEMBER,false,&first_iteration,summary,"cannot stat \"%s\" when reading %s\n",strerror(p->fts_errno),runtime_relative_path);
730 : :
731 : : } else {
732 : :
733 : 0 : slog_show(EVERY|UNDECOR|REMEMBER,false,&first_iteration,summary,"fts error \"%s\" when reading %s\n",strerror(p->fts_errno),runtime_relative_path);
734 : : }
735 : :
736 : 0 : break;
737 : : }
738 : 19399 : default:
739 : 19399 : break;
740 : : }
741 : : }
742 : :
743 [ + - ]: 439 : if(file_systems != NULL)
744 : : {
745 : 439 : fts_close(file_systems);
746 : 439 : file_systems = NULL;
747 : : }
748 : :
749 [ + + ]: 439 : if(root_directory_fd >= 0)
750 : : {
751 [ - + ]: 347 : if(close(root_directory_fd) != 0)
752 : : {
753 : 0 : slog(ERROR,"Failed to close traversal root directory descriptor: %s\n",strerror(errno));
754 : 0 : status = FAILURE;
755 : : }
756 : : }
757 : :
758 [ + + + - ]: 439 : if(global_interrupt_flag == true || (TRIUMPH & status) == 0)
759 : : {
760 : : break;
761 : : }
762 : : }
763 : :
764 : 439 : m_del(relative_path);
765 : 439 : m_del(file_buffer);
766 : :
767 : : // Clear the traversal root pointer before returning from this pass.
768 : : // The root descriptors belong to config->roots and must not be treated as active after the loop finishes
769 : 439 : summary->root = NULL;
770 : :
771 : : // Print completion banner only when traversal emitted visible path-level lines.
772 : : // Print preflight totals only for the stats-only pass from main().
773 [ + + ]: 439 : if(SUCCESS == status)
774 : : {
775 [ + + ]: 437 : if(summary->at_least_one_file_was_shown == true)
776 : : {
777 : :
778 : 250 : slog(EVERY,"File traversal complete\n");
779 : : }
780 : :
781 [ + + ]: 437 : if(summary->stats_only_pass == true)
782 : : {
783 : 92 : show_statistics(summary);
784 : : }
785 : : }
786 : :
787 [ + + ]: 439 : if(lock_checksum_violation_detected == true)
788 : : {
789 : 26 : slog(EVERY,BOLD "Warning! Data corruption detected for checksum-locked file!" RESET "\n");
790 : :
791 [ + - ]: 26 : if(SUCCESS == status)
792 : : {
793 : 26 : status = WARNING;
794 : : }
795 : : }
796 : :
797 : 439 : provide(status);
798 : : }
|