Keep '=' literal in URL paths so they match patterns (fixes #51)#77
Keep '=' literal in URL paths so they match patterns (fixes #51)#77tienvantranspk-tech wants to merge 2 commits into
Conversation
`_quote_path()` percent-encoded `=` (safe=`/%`) while `_quote_pattern()` kept it literal (safe=`/*%=`), so an Allow/Disallow pattern containing `=` could never match a URL whose path contains `=`. Example: with `Allow: /*/filter/page=*/$` and `Disallow: /`, the URL `/1/filter/page=5/` was wrongly disallowed. Add `=` to the safe set in `_quote_path()` so paths and patterns are encoded consistently. Fixes scrapy#51.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes robots.txt URL matching involving = in paths by preventing = from being percent-encoded during path quoting, and adds regression tests for the reported mismatch.
Changes:
- Keep
=unescaped in_quote_pathso URL paths match patterns that treat=as a literal character. - Add parametrized regression tests covering
=within wildcard patterns and in non-wildcard rules.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/test_on_google_spec.py | Adds regression coverage for = handling in paths/patterns (issue #51). |
| src/protego/_utils.py | Adjusts quoting rules to preserve = in the encoded path for matching parity with patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # "=" must be matched literally without wildcards too. | ||
| ("User-agent: *\nDisallow: /path=1\n", "http://example.com/path=1", False), | ||
| ("User-agent: *\nDisallow: /path=1\n", "http://example.com/path=2", True), |
| @pytest.mark.parametrize( | ||
| ("content", "url", "allowed"), | ||
| [ | ||
| # "=" in a wildcard pattern must match "=" in the URL path. | ||
| ( | ||
| "User-agent: *\nAllow: /*/filter/page=*/$\nDisallow: /\n", | ||
| "http://example.com/1/filter/page=5/", | ||
| True, | ||
| ), |
| # Keep "=" unescaped so paths match patterns, which also keep "=" literal | ||
| # (see _quote_pattern). https://github.com/scrapy/protego/issues/51 | ||
| path = quote(path, safe="/%=") |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #77 +/- ##
=======================================
Coverage 97.97% 97.97%
=======================================
Files 5 5
Lines 345 345
Branches 69 69
=======================================
Hits 338 338
Misses 4 4
Partials 3 3
🚀 New features to boost your workflow:
|
Per review feedback: cover a pre-encoded %3D URL path and a non-wildcard prefix match, and reword the comment so it does not read as exact-match.
|
Thanks for the review! I pushed a commit addressing the test feedback:
On extracting a shared constant for the |
|
I wonder… Is the right fix not encoding = in both cases, or is the right fix encoding it in both cases? |
|
Both directions give consistent matching; I went with keeping
Encoding |
|
What do you make of the following analysis? LLM analysisThe key language from RFC 9309 §2.2.2 is (emphasis mine):
This is significant for the PR's approach.
So RFC 9309 says both pattern and URL should end up with The RFC 9309-compliant fix would actually be to remove In summary:
The PR achieves consistency and fixes the real bug, but it does so in a way that's at odds with RFC 9309 and inconsistent with how all other reserved characters (like |
Fixes #51.
Problem
_quote_path()(used to normalize the URL being checked) percent-encodes=, because itsquote()call usessafe="/%". But_quote_pattern()(used to normalizeAllow/Disallowpatterns) keeps=literal viasafe="/*%=".Because of this asymmetry, a pattern containing
=can never match a URL whose path contains=: the pattern keepspage=, while the URL becomespage%3D.Example from the issue:
can_fetch("https://example.com/1/filter/page=5/", "mozilla")returnedFalse(the URL had been normalized to/1/filter/page%3D5/, which the.../page=.../pattern could not match), when it should beTrue.Fix
Add
=to thesafeset in_quote_path()so URL paths keep=literal, matching how patterns are already normalized.=is a valid path character (an RFC 3986 sub-delimiter) and does not need percent-encoding.Tests
Added
test_equals_sign_in_path, covering the wildcard case from the issue, the trailing-$boundary, and a plain (non-wildcard)=path. Full suite passes locally on Python 3.14 (4351 tests);ruff check/ruff formatare clean.