@@ -6,7 +6,7 @@ mod _json {
66 use super :: machinery;
77 use crate :: vm:: {
88 AsObject , Py , PyObjectRef , PyPayload , PyResult , VirtualMachine ,
9- builtins:: { PyBaseExceptionRef , PyStrRef , PyType , PyUtf8StrRef } ,
9+ builtins:: { PyBaseExceptionRef , PyStrRef , PyType } ,
1010 convert:: ToPyResult ,
1111 function:: { IntoFuncArgs , OptionalArg } ,
1212 protocol:: PyIterReturn ,
@@ -91,7 +91,7 @@ mod _json {
9191 impl JsonScanner {
9292 fn parse (
9393 & self ,
94- pystr : PyUtf8StrRef ,
94+ pystr : PyStrRef ,
9595 char_idx : usize ,
9696 byte_idx : usize ,
9797 scan_once : PyObjectRef ,
@@ -115,7 +115,7 @@ mod _json {
115115 // Parse string - pass slice starting after the quote
116116 let ( wtf8_result, chars_consumed, _bytes_consumed) =
117117 machinery:: scanstring ( & wtf8[ byte_idx + 1 ..] , char_idx + 1 , self . strict )
118- . map_err ( |e| py_decode_error ( e, pystr. clone ( ) . into_wtf8 ( ) , vm) ) ?;
118+ . map_err ( |e| py_decode_error ( e, pystr. clone ( ) , vm) ) ?;
119119 let end_char_idx = char_idx + 1 + chars_consumed;
120120 return Ok ( PyIterReturn :: Return (
121121 vm. new_tuple ( ( wtf8_result, end_char_idx) ) . into ( ) ,
@@ -142,11 +142,11 @@ mod _json {
142142 _ => { }
143143 }
144144
145- let s = & pystr . as_str ( ) [ byte_idx..] ;
145+ let rest = & bytes [ byte_idx..] ;
146146
147147 macro_rules! parse_const {
148148 ( $s: literal, $val: expr) => {
149- if s . starts_with( $s) {
149+ if rest . starts_with( $s. as_bytes ( ) ) {
150150 return Ok ( PyIterReturn :: Return (
151151 vm. new_tuple( ( $val, char_idx + $s. len( ) ) ) . into( ) ,
152152 ) ) ;
@@ -158,15 +158,15 @@ mod _json {
158158 parse_const ! ( "true" , true ) ;
159159 parse_const ! ( "false" , false ) ;
160160
161- if let Some ( ( res, len) ) = self . parse_number ( s , vm) {
161+ if let Some ( ( res, len) ) = self . parse_number ( rest , vm) {
162162 return Ok ( PyIterReturn :: Return (
163163 vm. new_tuple ( ( res?, char_idx + len) ) . into ( ) ,
164164 ) ) ;
165165 }
166166
167167 macro_rules! parse_constant {
168168 ( $s: literal) => {
169- if s . starts_with( $s) {
169+ if rest . starts_with( $s. as_bytes ( ) ) {
170170 return Ok ( PyIterReturn :: Return (
171171 vm. new_tuple( (
172172 self . parse_constant. call( ( $s, ) , vm) ?,
@@ -187,28 +187,31 @@ mod _json {
187187 ) ) )
188188 }
189189
190- fn parse_number ( & self , s : & str , vm : & VirtualMachine ) -> Option < ( PyResult , usize ) > {
190+ fn parse_number ( & self , bytes : & [ u8 ] , vm : & VirtualMachine ) -> Option < ( PyResult , usize ) > {
191191 flame_guard ! ( "JsonScanner::parse_number" ) ;
192192 let mut has_neg = false ;
193193 let mut has_decimal = false ;
194194 let mut has_exponent = false ;
195195 let mut has_e_sign = false ;
196196 let mut i = 0 ;
197- for c in s. chars ( ) {
198- match c {
199- '-' if i == 0 => has_neg = true ,
200- n if n. is_ascii_digit ( ) => { }
201- '.' if !has_decimal => has_decimal = true ,
202- 'e' | 'E' if !has_exponent => has_exponent = true ,
203- '+' | '-' if !has_e_sign => has_e_sign = true ,
197+ // JSON numbers are ASCII per RFC 8259 (digits, '-', '+', '.', 'e', 'E'),
198+ // so byte iteration is equivalent to char iteration here.
199+ for & b in bytes {
200+ match b {
201+ b'-' if i == 0 => has_neg = true ,
202+ b'0' ..=b'9' => { }
203+ b'.' if !has_decimal => has_decimal = true ,
204+ b'e' | b'E' if !has_exponent => has_exponent = true ,
205+ b'+' | b'-' if !has_e_sign => has_e_sign = true ,
204206 _ => break ,
205207 }
206208 i += 1 ;
207209 }
208210 if i == 0 || ( i == 1 && has_neg) {
209211 return None ;
210212 }
211- let buf = & s[ ..i] ;
213+ // SAFETY: the loop above accepts only ASCII bytes, so bytes[..i] is valid UTF-8.
214+ let buf = unsafe { core:: str:: from_utf8_unchecked ( & bytes[ ..i] ) } ;
212215 let ret = if has_decimal || has_exponent {
213216 // float
214217 if let Some ( ref parse_float) = self . parse_float {
@@ -228,11 +231,11 @@ mod _json {
228231 /// Returns (parsed_object, end_char_index, end_byte_index).
229232 fn parse_object (
230233 & self ,
231- pystr : PyUtf8StrRef ,
234+ pystr : PyStrRef ,
232235 start_char_idx : usize ,
233236 start_byte_idx : usize ,
234237 scan_once : & PyObjectRef ,
235- memo : & mut HashMap < String , PyStrRef > ,
238+ memo : & mut HashMap < Wtf8Buf , PyStrRef > ,
236239 vm : & VirtualMachine ,
237240 ) -> PyResult < ( PyObjectRef , usize , usize ) > {
238241 flame_guard ! ( "JsonScanner::parse_object" ) ;
@@ -275,18 +278,19 @@ mod _json {
275278 // Parse key string using scanstring with byte slice
276279 let ( key_wtf8, chars_consumed, bytes_consumed) =
277280 machinery:: scanstring ( & wtf8[ byte_idx..] , char_idx, self . strict )
278- . map_err ( |e| py_decode_error ( e, pystr. clone ( ) . into_wtf8 ( ) , vm) ) ?;
281+ . map_err ( |e| py_decode_error ( e, pystr. clone ( ) , vm) ) ?;
279282
280283 char_idx += chars_consumed;
281284 byte_idx += bytes_consumed;
282285
283- // Key memoization - reuse existing key strings
284- let key_str = key_wtf8. to_string ( ) ;
285- let key: PyObjectRef = match memo. get ( & key_str) {
286+ // Key memoization - reuse existing key strings.
287+ // Keyed by Wtf8Buf so lone surrogates in keys (legal per Python str)
288+ // are preserved; using String here would lossy-collapse surrogates to U+FFFD.
289+ let key: PyObjectRef = match memo. get ( & key_wtf8) {
286290 Some ( cached) => cached. clone ( ) . into ( ) ,
287291 None => {
288- let py_key = vm. ctx . new_str ( key_str . clone ( ) ) ;
289- memo. insert ( key_str , py_key. clone ( ) ) ;
292+ let py_key = vm. ctx . new_str ( key_wtf8 . clone ( ) ) ;
293+ memo. insert ( key_wtf8 , py_key. clone ( ) ) ;
290294 py_key. into ( )
291295 }
292296 } ;
@@ -389,11 +393,11 @@ mod _json {
389393 /// Returns (parsed_array, end_char_index, end_byte_index).
390394 fn parse_array (
391395 & self ,
392- pystr : PyUtf8StrRef ,
396+ pystr : PyStrRef ,
393397 start_char_idx : usize ,
394398 start_byte_idx : usize ,
395399 scan_once : & PyObjectRef ,
396- memo : & mut HashMap < String , PyStrRef > ,
400+ memo : & mut HashMap < Wtf8Buf , PyStrRef > ,
397401 vm : & VirtualMachine ,
398402 ) -> PyResult < ( PyObjectRef , usize , usize ) > {
399403 flame_guard ! ( "JsonScanner::parse_array" ) ;
@@ -507,10 +511,10 @@ mod _json {
507511 fn call_scan_once (
508512 & self ,
509513 scan_once : & PyObjectRef ,
510- pystr : PyUtf8StrRef ,
514+ pystr : PyStrRef ,
511515 char_idx : usize ,
512516 byte_idx : usize ,
513- memo : & mut HashMap < String , PyStrRef > ,
517+ memo : & mut HashMap < Wtf8Buf , PyStrRef > ,
514518 vm : & VirtualMachine ,
515519 ) -> PyResult < ( PyObjectRef , usize , usize ) > {
516520 // Recursion guard: parse_object/parse_array recurse into call_scan_once
@@ -521,7 +525,6 @@ mod _json {
521525 vm. with_recursion ( "while decoding a JSON object from a string" , || {
522526 let bytes = pystr. as_bytes ( ) ;
523527 let wtf8 = pystr. as_wtf8 ( ) ;
524- let s = pystr. as_str ( ) ;
525528
526529 let first_byte = match bytes. get ( byte_idx) {
527530 Some ( & b) => b,
@@ -532,11 +535,13 @@ mod _json {
532535
533536 match first_byte {
534537 b'"' => {
535- // String - pass slice starting after the quote
538+ // String - pass slice starting after the quote.
539+ // Feed the Wtf8Buf directly to new_str; routing through
540+ // .to_string() here would lossy-collapse surrogates to U+FFFD.
536541 let ( wtf8_result, chars_consumed, bytes_consumed) =
537542 machinery:: scanstring ( & wtf8[ byte_idx + 1 ..] , char_idx + 1 , self . strict )
538- . map_err ( |e| py_decode_error ( e, pystr. clone ( ) . into_wtf8 ( ) , vm) ) ?;
539- let py_str = vm. ctx . new_str ( wtf8_result. to_string ( ) ) ;
543+ . map_err ( |e| py_decode_error ( e, pystr. clone ( ) , vm) ) ?;
544+ let py_str = vm. ctx . new_str ( wtf8_result) ;
540545 Ok ( (
541546 py_str. into ( ) ,
542547 char_idx + 1 + chars_consumed,
@@ -580,14 +585,14 @@ mod _json {
580585 return Ok ( ( result, char_idx + 9 , byte_idx + 9 ) ) ;
581586 }
582587 // Negative number - numbers are ASCII so len == bytes
583- if let Some ( ( result, len) ) = self . parse_number ( & s [ byte_idx..] , vm) {
588+ if let Some ( ( result, len) ) = self . parse_number ( & bytes [ byte_idx..] , vm) {
584589 return Ok ( ( result?, char_idx + len, byte_idx + len) ) ;
585590 }
586591 Err ( self . make_decode_error ( "Expecting value" , pystr, char_idx, vm) )
587592 }
588593 b'0' ..=b'9' => {
589594 // Positive number - numbers are ASCII so len == bytes
590- if let Some ( ( result, len) ) = self . parse_number ( & s [ byte_idx..] , vm) {
595+ if let Some ( ( result, len) ) = self . parse_number ( & bytes [ byte_idx..] , vm) {
591596 return Ok ( ( result?, char_idx + len, byte_idx + len) ) ;
592597 }
593598 Err ( self . make_decode_error ( "Expecting value" , pystr, char_idx, vm) )
@@ -608,11 +613,11 @@ mod _json {
608613 let end_char_idx: isize = tuple. as_slice ( ) [ 1 ] . try_to_value ( vm) ?;
609614 // For fallback, we need to calculate byte_idx from char_idx
610615 // This is expensive but fallback should be rare
611- let end_byte_idx = s
612- . char_indices ( )
616+ let end_byte_idx = wtf8
617+ . code_point_indices ( )
613618 . nth ( end_char_idx as usize )
614619 . map ( |( i, _) | i)
615- . unwrap_or ( s . len ( ) ) ;
620+ . unwrap_or ( wtf8 . len ( ) ) ;
616621 Ok ( ( value, end_char_idx as usize , end_byte_idx) )
617622 }
618623 Err ( err) if err. fast_isinstance ( vm. ctx . exceptions . stop_iteration ) => {
@@ -629,12 +634,12 @@ mod _json {
629634 fn make_decode_error (
630635 & self ,
631636 msg : & str ,
632- s : PyUtf8StrRef ,
637+ s : PyStrRef ,
633638 pos : usize ,
634639 vm : & VirtualMachine ,
635640 ) -> PyBaseExceptionRef {
636641 let err = machinery:: DecodeError :: new ( msg, pos) ;
637- py_decode_error ( err, s. into_wtf8 ( ) , vm)
642+ py_decode_error ( err, s, vm)
638643 }
639644 }
640645
@@ -645,14 +650,13 @@ mod _json {
645650 return Err ( vm. new_value_error ( "idx cannot be negative" ) ) ;
646651 }
647652 let char_idx = char_idx as usize ;
648- let pystr = pystr. try_into_utf8 ( vm) ?;
649- let s = pystr. as_str ( ) ;
653+ let wtf8 = pystr. as_wtf8 ( ) ;
650654
651655 // Calculate byte index from char index (O(char_idx) but only at entry point)
652656 let byte_idx = if char_idx == 0 {
653657 0
654658 } else {
655- match s . char_indices ( ) . nth ( char_idx) {
659+ match wtf8 . code_point_indices ( ) . nth ( char_idx) {
656660 Some ( ( byte_i, _) ) => byte_i,
657661 None => {
658662 // char_idx is beyond the string length
0 commit comments