@@ -49,7 +49,7 @@ export class StreamJSON {
4949 this . parseEscape ( c , chunk [ i ] )
5050 break
5151 case State . IN_STRING_UNICODE :
52- this . parseUnicode ( chunk [ i ] )
52+ this . parseUnicode ( c , chunk [ i ] )
5353 break
5454 case State . IN_NUMBER :
5555 this . parseNumber ( c , chunk [ i ] )
@@ -162,10 +162,13 @@ export class StreamJSON {
162162 }
163163 }
164164
165- private parseUnicode ( ch : string ) : void {
165+ private parseUnicode ( c : number , ch : string ) : void {
166166 if ( ! HEX . test ( ch ) ) {
167167 this . emitError ( `Invalid hex digit in unicode escape: ${ ch } ` )
168+ this . uniAccum = ''
169+ this . uniCount = 0
168170 this . state = State . IN_STRING
171+ this . parseString ( c , ch )
169172 return
170173 }
171174 this . uniAccum += ch
@@ -193,13 +196,6 @@ export class StreamJSON {
193196
194197 private parseNumber ( c : number , ch : string ) : void {
195198 if ( c >= 0x30 && c <= 0x39 ) {
196- if ( ( this . numBuf === '0' || this . numBuf === '-0' ) && c >= 0x30 && c <= 0x39 ) {
197- // leading zero followed by another digit — emit error, end the 0, reprocess
198- this . emitError ( `Leading zero in number: ${ this . numBuf } ${ ch } ` )
199- this . endNumber ( )
200- this . reprocessAfterNumber ( c , ch )
201- return
202- }
203199 this . numBuf += ch
204200 } else if ( c === 0x2E || c === 0x65 || c === 0x45 ) {
205201 this . numBuf += ch
@@ -317,11 +313,15 @@ export class StreamJSON {
317313 if ( this . stack . length > 0 && this . stack [ this . stack . length - 1 ] . type === 0 ) {
318314 this . endContainer ( )
319315 this . afterValue ( )
316+ } else {
317+ this . emitError ( 'Unexpected }' )
320318 }
321319 } else if ( c === 0x5D ) { // ]
322320 if ( this . stack . length > 0 && this . stack [ this . stack . length - 1 ] . type === 1 ) {
323321 this . endContainer ( )
324322 this . afterValue ( )
323+ } else {
324+ this . emitError ( 'Unexpected ]' )
325325 }
326326 } else if ( c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D ) {
327327 // whitespace
@@ -349,6 +349,8 @@ export class StreamJSON {
349349 this . parseExpectValue ( c , ch )
350350 }
351351 }
352+ } else {
353+ this . emitError ( `Unexpected character: ${ ch } ` )
352354 }
353355 }
354356
@@ -392,27 +394,30 @@ export class StreamJSON {
392394 private endNumber ( ) : void {
393395 const buf = this . numBuf
394396 this . numBuf = ''
395- const val = Number ( buf )
396- if ( Number . isNaN ( val ) || ! Number . isFinite ( val ) ) {
397- this . emitError ( `Invalid number: ${ buf } ` )
398- // assign null to preserve position — prevents key drops (objects), index shifts (arrays), and undefined root
399- if ( this . stack . length > 0 ) {
400- const top = this . stack [ this . stack . length - 1 ]
401- if ( ( top . type === 0 && top . key !== null ) || top . type === 1 ) {
402- this . assignValue ( null )
403- }
404- } else {
405- this . assignValue ( null )
406- }
397+ const analysis = this . analyzeNumber ( buf )
398+ if ( analysis . kind === 'leading_zero' ) {
399+ this . emitError ( `Leading zero in number: ${ buf } ` )
400+ this . assignInvalidNumber ( )
407401 this . afterValue ( )
408402 return
409403 }
410- if ( buf . length > 1 && buf [ 0 ] === '0' && buf [ 1 ] >= '0' && buf [ 1 ] <= '9' ) {
411- this . emitError ( `Leading zero in number: ${ buf } ` )
404+ if ( analysis . kind === 'invalid' ) {
405+ this . emitError ( `Invalid number: ${ buf } ` )
406+ this . assignInvalidNumber ( )
407+ this . afterValue ( )
408+ return
412409 }
413- if ( buf . endsWith ( '.' ) ) {
410+ const valueText = analysis . kind === 'trailing_dot' ? buf . slice ( 0 , - 1 ) : buf
411+ if ( analysis . kind === 'trailing_dot' ) {
414412 this . emitError ( `Trailing dot in number: ${ buf } ` )
415413 }
414+ const val = Number ( valueText )
415+ if ( Number . isNaN ( val ) || ! Number . isFinite ( val ) ) {
416+ this . emitError ( `Invalid number: ${ buf } ` )
417+ this . assignInvalidNumber ( )
418+ this . afterValue ( )
419+ return
420+ }
416421 this . assignValue ( val )
417422 this . emit ( 'value' , this . currentPath ( ) , val , true )
418423 this . afterValue ( )
@@ -477,6 +482,55 @@ export class StreamJSON {
477482 }
478483 }
479484
485+ private analyzeNumber ( buf : string ) : { kind : 'valid' | 'trailing_dot' | 'leading_zero' | 'invalid' } {
486+ let i = 0
487+ const len = buf . length
488+
489+ if ( len === 0 ) return { kind : 'invalid' }
490+
491+ if ( buf [ i ] === '-' ) i ++
492+ if ( i === len ) return { kind : 'invalid' }
493+
494+ if ( buf [ i ] === '0' ) {
495+ i ++
496+ if ( i < len && buf [ i ] >= '0' && buf [ i ] <= '9' ) return { kind : 'leading_zero' }
497+ } else if ( buf [ i ] >= '1' && buf [ i ] <= '9' ) {
498+ i ++
499+ while ( i < len && buf [ i ] >= '0' && buf [ i ] <= '9' ) i ++
500+ } else {
501+ return { kind : 'invalid' }
502+ }
503+
504+ if ( i < len && buf [ i ] === '.' ) {
505+ i ++
506+ if ( i === len ) return { kind : 'trailing_dot' }
507+ if ( buf [ i ] < '0' || buf [ i ] > '9' ) return { kind : 'invalid' }
508+ while ( i < len && buf [ i ] >= '0' && buf [ i ] <= '9' ) i ++
509+ }
510+
511+ if ( i < len && ( buf [ i ] === 'e' || buf [ i ] === 'E' ) ) {
512+ i ++
513+ if ( i < len && ( buf [ i ] === '+' || buf [ i ] === '-' ) ) i ++
514+ if ( i === len ) return { kind : 'invalid' }
515+ if ( buf [ i ] < '0' || buf [ i ] > '9' ) return { kind : 'invalid' }
516+ while ( i < len && buf [ i ] >= '0' && buf [ i ] <= '9' ) i ++
517+ }
518+
519+ return i === len ? { kind : 'valid' } : { kind : 'invalid' }
520+ }
521+
522+ private assignInvalidNumber ( ) : void {
523+ // Preserve object keys and array indexes when a malformed number cannot be recovered.
524+ if ( this . stack . length > 0 ) {
525+ const top = this . stack [ this . stack . length - 1 ]
526+ if ( ( top . type === 0 && top . key !== null ) || top . type === 1 ) {
527+ this . assignValue ( null )
528+ }
529+ } else {
530+ this . assignValue ( null )
531+ }
532+ }
533+
480534 private flush ( ) : void {
481535 if ( this . state === State . IN_STRING || this . state === State . IN_STRING_ESCAPE || this . state === State . IN_STRING_UNICODE ) {
482536 this . flushHighSurrogate ( )
0 commit comments