Skip to content

Commit e295e4e

Browse files
committed
Build out the validation of google.protobuf.Any JSON support.
The upstream major languages directly use their type registries to eagerly validate `type_url`s. This means they will fail to decode or encode JSON for a lot of cases. The conformance tests only cover some of this so I hacked in some C++ unittests to see what errors are thrown and then made the matching cases here and expanded our error handling to duplicate the results. The bulk of the changes are really just update comments to cover what errors are now throw (not actual code changes).
1 parent e876103 commit e295e4e

7 files changed

+345
-37
lines changed

Sources/SwiftProtobuf/AnyMessageStorage.swift

+31-9
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,13 @@ extension AnyMessageStorage {
411411

412412
// _CustomJSONCodable support for Google_Protobuf_Any
413413
extension AnyMessageStorage {
414+
// Spec for Any says this should contain atleast one slash. Looking at upstream languages, most
415+
// actually look up the value in their runtime registries, but since we do deferred parsing
416+
// we can't assume the registry is complete, thus just do this minimal validation check.
417+
fileprivate func isTypeURLValid() -> Bool {
418+
_typeURL.contains("/")
419+
}
420+
414421
// Override the traversal-based JSON encoding
415422
// This builds an Any JSON representation from one of:
416423
// * The message we were initialized with,
@@ -423,11 +430,18 @@ extension AnyMessageStorage {
423430
case .binary(let valueData):
424431
// Follow the C++ protostream_objectsource.cc's
425432
// ProtoStreamObjectSource::RenderAny() special casing of an empty value.
426-
guard !valueData.isEmpty else {
433+
if valueData.isEmpty && _typeURL.isEmpty {
434+
return "{}"
435+
}
436+
guard isTypeURLValid() else {
427437
if _typeURL.isEmpty {
428-
return "{}"
438+
throw SwiftProtobufError.JSONEncoding.emptyAnyTypeURL()
429439
}
440+
throw SwiftProtobufError.JSONEncoding.invalidAnyTypeURL(type_url: _typeURL)
441+
}
442+
if valueData.isEmpty {
430443
var jsonEncoder = JSONEncoder()
444+
jsonEncoder.startObject()
431445
jsonEncoder.startField(name: "@type")
432446
jsonEncoder.putStringValue(value: _typeURL)
433447
jsonEncoder.endObject()
@@ -446,12 +460,21 @@ extension AnyMessageStorage {
446460
return try serializeAnyJSON(for: m, typeURL: _typeURL, options: options)
447461

448462
case .message(let msg):
449-
// We should have been initialized with a typeURL, but
450-
// ensure it wasn't cleared.
463+
// We should have been initialized with a typeURL, make sure it is valid.
464+
if !_typeURL.isEmpty && !isTypeURLValid() {
465+
throw SwiftProtobufError.JSONEncoding.invalidAnyTypeURL(type_url: _typeURL)
466+
}
467+
// If it was cleared, default it.
451468
let url = !_typeURL.isEmpty ? _typeURL : buildTypeURL(forMessage: msg, typePrefix: defaultAnyTypeURLPrefix)
452469
return try serializeAnyJSON(for: msg, typeURL: url, options: options)
453470

454471
case .contentJSON(let contentJSON, _):
472+
guard isTypeURLValid() else {
473+
if _typeURL.isEmpty {
474+
throw SwiftProtobufError.JSONEncoding.emptyAnyTypeURL()
475+
}
476+
throw SwiftProtobufError.JSONEncoding.invalidAnyTypeURL(type_url: _typeURL)
477+
}
455478
var jsonEncoder = JSONEncoder()
456479
jsonEncoder.startObject()
457480
jsonEncoder.startField(name: "@type")
@@ -488,11 +511,7 @@ extension AnyMessageStorage {
488511
try decoder.scanner.skipRequiredColon()
489512
if key == "@type" {
490513
_typeURL = try decoder.scanner.nextQuotedString()
491-
// Spec for Any says this should contain atleast one slash. Looking at
492-
// upstream languages, most actually look up the value in their runtime
493-
// registries, but since we do deferred parsing, just do this minimal
494-
// validation check.
495-
guard _typeURL.contains("/") else {
514+
guard isTypeURLValid() else {
496515
throw SwiftProtobufError.JSONDecoding.invalidAnyTypeURL(type_url: _typeURL)
497516
}
498517
} else {
@@ -501,6 +520,9 @@ extension AnyMessageStorage {
501520
jsonEncoder.append(text: keyValueJSON)
502521
}
503522
if decoder.scanner.skipOptionalObjectEnd() {
523+
if _typeURL.isEmpty {
524+
throw SwiftProtobufError.JSONDecoding.emptyAnyTypeURL()
525+
}
504526
// Capture the options, but set the messageDepthLimit to be what
505527
// was left right now, as that is the limit when the JSON is finally
506528
// parsed.

Sources/SwiftProtobuf/Message+JSONAdditions.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension Message {
2424
/// - Returns: A string containing the JSON serialization of the message.
2525
/// - Parameters:
2626
/// - options: The JSONEncodingOptions to use.
27-
/// - Throws: ``JSONEncodingError`` if encoding fails.
27+
/// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
2828
public func jsonString(
2929
options: JSONEncodingOptions = JSONEncodingOptions()
3030
) throws -> String {
@@ -43,7 +43,7 @@ extension Message {
4343
/// - Returns: A `SwiftProtobufContiguousBytes` containing the JSON serialization of the message.
4444
/// - Parameters:
4545
/// - options: The JSONEncodingOptions to use.
46-
/// - Throws: ``JSONEncodingError`` if encoding fails.
46+
/// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
4747
public func jsonUTF8Bytes<Bytes: SwiftProtobufContiguousBytes>(
4848
options: JSONEncodingOptions = JSONEncodingOptions()
4949
) throws -> Bytes {

Sources/SwiftProtobuf/Message+JSONAdditions_Data.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ extension Message {
5454
/// - Returns: A Data containing the JSON serialization of the message.
5555
/// - Parameters:
5656
/// - options: The JSONEncodingOptions to use.
57-
/// - Throws: ``JSONEncodingError`` if encoding fails.
57+
/// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
5858
public func jsonUTF8Data(
5959
options: JSONEncodingOptions = JSONEncodingOptions()
6060
) throws -> Data {

Sources/SwiftProtobuf/Message+JSONArrayAdditions.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension Message {
2525
/// - Parameters:
2626
/// - collection: The list of messages to encode.
2727
/// - options: The JSONEncodingOptions to use.
28-
/// - Throws: ``JSONEncodingError`` if encoding fails.
28+
/// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
2929
public static func jsonString<C: Collection>(
3030
from collection: C,
3131
options: JSONEncodingOptions = JSONEncodingOptions()
@@ -43,7 +43,7 @@ extension Message {
4343
/// - Parameters:
4444
/// - collection: The list of messages to encode.
4545
/// - options: The JSONEncodingOptions to use.
46-
/// - Throws: ``JSONEncodingError`` if encoding fails.
46+
/// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
4747
public static func jsonUTF8Bytes<C: Collection, Bytes: SwiftProtobufContiguousBytes>(
4848
from collection: C,
4949
options: JSONEncodingOptions = JSONEncodingOptions()

Sources/SwiftProtobuf/Message+JSONArrayAdditions_Data.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ extension Message {
6565
/// - Parameters:
6666
/// - collection: The list of messages to encode.
6767
/// - options: The JSONEncodingOptions to use.
68-
/// - Throws: ``JSONEncodingError`` if encoding fails.
68+
/// - Throws: ``SwiftProtobufError`` or ``JSONEncodingError`` if encoding fails.
6969
public static func jsonUTF8Data<C: Collection>(
7070
from collection: C,
7171
options: JSONEncodingOptions = JSONEncodingOptions()

Sources/SwiftProtobuf/SwiftProtobufError.swift

+49
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ extension SwiftProtobufError {
9292
case binaryDecodingError
9393
case binaryStreamDecodingError
9494
case jsonDecodingError
95+
case jsonEncodingError
9596

9697
var description: String {
9798
switch self {
@@ -101,6 +102,8 @@ extension SwiftProtobufError {
101102
return "Stream decoding error"
102103
case .jsonDecodingError:
103104
return "JSON decoding error"
105+
case .jsonEncodingError:
106+
return "JSON encoding error"
104107
}
105108
}
106109
}
@@ -131,6 +134,10 @@ extension SwiftProtobufError {
131134
Self(.jsonDecodingError)
132135
}
133136

137+
/// Errors arising from JSON encoding of messages.
138+
public static var jsonEncodingError: Self {
139+
Self(.jsonEncodingError)
140+
}
134141
}
135142

136143
/// A location within source code.
@@ -264,6 +271,48 @@ extension SwiftProtobufError {
264271
location: SourceLocation(function: function, file: file, line: line)
265272
)
266273
}
274+
275+
/// While decoding a `google.protobuf.Any` no `@type` field but the message had other fields.
276+
public static func emptyAnyTypeURL(
277+
function: String = #function,
278+
file: String = #fileID,
279+
line: Int = #line
280+
) -> SwiftProtobufError {
281+
SwiftProtobufError(
282+
code: .jsonDecodingError,
283+
message: "google.protobuf.Any '@type' was must be present if if the object is not empty.",
284+
location: SourceLocation(function: function, file: file, line: line)
285+
)
286+
}
267287
}
268288

289+
/// Errors arising from JSON encoding of messages.
290+
public enum JSONEncoding {
291+
/// While encoding a `google.protobuf.Any` encountered a malformed `type_url` field.
292+
public static func invalidAnyTypeURL(
293+
type_url: String,
294+
function: String = #function,
295+
file: String = #fileID,
296+
line: Int = #line
297+
) -> SwiftProtobufError {
298+
SwiftProtobufError(
299+
code: .jsonEncodingError,
300+
message: "google.protobuf.Any 'type_url' was invalid: \(type_url).",
301+
location: SourceLocation(function: function, file: file, line: line)
302+
)
303+
}
304+
305+
/// While encoding a `google.protobuf.Any` encountered an empty `type_url` field.
306+
public static func emptyAnyTypeURL(
307+
function: String = #function,
308+
file: String = #fileID,
309+
line: Int = #line
310+
) -> SwiftProtobufError {
311+
SwiftProtobufError(
312+
code: .jsonEncodingError,
313+
message: "google.protobuf.Any 'type_url' was empty, only allowed for empty objects.",
314+
location: SourceLocation(function: function, file: file, line: line)
315+
)
316+
}
317+
}
269318
}

0 commit comments

Comments
 (0)