@@ -96,7 +96,7 @@ impl JsonRepairer {
9696 . and_then ( |idx| self . prev_non_whitespace_index ( idx) ) ;
9797 let prev_char = prev_non_ws. and_then ( |idx| self . peek_at ( idx) ) ;
9898
99- if prev_char == Some ( ',' ) {
99+ if prev_char == Some ( ',' ) && stop_at_index != prev_non_ws {
100100 // {"a":"b,c,"d":"e"} -> stop at comma before quote.
101101 self . pos = input_start;
102102 self . output . truncate ( output_start) ;
@@ -116,9 +116,12 @@ impl JsonRepairer {
116116 self . pos = quote_pos + 1 ;
117117 } else if stop_at_delimiter && chars:: is_unquoted_string_delimiter ( c) {
118118 // URL like "https://..." should not stop at '/'.
119- if self . pos > input_start + 1
120- && self . peek_at ( self . pos . saturating_sub ( 1 ) ) == Some ( ':' )
121- && self . looks_like_url_start ( input_start + 1 , self . pos )
119+ if c == '/'
120+ && self . pos > input_start + 1
121+ && self . peek_at ( self . pos - 1 ) == Some ( ':' )
122+ && ( self . looks_like_url_start ( input_start + 1 , self . pos )
123+ || ( self . pos + 1 == self . chars . len ( )
124+ && self . ends_with_url_scheme ( input_start + 1 , self . pos ) ) )
122125 {
123126 while self . peek ( ) . is_some_and ( chars:: is_url_char) {
124127 self . output . push ( self . chars [ self . pos ] ) ;
@@ -173,6 +176,47 @@ impl JsonRepairer {
173176 }
174177
175178 if digits == 4 {
179+ let code_unit = self . hex_quad ( self . pos + 1 ) . ok_or_else ( || {
180+ self . error_at_kind (
181+ "Invalid unicode escape" ,
182+ backslash_pos,
183+ JsonRepairErrorKind :: InvalidUnicode ,
184+ )
185+ } ) ?;
186+ if ( 0xD800 ..=0xDBFF ) . contains ( & code_unit) {
187+ let next = self . pos + 5 ;
188+ let low = if self . peek_at ( next) == Some ( '\\' )
189+ && self . peek_at ( next + 1 ) == Some ( 'u' )
190+ {
191+ self . hex_quad ( next + 2 )
192+ } else {
193+ None
194+ } ;
195+ if !low. is_some_and ( |unit| ( 0xDC00 ..=0xDFFF ) . contains ( & unit) ) {
196+ return Err ( self . error_at_kind (
197+ "Invalid unicode surrogate pair" ,
198+ backslash_pos,
199+ JsonRepairErrorKind :: InvalidUnicode ,
200+ ) ) ;
201+ }
202+ self . output . push_str ( "\\ u" ) ;
203+ for i in 0 ..4 {
204+ self . output . push ( self . chars [ self . pos + 1 + i] ) ;
205+ }
206+ self . output . push_str ( "\\ u" ) ;
207+ for i in 0 ..4 {
208+ self . output . push ( self . chars [ next + 2 + i] ) ;
209+ }
210+ self . pos += 11 ;
211+ return Ok ( ( ) ) ;
212+ }
213+ if ( 0xDC00 ..=0xDFFF ) . contains ( & code_unit) {
214+ return Err ( self . error_at_kind (
215+ "Invalid unicode surrogate pair" ,
216+ backslash_pos,
217+ JsonRepairErrorKind :: InvalidUnicode ,
218+ ) ) ;
219+ }
176220 self . output . push_str ( "\\ u" ) ;
177221 for i in 0 ..4 {
178222 self . output . push ( self . chars [ self . pos + 1 + i] ) ;
@@ -202,13 +246,22 @@ impl JsonRepairer {
202246 }
203247 _ => {
204248 // Invalid escape: drop '\' and keep char.
205- self . output . push ( esc) ;
249+ self . push_string_char ( esc) ;
206250 self . pos += 1 ;
207251 }
208252 }
209253 Ok ( ( ) )
210254 }
211255
256+ fn hex_quad ( & self , start : usize ) -> Option < u16 > {
257+ let mut value = 0u16 ;
258+ for offset in 0 ..4 {
259+ let digit = self . peek_at ( start + offset) ?. to_digit ( 16 ) ? as u16 ;
260+ value = value * 16 + digit;
261+ }
262+ Some ( value)
263+ }
264+
212265 fn parse_string_char ( & mut self , c : char ) -> Result < ( ) > {
213266 if c >= '\u{0020}' && c != '"' && c != '\\' {
214267 self . output . push ( c) ;
@@ -324,6 +377,13 @@ impl JsonRepairer {
324377 self . pos -= 1 ;
325378 }
326379
380+ // A whitespace-only token has no value. Form feed is trimmed here but
381+ // is not consumed by parse_whitespace_and_comments. Returning false
382+ // prevents the array parser from looping at an unchanged position.
383+ if self . pos == start {
384+ return Ok ( false ) ;
385+ }
386+
327387 // Compare directly on char slice — no String allocation.
328388 if !is_key && self . slice_eq ( start, self . pos , "undefined" ) {
329389 self . output . push_str ( "null" ) ;
@@ -395,6 +455,17 @@ impl JsonRepairer {
395455 || self . matches_at ( start, "irc://" )
396456 }
397457
458+ fn ends_with_url_scheme ( & self , start : usize , slash_idx : usize ) -> bool {
459+ [
460+ "http:" , "https:" , "ftp:" , "mailto:" , "file:" , "data:" , "irc:" ,
461+ ]
462+ . iter ( )
463+ . any ( |scheme| {
464+ slash_idx >= start + scheme. len ( )
465+ && self . slice_eq ( slash_idx - scheme. len ( ) , slash_idx, scheme)
466+ } )
467+ }
468+
398469 fn is_known_wrapper_function ( & self , start : usize , end : usize ) -> bool {
399470 self . slice_starts_with_ignore_ascii_case ( start, end, "callback" )
400471 || self . slice_eq_ignore_ascii_case ( start, end, "cb" )
0 commit comments