Branch data Line data Source code
1 : : #include "sute.h"
2 : :
3 : : static const char fixture_root[] = "tests/fixtures/diffs/diff1";
4 : :
5 : : /**
6 : : * @brief Verify that a checksum-locked file size change is reported but not saved
7 : : *
8 : : * Covers README Example 10, case 1
9 : : *
10 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
11 : : * After one locked file grows on disk, the update run with `--rehash-locked`
12 : : * must warn about possible data corruption and keep the original database row
13 : : */
14 : 2 : static Return test0030_1(void)
15 : : {
16 : 2 : INITTEST;
17 : :
18 : : /*
19 : : * Allocate managed buffers for the captured application output and
20 : : * the expected regular-expression template used by this test
21 : : */
22 : 2 : m_create(char,result,MEMORY_STRING);
23 : 2 : m_create(char,pattern,MEMORY_STRING);
24 : :
25 : : /*
26 : : * Name the database, the protected database key, and the fixture file
27 : : * that will be changed on disk during the second half of the test
28 : : */
29 : 2 : const char *db_filename = "lock_s1.db";
30 : 2 : const char *locked_relative_path = "path1/AAA/BCB/CCC/a.txt";
31 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/BCB/CCC/a.txt";
32 : :
33 : : /*
34 : : * Keep before-and-after snapshots of the protected database row.
35 : : * They prove that a warning result did not silently update locked data
36 : : */
37 : 2 : CmpctStat db_stat_before = {0};
38 : 2 : CmpctStat db_stat_after = {0};
39 : 2 : sqlite3_int64 offset_before = 0;
40 : 2 : sqlite3_int64 offset_after = 0;
41 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
42 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
43 : :
44 : : /*
45 : : * Enable deterministic TESTING output so the captured application log
46 : : * can be compared against a template
47 : : */
48 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
49 : :
50 : : /*
51 : : * Work on a mutable copy of the fixture tree.
52 : : * The original fixture is restored during cleanup
53 : : */
54 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
55 : :
56 : : /*
57 : : * Create the baseline database.
58 : : * The lock pattern protects every relative path under the path1/ subtree
59 : : */
60 : 2 : const char *arguments = "--database=lock_s1.db --progress "
61 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
62 : :
63 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
64 : :
65 : : /*
66 : : * Verify the baseline run output.
67 : : * It must show a new database with path1/ files recorded as checksum-locked
68 : : */
69 : 2 : const char *filename = "templates/0030_001_1.txt";
70 : :
71 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
72 : :
73 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
74 : :
75 : : /*
76 : : * Read the locked row before corrupting the on-disk file.
77 : : * These values are expected to remain unchanged after the warning run
78 : : */
79 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
80 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
81 : :
82 : : /*
83 : : * Grow one checksum-locked file on disk.
84 : : * This creates a size mismatch against the protected database row
85 : : */
86 : 2 : ASSERT(SUCCESS == write_string_to_file("pad",locked_file_path,FILE_WRITE_APPEND));
87 : :
88 : : /*
89 : : * Run an update with deep locked-file verification enabled.
90 : : * The size mismatch must be reported as a warning, not saved to the DB
91 : : */
92 : 2 : arguments = "--update --check-level=QUICK --rehash-locked --lock-checksum=\"^path1/.*\" "
93 : : "--database=lock_s1.db tests/fixtures/diffs/diff1";
94 : :
95 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
96 : :
97 : : /*
98 : : * Verify the warning output.
99 : : * The changed locked file must be reported as possible data corruption
100 : : */
101 : 2 : filename = "templates/0030_001_2.txt";
102 : :
103 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
104 : :
105 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
106 : :
107 : : /*
108 : : * Re-read the same protected row after the warning run.
109 : : * The row must keep its original metadata, offset, and SHA512 checksum
110 : : */
111 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
112 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
113 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
114 : 2 : ASSERT(offset_before == offset_after);
115 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
116 : :
117 : : /*
118 : : * Remove the test database and restore the mutable fixture.
119 : : * This returns the workspace to the state expected by the next test
120 : : */
121 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
122 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
123 : :
124 : : /*
125 : : * Release managed buffers allocated by this test before returning
126 : : */
127 : 2 : m_del(pattern);
128 : 2 : m_del(result);
129 : :
130 : 2 : RETURN_STATUS;
131 : : }
132 : :
133 : : /**
134 : : * @brief Verify that watched timestamp drift on a locked file is reported but not saved
135 : : *
136 : : * Covers README Example 10, case 4
137 : : *
138 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
139 : : * After one locked file gets a metadata-only timestamp change, the update run
140 : : * with `--watch-timestamps` must warn and keep the original database row
141 : : */
142 : 2 : static Return test0030_2(void)
143 : : {
144 : 2 : INITTEST;
145 : :
146 : : /*
147 : : * Allocate managed buffers for the captured application output and
148 : : * the expected regular-expression template used by this test
149 : : */
150 : 2 : m_create(char,result,MEMORY_STRING);
151 : 2 : m_create(char,pattern,MEMORY_STRING);
152 : :
153 : : /*
154 : : * Name the database, the protected database key, and the fixture file
155 : : * whose timestamps will be changed without touching file contents
156 : : */
157 : 2 : const char *db_filename = "lock_s2.db";
158 : 2 : const char *locked_relative_path = "path1/AAA/BCB/CCC/a.txt";
159 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/BCB/CCC/a.txt";
160 : :
161 : : /*
162 : : * Keep before-and-after snapshots of the protected database row.
163 : : * They prove that a warning result did not silently update locked data
164 : : */
165 : 2 : CmpctStat db_stat_before = {0};
166 : 2 : CmpctStat db_stat_after = {0};
167 : 2 : sqlite3_int64 offset_before = 0;
168 : 2 : sqlite3_int64 offset_after = 0;
169 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
170 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
171 : :
172 : : /*
173 : : * Enable deterministic TESTING output so the captured application log
174 : : * can be compared against a template
175 : : */
176 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
177 : :
178 : : /*
179 : : * Work on a mutable copy of the fixture tree.
180 : : * The original fixture is restored during cleanup
181 : : */
182 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
183 : :
184 : : /*
185 : : * Create the baseline database.
186 : : * The lock pattern protects every relative path under the path1/ subtree
187 : : */
188 : 2 : const char *arguments = "--database=lock_s2.db --progress "
189 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
190 : :
191 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
192 : :
193 : : /*
194 : : * Verify the baseline run output.
195 : : * It must show a new database with path1/ files recorded as checksum-locked
196 : : */
197 : 2 : const char *filename = "templates/0030_002_1.txt";
198 : :
199 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
200 : :
201 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
202 : :
203 : : /*
204 : : * Read the locked row before changing file timestamps.
205 : : * These values are expected to remain unchanged after the warning run
206 : : */
207 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
208 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
209 : :
210 : : /*
211 : : * Change only timestamps for one checksum-locked file.
212 : : * The file size and contents stay unchanged, but ctime and mtime drift
213 : : */
214 : 2 : ASSERT(SUCCESS == touch_file_mtime_with_reference_delta_ns(NULL,locked_file_path,999));
215 : :
216 : : /*
217 : : * Run an update that treats timestamp drift as meaningful.
218 : : * The drift must be reported as a warning, not saved to the DB
219 : : */
220 : 2 : arguments = "--update --check-level=QUICK --watch-timestamps --lock-checksum=\"^path1/.*\" "
221 : : "--database=lock_s2.db tests/fixtures/diffs/diff1";
222 : :
223 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
224 : :
225 : : /*
226 : : * Verify the warning output.
227 : : * The changed locked file must be reported with ctime and mtime drift
228 : : */
229 : 2 : filename = "templates/0030_002_2.txt";
230 : :
231 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
232 : :
233 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
234 : :
235 : : /*
236 : : * Re-read the same protected row after the warning run.
237 : : * The row must keep its original metadata, offset, and SHA512 checksum
238 : : */
239 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
240 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
241 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
242 : 2 : ASSERT(offset_before == offset_after);
243 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
244 : :
245 : : /*
246 : : * Remove the test database and restore the mutable fixture.
247 : : * This returns the workspace to the state expected by the next test
248 : : */
249 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
250 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
251 : :
252 : : /*
253 : : * Release managed buffers allocated by this test before returning
254 : : */
255 : 2 : m_del(pattern);
256 : 2 : m_del(result);
257 : :
258 : 2 : RETURN_STATUS;
259 : : }
260 : :
261 : : /**
262 : : * @brief Verify that locked timestamp drift is ignored when timestamp watching is off
263 : : *
264 : : * Covers README Example 10, case 2
265 : : *
266 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
267 : : * After one locked file gets a metadata-only timestamp change, the update run
268 : : * without `--watch-timestamps` or `--rehash-locked` must finish successfully
269 : : * and keep the original database row
270 : : */
271 : 2 : static Return test0030_3(void)
272 : : {
273 : 2 : INITTEST;
274 : :
275 : : /*
276 : : * Allocate managed buffers for the captured application output,
277 : : * the expected regular-expression template, and the resolved fixture path
278 : : */
279 : 2 : m_create(char,result,MEMORY_STRING);
280 : 2 : m_create(char,pattern,MEMORY_STRING);
281 : 2 : m_create(char,target_path,MEMORY_STRING);
282 : :
283 : : /*
284 : : * Name the database, the protected database key, and the fixture file
285 : : * whose timestamps will be changed without touching file contents
286 : : */
287 : 2 : const char *db_filename = "lock_s3.db";
288 : 2 : const char *locked_relative_path = "path1/AAA/BCB/CCC/a.txt";
289 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/BCB/CCC/a.txt";
290 : :
291 : : /*
292 : : * Keep before-and-after snapshots of the protected database row.
293 : : * They prove that ignored timestamp drift did not silently update locked data
294 : : */
295 : 2 : CmpctStat db_stat_before = {0};
296 : 2 : CmpctStat db_stat_after = {0};
297 : 2 : sqlite3_int64 offset_before = 0;
298 : 2 : sqlite3_int64 offset_after = 0;
299 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
300 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
301 : 2 : struct stat file_stat_before = {0};
302 : 2 : struct stat file_stat_after = {0};
303 : :
304 : : /*
305 : : * Enable deterministic TESTING output so the captured application log
306 : : * can be compared against a template
307 : : */
308 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
309 : :
310 : : /*
311 : : * Work on a mutable copy of the fixture tree.
312 : : * The original fixture is restored during cleanup
313 : : */
314 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
315 : :
316 : : /*
317 : : * Create the baseline database.
318 : : * The lock pattern protects every relative path under the path1/ subtree
319 : : */
320 : 2 : const char *arguments = "--database=lock_s3.db --progress "
321 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
322 : :
323 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
324 : :
325 : : /*
326 : : * Verify the baseline run output.
327 : : * It must show a new database with path1/ files recorded as checksum-locked
328 : : */
329 : 2 : const char *filename = "templates/0030_003_1.txt";
330 : :
331 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
332 : :
333 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
334 : :
335 : : /*
336 : : * Resolve the changed fixture path and read its matching locked DB row.
337 : : * Before any drift, the file timestamps should match the stored metadata
338 : : */
339 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
340 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
341 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
342 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_before));
343 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_before,&file_stat_before));
344 : :
345 : : /*
346 : : * Change only timestamps for one checksum-locked file.
347 : : * The file size and contents stay unchanged, but ctime and mtime drift
348 : : */
349 : 2 : ASSERT(SUCCESS == touch_file_mtime_with_reference_delta_ns(NULL,locked_file_path,999));
350 : :
351 : : /*
352 : : * Confirm that the test setup produced real timestamp drift.
353 : : * The stored DB metadata must no longer match the current file timestamps
354 : : */
355 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_after));
356 : 2 : ASSERT(false == cmpctstat_matches_stat_timestamps(&db_stat_before,&file_stat_after));
357 : :
358 : : /*
359 : : * Run an update without timestamp watching or deep locked-file rehashing.
360 : : * The timestamp drift must be ignored and the run must finish successfully
361 : : */
362 : 2 : arguments = "--update --check-level=QUICK --lock-checksum=\"^path1/.*\" "
363 : : "--database=lock_s3.db tests/fixtures/diffs/diff1";
364 : :
365 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
366 : :
367 : : /*
368 : : * Verify the successful output.
369 : : * It must report that no database changes were needed
370 : : */
371 : 2 : filename = "templates/0030_003_2.txt";
372 : :
373 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
374 : :
375 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
376 : :
377 : : /*
378 : : * Re-read the same protected row after the successful update run.
379 : : * The row must keep its original metadata, offset, and SHA512 checksum
380 : : */
381 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
382 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
383 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
384 : 2 : ASSERT(offset_before == offset_after);
385 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
386 : :
387 : : /*
388 : : * Remove the test database and restore the mutable fixture.
389 : : * This returns the workspace to the state expected by the next test
390 : : */
391 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
392 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
393 : :
394 : : /*
395 : : * Release managed buffers allocated by this test before returning
396 : : */
397 : 2 : m_del(target_path);
398 : 2 : m_del(pattern);
399 : 2 : m_del(result);
400 : :
401 : 2 : RETURN_STATUS;
402 : : }
403 : :
404 : : /**
405 : : * @brief Verify that locked timestamp drift is safely synchronized after rehash
406 : : *
407 : : * Covers README Example 10, case 5
408 : : *
409 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
410 : : * After one locked file gets a metadata-only timestamp change, the update run
411 : : * with `--watch-timestamps` and `--rehash-locked` must rehash the file,
412 : : * finish successfully, and store the new timestamps without changing its checksum
413 : : */
414 : 2 : static Return test0030_4(void)
415 : : {
416 : 2 : INITTEST;
417 : :
418 : : /*
419 : : * Allocate managed buffers for the captured application output,
420 : : * the expected regular-expression template, and the resolved fixture path
421 : : */
422 : 2 : m_create(char,result,MEMORY_STRING);
423 : 2 : m_create(char,pattern,MEMORY_STRING);
424 : 2 : m_create(char,target_path,MEMORY_STRING);
425 : :
426 : : /*
427 : : * Name the database, the protected database key, and the fixture file
428 : : * whose timestamps will be changed without touching file contents
429 : : */
430 : 2 : const char *db_filename = "lock_s4.db";
431 : 2 : const char *locked_relative_path = "path1/AAA/BCB/CCC/a.txt";
432 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/BCB/CCC/a.txt";
433 : :
434 : : /*
435 : : * Keep file and database snapshots around the timestamp drift.
436 : : * They prove that the drift is real and later synchronized to the database
437 : : */
438 : 2 : struct stat file_stat_before = {0};
439 : 2 : struct stat file_stat_drift = {0};
440 : 2 : struct stat file_stat_after = {0};
441 : 2 : CmpctStat db_stat_before = {0};
442 : 2 : CmpctStat db_stat_after = {0};
443 : 2 : sqlite3_int64 offset_before = 0;
444 : 2 : sqlite3_int64 offset_after = 0;
445 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
446 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
447 : :
448 : : /*
449 : : * Enable deterministic TESTING output so the captured application log
450 : : * can be compared against a template
451 : : */
452 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
453 : :
454 : : /*
455 : : * Work on a mutable copy of the fixture tree.
456 : : * The original fixture is restored during cleanup
457 : : */
458 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
459 : :
460 : : /*
461 : : * Create the baseline database.
462 : : * The lock pattern protects every relative path under the path1/ subtree
463 : : */
464 : 2 : const char *arguments = "--database=lock_s4.db --progress "
465 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
466 : :
467 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
468 : :
469 : : /*
470 : : * Verify the baseline run output.
471 : : * It must show a new database with path1/ files recorded as checksum-locked
472 : : */
473 : 2 : const char *filename = "templates/0030_004_1.txt";
474 : :
475 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
476 : :
477 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
478 : :
479 : : /*
480 : : * Resolve the changed fixture path and read its matching locked DB row.
481 : : * Before any drift, the file timestamps should match the stored metadata
482 : : */
483 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
484 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
485 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
486 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_before));
487 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_before,&file_stat_before));
488 : :
489 : : /*
490 : : * Change only timestamps for one checksum-locked file.
491 : : * The file size and contents stay unchanged, but ctime and mtime drift
492 : : */
493 : 2 : ASSERT(SUCCESS == touch_file_mtime_with_reference_delta_ns(NULL,locked_file_path,999));
494 : :
495 : : /*
496 : : * Confirm that the test setup produced real timestamp drift.
497 : : * The stored DB metadata must no longer match the current file timestamps
498 : : */
499 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_drift));
500 : 2 : ASSERT(false == cmpctstat_matches_stat_timestamps(&db_stat_before,&file_stat_drift));
501 : :
502 : : /*
503 : : * Run an update that watches timestamps and rehashes locked files.
504 : : * The checksum match must make the timestamp-only drift safe to save
505 : : */
506 : 2 : arguments = "--update --check-level=QUICK --watch-timestamps --rehash-locked "
507 : : "--lock-checksum=\"^path1/.*\" --database=lock_s4.db "
508 : : "tests/fixtures/diffs/diff1";
509 : :
510 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
511 : :
512 : : /*
513 : : * Verify the successful update output.
514 : : * The changed locked file must be reported as rehashed and synchronized
515 : : */
516 : 2 : filename = "templates/0030_004_2.txt";
517 : :
518 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
519 : :
520 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
521 : :
522 : : /*
523 : : * Re-read the protected row and current file state after the update.
524 : : * The database timestamps must now match the file again
525 : : */
526 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
527 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
528 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_after));
529 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_after,&file_stat_after));
530 : :
531 : : /*
532 : : * Verify that only metadata timestamps changed in the stored row.
533 : : * Size, allocation, identity, SHA512 offset, and SHA512 digest must stay stable
534 : : */
535 : 2 : ASSERT(db_stat_before.st_size == db_stat_after.st_size);
536 : 2 : ASSERT(db_stat_before.st_blocks == db_stat_after.st_blocks);
537 : 2 : ASSERT(db_stat_before.st_dev == db_stat_after.st_dev);
538 : 2 : ASSERT(db_stat_before.st_ino == db_stat_after.st_ino);
539 : 2 : ASSERT(offset_before == offset_after);
540 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
541 : :
542 : : /*
543 : : * Remove the test database and restore the mutable fixture.
544 : : * This returns the workspace to the state expected by the next test
545 : : */
546 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
547 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
548 : :
549 : : /*
550 : : * Release managed buffers allocated by this test before returning
551 : : */
552 : 2 : m_del(target_path);
553 : 2 : m_del(pattern);
554 : 2 : m_del(result);
555 : :
556 : 2 : RETURN_STATUS;
557 : : }
558 : :
559 : : /**
560 : : * @brief Verify that locked timestamp drift is synchronized by rehash alone
561 : : *
562 : : * Covers README Example 10, case 5
563 : : *
564 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
565 : : * After one locked file gets a metadata-only timestamp change, the update run
566 : : * with `--rehash-locked` and without `--watch-timestamps` must rehash the file,
567 : : * finish successfully, and store the new timestamps without changing its checksum.
568 : : * CLI and configuration runs repeat these checks from the same saved database
569 : : */
570 : 2 : static Return test0030_5(void)
571 : : {
572 : 2 : INITTEST;
573 : :
574 : : /*
575 : : * Allocate managed buffers for the captured application output,
576 : : * the expected regular-expression template, and the resolved fixture path
577 : : */
578 : 2 : m_create(char,result,MEMORY_STRING);
579 : 2 : m_create(char,pattern,MEMORY_STRING);
580 : 2 : m_create(char,target_path,MEMORY_STRING);
581 : :
582 : : /*
583 : : * Name the database, the protected database key, and the fixture file
584 : : * whose timestamps will be changed without touching file contents
585 : : */
586 : 2 : const char *db_filename = "lock_s5.db";
587 : 2 : const char *db_backup_filename = "0030_005_2.db";
588 : 2 : const char *locked_relative_path = "path1/AAA/BCB/CCC/a.txt";
589 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/BCB/CCC/a.txt";
590 : :
591 : : /*
592 : : * Keep file and database snapshots around the timestamp drift.
593 : : * They prove that the drift is real and later synchronized to the database
594 : : */
595 : 2 : struct stat file_stat_before = {0};
596 : 2 : struct stat file_stat_drift = {0};
597 : 2 : struct stat file_stat_after = {0};
598 : 2 : CmpctStat db_stat_before = {0};
599 : 2 : CmpctStat db_stat_after = {0};
600 : 2 : sqlite3_int64 offset_before = 0;
601 : 2 : sqlite3_int64 offset_after = 0;
602 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
603 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
604 : :
605 : : /*
606 : : * Enable deterministic TESTING output so the captured application log
607 : : * can be compared against a template
608 : : */
609 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
610 : :
611 : : /*
612 : : * Work on a mutable copy of the fixture tree.
613 : : * The original fixture is restored during cleanup
614 : : */
615 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
616 : :
617 : : /*
618 : : * Create the baseline database.
619 : : * The lock pattern protects every relative path under the path1/ subtree
620 : : */
621 : 2 : const char *arguments = "--database=lock_s5.db --progress "
622 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
623 : :
624 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
625 : :
626 : : /*
627 : : * Verify the baseline run output.
628 : : * It must show a new database with path1/ files recorded as checksum-locked
629 : : */
630 : 2 : const char *filename = "templates/0030_005_1.txt";
631 : :
632 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
633 : :
634 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
635 : :
636 : : /*
637 : : * Resolve the changed fixture path and read its matching locked DB row.
638 : : * Before any drift, the file timestamps should match the stored metadata
639 : : */
640 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
641 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
642 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
643 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_before));
644 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_before,&file_stat_before));
645 : :
646 : : /*
647 : : * Change only timestamps for one checksum-locked file.
648 : : * The file size and contents stay unchanged, but ctime and mtime drift
649 : : */
650 : 2 : ASSERT(SUCCESS == touch_file_mtime_with_reference_delta_ns(NULL,locked_file_path,999));
651 : :
652 : : /*
653 : : * Confirm that the test setup produced real timestamp drift.
654 : : * The stored DB metadata must no longer match the current file timestamps
655 : : */
656 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_drift));
657 : 2 : ASSERT(false == cmpctstat_matches_stat_timestamps(&db_stat_before,&file_stat_drift));
658 : :
659 : : /* Save the database before either update synchronizes the changed timestamps */
660 : 2 : ASSERT(SUCCESS == copy_path(db_filename,db_backup_filename));
661 : :
662 : : /*
663 : : * Run an update that rehashes locked files without timestamp watching.
664 : : * The checksum match must make the timestamp-only drift safe to save
665 : : */
666 : 2 : arguments = "--update --check-level=QUICK --rehash-locked --lock-checksum=\"^path1/.*\" "
667 : : "--database=lock_s5.db tests/fixtures/diffs/diff1";
668 : :
669 : : /*
670 : : * Verify the successful update output.
671 : : * All locked files must be reported as successfully rehashed
672 : : */
673 : 2 : filename = "templates/0030_005_2.txt";
674 : :
675 : 2 : const struct argument_test_case update_cases[] = {
676 : : {arguments,COMPLETED,filename,STDERR_MUST_BE_EMPTY},
677 : : {0}
678 : : };
679 : 2 : ASSERT(SUCCESS == run_and_match_templates(update_cases,NULL,NULL));
680 : :
681 : : /*
682 : : * Re-read the protected row and current file state after the update.
683 : : * The database timestamps must now match the file again
684 : : */
685 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
686 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
687 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_after));
688 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_after,&file_stat_after));
689 : :
690 : : /*
691 : : * Verify that only metadata timestamps changed in the stored row.
692 : : * Size, allocation, identity, SHA512 offset, and SHA512 digest must stay stable
693 : : */
694 : 2 : ASSERT(db_stat_before.st_size == db_stat_after.st_size);
695 : 2 : ASSERT(db_stat_before.st_blocks == db_stat_after.st_blocks);
696 : 2 : ASSERT(db_stat_before.st_dev == db_stat_after.st_dev);
697 : 2 : ASSERT(db_stat_before.st_ino == db_stat_after.st_ino);
698 : 2 : ASSERT(offset_before == offset_after);
699 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
700 : :
701 : : /* Restore only the database, then repeat the update and every row check.
702 : : The file retains the same timestamp drift for the configuration run */
703 : 2 : ASSERT(SUCCESS == copy_path(db_backup_filename,db_filename));
704 : 2 : ASSERT(SUCCESS == copy_path("tests/templates/0030_005_2.conf","0030_005_2.conf"));
705 : 2 : const struct argument_test_case file_cases[] = {
706 : : {"--config=0030_005_2.conf",COMPLETED,filename,STDERR_MUST_BE_EMPTY},
707 : : {0}
708 : : };
709 : 2 : ASSERT(SUCCESS == run_and_match_templates(file_cases,NULL,NULL));
710 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
711 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
712 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_after));
713 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_after,&file_stat_after));
714 : 2 : ASSERT(db_stat_before.st_size == db_stat_after.st_size);
715 : 2 : ASSERT(db_stat_before.st_blocks == db_stat_after.st_blocks);
716 : 2 : ASSERT(db_stat_before.st_dev == db_stat_after.st_dev);
717 : 2 : ASSERT(db_stat_before.st_ino == db_stat_after.st_ino);
718 : 2 : ASSERT(offset_before == offset_after);
719 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
720 : 2 : call(delete_path_if_present("0030_005_2.conf"));
721 : 2 : call(delete_path_if_present(db_backup_filename));
722 : :
723 : : /*
724 : : * Remove the test database and restore the mutable fixture.
725 : : * This returns the workspace to the state expected by the next test
726 : : */
727 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
728 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
729 : :
730 : : /*
731 : : * Release managed buffers allocated by this test before returning
732 : : */
733 : 2 : m_del(target_path);
734 : 2 : m_del(pattern);
735 : 2 : m_del(result);
736 : :
737 : 2 : RETURN_STATUS;
738 : : }
739 : :
740 : : /**
741 : : * @brief Verify mixed locked-file results during a watched rehash audit
742 : : *
743 : : * Covers README Example 10
744 : : *
745 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
746 : : * One locked file then receives a metadata-only timestamp change, while another
747 : : * locked file keeps its on-disk contents but gets a corrupted stored SHA512 value.
748 : : * The update run with `--watch-timestamps` and `--rehash-locked` must synchronize
749 : : * the safe timestamp drift, report the checksum mismatch as corruption, and leave
750 : : * the mismatched DB checksum untouched
751 : : */
752 : 2 : static Return test0030_6(void)
753 : : {
754 : 2 : INITTEST;
755 : :
756 : : /*
757 : : * Allocate managed buffers for the captured application output,
758 : : * the expected regular-expression template, and resolved fixture paths
759 : : */
760 : 2 : m_create(char,result,MEMORY_STRING);
761 : 2 : m_create(char,pattern,MEMORY_STRING);
762 : 2 : m_create(char,drift_target_path,MEMORY_STRING);
763 : 2 : m_create(char,mismatch_target_path,MEMORY_STRING);
764 : :
765 : : /*
766 : : * Name the database, the locked file that will drift only by timestamps,
767 : : * and the locked file whose stored checksum will be corrupted in the DB
768 : : */
769 : 2 : const char *db_filename = "lock_s6.db";
770 : 2 : const char *drift_relative_path = "path1/AAA/BCB/CCC/a.txt";
771 : 2 : const char *drift_file_path = "tests/fixtures/diffs/diff1/path1/AAA/BCB/CCC/a.txt";
772 : 2 : const char *mismatch_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
773 : 2 : const char *mismatch_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
774 : :
775 : : /*
776 : : * Keep file and database snapshots for the timestamp-drift row.
777 : : * They prove that the safe drift is saved without changing the checksum
778 : : */
779 : 2 : struct stat drift_file_stat_before = {0};
780 : 2 : struct stat drift_file_stat_after_touch = {0};
781 : 2 : struct stat drift_file_stat_after_update = {0};
782 : 2 : CmpctStat drift_db_stat_before = {0};
783 : 2 : CmpctStat drift_db_stat_after = {0};
784 : 2 : sqlite3_int64 drift_offset_before = 0;
785 : 2 : sqlite3_int64 drift_offset_after = 0;
786 : 2 : unsigned char drift_sha512_before[SHA512_DIGEST_LENGTH] = {0};
787 : 2 : unsigned char drift_sha512_after[SHA512_DIGEST_LENGTH] = {0};
788 : :
789 : : /*
790 : : * Keep checksum snapshots for the mismatch row.
791 : : * They prove that a detected corruption warning does not silently repair the DB
792 : : */
793 : 2 : CmpctStat mismatch_db_stat_before = {0};
794 : 2 : CmpctStat mismatch_db_stat_after = {0};
795 : 2 : sqlite3_int64 mismatch_offset_before = 0;
796 : 2 : sqlite3_int64 mismatch_offset_tampered = 0;
797 : 2 : sqlite3_int64 mismatch_offset_after = 0;
798 : 2 : unsigned char mismatch_sha512_before[SHA512_DIGEST_LENGTH] = {0};
799 : 2 : unsigned char mismatch_sha512_tampered[SHA512_DIGEST_LENGTH] = {0};
800 : 2 : unsigned char mismatch_sha512_after[SHA512_DIGEST_LENGTH] = {0};
801 : 2 : unsigned char mismatch_file_sha512[SHA512_DIGEST_LENGTH] = {0};
802 : :
803 : : /*
804 : : * Enable deterministic TESTING output so the captured application log
805 : : * can be compared against a template
806 : : */
807 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
808 : :
809 : : /*
810 : : * Work on a mutable copy of the fixture tree.
811 : : * The original fixture is restored during cleanup
812 : : */
813 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
814 : :
815 : : /*
816 : : * Create the baseline database.
817 : : * The lock pattern protects every relative path under the path1/ subtree
818 : : */
819 : 2 : const char *arguments = "--database=lock_s6.db --progress "
820 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
821 : :
822 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
823 : :
824 : : /*
825 : : * Verify the baseline run output.
826 : : * It must show a new database with path1/ files recorded as checksum-locked
827 : : */
828 : 2 : const char *filename = "templates/0030_006_1.txt";
829 : :
830 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
831 : :
832 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
833 : :
834 : : /*
835 : : * Resolve fixture paths and read the rows that will be exercised.
836 : : * The baseline checksum for the mismatch row must match the real file
837 : : */
838 : 2 : ASSERT(SUCCESS == construct_path(drift_file_path,drift_target_path));
839 : 2 : ASSERT(SUCCESS == construct_path(mismatch_file_path,mismatch_target_path));
840 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,drift_relative_path,&drift_db_stat_before));
841 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,drift_relative_path,&drift_offset_before,drift_sha512_before));
842 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(drift_target_path),&drift_file_stat_before));
843 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&drift_db_stat_before,&drift_file_stat_before));
844 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,mismatch_relative_path,&mismatch_db_stat_before));
845 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,mismatch_relative_path,&mismatch_offset_before,mismatch_sha512_before));
846 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(mismatch_target_path),mismatch_file_sha512));
847 : 2 : ASSERT(0 == memcmp(mismatch_sha512_before,mismatch_file_sha512,(size_t)SHA512_DIGEST_LENGTH));
848 : :
849 : : /*
850 : : * Change only timestamps for one checksum-locked file.
851 : : * The file size and contents stay unchanged, but ctime and mtime drift
852 : : */
853 : 2 : ASSERT(SUCCESS == touch_file_mtime_with_reference_delta_ns(NULL,drift_file_path,999));
854 : :
855 : : /*
856 : : * Confirm that the test setup produced real timestamp drift.
857 : : * The stored DB metadata must no longer match the current file timestamps
858 : : */
859 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(drift_target_path),&drift_file_stat_after_touch));
860 : 2 : ASSERT(false == cmpctstat_matches_stat_timestamps(&drift_db_stat_before,&drift_file_stat_after_touch));
861 : :
862 : : /*
863 : : * Corrupt the stored checksum for a different locked file.
864 : : * The file on disk stays unchanged, so the next rehash must detect a mismatch
865 : : */
866 : 2 : ASSERT(SUCCESS == db_tamper_sha512(db_filename,mismatch_relative_path));
867 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,mismatch_relative_path,&mismatch_offset_tampered,mismatch_sha512_tampered));
868 : 2 : ASSERT(mismatch_offset_before == mismatch_offset_tampered);
869 : 2 : ASSERT(0 != memcmp(mismatch_sha512_before,mismatch_sha512_tampered,(size_t)SHA512_DIGEST_LENGTH));
870 : 2 : ASSERT(0 != memcmp(mismatch_file_sha512,mismatch_sha512_tampered,(size_t)SHA512_DIGEST_LENGTH));
871 : :
872 : : /*
873 : : * Run an update that watches timestamps and rehashes locked files.
874 : : * The safe drift must be saved, while the checksum mismatch must warn
875 : : */
876 : 2 : arguments = "--update --check-level=QUICK --watch-timestamps --rehash-locked "
877 : : "--lock-checksum=\"^path1/.*\" --database=lock_s6.db "
878 : : "tests/fixtures/diffs/diff1";
879 : :
880 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
881 : :
882 : : /*
883 : : * Verify the warning output against the real scenario.
884 : : * It must show synchronized timestamp drift and a separate checksum mismatch
885 : : */
886 : 2 : filename = "templates/0030_006_2.txt";
887 : :
888 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
889 : :
890 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
891 : :
892 : : /*
893 : : * Re-read the drift row and current file state after the warning run.
894 : : * The database timestamps must now match the file again
895 : : */
896 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,drift_relative_path,&drift_db_stat_after));
897 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,drift_relative_path,&drift_offset_after,drift_sha512_after));
898 : 2 : ASSERT(SUCCESS == get_file_stat(m_text(drift_target_path),&drift_file_stat_after_update));
899 : 2 : ASSERT(cmpctstat_matches_stat_timestamps(&drift_db_stat_after,&drift_file_stat_after_update));
900 : :
901 : : /*
902 : : * Verify that only metadata timestamps changed in the safely rehashed row.
903 : : * Size, allocation, identity, SHA512 offset, and SHA512 digest must stay stable
904 : : */
905 : 2 : ASSERT(drift_db_stat_before.st_size == drift_db_stat_after.st_size);
906 : 2 : ASSERT(drift_db_stat_before.st_blocks == drift_db_stat_after.st_blocks);
907 : 2 : ASSERT(drift_db_stat_before.st_dev == drift_db_stat_after.st_dev);
908 : 2 : ASSERT(drift_db_stat_before.st_ino == drift_db_stat_after.st_ino);
909 : 2 : ASSERT(drift_offset_before == drift_offset_after);
910 : 2 : ASSERT(0 == memcmp(drift_sha512_before,drift_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
911 : :
912 : : /*
913 : : * Re-read the mismatched row after the warning run.
914 : : * The corrupted stored checksum must remain unchanged instead of being repaired
915 : : */
916 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,mismatch_relative_path,&mismatch_db_stat_after));
917 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,mismatch_relative_path,&mismatch_offset_after,mismatch_sha512_after));
918 : 2 : ASSERT(0 == memcmp(&mismatch_db_stat_before,&mismatch_db_stat_after,sizeof(CmpctStat)));
919 : 2 : ASSERT(mismatch_offset_tampered == mismatch_offset_after);
920 : 2 : ASSERT(0 == memcmp(mismatch_sha512_tampered,mismatch_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
921 : 2 : ASSERT(0 != memcmp(mismatch_file_sha512,mismatch_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
922 : :
923 : : /*
924 : : * Remove the test database and restore the mutable fixture.
925 : : * This returns the workspace to the state expected by the next test
926 : : */
927 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
928 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
929 : :
930 : : /*
931 : : * Release managed buffers allocated by this test before returning
932 : : */
933 : 2 : m_del(mismatch_target_path);
934 : 2 : m_del(drift_target_path);
935 : 2 : m_del(pattern);
936 : 2 : m_del(result);
937 : :
938 : 2 : RETURN_STATUS;
939 : : }
940 : :
941 : : /**
942 : : * @brief Verify that locked content corruption is reported but not saved
943 : : *
944 : : * Covers README Example 10
945 : : *
946 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
947 : : * After bytes inside one locked file change on disk, the update run with
948 : : * `--rehash-locked` must detect the checksum mismatch, report corruption,
949 : : * and keep the original database row untouched.
950 : : * CLI and configuration runs use the same saved database and corrupted file
951 : : */
952 : 2 : static Return test0030_7(void)
953 : : {
954 : 2 : INITTEST;
955 : :
956 : : /*
957 : : * Allocate managed buffers for the captured application output,
958 : : * the expected regular-expression template, and the resolved fixture path
959 : : */
960 : 2 : m_create(char,result,MEMORY_STRING);
961 : 2 : m_create(char,pattern,MEMORY_STRING);
962 : 2 : m_create(char,target_path,MEMORY_STRING);
963 : :
964 : : /*
965 : : * Name the database, the protected database key, and the fixture file
966 : : * whose on-disk bytes will be changed without changing its logical size
967 : : */
968 : 2 : const char *db_filename = "lock_s7.db";
969 : 2 : const char *db_backup_filename = "0030_007_2.db";
970 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
971 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
972 : :
973 : : /*
974 : : * Keep file and database snapshots around the content tampering.
975 : : * They prove that the warning run does not silently trust corrupted data
976 : : */
977 : 2 : CmpctStat db_stat_before = {0};
978 : 2 : CmpctStat db_stat_after = {0};
979 : 2 : sqlite3_int64 offset_before = 0;
980 : 2 : sqlite3_int64 offset_after = 0;
981 : 2 : unsigned char db_sha512_before[SHA512_DIGEST_LENGTH] = {0};
982 : 2 : unsigned char db_sha512_after[SHA512_DIGEST_LENGTH] = {0};
983 : 2 : unsigned char file_sha512_before[SHA512_DIGEST_LENGTH] = {0};
984 : 2 : unsigned char file_sha512_after_tamper[SHA512_DIGEST_LENGTH] = {0};
985 : :
986 : : /*
987 : : * Enable deterministic TESTING output so the captured application log
988 : : * can be compared against a template
989 : : */
990 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
991 : :
992 : : /*
993 : : * Work on a mutable copy of the fixture tree.
994 : : * The original fixture is restored during cleanup
995 : : */
996 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
997 : :
998 : : /*
999 : : * Create the baseline database.
1000 : : * The lock pattern protects every relative path under the path1/ subtree
1001 : : */
1002 : 2 : const char *arguments = "--database=lock_s7.db --progress "
1003 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
1004 : :
1005 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1006 : :
1007 : : /*
1008 : : * Verify the baseline run output.
1009 : : * It must show a new database with path1/ files recorded as checksum-locked
1010 : : */
1011 : 2 : const char *filename = "templates/0030_007_1.txt";
1012 : :
1013 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1014 : :
1015 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1016 : :
1017 : : /*
1018 : : * Resolve the target fixture path and read its matching locked DB row.
1019 : : * Before tampering, the stored checksum must match the real file checksum
1020 : : */
1021 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
1022 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
1023 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,db_sha512_before));
1024 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_before));
1025 : 2 : ASSERT(0 == memcmp(db_sha512_before,file_sha512_before,(size_t)SHA512_DIGEST_LENGTH));
1026 : :
1027 : : /*
1028 : : * Change bytes inside one checksum-locked file on disk.
1029 : : * The helper preserves logical size, so the mismatch is about content integrity
1030 : : */
1031 : 2 : ASSERT(SUCCESS == tamper_locked_file_bytes(locked_file_path));
1032 : :
1033 : : /*
1034 : : * Confirm that the test setup produced real content corruption.
1035 : : * The current file checksum must no longer match the protected DB checksum
1036 : : */
1037 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_after_tamper));
1038 : 2 : ASSERT(0 != memcmp(file_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
1039 : 2 : ASSERT(0 != memcmp(db_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
1040 : :
1041 : : /* Preserve the database before either update checks the corrupted file */
1042 : 2 : ASSERT(SUCCESS == copy_path(db_filename,db_backup_filename));
1043 : :
1044 : : /*
1045 : : * Run an update with deep locked-file verification enabled.
1046 : : * The content mismatch must be reported as a warning, not saved to the DB
1047 : : */
1048 : 2 : arguments = "--update --check-level=QUICK --rehash-locked --lock-checksum=\"^path1/.*\" "
1049 : : "--database=lock_s7.db tests/fixtures/diffs/diff1";
1050 : :
1051 : : /*
1052 : : * Verify the warning output against the real scenario.
1053 : : * The changed locked file must be reported as checksum corruption
1054 : : */
1055 : 2 : filename = "templates/0030_007_2.txt";
1056 : :
1057 : 2 : const struct argument_test_case update_cases[] = {
1058 : : {arguments,WARNING,filename,STDERR_MUST_BE_EMPTY},
1059 : : {0}
1060 : : };
1061 : 2 : ASSERT(SUCCESS == run_and_match_templates(update_cases,NULL,NULL));
1062 : :
1063 : : /*
1064 : : * Re-read the same protected row after the warning run.
1065 : : * The row must keep its original metadata, offset, and SHA512 checksum
1066 : : */
1067 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
1068 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,db_sha512_after));
1069 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
1070 : 2 : ASSERT(offset_before == offset_after);
1071 : 2 : ASSERT(0 == memcmp(db_sha512_before,db_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
1072 : 2 : ASSERT(0 != memcmp(db_sha512_after,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
1073 : :
1074 : : /* Restore only the database, then repeat the warning and protected-row checks.
1075 : : The same corrupted contents remain in place for the configuration run */
1076 : 2 : ASSERT(SUCCESS == copy_path(db_backup_filename,db_filename));
1077 : 2 : ASSERT(SUCCESS == copy_path("tests/templates/0030_007_2.conf","0030_007_2.conf"));
1078 : 2 : const struct argument_test_case file_cases[] = {
1079 : : {"--config=0030_007_2.conf",WARNING,filename,STDERR_MUST_BE_EMPTY},
1080 : : {0}
1081 : : };
1082 : 2 : ASSERT(SUCCESS == run_and_match_templates(file_cases,NULL,NULL));
1083 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
1084 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,db_sha512_after));
1085 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
1086 : 2 : ASSERT(offset_before == offset_after);
1087 : 2 : ASSERT(0 == memcmp(db_sha512_before,db_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
1088 : 2 : ASSERT(0 != memcmp(db_sha512_after,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
1089 : 2 : call(delete_path_if_present("0030_007_2.conf"));
1090 : 2 : call(delete_path_if_present(db_backup_filename));
1091 : :
1092 : : /*
1093 : : * Remove the test database and restore the mutable fixture.
1094 : : * This returns the workspace to the state expected by the next test
1095 : : */
1096 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
1097 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
1098 : :
1099 : : /*
1100 : : * Release managed buffers allocated by this test before returning
1101 : : */
1102 : 2 : m_del(target_path);
1103 : 2 : m_del(pattern);
1104 : 2 : m_del(result);
1105 : :
1106 : 2 : RETURN_STATUS;
1107 : : }
1108 : :
1109 : : /**
1110 : : * @brief Verify that watched locked content corruption is reported but not saved
1111 : : *
1112 : : * Covers README Example 10
1113 : : *
1114 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
1115 : : * After bytes inside one locked file change on disk, the update run with
1116 : : * `--watch-timestamps` and `--rehash-locked` must detect checksum corruption.
1117 : : * Timestamp watching must not make the corrupted database row updateable
1118 : : */
1119 : 2 : static Return test0030_8(void)
1120 : : {
1121 : 2 : INITTEST;
1122 : :
1123 : : /*
1124 : : * Allocate managed buffers for the captured application output,
1125 : : * the expected regular-expression template, and the resolved fixture path
1126 : : */
1127 : 2 : m_create(char,result,MEMORY_STRING);
1128 : 2 : m_create(char,pattern,MEMORY_STRING);
1129 : 2 : m_create(char,target_path,MEMORY_STRING);
1130 : :
1131 : : /*
1132 : : * Name the database, the protected database key, and the fixture file
1133 : : * whose on-disk bytes will be changed without changing its logical size
1134 : : */
1135 : 2 : const char *db_filename = "lock_s8.db";
1136 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
1137 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
1138 : :
1139 : : /*
1140 : : * Keep file and database snapshots around the content tampering.
1141 : : * They prove that the warning run does not silently trust corrupted data
1142 : : */
1143 : 2 : CmpctStat db_stat_before = {0};
1144 : 2 : CmpctStat db_stat_after = {0};
1145 : 2 : sqlite3_int64 offset_before = 0;
1146 : 2 : sqlite3_int64 offset_after = 0;
1147 : 2 : unsigned char db_sha512_before[SHA512_DIGEST_LENGTH] = {0};
1148 : 2 : unsigned char db_sha512_after[SHA512_DIGEST_LENGTH] = {0};
1149 : 2 : unsigned char file_sha512_before[SHA512_DIGEST_LENGTH] = {0};
1150 : 2 : unsigned char file_sha512_after_tamper[SHA512_DIGEST_LENGTH] = {0};
1151 : :
1152 : : /*
1153 : : * Enable deterministic TESTING output so the captured application log
1154 : : * can be compared against a template
1155 : : */
1156 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
1157 : :
1158 : : /*
1159 : : * Work on a mutable copy of the fixture tree.
1160 : : * The original fixture is restored during cleanup
1161 : : */
1162 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
1163 : :
1164 : : /*
1165 : : * Create the baseline database.
1166 : : * The lock pattern protects every relative path under the path1/ subtree
1167 : : */
1168 : 2 : const char *arguments = "--database=lock_s8.db --progress "
1169 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
1170 : :
1171 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1172 : :
1173 : : /*
1174 : : * Verify the baseline run output.
1175 : : * It must show a new database with path1/ files recorded as checksum-locked
1176 : : */
1177 : 2 : const char *filename = "templates/0030_008_1.txt";
1178 : :
1179 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1180 : :
1181 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1182 : :
1183 : : /*
1184 : : * Resolve the target fixture path and read its matching locked DB row.
1185 : : * Before tampering, the stored checksum must match the real file checksum
1186 : : */
1187 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
1188 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
1189 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,db_sha512_before));
1190 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_before));
1191 : 2 : ASSERT(0 == memcmp(db_sha512_before,file_sha512_before,(size_t)SHA512_DIGEST_LENGTH));
1192 : :
1193 : : /*
1194 : : * Change bytes inside one checksum-locked file on disk.
1195 : : * The helper preserves logical size, so the mismatch is about content integrity
1196 : : */
1197 : 2 : ASSERT(SUCCESS == tamper_locked_file_bytes(locked_file_path));
1198 : :
1199 : : /*
1200 : : * Confirm that the test setup produced real content corruption.
1201 : : * The current file checksum must no longer match the protected DB checksum
1202 : : */
1203 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_after_tamper));
1204 : 2 : ASSERT(0 != memcmp(file_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
1205 : 2 : ASSERT(0 != memcmp(db_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
1206 : :
1207 : : /*
1208 : : * Run an update that watches timestamps and rehashes locked files.
1209 : : * The content mismatch must be reported as a warning, not saved to the DB
1210 : : */
1211 : 2 : arguments = "--update --check-level=QUICK --watch-timestamps --rehash-locked "
1212 : : "--lock-checksum=\"^path1/.*\" --database=lock_s8.db "
1213 : : "tests/fixtures/diffs/diff1";
1214 : :
1215 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
1216 : :
1217 : : /*
1218 : : * Verify the warning output against the real scenario.
1219 : : * The changed locked file must be reported as checksum corruption
1220 : : */
1221 : 2 : filename = "templates/0030_008_2.txt";
1222 : :
1223 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1224 : :
1225 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1226 : :
1227 : : /*
1228 : : * Re-read the same protected row after the warning run.
1229 : : * The row must keep its original metadata, offset, and SHA512 checksum
1230 : : */
1231 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
1232 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,db_sha512_after));
1233 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
1234 : 2 : ASSERT(offset_before == offset_after);
1235 : 2 : ASSERT(0 == memcmp(db_sha512_before,db_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
1236 : 2 : ASSERT(0 != memcmp(db_sha512_after,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
1237 : :
1238 : : /*
1239 : : * Remove the test database and restore the mutable fixture.
1240 : : * This returns the workspace to the state expected by the next test
1241 : : */
1242 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
1243 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
1244 : :
1245 : : /*
1246 : : * Release managed buffers allocated by this test before returning
1247 : : */
1248 : 2 : m_del(target_path);
1249 : 2 : m_del(pattern);
1250 : 2 : m_del(result);
1251 : :
1252 : 2 : RETURN_STATUS;
1253 : : }
1254 : :
1255 : : /**
1256 : : * @brief Verify that watched unchanged locked files finish successfully
1257 : : *
1258 : : * Covers README Example 10, case 3
1259 : : *
1260 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
1261 : : * When the locked files still match the stored metadata and `--rehash-locked`
1262 : : * is not used, `--watch-timestamps` must not create warnings or updates.
1263 : : * All protected rows must stay unchanged in the database
1264 : : */
1265 : 2 : static Return test0030_9(void)
1266 : : {
1267 : 2 : INITTEST;
1268 : :
1269 : : /*
1270 : : * Allocate managed buffers for the captured application output,
1271 : : * the expected regular-expression template, and reusable resolved paths
1272 : : */
1273 : 2 : m_create(char,result,MEMORY_STRING);
1274 : 2 : m_create(char,pattern,MEMORY_STRING);
1275 : 2 : m_create(char,target_path,MEMORY_STRING);
1276 : :
1277 : : /*
1278 : : * Name the database and every file protected by the lock pattern.
1279 : : * This test checks all protected rows, because no locked path should change
1280 : : */
1281 : 2 : const char *db_filename = "lock_s9.db";
1282 : 2 : const char *locked_relative_paths[] = {
1283 : : "path1/AAA/BCB/CCC/a.txt",
1284 : : "path1/AAA/ZAW/A/b/c/a_file.txt",
1285 : : "path1/AAA/ZAW/D/e/f/b_file.txt",
1286 : : };
1287 : 2 : const char *locked_file_paths[] = {
1288 : : "tests/fixtures/diffs/diff1/path1/AAA/BCB/CCC/a.txt",
1289 : : "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt",
1290 : : "tests/fixtures/diffs/diff1/path1/AAA/ZAW/D/e/f/b_file.txt",
1291 : : };
1292 : 2 : const size_t locked_path_count = sizeof(locked_relative_paths) / sizeof(locked_relative_paths[0]);
1293 : :
1294 : : /*
1295 : : * Keep before-and-after snapshots of every protected database row.
1296 : : * They prove that a successful watched update did not rewrite locked data
1297 : : */
1298 : 2 : CmpctStat db_stat_before[3] = {0};
1299 : 2 : CmpctStat db_stat_after[3] = {0};
1300 : 2 : struct stat file_stat_before[3] = {0};
1301 : 2 : struct stat file_stat_after[3] = {0};
1302 : 2 : sqlite3_int64 offset_before[3] = {0};
1303 : 2 : sqlite3_int64 offset_after[3] = {0};
1304 : 2 : unsigned char sha512_before[3][SHA512_DIGEST_LENGTH] = {0};
1305 : 2 : unsigned char sha512_after[3][SHA512_DIGEST_LENGTH] = {0};
1306 : :
1307 : : /*
1308 : : * Enable deterministic TESTING output so the captured application log
1309 : : * can be compared against a template
1310 : : */
1311 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
1312 : :
1313 : : /*
1314 : : * Work on a mutable copy of the fixture tree.
1315 : : * The original fixture is restored during cleanup
1316 : : */
1317 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
1318 : :
1319 : : /*
1320 : : * Create the baseline database.
1321 : : * The lock pattern protects every relative path under the path1/ subtree
1322 : : */
1323 : 2 : const char *arguments = "--database=lock_s9.db --progress "
1324 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
1325 : :
1326 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1327 : :
1328 : : /*
1329 : : * Verify the baseline run output.
1330 : : * It must show a new database with path1/ files recorded as checksum-locked
1331 : : */
1332 : 2 : const char *filename = "templates/0030_009_1.txt";
1333 : :
1334 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1335 : :
1336 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1337 : :
1338 : : /*
1339 : : * Read all protected rows and current file timestamps before the update.
1340 : : * For this README case, every locked file must still match the database
1341 : : */
1342 [ + + ]: 8 : for(size_t index = 0; index < locked_path_count; index++)
1343 : : {
1344 : 6 : ASSERT(SUCCESS == construct_path(locked_file_paths[index],target_path));
1345 : 6 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_paths[index],&db_stat_before[index]));
1346 : 6 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_paths[index],&offset_before[index],sha512_before[index]));
1347 : 6 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_before[index]));
1348 : 6 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_before[index],&file_stat_before[index]));
1349 : : }
1350 : :
1351 : : /*
1352 : : * Run an update that watches timestamps but does not rehash locked files.
1353 : : * Because all protected metadata still matches, the run must be successful
1354 : : */
1355 : 2 : arguments = "--update --check-level=QUICK --watch-timestamps --lock-checksum=\"^path1/.*\" "
1356 : : "--database=lock_s9.db tests/fixtures/diffs/diff1";
1357 : :
1358 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1359 : :
1360 : : /*
1361 : : * Verify the successful output.
1362 : : * It must report no changes and no locked-file warnings
1363 : : */
1364 : 2 : filename = "templates/0030_009_2.txt";
1365 : :
1366 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1367 : :
1368 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1369 : :
1370 : : /*
1371 : : * Re-read all protected rows after the successful watched update.
1372 : : * Metadata, SHA512 offsets, and SHA512 digests must remain unchanged
1373 : : */
1374 [ + + ]: 8 : for(size_t index = 0; index < locked_path_count; index++)
1375 : : {
1376 : 6 : ASSERT(SUCCESS == construct_path(locked_file_paths[index],target_path));
1377 : 6 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_paths[index],&db_stat_after[index]));
1378 : 6 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_paths[index],&offset_after[index],sha512_after[index]));
1379 : 6 : ASSERT(SUCCESS == get_file_stat(m_text(target_path),&file_stat_after[index]));
1380 : 6 : ASSERT(cmpctstat_matches_stat_timestamps(&db_stat_after[index],&file_stat_after[index]));
1381 : 6 : ASSERT(0 == memcmp(&db_stat_before[index],&db_stat_after[index],sizeof(CmpctStat)));
1382 : 6 : ASSERT(offset_before[index] == offset_after[index]);
1383 : 6 : ASSERT(0 == memcmp(sha512_before[index],sha512_after[index],(size_t)SHA512_DIGEST_LENGTH));
1384 : : }
1385 : :
1386 : : /*
1387 : : * Remove the test database and restore the mutable fixture.
1388 : : * This returns the workspace to the state expected by the next test
1389 : : */
1390 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
1391 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
1392 : :
1393 : : /*
1394 : : * Release managed buffers allocated by this test before returning
1395 : : */
1396 : 2 : m_del(target_path);
1397 : 2 : m_del(pattern);
1398 : 2 : m_del(result);
1399 : :
1400 : 2 : RETURN_STATUS;
1401 : : }
1402 : :
1403 : : /**
1404 : : * @brief Verify that deleting a locked file warns and keeps the DB row
1405 : : *
1406 : : * Covers README Example 9
1407 : : *
1408 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
1409 : : * When a locked file disappears before an update, the program must warn and
1410 : : * keep every locked row in the database. The deleted row must remain unchanged
1411 : : */
1412 : 2 : static Return test0030_10(void)
1413 : : {
1414 : 2 : INITTEST;
1415 : :
1416 : : /*
1417 : : * Allocate managed buffers for the captured application output,
1418 : : * the expected regular-expression template, and the resolved deleted path
1419 : : */
1420 : 2 : m_create(char,result,MEMORY_STRING);
1421 : 2 : m_create(char,pattern,MEMORY_STRING);
1422 : 2 : m_create(char,target_path,MEMORY_STRING);
1423 : :
1424 : : /*
1425 : : * Name the database, the deleted locked row, and the matching fixture file.
1426 : : * The other locked rows are checked separately to prove the whole lock set survives
1427 : : */
1428 : 2 : const char *db_filename = "lock_s10.db";
1429 : 2 : const char *deleted_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
1430 : 2 : const char *deleted_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
1431 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
1432 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
1433 : :
1434 : : /*
1435 : : * Keep before-and-after snapshots of the deleted locked row.
1436 : : * They prove that reporting the missing file did not rewrite protected data
1437 : : */
1438 : 2 : CmpctStat db_stat_before = {0};
1439 : 2 : CmpctStat db_stat_after = {0};
1440 : 2 : sqlite3_int64 offset_before = 0;
1441 : 2 : sqlite3_int64 offset_after = 0;
1442 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
1443 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
1444 : 2 : bool row_exists = false;
1445 : :
1446 : : /*
1447 : : * Enable deterministic TESTING output so the captured application log
1448 : : * can be compared against a template
1449 : : */
1450 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
1451 : :
1452 : : /*
1453 : : * Work on a mutable copy of the fixture tree.
1454 : : * The original fixture is restored during cleanup
1455 : : */
1456 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
1457 : :
1458 : : /*
1459 : : * Create the baseline database.
1460 : : * The lock pattern protects every relative path under the path1/ subtree
1461 : : */
1462 : 2 : const char *arguments = "--database=lock_s10.db --progress "
1463 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
1464 : :
1465 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1466 : :
1467 : : /*
1468 : : * Verify the baseline run output.
1469 : : * It must show a new database with path1/ files recorded as checksum-locked
1470 : : */
1471 : 2 : const char *filename = "templates/0030_010_1.txt";
1472 : :
1473 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1474 : :
1475 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1476 : :
1477 : : /*
1478 : : * Read the locked row before deleting its file.
1479 : : * The same values must still be present after the warning run
1480 : : */
1481 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,deleted_relative_path,&db_stat_before));
1482 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,deleted_relative_path,&offset_before,sha512_before));
1483 : 2 : ASSERT(SUCCESS == construct_path(deleted_file_path,target_path));
1484 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,deleted_relative_path,R_OK,FILE_ACCESS_ALLOWED));
1485 : :
1486 : : /*
1487 : : * Delete one checksum-locked file from disk.
1488 : : * This must be treated as a lock violation, not as a normal missing-file cleanup
1489 : : */
1490 : 2 : ASSERT(SUCCESS == delete_path(deleted_file_path));
1491 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,deleted_relative_path,R_OK,FILE_NOT_FOUND));
1492 : :
1493 : : /*
1494 : : * Run an update with the same lock pattern.
1495 : : * The missing locked file must be reported as a warning and kept in the DB
1496 : : */
1497 : 2 : arguments = "--update --check-level=QUICK --lock-checksum=\"^path1/.*\" "
1498 : : "--database=lock_s10.db tests/fixtures/diffs/diff1";
1499 : :
1500 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
1501 : :
1502 : : /*
1503 : : * Verify the warning output against the real scenario.
1504 : : * The deleted locked file must be reported as disappeared from disk
1505 : : */
1506 : 2 : filename = "templates/0030_010_2.txt";
1507 : :
1508 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1509 : :
1510 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1511 : :
1512 : : /*
1513 : : * Re-read the deleted file row after the warning run.
1514 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
1515 : : */
1516 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,deleted_relative_path,&db_stat_after));
1517 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,deleted_relative_path,&offset_after,sha512_after));
1518 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
1519 : 2 : ASSERT(offset_before == offset_after);
1520 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
1521 : :
1522 : : /*
1523 : : * Confirm that the full locked path set remains in the database.
1524 : : * A missing locked file must not cause any protected row to be dropped
1525 : : */
1526 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,deleted_relative_path,&row_exists));
1527 : 2 : ASSERT(row_exists == true);
1528 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
1529 : 2 : ASSERT(row_exists == true);
1530 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
1531 : 2 : ASSERT(row_exists == true);
1532 : :
1533 : : /*
1534 : : * Remove the test database and restore the mutable fixture.
1535 : : * This returns the workspace to the state expected by the next test
1536 : : */
1537 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
1538 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
1539 : :
1540 : : /*
1541 : : * Release managed buffers allocated by this test before returning
1542 : : */
1543 : 2 : m_del(target_path);
1544 : 2 : m_del(pattern);
1545 : 2 : m_del(result);
1546 : :
1547 : 2 : RETURN_STATUS;
1548 : : }
1549 : :
1550 : : /**
1551 : : * @brief Verify that --db-drop-inaccessible does not drop an access-denied locked file
1552 : : *
1553 : : * Covers README Examples 9 and 11
1554 : : *
1555 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
1556 : : * Then a test hook makes one locked file look access-denied during an update run
1557 : : * with `--db-drop-inaccessible`. The program must warn about the locked file
1558 : : * and keep the protected database row unchanged
1559 : : */
1560 : 2 : static Return test0030_11(void)
1561 : : {
1562 : 2 : INITTEST;
1563 : :
1564 : : /*
1565 : : * Allocate managed buffers for captured application output, the expected
1566 : : * regular-expression template, and the resolved locked file path
1567 : : */
1568 : 2 : m_create(char,result,MEMORY_STRING);
1569 : 2 : m_create(char,pattern,MEMORY_STRING);
1570 : 2 : m_create(char,target_path,MEMORY_STRING);
1571 : :
1572 : : /*
1573 : : * Name the database, the access-denied locked row, and the matching
1574 : : * fixture file. The other locked rows prove that the whole lock set survives
1575 : : */
1576 : 2 : const char *db_filename = "lock_s11.db";
1577 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
1578 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
1579 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
1580 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
1581 : :
1582 : : /*
1583 : : * Keep before-and-after snapshots of the access-denied locked row.
1584 : : * They prove that a warning result did not rewrite protected data
1585 : : */
1586 : 2 : CmpctStat db_stat_before = {0};
1587 : 2 : CmpctStat db_stat_after = {0};
1588 : 2 : sqlite3_int64 offset_before = 0;
1589 : 2 : sqlite3_int64 offset_after = 0;
1590 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
1591 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
1592 : 2 : bool row_exists = false;
1593 : :
1594 : : /*
1595 : : * Enable deterministic TESTING output so the captured application log
1596 : : * can be compared against a template
1597 : : */
1598 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
1599 : :
1600 : : /*
1601 : : * Work on a mutable copy of the fixture tree.
1602 : : * The original fixture is restored during cleanup
1603 : : */
1604 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
1605 : :
1606 : : /*
1607 : : * Create the baseline database.
1608 : : * The lock pattern protects every relative path under the path1/ subtree
1609 : : */
1610 : 2 : const char *arguments = "--database=lock_s11.db --progress "
1611 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
1612 : :
1613 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1614 : :
1615 : : /*
1616 : : * Verify the baseline run output.
1617 : : * It must show a new database with path1/ files recorded as checksum-locked
1618 : : */
1619 : 2 : const char *filename = "templates/0030_011_1.txt";
1620 : :
1621 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1622 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1623 : :
1624 : : /*
1625 : : * Read the locked row before making access checks fail.
1626 : : * The same values must still be present after the warning run
1627 : : */
1628 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
1629 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
1630 : :
1631 : : /*
1632 : : * Confirm the fixture file itself is present and readable.
1633 : : * This test covers an access-denied report, not a missing-file scenario
1634 : : */
1635 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
1636 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ALLOWED));
1637 : :
1638 : : /*
1639 : : * Force access checks for exactly this locked file to return access denied.
1640 : : * This models a protected file that the application can see in the tree but cannot read
1641 : : */
1642 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",locked_relative_path));
1643 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS","FILE_ACCESS_DENIED"));
1644 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_DENIED));
1645 : :
1646 : : /*
1647 : : * Run an update that would normally drop inaccessible records.
1648 : : * The checksum lock must override that cleanup and turn the condition into a warning
1649 : : */
1650 : 2 : arguments = "--update --check-level=QUICK --db-drop-inaccessible --lock-checksum=\"^path1/.*\" "
1651 : : "--database=lock_s11.db tests/fixtures/diffs/diff1";
1652 : :
1653 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
1654 : :
1655 : : /*
1656 : : * Disable the access-check hook before inspecting output and database state.
1657 : : * Later assertions should observe the real filesystem again
1658 : : */
1659 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",""));
1660 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS",""));
1661 : :
1662 : : /*
1663 : : * Verify the warning output against the real command-line scenario.
1664 : : * The run must report checksum-locked access denial and return WARNING
1665 : : */
1666 : 2 : filename = "templates/0030_011_2.txt";
1667 : :
1668 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1669 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1670 : :
1671 : : /*
1672 : : * Re-read the access-denied locked row after the warning run.
1673 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
1674 : : */
1675 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
1676 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
1677 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
1678 : 2 : ASSERT(offset_before == offset_after);
1679 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
1680 : :
1681 : : /*
1682 : : * Confirm that the full locked path set remains in the database.
1683 : : * --db-drop-inaccessible must not drop any checksum-locked row
1684 : : */
1685 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,locked_relative_path,&row_exists));
1686 : 2 : ASSERT(row_exists == true);
1687 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
1688 : 2 : ASSERT(row_exists == true);
1689 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
1690 : 2 : ASSERT(row_exists == true);
1691 : :
1692 : : /*
1693 : : * Remove the test database and restore the mutable fixture.
1694 : : * This returns the workspace to the state expected by the next test
1695 : : */
1696 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
1697 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
1698 : :
1699 : : /*
1700 : : * Release managed buffers allocated by this test before returning
1701 : : */
1702 : 2 : m_del(target_path);
1703 : 2 : m_del(pattern);
1704 : 2 : m_del(result);
1705 : :
1706 : 2 : RETURN_STATUS;
1707 : : }
1708 : :
1709 : : /**
1710 : : * @brief Verify that --db-drop-inaccessible does not drop a locked file after an access-check failure
1711 : : *
1712 : : * Covers README Examples 9 and 11
1713 : : *
1714 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
1715 : : * Then a test hook makes one locked file return an access-check failure during
1716 : : * an update run with `--db-drop-inaccessible`. The program must warn about the
1717 : : * locked file and keep the protected database row unchanged
1718 : : */
1719 : 2 : static Return test0030_12(void)
1720 : : {
1721 : 2 : INITTEST;
1722 : :
1723 : : /*
1724 : : * Allocate managed buffers for captured application output, the expected
1725 : : * regular-expression template, and the resolved locked file path
1726 : : */
1727 : 2 : m_create(char,result,MEMORY_STRING);
1728 : 2 : m_create(char,pattern,MEMORY_STRING);
1729 : 2 : m_create(char,target_path,MEMORY_STRING);
1730 : :
1731 : : /*
1732 : : * Name the database, the locked row that will fail access checking, and
1733 : : * the matching fixture file. The other locked rows prove that the whole lock set survives
1734 : : */
1735 : 2 : const char *db_filename = "lock_s12.db";
1736 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
1737 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
1738 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
1739 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
1740 : :
1741 : : /*
1742 : : * Keep before-and-after snapshots of the locked row that will fail access checking.
1743 : : * They prove that a warning result did not rewrite protected data
1744 : : */
1745 : 2 : CmpctStat db_stat_before = {0};
1746 : 2 : CmpctStat db_stat_after = {0};
1747 : 2 : sqlite3_int64 offset_before = 0;
1748 : 2 : sqlite3_int64 offset_after = 0;
1749 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
1750 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
1751 : 2 : bool row_exists = false;
1752 : :
1753 : : /*
1754 : : * Enable deterministic TESTING output so the captured application log
1755 : : * can be compared against a template
1756 : : */
1757 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
1758 : :
1759 : : /*
1760 : : * Work on a mutable copy of the fixture tree.
1761 : : * The original fixture is restored during cleanup
1762 : : */
1763 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
1764 : :
1765 : : /*
1766 : : * Create the baseline database.
1767 : : * The lock pattern protects every relative path under the path1/ subtree
1768 : : */
1769 : 2 : const char *arguments = "--database=lock_s12.db --progress "
1770 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
1771 : :
1772 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1773 : :
1774 : : /*
1775 : : * Verify the baseline run output.
1776 : : * It must show a new database with path1/ files recorded as checksum-locked
1777 : : */
1778 : 2 : const char *filename = "templates/0030_012_1.txt";
1779 : :
1780 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1781 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1782 : :
1783 : : /*
1784 : : * Read the locked row before making access checks fail.
1785 : : * The same values must still be present after the warning run
1786 : : */
1787 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
1788 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
1789 : :
1790 : : /*
1791 : : * Confirm the fixture file itself is present and readable.
1792 : : * This test covers an access-check failure, not a missing-file scenario
1793 : : */
1794 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
1795 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ALLOWED));
1796 : :
1797 : : /*
1798 : : * Force access checks for exactly this locked file to return an access-check failure.
1799 : : * This models an unexpected access-check problem while the checksum lock is active
1800 : : */
1801 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",locked_relative_path));
1802 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS","FILE_ACCESS_ERROR"));
1803 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ERROR));
1804 : :
1805 : : /*
1806 : : * Run an update that would normally drop inaccessible records.
1807 : : * The checksum lock must override that cleanup and turn the condition into a warning
1808 : : */
1809 : 2 : arguments = "--update --check-level=QUICK --db-drop-inaccessible --lock-checksum=\"^path1/.*\" "
1810 : : "--database=lock_s12.db tests/fixtures/diffs/diff1";
1811 : :
1812 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
1813 : :
1814 : : /*
1815 : : * Disable the access-check hook before inspecting output and database state.
1816 : : * Later assertions should observe the real filesystem again
1817 : : */
1818 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",""));
1819 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS",""));
1820 : :
1821 : : /*
1822 : : * Verify the warning output against the real command-line scenario.
1823 : : * The run must report checksum-locked access-check failure and return WARNING
1824 : : */
1825 : 2 : filename = "templates/0030_012_2.txt";
1826 : :
1827 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1828 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1829 : :
1830 : : /*
1831 : : * Re-read the access-check-failed locked row after the warning run.
1832 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
1833 : : */
1834 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
1835 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
1836 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
1837 : 2 : ASSERT(offset_before == offset_after);
1838 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
1839 : :
1840 : : /*
1841 : : * Confirm that the full locked path set remains in the database.
1842 : : * --db-drop-inaccessible must not drop any checksum-locked row
1843 : : */
1844 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,locked_relative_path,&row_exists));
1845 : 2 : ASSERT(row_exists == true);
1846 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
1847 : 2 : ASSERT(row_exists == true);
1848 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
1849 : 2 : ASSERT(row_exists == true);
1850 : :
1851 : : /*
1852 : : * Remove the test database and restore the mutable fixture.
1853 : : * This returns the workspace to the state expected by the next test
1854 : : */
1855 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
1856 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
1857 : :
1858 : : /*
1859 : : * Release managed buffers allocated by this test before returning
1860 : : */
1861 : 2 : m_del(target_path);
1862 : 2 : m_del(pattern);
1863 : 2 : m_del(result);
1864 : :
1865 : 2 : RETURN_STATUS;
1866 : : }
1867 : :
1868 : : /**
1869 : : * @brief Verify that --db-drop-ignored does not drop a deleted checksum-locked file
1870 : : *
1871 : : * Covers README Example 9
1872 : : *
1873 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
1874 : : * Then one locked file is deleted and the update run ignores the whole protected
1875 : : * subtree with `--db-drop-ignored`. The program must warn about the missing
1876 : : * locked file and keep the protected database row unchanged
1877 : : */
1878 : 2 : static Return test0030_13(void)
1879 : : {
1880 : 2 : INITTEST;
1881 : :
1882 : : /*
1883 : : * Allocate managed buffers for captured application output, the expected
1884 : : * regular-expression template, and the resolved deleted file path
1885 : : */
1886 : 2 : m_create(char,result,MEMORY_STRING);
1887 : 2 : m_create(char,pattern,MEMORY_STRING);
1888 : 2 : m_create(char,target_path,MEMORY_STRING);
1889 : :
1890 : : /*
1891 : : * Name the database, the deleted locked row, and the matching fixture file.
1892 : : * The other locked rows prove that the whole lock set survives
1893 : : */
1894 : 2 : const char *db_filename = "lock_s13.db";
1895 : 2 : const char *deleted_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
1896 : 2 : const char *deleted_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
1897 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
1898 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
1899 : :
1900 : : /*
1901 : : * Keep before-and-after snapshots of the deleted locked row.
1902 : : * They prove that a warning result did not rewrite protected data
1903 : : */
1904 : 2 : CmpctStat db_stat_before = {0};
1905 : 2 : CmpctStat db_stat_after = {0};
1906 : 2 : sqlite3_int64 offset_before = 0;
1907 : 2 : sqlite3_int64 offset_after = 0;
1908 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
1909 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
1910 : 2 : bool row_exists = false;
1911 : :
1912 : : /*
1913 : : * Enable deterministic TESTING output so the captured application log
1914 : : * can be compared against a template
1915 : : */
1916 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
1917 : :
1918 : : /*
1919 : : * Work on a mutable copy of the fixture tree.
1920 : : * The original fixture is restored during cleanup
1921 : : */
1922 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
1923 : :
1924 : : /*
1925 : : * Create the baseline database.
1926 : : * The lock pattern protects every relative path under the path1/ subtree
1927 : : */
1928 : 2 : const char *arguments = "--database=lock_s13.db --progress "
1929 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
1930 : :
1931 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
1932 : :
1933 : : /*
1934 : : * Verify the baseline run output.
1935 : : * It must show a new database with path1/ files recorded as checksum-locked
1936 : : */
1937 : 2 : const char *filename = "templates/0030_013_1.txt";
1938 : :
1939 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1940 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1941 : :
1942 : : /*
1943 : : * Read the locked row before deleting its file.
1944 : : * The same values must still be present after the warning run
1945 : : */
1946 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,deleted_relative_path,&db_stat_before));
1947 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,deleted_relative_path,&offset_before,sha512_before));
1948 : :
1949 : : /*
1950 : : * Confirm the fixture file exists, then delete it from disk.
1951 : : * The later update must treat this as a checksum-lock violation, not ignored cleanup
1952 : : */
1953 : 2 : ASSERT(SUCCESS == construct_path(deleted_file_path,target_path));
1954 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,deleted_relative_path,R_OK,FILE_ACCESS_ALLOWED));
1955 : 2 : ASSERT(SUCCESS == delete_path(deleted_file_path));
1956 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,deleted_relative_path,R_OK,FILE_NOT_FOUND));
1957 : :
1958 : : /*
1959 : : * Run an update that ignores the whole protected subtree and allows ignored DB cleanup.
1960 : : * The checksum lock must override --db-drop-ignored for the missing protected file
1961 : : */
1962 : 2 : arguments = "--update --check-level=QUICK --lock-checksum=\"^path1/.*\" "
1963 : : "--ignore=\"^path1/.*\" --db-drop-ignored "
1964 : : "--database=lock_s13.db tests/fixtures/diffs/diff1";
1965 : :
1966 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
1967 : :
1968 : : /*
1969 : : * Verify the warning output against the real command-line scenario.
1970 : : * The deleted locked file must be reported as disappeared and kept in the DB
1971 : : */
1972 : 2 : filename = "templates/0030_013_2.txt";
1973 : :
1974 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
1975 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
1976 : :
1977 : : /*
1978 : : * Re-read the deleted locked row after the warning run.
1979 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
1980 : : */
1981 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,deleted_relative_path,&db_stat_after));
1982 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,deleted_relative_path,&offset_after,sha512_after));
1983 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
1984 : 2 : ASSERT(offset_before == offset_after);
1985 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
1986 : :
1987 : : /*
1988 : : * Confirm that the full locked path set remains in the database.
1989 : : * --db-drop-ignored must not drop any checksum-locked row
1990 : : */
1991 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,deleted_relative_path,&row_exists));
1992 : 2 : ASSERT(row_exists == true);
1993 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
1994 : 2 : ASSERT(row_exists == true);
1995 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
1996 : 2 : ASSERT(row_exists == true);
1997 : :
1998 : : /*
1999 : : * Remove the test database and restore the mutable fixture.
2000 : : * This returns the workspace to the state expected by the next test
2001 : : */
2002 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
2003 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
2004 : :
2005 : : /*
2006 : : * Release managed buffers allocated by this test before returning
2007 : : */
2008 : 2 : m_del(target_path);
2009 : 2 : m_del(pattern);
2010 : 2 : m_del(result);
2011 : :
2012 : 2 : RETURN_STATUS;
2013 : : }
2014 : :
2015 : : /**
2016 : : * @brief Verify that --rehash-locked still checks ignored checksum-locked files
2017 : : *
2018 : : * Covers README Examples 9, 10, and 11
2019 : : *
2020 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
2021 : : * Then one locked file is changed on disk and the update run ignores the whole
2022 : : * protected subtree. `--rehash-locked` must still rehash the locked files,
2023 : : * report the corrupted file, and keep the protected database row unchanged
2024 : : */
2025 : 2 : static Return test0030_14(void)
2026 : : {
2027 : 2 : INITTEST;
2028 : :
2029 : : /*
2030 : : * Allocate managed buffers for captured application output, the expected
2031 : : * regular-expression template, and the resolved corrupted file path
2032 : : */
2033 : 2 : m_create(char,result,MEMORY_STRING);
2034 : 2 : m_create(char,pattern,MEMORY_STRING);
2035 : 2 : m_create(char,target_path,MEMORY_STRING);
2036 : :
2037 : : /*
2038 : : * Name the database, the protected database key, and the fixture file
2039 : : * whose on-disk bytes will be changed while the path is also ignored
2040 : : */
2041 : 2 : const char *db_filename = "lock_s14.db";
2042 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
2043 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
2044 : :
2045 : : /*
2046 : : * Keep file and database snapshots around the content tampering.
2047 : : * They prove that ignored locked corruption is detected but not saved
2048 : : */
2049 : 2 : CmpctStat db_stat_before = {0};
2050 : 2 : CmpctStat db_stat_after = {0};
2051 : 2 : sqlite3_int64 offset_before = 0;
2052 : 2 : sqlite3_int64 offset_after = 0;
2053 : 2 : unsigned char db_sha512_before[SHA512_DIGEST_LENGTH] = {0};
2054 : 2 : unsigned char db_sha512_after[SHA512_DIGEST_LENGTH] = {0};
2055 : 2 : unsigned char file_sha512_before[SHA512_DIGEST_LENGTH] = {0};
2056 : 2 : unsigned char file_sha512_after_tamper[SHA512_DIGEST_LENGTH] = {0};
2057 : :
2058 : : /*
2059 : : * Enable deterministic TESTING output so the captured application log
2060 : : * can be compared against a template
2061 : : */
2062 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
2063 : :
2064 : : /*
2065 : : * Work on a mutable copy of the fixture tree.
2066 : : * The original fixture is restored during cleanup
2067 : : */
2068 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
2069 : :
2070 : : /*
2071 : : * Create the baseline database.
2072 : : * The lock pattern protects every relative path under the path1/ subtree
2073 : : */
2074 : 2 : const char *arguments = "--database=lock_s14.db --progress "
2075 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
2076 : :
2077 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
2078 : :
2079 : : /*
2080 : : * Verify the baseline run output.
2081 : : * It must show a new database with path1/ files recorded as checksum-locked
2082 : : */
2083 : 2 : const char *filename = "templates/0030_014_1.txt";
2084 : :
2085 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2086 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2087 : :
2088 : : /*
2089 : : * Resolve the target fixture path and read its matching locked DB row.
2090 : : * Before tampering, the stored checksum must match the real file checksum
2091 : : */
2092 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
2093 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
2094 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,db_sha512_before));
2095 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_before));
2096 : 2 : ASSERT(0 == memcmp(db_sha512_before,file_sha512_before,(size_t)SHA512_DIGEST_LENGTH));
2097 : :
2098 : : /*
2099 : : * Change bytes inside one checksum-locked file on disk.
2100 : : * The helper preserves logical size, so the mismatch is about content integrity
2101 : : */
2102 : 2 : ASSERT(SUCCESS == tamper_locked_file_bytes(locked_file_path));
2103 : :
2104 : : /*
2105 : : * Confirm that the test setup produced real content corruption.
2106 : : * The current file checksum must no longer match the protected DB checksum
2107 : : */
2108 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_after_tamper));
2109 : 2 : ASSERT(0 != memcmp(file_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
2110 : 2 : ASSERT(0 != memcmp(db_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
2111 : :
2112 : : /*
2113 : : * Run an update that ignores the protected subtree but rehashes locked files.
2114 : : * The ignored corrupted file must still be reported as a checksum violation
2115 : : */
2116 : 2 : arguments = "--update --check-level=QUICK --rehash-locked --lock-checksum=\"^path1/.*\" "
2117 : : "--ignore=\"^path1/.*\" --database=lock_s14.db "
2118 : : "tests/fixtures/diffs/diff1";
2119 : :
2120 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
2121 : :
2122 : : /*
2123 : : * Verify the warning output against the real command-line scenario.
2124 : : * Ignored directories are reported, but locked files are still rehashed
2125 : : */
2126 : 2 : filename = "templates/0030_014_2.txt";
2127 : :
2128 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2129 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2130 : :
2131 : : /*
2132 : : * Re-read the same protected row after the warning run.
2133 : : * The row must keep its original metadata, offset, and SHA512 checksum
2134 : : */
2135 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
2136 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,db_sha512_after));
2137 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
2138 : 2 : ASSERT(offset_before == offset_after);
2139 : 2 : ASSERT(0 == memcmp(db_sha512_before,db_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
2140 : 2 : ASSERT(0 != memcmp(db_sha512_after,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
2141 : :
2142 : : /*
2143 : : * Remove the test database and restore the mutable fixture.
2144 : : * This returns the workspace to the state expected by the next test
2145 : : */
2146 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
2147 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
2148 : :
2149 : : /*
2150 : : * Release managed buffers allocated by this test before returning
2151 : : */
2152 : 2 : m_del(target_path);
2153 : 2 : m_del(pattern);
2154 : 2 : m_del(result);
2155 : :
2156 : 2 : RETURN_STATUS;
2157 : : }
2158 : :
2159 : : /**
2160 : : * @brief Verify that --include does not weaken checksum-lock cleanup protection
2161 : : *
2162 : : * Covers README Example 9
2163 : : *
2164 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
2165 : : * Then one locked file is deleted while the update run ignores the whole
2166 : : * protected subtree and restores only a different locked file through `--include`.
2167 : : * `--db-drop-ignored` must not remove the missing locked row: the program must
2168 : : * report a warning and keep the protected database row unchanged
2169 : : */
2170 : 2 : static Return test0030_15(void)
2171 : : {
2172 : 2 : INITTEST;
2173 : :
2174 : : /*
2175 : : * Allocate managed buffers for captured application output, the expected
2176 : : * regular-expression template, and the resolved deleted file path
2177 : : */
2178 : 2 : m_create(char,result,MEMORY_STRING);
2179 : 2 : m_create(char,pattern,MEMORY_STRING);
2180 : 2 : m_create(char,target_path,MEMORY_STRING);
2181 : :
2182 : : /*
2183 : : * Name the database, the deleted protected row, and the other locked rows
2184 : : * that must stay in the database after ignored cleanup
2185 : : */
2186 : 2 : const char *db_filename = "lock_s15.db";
2187 : 2 : const char *deleted_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
2188 : 2 : const char *deleted_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
2189 : 2 : const char *other_locked_relative_path = "path1/AAA/ZAW/D/e/f/b_file.txt";
2190 : 2 : const char *included_locked_relative_path = "path1/AAA/BCB/CCC/a.txt";
2191 : :
2192 : : /*
2193 : : * Keep a before/after snapshot of the deleted locked row.
2194 : : * These values prove that warning cleanup did not rewrite protected data
2195 : : */
2196 : 2 : CmpctStat db_stat_before = {0};
2197 : 2 : CmpctStat db_stat_after = {0};
2198 : 2 : sqlite3_int64 offset_before = 0;
2199 : 2 : sqlite3_int64 offset_after = 0;
2200 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
2201 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
2202 : 2 : bool row_exists = false;
2203 : :
2204 : : /*
2205 : : * Enable deterministic TESTING output so the captured application log
2206 : : * can be compared against a template
2207 : : */
2208 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
2209 : :
2210 : : /*
2211 : : * Work on a mutable copy of the fixture tree.
2212 : : * The original fixture is restored during cleanup
2213 : : */
2214 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
2215 : :
2216 : : /*
2217 : : * Create the baseline database.
2218 : : * The lock pattern protects every relative path under the path1/ subtree
2219 : : */
2220 : 2 : const char *arguments = "--database=lock_s15.db --progress "
2221 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
2222 : :
2223 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
2224 : :
2225 : : /*
2226 : : * Verify the baseline run output.
2227 : : * It must show a new database with path1/ files recorded as checksum-locked
2228 : : */
2229 : 2 : const char *filename = "templates/0030_015_1.txt";
2230 : :
2231 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2232 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2233 : :
2234 : : /*
2235 : : * Read the locked row before deleting its file.
2236 : : * The same values must still be present after the warning run
2237 : : */
2238 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,deleted_relative_path,&db_stat_before));
2239 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,deleted_relative_path,&offset_before,sha512_before));
2240 : :
2241 : : /*
2242 : : * Confirm the fixture file exists, then delete it from disk.
2243 : : * The later update must treat this as a checksum-lock violation, not ignored cleanup
2244 : : */
2245 : 2 : ASSERT(SUCCESS == construct_path(deleted_file_path,target_path));
2246 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,deleted_relative_path,R_OK,FILE_ACCESS_ALLOWED));
2247 : 2 : ASSERT(SUCCESS == delete_path(deleted_file_path));
2248 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,deleted_relative_path,R_OK,FILE_NOT_FOUND));
2249 : :
2250 : : /*
2251 : : * Run an update that ignores the protected subtree, restores one different
2252 : : * locked file through --include, and allows ignored DB cleanup.
2253 : : * The deleted locked row is outside the restored include subset, but the
2254 : : * checksum lock must still override --db-drop-ignored
2255 : : */
2256 : 2 : arguments = "--update --check-level=QUICK --lock-checksum=\"^path1/.*\" "
2257 : : "--ignore=\"^path1/.*\" --include=\"^path1/AAA/BCB/CCC/a\\.txt$\" "
2258 : : "--db-drop-ignored --database=lock_s15.db tests/fixtures/diffs/diff1";
2259 : :
2260 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
2261 : :
2262 : : /*
2263 : : * Verify the warning output against the real command-line scenario.
2264 : : * The deleted locked file must be reported as disappeared and kept in the DB
2265 : : */
2266 : 2 : filename = "templates/0030_015_2.txt";
2267 : :
2268 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2269 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2270 : :
2271 : : /*
2272 : : * Re-read the deleted locked row after the warning run.
2273 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
2274 : : */
2275 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,deleted_relative_path,&db_stat_after));
2276 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,deleted_relative_path,&offset_after,sha512_after));
2277 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
2278 : 2 : ASSERT(offset_before == offset_after);
2279 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
2280 : :
2281 : : /*
2282 : : * Confirm that the deleted locked row, another ignored locked row, and the
2283 : : * included locked row all remain in the database
2284 : : */
2285 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,deleted_relative_path,&row_exists));
2286 : 2 : ASSERT(row_exists == true);
2287 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path,&row_exists));
2288 : 2 : ASSERT(row_exists == true);
2289 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,included_locked_relative_path,&row_exists));
2290 : 2 : ASSERT(row_exists == true);
2291 : :
2292 : : /*
2293 : : * Remove the test database and restore the mutable fixture.
2294 : : * This returns the workspace to the state expected by the next test
2295 : : */
2296 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
2297 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
2298 : :
2299 : : /*
2300 : : * Release managed buffers allocated by this test before returning
2301 : : */
2302 : 2 : m_del(target_path);
2303 : 2 : m_del(pattern);
2304 : 2 : m_del(result);
2305 : :
2306 : 2 : RETURN_STATUS;
2307 : : }
2308 : :
2309 : : /**
2310 : : * @brief Verify that --rehash-locked checks locked files outside the restored --include subset
2311 : : *
2312 : : * Covers README Examples 9 and 10
2313 : : *
2314 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
2315 : : * Then one locked file is changed on disk while the update run ignores the whole
2316 : : * protected subtree and restores only a different locked file through `--include`.
2317 : : * `--rehash-locked` must still rehash the corrupted locked file, report a warning,
2318 : : * and keep the protected database row unchanged
2319 : : */
2320 : 2 : static Return test0030_16(void)
2321 : : {
2322 : 2 : INITTEST;
2323 : :
2324 : : /*
2325 : : * Allocate managed buffers for captured application output, the expected
2326 : : * regular-expression template, and the resolved corrupted file path
2327 : : */
2328 : 2 : m_create(char,result,MEMORY_STRING);
2329 : 2 : m_create(char,pattern,MEMORY_STRING);
2330 : 2 : m_create(char,target_path,MEMORY_STRING);
2331 : :
2332 : : /*
2333 : : * Name the database, the protected database key, and the fixture file
2334 : : * whose on-disk bytes will be changed outside the restored include subset
2335 : : */
2336 : 2 : const char *db_filename = "lock_s16.db";
2337 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
2338 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
2339 : :
2340 : : /*
2341 : : * Keep file and database snapshots around the content tampering.
2342 : : * They prove that ignored locked corruption is detected but not saved
2343 : : */
2344 : 2 : CmpctStat db_stat_before = {0};
2345 : 2 : CmpctStat db_stat_after = {0};
2346 : 2 : struct stat file_stat_before = {0};
2347 : 2 : struct stat file_stat_after_tamper = {0};
2348 : 2 : sqlite3_int64 offset_before = 0;
2349 : 2 : sqlite3_int64 offset_after = 0;
2350 : 2 : unsigned char db_sha512_before[SHA512_DIGEST_LENGTH] = {0};
2351 : 2 : unsigned char db_sha512_after[SHA512_DIGEST_LENGTH] = {0};
2352 : 2 : unsigned char file_sha512_before[SHA512_DIGEST_LENGTH] = {0};
2353 : 2 : unsigned char file_sha512_after_tamper[SHA512_DIGEST_LENGTH] = {0};
2354 : :
2355 : : /*
2356 : : * Enable deterministic TESTING output so the captured application log
2357 : : * can be compared against a template
2358 : : */
2359 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
2360 : :
2361 : : /*
2362 : : * Work on a mutable copy of the fixture tree.
2363 : : * The original fixture is restored during cleanup
2364 : : */
2365 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
2366 : :
2367 : : /*
2368 : : * Create the baseline database.
2369 : : * The lock pattern protects every relative path under the path1/ subtree
2370 : : */
2371 : 2 : const char *arguments = "--database=lock_s16.db --progress "
2372 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
2373 : :
2374 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
2375 : :
2376 : : /*
2377 : : * Verify the baseline run output.
2378 : : * It must show a new database with path1/ files recorded as checksum-locked
2379 : : */
2380 : 2 : const char *filename = "templates/0030_016_1.txt";
2381 : :
2382 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2383 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2384 : :
2385 : : /*
2386 : : * Resolve the target fixture path and read its matching locked DB row.
2387 : : * Before tampering, the stored checksum must match the real file checksum
2388 : : */
2389 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
2390 : 2 : ASSERT(0 == stat(m_text(target_path),&file_stat_before));
2391 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
2392 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,db_sha512_before));
2393 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_before));
2394 : 2 : ASSERT(0 == memcmp(db_sha512_before,file_sha512_before,(size_t)SHA512_DIGEST_LENGTH));
2395 : :
2396 : : /*
2397 : : * Change bytes inside one checksum-locked file on disk.
2398 : : * The helper preserves logical size, so the mismatch is about content integrity
2399 : : */
2400 : 2 : ASSERT(SUCCESS == tamper_locked_file_bytes(locked_file_path));
2401 : :
2402 : : /*
2403 : : * Confirm that the test setup produced real content corruption.
2404 : : * The file size must stay the same, while the checksum must differ
2405 : : */
2406 : 2 : ASSERT(0 == stat(m_text(target_path),&file_stat_after_tamper));
2407 : 2 : ASSERT(file_stat_before.st_size == file_stat_after_tamper.st_size);
2408 : 2 : ASSERT(SUCCESS == compute_file_sha512_monocypher(m_text(target_path),file_sha512_after_tamper));
2409 : 2 : ASSERT(0 != memcmp(file_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
2410 : 2 : ASSERT(0 != memcmp(db_sha512_before,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
2411 : :
2412 : : /*
2413 : : * Run an update that ignores the protected subtree, restores one different
2414 : : * locked file through --include, and rehashes locked files.
2415 : : * The corrupted locked row is outside the restored include subset, but
2416 : : * --rehash-locked must still check it and report a checksum violation
2417 : : */
2418 : 2 : arguments = "--update --check-level=QUICK --rehash-locked --lock-checksum=\"^path1/.*\" "
2419 : : "--ignore=\"^path1/.*\" --include=\"^path1/AAA/BCB/CCC/a\\.txt$\" "
2420 : : "--database=lock_s16.db tests/fixtures/diffs/diff1";
2421 : :
2422 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
2423 : :
2424 : : /*
2425 : : * Verify the warning output against the real command-line scenario.
2426 : : * Ignored directories are reported, but locked files are still rehashed
2427 : : */
2428 : 2 : filename = "templates/0030_016_2.txt";
2429 : :
2430 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2431 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2432 : :
2433 : : /*
2434 : : * Re-read the same protected row after the warning run.
2435 : : * The row must keep its original metadata, offset, and SHA512 checksum
2436 : : */
2437 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
2438 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,db_sha512_after));
2439 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
2440 : 2 : ASSERT(offset_before == offset_after);
2441 : 2 : ASSERT(0 == memcmp(db_sha512_before,db_sha512_after,(size_t)SHA512_DIGEST_LENGTH));
2442 : 2 : ASSERT(0 != memcmp(db_sha512_after,file_sha512_after_tamper,(size_t)SHA512_DIGEST_LENGTH));
2443 : :
2444 : : /*
2445 : : * Remove the test database and restore the mutable fixture.
2446 : : * This returns the workspace to the state expected by the next test
2447 : : */
2448 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
2449 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
2450 : :
2451 : : /*
2452 : : * Release managed buffers allocated by this test before returning
2453 : : */
2454 : 2 : m_del(target_path);
2455 : 2 : m_del(pattern);
2456 : 2 : m_del(result);
2457 : :
2458 : 2 : RETURN_STATUS;
2459 : : }
2460 : :
2461 : : /**
2462 : : * @brief Verify that --ignore does not hide access denial for a checksum-locked file
2463 : : *
2464 : : * Covers README Examples 9 and 11
2465 : : *
2466 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
2467 : : * Then a test hook makes one locked file look access-denied during an update run
2468 : : * that also ignores the protected subtree and enables `--db-drop-inaccessible`.
2469 : : * The program must warn about the ignored locked file and keep the protected
2470 : : * database row unchanged
2471 : : */
2472 : 2 : static Return test0030_17(void)
2473 : : {
2474 : 2 : INITTEST;
2475 : :
2476 : : /*
2477 : : * Allocate managed buffers for captured application output, the expected
2478 : : * regular-expression template, and the resolved locked file path
2479 : : */
2480 : 2 : m_create(char,result,MEMORY_STRING);
2481 : 2 : m_create(char,pattern,MEMORY_STRING);
2482 : 2 : m_create(char,target_path,MEMORY_STRING);
2483 : :
2484 : : /*
2485 : : * Name the database, the access-denied locked row, and the matching fixture file.
2486 : : * The other locked rows prove that the whole lock set survives ignored cleanup
2487 : : */
2488 : 2 : const char *db_filename = "lock_s17.db";
2489 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
2490 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
2491 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
2492 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
2493 : :
2494 : : /*
2495 : : * Keep before-and-after snapshots of the access-denied locked row.
2496 : : * They prove that a warning result did not rewrite protected data
2497 : : */
2498 : 2 : CmpctStat db_stat_before = {0};
2499 : 2 : CmpctStat db_stat_after = {0};
2500 : 2 : sqlite3_int64 offset_before = 0;
2501 : 2 : sqlite3_int64 offset_after = 0;
2502 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
2503 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
2504 : 2 : bool row_exists = false;
2505 : :
2506 : : /*
2507 : : * Enable deterministic TESTING output so the captured application log
2508 : : * can be compared against a template
2509 : : */
2510 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
2511 : :
2512 : : /*
2513 : : * Work on a mutable copy of the fixture tree.
2514 : : * The original fixture is restored during cleanup
2515 : : */
2516 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
2517 : :
2518 : : /*
2519 : : * Create the baseline database.
2520 : : * The lock pattern protects every relative path under the path1/ subtree
2521 : : */
2522 : 2 : const char *arguments = "--database=lock_s17.db --progress "
2523 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
2524 : :
2525 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
2526 : :
2527 : : /*
2528 : : * Verify the baseline run output.
2529 : : * It must show a new database with path1/ files recorded as checksum-locked
2530 : : */
2531 : 2 : const char *filename = "templates/0030_017_1.txt";
2532 : :
2533 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2534 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2535 : :
2536 : : /*
2537 : : * Read the locked row before making access checks fail.
2538 : : * The same values must still be present after the warning run
2539 : : */
2540 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
2541 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
2542 : :
2543 : : /*
2544 : : * Confirm the fixture file itself is present and readable.
2545 : : * This test covers access denial for an existing ignored locked file
2546 : : */
2547 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
2548 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ALLOWED));
2549 : :
2550 : : /*
2551 : : * Force access checks for exactly this locked file to return access denied.
2552 : : * This models a visible protected file that cannot be read during traversal
2553 : : */
2554 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",locked_relative_path));
2555 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS","FILE_ACCESS_DENIED"));
2556 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_DENIED));
2557 : :
2558 : : /*
2559 : : * Run an update that ignores the protected subtree and would normally drop
2560 : : * inaccessible records. The checksum lock must override both conditions
2561 : : */
2562 : 2 : arguments = "--update --check-level=QUICK --db-drop-inaccessible --lock-checksum=\"^path1/.*\" "
2563 : : "--ignore=\"^path1/.*\" --database=lock_s17.db tests/fixtures/diffs/diff1";
2564 : :
2565 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
2566 : :
2567 : : /*
2568 : : * Disable the access-check hook before inspecting output and database state.
2569 : : * Later assertions should observe the real filesystem again
2570 : : */
2571 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",""));
2572 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS",""));
2573 : :
2574 : : /*
2575 : : * Verify the warning output against the real command-line scenario.
2576 : : * The ignored locked file must still report checksum-locked access denial
2577 : : */
2578 : 2 : filename = "templates/0030_017_2.txt";
2579 : :
2580 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2581 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2582 : :
2583 : : /*
2584 : : * Re-read the access-denied locked row after the warning run.
2585 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
2586 : : */
2587 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
2588 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
2589 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
2590 : 2 : ASSERT(offset_before == offset_after);
2591 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
2592 : :
2593 : : /*
2594 : : * Confirm that the full locked path set remains in the database.
2595 : : * --ignore and --db-drop-inaccessible must not drop any checksum-locked row
2596 : : */
2597 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,locked_relative_path,&row_exists));
2598 : 2 : ASSERT(row_exists == true);
2599 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
2600 : 2 : ASSERT(row_exists == true);
2601 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
2602 : 2 : ASSERT(row_exists == true);
2603 : :
2604 : : /*
2605 : : * Remove the test database and restore the mutable fixture.
2606 : : * This returns the workspace to the state expected by the next test
2607 : : */
2608 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
2609 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
2610 : :
2611 : : /*
2612 : : * Release managed buffers allocated by this test before returning
2613 : : */
2614 : 2 : m_del(target_path);
2615 : 2 : m_del(pattern);
2616 : 2 : m_del(result);
2617 : :
2618 : 2 : RETURN_STATUS;
2619 : : }
2620 : :
2621 : : /**
2622 : : * @brief Verify that --ignore does not hide access-check failure for a checksum-locked file
2623 : : *
2624 : : * Covers README Examples 9 and 11
2625 : : *
2626 : : * Creates a baseline database with the `path1/` subtree protected by `--lock-checksum`.
2627 : : * Then a test hook makes one locked file return an access-check failure during
2628 : : * an update run that also ignores the protected subtree and enables
2629 : : * `--db-drop-inaccessible`. The program must warn about the ignored locked file
2630 : : * and keep the protected database row unchanged
2631 : : */
2632 : 2 : static Return test0030_18(void)
2633 : : {
2634 : 2 : INITTEST;
2635 : :
2636 : : /*
2637 : : * Allocate managed buffers for captured application output, the expected
2638 : : * regular-expression template, and the resolved locked file path
2639 : : */
2640 : 2 : m_create(char,result,MEMORY_STRING);
2641 : 2 : m_create(char,pattern,MEMORY_STRING);
2642 : 2 : m_create(char,target_path,MEMORY_STRING);
2643 : :
2644 : : /*
2645 : : * Name the database, the locked row that will fail access checking, and the
2646 : : * matching fixture file. The other locked rows prove that the whole lock set survives ignored cleanup
2647 : : */
2648 : 2 : const char *db_filename = "lock_s18.db";
2649 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
2650 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
2651 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
2652 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
2653 : :
2654 : : /*
2655 : : * Keep before-and-after snapshots of the locked row that will fail access checking.
2656 : : * They prove that a warning result did not rewrite protected data
2657 : : */
2658 : 2 : CmpctStat db_stat_before = {0};
2659 : 2 : CmpctStat db_stat_after = {0};
2660 : 2 : sqlite3_int64 offset_before = 0;
2661 : 2 : sqlite3_int64 offset_after = 0;
2662 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
2663 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
2664 : 2 : bool row_exists = false;
2665 : :
2666 : : /*
2667 : : * Enable deterministic TESTING output so the captured application log
2668 : : * can be compared against a template
2669 : : */
2670 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
2671 : :
2672 : : /*
2673 : : * Work on a mutable copy of the fixture tree.
2674 : : * The original fixture is restored during cleanup
2675 : : */
2676 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
2677 : :
2678 : : /*
2679 : : * Create the baseline database.
2680 : : * The lock pattern protects every relative path under the path1/ subtree
2681 : : */
2682 : 2 : const char *arguments = "--database=lock_s18.db --progress "
2683 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
2684 : :
2685 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
2686 : :
2687 : : /*
2688 : : * Verify the baseline run output.
2689 : : * It must show a new database with path1/ files recorded as checksum-locked
2690 : : */
2691 : 2 : const char *filename = "templates/0030_018_1.txt";
2692 : :
2693 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2694 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2695 : :
2696 : : /*
2697 : : * Read the locked row before making access checks fail.
2698 : : * The same values must still be present after the warning run
2699 : : */
2700 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
2701 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
2702 : :
2703 : : /*
2704 : : * Confirm the fixture file itself is present and readable.
2705 : : * This test covers an access-check failure for an existing ignored locked file
2706 : : */
2707 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
2708 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ALLOWED));
2709 : :
2710 : : /*
2711 : : * Force access checks for exactly this locked file to return an access-check failure.
2712 : : * This models an unexpected access-check problem during ignored locked traversal
2713 : : */
2714 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",locked_relative_path));
2715 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS","FILE_ACCESS_ERROR"));
2716 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ERROR));
2717 : :
2718 : : /*
2719 : : * Run an update that ignores the protected subtree and would normally drop
2720 : : * inaccessible records. The checksum lock must override both conditions
2721 : : */
2722 : 2 : arguments = "--update --check-level=QUICK --db-drop-inaccessible --lock-checksum=\"^path1/.*\" "
2723 : : "--ignore=\"^path1/.*\" --database=lock_s18.db tests/fixtures/diffs/diff1";
2724 : :
2725 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
2726 : :
2727 : : /*
2728 : : * Disable the access-check hook before inspecting output and database state.
2729 : : * Later assertions should observe the real filesystem again
2730 : : */
2731 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",""));
2732 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS",""));
2733 : :
2734 : : /*
2735 : : * Verify the warning output against the real command-line scenario.
2736 : : * The ignored locked file must still report checksum-locked access-check failure
2737 : : */
2738 : 2 : filename = "templates/0030_018_2.txt";
2739 : :
2740 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2741 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2742 : :
2743 : : /*
2744 : : * Re-read the access-check-failed locked row after the warning run.
2745 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
2746 : : */
2747 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
2748 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
2749 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
2750 : 2 : ASSERT(offset_before == offset_after);
2751 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
2752 : :
2753 : : /*
2754 : : * Confirm that the full locked path set remains in the database.
2755 : : * --ignore and --db-drop-inaccessible must not drop any checksum-locked row
2756 : : */
2757 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,locked_relative_path,&row_exists));
2758 : 2 : ASSERT(row_exists == true);
2759 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
2760 : 2 : ASSERT(row_exists == true);
2761 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
2762 : 2 : ASSERT(row_exists == true);
2763 : :
2764 : : /*
2765 : : * Remove the test database and restore the mutable fixture.
2766 : : * This returns the workspace to the state expected by the next test
2767 : : */
2768 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
2769 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
2770 : :
2771 : : /*
2772 : : * Release managed buffers allocated by this test before returning
2773 : : */
2774 : 2 : m_del(target_path);
2775 : 2 : m_del(pattern);
2776 : 2 : m_del(result);
2777 : :
2778 : 2 : RETURN_STATUS;
2779 : : }
2780 : :
2781 : : /**
2782 : : * @brief Verify that cleanup keeps an access-denied checksum-locked row
2783 : : *
2784 : : * Covers README Examples 9 and 11
2785 : : *
2786 : : * Creates a baseline database with `path1/` protected by `--lock-checksum`.
2787 : : * Then the test makes one locked file unreadable and skips `file_list`, so
2788 : : * `db_delete_missing_metadata` must handle the unavailable DB row directly.
2789 : : * Even with `--db-drop-inaccessible`, the checksum lock must report a warning
2790 : : * and keep the protected database row unchanged
2791 : : */
2792 : 2 : static Return test0030_19(void)
2793 : : {
2794 : 2 : INITTEST;
2795 : :
2796 : : /*
2797 : : * Allocate managed buffers for captured application output, the expected
2798 : : * regular-expression template, and the resolved access-denied file path
2799 : : */
2800 : 2 : m_create(char,result,MEMORY_STRING);
2801 : 2 : m_create(char,pattern,MEMORY_STRING);
2802 : 2 : m_create(char,target_path,MEMORY_STRING);
2803 : :
2804 : : /*
2805 : : * Name the database, the protected database key, and the fixture file
2806 : : * whose read access will be denied during cleanup-only processing
2807 : : */
2808 : 2 : const char *db_filename = "lock_s19.db";
2809 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
2810 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
2811 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
2812 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
2813 : :
2814 : : /*
2815 : : * Keep database snapshots around the warning run.
2816 : : * They prove that cleanup reports the access denial but does not rewrite the locked row
2817 : : */
2818 : 2 : CmpctStat db_stat_before = {0};
2819 : 2 : CmpctStat db_stat_after = {0};
2820 : 2 : sqlite3_int64 offset_before = 0;
2821 : 2 : sqlite3_int64 offset_after = 0;
2822 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
2823 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
2824 : 2 : bool row_exists = false;
2825 : :
2826 : : /*
2827 : : * Enable deterministic TESTING output so the captured application log
2828 : : * can be compared against a template
2829 : : */
2830 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
2831 : :
2832 : : /*
2833 : : * Work on a mutable copy of the fixture tree.
2834 : : * The original fixture is restored during cleanup
2835 : : */
2836 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
2837 : :
2838 : : /*
2839 : : * Create the baseline database.
2840 : : * The lock pattern protects every relative path under the path1/ subtree
2841 : : */
2842 : 2 : const char *arguments = "--database=lock_s19.db --progress "
2843 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
2844 : :
2845 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
2846 : :
2847 : : /*
2848 : : * Verify the baseline run output.
2849 : : * It must show a new database with path1/ files recorded as checksum-locked
2850 : : */
2851 : 2 : const char *filename = "templates/0030_019_1.txt";
2852 : :
2853 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2854 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2855 : :
2856 : : /*
2857 : : * Read the locked row before making its file unreadable.
2858 : : * The same values must still be present after cleanup reports the warning
2859 : : */
2860 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
2861 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
2862 : :
2863 : : /*
2864 : : * Confirm the fixture file exists and is readable, then deny read access.
2865 : : * This makes the later cleanup pass handle a real access-denied DB row
2866 : : */
2867 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
2868 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ALLOWED));
2869 : 2 : ASSERT(SUCCESS == change_mode(locked_file_path,0000));
2870 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_DENIED));
2871 : :
2872 : : /*
2873 : : * Skip both file_list passes.
2874 : : * This forces db_delete_missing_metadata to be the code path that checks the locked DB row
2875 : : */
2876 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_SKIP_FILE_LIST","1"));
2877 : :
2878 : : /*
2879 : : * Run an update that would normally drop inaccessible records.
2880 : : * The checksum lock must override that cleanup and turn the condition into a warning
2881 : : */
2882 : 2 : arguments = "--update --check-level=QUICK --db-drop-inaccessible --lock-checksum=\"^path1/.*\" "
2883 : : "--database=lock_s19.db tests/fixtures/diffs/diff1";
2884 : :
2885 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
2886 : :
2887 : : /*
2888 : : * Re-enable file_list and restore file permissions before inspecting state.
2889 : : * The captured output still reflects the access-denied cleanup run above
2890 : : */
2891 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_SKIP_FILE_LIST",""));
2892 : 2 : ASSERT(SUCCESS == change_mode(locked_file_path,0644));
2893 : :
2894 : : /*
2895 : : * Verify the warning output against the real command-line scenario.
2896 : : * With file_list bypassed, db_delete_missing_metadata must report access denied
2897 : : */
2898 : 2 : filename = "templates/0030_019_2.txt";
2899 : :
2900 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
2901 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
2902 : :
2903 : : /*
2904 : : * Re-read the access-denied locked row after the warning run.
2905 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
2906 : : */
2907 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
2908 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
2909 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
2910 : 2 : ASSERT(offset_before == offset_after);
2911 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
2912 : :
2913 : : /*
2914 : : * Confirm that the full locked path set remains in the database.
2915 : : * --db-drop-inaccessible must not drop any checksum-locked row during cleanup
2916 : : */
2917 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,locked_relative_path,&row_exists));
2918 : 2 : ASSERT(row_exists == true);
2919 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
2920 : 2 : ASSERT(row_exists == true);
2921 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
2922 : 2 : ASSERT(row_exists == true);
2923 : :
2924 : : /*
2925 : : * Remove the test database and restore the mutable fixture.
2926 : : * This returns the workspace to the state expected by the next test
2927 : : */
2928 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
2929 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
2930 : :
2931 : : /*
2932 : : * Release managed buffers allocated by this test before returning
2933 : : */
2934 : 2 : m_del(target_path);
2935 : 2 : m_del(pattern);
2936 : 2 : m_del(result);
2937 : :
2938 : 2 : RETURN_STATUS;
2939 : : }
2940 : :
2941 : : /**
2942 : : * @brief Verify that cleanup keeps a checksum-locked row after an access-check failure
2943 : : *
2944 : : * Covers README Examples 9 and 11
2945 : : *
2946 : : * Creates a baseline database with `path1/` protected by `--lock-checksum`.
2947 : : * Then the test makes access checking fail for one locked file and skips
2948 : : * `file_list`, so `db_delete_missing_metadata` must handle the unavailable DB
2949 : : * row directly. Even with `--db-drop-inaccessible`, the checksum lock must
2950 : : * report a warning and keep the protected database row unchanged
2951 : : */
2952 : 2 : static Return test0030_20(void)
2953 : : {
2954 : 2 : INITTEST;
2955 : :
2956 : : /*
2957 : : * Allocate managed buffers for captured application output, the expected
2958 : : * regular-expression template, and the resolved access-failed file path
2959 : : */
2960 : 2 : m_create(char,result,MEMORY_STRING);
2961 : 2 : m_create(char,pattern,MEMORY_STRING);
2962 : 2 : m_create(char,target_path,MEMORY_STRING);
2963 : :
2964 : : /*
2965 : : * Name the database, the protected database key, and the fixture file
2966 : : * whose access check will fail during cleanup-only processing
2967 : : */
2968 : 2 : const char *db_filename = "lock_s20.db";
2969 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
2970 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
2971 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
2972 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
2973 : :
2974 : : /*
2975 : : * Keep database snapshots around the warning run.
2976 : : * They prove that cleanup reports the access-check failure but does not rewrite the locked row
2977 : : */
2978 : 2 : CmpctStat db_stat_before = {0};
2979 : 2 : CmpctStat db_stat_after = {0};
2980 : 2 : sqlite3_int64 offset_before = 0;
2981 : 2 : sqlite3_int64 offset_after = 0;
2982 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
2983 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
2984 : 2 : bool row_exists = false;
2985 : :
2986 : : /*
2987 : : * Enable deterministic TESTING output so the captured application log
2988 : : * can be compared against a template
2989 : : */
2990 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
2991 : :
2992 : : /*
2993 : : * Work on a mutable copy of the fixture tree.
2994 : : * The original fixture is restored during cleanup
2995 : : */
2996 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
2997 : :
2998 : : /*
2999 : : * Create the baseline database.
3000 : : * The lock pattern protects every relative path under the path1/ subtree
3001 : : */
3002 : 2 : const char *arguments = "--database=lock_s20.db --progress "
3003 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
3004 : :
3005 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
3006 : :
3007 : : /*
3008 : : * Verify the baseline run output.
3009 : : * It must show a new database with path1/ files recorded as checksum-locked
3010 : : */
3011 : 2 : const char *filename = "templates/0030_020_1.txt";
3012 : :
3013 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
3014 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
3015 : :
3016 : : /*
3017 : : * Read the locked row before making its access check fail.
3018 : : * The same values must still be present after cleanup reports the warning
3019 : : */
3020 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
3021 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
3022 : :
3023 : : /*
3024 : : * Confirm the fixture file itself is present and readable.
3025 : : * This test covers an access-check failure for an existing DB row
3026 : : */
3027 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
3028 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ALLOWED));
3029 : :
3030 : : /*
3031 : : * Skip both file_list passes and force access checks for exactly this
3032 : : * locked file to return an access-check failure
3033 : : */
3034 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_SKIP_FILE_LIST","1"));
3035 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",locked_relative_path));
3036 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS","FILE_ACCESS_ERROR"));
3037 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ERROR));
3038 : :
3039 : : /*
3040 : : * Run an update that would normally drop inaccessible records.
3041 : : * The checksum lock must override that cleanup and turn the condition into a warning
3042 : : */
3043 : 2 : arguments = "--update --check-level=QUICK --db-drop-inaccessible --lock-checksum=\"^path1/.*\" "
3044 : : "--database=lock_s20.db tests/fixtures/diffs/diff1";
3045 : :
3046 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
3047 : :
3048 : : /*
3049 : : * Disable test hooks before inspecting output and database state.
3050 : : * Later assertions should observe the real filesystem again
3051 : : */
3052 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_SKIP_FILE_LIST",""));
3053 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",""));
3054 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS",""));
3055 : :
3056 : : /*
3057 : : * Verify the warning output against the real command-line scenario.
3058 : : * With file_list bypassed, db_delete_missing_metadata must report access-check failure
3059 : : */
3060 : 2 : filename = "templates/0030_020_2.txt";
3061 : :
3062 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
3063 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
3064 : :
3065 : : /*
3066 : : * Re-read the access-check-failed locked row after the warning run.
3067 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
3068 : : */
3069 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
3070 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
3071 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
3072 : 2 : ASSERT(offset_before == offset_after);
3073 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
3074 : :
3075 : : /*
3076 : : * Confirm that the full locked path set remains in the database.
3077 : : * --db-drop-inaccessible must not drop any checksum-locked row during cleanup
3078 : : */
3079 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,locked_relative_path,&row_exists));
3080 : 2 : ASSERT(row_exists == true);
3081 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
3082 : 2 : ASSERT(row_exists == true);
3083 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
3084 : 2 : ASSERT(row_exists == true);
3085 : :
3086 : : /*
3087 : : * Remove the test database and restore the mutable fixture.
3088 : : * This returns the workspace to the state expected by the next test
3089 : : */
3090 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
3091 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
3092 : :
3093 : : /*
3094 : : * Release managed buffers allocated by this test before returning
3095 : : */
3096 : 2 : m_del(target_path);
3097 : 2 : m_del(pattern);
3098 : 2 : m_del(result);
3099 : :
3100 : 2 : RETURN_STATUS;
3101 : : }
3102 : :
3103 : : /**
3104 : : * @brief Verify that cleanup keeps an ignored checksum-locked row after an access-check failure
3105 : : *
3106 : : * Covers README Examples 9 and 11
3107 : : *
3108 : : * Creates a baseline database with `path1/` protected by `--lock-checksum`.
3109 : : * Then the test makes access checking fail for one locked file and skips
3110 : : * `file_list`, so `db_delete_missing_metadata` must handle an ignored DB row
3111 : : * directly. Even with `--ignore` and `--db-drop-ignored`, the checksum lock
3112 : : * must report a warning and keep the protected database row unchanged
3113 : : */
3114 : 2 : static Return test0030_21(void)
3115 : : {
3116 : 2 : INITTEST;
3117 : :
3118 : : /*
3119 : : * Allocate managed buffers for captured application output, the expected
3120 : : * regular-expression template, and the resolved access-failed file path
3121 : : */
3122 : 2 : m_create(char,result,MEMORY_STRING);
3123 : 2 : m_create(char,pattern,MEMORY_STRING);
3124 : 2 : m_create(char,target_path,MEMORY_STRING);
3125 : :
3126 : : /*
3127 : : * Name the database, the protected database key, and the fixture file
3128 : : * whose access check will fail during ignored cleanup-only processing
3129 : : */
3130 : 2 : const char *db_filename = "lock_s21.db";
3131 : 2 : const char *locked_relative_path = "path1/AAA/ZAW/A/b/c/a_file.txt";
3132 : 2 : const char *locked_file_path = "tests/fixtures/diffs/diff1/path1/AAA/ZAW/A/b/c/a_file.txt";
3133 : 2 : const char *other_locked_relative_path_1 = "path1/AAA/ZAW/D/e/f/b_file.txt";
3134 : 2 : const char *other_locked_relative_path_2 = "path1/AAA/BCB/CCC/a.txt";
3135 : :
3136 : : /*
3137 : : * Keep database snapshots around the warning run.
3138 : : * They prove that ignored cleanup reports the access-check failure but does not rewrite the locked row
3139 : : */
3140 : 2 : CmpctStat db_stat_before = {0};
3141 : 2 : CmpctStat db_stat_after = {0};
3142 : 2 : sqlite3_int64 offset_before = 0;
3143 : 2 : sqlite3_int64 offset_after = 0;
3144 : 2 : unsigned char sha512_before[SHA512_DIGEST_LENGTH] = {0};
3145 : 2 : unsigned char sha512_after[SHA512_DIGEST_LENGTH] = {0};
3146 : 2 : bool row_exists = false;
3147 : :
3148 : : /*
3149 : : * Enable deterministic TESTING output so the captured application log
3150 : : * can be compared against a template
3151 : : */
3152 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTING","true"));
3153 : :
3154 : : /*
3155 : : * Work on a mutable copy of the fixture tree.
3156 : : * The original fixture is restored during cleanup
3157 : : */
3158 : 2 : ASSERT(SUCCESS == prepare_mutable_fixture("tests/fixtures/diffs/diff1"));
3159 : :
3160 : : /*
3161 : : * Create the baseline database.
3162 : : * The lock pattern protects every relative path under the path1/ subtree
3163 : : */
3164 : 2 : const char *arguments = "--database=lock_s21.db --progress "
3165 : : "--lock-checksum=\"^path1/.*\" tests/fixtures/diffs/diff1";
3166 : :
3167 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,COMPLETED,ALLOW_BOTH));
3168 : :
3169 : : /*
3170 : : * Verify the baseline run output.
3171 : : * It must show a new database with path1/ files recorded as checksum-locked
3172 : : */
3173 : 2 : const char *filename = "templates/0030_021_1.txt";
3174 : :
3175 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
3176 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
3177 : :
3178 : : /*
3179 : : * Read the locked row before making its access check fail.
3180 : : * The same values must still be present after ignored cleanup reports the warning
3181 : : */
3182 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_before));
3183 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_before,sha512_before));
3184 : :
3185 : : /*
3186 : : * Confirm the fixture file itself is present and readable.
3187 : : * This test covers an access-check failure for an existing ignored DB row
3188 : : */
3189 : 2 : ASSERT(SUCCESS == construct_path(locked_file_path,target_path));
3190 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ALLOWED));
3191 : :
3192 : : /*
3193 : : * Skip both file_list passes and force access checks for exactly this
3194 : : * locked file to return an access-check failure
3195 : : */
3196 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_SKIP_FILE_LIST","1"));
3197 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",locked_relative_path));
3198 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS","FILE_ACCESS_ERROR"));
3199 : 2 : ASSERT(SUCCESS == expect_file_access_from_root(fixture_root,locked_relative_path,R_OK,FILE_ACCESS_ERROR));
3200 : :
3201 : : /*
3202 : : * Run an update that would normally drop ignored records.
3203 : : * The checksum lock must override that cleanup and turn the condition into a warning
3204 : : */
3205 : 2 : arguments = "--update --check-level=QUICK --lock-checksum=\"^path1/.*\" "
3206 : : "--ignore=\"^path1/.*\" --db-drop-ignored "
3207 : : "--database=lock_s21.db tests/fixtures/diffs/diff1";
3208 : :
3209 : 2 : ASSERT(SUCCESS == runit(arguments,result,NULL,WARNING,ALLOW_BOTH));
3210 : :
3211 : : /*
3212 : : * Disable test hooks before inspecting output and database state.
3213 : : * Later assertions should observe the real filesystem again
3214 : : */
3215 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_SKIP_FILE_LIST",""));
3216 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_SUFFIX",""));
3217 : 2 : ASSERT(SUCCESS == set_environment_variable("TESTITALL_TEST_ENV_FILE_ACCESS_STATUS",""));
3218 : :
3219 : : /*
3220 : : * Verify the warning output against the real command-line scenario.
3221 : : * With file_list bypassed, ignored cleanup must report access-check failure
3222 : : */
3223 : 2 : filename = "templates/0030_021_2.txt";
3224 : :
3225 : 2 : ASSERT(SUCCESS == get_file_content(filename,pattern));
3226 : 2 : ASSERT(SUCCESS == match_pattern(result,pattern,filename));
3227 : :
3228 : : /*
3229 : : * Re-read the ignored access-check-failed locked row after the warning run.
3230 : : * Its metadata, SHA512 offset, and SHA512 digest must stay unchanged
3231 : : */
3232 : 2 : ASSERT(SUCCESS == db_read_cmpctstat_by_relative_path(db_filename,locked_relative_path,&db_stat_after));
3233 : 2 : ASSERT(SUCCESS == read_final_sha512_from_db(db_filename,locked_relative_path,&offset_after,sha512_after));
3234 : 2 : ASSERT(0 == memcmp(&db_stat_before,&db_stat_after,sizeof(CmpctStat)));
3235 : 2 : ASSERT(offset_before == offset_after);
3236 : 2 : ASSERT(0 == memcmp(sha512_before,sha512_after,(size_t)SHA512_DIGEST_LENGTH));
3237 : :
3238 : : /*
3239 : : * Confirm that the full locked path set remains in the database.
3240 : : * --db-drop-ignored must not drop any checksum-locked row during cleanup
3241 : : */
3242 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,locked_relative_path,&row_exists));
3243 : 2 : ASSERT(row_exists == true);
3244 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_1,&row_exists));
3245 : 2 : ASSERT(row_exists == true);
3246 : 2 : ASSERT(SUCCESS == db_relative_path_exists(db_filename,other_locked_relative_path_2,&row_exists));
3247 : 2 : ASSERT(row_exists == true);
3248 : :
3249 : : /*
3250 : : * Remove the test database and restore the mutable fixture.
3251 : : * This returns the workspace to the state expected by the next test
3252 : : */
3253 : 2 : ASSERT(SUCCESS == delete_path(db_filename));
3254 : 2 : ASSERT(SUCCESS == restore_mutable_fixture("tests/fixtures/diffs/diff1"));
3255 : :
3256 : : /*
3257 : : * Release managed buffers allocated by this test before returning
3258 : : */
3259 : 2 : m_del(target_path);
3260 : 2 : m_del(pattern);
3261 : 2 : m_del(result);
3262 : :
3263 : 2 : RETURN_STATUS;
3264 : : }
3265 : :
3266 : : /**
3267 : : * @brief Run the README lock-checksum scenario tests
3268 : : *
3269 : : * Covers README Examples 9 and 10
3270 : : *
3271 : : * Exercises protected checksum paths, locked-file rehashing, timestamp
3272 : : * watching, and interactions with cleanup options for inaccessible or ignored files
3273 : : */
3274 : 2 : Return test0030(void)
3275 : : {
3276 : 2 : INITTEST;
3277 : :
3278 : 2 : SLOWTEST;
3279 : :
3280 : 2 : TEST(test0030_1,"Locked size change warns and keeps the protected DB row unchanged");
3281 : 2 : TEST(test0030_2,"Watched locked timestamp drift warns and keeps the protected DB row unchanged");
3282 : 2 : TEST(test0030_3,"Unwatched locked timestamp drift is ignored and keeps the protected DB row unchanged");
3283 : 2 : TEST(test0030_4,"--rehash-locked synchronizes watched timestamp drift after checksum match");
3284 : 2 : TEST(test0030_5,"--rehash-locked synchronizes timestamp drift even without --watch-timestamps");
3285 : 2 : TEST(test0030_6,"Watched rehash reports a stored locked-checksum mismatch");
3286 : 2 : TEST(test0030_7,"--rehash-locked reports locked content corruption and keeps the DB row unchanged");
3287 : 2 : TEST(test0030_8,"Watched --rehash-locked reports locked content corruption and keeps the DB row unchanged");
3288 : 2 : TEST(test0030_9,"Watched unchanged locked files complete successfully without DB drift");
3289 : 2 : TEST(test0030_10,"Deleted locked file warns and keeps the protected DB row unchanged");
3290 : 2 : TEST(test0030_11,"--db-drop-inaccessible does not drop an access-denied locked file");
3291 : 2 : TEST(test0030_12,"--db-drop-inaccessible does not drop a locked file after access-check failure");
3292 : 2 : TEST(test0030_13,"--db-drop-ignored does not drop a deleted checksum-locked file");
3293 : 2 : TEST(test0030_14,"--rehash-locked still detects corruption in an ignored locked file");
3294 : 2 : TEST(test0030_15,"--include does not let --db-drop-ignored drop a deleted locked file");
3295 : 2 : TEST(test0030_16,"--rehash-locked checks ignored locked corruption outside the restored include subset");
3296 : 2 : TEST(test0030_17,"--ignore does not let --db-drop-inaccessible drop an access-denied locked file");
3297 : 2 : TEST(test0030_18,"--ignore does not let --db-drop-inaccessible drop a locked file after access-check failure");
3298 : 2 : TEST(test0030_19,"Cleanup preserves an access-denied checksum-locked row despite --db-drop-inaccessible");
3299 : 2 : TEST(test0030_20,"Cleanup preserves a checksum-locked row after access-check failure");
3300 : 2 : TEST(test0030_21,"Cleanup preserves an ignored checksum-locked row after access-check failure");
3301 : :
3302 : 2 : RETURN_STATUS;
3303 : : }
|