diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index f4e2cea..be936fc 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -212,8 +212,8 @@ def _parseparam(s: str) -> list[str]: if i == -1: f = s[start:end] else: - f = s[start:i].rstrip().lower() + "=" + s[i + 1 : end].lstrip() - plist.append(f.strip()) + f = s[start:i].rstrip(" \t").lower() + "=" + s[i + 1 : end].lstrip(" \t") + plist.append(f.strip(" \t")) start = end return plist @@ -232,7 +232,7 @@ def parse_options_header(value: str | bytes | None) -> tuple[bytes, dict[bytes, # If we have no options, return the string as-is. if ";" not in value: - return (value.lower().strip().encode("latin-1"), {}) + return (value.lower().strip(" \t").encode("latin-1"), {}) ctype, *segments = _parseparam(value) options: dict[bytes, bytes] = {} diff --git a/tests/test_data/http/non_ascii_whitespace_before_filename.http b/tests/test_data/http/non_ascii_whitespace_before_filename.http new file mode 100644 index 0000000..7d06847 --- /dev/null +++ b/tests/test_data/http/non_ascii_whitespace_before_filename.http @@ -0,0 +1,9 @@ +------boundary +Content-Disposition: form-data; name="a"; filename="a.txt" + +first +------boundary +Content-Disposition: form-data; name="b";…filename="b.txt" + +second +------boundary-- diff --git a/tests/test_data/http/non_ascii_whitespace_before_filename.yaml b/tests/test_data/http/non_ascii_whitespace_before_filename.yaml new file mode 100644 index 0000000..8f084e0 --- /dev/null +++ b/tests/test_data/http/non_ascii_whitespace_before_filename.yaml @@ -0,0 +1,8 @@ +boundary: ----boundary +expected: + - name: a + type: field + data: !!binary "Zmlyc3Q=" + - name: b + type: field + data: !!binary "c2Vjb25k" diff --git a/tests/test_multipart.py b/tests/test_multipart.py index e2ad6ca..31b84e3 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -272,6 +272,14 @@ def test_single_param_with_spaces(self) -> None: self.assertEqual(t, b"application/json") self.assertEqual(p, {b"par": b"val"}) + def test_tabs_around_param_are_trimmed(self) -> None: + _, p = parse_options_header(b"application/json;\t par \t=\t val\t") + self.assertEqual(p, {b"par": b"val"}) + + def test_non_breaking_space_is_not_trimmed(self) -> None: + _, p = parse_options_header(b"form-data; name=voil\xc3\xa0") + self.assertEqual(p, {b"name": b"voil\xc3\xa0"}) + def test_multiple_params(self) -> None: t, p = parse_options_header(b"application/json;par=val;asdf=foo") self.assertEqual(t, b"application/json")