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