Validate topology and destructive path boundaries (#7)

* Validate topology and destructive path boundaries

- Canonicalize sources/backups_dir and reject overlap in either direction
- Reject duplicate sources and destination basename collisions
- Reject non-normalized relative paths in nest_hardlink
- Give each backup repository a persistent identity, recorded in every
  snapshot's completion manifest
- Verify repo identity, directory type (no symlinks), containment, and
  device before any recursive snapshot/staging-dir deletion, refusing to
  cross mount points

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address Copilot review: root-path check, symlink/TOCTOU-safe writes, legacy marker adoption

- _is_same_or_within: drop the redundant, root-mishandling startswith
  check; commonpath() alone is already component-aware
- _ensure_repo_id/_write_manifest: write via temp-file + atomic rename
  (O_NOFOLLOW, O_EXCL on the temp file) instead of open+write in place,
  so writes can never go through a pre-existing symlink and are never
  left half-written
- _verify_safe_to_delete: distinguish a legacy empty marker (pre-dates
  the manifest format, can only be a pre-migration artifact) from a
  non-empty-but-invalid one (corrupt or forged); adopt the former into
  the repository instead of refusing to delete it forever

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-08-03 22:26:51 +00:00
1 parent a987180653
commit 72a8fe2b59
7 files changed
+450 -33

No files matched your search

+3 -5
View File
@@ -384,10 +384,9 @@ class TestDirectoryValidation:
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b', 'backups',
str(source)]):
with mock.patch('os.path.abspath',
return_value=str(backups_dir)) as m_abspath:
with mock.patch('os.getcwd', return_value=str(tmp_path)):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b', 'backups',
str(source)]):
with mock.patch('os.path.isdir', return_value=True):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True) as m_lock:
@@ -399,7 +398,6 @@ class TestDirectoryValidation:
'curateipsum.backup.release_backups_lock'):
cli.main()
m_abspath.assert_called_with('backups')
m_lock.assert_called_once_with(str(backups_dir), False)