diff --git a/Libraries/Connect/Internal/Interceptors/ConnectInterceptor.swift b/Libraries/Connect/Internal/Interceptors/ConnectInterceptor.swift index eacb7224..171be9e9 100644 --- a/Libraries/Connect/Internal/Interceptors/ConnectInterceptor.swift +++ b/Libraries/Connect/Internal/Interceptors/ConnectInterceptor.swift @@ -158,7 +158,7 @@ extension ConnectInterceptor: StreamInterceptor { @Sendable func handleStreamRawInput(_ input: Data, proceed: @escaping (Data) -> Void) { - proceed(Envelope.packMessage(input, using: self.config.requestCompression)) + proceed(Envelope._packMessage(input, using: self.config.requestCompression)) } @Sendable @@ -190,7 +190,7 @@ extension ConnectInterceptor: StreamInterceptor { let responseCompressionPool = self.streamResponseHeaders.value?[ HeaderConstants.connectStreamingContentEncoding ]?.first.flatMap { self.config.responseCompressionPool(forName: $0) } - if responseCompressionPool == nil && Envelope.isCompressed(data) { + if responseCompressionPool == nil && Envelope._isCompressed(data) { proceed(.complete( code: .internalError, error: ConnectError( code: .internalError, message: "received unexpected compressed message" @@ -199,7 +199,7 @@ extension ConnectInterceptor: StreamInterceptor { return } - let (headerByte, message) = try Envelope.unpackMessage( + let (headerByte, message) = try Envelope._unpackMessage( data, compressionPool: responseCompressionPool ) let isEndStream = 0b00000010 & headerByte != 0 diff --git a/Libraries/Connect/Internal/Interceptors/GRPCWebInterceptor.swift b/Libraries/Connect/Internal/Interceptors/GRPCWebInterceptor.swift index eb64e7ed..c47ef6c6 100644 --- a/Libraries/Connect/Internal/Interceptors/GRPCWebInterceptor.swift +++ b/Libraries/Connect/Internal/Interceptors/GRPCWebInterceptor.swift @@ -32,12 +32,12 @@ extension GRPCWebInterceptor: UnaryInterceptor { proceed: @escaping (Result, ConnectError>) -> Void ) { // gRPC-Web unary payloads are enveloped. - let envelopedRequestBody = Envelope.packMessage( + let envelopedRequestBody = Envelope._packMessage( request.message ?? Data(), using: self.config.requestCompression ) proceed(.success(HTTPRequest( url: request.url, - headers: request.headers.addingGRPCHeaders(using: self.config, grpcWeb: true), + headers: request.headers._addingGRPCHeaders(using: self.config, grpcWeb: true), message: envelopedRequestBody, method: request.method, trailers: nil, @@ -57,7 +57,7 @@ extension GRPCWebInterceptor: UnaryInterceptor { } guard let responseData = response.message, !responseData.isEmpty else { - let (grpcCode, connectError) = ConnectError.parseGRPCHeaders( + let (grpcCode, connectError) = ConnectError._parseGRPCHeaders( response.headers, trailers: response.trailers ) @@ -102,7 +102,7 @@ extension GRPCWebInterceptor: UnaryInterceptor { let compressionPool = response.headers[HeaderConstants.grpcContentEncoding]? .first .flatMap { self.config.responseCompressionPool(forName: $0) } - if compressionPool == nil && Envelope.isCompressed(responseData) { + if compressionPool == nil && Envelope._isCompressed(responseData) { proceed(HTTPResponse( code: .internalError, headers: response.headers, message: nil, trailers: response.trailers, @@ -120,9 +120,9 @@ extension GRPCWebInterceptor: UnaryInterceptor { // message data. // 2. The (headers and length prefixed) trailers data. // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md - let firstChunkLength = Envelope.messageLength(forPackedData: responseData) - let prefixedFirstChunkLength = Envelope.prefixLength + firstChunkLength - let firstChunk = try Envelope.unpackMessage( + let firstChunkLength = Envelope._messageLength(forPackedData: responseData) + let prefixedFirstChunkLength = Envelope._prefixLength + firstChunkLength + let firstChunk = try Envelope._unpackMessage( Data(responseData.prefix(upTo: prefixedFirstChunkLength)), compressionPool: compressionPool ) @@ -135,7 +135,7 @@ extension GRPCWebInterceptor: UnaryInterceptor { } else { let trailersData = Data(responseData.suffix(from: prefixedFirstChunkLength)) let unpackedTrailers = try Trailers.fromGRPCHeadersBlock( - try Envelope.unpackMessage( + try Envelope._unpackMessage( trailersData, compressionPool: compressionPool ).unpacked ) @@ -165,7 +165,7 @@ extension GRPCWebInterceptor: StreamInterceptor { ) { proceed(.success(HTTPRequest( url: request.url, - headers: request.headers.addingGRPCHeaders(using: self.config, grpcWeb: true), + headers: request.headers._addingGRPCHeaders(using: self.config, grpcWeb: true), message: request.message, method: request.method, trailers: nil, @@ -175,7 +175,7 @@ extension GRPCWebInterceptor: StreamInterceptor { @Sendable func handleStreamRawInput(_ input: Data, proceed: @escaping (Data) -> Void) { - proceed(Envelope.packMessage(input, using: self.config.requestCompression)) + proceed(Envelope._packMessage(input, using: self.config.requestCompression)) } @Sendable @@ -198,11 +198,11 @@ extension GRPCWebInterceptor: StreamInterceptor { return } - if let grpcCode = headers.grpcStatus() { + if let grpcCode = headers._grpcStatus() { // Headers-only response. proceed(.complete( code: grpcCode, - error: ConnectError.parseGRPCHeaders(nil, trailers: headers).error, + error: ConnectError._parseGRPCHeaders(nil, trailers: headers).error, trailers: headers )) } else { @@ -215,13 +215,13 @@ extension GRPCWebInterceptor: StreamInterceptor { let responseCompressionPool = self.streamResponseHeaders.value?[ HeaderConstants.grpcContentEncoding ]?.first.flatMap { self.config.responseCompressionPool(forName: $0) } - let (headerByte, unpackedData) = try Envelope.unpackMessage( + let (headerByte, unpackedData) = try Envelope._unpackMessage( data, compressionPool: responseCompressionPool ) let isTrailers = 0b10000000 & headerByte != 0 if isTrailers { let trailers = try Trailers.fromGRPCHeadersBlock(unpackedData) - let (grpcCode, error) = ConnectError.parseGRPCHeaders( + let (grpcCode, error) = ConnectError._parseGRPCHeaders( self.streamResponseHeaders.value, trailers: trailers ) if grpcCode == .ok { @@ -290,7 +290,7 @@ private extension Trailers { private extension HTTPResponse { func withHandledGRPCWebTrailers(_ trailers: Trailers, message: Data?) -> Self { - let (grpcCode, error) = ConnectError.parseGRPCHeaders(self.headers, trailers: trailers) + let (grpcCode, error) = ConnectError._parseGRPCHeaders(self.headers, trailers: trailers) if grpcCode != .ok || error != nil { return HTTPResponse( // Rewrite the gRPC code if it is "ok" but `connectError` is non-nil. diff --git a/Libraries/Connect/PackageInternal/ConnectError+GRPC.swift b/Libraries/Connect/PackageInternal/ConnectError+GRPC.swift index 38a83f6d..54f96adf 100644 --- a/Libraries/Connect/PackageInternal/ConnectError+GRPC.swift +++ b/Libraries/Connect/PackageInternal/ConnectError+GRPC.swift @@ -25,11 +25,16 @@ extension ConnectError { /// passed in the headers block for gRPC-Web. /// /// - returns: A tuple containing the gRPC status code and an optional error. - public static func parseGRPCHeaders( + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public static func _parseGRPCHeaders( _ headers: Headers?, trailers: Trailers? ) -> (grpcCode: Code, error: ConnectError?) { // "Trailers-only" responses can be sent in the headers or trailers block. - guard let grpcCode = trailers?.grpcStatus() ?? headers?.grpcStatus() else { + guard let grpcCode = trailers?._grpcStatus() ?? headers?._grpcStatus() else { return (.unknown, ConnectError(code: .unknown, message: "RPC response missing status")) } diff --git a/Libraries/Connect/PackageInternal/Envelope.swift b/Libraries/Connect/PackageInternal/Envelope.swift index e8378134..68c479cf 100644 --- a/Libraries/Connect/PackageInternal/Envelope.swift +++ b/Libraries/Connect/PackageInternal/Envelope.swift @@ -19,9 +19,19 @@ import SwiftProtobuf /// to change. When the compiler supports it, this should be package-internal.** /// /// Provides functionality for packing and unpacking (headers and length prefixed) messages. +@available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." +) public enum Envelope { /// The total number of bytes that will prefix a message. - public static var prefixLength: Int { + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public static var _prefixLength: Int { return 5 // Header flags (1 byte) + message length (4 bytes) } @@ -35,7 +45,12 @@ public enum Envelope { /// - parameter compression: Configuration to use for compressing the message. /// /// - returns: Serialized/enveloped data for transmission. - public static func packMessage( + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public static func _packMessage( _ source: Data, using compression: ProtocolClientConfig.RequestCompression? ) -> Data { var buffer = Data() @@ -70,7 +85,12 @@ public enum Envelope { /// /// - returns: A tuple that includes the header byte and the un-prefixed and decompressed /// message. - public static func unpackMessage( + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public static func _unpackMessage( _ source: Data, compressionPool: CompressionPool? ) throws -> (headerByte: UInt8, unpacked: Data) { if source.isEmpty { @@ -79,7 +99,7 @@ public enum Envelope { let headerByte = source[0] let isCompressed = 0b00000001 & headerByte != 0 - let messageData = Data(source.dropFirst(self.prefixLength)) + let messageData = Data(source.dropFirst(self._prefixLength)) if isCompressed { guard let compressionPool = compressionPool else { throw Error.missingExpectedCompressionPool @@ -96,7 +116,12 @@ public enum Envelope { /// - parameter packedData: The packed data to analyze. /// /// - returns: True if the data is compressed. - public static func isCompressed(_ packedData: Data) -> Bool { + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public static func _isCompressed(_ packedData: Data) -> Bool { return !packedData.isEmpty && (0b00000001 & packedData[0] != 0) } @@ -111,8 +136,13 @@ public enum Envelope { /// - returns: The length of the next expected message in the packed data. If multiple chunks /// are specified, this will return the length of the first. Returns -1 if there is /// not enough prefix data to determine the message length. - public static func messageLength(forPackedData data: Data) -> Int { - guard data.count >= self.prefixLength else { + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public static func _messageLength(forPackedData data: Data) -> Int { + guard data.count >= self._prefixLength else { return -1 } diff --git a/Libraries/Connect/PackageInternal/Headers+GRPC.swift b/Libraries/Connect/PackageInternal/Headers+GRPC.swift index a9cace45..08235204 100644 --- a/Libraries/Connect/PackageInternal/Headers+GRPC.swift +++ b/Libraries/Connect/PackageInternal/Headers+GRPC.swift @@ -25,7 +25,12 @@ extension Headers { /// - parameter grpcWeb: Should be true if using gRPC-Web, false if gRPC. /// /// - returns: A set of updated headers. - public func addingGRPCHeaders(using config: ProtocolClientConfig, grpcWeb: Bool) -> Self { + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public func _addingGRPCHeaders(using config: ProtocolClientConfig, grpcWeb: Bool) -> Self { var headers = self headers[HeaderConstants.grpcAcceptEncoding] = config .acceptCompressionPoolNames() diff --git a/Libraries/Connect/PackageInternal/Trailers+gRPC.swift b/Libraries/Connect/PackageInternal/Trailers+gRPC.swift index b994d9cf..15ec21f7 100644 --- a/Libraries/Connect/PackageInternal/Trailers+gRPC.swift +++ b/Libraries/Connect/PackageInternal/Trailers+gRPC.swift @@ -19,7 +19,12 @@ extension Trailers { /// Identifies the status code from gRPC and gRPC-Web trailers. /// /// - returns: The gRPC status code, if specified. - public func grpcStatus() -> Code? { + @available( + swift, + deprecated: 100.0, + message: "This is an internal-only API which will be made package-private in Swift 6." + ) + public func _grpcStatus() -> Code? { return self[HeaderConstants.grpcStatus]? .first .flatMap(Int.init) diff --git a/Libraries/Connect/Public/Implementation/Clients/ProtocolClient.swift b/Libraries/Connect/Public/Implementation/Clients/ProtocolClient.swift index 7fd6f858..27f1c294 100644 --- a/Libraries/Connect/Public/Implementation/Clients/ProtocolClient.swift +++ b/Libraries/Connect/Public/Implementation/Clients/ProtocolClient.swift @@ -299,12 +299,12 @@ extension ProtocolClient: ProtocolClientInterface { // Handle cases where multiple messages are received in a single chunk. responseBuffer += data while true { - let messageLength = Envelope.messageLength(forPackedData: responseBuffer) + let messageLength = Envelope._messageLength(forPackedData: responseBuffer) if messageLength < 0 { return } - let prefixedMessageLength = Envelope.prefixLength + messageLength + let prefixedMessageLength = Envelope._prefixLength + messageLength guard responseBuffer.count >= prefixedMessageLength else { return } diff --git a/Libraries/ConnectNIO/Internal/GRPCInterceptor.swift b/Libraries/ConnectNIO/Internal/GRPCInterceptor.swift index b6b5e377..4eff0710 100644 --- a/Libraries/ConnectNIO/Internal/GRPCInterceptor.swift +++ b/Libraries/ConnectNIO/Internal/GRPCInterceptor.swift @@ -34,13 +34,13 @@ extension GRPCInterceptor: UnaryInterceptor { proceed: @escaping (Result, ConnectError>) -> Void ) { // gRPC unary payloads are enveloped. - let envelopedRequestBody = Envelope.packMessage( + let envelopedRequestBody = Envelope._packMessage( request.message ?? Data(), using: self.config.requestCompression ) proceed(.success(HTTPRequest( url: request.url, - headers: request.headers.addingGRPCHeaders(using: self.config, grpcWeb: false), + headers: request.headers._addingGRPCHeaders(using: self.config, grpcWeb: false), message: envelopedRequestBody, method: request.method, trailers: nil, @@ -72,12 +72,12 @@ extension GRPCInterceptor: UnaryInterceptor { return } - let (grpcCode, connectError) = ConnectError.parseGRPCHeaders( + let (grpcCode, connectError) = ConnectError._parseGRPCHeaders( response.headers, trailers: response.trailers ) guard grpcCode == .ok, let rawData = response.message, !rawData.isEmpty else { - if response.trailers.grpcStatus() == nil && response.message?.isEmpty == false { + if response.trailers._grpcStatus() == nil && response.message?.isEmpty == false { proceed(HTTPResponse( code: .internalError, headers: response.headers, @@ -117,7 +117,7 @@ extension GRPCInterceptor: UnaryInterceptor { .headers[HeaderConstants.grpcContentEncoding]? .first .flatMap { self.config.responseCompressionPool(forName: $0) } - if compressionPool == nil && Envelope.isCompressed(rawData) { + if compressionPool == nil && Envelope._isCompressed(rawData) { proceed(HTTPResponse( code: .internalError, headers: response.headers, message: nil, trailers: response.trailers, error: ConnectError( @@ -140,7 +140,7 @@ extension GRPCInterceptor: UnaryInterceptor { } do { - let messageData = try Envelope.unpackMessage( + let messageData = try Envelope._unpackMessage( rawData, compressionPool: compressionPool ).unpacked proceed(HTTPResponse( @@ -172,7 +172,7 @@ extension GRPCInterceptor: StreamInterceptor { ) { proceed(.success(HTTPRequest( url: request.url, - headers: request.headers.addingGRPCHeaders(using: self.config, grpcWeb: false), + headers: request.headers._addingGRPCHeaders(using: self.config, grpcWeb: false), message: request.message, method: request.method, trailers: nil, @@ -182,7 +182,7 @@ extension GRPCInterceptor: StreamInterceptor { @Sendable func handleStreamRawInput(_ input: Data, proceed: @escaping (Data) -> Void) { - proceed(Envelope.packMessage(input, using: self.config.requestCompression)) + proceed(Envelope._packMessage(input, using: self.config.requestCompression)) } @Sendable @@ -214,7 +214,7 @@ extension GRPCInterceptor: StreamInterceptor { let responseCompressionPool = self.streamResponseHeaders.value?[ HeaderConstants.grpcContentEncoding ]?.first.flatMap { self.config.responseCompressionPool(forName: $0) } - if responseCompressionPool == nil && Envelope.isCompressed(rawData) { + if responseCompressionPool == nil && Envelope._isCompressed(rawData) { proceed(.complete( code: .internalError, error: ConnectError( code: .internalError, message: "received unexpected compressed message" @@ -223,7 +223,7 @@ extension GRPCInterceptor: StreamInterceptor { return } - let unpackedMessage = try Envelope.unpackMessage( + let unpackedMessage = try Envelope._unpackMessage( rawData, compressionPool: responseCompressionPool ).unpacked proceed(.message(unpackedMessage)) @@ -239,7 +239,7 @@ extension GRPCInterceptor: StreamInterceptor { return } - let (grpcCode, connectError) = ConnectError.parseGRPCHeaders( + let (grpcCode, connectError) = ConnectError._parseGRPCHeaders( self.streamResponseHeaders.value, trailers: trailers ) @@ -275,8 +275,8 @@ extension GRPCInterceptor: StreamInterceptor { private extension Envelope { static func containsMultipleGRPCMessages(_ packedData: Data) -> Bool { - let messageLength = self.messageLength(forPackedData: packedData) - return packedData.count > messageLength + self.prefixLength + let messageLength = self._messageLength(forPackedData: packedData) + return packedData.count > messageLength + self._prefixLength } } diff --git a/Tests/UnitTests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift b/Tests/UnitTests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift index 6fc0ee22..080e0f4c 100644 --- a/Tests/UnitTests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift +++ b/Tests/UnitTests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift @@ -18,62 +18,62 @@ import XCTest final class EnvelopeTests: XCTestCase { func testPackingAndUnpackingCompressedMessage() throws { let originalData = Data(repeating: 0xa, count: 50) - let packed = Envelope.packMessage( + let packed = Envelope._packMessage( originalData, using: .init(minBytes: 10, pool: GzipCompressionPool()) ) let compressed = try GzipCompressionPool().compress(data: originalData) XCTAssertEqual(packed[0], 1) // Compression flag = true - XCTAssertTrue(Envelope.isCompressed(packed)) - XCTAssertEqual(Envelope.messageLength(forPackedData: packed), compressed.count) + XCTAssertTrue(Envelope._isCompressed(packed)) + XCTAssertEqual(Envelope._messageLength(forPackedData: packed), compressed.count) XCTAssertEqual(packed[5...], compressed) // Post-prefix data should match compressed value - let unpacked = try Envelope.unpackMessage(packed, compressionPool: GzipCompressionPool()) + let unpacked = try Envelope._unpackMessage(packed, compressionPool: GzipCompressionPool()) XCTAssertEqual(unpacked.unpacked, originalData) XCTAssertEqual(unpacked.headerByte, 1) // Compression flag = true } func testPackingAndUnpackingUncompressedMessageBecauseCompressionMinBytesIsNil() throws { let originalData = Data(repeating: 0xa, count: 50) - let packed = Envelope.packMessage(originalData, using: nil) + let packed = Envelope._packMessage(originalData, using: nil) XCTAssertEqual(packed[0], 0) // Compression flag = false - XCTAssertFalse(Envelope.isCompressed(packed)) - XCTAssertEqual(Envelope.messageLength(forPackedData: packed), originalData.count) + XCTAssertFalse(Envelope._isCompressed(packed)) + XCTAssertEqual(Envelope._messageLength(forPackedData: packed), originalData.count) XCTAssertEqual(packed[5...], originalData) // Post-prefix data should match compressed value // Compression pool should be ignored since the message is not compressed - let unpacked = try Envelope.unpackMessage(packed, compressionPool: GzipCompressionPool()) + let unpacked = try Envelope._unpackMessage(packed, compressionPool: GzipCompressionPool()) XCTAssertEqual(unpacked.unpacked, originalData) XCTAssertEqual(unpacked.headerByte, 0) // Compression flag = false } func testPackingAndUnpackingUncompressedMessageBecauseMessageIsTooSmall() throws { let originalData = Data(repeating: 0xa, count: 50) - let packed = Envelope.packMessage( + let packed = Envelope._packMessage( originalData, using: .init(minBytes: 100, pool: GzipCompressionPool()) ) XCTAssertEqual(packed[0], 0) // Compression flag = false - XCTAssertFalse(Envelope.isCompressed(packed)) - XCTAssertEqual(Envelope.messageLength(forPackedData: packed), originalData.count) + XCTAssertFalse(Envelope._isCompressed(packed)) + XCTAssertEqual(Envelope._messageLength(forPackedData: packed), originalData.count) XCTAssertEqual(packed[5...], originalData) // Post-prefix data should match compressed value // Compression pool should be ignored since the message is not compressed - let unpacked = try Envelope.unpackMessage(packed, compressionPool: GzipCompressionPool()) + let unpacked = try Envelope._unpackMessage(packed, compressionPool: GzipCompressionPool()) XCTAssertEqual(unpacked.unpacked, originalData) XCTAssertEqual(unpacked.headerByte, 0) // Compression flag = false } func testThrowsWhenUnpackingCompressedMessageWithoutDecompressionPool() throws { let originalData = Data(repeating: 0xa, count: 50) - let packed = Envelope.packMessage( + let packed = Envelope._packMessage( originalData, using: .init(minBytes: 10, pool: GzipCompressionPool()) ) let compressed = try GzipCompressionPool().compress(data: originalData) XCTAssertEqual(packed[0], 1) // Compression flag = true - XCTAssertTrue(Envelope.isCompressed(packed)) - XCTAssertEqual(Envelope.messageLength(forPackedData: packed), compressed.count) + XCTAssertTrue(Envelope._isCompressed(packed)) + XCTAssertEqual(Envelope._messageLength(forPackedData: packed), compressed.count) XCTAssertEqual(packed[5...], compressed) // Post-prefix data should match compressed value - XCTAssertThrowsError(try Envelope.unpackMessage(packed, compressionPool: nil)) { error in + XCTAssertThrowsError(try Envelope._unpackMessage(packed, compressionPool: nil)) { error in XCTAssertEqual(error as? Envelope.Error, .missingExpectedCompressionPool) } } @@ -82,7 +82,7 @@ final class EnvelopeTests: XCTestCase { // Messages are incomplete if they do not contain enough data for the 5-byte prefix for length in 0..<5 { let data = Data(repeating: 0xa, count: length) - XCTAssertEqual(Envelope.messageLength(forPackedData: data), -1) + XCTAssertEqual(Envelope._messageLength(forPackedData: data), -1) } } }