LCOV - code coverage report
Current view: top level - src - db_validate_paths.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 76.6 % 111 85
Test Date: 2026-07-12 01:01:34 Functions: 100.0 % 1 1
Branches: 65.9 % 82 54

             Branch data     Line data    Source code
       1                 :             : #include "precizer.h"
       2                 :             : 
       3                 :             : /**
       4                 :             :  * @brief Validate stored DB path prefixes against the current traversal roots
       5                 :             :  *
       6                 :             :  * @details Skips validation in compare mode. When the primary database has just
       7                 :             :  * been created, no stored-prefix comparison is needed. Otherwise the function
       8                 :             :  * compares the root strings in `config->roots` with prefixes stored in the
       9                 :             :  * database. The comparison is textual on purpose: `another_path`,
      10                 :             :  * `./another_path`, and `/absolute/path/another_path` are different database
      11                 :             :  * roots because the user chose different root spellings. Prefixes missing from
      12                 :             :  * the current root set are recorded for later cleanup when the user explicitly
      13                 :             :  * allows it
      14                 :             :  *
      15                 :             :  * If mismatches are found and `--force` is not enabled, the function returns
      16                 :             :  * `WARNING` so the caller can stop before replacing stored path metadata. When
      17                 :             :  * `--force` is enabled, execution continues and the current roots may be
      18                 :             :  * written to the database. For example, opening an existing database with a
      19                 :             :  * different traversal root warns before any path metadata is replaced
      20                 :             :  *
      21                 :             :  * @warning Using `--force` incorrectly can replace path, file, and checksum
      22                 :             :  * metadata in the database
      23                 :             :  *
      24                 :             :  * @return `SUCCESS` when prefixes match or forced path replacement is allowed,
      25                 :             :  *         `WARNING` when mismatches are found without `--force`, or `FAILURE`
      26                 :             :  *         on SQLite or memory errors
      27                 :             :  */
      28                 :         476 : Return db_validate_paths(void)
      29                 :             : {
      30                 :             :         /* Status returned by this function through provide()
      31                 :             :            Default value assumes successful completion */
      32                 :         476 :         Return status = SUCCESS;
      33                 :             : 
      34                 :             :         /* In compare mode this function has nothing useful to validate.
      35                 :             :            Compare mode works with two already opened databases, so checking whether
      36                 :             :            the primary database roots should be replaced would be unrelated here */
      37         [ +  + ]:         476 :         if(config->compare == true)
      38                 :             :         {
      39                 :         112 :                 provide(status);
      40                 :             :         }
      41                 :             : 
      42                 :             :         /* A user interrupt should stop this validation before any extra database work
      43                 :             :            starts. Returning the current status lets the regular shutdown path finish
      44                 :             :            cleanly after Ctrl+C */
      45         [ -  + ]:         364 :         if(global_interrupt_flag == true)
      46                 :             :         {
      47                 :           0 :                 provide(status);
      48                 :             :         }
      49                 :             : 
      50                 :             :         /* The main select statement finds stored path prefixes that are not part of
      51                 :             :            the current command-line roots. The insert statement records each mismatch
      52                 :             :            in a temporary helper table for later cleanup decisions */
      53                 :         364 :         sqlite3_stmt *select_stmt = NULL;
      54                 :         364 :         sqlite3_stmt *insert_stmt = NULL;
      55                 :         364 :         int rc = 0;
      56                 :             : 
      57                 :             :         /* The paths are assumed to match until SQLite returns at least one stored
      58                 :             :            prefix that is missing from the current command-line roots */
      59                 :         364 :         bool paths_are_equal = true;
      60                 :             : 
      61                 :             :         /* The SELECT text is assembled dynamically because the number of roots is not
      62                 :             :            fixed. Values are still bound as SQL parameters, so path text is not pasted
      63                 :             :            directly into the SQL request */
      64                 :         364 :         m_create(char,select_sql,MEMORY_STRING);
      65                 :             : 
      66                 :             :         /* Build the query that finds stored prefixes outside the current root set.
      67                 :             :            Each root becomes one placeholder in the NOT IN list */
      68         [ +  - ]:         364 :         if(config->roots.length != 0)
      69                 :             :         {
      70                 :         364 :                 status = m_concat_literal(select_sql,"SELECT ID FROM paths WHERE prefix NOT IN (");
      71                 :             : 
      72         [ -  + ]:         364 :                 if(SUCCESS != status)
      73                 :             :                 {
      74                 :           0 :                         m_del(select_sql);
      75                 :           0 :                         provide(status);
      76                 :             :                 }
      77                 :             : 
      78                 :         364 :                 bool has_previous_root = false;
      79                 :             : 
      80                 :             :                 /* Add one SQL placeholder for each configured traversal root. Commas are
      81                 :             :                    inserted only between placeholders, which keeps the generated SQL valid
      82                 :             :                    for any number of roots */
      83   [ +  +  +  + ]:        1094 :                 m_string_array_foreach(conf(roots),root)
      84                 :             :                 {
      85         [ +  + ]:         366 :                         if(has_previous_root == true)
      86                 :             :                         {
      87                 :             :                                 /* Add a separator between root parameters. This is only needed
      88                 :             :                                    after the first placeholder has already been written */
      89                 :           2 :                                 status = m_concat_literal(select_sql,",");
      90                 :             : 
      91         [ -  + ]:           2 :                                 if(SUCCESS != status)
      92                 :             :                                 {
      93                 :           0 :                                         m_del(select_sql);
      94                 :           0 :                                         provide(status);
      95                 :             :                                 }
      96                 :             :                         }
      97                 :             : 
      98                 :         366 :                         status = m_concat_literal(select_sql,"?");
      99                 :             : 
     100         [ -  + ]:         366 :                         if(SUCCESS != status)
     101                 :             :                         {
     102                 :           0 :                                 m_del(select_sql);
     103                 :           0 :                                 provide(status);
     104                 :             :                         }
     105                 :             : 
     106                 :         366 :                         has_previous_root = true;
     107                 :             :                 }
     108                 :             : 
     109                 :             :                 /* Close the SQL request after all placeholders have been added. From this
     110                 :             :                    point the statement text is ready for sqlite3_prepare_v2() */
     111                 :         364 :                 status = m_concat_literal(select_sql,");");
     112                 :             : 
     113         [ -  + ]:         364 :                 if(SUCCESS != status)
     114                 :             :                 {
     115                 :           0 :                         m_del(select_sql);
     116                 :           0 :                         provide(status);
     117                 :             :                 }
     118                 :             :         }
     119                 :             : 
     120                 :             :         /* Prepare the SELECT statement that will ask SQLite for saved prefixes which
     121                 :             :            are no longer present in the current root list */
     122                 :         364 :         rc = sqlite3_prepare_v2(config->db,m_text(select_sql),-1,&select_stmt,NULL);
     123                 :             : 
     124         [ -  + ]:         364 :         if(SQLITE_OK != rc)
     125                 :             :         {
     126                 :           0 :                 log_sqlite_error(config->db,rc,NULL,"Can't prepare select statement %s",m_text(select_sql));
     127                 :           0 :                 status = FAILURE;
     128                 :             :         }
     129                 :             : 
     130         [ +  - ]:         364 :         if(SUCCESS == status)
     131                 :             :         {
     132                 :             :                 /* Bind every current root to its matching placeholder. Binding keeps paths
     133                 :             :                    with quotes, spaces, or other special characters safe and unambiguous */
     134                 :         364 :                 int bind_index = 1;
     135                 :             : 
     136   [ +  +  +  + ]:        1094 :                 m_string_array_foreach(conf(roots),root)
     137                 :             :                 {
     138                 :         366 :                         const char *root_path = m_text(root);
     139                 :         366 :                         size_t root_path_length = 0;
     140                 :             : 
     141                 :             :                         /* Ask libmem for the exact string length before binding. SQLite then
     142                 :             :                            receives the path and its byte count explicitly */
     143         [ +  - ]:         366 :                         if(SUCCESS == status)
     144                 :             :                         {
     145                 :         366 :                                 status = m_string_length(root,&root_path_length);
     146                 :             :                         }
     147                 :             : 
     148                 :             :                         /* Attach the root value to the prepared SELECT statement. A binding
     149                 :             :                            failure is a technical error because validation cannot continue with
     150                 :             :                            an incomplete root list */
     151         [ +  - ]:         366 :                         if(SUCCESS == status)
     152                 :             :                         {
     153                 :         366 :                                 rc = sqlite3_bind_text(select_stmt,bind_index,root_path,(int)root_path_length,NULL);
     154                 :             : 
     155         [ -  + ]:         366 :                                 if(SQLITE_OK != rc)
     156                 :             :                                 {
     157                 :           0 :                                         log_sqlite_error(config->db,rc,NULL,"Error binding root path in select");
     158                 :           0 :                                         status = FAILURE;
     159                 :             :                                 }
     160                 :             :                         }
     161                 :             : 
     162                 :             :                         /* Move to the next SQL placeholder only after the current root was
     163                 :             :                            bound successfully */
     164         [ +  - ]:         366 :                         if(SUCCESS == status)
     165                 :             :                         {
     166                 :         366 :                                 bind_index++;
     167                 :             :                         }
     168                 :             : 
     169                 :             :                         /* Once a binding-related error happens, the remaining roots are no
     170                 :             :                            longer useful for this statement. Stop the loop and let cleanup run */
     171         [ -  + ]:         366 :                         if(SUCCESS != status)
     172                 :             :                         {
     173                 :           0 :                                 break;
     174                 :             :                         }
     175                 :             :                 }
     176                 :             :         }
     177                 :             : 
     178         [ +  - ]:         364 :         if(SUCCESS == status)
     179                 :             :         {
     180                 :             :                 /* Execute the SELECT and handle every stored prefix that is missing from
     181                 :             :                    the current root set. Any returned row means the database and command
     182                 :             :                    line describe different traversal roots */
     183         [ +  + ]:         732 :                 while(SQLITE_ROW == (rc = sqlite3_step(select_stmt)))
     184                 :             :                 {
     185                 :           4 :                         sqlite3_int64 path_ID = -1;
     186                 :             : 
     187                 :           4 :                         path_ID = sqlite3_column_int64(select_stmt,0);
     188                 :             : 
     189                 :             :                         /* A valid path ID marks a real mismatch. Store it in the helper table
     190                 :             :                            so later code can decide which database records are outside the new
     191                 :             :                            root scope */
     192         [ -  + ]:           4 :                         if(path_ID != -1)
     193                 :             :                         {
     194                 :           4 :                                 paths_are_equal = false;
     195                 :             : 
     196                 :           4 :                                 const char *insert_sql = "INSERT INTO the_path_id_does_not_exists (path_id) VALUES (?1);";
     197                 :             : 
     198                 :             :                                 /* Prepare a small insert statement for the mismatched path ID.
     199                 :             :                                    Keeping this step separate makes SQLite errors visible with the
     200                 :             :                                    exact operation that failed */
     201                 :           4 :                                 rc = sqlite3_prepare_v2(config->db,insert_sql,-1,&insert_stmt,NULL);
     202                 :             : 
     203         [ -  + ]:           4 :                                 if(SQLITE_OK != rc)
     204                 :             :                                 {
     205                 :           0 :                                         log_sqlite_error(config->db,rc,NULL,"Can't prepare insert statement");
     206                 :           0 :                                         status = FAILURE;
     207                 :             :                                 }
     208                 :             : 
     209                 :             :                                 /* Bind the path ID returned by the SELECT. The helper table stores
     210                 :             :                                    identifiers, not path strings, so later cleanup can work by DB
     211                 :             :                                    row identity */
     212         [ +  - ]:           4 :                                 if(SUCCESS == status)
     213                 :             :                                 {
     214                 :           4 :                                         rc = sqlite3_bind_int64(insert_stmt,1,path_ID);
     215                 :             : 
     216         [ -  + ]:           4 :                                         if(SQLITE_OK != rc)
     217                 :             :                                         {
     218                 :           0 :                                                 log_sqlite_error(config->db,rc,NULL,"Error binding value in insert");
     219                 :           0 :                                                 status = FAILURE;
     220                 :             :                                         }
     221                 :             :                                 }
     222                 :             : 
     223                 :             :                                 /* Write the mismatched path ID into the helper table. If this does
     224                 :             :                                    not finish with SQLITE_DONE, the database state is not reliable
     225                 :             :                                    enough to continue normal validation */
     226         [ +  - ]:           4 :                                 if(SUCCESS == status)
     227                 :             :                                 {
     228                 :           4 :                                         rc = sqlite3_step(insert_stmt);
     229                 :             : 
     230         [ -  + ]:           4 :                                         if(rc != SQLITE_DONE)
     231                 :             :                                         {
     232                 :           0 :                                                 log_sqlite_error(config->db,rc,NULL,"Insert statement didn't return DONE");
     233                 :           0 :                                                 status = FAILURE;
     234                 :             :                                         }
     235                 :             :                                 }
     236                 :             : 
     237                 :             :                                 /* Finalize the insert statement for this row before continuing.
     238                 :             :                                    This releases SQLite resources even when the row caused an
     239                 :             :                                    error */
     240                 :           4 :                                 sqlite3_finalize(insert_stmt);
     241                 :             :                         }
     242                 :             :                 }
     243                 :             : 
     244                 :             :                 /* A SELECT loop must end with SQLITE_DONE. Any other result means SQLite
     245                 :             :                    stopped for an error, so the validation result cannot be trusted */
     246         [ -  + ]:         364 :                 if(SQLITE_DONE != rc)
     247                 :             :                 {
     248                 :           0 :                         log_sqlite_error(config->db,rc,NULL,"Select statement didn't finish with DONE");
     249                 :           0 :                         status = FAILURE;
     250                 :             :                 }
     251                 :             :         }
     252                 :             : 
     253                 :             :         /* Release the dynamically assembled SQL text before leaving the database
     254                 :             :            scan section. The prepared statement owns its compiled copy already */
     255                 :         364 :         m_del(select_sql);
     256                 :             : 
     257                 :             :         /* Finalize the SELECT statement after all rows were processed. This is safe
     258                 :             :            even when preparation failed because the statement pointer starts as NULL */
     259                 :         364 :         sqlite3_finalize(select_stmt);
     260                 :             : 
     261         [ +  - ]:         364 :         if(SUCCESS == status)
     262                 :             :         {
     263                 :             :                 /* A brand new primary database has no old path prefixes to protect.
     264                 :             :                    There is no meaningful mismatch to report because the current roots are
     265                 :             :                    the first roots this database has ever seen */
     266   [ +  +  +  - ]:         364 :                 if(config->db_primary_file_exists == false && config->compare == false)
     267                 :             :                 {
     268                 :         207 :                         slog(TRACE,"The brand new database has just been created. No need to verify the paths stored in the database against those passed as command-line arguments\n");
     269                 :         207 :                         provide(status);
     270                 :             : 
     271                 :             :                 } else {
     272                 :             : 
     273         [ +  + ]:         157 :                         if(paths_are_equal == true)
     274                 :             :                         {
     275                 :             :                                 /* No stored prefix falls outside the current root list. The user
     276                 :             :                                    can continue because the command-line paths match the database
     277                 :             :                                    scope */
     278                 :         153 :                                 slog(TRACE,"The paths written against the database and the paths passed as arguments are completely identical\n");
     279                 :             :                         } else {
     280                 :             :                                 /* At least one saved prefix is outside the current roots. Warn the
     281                 :             :                                    user because continuing may replace path metadata and make old
     282                 :             :                                    file records unreachable */
     283                 :           4 :                                 slog(EVERY,"The paths passed as arguments differ from those saved in the database. File paths and checksum information may be lost!\n");
     284                 :             : 
     285         [ +  - ]:           4 :                                 if(!(rational_logger_mode & SILENT))
     286                 :             :                                 {
     287                 :             :                                         /* In normal output modes, show the user exactly which prefixes
     288                 :             :                                            are already stored in the database. Silent mode skips this
     289                 :             :                                            explanatory list */
     290                 :           4 :                                         slog(EVERY,"Paths saved in the database:\n");
     291                 :             : 
     292                 :           4 :                                         sqlite3_stmt *stmt = NULL;
     293                 :           4 :                                         int rc_stmt = 0;
     294                 :           4 :                                         char const *sql = "SELECT prefix FROM paths;";
     295                 :             : 
     296                 :             :                                         /* Prepare a simple display query for the stored prefixes. This
     297                 :             :                                            query is only for the warning message shown to the user */
     298                 :           4 :                                         rc_stmt = sqlite3_prepare_v2(config->db,sql,-1,&stmt,NULL);
     299                 :             : 
     300         [ -  + ]:           4 :                                         if(SQLITE_OK != rc_stmt)
     301                 :             :                                         {
     302                 :           0 :                                                 log_sqlite_error(config->db,rc_stmt,NULL,"Can't prepare select statement %s",sql);
     303                 :           0 :                                                 status = FAILURE;
     304                 :             :                                         }
     305                 :             : 
     306         [ +  - ]:           4 :                                         if(SUCCESS == status)
     307                 :             :                                         {
     308                 :             :                                                 /* Print each stored prefix without logger decorations so
     309                 :             :                                                    the list is easy to read or copy */
     310         [ +  + ]:           8 :                                                 while(SQLITE_ROW == (rc_stmt = sqlite3_step(stmt)))
     311                 :             :                                                 {
     312                 :           4 :                                                         const char *prefix = (const char *)sqlite3_column_text(stmt,0);
     313                 :             : 
     314                 :           4 :                                                         slog(EVERY|UNDECOR,"%s\n",prefix);
     315                 :             :                                                 }
     316                 :             : 
     317                 :             :                                                 /* The display query must also end cleanly. If it does not,
     318                 :             :                                                    report the SQLite error instead of presenting an
     319                 :             :                                                    incomplete list as trustworthy */
     320         [ -  + ]:           4 :                                                 if(SQLITE_DONE != rc_stmt)
     321                 :             :                                                 {
     322                 :           0 :                                                         log_sqlite_error(config->db,rc_stmt,NULL,"Select statement didn't finish with DONE");
     323                 :           0 :                                                         status = FAILURE;
     324                 :             :                                                 }
     325                 :             :                                         }
     326                 :             : 
     327                 :             :                                         /* Release the display query statement after the saved-prefix
     328                 :             :                                            list has been printed or skipped because of an error */
     329                 :           4 :                                         sqlite3_finalize(stmt);
     330                 :             :                                 }
     331                 :             : 
     332         [ +  + ]:           4 :                                 if(config->force == true)
     333                 :             :                                 {
     334                 :             :                                         /* With --force the user explicitly accepts replacing the stored
     335                 :             :                                            path scope. Show the new roots that will be written unless
     336                 :             :                                            output is intentionally silent */
     337         [ +  - ]:           2 :                                         if(!(rational_logger_mode & SILENT))
     338                 :             :                                         {
     339                 :           2 :                                                 slog(EVERY,"The " BOLD "--force" RESET " option has been used, so the following paths will be written to the %s:\n",confstr(db_file_name));
     340                 :             : 
     341                 :             :                                                 /* Show the configured traversal roots that will replace
     342                 :             :                                                    stored prefixes. These are the root strings the program
     343                 :             :                                                    will treat as the database scope from now on */
     344   [ +  +  +  + ]:           6 :                                                 m_string_array_foreach(conf(roots),root)
     345                 :             :                                                 {
     346                 :           2 :                                                         slog(EVERY|UNDECOR,"%s\n",m_text(root));
     347                 :             :                                                 }
     348                 :             :                                         }
     349                 :             :                                 } else {
     350                 :             :                                         /* Without --force the safe choice is to stop with a warning.
     351                 :             :                                            This gives the user a chance to confirm the path replacement
     352                 :             :                                            instead of losing metadata by accident */
     353                 :           2 :                                         slog(EVERY,"Use the " BOLD "--force" RESET " option only when the PATHS stored in the database need"
     354                 :             :                                                 " to be updated. Warning: If this option is used incorrectly, file and checksum information"
     355                 :             :                                                 " in the database may be lost or completely replaced with different values.\n");
     356                 :           2 :                                         status = WARNING;
     357                 :             :                                 }
     358                 :             :                         }
     359                 :             :                 }
     360                 :             :         }
     361                 :             : 
     362                 :         157 :         provide(status);
     363                 :             : }
        

Generated by: LCOV version 2.0-1