From 18cb3ab7e75d48f9fc534da295cee6a64f0b19a7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Apr 2023 11:58:22 +0100 Subject: [PATCH 1/3] Tests for failing optional percent encoding --- tests/test_urlparse.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_urlparse.py b/tests/test_urlparse.py index ec1fd20d03..95bf93e84d 100644 --- a/tests/test_urlparse.py +++ b/tests/test_urlparse.py @@ -126,6 +126,23 @@ def test_urlparse_leading_dot_prefix_on_relative_url(): assert url.path == "../abc" +# Tests for optional percent encoding + +def test_param_requires_encoding(): + url = httpx.URL('http://webservice', params={"u": "with spaces"}) + assert str(url) == 'http://webservice?u=with%20spaces' + + +def test_param_does_not_require_encoding(): + url = httpx.URL('http://webservice', params={"u": "with%20spaces"}) + assert str(url) == 'http://webservice?u=with%20spaces' + + +def test_param_with_existing_escape_requires_encoding(): + url = httpx.URL('http://webservice', params={"u": "http://example.com?q=foo%2Fa"}) + assert str(url) == 'http://webservice?u=http%3A//example.com%3Fq%3Dfoo%252Fa' + + # Tests for invalid URLs From 6c76aace11fa973c19372daace2174e05e413365 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Apr 2023 12:02:53 +0100 Subject: [PATCH 2/3] Linting --- tests/test_urlparse.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_urlparse.py b/tests/test_urlparse.py index 95bf93e84d..6b5c834427 100644 --- a/tests/test_urlparse.py +++ b/tests/test_urlparse.py @@ -128,19 +128,20 @@ def test_urlparse_leading_dot_prefix_on_relative_url(): # Tests for optional percent encoding + def test_param_requires_encoding(): - url = httpx.URL('http://webservice', params={"u": "with spaces"}) - assert str(url) == 'http://webservice?u=with%20spaces' + url = httpx.URL("http://webservice", params={"u": "with spaces"}) + assert str(url) == "http://webservice?u=with%20spaces" def test_param_does_not_require_encoding(): - url = httpx.URL('http://webservice', params={"u": "with%20spaces"}) - assert str(url) == 'http://webservice?u=with%20spaces' + url = httpx.URL("http://webservice", params={"u": "with%20spaces"}) + assert str(url) == "http://webservice?u=with%20spaces" def test_param_with_existing_escape_requires_encoding(): - url = httpx.URL('http://webservice', params={"u": "http://example.com?q=foo%2Fa"}) - assert str(url) == 'http://webservice?u=http%3A//example.com%3Fq%3Dfoo%252Fa' + url = httpx.URL("http://webservice", params={"u": "http://example.com?q=foo%2Fa"}) + assert str(url) == "http://webservice?u=http%3A//example.com%3Fq%3Dfoo%252Fa" # Tests for invalid URLs From 6880e2e4233305878055bfb7d6524619ecc553ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Apr 2023 12:05:25 +0100 Subject: [PATCH 3/3] Fix for optional percent escaping --- httpx/_urlparse.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/httpx/_urlparse.py b/httpx/_urlparse.py index 0fbec3584a..6522d917bc 100644 --- a/httpx/_urlparse.py +++ b/httpx/_urlparse.py @@ -399,7 +399,7 @@ def normalize_path(path: str) -> str: def percent_encode(char: str) -> str: """ - Replace every character in a string with the percent-encoded representation. + Replace a single character with the percent-encoded representation. Characters outside the ASCII range are represented with their a percent-encoded representation of their UTF-8 byte sequence. @@ -411,13 +411,29 @@ def percent_encode(char: str) -> str: return "".join([f"%{byte:02x}" for byte in char.encode("utf-8")]).upper() +def is_safe(string: str, safe: str = "/") -> bool: + """ + Determine if a given string is already quote-safe. + """ + NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + "%" + + # All characters must already be non-escaping or '%' + for char in string: + if char not in NON_ESCAPED_CHARS: + return False + + # Any '%' characters must be valid '%xx' escape sequences. + return string.count("%") == len(PERCENT_ENCODED_REGEX.findall(string)) + + def quote(string: str, safe: str = "/") -> str: - NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe - if string.count("%") == len(PERCENT_ENCODED_REGEX.findall(string)): - # If all occurances of '%' are valid '%xx' escapes, then treat - # percent as a non-escaping character. - NON_ESCAPED_CHARS += "%" + """ + Use percent-encoding to quote a string if required. + """ + if is_safe(string, safe=safe): + return string + NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe return "".join( [char if char in NON_ESCAPED_CHARS else percent_encode(char) for char in string] )