Skip to content

Commit 35dd374

Browse files
authored
Merge pull request #384 from mattpolzin/bugfix/382/url_template_stack_overflow2
Fix `URLTemplate` stack overflow
2 parents 03840ad + 24d7950 commit 35dd374

File tree

3 files changed

+75
-53
lines changed

3 files changed

+75
-53
lines changed

Sources/OpenAPIKitCore/URLTemplate/URLTemplate+Parsing.swift

+40-49
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,55 @@
77

88
extension URLTemplate {
99
internal static func scan(
10-
_ string: String,
11-
partialToken: PartialToken?,
12-
from remainder: Substring,
13-
addingTo tokens: [Component]
10+
_ string: String
1411
) throws -> [Component] {
15-
guard let next = remainder.first else {
16-
guard partialToken == nil || partialToken?.type == .constant else {
17-
throw ParsingError.unterminatedVariable(name: String(partialToken?.string ?? ""))
18-
}
19-
return tokens + tokenArray(from: partialToken)
20-
}
21-
let nextFirstIndex = remainder.index(remainder.startIndex, offsetBy: 1, limitedBy: remainder.endIndex)
12+
var tokens = [Component]()
13+
var remainder = string[...]
14+
var partialToken: PartialToken? = nil
15+
16+
while let next = remainder.first {
17+
let nextFirstIndex = remainder.index(remainder.startIndex, offsetBy: 1, limitedBy: remainder.endIndex)
2218

23-
switch (partialToken?.type, next) {
24-
case (nil, "{"),
25-
(.constant, "{"):
26-
guard let newFirstIndex = nextFirstIndex else {
27-
throw ParsingError.unterminatedVariable(name: "")
28-
}
29-
let newTokens = tokens + tokenArray(from: partialToken)
30-
return try scan(
31-
string,
32-
partialToken: .init(type: .variable, string: remainder[newFirstIndex..<newFirstIndex]),
33-
from: remainder.dropFirst(),
34-
addingTo: newTokens
35-
)
19+
switch (partialToken?.type, next) {
20+
case (nil, "{"),
21+
(.constant, "{"):
22+
guard let newFirstIndex = nextFirstIndex else {
23+
throw ParsingError.unterminatedVariable(name: "")
24+
}
25+
tokens += tokenArray(from: partialToken)
26+
partialToken = .init(type: .variable, string: remainder[newFirstIndex..<newFirstIndex])
27+
remainder = remainder.dropFirst()
3628

37-
case (.variable, "}"):
38-
let newTokens = tokens + tokenArray(from: partialToken)
39-
return try scan(string, partialToken: nil, from: remainder.dropFirst(), addingTo: newTokens)
29+
case (.variable, "}"):
30+
tokens += tokenArray(from: partialToken)
31+
partialToken = nil
32+
remainder = remainder.dropFirst()
4033

41-
case (nil, "}"),
42-
(.constant, "}"):
43-
throw ParsingError.variableEndedWithoutStarting(name: partialToken.map { String($0.string) } ?? "")
34+
case (nil, "}"),
35+
(.constant, "}"):
36+
throw ParsingError.variableEndedWithoutStarting(name: partialToken.map { String($0.string) } ?? "")
4437

45-
case (.variable, "{"):
46-
throw ParsingError.variableStartedWithinVariable(name: partialToken.map { String($0.string) } ?? "")
38+
case (.variable, "{"):
39+
throw ParsingError.variableStartedWithinVariable(name: partialToken.map { String($0.string) } ?? "")
4740

48-
case (nil, _):
49-
return try scan(
50-
string,
51-
partialToken: .init(type: .constant, string: remainder[remainder.startIndex...remainder.startIndex]),
52-
from: remainder.dropFirst(),
53-
addingTo: tokens
54-
)
41+
case (nil, _):
42+
partialToken = .init(type: .constant, string: remainder[remainder.startIndex...remainder.startIndex])
43+
remainder = remainder.dropFirst()
5544

56-
case (.constant, _),
57-
(.variable, _):
58-
guard nextFirstIndex != nil, let reifiedPartialToken = partialToken else {
59-
return tokens + tokenArray(from: partialToken)
45+
case (.constant, _),
46+
(.variable, _):
47+
guard nextFirstIndex != nil, let reifiedPartialToken = partialToken else {
48+
tokens += tokenArray(from: partialToken)
49+
continue
50+
}
51+
partialToken = reifiedPartialToken.advancingStringByOne(within: string)
52+
remainder = remainder.dropFirst()
6053
}
61-
return try scan(
62-
string,
63-
partialToken: reifiedPartialToken.advancingStringByOne(within: string),
64-
from: remainder.dropFirst(),
65-
addingTo: tokens
66-
)
6754
}
55+
guard partialToken == nil || partialToken?.type == .constant else {
56+
throw ParsingError.unterminatedVariable(name: String(partialToken?.string ?? ""))
57+
}
58+
return tokens + tokenArray(from: partialToken)
6859
}
6960

7061
internal static func tokenArray(from partial: PartialToken?) -> [Component] {

Sources/OpenAPIKitCore/URLTemplate/URLTemplate.swift

+1-4
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,7 @@ public struct URLTemplate: Hashable, RawRepresentable {
111111
public init(templateString: String) throws {
112112
rawValue = templateString
113113
components = try URLTemplate.scan(
114-
templateString,
115-
partialToken: nil,
116-
from: templateString[...],
117-
addingTo: []
114+
templateString
118115
)
119116
}
120117

Tests/OpenAPIKitCoreTests/URLTemplate/URLTemplateTests.swift

+34
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,37 @@ extension URLTemplateTests {
375375
fileprivate struct TemplatedURLWrapper: Codable {
376376
let url: URLTemplate?
377377
}
378+
379+
// MARK: - Stack Overflow Regression Test
380+
#if swift(>=5.5)
381+
import Dispatch
382+
383+
extension URLTemplateTests {
384+
struct StackFoo: Decodable {
385+
var val: URLTemplate
386+
}
387+
388+
static func stackWork() throws {
389+
let data = Data("""
390+
{
391+
\"val\": \"https://\(Array(repeating: "foo.", count: 1000).joined())com/\"
392+
}
393+
394+
""".utf8)
395+
let document = try JSONDecoder().decode(
396+
StackFoo.self,
397+
from: data
398+
)
399+
print(document)
400+
}
401+
402+
func test_avoid_stack_overflow() async throws {
403+
try await withThrowingTaskGroup(of: Void.self) { group in
404+
group.addTask {
405+
try URLTemplateTests.stackWork()
406+
}
407+
try await group.waitForAll()
408+
}
409+
}
410+
}
411+
#endif

0 commit comments

Comments
 (0)