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 : : }
|