diff --git a/wire-runtime-swift/src/main/swift/ProtoCodable/ProtoReader.swift b/wire-runtime-swift/src/main/swift/ProtoCodable/ProtoReader.swift index 316333af35..8b43316f95 100644 --- a/wire-runtime-swift/src/main/swift/ProtoCodable/ProtoReader.swift +++ b/wire-runtime-swift/src/main/swift/ProtoCodable/ProtoReader.swift @@ -788,7 +788,9 @@ public final class ProtoReader { func readBuffer() throws -> UnsafeRawBufferPointer { guard case let .lengthDelimited(length) = state else { - fatalError("Decoding field as length delimited when key was not LENGTH_DELIMITED") + throw ProtoDecoder.Error.invalidStructure( + message: "Decoding field as length delimited when key was not LENGTH_DELIMITED" + ) } state = .tag @@ -798,7 +800,9 @@ public final class ProtoReader { /** Reads a `bytes` field value from the stream. The length is read from the stream prior to the actual data. */ func readData() throws -> Data { guard case let .lengthDelimited(length) = state else { - fatalError("Decoding field as length delimited when key was not LENGTH_DELIMITED") + throw ProtoDecoder.Error.invalidStructure( + message: "Decoding field as length delimited when key was not LENGTH_DELIMITED" + ) } state = .tag @@ -807,21 +811,33 @@ public final class ProtoReader { /** Reads a 32-bit little-endian integer from the stream. */ func readFixed32() throws -> UInt32 { - precondition(state == .fixed32 || state == .packedValue) + guard state == .fixed32 || state == .packedValue else { + throw ProtoDecoder.Error.invalidStructure( + message: "Decoding field as fixed32 when key was not FIXED32" + ) + } state = .tag return try buffer.readFixed32() } /** Reads a 64-bit little-endian integer from the stream. */ func readFixed64() throws -> UInt64 { - precondition(state == .fixed64 || state == .packedValue) + guard state == .fixed64 || state == .packedValue else { + throw ProtoDecoder.Error.invalidStructure( + message: "Decoding field as fixed64 when key was not FIXED64" + ) + } state = .tag return try buffer.readFixed64() } /** Reads a raw varint up to 64 bits in length from the stream. */ func readVarint() throws -> UInt64 { - precondition(state == .varint || state == .packedValue) + guard state == .varint || state == .packedValue else { + throw ProtoDecoder.Error.invalidStructure( + message: "Decoding field as varint when key was not VARINT" + ) + } state = .tag return try buffer.readVarint() diff --git a/wire-runtime-swift/src/test/swift/ProtoDecoderTests.swift b/wire-runtime-swift/src/test/swift/ProtoDecoderTests.swift index e25d6d1631..b281162735 100644 --- a/wire-runtime-swift/src/test/swift/ProtoDecoderTests.swift +++ b/wire-runtime-swift/src/test/swift/ProtoDecoderTests.swift @@ -67,6 +67,26 @@ final class ProtoDecoderTests: XCTestCase { } } + func testDecodeRejectsKnownFieldWithMismatchedWireType() throws { + // Person declares field 1 `name` as a string and field 2 `id` as a varint int32. + let payloads = [ + "0A0161_1500000000", // id sent as fixed32 + "0A0161_110000000000000000", // id sent as fixed64 + "0A0161_1200", // id sent as length delimited + "0801", // name sent as varint + "0D00000000", // name sent as fixed32 + ] + + for payload in payloads { + let data = Foundation.Data(hexEncoded: payload)! + XCTAssertThrowsError(try ProtoDecoder().decode(Person.self, from: data), payload) { error in + guard case ProtoDecoder.Error.invalidStructure = error else { + return XCTFail("Unexpected error for \(payload): \(error)") + } + } + } + } + func testDecodeEmptySizeDelimitedData() throws { let decoder = ProtoDecoder() let object = try decoder.decodeSizeDelimited(SimpleOptional2.self, from: Foundation.Data()) diff --git a/wire-runtime-swift/src/test/swift/ProtoReaderTests.swift b/wire-runtime-swift/src/test/swift/ProtoReaderTests.swift index 1342e77d38..b6669ba9d3 100644 --- a/wire-runtime-swift/src/test/swift/ProtoReaderTests.swift +++ b/wire-runtime-swift/src/test/swift/ProtoReaderTests.swift @@ -1202,6 +1202,91 @@ final class ProtoReaderTests: XCTestCase { } } + func testReadBufferRejectsMismatchedWireType() throws { + let data = Foundation.Data(hexEncoded: """ + 08 // (Tag 1 | Varint) + 01 // Value 1 + """)! + + XCTAssertThrowsError( + try test(data: data) { reader in + _ = try reader.forEachTag { _ in + _ = try reader.readBuffer() + } + } + ) { error in + assertInvalidStructureError(error, message: "Decoding field as length delimited when key was not LENGTH_DELIMITED") + } + } + + func testReadDataRejectsMismatchedWireType() throws { + let data = Foundation.Data(hexEncoded: """ + 0D // (Tag 1 | Fixed32) + 00000000 // Value 0 + """)! + + XCTAssertThrowsError( + try test(data: data) { reader in + _ = try reader.forEachTag { _ in + _ = try reader.readData() + } + } + ) { error in + assertInvalidStructureError(error, message: "Decoding field as length delimited when key was not LENGTH_DELIMITED") + } + } + + func testReadFixed32RejectsMismatchedWireType() throws { + let data = Foundation.Data(hexEncoded: """ + 08 // (Tag 1 | Varint) + 01 // Value 1 + """)! + + XCTAssertThrowsError( + try test(data: data) { reader in + _ = try reader.forEachTag { _ in + _ = try reader.readFixed32() + } + } + ) { error in + assertInvalidStructureError(error, message: "Decoding field as fixed32 when key was not FIXED32") + } + } + + func testReadFixed64RejectsMismatchedWireType() throws { + let data = Foundation.Data(hexEncoded: """ + 0D // (Tag 1 | Fixed32) + 00000000 // Value 0 + """)! + + XCTAssertThrowsError( + try test(data: data) { reader in + _ = try reader.forEachTag { _ in + _ = try reader.readFixed64() + } + } + ) { error in + assertInvalidStructureError(error, message: "Decoding field as fixed64 when key was not FIXED64") + } + } + + func testReadVarintRejectsMismatchedWireType() throws { + let data = Foundation.Data(hexEncoded: """ + 0A // (Tag 1 | Length Delimited) + 00 // Length 0 + """)! + + XCTAssertThrowsError( + try test(data: data) { reader in + _ = try reader.forEachTag { _ in + _ = try reader.readVarint() + } + } + ) { error in + assertInvalidStructureError(error, message: "Decoding field as varint when key was not VARINT") + } + } + func testNestedMessageRejectsOversizedLength() throws { let data = Foundation.Data(hexEncoded: """ 12 // (Tag 2 | Length Delimited) @@ -1520,6 +1605,14 @@ final class ProtoReaderTests: XCTestCase { XCTAssertEqual(message, "Negative length: -128. Reader position: \(readerPosition). Last read tag: 1.") } + private func assertInvalidStructureError(_ error: Error, message expectedMessage: String) { + guard case let ProtoDecoder.Error.invalidStructure(message) = error else { + XCTFail("Unexpected error: \(error)") + return + } + XCTAssertEqual(message, expectedMessage) + } + private func assertUnexpectedEndError(_ error: Error) { guard case ProtoDecoder.Error.unexpectedEndOfData = error else { XCTFail("Unexpected error: \(error)")