diff --git a/Sources/SwiftProtobuf/BinaryDecodingError.swift b/Sources/SwiftProtobuf/BinaryDecodingError.swift index a0ee0c6a5..5e1bb6d3d 100644 --- a/Sources/SwiftProtobuf/BinaryDecodingError.swift +++ b/Sources/SwiftProtobuf/BinaryDecodingError.swift @@ -23,4 +23,8 @@ public enum BinaryDecodingError: Error { case malformedProtobuf /// The data being parsed does not match the type specified in the proto file case schemaMismatch + /// The message or nested messages definitions have required fields, and the + /// binary data did not include values for them. The `partial` support will + /// allow this incomplete data to be decoded. + case missingRequiredFields } diff --git a/Sources/SwiftProtobuf/BinaryTypeAdditions.swift b/Sources/SwiftProtobuf/BinaryTypeAdditions.swift index 47999fe93..5eefd475e 100644 --- a/Sources/SwiftProtobuf/BinaryTypeAdditions.swift +++ b/Sources/SwiftProtobuf/BinaryTypeAdditions.swift @@ -268,7 +268,20 @@ public extension Message { return visitor.serializedSize } - init(serializedData data: Data, extensions: ExtensionSet? = nil) throws { + /// Initializes the message by decoding the Protocol Buffer binary serialization + /// format for this message. + /// + /// - Parameters: + /// - serializedData: The binary serialization data to decode. + /// - extensions: An `ExtensionSet` to look up and decode any extensions in this + /// message or messages nested within this message's fields. + /// - partial: By default, the binary serialization format requires all `required` + /// fields be present; when `partial` is `false`, + /// `BinaryDecodingError.missingRequiredFields` is thrown if any were missing. + /// When `partial` is `true`, then partial messages are allowed, and + /// `Message.isInitialized` is not checked. + /// - Throws: An instance of `BinaryDecodingError` on failure. + init(serializedData data: Data, extensions: ExtensionSet? = nil, partial: Bool = false) throws { self.init() if !data.isEmpty { try data.withUnsafeBytes { (pointer: UnsafePointer) in @@ -277,6 +290,9 @@ public extension Message { extensions: extensions) } } + if !partial && !isInitialized { + throw BinaryDecodingError.missingRequiredFields + } } } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 1d563dc75..36fc4e375 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -781,7 +781,8 @@ extension Test_Required { ("test_NestedInProto2_IsInitialized", {try run_test(test:($0 as! Test_Required).test_NestedInProto2_IsInitialized)}), ("test_NestedInProto3_IsInitialized", {try run_test(test:($0 as! Test_Required).test_NestedInProto3_IsInitialized)}), ("test_map_isInitialized", {try run_test(test:($0 as! Test_Required).test_map_isInitialized)}), - ("test_Extensions_isInitialized", {try run_test(test:($0 as! Test_Required).test_Extensions_isInitialized)}) + ("test_Extensions_isInitialized", {try run_test(test:($0 as! Test_Required).test_Extensions_isInitialized)}), + ("test_decodeRequired", {try run_test(test:($0 as! Test_Required).test_decodeRequired)}) ] } } @@ -789,6 +790,7 @@ extension Test_Required { extension Test_SmallRequired { static var allTests: [(String, (XCTestCase) throws -> ())] { return [ + ("test_decodeRequired", {try run_test(test:($0 as! Test_SmallRequired).test_decodeRequired)}) ] } } diff --git a/Tests/SwiftProtobufTests/Test_Required.swift b/Tests/SwiftProtobufTests/Test_Required.swift index 5c1f976bc..934c84ee7 100644 --- a/Tests/SwiftProtobufTests/Test_Required.swift +++ b/Tests/SwiftProtobufTests/Test_Required.swift @@ -30,97 +30,11 @@ import Foundation import XCTest -// TODO(#98): The tests in this class are currently disabled since they are -// verifying incorrect behavior that needs to be fixed and were broken by -// another change. +import SwiftProtobuf + class Test_Required: XCTestCase, PBTestHelpers { typealias MessageTestType = ProtobufUnittest_TestAllRequiredTypes - let expected: [UInt8] = [ - 8, 0, - 16, 0, - 24, 0, - 32, 0, - 40, 0, - 48, 0, - 61, 0, 0, 0, 0, - 65, 0, 0, 0, 0, 0, 0, 0, 0, - 77, 0, 0, 0, 0, - 81, 0, 0, 0, 0, 0, 0, 0, 0, - 93, 0, 0, 0, 0, - 97, 0, 0, 0, 0, 0, 0, 0, 0, - 104, 0, - 114, 0, - 122, 0, - 168, 1, 1, // required_nested_enum - 176, 1, 4, - 184, 1, 7, - 194, 1, 0, // required_string_piece - 202, 1, 0, // required_cord - 232, 3, 41, // required default_int32 - 240, 3, 42, - 248, 3, 43, - 128, 4, 44, - 136, 4, 89, - 144, 4, 92, - 157, 4, 47, 0, 0, 0, - 161, 4, 48, 0, 0, 0, 0, 0, 0, 0, - 173, 4, 49, 0, 0, 0, - 177, 4, 206, 255, 255, 255, 255, 255, 255, 255, - 189, 4, 0, 0, 78, 66, - 193, 4, 0, 0, 0, 0, 0, 100, 233, 64, - 200, 4, 1, - 210, 4, 5, 104, 101, 108, 108, 111, - 218, 4, 5, 119, 111, 114, 108, 100, - 136, 5, 2, - 144, 5, 5, - 152, 5, 8, - 162, 5, 3, 97, 98, 99, - 170, 5, 3, 49, 50, 51] - - let expectedJSON = ("{" - + "\"requiredInt32\":0," - + "\"requiredInt64\":\"0\"," - + "\"requiredUint32\":0," - + "\"requiredUint64\":\"0\"," - + "\"requiredSint32\":0," - + "\"requiredSint64\":\"0\"," - + "\"requiredFixed32\":0," - + "\"requiredFixed64\":\"0\"," - + "\"requiredSfixed32\":0," - + "\"requiredSfixed64\":\"0\"," - + "\"requiredFloat\":0," - + "\"requiredDouble\":0," - + "\"requiredBool\":false," - + "\"requiredString\":\"\"," - + "\"requiredBytes\":\"\"," - + "\"requiredNestedEnum\":\"FOO\"," - + "\"requiredForeignEnum\":\"FOREIGN_FOO\"," - + "\"requiredImportEnum\":\"IMPORT_FOO\"," - + "\"requiredStringPiece\":\"\"," - + "\"requiredCord\":\"\"," - + "\"defaultInt32\":41," - + "\"defaultInt64\":\"42\"," - + "\"defaultUint32\":43," - + "\"defaultUint64\":\"44\"," - + "\"defaultSint32\":-45," - + "\"defaultSint64\":\"46\"," - + "\"defaultFixed32\":47," - + "\"defaultFixed64\":\"48\"," - + "\"defaultSfixed32\":49," - + "\"defaultSfixed64\":\"-50\"," - + "\"defaultFloat\":51.5," - + "\"defaultDouble\":52000," - + "\"defaultBool\":true," - + "\"defaultString\":\"hello\"," - + "\"defaultBytes\":\"d29ybGQ=\"," - + "\"defaultNestedEnum\":\"BAR\"," - + "\"defaultForeignEnum\":\"FOREIGN_BAR\"," - + "\"defaultImportEnum\":\"IMPORT_BAR\"," - + "\"defaultStringPiece\":\"abc\"," - + "\"defaultCord\":\"123\"" - + "}") - func test_IsInitialized() { // message declared in proto2 syntax file with required fields. var msg = ProtobufUnittest_TestRequired() @@ -226,113 +140,162 @@ class Test_Required: XCTestCase, PBTestHelpers { XCTAssertTrue(msg.isInitialized) } - func DISABLED_test_bare() throws { - // Because we always encode required fields, we get a non-trivial - // output even for a bare object. - let o = MessageTestType() - XCTAssertEqual(try o.serializedBytes(), expected) - XCTAssertEqual(try o.jsonString(), expectedJSON) - } - - func DISABLED_test_requiredInt32() { - var a = expected - a[1] = 1 - assertEncode(a) {(o: inout MessageTestType) in - o.requiredInt32 = 1 - } - assertDecodeSucceeds([8, 2]) { - let val: Int32 = $0.requiredInt32 // Verify non-optional - return val == 2 + // Helper to assert decoding fails with a not initialized error. + fileprivate func assertDecodeFailsNotInitialized(_ bytes: [UInt8], file: XCTestFileArgType = #file, line: UInt = #line) { + do { + let _ = try MessageTestType(serializedData: Data(bytes: bytes)) + XCTFail("Swift decode should have failed: \(bytes)", file: file, line: line) + } catch BinaryDecodingError.missingRequiredFields { + // Correct error! + } catch let e { + XCTFail("Decoding \(bytes) got wrong error: \(e)", file: file, line: line) } } - func DISABLED_test_requiredFloat() { - var a = expected - a[44] = 63 // float value is 0, 0, 0, 63 - assertEncode(a) {(o: inout MessageTestType) in - o.requiredFloat = 0.5 - } - assertDecodeSucceeds([93, 0, 0, 0, 0]) { - let val: Float = $0.requiredFloat // Verify non-optional - return val == 0.0 + // Helper to assert decoding partial succeeds. + fileprivate func assertPartialDecodeSucceeds(_ bytes: [UInt8], _ expectedTextFormat: String, file: XCTestFileArgType = #file, line: UInt = #line) { + do { + let msg = try MessageTestType(serializedData: Data(bytes: bytes), partial: true) + var expected = "SwiftProtobufTests.ProtobufUnittest_TestAllRequiredTypes:\n" + if !expectedTextFormat.isEmpty { + expected += expectedTextFormat + "\n" + } + XCTAssertEqual(msg.debugDescription, expected, "While decoding \(bytes)", file: file, line: line) + } catch let e { + XCTFail("Decoding \(bytes) failed with error: \(e)", file: file, line: line) } } - func DISABLED_test_requiredString() { - // Splice the expected value for this field - let prefix = expected[0..<56] - let field: [UInt8] = [114, 1, 97] - let suffix = expected[58..