Skip to content

Refuse to delete through a path that no longer leads inside the scanned folder [patch] - #204

Merged
matt-edmondson merged 4 commits into
mainfrom
fix/202-stay-inside-scanned-root
Nov 4, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
fix/202-stay-inside-scanned-root

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Deletion used the path string recorded at scan time. If a subdirectory was swapped for a symlink to an outside folder during the confirmation prompt, File.Delete followed the link and deleted an identical file outside the scanned root.

Changes

  • New RealPath helper. It resolves an absolute path by following every symlink and junction along it, for both relative and absolute targets and through nested links, with the same 40-link limit Linux uses. IsWithin compares paths case-insensitively on Windows and macOS, and treats a name that only shares the root's prefix (root-sibling) as outside it.
  • Deduplicator.DeleteDuplicates(groups, root) now takes the scanned root. Immediately before each delete, the file's directory must resolve to the resolved root or somewhere under it. Otherwise the file is skipped with a reason that names where its directory now leads. If a path can't be resolved, the file is also skipped.
  • The Deduplicate verb passes options.Path, and test call sites pass tree.Root.
  • README: one paragraph on the guarantee.

This takes the issue's first suggested fix, a resolve-and-contain check placed last, right before the delete. That shrinks the race window from the length of the prompt to microseconds but does not close it completely. A complete fix needs handle-relative unlinkat, which would mean platform-specific deletion code, so it is left out of this PR.

Tests

  • ASubdirectorySwappedForALinkOutsideTheRootIsNotDeletedThrough reproduces the issue. The outside file survives and is reported as skipped. The test fails with the check disabled and passes with it enabled.
  • ARootReachedThroughALinkStillDeletesItsDuplicates checks that a symlinked root isn't over-blocked.
  • RealPathFollowsNestedRelativeLinks covers the resolver and the prefix-sibling case.
  • Full suite: 124 total, 0 failed, 3 skipped (the same skips as on main).

Note: #203 (for #201) also changes the DeleteDuplicates signature by adding an optional afterDelete parameter, so whichever PR merges second will need a one-line merge of the two parameters.

Fixes #202


Generated by Claude Code

…ed folder [patch]

Deletion used the path string recorded at scan time. If a directory on that
path was replaced by a symlink after the scan, for example during the
"Proceed with deletion?" prompt, File.Delete followed it and removed an
identical file outside the root the user approved, and the run reported it
as an ordinary deletion.

DeleteDuplicates now takes the scanned root. Immediately before each delete,
the file's directory is resolved with every symlink and junction followed
(RealPath) and must lie at or under the resolved root. Otherwise the file is
skipped with a reason giving where its directory now leads. A root that is
itself reached through a link still works, because both sides are resolved.

Fixes #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5MMN2e4Pk8UWLnvp9
Comment thread FileDeduplicator/RealPath.cs Fixed
Comment thread FileDeduplicator/RealPath.cs Fixed
…t [patch]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5MMN2e4Pk8UWLnvp9
…t paths [patch]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5MMN2e4Pk8UWLnvp9
…canned-root

# Conflicts:
#	FileDeduplicator/Deduplicator.cs
#	README.md
@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit c8d0021 into main Nov 4, 2026
15 checks passed
@matt-edmondson
matt-edmondson deleted the fix/202-stay-inside-scanned-root branch November 4, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deduplicate deletes a file outside the scanned root when a subdirectory is replaced by a symlink during the "Proceed with deletion?" prompt

2 participants