Skip to content

Commit 8cce154

Browse files
committed
Audit _CGPathParseString implementation
1 parent 7e68498 commit 8cce154

File tree

3 files changed

+108
-145
lines changed

3 files changed

+108
-145
lines changed

Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN
1717
/// Parses a path string and appends the path elements to a mutable path.
1818
///
1919
/// The string format uses space-separated numbers followed by command characters:
20-
/// - `m` - move to (x y)
21-
/// - `l` - line to (x y)
22-
/// - `c` - cubic curve (cp1x cp1y cp2x cp2y x y)
23-
/// - `q` - quad curve (cpx cpy x y)
24-
/// - `t` - smooth quad curve (x y), reflects previous control point
25-
/// - `v` - smooth cubic curve (cp2x cp2y x y), uses current point as cp1
26-
/// - `y` - shorthand cubic (cp1x cp1y x y), cp2 equals endpoint
27-
/// - `h` - close subpath
28-
/// - `re` - rectangle (x y width height)
20+
///
21+
/// | Command | Parameters | Description |
22+
/// |---------|------------|-------------|
23+
/// | `m` | x y | Move to point |
24+
/// | `l` | x y | Line to point |
25+
/// | `c` | cp1x cp1y cp2x cp2y x y | Cubic Bézier curve |
26+
/// | `q` | cpx cpy x y | Quadratic Bézier curve |
27+
/// | `t` | x y | Smooth quadratic curve (reflects previous control point) |
28+
/// | `v` | cp2x cp2y x y | Smooth cubic curve (uses last point as cp1) |
29+
/// | `y` | cp1x cp1y x y | Shorthand cubic (cp2 equals endpoint) |
30+
/// | `h` | (none) | Close subpath |
31+
/// | `re` | x y width height | Rectangle |
32+
///
33+
/// Whitespace characters (space, tab, newline, carriage return) are skipped.
34+
/// Numbers can be integers, decimals, or special values like `Inf`.
2935
///
3036
/// - Parameters:
3137
/// - path: The mutable path to append elements to.

Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m

Lines changed: 63 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -16,168 +16,122 @@
1616
@import OpenRenderBox;
1717
#endif
1818

