Report invalid \uXXXX escape position at the u character - #7676
Conversation
CPython's json decoder reports the position of the `u` specifier when a \uXXXX escape fails to parse, but RustPython was reporting the preceding `\`. For surrogate-pair cases (\uXXXX\uYYYY) the second call was passing char_offset + next_char_i + 1, which lands on the first hex digit of the first escape -- unrelated to the actual failure site. Pass next_char_i (position of the primary `u`) to the primary decode_unicode call, and capture the second `u`'s char index from the next_tuple peek to pass to the surrogate-pair decode_unicode call. Verified: 13 targeted probes across invalid-hex, short, and pair cases now all match CPython positions. test.test_json 214 tests pass with no regressions.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughscanstring now passes the index of the Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
youknowone
left a comment
There was a problem hiding this comment.
was there any test affected by this change?
|
Re-verified against CPython 3.14.4 (RustPython's stated target version): all 13 escape-position probe cases match. Original PR body referenced 3.13.4 because that was the system Python at probe time — for completeness, the error position convention is identical between 3.13 and 3.14 (the validator code in Python/ast.c has been stable for this since at least 3.10). |
Head branch was pushed to by a user without write access
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
extra_tests/snippets/stdlib_json.py (1)
244-257: Optional: add surrogate-pair second-\ucoverage.The PR description highlights the surrogate-pair path (
\uXXXX\uYYYY) where the secondu's position was previously off by one (landing on the first hex digit of the first escape). The two added tests only exercise the primary\upath. Consider adding a case like'"\\uD834\\uXYZW"'and assertinge.pos == 8(the secondu) so the regression coverage matches the full scope of the fix.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@extra_tests/snippets/stdlib_json.py` around lines 244 - 257, Add a test that exercises the surrogate-pair second-`\u` path: call json.loads with the string '"\\uD834\\uXYZW"' and catch json.JSONDecodeError, asserting that e.pos == 8 (the position of the second 'u'); place this alongside the existing invalid-escape tests so the surrogate-pair case (referencing json.loads and json.JSONDecodeError) verifies the regression fix for the second `\u` position.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extra_tests/snippets/stdlib_json.py`:
- Around line 249-250: Replace the insecure "else: assert False, 'expected
JSONDecodeError'" pattern with an explicit raise so the check isn't removed
under -O; change those else branches to "raise AssertionError('expected
JSONDecodeError')" (apply to both occurrences around the JSONDecodeError test
blocks).
---
Nitpick comments:
In `@extra_tests/snippets/stdlib_json.py`:
- Around line 244-257: Add a test that exercises the surrogate-pair second-`\u`
path: call json.loads with the string '"\\uD834\\uXYZW"' and catch
json.JSONDecodeError, asserting that e.pos == 8 (the position of the second
'u'); place this alongside the existing invalid-escape tests so the
surrogate-pair case (referencing json.loads and json.JSONDecodeError) verifies
the regression fix for the second `\u` position.
🪄 Autofix (Beta)
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
Run ID: 56065126-ff7a-4d9e-8ff8-faf24914efee
📒 Files selected for processing (1)
extra_tests/snippets/stdlib_json.py
|
there was no existing cpython test in Lib/test/test_json/ checks the exact "pos" value for invalid \uXXXX escapes — they only regex-match the message text, so this fix doesn't unmask any test so i added a focused regression test in extra_tests/snippets/stdlib_json.py covering both the leading-position case ("\uXYZW" → pos=2) and the offset case ("abc\uZZZZ" → pos=5). Verified byte-identical against CPython 3.14.4 thanks for catching ! |
Summary
json.loadsof a string with an invalid\uXXXXescape reported a decode error position one character earlier than CPython: at the preceding\instead of at theuspecifier. For surrogate-pair paths (\uXXXX\uYYYYwhere the second escape is invalid), the position was off by more — it landed on the first hex digit of the first escape rather than on the secondu.Examples (before / after):
json.loads('"\\uG000"')json.loads('"abc\\uG000"')json.loads('"\\u1234\\uG000"')json.loads('"\\ud83d\\uG000"')json.loads('"\\ud83d\\u"')json.loads('"\\ud83d\\uDE0G"')Fix
In
crates/stdlib/src/json/machinery.rs::scanstring:\uXXXXcall: passchar_offset + next_char_i(position of theu) instead ofchar_offset + char_i(position of the\).u's char index from thenext_tuple()peek and passchar_offset + u2_char_i. The previouschar_offset + next_char_i + 1referred to the first escape's position — unrelated to where the second escape fails.No behavioural change on the success path, on non-
\u\escapeerrors, on unterminated strings, or on lone-surrogate preservation (pair filter unchanged). Decoded output bit-for-bit identical.Verification
Targeted probes (13 cases)
Invalid hex digit at position 1/2/3/4, short escapes (
\u,\u1,\u12,\u123), escape preceded by ASCII, valid-then-invalid, and surrogate-pair error paths. All positions match CPython 3.13.4 after the fix.Test suite
No regressions.
Pre-push
cargo fmt --all --checkcleancargo clippy -p rustpython-stdlib --all-targets -- -D warningscleanprek run --all-files— all hooks passScope
Single file, +7/-4 lines (3 substantive + 4 comment lines). Independent from #7675 (decoder WTF-8 refactor) — this PR touches
machinery::scanstringerror-reporting path only; #7675 touchesjson.rsscanner frontend. Either order of merge is fine.Summary by CodeRabbit
Bug Fixes
Tests