From f121497008d485fdcaa9c89b96b26a1932aeab0a Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 30 Aug 2019 02:42:19 +0300 Subject: [PATCH] fix(ios): handle path data without whitespace/comma after arc flags #1080 --- ios/Utils/RNSVGPathParser.m | 392 +++++++++++++++++++++++++++--------- 1 file changed, 298 insertions(+), 94 deletions(-) diff --git a/ios/Utils/RNSVGPathParser.m b/ios/Utils/RNSVGPathParser.m index 8fea698b..c50312c8 100644 --- a/ios/Utils/RNSVGPathParser.m +++ b/ios/Utils/RNSVGPathParser.m @@ -13,9 +13,10 @@ @implementation RNSVGPathParser { - NSString* _d; - NSString* _originD; - NSRegularExpression* _pathRegularExpression; + char prev_cmd; + NSUInteger i; + NSUInteger l; + NSString* s; float _penX; float _penY; float _penDownX; @@ -26,117 +27,173 @@ BOOL _penDownSet; } -- (instancetype) initWithPathString:(NSString *)d +- (instancetype)initWithPathString:(NSString *)d { if (self = [super init]) { - NSRegularExpression* decimalRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"(\\.\\d+)(?=\\-?\\.)" options:0 error:nil]; - _originD = d; - _d = [decimalRegularExpression stringByReplacingMatchesInString:d options:0 range:NSMakeRange(0, [d length]) withTemplate:@"$1,"]; - _pathRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"[a-df-z]|[\\-+]?(?:[\\d.]e[\\-+]?|[^\\s\\-+,a-z])+" options:NSRegularExpressionCaseInsensitive error:nil]; + prev_cmd = ' '; + l = [d length]; + i = 0; + s = d; } return self; } +#define NEXT_FLOAT [self parse_list_number] +#define NEXT_BOOL [self parse_flag] + - (CGPathRef)getPath { CGMutablePathRef path = CGPathCreateMutable(); - NSArray* results = [_pathRegularExpression matchesInString:_d options:0 range:NSMakeRange(0, [_d length])]; - unsigned long count = [results count]; + while (i < l) { + [self skip_spaces]; - if (count) { - NSUInteger i = 0; - #define NEXT_VALUE [self getNextValue:results[i++]] - #define NEXT_FLOAT [self float:NEXT_VALUE] - #define NEXT_BOOL [self bool:NEXT_VALUE] - NSString* lastCommand; - NSString* command = NEXT_VALUE; + bool has_prev_cmd = prev_cmd != ' '; + char first_char = [s characterAtIndex:i]; - @try { - while (command) { - if ([command isEqualToString:@"m"]) { // moveTo command - [self move:path x:NEXT_FLOAT y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"M"]) { - [self moveTo:path x:NEXT_FLOAT y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"l"]) { // lineTo command - [self line:path x:NEXT_FLOAT y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"L"]) { - [self lineTo:path x:NEXT_FLOAT y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"h"]) { // horizontalTo command - [self line:path x:NEXT_FLOAT y:0]; - } else if ([command isEqualToString:@"H"]) { - [self lineTo:path x:NEXT_FLOAT y:_penY]; - } else if ([command isEqualToString:@"v"]) { // verticalTo command - [self line:path x:0 y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"V"]) { - [self lineTo:path x:_penX y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"c"]) { // curveTo command - [self curve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; - } else if ([command isEqualToString:@"C"]) { - [self curveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; - } else if ([command isEqualToString:@"s"]) { // smoothCurveTo command - [self smoothCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; - } else if ([command isEqualToString:@"S"]) { - [self smoothCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; - } else if ([command isEqualToString:@"q"]) { // quadraticBezierCurveTo command - [self quadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"Q"]) { - [self quadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"t"]) {// smoothQuadraticBezierCurveTo command - [self smoothQuadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"T"]) { - [self smoothQuadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"a"]) { // arcTo command - [self arc:path rx:NEXT_FLOAT ry:NEXT_FLOAT rotation:NEXT_FLOAT outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_FLOAT y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"A"]) { - [self arcTo:path rx:NEXT_FLOAT ry:NEXT_FLOAT rotation:NEXT_FLOAT outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_FLOAT y:NEXT_FLOAT]; - } else if ([command isEqualToString:@"z"]) { // close command - [self close:path]; - } else if ([command isEqualToString:@"Z"]) { - [self close:path]; - } else { - command = lastCommand; - i--; - continue; - } - - lastCommand = command; - if ([lastCommand isEqualToString:@"m"]) { - lastCommand = @"l"; - } else if ([lastCommand isEqualToString:@"M"]) { - lastCommand = @"L"; - } - - command = i < count ? NEXT_VALUE : nil; - } - } @catch (NSException *exception) { - RCTLogWarn(@"Invalid CGPath format: %@", _originD); + if (!has_prev_cmd && first_char != 'M' && first_char != 'm') { + // The first segment must be a MoveTo. + RCTLogError(@"UnexpectedData: %@", s); CGPathRelease(path); return nil; } + // TODO: simplify + bool is_implicit_move_to = false; + char cmd = ' '; + if ([self is_cmd:first_char]) { + is_implicit_move_to = false; + cmd = first_char; + i += 1; + } else if ([self is_number_start:first_char] && has_prev_cmd) { + if (prev_cmd == 'Z' || prev_cmd == 'z') { + // ClosePath cannot be followed by a number. + RCTLogError(@"UnexpectedData: %@", s); + CGPathRelease(path); + return nil; + } + + if (prev_cmd == 'M' || prev_cmd == 'm') { + // 'If a moveto is followed by multiple pairs of coordinates, + // the subsequent pairs are treated as implicit lineto commands.' + // So we parse them as LineTo. + is_implicit_move_to = true; + if ([self is_absolute:prev_cmd]) { + cmd = 'L'; + } else { + cmd = 'l'; + } + } else { + is_implicit_move_to = false; + cmd = prev_cmd; + } + } else { + RCTLogError(@"UnexpectedData: %@", s); + CGPathRelease(path); + return nil; + } + + bool absolute = [self is_absolute:cmd]; + switch (cmd) { + case 'm': { + [self move:path x:NEXT_FLOAT y:NEXT_FLOAT]; + break; + } + case 'M': { + [self moveTo:path x:NEXT_FLOAT y:NEXT_FLOAT]; + break; + } + case 'l': { + [self line:path x:NEXT_FLOAT y:NEXT_FLOAT]; + break; + } + case 'L': { + [self lineTo:path x:NEXT_FLOAT y:NEXT_FLOAT]; + break; + } + case 'h': { + [self line:path x:NEXT_FLOAT y:0]; + break; + } + case 'H': { + [self lineTo:path x:NEXT_FLOAT y:_penY]; + break; + } + case 'v': { + [self line:path x:0 y:NEXT_FLOAT]; + break; + } + case 'V': { + [self lineTo:path x:_penX y:NEXT_FLOAT]; + break; + } + case 'c': { + [self curve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; + break; + } + case 'C': { + [self curveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; + break; + } + case 's': { + [self smoothCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; + break; + } + case 'S': { + [self smoothCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; + break; + } + case 'q': { + [self quadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT]; + break; + } + case 'Q': { + [self quadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT]; + break; + } + case 't': { + [self smoothQuadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT]; + break; + } + case 'T': { + [self smoothQuadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT]; + break; + } + case 'a': { + [self arc:path rx:NEXT_FLOAT ry:NEXT_FLOAT rotation:NEXT_FLOAT outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_FLOAT y:NEXT_FLOAT]; + break; + } + case 'A': { + [self arcTo:path rx:NEXT_FLOAT ry:NEXT_FLOAT rotation:NEXT_FLOAT outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_FLOAT y:NEXT_FLOAT]; + break; + } + case 'z': + case 'Z': { + [self close:path]; + break; + } + default: { + RCTLogError(@"UnexpectedData: %@", s); + CGPathRelease(path); + return nil; + } + } + + + if (is_implicit_move_to) { + if (absolute) { + prev_cmd = 'M'; + } else { + prev_cmd = 'm'; + } + } else { + prev_cmd = cmd; + } + } return (CGPathRef)CFAutorelease(path); } -- (NSString *)getNextValue:(NSTextCheckingResult *)result -{ - if (!result) { - return nil; - } - return [_d substringWithRange:NSMakeRange(result.range.location, result.range.length)]; -} - -- (float)float:(NSString *)value -{ - return [value floatValue]; -} - -- (BOOL)bool:(NSString *)value -{ - return ![value isEqualToString:@"0"]; -} - - (void)move:(CGMutablePathRef)path x:(float)x y:(float)y { [self moveTo:path x:x + _penX y:y + _penY]; @@ -373,4 +430,151 @@ } } +- (void)skip_spaces { + while ([[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[s characterAtIndex:i]]) i++; +} + +- (bool)is_cmd:(char)c { + switch (c) { + case 'M': + case 'm': + case 'Z': + case 'z': + case 'L': + case 'l': + case 'H': + case 'h': + case 'V': + case 'v': + case 'C': + case 'c': + case 'S': + case 's': + case 'Q': + case 'q': + case 'T': + case 't': + case 'A': + case 'a': + return true; + } + return false; +} + +- (bool)is_number_start:(char)c { + return (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+'; +} + +- (bool)is_absolute:(char)c { + return [[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember:c]; +} + +// By the SVG spec 'large-arc' and 'sweep' must contain only one char +// and can be written without any separators, e.g.: 10 20 30 01 10 20. +- (bool)parse_flag { + [self skip_spaces]; + + char c = [s characterAtIndex:i]; + switch (c) { + case '0': + case '1': { + i += 1; + if ([s characterAtIndex:i] == ',') { + i += 1; + } + [self skip_spaces]; + break; + } + default: + RCTLogError(@"UnexpectedData: %@", s); + } + + return c == '1'; +} + +- (float)parse_list_number { + if (i == l) { + RCTLogError(@"UnexpectedEnd: %@", s); + } + + float n = [self parse_number]; + [self skip_spaces]; + [self parse_list_separator]; + + return n; +} + +- (float)parse_number { + // Strip off leading whitespaces. + [self skip_spaces]; + + if (i == l) { + RCTLogError(@"InvalidNumber: %@", s); + } + + NSUInteger start = i; + + char c = [s characterAtIndex:i]; + + // Consume sign. + if (c == '-' || c == '+') { + i += 1; + c = [s characterAtIndex:i]; + } + + // Consume integer. + if (c >= '0' && c <= '9') { + [self skip_digits]; + c = [s characterAtIndex:i]; + } else if (c != '.') { + RCTLogError(@"InvalidNumber: %@", s); + } + + // Consume fraction. + if (c == '.') { + i += 1; + [self skip_digits]; + c = [s characterAtIndex:i]; + } + + if (c == 'e' || c == 'E') { + char c2 = [s characterAtIndex:i + 1]; + // Check for `em`/`ex`. + if (c2 != 'm' && c2 != 'x') { + i += 1; + c = [s characterAtIndex:i]; + + if (c == '+' || c == '-') { + i += 1; + [self skip_digits]; + } else if (c >= '0' && c <= '9') { + [self skip_digits]; + } else { + RCTLogError(@"InvalidNumber: %@", s); + } + } + } + + NSString* num = [s substringWithRange:NSMakeRange(start, i - start)]; + float n = [num floatValue]; + + // inf, nan, etc. are an error. + if (!isfinite(n)) { + RCTLogError(@"InvalidNumber: %@", s); + } + + return n; +} + +- (void)parse_list_separator { + if ([s characterAtIndex:i] == ',') { + i += 1; + } +} + +- (void)skip_digits { + while ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[s characterAtIndex:i]]) i++; +} + @end +