From 1971d448b7dae5c5748729e2ea4736c65d4d8a62 Mon Sep 17 00:00:00 2001 From: pawelmajcher <51097162+pawelmajcher@users.noreply.github.com> Date: Sun, 5 Oct 2025 03:14:55 +0200 Subject: [PATCH 1/3] Allow creating and merging messages using serialized bytes provided by RawSpan on platforms using Swift 6.2+ --- .../Message+BinaryAdditions.swift | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/Sources/SwiftProtobuf/Message+BinaryAdditions.swift b/Sources/SwiftProtobuf/Message+BinaryAdditions.swift index ee35f76da..f6814b28a 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 26, iOS 26, tvOS 26, visionOS 26, *) + 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 26, iOS 26, tvOS 26, visionOS 26, *) + 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 From cfcdbc16ad91f48aef9f819f7a850e63d940ef85 Mon Sep 17 00:00:00 2001 From: pawelmajcher <51097162+pawelmajcher@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:23:24 +0200 Subject: [PATCH 2/3] Support previous platform versions (RawSpan backport) --- Sources/SwiftProtobuf/Message+BinaryAdditions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftProtobuf/Message+BinaryAdditions.swift b/Sources/SwiftProtobuf/Message+BinaryAdditions.swift index f6814b28a..00522652d 100644 --- a/Sources/SwiftProtobuf/Message+BinaryAdditions.swift +++ b/Sources/SwiftProtobuf/Message+BinaryAdditions.swift @@ -116,7 +116,7 @@ extension Message { /// - options: The ``BinaryDecodingOptions`` to use. /// - Throws: ``BinaryDecodingError`` if decoding fails. @inlinable - @available(macOS 26, iOS 26, tvOS 26, visionOS 26, *) + @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, @@ -179,7 +179,7 @@ extension Message { /// - options: The ``BinaryDecodingOptions`` to use. /// - Throws: ``BinaryDecodingError`` if decoding fails. @inlinable - @available(macOS 26, iOS 26, tvOS 26, visionOS 26, *) + @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, From 98724404eef645a0f6fb87cc21d33833a6fc1ca3 Mon Sep 17 00:00:00 2001 From: pawelmajcher <51097162+pawelmajcher@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:44:27 +0200 Subject: [PATCH 3/3] Add tests to initializing and merging messages with RawSpan --- Tests/SwiftProtobufTests/Test_RawSpan.swift | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 Tests/SwiftProtobufTests/Test_RawSpan.swift 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