Skip to content

Fix malformed Unicode error constructors - #8497

Open
youknowone wants to merge 2 commits into
RustPython:mainfrom
youknowone:agent/fix-unicode-error-constructors
Open

Fix malformed Unicode error constructors#8497
youknowone wants to merge 2 commits into
RustPython:mainfrom
youknowone:agent/fix-unicode-error-constructors

Conversation

@youknowone

@youknowone youknowone commented Aug 11, 2026

Copy link
Copy Markdown
Member

Fixes #8352.

Summary

  • replace every remaining message-only UnicodeDecodeError and UnicodeEncodeError construction with fully initialized exceptions
  • preserve the encoding, source object, failure range, and reason at CSV, socket, filesystem, array, tkinter, Windows codec, and POSIX call sites
  • remove the malformed helpers and rename the correct decode constructor to new_unicode_decode_error
  • preserve Windows filename surrogates through WTF-8 instead of rejecting them as invalid UTF-8

Root cause and impact

The old macro-generated helpers bypassed Unicode error initialization and created one-argument exceptions. Those exceptions had no encoding, object, start, end, or reason attributes, and their string representation could be empty. All affected paths now produce structurally valid exceptions that error handlers and user code can inspect.

Validation

  • cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi
  • cargo check -p rustpython-stdlib --features tkinter
  • cargo check from crates/capi
  • targeted Windows runtime assertions for array UTF-16 reconstruction, mbcs/oem, nt path decoding, and the original CSV reproduction
  • pre-commit hooks (rustfmt, cspell, merge-conflict and patch checks)

The standalone C-API cargo test reaches the link step but cannot run in this checkout because its Windows configuration references the unavailable placeholder library pythonXY.lib; its cargo check passes.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Unicode decoding errors with accurate encodings, byte ranges, offsets, and offending data.
    • UTF-16 conversion failures now identify malformed surrogate sequences more precisely.
    • Windows code-page encoding errors now respect configured error handlers.
    • Windows filename and byte-path handling preserves surrogate-containing data instead of rejecting it.
    • Hostname, login, locale, socket, CSV, and environment decoding failures now provide more consistent error details.
    • Invalid array and wide-character conversions now report more appropriate exception types.

Replace message-only Unicode decode and encode errors with fully initialized exceptions, preserve source objects and failure ranges, and remove the obsolete constructors.

Assisted-by: Codex:gpt-5
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Unicode error handling

Layer / File(s) Summary
Replace malformed Unicode error helpers
crates/vm/src/vm/vm_new.rs, crates/capi/src/pyerrors.rs, crates/stdlib/src/csv.rs, crates/stdlib/src/socket.rs, crates/vm/src/...
The structured Unicode decode constructor replaces the malformed helper. Direct call sites now use the renamed constructor.
Preserve decoding failure details
crates/host_env/src/posix.rs, crates/stdlib/src/array.rs, crates/stdlib/src/tkinter.rs, crates/vm/src/stdlib/posix.rs
Decoding paths retain invalid bytes, offsets, encodings, and failure reasons. UTF-16 validation reports malformed surrogate sequences.
Update Windows codecs
crates/vm/src/stdlib/_codecs.rs
MBCS and OEM conversions use structured UTF-16 decode errors and configured encode error handlers.
Preserve Windows path data
crates/vm/src/stdlib/nt.rs
Windows byte-path handling uses detailed UTF-8 errors and WTF-8 conversion for surrogate-containing paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: z-ca-2026

Suggested reviewers: shaharnaveh

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes migrate affected decode and encode error paths, remove obsolete helpers, and preserve required Unicode error details across the listed modules.
Out of Scope Changes check ✅ Passed All summarized changes support the linked issue by correcting Unicode error construction or preserving required byte and surrogate data in affected paths.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: correcting malformed Unicode error constructors across the codebase.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Assisted-by: Codex:gpt-5
@github-actions

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] lib: cpython/Lib/codecs.py
[x] test: cpython/Lib/test/test_charmapcodec.py
[ ] test: cpython/Lib/test/test_codeccallbacks.py (TODO: 9)
[x] test: cpython/Lib/test/test_codecencodings_cn.py (TODO: 4)
[x] test: cpython/Lib/test/test_codecencodings_hk.py (TODO: 1)
[x] test: cpython/Lib/test/test_codecencodings_iso2022.py (TODO: 5)
[x] test: cpython/Lib/test/test_codecencodings_jp.py (TODO: 7)
[x] test: cpython/Lib/test/test_codecencodings_kr.py (TODO: 3)
[x] test: cpython/Lib/test/test_codecencodings_tw.py (TODO: 1)
[ ] test: cpython/Lib/test/test_codecmaps_cn.py (TODO: 3)
[ ] test: cpython/Lib/test/test_codecmaps_hk.py (TODO: 1)
[ ] test: cpython/Lib/test/test_codecmaps_jp.py (TODO: 6)
[ ] test: cpython/Lib/test/test_codecmaps_kr.py (TODO: 3)
[ ] test: cpython/Lib/test/test_codecmaps_tw.py (TODO: 3)
[ ] test: cpython/Lib/test/test_codecs.py (TODO: 12)
[ ] test: cpython/Lib/test/test_multibytecodec.py (TODO: 1)
[x] test: cpython/Lib/test/testcodec.py

