@@ -370,8 +370,9 @@ public static bool TryParseSvgPath(ReadOnlySpan<char> svgPath, [NotNullWhen(true
370370 char ch = svgPath [ 0 ] ;
371371 if ( char . IsDigit ( ch ) || ch == '-' || ch == '+' || ch == '.' )
372372 {
373- // Are we are the end of the string or we are at the end of the path?
374- if ( svgPath . Length == 0 || op == 'Z' )
373+ // SVG allows repeated operand groups to reuse the previous command.
374+ // A leading number is only valid once a drawable command is active.
375+ if ( op is '\0 ' or 'Z' )
375376 {
376377 return false ;
377378 }
@@ -393,107 +394,164 @@ public static bool TryParseSvgPath(ReadOnlySpan<char> svgPath, [NotNullWhen(true
393394 svgPath = TrimSeparator ( svgPath [ 1 ..] ) ;
394395 }
395396
397+ // Read every operand for the command before appending geometry. That keeps
398+ // malformed or truncated data from leaking a partially parsed segment into the path.
396399 switch ( op )
397400 {
398401 case 'M' :
399- svgPath = FindPoint ( svgPath , relative , c , out point1 ) ;
400- builder . MoveTo ( point1 ) ;
402+ if ( ! TryFindPoint ( ref svgPath , relative , c , out point1 ) )
403+ {
404+ return false ;
405+ }
406+
407+ _ = builder . MoveTo ( point1 ) ;
401408 previousOp = '\0 ' ;
409+
410+ // Extra coordinate pairs after a move command are implicit line commands.
402411 op = 'L' ;
403412 c = point1 ;
404413 break ;
405414 case 'L' :
406- svgPath = FindPoint ( svgPath , relative , c , out point1 ) ;
407- builder . LineTo ( point1 ) ;
415+ if ( ! TryFindPoint ( ref svgPath , relative , c , out point1 ) )
416+ {
417+ return false ;
418+ }
419+
420+ _ = builder . LineTo ( point1 ) ;
408421 c = point1 ;
409422 break ;
410423 case 'H' :
411- svgPath = FindScaler ( svgPath , out float x ) ;
424+ if ( ! TryFindScaler ( ref svgPath , out float x ) )
425+ {
426+ return false ;
427+ }
428+
412429 if ( relative )
413430 {
414431 x += c . X ;
415432 }
416433
417- builder . LineTo ( x , c . Y ) ;
434+ if ( ! float . IsFinite ( x ) )
435+ {
436+ return false ;
437+ }
438+
439+ _ = builder . LineTo ( x , c . Y ) ;
418440 c . X = x ;
419441 break ;
420442 case 'V' :
421- svgPath = FindScaler ( svgPath , out float y ) ;
443+ if ( ! TryFindScaler ( ref svgPath , out float y ) )
444+ {
445+ return false ;
446+ }
447+
422448 if ( relative )
423449 {
424450 y += c . Y ;
425451 }
426452
427- builder . LineTo ( c . X , y ) ;
453+ if ( ! float . IsFinite ( y ) )
454+ {
455+ return false ;
456+ }
457+
458+ _ = builder . LineTo ( c . X , y ) ;
428459 c . Y = y ;
429460 break ;
430461 case 'C' :
431- svgPath = FindPoint ( svgPath , relative , c , out point1 ) ;
432- svgPath = FindPoint ( svgPath , relative , c , out point2 ) ;
433- svgPath = FindPoint ( svgPath , relative , c , out point3 ) ;
434- builder . CubicBezierTo ( point1 , point2 , point3 ) ;
462+ if ( ! TryFindPoint ( ref svgPath , relative , c , out point1 )
463+ || ! TryFindPoint ( ref svgPath , relative , c , out point2 )
464+ || ! TryFindPoint ( ref svgPath , relative , c , out point3 ) )
465+ {
466+ return false ;
467+ }
468+
469+ _ = builder . CubicBezierTo ( point1 , point2 , point3 ) ;
435470 lastc = point2 ;
436471 c = point3 ;
437472 break ;
438473 case 'S' :
439- svgPath = FindPoint ( svgPath , relative , c , out point2 ) ;
440- svgPath = FindPoint ( svgPath , relative , c , out point3 ) ;
474+ if ( ! TryFindPoint ( ref svgPath , relative , c , out point2 )
475+ || ! TryFindPoint ( ref svgPath , relative , c , out point3 ) )
476+ {
477+ return false ;
478+ }
479+
441480 point1 = c ;
442481 if ( previousOp is 'C' or 'S' )
443482 {
483+ // Smooth cubic curves mirror the previous cubic control point.
484+ // Without a preceding cubic command, the current point is the control point.
444485 point1 . X -= lastc . X - c . X ;
445486 point1 . Y -= lastc . Y - c . Y ;
446487 }
447488
448- builder . CubicBezierTo ( point1 , point2 , point3 ) ;
489+ _ = builder . CubicBezierTo ( point1 , point2 , point3 ) ;
449490 lastc = point2 ;
450491 c = point3 ;
451492 break ;
452493 case 'Q' : // Quadratic Bezier Curve
453- svgPath = FindPoint ( svgPath , relative , c , out point1 ) ;
454- svgPath = FindPoint ( svgPath , relative , c , out point2 ) ;
455- builder . QuadraticBezierTo ( point1 , point2 ) ;
494+ if ( ! TryFindPoint ( ref svgPath , relative , c , out point1 )
495+ || ! TryFindPoint ( ref svgPath , relative , c , out point2 ) )
496+ {
497+ return false ;
498+ }
499+
500+ _ = builder . QuadraticBezierTo ( point1 , point2 ) ;
456501 lastc = point1 ;
457502 c = point2 ;
458503 break ;
459504 case 'T' :
460- svgPath = FindPoint ( svgPath , relative , c , out point2 ) ;
505+ if ( ! TryFindPoint ( ref svgPath , relative , c , out point2 ) )
506+ {
507+ return false ;
508+ }
509+
461510 point1 = c ;
462511 if ( previousOp is 'Q' or 'T' )
463512 {
513+ // Smooth quadratic curves mirror the previous quadratic control point.
514+ // Without a preceding quadratic command, the current point is the control point.
464515 point1 . X -= lastc . X - c . X ;
465516 point1 . Y -= lastc . Y - c . Y ;
466517 }
467518
468- builder . QuadraticBezierTo ( point1 , point2 ) ;
519+ _ = builder . QuadraticBezierTo ( point1 , point2 ) ;
469520 lastc = point1 ;
470521 c = point2 ;
471522 break ;
472523 case 'A' :
473- if ( TryFindScaler ( ref svgPath , out float radiiX )
474- && TryTrimSeparator ( ref svgPath )
475- && TryFindScaler ( ref svgPath , out float radiiY )
476- && TryTrimSeparator ( ref svgPath )
477- && TryFindScaler ( ref svgPath , out float angle )
478- && TryTrimSeparator ( ref svgPath )
479- && TryFindScaler ( ref svgPath , out float largeArc )
480- && TryTrimSeparator ( ref svgPath )
481- && TryFindScaler ( ref svgPath , out float sweep )
482- && TryFindPoint ( ref svgPath , relative , c , out PointF point ) )
524+ // Arc flags are single SVG grammar tokens, not numbers. Reading them as
525+ // scalars would accept malformed flag/end-point boundaries such as "04445".
526+ if ( ! TryFindScaler ( ref svgPath , out float radiiX )
527+ || ! TryTrimSeparator ( ref svgPath )
528+ || ! TryFindScaler ( ref svgPath , out float radiiY )
529+ || ! TryTrimSeparator ( ref svgPath )
530+ || ! TryFindScaler ( ref svgPath , out float angle )
531+ || ! TryTrimSeparator ( ref svgPath )
532+ || ! TryFindFlag ( ref svgPath , out bool largeArc )
533+ || ! TryTrimSeparator ( ref svgPath )
534+ || ! TryFindFlag ( ref svgPath , out bool sweep )
535+ || ! TryFindPoint ( ref svgPath , relative , c , out PointF point ) )
483536 {
484- builder . ArcTo ( radiiX , radiiY , angle , largeArc == 1 , sweep == 1 , point ) ;
485- c = point ;
537+ return false ;
486538 }
487539
540+ _ = builder . ArcTo ( radiiX , radiiY , angle , largeArc , sweep , point ) ;
541+ c = point ;
488542 break ;
489543 case 'Z' :
490- builder . CloseFigure ( ) ;
544+ _ = builder . CloseFigure ( ) ;
491545 c = first ;
492546 break ;
493547 case '~' :
494- svgPath = FindPoint ( svgPath , relative , c , out point1 ) ;
495- svgPath = FindPoint ( svgPath , relative , c , out point2 ) ;
496- builder . MoveTo ( point1 ) . LineTo ( point2 ) ;
548+ if ( ! TryFindPoint ( ref svgPath , relative , c , out point1 )
549+ || ! TryFindPoint ( ref svgPath , relative , c , out point2 ) )
550+ {
551+ return false ;
552+ }
553+
554+ _ = builder . MoveTo ( point1 ) . LineTo ( point2 ) ;
497555 break ;
498556 default :
499557 return false ;
@@ -511,8 +569,28 @@ public static bool TryParseSvgPath(ReadOnlySpan<char> svgPath, [NotNullWhen(true
511569 return true ;
512570 }
513571
572+ private static bool TryFindFlag ( ref ReadOnlySpan < char > str , out bool value )
573+ {
574+ str = TrimSeparator ( str ) ;
575+
576+ // https://www.w3.org/TR/SVG11/paths.html#PathDataBNF
577+ // flag: "0" | "1"
578+ // Adjacent flags are valid, so this consumes exactly one character.
579+ if ( str . Length == 0 || ( str [ 0 ] is not '0' and not '1' ) )
580+ {
581+ value = default ;
582+ return false ;
583+ }
584+
585+ value = str [ 0 ] == '1' ;
586+ str = str [ 1 ..] ;
587+ return true ;
588+ }
589+
514590 private static bool TryTrimSeparator ( ref ReadOnlySpan < char > str )
515591 {
592+ // SVG separators are optional in places where the next token can be
593+ // recognized unambiguously. Keep this chainable with the operand readers.
516594 ReadOnlySpan < char > result = TrimSeparator ( str ) ;
517595 if ( str [ ^ result . Length ..] . StartsWith ( result ) )
518596 {
@@ -525,11 +603,10 @@ private static bool TryTrimSeparator(ref ReadOnlySpan<char> str)
525603
526604 private static bool TryFindScaler ( ref ReadOnlySpan < char > str , out float value )
527605 {
528- ReadOnlySpan < char > result = FindScaler ( str , out float valueInner ) ;
529- if ( str [ ^ result . Length .. ] . StartsWith ( result ) )
606+ ReadOnlySpan < char > source = TrimSeparator ( str ) ;
607+ if ( TryReadScalar ( source , out value , out int length ) )
530608 {
531- value = valueInner ;
532- str = result ;
609+ str = source [ length ..] ;
533610 return true ;
534611 }
535612
@@ -539,55 +616,52 @@ private static bool TryFindScaler(ref ReadOnlySpan<char> str, out float value)
539616
540617 private static bool TryFindPoint ( ref ReadOnlySpan < char > str , bool relative , PointF current , out PointF value )
541618 {
542- ReadOnlySpan < char > result = FindPoint ( str , relative , current , out PointF valueInner ) ;
543- if ( str [ ^ result . Length ..] . StartsWith ( result ) )
619+ if ( TryFindScaler ( ref str , out float x ) && TryFindScaler ( ref str , out float y ) )
544620 {
545- value = valueInner ;
546- str = result ;
621+ // Relative operands can overflow after adding the current point even when
622+ // each parsed scalar is finite, so validate the absolute result as well.
623+ if ( relative )
624+ {
625+ x += current . X ;
626+ y += current . Y ;
627+ }
628+
629+ if ( ! float . IsFinite ( x ) || ! float . IsFinite ( y ) )
630+ {
631+ value = default ;
632+ return false ;
633+ }
634+
635+ value = new PointF ( x , y ) ;
547636 return true ;
548637 }
549638
550639 value = default ;
551640 return false ;
552641 }
553642
554- private static ReadOnlySpan < char > FindPoint ( ReadOnlySpan < char > str , bool isRelative , PointF relative , out PointF value )
555- {
556- str = FindScaler ( str , out float x ) ;
557- str = FindScaler ( str , out float y ) ;
558- if ( isRelative )
559- {
560- x += relative . X ;
561- y += relative . Y ;
562- }
563-
564- value = new PointF ( x , y ) ;
565- return str ;
566- }
567-
568- private static ReadOnlySpan < char > FindScaler ( ReadOnlySpan < char > str , out float scaler )
643+ private static bool TryReadScalar ( ReadOnlySpan < char > str , out float scaler , out int length )
569644 {
570- str = TrimSeparator ( str ) ;
571- scaler = 0 ;
572-
645+ // SVG path numbers can be tightly packed: "10-20" is two numbers, as is
646+ // "0.5.6". Stop at the first character that belongs to the next token.
573647 bool hasDot = false ;
574648 for ( int i = 0 ; i < str . Length ; i ++ )
575649 {
576650 char ch = str [ i ] ;
577651
578652 if ( IsSeparator ( ch ) )
579653 {
580- scaler = ParseFloat ( str [ .. i ] ) ;
581- return str [ i .. ] ;
654+ length = i ;
655+ return TryParseFloat ( str [ .. length ] , out scaler ) ;
582656 }
583657
584658 if ( ch == '.' )
585659 {
586660 if ( hasDot )
587661 {
588662 // Second decimal point starts a new number.
589- scaler = ParseFloat ( str [ .. i ] ) ;
590- return str [ i .. ] ;
663+ length = i ;
664+ return TryParseFloat ( str [ .. length ] , out scaler ) ;
591665 }
592666
593667 hasDot = true ;
@@ -599,24 +673,20 @@ private static ReadOnlySpan<char> FindScaler(ReadOnlySpan<char> str, out float s
599673 char prev = str [ i - 1 ] ;
600674 if ( prev is not 'e' and not 'E' )
601675 {
602- scaler = ParseFloat ( str [ .. i ] ) ;
603- return str [ i .. ] ;
676+ length = i ;
677+ return TryParseFloat ( str [ .. length ] , out scaler ) ;
604678 }
605679 }
606680 else if ( char . IsLetter ( ch ) )
607681 {
608682 // Hit a command letter; end this number.
609- scaler = ParseFloat ( str [ .. i ] ) ;
610- return str [ i .. ] ;
683+ length = i ;
684+ return TryParseFloat ( str [ .. length ] , out scaler ) ;
611685 }
612686 }
613687
614- if ( str . Length > 0 )
615- {
616- scaler = ParseFloat ( str ) ;
617- }
618-
619- return [ ] ;
688+ length = str . Length ;
689+ return TryParseFloat ( str , out scaler ) ;
620690 }
621691
622692 private static bool IsSeparator ( char ch )
@@ -641,6 +711,6 @@ private static ReadOnlySpan<char> TrimSeparator(ReadOnlySpan<char> data)
641711 return data [ idx ..] ;
642712 }
643713
644- private static float ParseFloat ( ReadOnlySpan < char > str )
645- => float . Parse ( str , provider : CultureInfo . InvariantCulture ) ;
714+ private static bool TryParseFloat ( ReadOnlySpan < char > str , out float value )
715+ => float . TryParse ( str , CultureInfo . InvariantCulture , out value ) && float . IsFinite ( value ) ;
646716}
0 commit comments