LCOV - code coverage report
Current view: top level - tests/src - test0030.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 1162 1162
Test Date: 2026-10-20 14:27:43 Functions: 100.0 % 22 22
Branches: 100.0 % 4 4

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

Generated by: LCOV version 2.0-1