Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions reponeedle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,25 @@ def resolve_and_validate_anchors(repo_dir: str, anchors: Optional[List[str]]) ->
continue
raw_anchor = a.strip()

# Check path interpretation 1: Relative to repo_dir (e.g. "auth/session.py" with --repo "dir")
candidate_1 = os.path.normpath(os.path.join(abs_repo, raw_anchor))
# Check path interpretation 2: Relative to current working directory or absolute
candidate_2 = os.path.normpath(os.path.abspath(raw_anchor))
# The exact spelling is tried first. On POSIX, a Windows-style spelling ("src\\a.py") is then
# retried with forward slashes, consistent with retrieve(anchor_files=...). A real file whose
# name contains a backslash still takes precedence. Containment checks below apply either way.
spellings = [raw_anchor]
if os.sep == "/" and "\\" in raw_anchor:
spellings.append(raw_anchor.replace("\\", "/"))

resolved_target = None
if os.path.exists(candidate_1) and os.path.isfile(candidate_1):
resolved_target = candidate_1
elif os.path.exists(candidate_2) and os.path.isfile(candidate_2):
resolved_target = candidate_2
for spelling in spellings:
# Interpretation 1: relative to repo_dir (e.g. "auth/session.py" with --repo "dir").
candidate_1 = os.path.normpath(os.path.join(abs_repo, spelling))
# Interpretation 2: relative to the current working directory, or absolute.
candidate_2 = os.path.normpath(os.path.abspath(spelling))
if os.path.isfile(candidate_1):
resolved_target = candidate_1
elif os.path.isfile(candidate_2):
resolved_target = candidate_2
if resolved_target is not None:
break

if resolved_target is None:
raise ValueError(
Expand Down
7 changes: 7 additions & 0 deletions tests/test_cli_anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ def test_resolve_and_validate_anchors_outside_repo(self, change_impact_repo: Pat
with pytest.raises(ValueError, match="resolves outside repository root"):
resolve_and_validate_anchors(str(change_impact_repo), [str(outside_file)])

def test_resolve_and_validate_anchors_backslash_traversal_rejected(self, change_impact_repo: Path):
# The backslash-to-slash retry (POSIX) must not open a traversal route.
outside_file = change_impact_repo.parent / "outside_bs.py"
outside_file.write_text("print('outside')", encoding="utf-8")
with pytest.raises(ValueError, match="resolves outside repository root"):
resolve_and_validate_anchors(str(change_impact_repo), ["..\\outside_bs.py"])

def test_cli_change_impact_single_anchor(self, change_impact_repo: Path, capsys: pytest.CaptureFixture):
exit_code = main([
"Fix payment gateway timeout",
Expand Down
Loading