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

Generated by: LCOV version 2.0-1