dependencies:

  • codecs

dependent tests: (161 tests)

  • codecs: test_charmapcodec test_codeccallbacks test_codecs test_eof test_exceptions test_importlib test_inspect test_io test_json test_locale test_logging test_multibytecodec test_os test_pdb test_plistlib test_sax test_str test_sys
    • encodings: test_pydoc
      • locale: test__locale test_builtin test_c_locale_coercion test_calendar test_decimal test_float test_format test_re test_regrtest test_strftime test_strptime test_types test_utf8_mode
    • json: test_embed test_pyrepl test_subprocess test_sysconfig test_tomllib test_tools test_traceback test_zoneinfo
      • importlib.metadata: test_importlib
      • multiprocessing.resource_tracker: test_concurrent_futures
    • pickle: test_annotationlib test_argparse test_array test_ast test_asyncio test_bool test_bytes test_bz2 test_collections test_concurrent_futures test_configparser test_coroutines test_csv test_ctypes test_defaultdict test_deque test_descr test_dict test_dictviews test_email test_enum test_enumerate test_fractions test_functools test_generators test_genericalias test_http_cookies test_ipaddress test_iter test_itertools test_list test_lzma test_memoryio test_memoryview test_minidom test_opcache test_operator test_ordered_dict test_pathlib test_pickle test_picklebuffer test_pickletools test_platform test_positional_only_arg test_posix test_random test_range test_set test_shelve test_slice test_socket test_statistics test_string test_structseq test_super test_time test_trace test_tuple test_turtle test_type_aliases test_type_params test_typing test_unittest test_uuid test_xml_dom_minicompat test_xml_etree test_xpickle test_zipfile test_zlib test_zoneinfo
      • tracemalloc: test_tracemalloc
    • plistlib:
      • platform: test__osx_support test_asyncio test_baseexception test_cmath test_ctypes test_fcntl test_math test_mimetypes test_shutil test_ssl test_winreg test_wsgiref
    • tokenize: test_linecache test_peg_generator test_tabnanny test_tokenize test_unparse
      • inspect: test_abc test_asyncgen test_buffer test_clinic test_code test_grammar test_monitoring test_ntpath test_patma test_posixpath test_signal test_sqlite3 test_type_annotations test_yield_from test_zipimport test_zipimport_support
      • linecache: test_bdb
      • traceback: test_asyncio test_code_module test_contextlib test_contextlib_async test_dictcomps test_http_cookiejar test_importlib test_listcomps test_pyexpat test_setcomps test_threadedtempfile test_threading test_unittest test_with

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@youknowone
youknowone marked this pull request as ready for review August 12, 2026 01:39
@youknowone

Copy link
Copy Markdown
Member Author

