Handoff document for resuming work on bit-backup. Based only on the current
repo state (branch develop, HEAD 6eaae6b) and observed build/test behavior.
- What:
bit-backupis a C++23 command-line tool that detects "bit rot" (silent data corruption) by storing SHA-512 checksums of files in a local SQLite database (.bitbackup.sqlite3) and re-verifying them on later runs. - Main goal: long-term data integrity for large file trees.
- Current phase: the original transliterated-from-Java code has been made
performant and extended with features. Core is stable; recent work focused on
performance (batched DB writes, parallel hashing),
.bitbackupignorecorrectness/speed, error handling, and a new directory-locking feature. - Important architectural decisions:
- Command pattern:
BitBackupProgramdispatches toCheckCommand(default),HelpCommand,VersionCommand. - Persistence behind interfaces (
FileRepository,SystemItemRepository) with a single SQLite implementation; one shared SQLite connection per run, all writes batched in transactions. - Schema evolves via an append-only, hash-validated migration array.
- DB self-integrity hash uses a rollback journal (NOT WAL) so the single
.sqlite3file is always complete after commit. - Hashing is parallel; DB writes stay single-threaded.
- Command pattern:
- Build: WORKS. CMake (Release) builds
bit_backupand (with-DENABLE_TESTS=ON) theTeststarget cleanly. Toolchain in use: GCC 14, CMake 3.31, OpenSSL 3.5, bundled SQLiteCpp + googletest submodules. - Tests: PASS —
ctestreports 49/49 passing. - CLI available:
- Commands:
check(default when no command given),help,version. checkoptions:dir=,report=true,verbose=true,bitbackupindex=true,threads=N,quick=true,scrub=N(0–100),confirm=delete(interactive, per-violation permanent-removal prompt for stuck locked-file deletions).- Exit code:
checkreturns non-zero (1) when bit rot OR a lock violation is found;help/versionalways return 0; unknown command/arguments print a clean error and return 1 (no more SIGABRT).
- Commands:
- Recently implemented (working): batched SQLite writes; parallel SHA-512
hashing;
quick/scrubmodes;.bitbackupignoreprecompiled regex + fixed leading-slash/CRLF handling + directory pruning + negation/trailing-slash; graceful error handling;.bitbackuplockdirectory locking with aLOCKEDDB column. - Working demos/examples: the golden characterization scenario (add / modify / delete / silent bit rot) and the lock end-to-end tests all pass; manual verification of locking, quick/scrub, and the migration upgrade path was done.
- What does NOT work yet / caveats:
- Running
checkwithdir=<path>from a different current working directory is unsafe when bit rot is found (see §4). - Options must follow an explicit
check(e.g.bit_backup quick=truealone is treated as an unknown command and errors out). quickmode intentionally does not detect silent rot for non-locked files.
- Running
6eaae6bFeature:check confirm=deleteinteractively asks, per "locked file deleted" violation, whether to permanently remove it from the DB (yremoves it — even overriding an active lock — anything else/EOF leaves it exactly as before). This is the supported way to resolve violations that can never auto-clear (e.g. a whole locked subtree deleted together with its.bitbackuplockmarker in one shot) without resorting to raw SQLite edits, which would desync the DB's own self-integrity checksum and make the nextcheckabort at part 1.CheckCommandgained a second constructor taking an injectablestd::istream&(defaults tostd::cin) so the prompt is testable without real stdin. AddedCheckCommandLockTest.ConfirmDeleteAcceptsBypassDeletionWhenConfirmed/.ConfirmDeleteKeepsViolationWhenDeclined. Only covers deletion-type violations so far — "locked file modified" / "new file in locked directory" are not yet confirmable this way.2dc752eFix: removing.bitbackuplockafter a file was deleted while locked now actually unlocks it. Previouslypart7RemoveDeletedFilesFromDbkeptfileInDb.locked == 1as a permanent fallback, so a "locked file deleted" violation could never clear even after the marker was removed. Now it only stays a violation if the file's containing directory is also gone (the "whole locked subtree deleted at once" bypass this fallback exists to catch, perDeletedLockedFileWithMarkerGoneStillReported); if the directory still exists, removing the marker resumes normal deletion handling and the row is finally removed, matching the README's documented contract. AddedCheckCommandLockTest.UnlockingResolvesPreviouslyReportedDeletion.cec3346Flag deleted locked files asKOin the DB (kept row, frozen mtime/hash, result set to KO).4cd53a2Directory locking via.bitbackuplock: migration #5 adds aLOCKEDcolumn;FsFile.locked;CheckCommandpart4 lock-root detection +isPathLocked; part6/7/8 frozen-set semantics; red summary; non-zero exit; 8 e2e lock tests + repoLOCKEDround-trip test.e3dc2f1Catch exceptions inmain()→ clean redError:+ exit 1 instead ofstd::terminate/SIGABRT;BitBackupProgram::runnow returns an exit code.91052d5End-to-end bit-rot detection tests (CheckCommandBitRotTests.cpp).a3fcbc8.bitbackupignoregitignore-style negation (!) + trailing-slash directory patterns.ac328b5Prune ignored directories during the scan (disable_recursion_pending).e370e5bPrecompile ignore regexes once + fix leading-slash/CRLF + auto-ignore metadata files + unit tests.bc9e2faParallel hashing,quick/scrubmodes, 1 MiB read buffer.c457f6aBatched SQLite writes (single connection, transactions); re-enabled the GTest target; fixed a CMake bug whereMain.cppleaked into the core lib.
Only untracked file: .bitbackupignore in the repo root (pre-existing, not part
of this work).
There is no build or test blocker — everything builds and ctest is green.
The most important open correctness issue is CWD-relative path handling in the bit-rot summary and the CSV report:
- Symptom: when
checkis run withdir=<somewhere-else>from a different process working directory AND bit rot is found, the summary re-hash throwsFile does not exist(caught bymain→ printsError:and exits 1) instead of printing the bit-rot report. Withreport=truethe report rows are also computed against the wrong path. - Failing command (manual repro): from
/tmp, runbit_backup check dir=/path/with/bitrot report=true— the summary/report re-hash resolves./<relativepath>against/tmp, not againstdir=. - Failing test: none yet — the existing e2e tests
chdirinto the fixture dir, so they never exercise thedir=+ foreign-CWD path. Needs a test. - Affected files/modules:
src/BitBackup/Commands/CheckCommand.cpp— the summary loop insiderun()andpart9CreateReportCsvIfNeeded, both of which buildFile("./" + f.absolutePath). - Suspected cause: those two spots use
"./" + absolutePath(process CWD) instead ofbitBackupContext.getWorkingDirectory() + "/" + absolutePath(which part8's detection already uses correctly). - Already tried: nothing fixed yet; identified by code inspection. part8's actual detection/DB update is correct; only the human-facing summary/report use the wrong base path.
- CONFIRMED BUG: bit-rot summary +
part9report use"./" + absolutePath(CWD-relative) — wrong/throws whendir=differs from the process CWD. See §4. - CONFIRMED (UX wart): the first CLI argument is always the command, so
options without
check(bit_backup quick=true) error with "Invalid command!". Documented in README; not yet softened. - INCOMPLETE: nested
.bitbackupignorefiles are not loaded; only the root one is applied (the older recursive loader is dead code). - INCOMPLETE / DOCUMENTED:
**globstar is not special-cased — a single*already crosses/in this implementation, so patterns with a slash are broader than gitignore's single-level*. - BY DESIGN:
quick/partialscrubskip silent-rot detection for unchanged-modtime, non-locked files. - TECH DEBT: dead code remains (
foundFilesInCurrentDir,Utils::listAllFilesInDir);part8keeps an unusedfilesToBeRemovedFromDbparameter;found.reserve(200000)is a magic number;BitBackupContextuses rawnew/delete. (A broad "step 6" cleanup was explicitly deferred by the owner — do not start it unprompted.) - SECURITY / NEEDS ACTION (owner):
.git/configoriginURL contains a plaintext GitHub PAT. Recommend rotating it and using a credential helper / SSH. - MINOR:
SqliteDatabaseMigration::getInstance()allocates a singleton that is never freed;getCurrentDateTime()in the migration usesstd::localtime(single-threaded there, so fine).
- Entry / dispatch:
src/.../Core/Main.cpp→BitBackupProgram::run→ resolves the command bygetName()→Command::run(args).maincatchesstd::exceptionand maps a non-emptycheckresult to exit code 1. - Check flow (
CheckCommand::run, parts 1–10): 1 verify DB self-hash · 2 migrate schema · 3 update version · 4 scan filesystem (collects ignore + lock roots) · 5 load DB rows · 6 add new files (parallel hash) · 7 remove deleted (lock-aware) · 8 compare content/modtime (parallel hash, lock-aware) · 9 optional CSV report · 10 recompute DB self-hash. - Key modules:
Core/BitBackupIgnoreRegex(precompiled patterns,test()/matchesDirectoryContents()),Core/ListSet(vector + hash-set),Persistence/Impl/Sqlite/FileRepositoryImplSqlite(shared connection, batchedcreate/list/updateAll/removeAll/updateLastCheckDate),Persistence/Impl/Sqlite/SqliteDatabaseMigration(+Migrations.h). - Data flow for locking: part4 records dirs containing
.bitbackuplockaslockRoots(""= working-dir root);isPathLocked(rel)checks ancestors; theLOCKEDcolumn persists the state so a deletion is still caught as a violation if the marker and its directory vanish together in one shot. If only the marker is removed (the directory still exists),part7now treats the persistedLOCKEDflag as resolved and lets the deletion resume normal handling (row removed) — see the2dc752efix in §3. Violations that still can't auto-resolve (whole subtree + marker deleted together) require an explicit per-itemyundercheck confirm=delete(6eaae6b) to remove. - Invariants that MUST hold:
- Default (unlocked, no-flag)
checkbehavior must stay byte-identical — the golden scenario is the guard. migrations[]is append-only; existing entries are hash-validated at runtime and must never be edited. Add new migrations + bumpMIGRATION_COUNT.- Keep the rollback journal (do not switch SQLite to WAL) — the
.sqlite3self-integrity hash relies on the single file being complete after commit. - Hashing may be parallel; all DB writes must stay single-threaded.
last_modified_string/print_clockuselocaltime_r(thread-safe) because they run inside worker threads — keep it that way.- Locked files: never overwrite stored
LAST_MODIFICATION_DATE,HASH_SUM_VALUE,SIZE.
- Default (unlocked, no-flag)
- Compatibility: existing 4-migration DBs auto-upgrade to migration 5
(verified).
FileRepositoryis the persistence boundary; changing its interface affectsCheckCommandand tests.
# Configure (with tests) and build everything
cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_TESTS=ON
cmake --build build -j"$(nproc)"
# Run the tool
./build/bit_backup # = check, current directory
./build/bit_backup check dir=/path/to/data
./build/bit_backup check threads=8 quick=true
./build/bit_backup version
./build/bit_backup help
# Run the whole test suite
cd build && ctest --output-on-failure
# Run a single test group
./build/Tests --gtest_filter='CheckCommandLockTest.*'
./build/Tests --gtest_filter='CheckCommandBitRotTest.*'
# Reproduce the §4 bug (run from a DIFFERENT cwd than the data dir):
# 1) make a dir D with a file, run check inside D to index it
# 2) corrupt the file's bytes but restore its old mtime (silent rot)
# 3) from /tmp: /abs/path/build/bit_backup check dir=D report=true
# -> expect: should report bit rot; actually errors on a wrong "./path"
# Lock demo (frozen directory):
# index a dir, then `touch <dir>/subdir/.bitbackuplock`, run check again,
# then modify a file under subdir and run check -> violation + exit 1,
# stored mtime/hash unchanged.
# confirm=delete demo (resolving a stuck locked-deletion violation):
# index a dir, lock it, delete the whole subtree + marker in one shot,
# run check (violation, stuck forever without confirm), then:
# ./build/bit_backup check confirm=delete
# -> prompts "... Permanently remove from the database? [y/N]:" per item.No linter/formatter is configured in the repo.
-
Add a failing test for the
dir=+ foreign-CWD bit-rot path.- Goal: lock in the §4 bug with a red test before fixing.
- Files: new
tests/BitBackup/Commands/CheckCommandDirArgTests.cpp(do NOTchdir; passdir=<temp>while CWD stays elsewhere; cause silent rot). - Verify:
cd build && ctest— the new test should FAIL initially.
-
Fix CWD-relative paths in the summary + report.
- Goal: use the working directory instead of
"./"sodir=works from any CWD. - Files:
src/BitBackup/Commands/CheckCommand.cpp— the bit-rot summary loop inrun()andpart9CreateReportCsvIfNeeded(replace"./" + f.absolutePathwithbitBackupContext.getWorkingDirectory()/bitBackupFiles.workingDirjoined paths). - Verify: the task-1 test now PASSES;
cteststays 49+/all green; golden scenario still identical.
- Goal: use the working directory instead of
-
Remove the unused
filesToBeRemovedFromDbparameter from part8.- Goal: kill a dead parameter and its warning.
- Files:
CheckCommand.cpp/CheckCommand.h(signature + the single caller inrun()). - Verify:
cmake --build buildclean;ctestgreen.
-
Make options-without-
checknot error (small UX fix) — OPTIONAL.- Goal:
bit_backup quick=trueshould behave likecheck quick=true(or at least exit cleanly). Decide semantics first. - Files:
Core/BitBackupArgs.cpp(command detection) and/orCore/BitBackupProgram.cpp. - Verify:
./build/bit_backup quick=true; echo $?returns 0 and runs a check; add a small test for the chosen behavior.
- Goal:
- No broad refactor / "step 6" cleanup (DI, dead-code removal, smart pointers, OpenSSL EVP migration) — the owner explicitly deferred this.
- Do not switch SQLite to WAL — it breaks the
.sqlite3self-integrity hash. - Do not edit existing
migrations[]strings — they are hash-validated; only append a new migration and bumpMIGRATION_COUNT. - No
FileRepositoryinterface changes without updating all callers and tests and checking the migration/DB compatibility path. - Do not change the default (unlocked, no-flag)
checkbehavior without re-running the golden scenario; it must stay identical. - No new subcommands (e.g.
lock/unlock/status) until §4 is fixed. (check confirm=deleteadded in6eaae6bis acheckoption, not a new subcommand, so it doesn't violate this — keep future additions the same way.) - Do not "fix" the embedded git PAT in code — that's an owner/ops action (rotate + credential helper), not a source change.
Read NEXT.md in the repo root first. Work only on "Next smallest task #1"
(and then #2) from it: add a failing test that runs `check` with dir=<temp>
from a different current working directory and triggers silent bit rot, then
fix the CWD-relative "./"+absolutePath usage in CheckCommand.cpp's run()
summary and part9CreateReportCsvIfNeeded so it uses the working directory.
Inspect only the files needed for that task (CheckCommand.cpp/.h and the
existing CheckCommandBitRotTests.cpp as a template). Do not refactor unrelated
code, do not change the migrations array, do not switch to WAL, and do not
alter default unlocked check behavior. Make one small, verified change.
Build and test with:
cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_TESTS=ON
cmake --build build -j"$(nproc)"
cd build && ctest --output-on-failure
Confirm the new test goes red-then-green and that the golden default behavior
is unchanged. Then update NEXT.md (move the finished task out, refresh status).