From 35fe452153c9acb9277f4da9775e4ec00f962b32 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:06:56 -0300 Subject: [PATCH] Follow CPython's padding rules in a2b_base64 With strict_mode=True the decoder accepted padding CPython rejects: >>> binascii.a2b_base64(b'YWJj=', strict_mode=True) b'abc' It also dropped data in the default mode, because it returned at the first pad that completed a quad: >>> binascii.a2b_base64(b'abc=a') b'i\xb7' # CPython: b'i\xb7\x1a' The loop now counts pads and decides at the end, the way CPython does, so a stray pad is ignored outside strict mode and named as leading, excess or discontinuous inside it. DecodeError::InvalidPadding was dead in this file and now carries the excess padding message. test_base64_strict_mode and test_base64_excess_data were marked as expected failures and pass now. Assisted-by: Claude Code:claude-opus-5 --- Lib/test/test_binascii.py | 2 -- crates/stdlib/src/binascii.rs | 63 ++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_binascii.py b/Lib/test/test_binascii.py index 48631cecec7..82eabf4bb06 100644 --- a/Lib/test/test_binascii.py +++ b/Lib/test/test_binascii.py @@ -117,7 +117,6 @@ def addnoise(line): # empty strings. TBD: shouldn't it raise an exception instead ? self.assertEqual(binascii.a2b_base64(self.type2test(fillers)), b'') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_base64_strict_mode(self): # Test base64 with strict mode on def _assertRegexTemplate(assert_regex: str, data: bytes, non_strict_mode_expected_result: bytes): @@ -175,7 +174,6 @@ def assertExcessPadding(data, non_strict_mode_expected_result: bytes): assertExcessPadding(b'abcd====', b'i\xb7\x1d') assertExcessPadding(b'abcd=====', b'i\xb7\x1d') - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'i' != b'i\xb7' def test_base64_excess_data(self): # Test excess data exceptions def assertExcessData(data, expected): diff --git a/crates/stdlib/src/binascii.rs b/crates/stdlib/src/binascii.rs index d0cdc2148e7..8f73f262e40 100644 --- a/crates/stdlib/src/binascii.rs +++ b/crates/stdlib/src/binascii.rs @@ -291,31 +291,38 @@ mod decl { return Ok(vec![]); } - if strict_mode && b[0] == PAD { - return Err(base64::DecodeError::InvalidByte(0, 61)); - } - let mut decoded: Vec = vec![]; let mut quad_pos = 0; // position in the nibble let mut pads = 0; let mut left_char: u8 = 0; - let mut padding_started = false; for (i, &el) in b.iter().enumerate() { if el == PAD { - padding_started = true; - pads += 1; - if quad_pos >= 2 && quad_pos + pads >= 4 { - if strict_mode && i + 1 < b.len() { - // Represents excess data after padding error - return Err(base64::DecodeError::InvalidLastSymbol(i, PAD)); - } + // A pad that finishes the quad it belongs to is the expected one. + if quad_pos >= 2 && quad_pos + pads <= 4 { + continue; + } - return Ok(decoded); + // RFC 4648 section 3.3 allows a decoder to ignore a pad that + // shows up anywhere else, so only strict mode complains. + if !strict_mode { + continue; } - continue; + if quad_pos == 1 { + // A single data character cannot be padded into a quad, + // and the check after the loop already reports that. + break; + } + + return Err(if quad_pos == 0 && i == 0 { + // Represents leading padding error + base64::DecodeError::InvalidByte(0, PAD) + } else { + // Represents excess padding error + base64::DecodeError::InvalidPadding + }); } let binary_char = BASE64_TABLE[el as usize]; @@ -327,9 +334,14 @@ mod decl { continue; } - if strict_mode && padding_started { - // Represents discontinuous padding error - return Err(base64::DecodeError::InvalidByte(i, PAD)); + if pads > 0 && strict_mode { + return Err(if quad_pos + pads == 4 { + // Represents excess data after padding error + base64::DecodeError::InvalidLastSymbol(i, PAD) + } else { + // Represents discontinuous padding error + base64::DecodeError::InvalidByte(i, PAD) + }); } pads = 0; @@ -361,14 +373,19 @@ mod decl { } } - match quad_pos { - 0 => Ok(decoded), - 1 => Err(base64::DecodeError::InvalidLastSymbol( + if quad_pos == 1 { + // One data character too many: no input encodes to that length. + return Err(base64::DecodeError::InvalidLastSymbol( decoded.len() / 3 * 4 + 1, 0, - )), - _ => Err(base64::DecodeError::InvalidLength(quad_pos)), + )); } + + if quad_pos != 0 && quad_pos + pads < 4 { + return Err(base64::DecodeError::InvalidLength(quad_pos)); + } + + Ok(decoded) }) .map_err(|err| super::Base64DecodeError(err).to_pyexception(vm)) } @@ -864,7 +881,7 @@ impl ToPyException for Base64DecodeError { } // TODO: clean up errors DecodeError::InvalidLength(_) => "Incorrect padding".to_owned(), - DecodeError::InvalidPadding => "Incorrect padding".to_owned(), + DecodeError::InvalidPadding => "Excess padding not allowed".to_owned(), }; new_binascii_error(format!("error decoding base64: {message}"), vm) }