Match CPython error type for non-ASCII struct format arguments (#7681)

* Match CPython error type for non-ASCII struct format arguments

Struct() raised the wrong exception type when the format argument
contained non-ASCII characters:
- str input with non-ASCII char: RustPython raised UnicodeDecodeError
  with an empty message; CPython raises UnicodeEncodeError as if
  format.encode('ascii') had been called directly.
- bytes input with non-ASCII byte: same wrong UnicodeDecodeError;
  CPython passes the bytes through to the format parser, which then
  errors with struct.error("bad char in struct format").

Restructure IntoStructFormatBytes::try_from_object to:
- raise UnicodeEncodeError("ascii", s, start, start+1, "ordinal not
  in range(128)") for non-ASCII str, with start computed as the
  first non-ASCII code point position (matching CPython's natural
  encoding-error format);
- raise struct.error("bad char in struct format") for non-ASCII bytes,
  produced via the existing new_struct_error helper.

Probed byte-identical with CPython 3.14.4 for both cases. Full
test.test_struct (43 tests) passes with no regressions. Sanity-tested
all standard format/pack/unpack/calcsize call shapes remain unchanged.

* Add regression test for non-ASCII format string error types

* Use raise AssertionError instead of assert False (B011)
This commit is contained in:
Changjoon authored and GitHub committed 2026-04-26 12:31:54 +09:00
1 parent 6ab1f806ba
commit be43bb6dbf
2 files changed
+46 -8

No files matched your search

+30 -8
View File
@@ -26,20 +26,42 @@ pub(crate) mod _struct {
impl TryFromObject for IntoStructFormatBytes {
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
// CPython turns str to bytes but we do reversed way here
// The only performance difference is this transition cost
// CPython turns str to bytes (via str.encode('ascii')) but we keep str.
// The error reporting for non-ASCII input still matches CPython:
// - str input with non-ASCII char: UnicodeEncodeError, the same exception
// str.encode('ascii') would produce.
// - bytes input with non-ASCII byte: struct.error("bad char in struct format"),
// matching CPython where bytes are passed through to the format parser.
let fmt = match_class!(match obj {
s @ PyStr => s.isascii().then_some(s),
b @ PyBytes => ascii::AsciiStr::from_ascii(&b)
.ok()
.map(|s| vm.ctx.new_str(s)),
s @ PyStr => {
if !s.isascii() {
let start = s
.as_wtf8()
.code_points()
.position(|cp| !cp.to_char().is_some_and(|c| c.is_ascii()))
.unwrap_or(0);
return Err(vm.new_unicode_encode_error_real(
vm.ctx.new_str("ascii"),
s,
start,
start + 1,
vm.ctx.new_str("ordinal not in range(128)"),
));
}
s
}
b @ PyBytes => {
let ascii_str = ascii::AsciiStr::from_ascii(&b).map_err(|_| {
new_struct_error(vm, "bad char in struct format".to_owned())
})?;
vm.ctx.new_str(ascii_str)
}
other =>
return Err(vm.new_type_error(format!(
"Struct() argument 1 must be a str or bytes object, not {}",
other.class().name()
))),
})
.ok_or_else(|| vm.new_unicode_decode_error("Struct format must be a ascii string"))?;
});
Ok(Self(fmt))
}
}
+16
View File
@@ -76,3 +76,19 @@ assert data == b"\0"
assert struct.error.__module__ == "struct"
assert struct.error.__name__ == "error"
# Non-ASCII format string: error type matches CPython.
# str → UnicodeEncodeError (encoding='ascii')
# bytes → struct.error
try:
struct.Struct("\udc00")
except UnicodeEncodeError as e:
assert e.encoding == "ascii"
else:
raise AssertionError("expected UnicodeEncodeError")
with assert_raises(UnicodeEncodeError):
struct.Struct("한")
with assert_raises(struct.error):
struct.Struct(b"\xff")