fix(ios): handle path data without whitespace/comma after arc flags

#1080
This commit is contained in:
Mikael Sand
2019-08-30 02:42:19 +03:00
parent 79956a98e5
commit f121497008
+298 -94
View File
@@ -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<NSTextCheckingResult *>* 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