From be43bb6dbf22d7de41d8630a6691ccf773eaf8df Mon Sep 17 00:00:00 2001 From: Changjoon Date: Sun, 26 Apr 2026 12:31:54 +0900 Subject: [PATCH] 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) --- crates/stdlib/src/pystruct.rs | 38 +++++++++++++++++++++------ extra_tests/snippets/stdlib_struct.py | 16 +++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/crates/stdlib/src/pystruct.rs b/crates/stdlib/src/pystruct.rs index 9c5d67f39..8cf1023c8 100644 --- a/crates/stdlib/src/pystruct.rs +++ b/crates/stdlib/src/pystruct.rs @@ -26,20 +26,42 @@ pub(crate) mod _struct { impl TryFromObject for IntoStructFormatBytes { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - // 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)) } } diff --git a/extra_tests/snippets/stdlib_struct.py b/extra_tests/snippets/stdlib_struct.py index 3f97f2519..34bb5cc04 100644 --- a/extra_tests/snippets/stdlib_struct.py +++ b/extra_tests/snippets/stdlib_struct.py @@ -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")