Skip to content

Commit 35fe452

Browse files
committed
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
1 parent 3d2ee64 commit 35fe452

2 files changed

Lines changed: 40 additions & 25 deletions

File tree

‎Lib/test/test_binascii.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ def addnoise(line):
117117
# empty strings. TBD: shouldn't it raise an exception instead ?
118118
self.assertEqual(binascii.a2b_base64(self.type2test(fillers)), b'')
119119

120-
@unittest.expectedFailure # TODO: RUSTPYTHON
121120
def test_base64_strict_mode(self):
122121
# Test base64 with strict mode on
123122
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):
175174
assertExcessPadding(b'abcd====', b'i\xb7\x1d')
176175
assertExcessPadding(b'abcd=====', b'i\xb7\x1d')
177176

178-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'i' != b'i\xb7'
179177
def test_base64_excess_data(self):
180178
# Test excess data exceptions
181179
def assertExcessData(data, expected):

‎crates/stdlib/src/binascii.rs‎

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -291,31 +291,38 @@ mod decl {
291291
return Ok(vec![]);
292292
}
293293

294-
if strict_mode && b[0] == PAD {
295-
return Err(base64::DecodeError::InvalidByte(0, 61));
296-
}
297-
298294
let mut decoded: Vec<u8> = vec![];
299295

300296
let mut quad_pos = 0; // position in the nibble
301297
let mut pads = 0;
302298
let mut left_char: u8 = 0;
303-
let mut padding_started = false;
304299
for (i, &el) in b.iter().enumerate() {
305300
if el == PAD {
306-
padding_started = true;
307-
308301
pads += 1;
309-
if quad_pos >= 2 && quad_pos + pads >= 4 {
310-
if strict_mode && i + 1 < b.len() {
311-
// Represents excess data after padding error
312-
return Err(base64::DecodeError::InvalidLastSymbol(i, PAD));
313-
}
302+
// A pad that finishes the quad it belongs to is the expected one.
303+
if quad_pos >= 2 && quad_pos + pads <= 4 {
304+
continue;
305+
}
314306

315-
return Ok(decoded);
307+
// RFC 4648 section 3.3 allows a decoder to ignore a pad that
308+
// shows up anywhere else, so only strict mode complains.
309+
if !strict_mode {
310+
continue;
316311
}
317312

318-
continue;
313+
if quad_pos == 1 {
314+
// A single data character cannot be padded into a quad,
315+
// and the check after the loop already reports that.
316+
break;
317+
}
318+
319+
return Err(if quad_pos == 0 && i == 0 {
320+
// Represents leading padding error
321+
base64::DecodeError::InvalidByte(0, PAD)
322+
} else {
323+
// Represents excess padding error
324+
base64::DecodeError::InvalidPadding
325+
});
319326
}
320327

321328
let binary_char = BASE64_TABLE[el as usize];
@@ -327,9 +334,14 @@ mod decl {
327334
continue;
328335
}
329336

330-
if strict_mode && padding_started {
331-
// Represents discontinuous padding error
332-
return Err(base64::DecodeError::InvalidByte(i, PAD));
337+
if pads > 0 && strict_mode {
338+
return Err(if quad_pos + pads == 4 {
339+
// Represents excess data after padding error
340+
base64::DecodeError::InvalidLastSymbol(i, PAD)
341+
} else {
342+
// Represents discontinuous padding error
343+
base64::DecodeError::InvalidByte(i, PAD)
344+
});
333345
}
334346
pads = 0;
335347

@@ -361,14 +373,19 @@ mod decl {
361373
}
362374
}
363375

364-
match quad_pos {
365-
0 => Ok(decoded),
366-
1 => Err(base64::DecodeError::InvalidLastSymbol(
376+
if quad_pos == 1 {
377+
// One data character too many: no input encodes to that length.
378+
return Err(base64::DecodeError::InvalidLastSymbol(
367379
decoded.len() / 3 * 4 + 1,
368380
0,
369-
)),
370-
_ => Err(base64::DecodeError::InvalidLength(quad_pos)),
381+
));
371382
}
383+
384+
if quad_pos != 0 && quad_pos + pads < 4 {
385+
return Err(base64::DecodeError::InvalidLength(quad_pos));
386+
}
387+
388+
Ok(decoded)
372389
})
373390
.map_err(|err| super::Base64DecodeError(err).to_pyexception(vm))
374391
}
@@ -864,7 +881,7 @@ impl ToPyException for Base64DecodeError {
864881
}
865882
// TODO: clean up errors
866883
DecodeError::InvalidLength(_) => "Incorrect padding".to_owned(),
867-
DecodeError::InvalidPadding => "Incorrect padding".to_owned(),
884+
DecodeError::InvalidPadding => "Excess padding not allowed".to_owned(),
868885
};
869886
new_binascii_error(format!("error decoding base64: {message}"), vm)
870887
}

0 commit comments

Comments
 (0)