Skip to content

Commit f8f88d2

Browse files
authored
Handle malformed content types (#339)
Handle malformed content types ### Motivation Before this PR, when a malformed content type (one that wasn't in the format `foo/bar`) was provided in the OpenAPI document, the generator would crash on a precondition failure, making it difficult to debug. ### Modifications Turn the precondition failure into a thrown error. ### Result Malformed content types now produce a thrown error instead of a crash. ### Test Plan All tests pass. Reviewed by: dnadoba Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (compatibility test) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #339
1 parent 110de34 commit f8f88d2

File tree

8 files changed

+76
-55
lines changed

8 files changed

+76
-55
lines changed

Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,12 @@ struct TextBasedRenderer: RendererProtocol {
346346
func renderedLiteral(_ literal: LiteralDescription) -> String {
347347
switch literal {
348348
case let .string(string):
349-
return "\"\(string)\""
349+
// Use a raw literal if the string contains a quote/backslash.
350+
if string.contains("\"") || string.contains("\\") {
351+
return "#\"\(string)\"#"
352+
} else {
353+
return "\"\(string)\""
354+
}
350355
case let .int(int):
351356
return "\(int)"
352357
case let .bool(bool):

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ extension FileTranslator {
6060
let caseName = swiftSafeName(for: rawValue)
6161
return (caseName, .string(rawValue))
6262
case .integer:
63-
guard let rawValue = anyValue as? Int else {
63+
let rawValue: Int
64+
if let intRawValue = anyValue as? Int {
65+
rawValue = intRawValue
66+
} else if let stringRawValue = anyValue as? String, let intRawValue = Int(stringRawValue) {
67+
rawValue = intRawValue
68+
} else {
6469
throw GenericError(message: "Disallowed value for an integer enum '\(typeName)': \(anyValue)")
6570
}
6671
let caseName = rawValue < 0 ? "_n\(abs(rawValue))" : "_\(rawValue)"

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ extension FileTranslator {
3838
inParent parent: TypeName
3939
) throws -> TypedSchemaContent? {
4040
guard
41-
let content = bestSingleContent(
41+
let content = try bestSingleContent(
4242
map,
4343
excludeBinary: excludeBinary,
4444
foundIn: parent.description
@@ -78,7 +78,7 @@ extension FileTranslator {
7878
excludeBinary: Bool = false,
7979
inParent parent: TypeName
8080
) throws -> [TypedSchemaContent] {
81-
let contents = supportedContents(
81+
let contents = try supportedContents(
8282
map,
8383
excludeBinary: excludeBinary,
8484
foundIn: parent.description
@@ -111,18 +111,19 @@ extension FileTranslator {
111111
/// - foundIn: The location where this content is parsed.
112112
/// - Returns: the detected content type + schema, nil if no supported
113113
/// schema found or if empty.
114+
/// - Throws: If parsing of any of the contents throws.
114115
func supportedContents(
115116
_ contents: OpenAPI.Content.Map,
116117
excludeBinary: Bool = false,
117118
foundIn: String
118-
) -> [SchemaContent] {
119+
) throws -> [SchemaContent] {
119120
guard !contents.isEmpty else {
120121
return []
121122
}
122123
return
123-
contents
124+
try contents
124125
.compactMap { key, value in
125-
parseContentIfSupported(
126+
try parseContentIfSupported(
126127
contentKey: key,
127128
contentValue: value,
128129
excludeBinary: excludeBinary,
@@ -145,11 +146,12 @@ extension FileTranslator {
145146
/// - foundIn: The location where this content is parsed.
146147
/// - Returns: the detected content type + schema, nil if no supported
147148
/// schema found or if empty.
149+
/// - Throws: If a malformed content type string is encountered.
148150
func bestSingleContent(
149151
_ map: OpenAPI.Content.Map,
150152
excludeBinary: Bool = false,
151153
foundIn: String
152-
) -> SchemaContent? {
154+
) throws -> SchemaContent? {
153155
guard !map.isEmpty else {
154156
return nil
155157
}
@@ -159,19 +161,25 @@ extension FileTranslator {
159161
foundIn: foundIn
160162
)
161163
}
162-
let chosenContent: (SchemaContent, OpenAPI.Content)?
163-
if let (contentKey, contentValue) = map.first(where: { $0.key.isJSON }) {
164-
let contentType = contentKey.asGeneratorContentType
164+
let mapWithContentTypes = try map.map { key, content in
165+
try (type: key.asGeneratorContentType, value: content)
166+
}
167+
168+
let chosenContent: (type: ContentType, schema: SchemaContent, content: OpenAPI.Content)?
169+
if let (contentType, contentValue) = mapWithContentTypes.first(where: { $0.type.isJSON }) {
165170
chosenContent = (
171+
contentType,
166172
.init(
167173
contentType: contentType,
168174
schema: contentValue.schema
169175
),
170176
contentValue
171177
)
172-
} else if !excludeBinary, let (contentKey, contentValue) = map.first(where: { $0.key.isBinary }) {
173-
let contentType = contentKey.asGeneratorContentType
178+
} else if !excludeBinary,
179+
let (contentType, contentValue) = mapWithContentTypes.first(where: { $0.type.isBinary })
180+
{
174181
chosenContent = (
182+
contentType,
175183
.init(
176184
contentType: contentType,
177185
schema: .b(.string(format: .binary))
@@ -186,18 +194,18 @@ extension FileTranslator {
186194
chosenContent = nil
187195
}
188196
if let chosenContent {
189-
let contentType = chosenContent.0.contentType
197+
let contentType = chosenContent.type
190198
if contentType.lowercasedType == "multipart"
191199
|| contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded")
192200
{
193201
diagnostics.emitUnsupportedIfNotNil(
194-
chosenContent.1.encoding,
202+
chosenContent.content.encoding,
195203
"Custom encoding for multipart/formEncoded content",
196204
foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)"
197205
)
198206
}
199207
}
200-
return chosenContent?.0
208+
return chosenContent?.schema
201209
}
202210

203211
/// Returns a wrapped version of the provided content if supported, returns
@@ -215,14 +223,15 @@ extension FileTranslator {
215223
/// type should be skipped, for example used when encoding headers.
216224
/// - foundIn: The location where this content is parsed.
217225
/// - Returns: The detected content type + schema, nil if unsupported.
226+
/// - Throws: If a malformed content type string is encountered.
218227
func parseContentIfSupported(
219228
contentKey: OpenAPI.ContentType,
220229
contentValue: OpenAPI.Content,
221230
excludeBinary: Bool = false,
222231
foundIn: String
223-
) -> SchemaContent? {
224-
if contentKey.isJSON {
225-
let contentType = contentKey.asGeneratorContentType
232+
) throws -> SchemaContent? {
233+
let contentType = try contentKey.asGeneratorContentType
234+
if contentType.isJSON {
226235
if contentType.lowercasedType == "multipart"
227236
|| contentType.lowercasedTypeAndSubtype.contains("application/x-www-form-urlencoded")
228237
{
@@ -237,15 +246,13 @@ extension FileTranslator {
237246
schema: contentValue.schema
238247
)
239248
}
240-
if contentKey.isUrlEncodedForm {
241-
let contentType = ContentType(contentKey.typeAndSubtype)
249+
if contentType.isUrlEncodedForm {
242250
return .init(
243251
contentType: contentType,
244252
schema: contentValue.schema
245253
)
246254
}
247-
if !excludeBinary, contentKey.isBinary {
248-
let contentType = contentKey.asGeneratorContentType
255+
if !excludeBinary, contentType.isBinary {
249256
return .init(
250257
contentType: contentType,
251258
schema: .b(.string(format: .binary))

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414
import OpenAPIKit
15+
import Foundation
1516

1617
/// A content type of a request, response, and other types.
1718
///
@@ -147,12 +148,24 @@ struct ContentType: Hashable {
147148
}
148149

149150
/// Creates a new content type by parsing the specified MIME type.
150-
/// - Parameter rawValue: A MIME type, for example "application/json". Must
151+
/// - Parameter string: A MIME type, for example "application/json". Must
151152
/// not be empty.
152-
init(_ rawValue: String) {
153-
precondition(!rawValue.isEmpty, "rawValue of a ContentType cannot be empty.")
153+
/// - Throws: If a malformed content type string is encountered.
154+
init(string: String) throws {
155+
struct InvalidContentTypeString: Error, LocalizedError, CustomStringConvertible {
156+
var string: String
157+
var description: String {
158+
"Invalid content type string: '\(string)', must have 2 components separated by a slash."
159+
}
160+
var errorDescription: String? {
161+
description
162+
}
163+
}
164+
guard !string.isEmpty else {
165+
throw InvalidContentTypeString(string: "")
166+
}
154167
var semiComponents =
155-
rawValue
168+
string
156169
.split(separator: ";")
157170
let typeAndSubtypeComponent = semiComponents.removeFirst()
158171
self.originallyCasedParameterPairs = semiComponents.map { component in
@@ -168,10 +181,9 @@ struct ContentType: Hashable {
168181
rawTypeAndSubtype
169182
.split(separator: "/")
170183
.map(String.init)
171-
precondition(
172-
typeAndSubtype.count == 2,
173-
"Invalid ContentType string, must have 2 components separated by a slash."
174-
)
184+
guard typeAndSubtype.count == 2 else {
185+
throw InvalidContentTypeString(string: rawTypeAndSubtype)
186+
}
175187
self.originallyCasedType = typeAndSubtype[0]
176188
self.originallyCasedSubtype = typeAndSubtype[1]
177189
}
@@ -251,27 +263,11 @@ struct ContentType: Hashable {
251263

252264
extension OpenAPI.ContentType {
253265

254-
/// A Boolean value that indicates whether the content type
255-
/// is a type of JSON.
256-
var isJSON: Bool {
257-
asGeneratorContentType.isJSON
258-
}
259-
260-
/// A Boolean value that indicates whether the content type
261-
/// is a URL-encoded form.
262-
var isUrlEncodedForm: Bool {
263-
asGeneratorContentType.isUrlEncodedForm
264-
}
265-
266-
/// A Boolean value that indicates whether the content type
267-
/// is just binary data.
268-
var isBinary: Bool {
269-
asGeneratorContentType.isBinary
270-
}
271-
272266
/// Returns the content type wrapped in the generator's representation
273267
/// of a content type, as opposed to the one from OpenAPIKit.
274268
var asGeneratorContentType: ContentType {
275-
ContentType(rawValue)
269+
get throws {
270+
try ContentType(string: rawValue)
271+
}
276272
}
277273
}

Sources/_OpenAPIGeneratorCore/Translator/Responses/acceptHeaderContentTypes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ extension FileTranslator {
3131
.responseOutcomes
3232
.flatMap { outcome in
3333
let response = try outcome.response.resolve(in: components)
34-
return supportedContents(response.content, foundIn: description.operationID)
34+
return try supportedContents(response.content, foundIn: description.operationID)
3535
}
3636
.map { content in
3737
content.contentType

Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ final class Test_TextBasedRenderer: XCTestCase {
187187
"hi"
188188
"""#
189189
)
190+
try _test(
191+
.string("this string: \"foo\""),
192+
renderedBy: renderer.renderedLiteral,
193+
rendersAs:
194+
#"""
195+
#"this string: "foo""#
196+
"""#
197+
)
190198
try _test(
191199
.nil,
192200
renderedBy: renderer.renderedLiteral,

Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentSwiftName.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ final class Test_ContentSwiftName: Test_Core {
4545
("application/foo; bar=baz; boo=foo", "application_foo_bar_baz_boo_foo"),
4646
("application/foo; bar = baz", "application_foo_bar_baz"),
4747
]
48-
for item in cases {
49-
let contentType = try XCTUnwrap(ContentType(item.0))
50-
XCTAssertEqual(nameMaker(contentType), item.1, "Case \(item.0) failed")
48+
for (string, name) in cases {
49+
let contentType = try XCTUnwrap(ContentType(string: string))
50+
XCTAssertEqual(nameMaker(contentType), name, "Case \(string) failed")
5151
}
5252
}
5353
}

Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentType.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ final class Test_ContentType: Test_Core {
170170
originallyCasedTypeAndSubtype,
171171
originallyCasedOutputWithParameters
172172
) in cases {
173-
let contentType = ContentType(rawValue)
173+
let contentType = try ContentType(string: rawValue)
174174
XCTAssertEqual(contentType.category, category)
175175
XCTAssertEqual(contentType.lowercasedType, type)
176176
XCTAssertEqual(contentType.lowercasedSubtype, subtype)

0 commit comments

Comments
 (0)