chore(compile): tighten link_resolver._resolve_path input guards#991
Conversation
c881fc8 to
8025653
Compare
Maintainer rebase + panel verdict (per #1064 FIX-NOW orchestration)Rebased on Verdict: READY Panel findings (7 personas)
Test evidencePer fidelity matrix: pure path-resolution, tmp_path-backed unit tests are the contracted integration surface. Merging via squash next. |
danielmeppiel
left a comment
There was a problem hiding this comment.
Approved per maintainer panel verdict (see prior comment); CI green.
There was a problem hiding this comment.
Pull request overview
Tightens link_resolver._resolve_path input handling so empty / whitespace-only link targets resolve to None instead of incorrectly resolving to base_path, and adds unit tests to lock in the intended edge-case behavior discussed in #841.
Changes:
- Add an early-return guard in
_resolve_pathfor empty / whitespace-only input. - Add a new
TestResolvePathInputGuardstest class covering empty/whitespace, NUL bytes, traversal-like strings,file://-like inputs, and a normal relative path case. - Update test imports to exercise
_resolve_pathdirectly.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/compilation/link_resolver.py |
Adds an early-return guard to prevent empty/whitespace paths from resolving to the base directory. |
tests/unit/compilation/test_link_resolver.py |
Adds focused unit tests to validate _resolve_path behavior for several problematic inputs. |
Copilot's findings
Comments suppressed due to low confidence (3)
tests/unit/compilation/test_link_resolver.py:498
- This is also platform-dependent: on Windows, pathlib may treat
file:as a drive component, making the path "absolute" and causing_resolve_pathto return it unchanged (so it would not start with base_dir). Please skip/parametrize this test for POSIX-only behavior, or assert the Windows behavior explicitly.
def test_file_uri_on_posix_is_treated_as_relative(self, base_dir):
"""`file://...` is not absolute on POSIX, so it joins under base_dir rather than escaping it."""
result = _resolve_path("file:///etc/passwd", base_dir)
assert result is not None
assert str(result).startswith(str(base_dir))
src/apm_cli/compilation/link_resolver.py:450
_resolve_pathcan currently return aPathcontaining an embedded NUL (e.g., from markdown content). Call sites in this module then call.exists()on thatPathwithout catchingValueError, which will raise "embedded null byte" and crash compilation/validation. Consider rejecting inputs containing\x00(returnNone) so downstream.exists()checks stay safe.
if not path or not path.strip():
return None
try:
if Path(path).is_absolute():
return Path(path)
else:
return base_path / path
except (OSError, ValueError):
tests/unit/compilation/test_link_resolver.py:492
- These assertions are POSIX-specific (on Windows, backslash is a path separator, so this input will be split into segments and the expected literal-segment behavior will not hold). Please skip/parametrize this test by platform (e.g., only run on non-nt) or adjust expectations for Windows to avoid post-merge failures in the Windows unit-test job.
def test_posix_backslash_traversal_stays_relative(self, base_dir):
"""Backslashes are literal characters on POSIX, so the path stays under base_dir."""
result = _resolve_path("foo\\..\\..\\etc\\passwd", base_dir)
assert result is not None
# The literal backslash filename is interpreted as a single segment under base_dir.
assert result == base_dir / "foo\\..\\..\\etc\\passwd"
- Files reviewed: 2/2 changed files
- Comments generated: 1
| def test_embedded_nul_byte_does_not_crash(self, base_dir): | ||
| """An embedded NUL byte must not crash _resolve_path itself. | ||
|
|
||
| The current containment relies on the caller's `.exists()` check to | ||
| reject the resulting path -- this lock-in is documented in the issue | ||
| ("Current containment code handles these correctly"). | ||
| """ | ||
| # Either return value is acceptable; what matters is no exception. | ||
| result = _resolve_path("foo\x00bar", base_dir) | ||
| assert result is None or isinstance(result, Path) |
There was a problem hiding this comment.
This test/docstring claims the caller's .exists() check will "reject" a path with an embedded NUL, but in practice Path.exists() raises ValueError for embedded null bytes unless explicitly caught. If the intent is "must not crash overall", it would be better to have _resolve_path return None for \x00 inputs and assert that here, rather than allowing a Path return value.
This issue also appears in the following locations of the same file:
- line 487
- line 494
| def test_embedded_nul_byte_does_not_crash(self, base_dir): | |
| """An embedded NUL byte must not crash _resolve_path itself. | |
| The current containment relies on the caller's `.exists()` check to | |
| reject the resulting path -- this lock-in is documented in the issue | |
| ("Current containment code handles these correctly"). | |
| """ | |
| # Either return value is acceptable; what matters is no exception. | |
| result = _resolve_path("foo\x00bar", base_dir) | |
| assert result is None or isinstance(result, Path) | |
| def test_embedded_nul_byte_returns_none(self, base_dir): | |
| """An embedded NUL byte should be rejected by _resolve_path.""" | |
| result = _resolve_path("foo\x00bar", base_dir) | |
| assert result is None |
Adds an early-return guard for empty / whitespace-only input. Previously
`_resolve_path("")` and `_resolve_path(" ")` returned the base directory
via `Path(base_dir) / ""`, which is semantically wrong (an empty link
should resolve to nothing) even though existing callers' `.exists()` check
masked the bug.
Adds a TestResolvePathInputGuards class that locks in the behaviour for:
- empty string
- whitespace-only (spaces, tab, newline)
- embedded NUL byte (must not crash; containment is the caller's job)
- POSIX backslash traversal (literal segment, stays under base_dir)
- file:// URI on POSIX (treated relative, joined under base_dir)
- non-existent relative target (happy path)
Closes microsoft#841
8025653 to
1ba83d7
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#1065) * fix: address Copilot review findings on #1055 and #991 (consolidated) Folds the still-actionable findings from the post-merge Copilot reviews of PRs #1055 and #991 into a single PR (no follow-up issue / PR sprawl). #1055 (CLAUDE_CONFIG_DIR support): - Document why the absolute-path fallback in TargetProfile.for_scope() is safe for downstream consumers (pathlib's 'rhs absolute wins' rule on '/' joins). Comment-only; no behavior change. - Findings already addressed by the original PR (verified): * resolve(strict=False) collapses '..' before relative_to(home) * docs/guides/dependencies.md + integrations/ide-tool-integration.md already cover CLAUDE_CONFIG_DIR in the merged version * test_scope_install_uninstall.py:382 + test_scope_integration.py already exercise install/uninstall + outside-home + traversal cases #991 (link_resolver guards): - Correct the embedded-NUL-byte test docstring: Path.exists() raises ValueError on NUL on most platforms (it does not silently return False). Cosmetic; test behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address Copilot review on #1065 Two valid findings from copilot-pull-request-reviewer: 1. link_resolver._resolve_path: NUL byte in input previously returned a Path that crashed downstream .exists() / .read_text() with ValueError, aborting markdown link resolution. Reject NUL at the resolver boundary so callers (resolve_markdown_links, validate_link_targets) get None as documented. Tightened test to assert None instead of 'either is fine'. 2. integration/targets.for_scope: prior comment claimed absolute root_dir was 'safe' for all downstream consumers. install/services. _deployed_path_entry would actually raise RuntimeError for an out-of-tree absolute path. Today this is unreachable because user-scope CLAUDE installs do not flow through that translator, but the comment now records the constraint so a future refactor that lockfiles user-scope deploys treats it as a dynamic-root case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #841.
Summary
Adds an early-return guard for empty / whitespace-only input in `_resolve_path` (Minor 1 + Minor 2 from the issue). Previously `_resolve_path("")` and `_resolve_path(" ")` returned the base directory via `Path(base_dir) / ""`, which is semantically wrong even though the existing `.exists()` checks in the call sites masked the bug.
```python
if not path or not path.strip():
return None
```
Tests (Minor 3)
Adds `TestResolvePathInputGuards` to `tests/unit/compilation/test_link_resolver.py` covering each case the issue called out:
Verification
`pytest tests/unit/compilation/test_link_resolver.py` -> 23 / 23 pass (17 existing + 6 new).