]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Fix optional percent encoding behaviour. (#2671)
authorTom Christie <tom@tomchristie.com>
Wed, 19 Apr 2023 12:21:42 +0000 (13:21 +0100)
committerGitHub <noreply@github.com>
Wed, 19 Apr 2023 12:21:42 +0000 (13:21 +0100)
* Tests for failing optional percent encoding

* Linting

* Fix for optional percent escaping

httpx/_urlparse.py
tests/test_urlparse.py

index 0fbec3584a20dd64caf8cc7085deeb276525e4c6..6522d917bc90a8c091d42570d581169da4c5e4ad 100644 (file)
@@ -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]
     )
index ec1fd20d03257899d16f038ff02d9d493e859fa6..6b5c83442790ef2ae9b30691af7118f6f42f11dc 100644 (file)
@@ -126,6 +126,24 @@ 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