19-
// NOTE: Not audited yet. Use with caution.
2019
BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) {
21-
if (path == NULL || utf8CString == NULL) {
22-
return NO;
23-
}
24-
25-
CGFloat numbers[6];
20+
double numbers[6];
2621
int numCount = 0;
2722
CGFloat currentX = 0.0, currentY = 0.0;
2823
CGFloat lastControlX = 0.0, lastControlY = 0.0;
29-
3024
const char *ptr = utf8CString;
31-
32-
while (YES) {
33-
// Skip whitespace (characters <= 0x1f or space 0x20)
34-
while (*ptr != '\0' && (*ptr <= 0x1f || *ptr == ' ')) {
35-
ptr++;
36-
}
37-
38-
if (*ptr == '\0') {
39-
// End of string - success if all numbers consumed
40-
return numCount == 0;
41-
}
42-
43-
char c = *ptr;
44-
45-
// Check if character can start a number
46-
// Valid: digits 0-9 (0x30-0x39), '-', '+', '.', 'e', 'E', 'p', 'x', "inf"
47-
BOOL isNumberStart = NO;
48-
if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.') {
49-
isNumberStart = YES;
50-
} else if (c == 'e' || c == 'E' || c == 'p' || c == 'x') {
51-
isNumberStart = YES;
52-
} else if (c == 'i') {
53-
// Check for "inf"
54-
if (ptr[1] == 'n' && ptr[2] == 'f') {
55-
isNumberStart = YES;
56-
}
57-
}
58-
59-
if (isNumberStart) {
60-
if (numCount >= 6) {
61-
return NO;
25+
do {
26+
while (*ptr <= 0x1f) {
27+
switch (*ptr) {
28+
case 0: return true;
29+
case 9: case 10: case 12: case 13: ptr++; break;
30+
default: return false;
6231
}
63-
char *endPtr;
64-
numbers[numCount++] = strtod_l(ptr, &endPtr, NULL);
65-
ptr = endPtr;
66-
continue;
6732
}
68-
69-
// Process command character
33+
unsigned char c = (unsigned char)*ptr;
34+
BOOL isNumberStart = NO;
7035
switch (c) {
36+
case ' ': break;
37+
case '+': case '-': case '.': case '0' ... '9':
38+
case 'E': case 'P': case 'X': case 'e': case 'p': case 'x':
39+
isNumberStart = YES;
40+
break;
41+
case 'I':
42+
if (ptr[1] == 'n' && ptr[2] == 'f') { isNumberStart = YES; }
43+
break;
7144
case 'm':
7245
if (numCount != 2) return NO;
7346
CGPathMoveToPoint(path, NULL, numbers[0], numbers[1]);
74-
lastControlX = currentX = numbers[0];
75-
lastControlY = currentY = numbers[1];
47+
currentX = lastControlX = numbers[0];
48+
currentY = lastControlY = numbers[1];
7649
numCount = 0;
7750
break;
78-
7951
case 'l':
8052
if (numCount != 2) return NO;
8153
CGPathAddLineToPoint(path, NULL, numbers[0], numbers[1]);
82-
lastControlX = currentX = numbers[0];
83-
lastControlY = currentY = numbers[1];
54+
currentX = lastControlX = numbers[0];
55+
currentY = lastControlY = numbers[1];
8456
numCount = 0;
8557
break;
86-
8758
case 'c':
8859
if (numCount != 6) return NO;
89-
CGPathAddCurveToPoint(path, NULL,
90-
numbers[0], numbers[1],
91-
numbers[2], numbers[3],
92-
numbers[4], numbers[5]);
93-
lastControlX = numbers[2];
94-
lastControlY = numbers[3];
95-
currentX = numbers[4];
96-
currentY = numbers[5];
60+
CGPathAddCurveToPoint(path, NULL, numbers[0], numbers[1],
61+
numbers[2], numbers[3], numbers[4], numbers[5]);
62+
currentX = numbers[2];
63+
currentY = numbers[3];
64+
lastControlX = numbers[4];
65+
lastControlY = numbers[5];
9766
numCount = 0;
9867
break;
99-
10068
case 'q':
10169
if (numCount != 4) return NO;
102-
CGPathAddQuadCurveToPoint(path, NULL,
103-
numbers[0], numbers[1],
70+
CGPathAddQuadCurveToPoint(path, NULL, numbers[0], numbers[1],
10471
numbers[2], numbers[3]);
105-
lastControlX = numbers[0];
106-
lastControlY = numbers[1];
107-
currentX = numbers[2];
108-
currentY = numbers[3];
72+
currentX = numbers[0];
73+
currentY = numbers[1];
74+
lastControlX = numbers[2];
75+
lastControlY = numbers[3];
10976
numCount = 0;
11077
break;
111-
11278
case 't':
113-
// Smooth quad curve: reflect last control point
11479
if (numCount != 2) return NO;
11580
{
11681
CGFloat reflectedX = currentX - 2.0 * lastControlX;
11782
CGFloat reflectedY = currentY - 2.0 * lastControlY;
118-
CGPathAddQuadCurveToPoint(path, NULL,
119-
reflectedX, reflectedY,
83+
CGPathAddQuadCurveToPoint(path, NULL, reflectedX, reflectedY,
12084
numbers[0], numbers[1]);
121-
lastControlX = reflectedX;
122-
lastControlY = reflectedY;
123-
currentX = numbers[0];
124-
currentY = numbers[1];
85+
currentX = reflectedX;
86+
currentY = reflectedY;
87+
lastControlX = numbers[0];
88+
lastControlY = numbers[1];
12589
}
12690
numCount = 0;
12791
break;
128-
12992
case 'v':
130-
// Smooth cubic curve: use current point as cp1
13193
if (numCount != 4) return NO;
132-
CGPathAddCurveToPoint(path, NULL,
133-
currentX, currentY,
134-
numbers[0], numbers[1],
135-
numbers[2], numbers[3]);
136-
lastControlX = numbers[0];
137-
lastControlY = numbers[1];
138-
currentX = numbers[2];
139-
currentY = numbers[3];
94+
CGPathAddCurveToPoint(path, NULL, lastControlX, lastControlY,
95+
numbers[0], numbers[1], numbers[2], numbers[3]);
96+
lastControlX = numbers[2];
97+
lastControlY = numbers[3];
14098
numCount = 0;
14199
break;
142-
143100
case 'y':
144-
// Shorthand cubic: cp2 = endpoint
145101
if (numCount != 4) return NO;
146-
CGPathAddCurveToPoint(path, NULL,
147-
numbers[0], numbers[1],
148-
numbers[2], numbers[3],
149-
numbers[2], numbers[3]);
150-
currentX = numbers[2];
151-
currentY = numbers[3];
102+
CGPathAddCurveToPoint(path, NULL, numbers[0], numbers[1],
103+
numbers[2], numbers[3], numbers[2], numbers[3]);
104+
lastControlX = numbers[2];
105+
lastControlY = numbers[3];
152106
numCount = 0;
153107
break;
154-
155108
case 'h':
156109
if (numCount != 0) return NO;
157110
CGPathCloseSubpath(path);
111+
lastControlX = 0.0;
158112
lastControlY = 0.0;
159113
numCount = 0;
160114
break;
161-
162115
case 'r':
163-
// Check for "re" (rectangle)
164-
if (ptr[1] == 'e') {
165-
if (numCount != 4) return NO;
166-
CGPathAddRect(path, NULL, CGRectMake(numbers[0], numbers[1],
167-
numbers[2], numbers[3]));
168-
currentX = numbers[0];
169-
currentY = numbers[1];
170-
numCount = 0;
171-
ptr++; // Skip 'e'
172-
break;
173-
}
174-
return NO;
175-
116+
if (ptr[1] != 'e') return NO;
117+
if (numCount != 4) return NO;
118+
CGPathAddRect(path, NULL, CGRectMake(numbers[0], numbers[1],
119+
numbers[2], numbers[3]));
120+
ptr++;
121+
numCount = 0;
122+
break;
176123
default:
177-
return NO;
124+
return false;
178125
}
179-
ptr++;
180-
}
126+
if (isNumberStart) {
127+
if (numCount == 6) return false;
128+
char *endPtr;
129+
numbers[numCount++] = strtod_l(ptr, &endPtr, NULL);
130+
ptr = endPtr;
131+
} else {
132+
ptr++;
133+
}
134+
} while (1);
181135
}
182136

183137
typedef struct PathInfo {

Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,41 @@ struct CGPath_OpenSwiftUITests {
1414

1515
struct ParseString {
1616
@Test(arguments: [
17-
// Basic commands: move, line, close
18-
("100 0 m 200 100 l h", " 100 0 m 200 100 l h"),
19-
// Multiple lines
20-
("0 0 m 100 0 l 100 100 l 0 100 l h", " 0 0 m 100 0 l 100 100 l 0 100 l h"),
21-
// Quad curve
22-
("0 0 m 50 0 100 100 q", " 0 0 m 50 0 100 100 q"),
23-
// Cubic curve
24-
("0 0 m 25 0 75 100 100 100 c", " 0 0 m 25 0 75 100 100 100 c"),
25-
// Rectangle
26-
("10 20 30 40 re", " 10 20 m 40 20 l 40 60 l 10 60 l h"),
17+
// m - move to
18+
("100 0 m 200 100 l h", true, " 100 0 m 200 100 l h"),
19+
// l - line to (multiple)
20+
("0 0 m 100 0 l 100 100 l 0 100 l h", true, " 0 0 m 100 0 l 100 100 l 0 100 l h"),
21+
// q - quad curve
22+
("0 0 m 50 0 100 100 q", true, " 0 0 m 50 0 100 100 q"),
23+
// c - cubic curve
24+
("0 0 m 25 0 75 100 100 100 c", true, " 0 0 m 25 0 75 100 100 100 c"),
25+
// v - smooth cubic (cp1 = last point)
26+
("0 0 m 50 50 100 100 v", true, " 0 0 m 0 0 50 50 100 100 c"),
27+
// y - shorthand cubic (cp2 = endpoint)
28+
("0 0 m 25 0 100 100 y", true, " 0 0 m 25 0 100 100 100 100 c"),
29+
// re - rectangle
30+
("10 20 30 40 re", true, " 10 20 m 40 20 l 40 60 l 10 60 l h"),
2731
// Negative numbers
28-
("-10 -20 m 30 40 l", " -10 -20 m 30 40 l"),
32+
("-10 -20 m 30 40 l", true, " -10 -20 m 30 40 l"),
2933
// Decimal numbers
30-
("0.5 1.5 m 2.5 3.5 l", " 0.5 1.5 m 2.5 3.5 l"),
34+
("0.5 1.5 m 2.5 3.5 l", true, " 0.5 1.5 m 2.5 3.5 l"),
35+
// Whitespace variants (tab, newline)
36+
("0\t0\nm\n100\t100\tl", true, " 0 0 m 100 100 l"),
37+
// Invalid: unknown command
38+
("0 0 z", false, ""),
39+
// Invalid: wrong number of parameters
40+
("0 m", false, ""),
41+
// Invalid: too many numbers
42+
("1 2 3 4 5 6 7 m", false, ""),
3143
])
32-
func parseString(input: String, expected: String) {
44+
func parseString(input: String, expectedResult: Bool, expectedString: String) {
3345
let path = CGMutablePath()
3446
let result = _CGPathParseString(path, input)
35-
#expect(result == true)
36-
let description = _CGPathCopyDescription(path, 0)
37-
#expect(description == expected)
38-
}
39-
40-
@Test
41-
func parseStringWithInvalidInput() {
42-
let path = CGMutablePath()
43-
// Unknown command
44-
#expect(_CGPathParseString(path, "0 0 z") == false)
45-
// Wrong number of parameters for move
46-
#expect(_CGPathParseString(path, "0 m") == false)
47-
// Too many numbers without command
48-
#expect(_CGPathParseString(path, "1 2 3 4 5 6 7 m") == false)
47+
#expect(result == expectedResult)
48+
if expectedResult {
49+
let description = _CGPathCopyDescription(path, 0)
50+
#expect(description == expectedString)
51+
}
4952
}
5053
}
5154

0 commit comments

Comments
 (0)