Skip to content

Commit 625e5bf

Browse files
Report invalid \uXXXX escape position at the u character (#7676)
* Report invalid \uXXXX escape position at the u character 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. * Add regression test for invalid \uXXXX escape position * Use raise AssertionError instead of assert False (B011)
1 parent a2afaf0 commit 625e5bf

2 files changed

Lines changed: 23 additions & 4 deletions

File tree

crates/stdlib/src/json/machinery.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,17 +231,20 @@ pub fn scanstring<'a>(
231231
'r' => "\r",
232232
't' => "\t",
233233
'u' => {
234-
let mut uni = decode_unicode(&mut chars, char_offset + char_i)?;
234+
// Error position for an invalid \uXXXX escape points at the
235+
// `u`, not the `\` -- matches CPython's json.decoder.
236+
let mut uni = decode_unicode(&mut chars, char_offset + next_char_i)?;
235237
chunk_start = byte_i + 6;
236238
if let Some(lead) = uni.to_lead_surrogate() {
237239
// uni is a surrogate -- try to find its pair
238240
let mut chars2 = chars.clone();
239-
if let Some(((_, (byte_pos2, _)), (_, _))) = chars2
241+
if let Some(((_, (byte_pos2, _)), (u2_char_i, (_, _)))) = chars2
240242
.next_tuple()
241243
.filter(|((_, (_, c1)), (_, (_, c2)))| *c1 == '\\' && *c2 == 'u')
242244
{
243-
let uni2 =
244-
decode_unicode(&mut chars2, char_offset + next_char_i + 1)?;
245+
// u2_char_i is the `u` of the second \uXXXX; same
246+
// position convention as the primary call above.
247+
let uni2 = decode_unicode(&mut chars2, char_offset + u2_char_i)?;
245248
if let Some(trail) = uni2.to_trail_surrogate() {
246249
// ok, we found what we were looking for -- \uXXXX\uXXXX, both surrogates
247250
uni = lead.merge(trail).into();

extra_tests/snippets/stdlib_json.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,19 @@ class Dict(dict):
239239
RecursionError,
240240
lambda: json.loads(('[{"x":' * _deep) + "1" + ("}]" * _deep)),
241241
)
242+
243+
244+
# Invalid \uXXXX escape: error position points at the 'u', matching CPython.
245+
try:
246+
json.loads('"\\uXYZW"')
247+
except json.JSONDecodeError as e:
248+
assert e.pos == 2, f"expected pos=2, got {e.pos}"
249+
else:
250+
raise AssertionError("expected JSONDecodeError")
251+
252+
try:
253+
json.loads('"abc\\uZZZZ"')
254+
except json.JSONDecodeError as e:
255+
assert e.pos == 5, f"expected pos=5, got {e.pos}"
256+
else:
257+
raise AssertionError("expected JSONDecodeError")

0 commit comments

Comments
 (0)