@devyubin Could you please review this patch?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/vm/src/stdlib/nt.rs`:
- Around line 52-70: Update utf8_from_bytes to decode Windows paths using
validated WTF-8 with surrogatepass semantics, allowing valid surrogate byte
sequences such as ED A0 80 to be preserved. Keep malformed-byte validation and
the existing error ranges and reasons unchanged, so _path_splitroot_ex and
_path_normpath continue returning Unicode decode errors for invalid input.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: fdefb466-99b4-4c36-98e1-7116a2e94fe8

📥 Commits

Reviewing files that changed from the base of the PR and between 81df1ff and 60c4100.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_codeccallbacks.py is excluded by !Lib/**
📒 Files selected for processing (13)
  • crates/capi/src/pyerrors.rs
  • crates/host_env/src/posix.rs
  • crates/stdlib/src/array.rs
  • crates/stdlib/src/csv.rs
  • crates/stdlib/src/socket.rs
  • crates/stdlib/src/tkinter.rs
  • crates/vm/src/codecs.rs
  • crates/vm/src/function/fspath.rs
  • crates/vm/src/stdlib/_codecs.rs
  • crates/vm/src/stdlib/nt.rs
  • crates/vm/src/stdlib/os.rs
  • crates/vm/src/stdlib/posix.rs
  • crates/vm/src/vm/vm_new.rs

Comment on lines +52 to +70
fn utf8_from_bytes<'a>(bytes: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a str> {
core::str::from_utf8(bytes).map_err(|err| {
let reason = match err.error_len() {
None => "unexpected end of data",
Some(_) => match bytes[err.valid_up_to()] {
0xc2..=0xf4 => "invalid continuation byte",
_ => "invalid start byte",
},
};
vm.new_unicode_decode_error(
vm.ctx.new_str("utf-8"),
vm.ctx.new_bytes(bytes.to_vec()),
err.valid_up_to(),
err.error_len()
.map_or(bytes.len(), |len| err.valid_up_to() + len),
vm.ctx.new_str(reason),
)
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline crates/vm/src/stdlib/nt.rs --items all --type function
rg -n -C 4 'fn utf8_from_bytes|from_utf8|Wtf8Buf|_path_splitroot_ex|_path_normpath' \
  crates/vm/src/stdlib/nt.rs
rg -n -C 3 --glob '*.rs' '_path_splitroot_ex|_path_normpath|surrogatepass|Wtf8Buf' crates

Repository: RustPython/RustPython

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- nt.rs relevant implementation ---'
sed -n '45,75p;680,750p;900,940p' crates/vm/src/stdlib/nt.rs

printf '%s\n' '--- WTF-8 constructors and validation ---'
rg -n -C 5 'from_bytes|is_utf8|encode_wide|decode_utf16|Wtf8Buf' crates/wtf8/src/lib.rs crates/host/src crates/common/src 2>/dev/null | head -n 240

printf '%s\n' '--- Windows path argument and test coverage ---'
rg -n -C 5 '_path_splitroot_ex|_path_normpath|splitroot|normpath|invalid continuation byte|invalid start byte' --glob '*.rs' --glob '*.py' --glob '*.toml' . | head -n 320

printf '%s\n' '--- relevant package metadata ---'
rg -n 'rustpython-wtf8|rustpython_common|windows' crates/vm/Cargo.toml crates/wtf8/Cargo.toml Cargo.toml

Repository: RustPython/RustPython

Length of output: 19375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '45,75p;680,750p;900,940p' crates/vm/src/stdlib/nt.rs
rg -n -C 5 'from_bytes|is_utf8|encode_wide|decode_utf16|Wtf8Buf' crates/wtf8/src/lib.rs crates/host/src crates/common/src 2>/dev/null | head -n 240
rg -n -C 5 '_path_splitroot_ex|_path_normpath|splitroot|normpath|invalid continuation byte|invalid start byte' --glob '*.rs' --glob '*.py' --glob '*.toml' . | head -n 320
rg -n 'rustpython-wtf8|rustpython_common|windows' crates/vm/Cargo.toml crates/wtf8/Cargo.toml Cargo.toml

Repository: RustPython/RustPython

Length of output: 19295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact WTF-8 validator ---'
sed -n '1,220p' crates/wtf8/src/lib.rs
rg -n -C 8 'fn from_bytes|from_bytes\(' crates/wtf8 crates | head -n 180

printf '%s\n' '--- path API tests and fixtures ---'
rg -n -C 8 '_path_splitroot_ex|_path_normpath|splitroot|normpath' . \
  -g '*.rs' -g '*.py' -g '*.toml' -g '*.md' | head -n 360

printf '%s\n' '--- CPython Windows filesystem decoding references ---'
curl -L --fail --silent --show-error \
  https://raw.githubusercontent.com/python/cpython/main/Modules/posixmodule.c |
  rg -n -C 6 'DecodeFSDefault|splitroot|normpath|surrogatepass' | head -n 220

Repository: RustPython/RustPython

Length of output: 45547


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' crates/wtf8/src/lib.rs
rg -n -C 8 'fn from_bytes|from_bytes\(' crates/wtf8 crates | head -n 180
rg -n -C 8 '_path_splitroot_ex|_path_normpath|splitroot|normpath' . \
  -g '*.rs' -g '*.py' -g '*.toml' -g '*.md' | head -n 360
curl -L --fail --silent --show-error \
  https://raw.githubusercontent.com/python/cpython/main/Modules/posixmodule.c |
  rg -n -C 6 'DecodeFSDefault|splitroot|normpath|surrogatepass' | head -n 220

Repository: RustPython/RustPython

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- WTF-8 surrogate decoder ---'
sed -n '700,805p' crates/wtf8/src/lib.rs

printf '%s\n' '--- CPython filesystem codec policy ---'
curl -L --fail --silent --show-error \
  https://raw.githubusercontent.com/python/cpython/main/Objects/unicodeobject.c |
  rg -n -C 8 'filesystem|surrogatepass|PyUnicode_DecodeFSDefault|_PyUnicode_DecodeUTF8Stateful' |
  head -n 260

printf '%s\n' '--- standalone WTF-8 behavior probe ---'
python3 - <<'PY'
def strict_utf8(data):
    try:
        return ("ok", data.decode("utf-8"))
    except UnicodeDecodeError as e:
        return ("error", e.start, e.end, e.reason)

def wtf8_valid(data):
    i = 0
    while i < len(data):
        try:
            data[i:].decode("utf-8")
            return True
        except UnicodeDecodeError as e:
            pos = i + e.start
            # RustPython's Wtf8::from_bytes accepts exactly a 3-byte
            # UTF-8 encoding of one UTF-16 surrogate at the failure point.
            if pos + 3 > len(data):
                return False
            b0, b1, b2 = data[pos:pos + 3]
            if not (b0 == 0xED and 0xA0 <= b1 <= 0xBF and 0x80 <= b2 <= 0xBF):
                return False
            i = pos + 3
    return True

cases = {
    "ascii": b"a",
    "valid_utf8": "é".encode(),
    "lone_high_surrogate": b"\xed\xa0\x80",
    "lone_low_surrogate": b"\xed\xb0\x80",
    "paired_surrogates": b"\xed\xa0\x80\xed\xb0\x80",
    "bad_continuation": b"\xc2A",
    "truncated": b"\xe2\x82",
    "overlong": b"\xc0\x80",
}
for name, data in cases.items():
    print(name, strict_utf8(data), "wtf8_valid=", wtf8_valid(data))
PY

Repository: RustPython/RustPython

Length of output: 12979


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '700,805p' crates/wtf8/src/lib.rs
curl -L --fail --silent --show-error \
  https://raw.githubusercontent.com/python/cpython/main/Objects/unicodeobject.c |
  rg -n -C 8 'filesystem|surrogatepass|PyUnicode_DecodeFSDefault|_PyUnicode_DecodeUTF8Stateful' |
  head -n 260

python3 - <<'PY'
def strict_utf8(data):
    try:
        return ("ok", data.decode("utf-8"))
    except UnicodeDecodeError as e:
        return ("error", e.start, e.end, e.reason)

def wtf8_valid(data):
    i = 0
    while i < len(data):
        try:
            data[i:].decode("utf-8")
            return True
        except UnicodeDecodeError as e:
            pos = i + e.start
            if pos + 3 > len(data):
                return False
            b0, b1, b2 = data[pos:pos + 3]
            if not (b0 == 0xED and 0xA0 <= b1 <= 0xBF and 0x80 <= b2 <= 0xBF):
                return False
            i = pos + 3
    return True

cases = {
    "ascii": b"a",
    "valid_utf8": "é".encode(),
    "lone_high_surrogate": b"\xed\xa0\x80",
    "lone_low_surrogate": b"\xed\xb0\x80",
    "paired_surrogates": b"\xed\xa0\x80\xed\xb0\x80",
    "bad_continuation": b"\xc2A",
    "truncated": b"\xe2\x82",
    "overlong": b"\xc0\x80",
}
for name, data in cases.items():
    print(name, strict_utf8(data), "wtf8_valid=", wtf8_valid(data))
PY

Repository: RustPython/RustPython

Length of output: 12867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- surrogate validation bounds ---'
sed -n '785,845p' crates/wtf8/src/lib.rs

printf '%s\n' '--- Windows filesystem defaults in CPython ---'
curl -L --fail --silent --show-error \
  https://raw.githubusercontent.com/python/cpython/main/Python/initconfig.c |
  rg -n -C 8 'filesystem_errors|surrogatepass|utf-8' | head -n 240

printf '%s\n' '--- current helper error mapping ---'
nl -ba crates/vm/src/stdlib/nt.rs | sed -n '52,72p'

Repository: RustPython/RustPython

Length of output: 8670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '785,845p' crates/wtf8/src/lib.rs
curl -L --fail --silent --show-error \
  https://raw.githubusercontent.com/python/cpython/main/Python/initconfig.c |
  rg -n -C 8 'filesystem_errors|surrogatepass|utf-8' | head -n 240
nl -ba crates/vm/src/stdlib/nt.rs | sed -n '52,72p'

Repository: RustPython/RustPython

Length of output: 8549


Decode Windows byte paths with WTF-8 semantics.

utf8_from_bytes rejects valid WTF-8 such as b"\xED\xA0\x80". Both _path_splitroot_ex and _path_normpath therefore fail before preserving the surrogate. Use validated WTF-8 decoding with Windows surrogatepass behavior, while retaining the current malformed-byte ranges and reasons.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm/src/stdlib/nt.rs` around lines 52 - 70, Update utf8_from_bytes to
decode Windows paths using validated WTF-8 with surrogatepass semantics,
allowing valid surrogate byte sequences such as ED A0 80 to be preserved. Keep
malformed-byte validation and the existing error ranges and reasons unchanged,
so _path_splitroot_ex and _path_normpath continue returning Unicode decode
errors for invalid input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace malformed new_unicode_decode_error / new_unicode_encode_error helpers with the _real variants

1 participant