diff --git a/Sources/SwiftProtobuf/Message+BinaryAdditions.swift b/Sources/SwiftProtobuf/Message+BinaryAdditions.swift index ee35f76da..00522652d 100644 --- a/Sources/SwiftProtobuf/Message+BinaryAdditions.swift +++ b/Sources/SwiftProtobuf/Message+BinaryAdditions.swift @@ -100,6 +100,34 @@ extension Message { try merge(serializedBytes: bytes, extensions: extensions, partial: partial, options: options) } + #if compiler(>=6.2) + /// Creates a new message by decoding the bytes provided by a `RawSpan` + /// containing a serialized message in Protocol Buffer binary format. + /// + /// - Parameters: + /// - serializedBytes: The `RawSpan` of binary-encoded message data to decode. + /// - extensions: An ``ExtensionMap`` used to look up and decode any + /// extensions in this message or messages nested within this message's + /// fields. + /// - partial: If `false` (the default), this method will check + /// ``Message/isInitialized-6abgi`` after decoding to verify that all required + /// fields are present. If any are missing, this method throws + /// ``BinaryDecodingError/missingRequiredFields``. + /// - options: The ``BinaryDecodingOptions`` to use. + /// - Throws: ``BinaryDecodingError`` if decoding fails. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public init( + serializedBytes bytes: RawSpan, + extensions: (any ExtensionMap)? = nil, + partial: Bool = false, + options: BinaryDecodingOptions = BinaryDecodingOptions() + ) throws { + self.init() + try merge(serializedBytes: bytes, extensions: extensions, partial: partial, options: options) + } + #endif + /// Updates the message by decoding the given `SwiftProtobufContiguousBytes` value /// containing a serialized message in Protocol Buffer binary format into the /// receiver. @@ -131,6 +159,39 @@ extension Message { } } + #if compiler(>=6.2) + /// Updates the message by decoding the bytes provided by a `RawSpan` containing + /// a serialized message in Protocol Buffer binary format into the receiver. + /// + /// - Note: If this method throws an error, the message may still have been + /// partially mutated by the binary data that was decoded before the error + /// occurred. + /// + /// - Parameters: + /// - serializedBytes: The `RawSpan` of binary-encoded message data to decode. + /// - extensions: An ``ExtensionMap`` used to look up and decode any + /// extensions in this message or messages nested within this message's + /// fields. + /// - partial: If `false` (the default), this method will check + /// ``Message/isInitialized-6abgi`` after decoding to verify that all required + /// fields are present. If any are missing, this method throws + /// ``BinaryDecodingError/missingRequiredFields``. + /// - options: The ``BinaryDecodingOptions`` to use. + /// - Throws: ``BinaryDecodingError`` if decoding fails. + @inlinable + @available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) + public mutating func merge( + serializedBytes bytes: RawSpan, + extensions: (any ExtensionMap)? = nil, + partial: Bool = false, + options: BinaryDecodingOptions = BinaryDecodingOptions() + ) throws { + try bytes.withUnsafeBytes { (body: UnsafeRawBufferPointer) in + try _merge(rawBuffer: body, extensions: extensions, partial: partial, options: options) + } + } + #endif + // Helper for `merge()`s to keep the Decoder internal to SwiftProtobuf while // allowing the generic over `SwiftProtobufContiguousBytes` to get better codegen from the // compiler by being `@inlinable`. For some discussion on this see diff --git a/Tests/SwiftProtobufTests/Test_RawSpan.swift b/Tests/SwiftProtobufTests/Test_RawSpan.swift new file mode 100644 index 000000000..8ea0b7a82 --- /dev/null +++ b/Tests/SwiftProtobufTests/Test_RawSpan.swift @@ -0,0 +1,84 @@ +import Foundation +import SwiftProtobuf +import XCTest + +#if compiler(>=6.2) + +final class Test_RawSpan: XCTestCase { + func testEmptyRawSpan() throws { + guard #available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) else { + throw XCTSkip("Span structs not available on selected platform") + } + + let emptyRawSpan = RawSpan() + + let decoded = try SwiftProtoTesting_TestAllTypes(serializedBytes: emptyRawSpan) + let expected = SwiftProtoTesting_TestAllTypes() + + XCTAssertEqual(decoded, expected, "Empty span should decode to equal empty message") + } + + func testRawSpanReencodedEmptyByteArray() throws { + guard #available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *) else { + throw XCTSkip("span.bytes not available on selected platform") + } + + let expected: [UInt8] = [] + let expectedRawSpan = expected.span.bytes + + let decoded = try SwiftProtoTesting_TestAllTypes(serializedBytes: expectedRawSpan) + let reencoded: [UInt8] = try decoded.serializedBytes() + + XCTAssertEqual( + reencoded, + expected, + "Raw span of empty array of bytes should decode and encode as empty message" + ) + } + + func testRawSpanDataEncodeDecode() throws { + guard #available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) else { + throw XCTSkip("Span structs not available on selected platform") + } + + let expected = SwiftProtoTesting_TestAllTypes.with { + $0.optionalInt32 = 1 + $0.optionalInt64 = Int64.max + $0.optionalString = "RawSpan test" + $0.repeatedBool = [true, false] + } + + let encoded: Data = try expected.serializedBytes() + let encodedRawSpan: RawSpan = encoded.bytes + + let decoded = try SwiftProtoTesting_TestAllTypes(serializedBytes: encodedRawSpan) + + XCTAssertEqual(decoded, expected, "") + } + + func testRawSpanTruncated() throws { + guard #available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) else { + throw XCTSkip("Span structs not available on selected platform") + } + + let expected = SwiftProtoTesting_TestAllTypes.with { + $0.optionalInt32 = 1 + $0.optionalInt64 = Int64.max + $0.optionalString = "RawSpan test" + $0.repeatedBool = [true, false] + } + + let encoded: Data = try expected.serializedBytes() + let truncatedRawSpan: RawSpan = encoded.bytes.extracting(droppingLast: 1) + + var decoded = SwiftProtoTesting_TestAllTypes() + + XCTAssertThrowsError( + try decoded.merge(serializedBytes: truncatedRawSpan) + ) { error in + XCTAssertEqual(error as? BinaryDecodingError, BinaryDecodingError.truncated) + } + } +} + +#endif