From 2ee0eabb4e59652a03250b839f7adfa5efce6096 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 17 Jul 2026 00:35:21 +0200 Subject: [PATCH 1/4] Fix bytes loader Base64 validation --- .../fragments/+strict_base64.bugfix.rst | 1 + .../_internal/morphing/concrete_provider.py | 9 +++++++++ tests/unit/morphing/test_concrete_provider.py | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 docs/changelog/fragments/+strict_base64.bugfix.rst diff --git a/docs/changelog/fragments/+strict_base64.bugfix.rst b/docs/changelog/fragments/+strict_base64.bugfix.rst new file mode 100644 index 00000000..9e13e2bf --- /dev/null +++ b/docs/changelog/fragments/+strict_base64.bugfix.rst @@ -0,0 +1 @@ +Reject leading and excess padding when loading Base64-encoded bytes-like values. diff --git a/src/adaptix/_internal/morphing/concrete_provider.py b/src/adaptix/_internal/morphing/concrete_provider.py index e910dcfb..65f9ad00 100644 --- a/src/adaptix/_internal/morphing/concrete_provider.py +++ b/src/adaptix/_internal/morphing/concrete_provider.py @@ -288,9 +288,18 @@ def bytes_base64_loader(data): encoded = data.encode("ascii") except AttributeError: raise TypeLoadError(str, data) + except UnicodeEncodeError: + raise ValueLoadError("Bad base64 string", data) if not B64_PATTERN.fullmatch(encoded): raise ValueLoadError("Bad base64 string", data) + if b"=" in encoded: + # a2b_base64 accepts leading and excess padding, so reject padding + # beyond the amount implied by the unpadded data length first. + unpadded = encoded.rstrip(b"=") + padding_length = len(encoded) - len(unpadded) + if not unpadded or padding_length > -len(unpadded) % 4: + raise ValueLoadError("Bad base64 string", data) try: return a2b_base64(encoded) diff --git a/tests/unit/morphing/test_concrete_provider.py b/tests/unit/morphing/test_concrete_provider.py index 126b6020..5b77230d 100644 --- a/tests/unit/morphing/test_concrete_provider.py +++ b/tests/unit/morphing/test_concrete_provider.py @@ -353,12 +353,20 @@ def test_bytes_like_provider( b64_string = b"YWJjZA==" assert get_string(loader(b64_string.decode())) == string + assert get_string(loader("")) == "" + assert get_string(loader("YQ==")) == "a" + assert get_string(loader("YWI=")) == "ab" raises_exc( ValueLoadError("Bad base64 string", "Hello, world"), lambda: loader("Hello, world"), ) + raises_exc( + ValueLoadError("Bad base64 string", "🦄"), + lambda: loader("🦄"), + ) + raises_exc( ValueLoadError( msg="Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4", @@ -372,6 +380,18 @@ def test_bytes_like_provider( lambda: loader("YWJjZA"), ) + for invalid_base64 in ( + "=", + "==", + "AAA==", + "AAAA=", + "AAAA==", + ): + raises_exc( + ValueLoadError("Bad base64 string", invalid_base64), + lambda invalid_base64=invalid_base64: loader(invalid_base64), + ) + raises_exc( TypeLoadError(str, 108), lambda: loader(108), From b30e8ec216e41defb1fa24a667b778de48e1f80c Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 17 Jul 2026 00:43:07 +0200 Subject: [PATCH 2/4] Deduplicate Base64 validation message --- src/adaptix/_internal/morphing/concrete_provider.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/adaptix/_internal/morphing/concrete_provider.py b/src/adaptix/_internal/morphing/concrete_provider.py index 65f9ad00..31f800ee 100644 --- a/src/adaptix/_internal/morphing/concrete_provider.py +++ b/src/adaptix/_internal/morphing/concrete_provider.py @@ -275,6 +275,7 @@ def provide_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> B64_PATTERN = re.compile(b"[A-Za-z0-9+/]*={0,2}") +BAD_BASE64_STRING = "Bad base64 string" @for_predicate(bytes) @@ -289,17 +290,17 @@ def bytes_base64_loader(data): except AttributeError: raise TypeLoadError(str, data) except UnicodeEncodeError: - raise ValueLoadError("Bad base64 string", data) + raise ValueLoadError(BAD_BASE64_STRING, data) if not B64_PATTERN.fullmatch(encoded): - raise ValueLoadError("Bad base64 string", data) + raise ValueLoadError(BAD_BASE64_STRING, data) if b"=" in encoded: # a2b_base64 accepts leading and excess padding, so reject padding # beyond the amount implied by the unpadded data length first. unpadded = encoded.rstrip(b"=") padding_length = len(encoded) - len(unpadded) if not unpadded or padding_length > -len(unpadded) % 4: - raise ValueLoadError("Bad base64 string", data) + raise ValueLoadError(BAD_BASE64_STRING, data) try: return a2b_base64(encoded) From 686a15acc75fdeea8a0cc1750fc1a1f295b208e1 Mon Sep 17 00:00:00 2001 From: gaoflow Date: Sat, 12 Sep 2026 16:47:07 +0200 Subject: [PATCH 3/4] Narrow Base64 loader fix to Unicode errors --- .../changelog/fragments/+strict_base64.bugfix.rst | 2 +- .../_internal/morphing/concrete_provider.py | 12 ++---------- tests/unit/morphing/test_concrete_provider.py | 15 --------------- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/docs/changelog/fragments/+strict_base64.bugfix.rst b/docs/changelog/fragments/+strict_base64.bugfix.rst index 9e13e2bf..905c6e6d 100644 --- a/docs/changelog/fragments/+strict_base64.bugfix.rst +++ b/docs/changelog/fragments/+strict_base64.bugfix.rst @@ -1 +1 @@ -Reject leading and excess padding when loading Base64-encoded bytes-like values. +Report non-ASCII Base64 input as a loader error for bytes-like values. diff --git a/src/adaptix/_internal/morphing/concrete_provider.py b/src/adaptix/_internal/morphing/concrete_provider.py index 31f800ee..ea1c4a5f 100644 --- a/src/adaptix/_internal/morphing/concrete_provider.py +++ b/src/adaptix/_internal/morphing/concrete_provider.py @@ -275,7 +275,6 @@ def provide_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> B64_PATTERN = re.compile(b"[A-Za-z0-9+/]*={0,2}") -BAD_BASE64_STRING = "Bad base64 string" @for_predicate(bytes) @@ -290,17 +289,10 @@ def bytes_base64_loader(data): except AttributeError: raise TypeLoadError(str, data) except UnicodeEncodeError: - raise ValueLoadError(BAD_BASE64_STRING, data) + raise ValueLoadError("Bad base64 string", data) if not B64_PATTERN.fullmatch(encoded): - raise ValueLoadError(BAD_BASE64_STRING, data) - if b"=" in encoded: - # a2b_base64 accepts leading and excess padding, so reject padding - # beyond the amount implied by the unpadded data length first. - unpadded = encoded.rstrip(b"=") - padding_length = len(encoded) - len(unpadded) - if not unpadded or padding_length > -len(unpadded) % 4: - raise ValueLoadError(BAD_BASE64_STRING, data) + raise ValueLoadError("Bad base64 string", data) try: return a2b_base64(encoded) diff --git a/tests/unit/morphing/test_concrete_provider.py b/tests/unit/morphing/test_concrete_provider.py index 5b77230d..61809142 100644 --- a/tests/unit/morphing/test_concrete_provider.py +++ b/tests/unit/morphing/test_concrete_provider.py @@ -353,9 +353,6 @@ def test_bytes_like_provider( b64_string = b"YWJjZA==" assert get_string(loader(b64_string.decode())) == string - assert get_string(loader("")) == "" - assert get_string(loader("YQ==")) == "a" - assert get_string(loader("YWI=")) == "ab" raises_exc( ValueLoadError("Bad base64 string", "Hello, world"), @@ -380,18 +377,6 @@ def test_bytes_like_provider( lambda: loader("YWJjZA"), ) - for invalid_base64 in ( - "=", - "==", - "AAA==", - "AAAA=", - "AAAA==", - ): - raises_exc( - ValueLoadError("Bad base64 string", invalid_base64), - lambda invalid_base64=invalid_base64: loader(invalid_base64), - ) - raises_exc( TypeLoadError(str, 108), lambda: loader(108), From 67eb34f345ea6f99a81e741d77c74765d92d87f6 Mon Sep 17 00:00:00 2001 From: gaoflow Date: Mon, 5 Oct 2026 16:49:03 +0200 Subject: [PATCH 4/4] Fix ParamSpec bound source expectations --- tests/unit/type_tools/test_normalize_type.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/type_tools/test_normalize_type.py b/tests/unit/type_tools/test_normalize_type.py index 680c0d46..edf242d4 100644 --- a/tests/unit/type_tools/test_normalize_type.py +++ b/tests/unit/type_tools/test_normalize_type.py @@ -838,7 +838,7 @@ def test_forward_ref_at_type_var_limit(): P_FR1, NormParamSpec( P_FR1, - limit=Bound(nt_zero(int, source=ForwardRef("int"))), + limit=Bound(nt_zero(int, source=P_FR1.__bound__)), source=P_FR1, default=None, ), @@ -847,7 +847,7 @@ def test_forward_ref_at_type_var_limit(): P_FR2, NormParamSpec( P_FR2, - limit=Bound(nt_zero(MyForwardClass, source=ForwardRef("MyForwardClass"))), + limit=Bound(nt_zero(MyForwardClass, source=P_FR2.__bound__)), source=P_FR2, default=None, ),