Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a663a27
Add client-side logic of GRPCStreamStateMachine
gjcairo Feb 1, 2024
31f91f0
Add server-side logic to GRPCStreamStateMachine
gjcairo Feb 6, 2024
6b5af81
Add individual state structs for each state
gjcairo Feb 7, 2024
83fa5a8
Buffer inbound messages
gjcairo Feb 7, 2024
3169e8b
Replace preconditionFailures with assertionFailure+error
gjcairo Feb 7, 2024
406a5a9
Close (de)compressor when closing
gjcairo Feb 7, 2024
55118b4
Add some tests and multiple refactors
gjcairo Feb 7, 2024
5a29e51
Add validations to server-side receive metadata
gjcairo Feb 8, 2024
bf9ea2d
Tests for client side
gjcairo Feb 9, 2024
1e78321
Tests for server side
gjcairo Feb 12, 2024
89f3ea4
Multiple PR changes
gjcairo Feb 13, 2024
21ac2d8
Fix encoding negotiation and return headers
gjcairo Feb 16, 2024
35eef1a
Merge both state machines
gjcairo Feb 16, 2024
1938ae2
More PR changes
gjcairo Feb 16, 2024
ed19ae7
Formatting
gjcairo Feb 16, 2024
c9d1eca
PR changes
gjcairo Feb 16, 2024
cf325f5
Refactor client tests
gjcairo Feb 19, 2024
91ad5d8
More PR changes
gjcairo Feb 19, 2024
4061768
Formatting
gjcairo Feb 19, 2024
e01293e
Refactor server tests
gjcairo Feb 20, 2024
d17103d
Add OnNextOutboundMessage
gjcairo Feb 20, 2024
469806a
End (de)compressor at the right time
gjcairo Feb 20, 2024
d256376
Fix tests
gjcairo Feb 20, 2024
4c91296
Small test refactor
gjcairo Feb 21, 2024
389b6d5
Encode grpc status message in trailers
gjcairo Feb 21, 2024
507018c
Draft new common paths tests
gjcairo Feb 21, 2024
3219dc0
Formatting
gjcairo Feb 21, 2024
2bff8ab
Don't allow server to end stream by sending a message
gjcairo Feb 22, 2024
7396271
Small PR change
gjcairo Mar 5, 2024
6ee3bd0
PR changes
gjcairo Mar 5, 2024
67bfe05
Add tearDown method
gjcairo Mar 6, 2024
2b58fd2
Fix tests and formatting
gjcairo Mar 6, 2024
960af7c
PR changes
gjcairo Mar 8, 2024
5697b1c
Disable swift-format rule
gjcairo Mar 8, 2024
59793b6
Change import
gjcairo Mar 8, 2024
72b0754
Remove some code duplication
gjcairo Mar 8, 2024
2b383bd
Remove unnecessary special handling of status message in metadata
gjcairo Mar 8, 2024
ee12092
Remove redundant enum case
gjcairo Mar 8, 2024
1e3c9dd
Change status code in error case when grpc-status is missing
gjcairo Mar 8, 2024
1805b52
Avoid allocation
gjcairo Mar 8, 2024
cfa9ce2
Move state change outside of validation method
gjcairo Mar 8, 2024
154e2c5
Remove unused code
gjcairo Mar 8, 2024
cec6788
Simplify compression state logic
gjcairo Mar 8, 2024
ed17c55
Formatting
gjcairo Mar 8, 2024
627ce2a
Change some types to be fileprivate
gjcairo Mar 11, 2024
54c8e1c
Remove some duplication
gjcairo Mar 11, 2024
83661a1
Remove trailersOnly parameter from send(status:metadata:)
gjcairo Mar 12, 2024
9aa7705
Formatting
gjcairo Mar 12, 2024
61135de
Better handle invalid headers on client side
gjcairo Mar 14, 2024
96c0f8a
Change order of required request headers
gjcairo Mar 15, 2024
c33dcc9
Fix server transition from idle to open when client is closed
gjcairo Mar 15, 2024
f8faf8a
Merge branch 'main' into stream-state-machine
glbrntt Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 147 additions & 20 deletions Sources/GRPCCore/Internal/Base64.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
met:

1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
Expand Down Expand Up @@ -70,25 +70,59 @@
SOFTWARE.
*/

internal enum Base64 {}

extension Base64 {
internal struct DecodingOptions: OptionSet {
// swift-format-ignore: DontRepeatTypeInStaticProperties
enum Base64 {
struct DecodingOptions: OptionSet {
internal let rawValue: UInt
internal init(rawValue: UInt) { self.rawValue = rawValue }

internal static let base64UrlAlphabet = DecodingOptions(rawValue: UInt(1 << 0))
internal static let omitPaddingCharacter = DecodingOptions(rawValue: UInt(1 << 1))
}

internal enum DecodingError: Error, Equatable {
enum DecodingError: Error, Equatable {
case invalidLength
case invalidCharacter(UInt8)
case unexpectedPaddingCharacter
case unexpectedEnd
}

internal static func decode(
static func encode<Buffer: Collection>(bytes: Buffer) -> String where Buffer.Element == UInt8 {
guard !bytes.isEmpty else {
return ""
}

// In Base64, 3 bytes become 4 output characters, and we pad to the
// nearest multiple of four.
let base64StringLength = ((bytes.count + 2) / 3) * 4
let alphabet = Base64.encodeBase64

return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in
var input = bytes.makeIterator()
var offset = 0
while let firstByte = input.next() {
let secondByte = input.next()
let thirdByte = input.next()

backingStorage[offset] = Base64.encode(alphabet: alphabet, firstByte: firstByte)
backingStorage[offset + 1] = Base64.encode(
alphabet: alphabet,
firstByte: firstByte,
secondByte: secondByte
)
backingStorage[offset + 2] = Base64.encode(
alphabet: alphabet,
secondByte: secondByte,
thirdByte: thirdByte
)
backingStorage[offset + 3] = Base64.encode(alphabet: alphabet, thirdByte: thirdByte)
offset += 4
}
return offset
}
}

static func decode(
string encoded: String,
options: DecodingOptions = []
) throws -> [UInt8] {
Expand Down Expand Up @@ -204,7 +238,7 @@ extension Base64 {
}
}

static func withUnsafeDecodingTablesAsBufferPointers<R>(
private static func withUnsafeDecodingTablesAsBufferPointers<R>(
options: Base64.DecodingOptions,
_ body: (
UnsafeBufferPointer<UInt32>, UnsafeBufferPointer<UInt32>, UnsafeBufferPointer<UInt32>,
Expand Down Expand Up @@ -232,10 +266,64 @@ extension Base64 {
}
}

internal static let encodePaddingCharacter: UInt8 = 61
static let badCharacter: UInt32 = 0x01FF_FFFF
private static let encodePaddingCharacter: UInt8 = 61

private static let encodeBase64: [UInt8] = [
UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"),
UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"),
UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"),
UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"),
UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"),
UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"),
UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"),
UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"),
UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"),
UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"),
UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"),
UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"),
UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"),
UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"),
UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"),
UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"),
]

private static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 {
let index = firstByte >> 2
return alphabet[Int(index)]
}

private static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 {
var index = (firstByte & 0b00000011) << 4
if let secondByte = secondByte {
index += (secondByte & 0b11110000) >> 4
}
return alphabet[Int(index)]
}

static let decoding0: [UInt32] = [
private static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 {
guard let secondByte = secondByte else {
// No second byte means we are just emitting padding.
return Base64.encodePaddingCharacter
}
var index = (secondByte & 0b00001111) << 2
if let thirdByte = thirdByte {
index += (thirdByte & 0b11000000) >> 6
}
return alphabet[Int(index)]
}

private static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 {
guard let thirdByte = thirdByte else {
// No third byte means just padding.
return Base64.encodePaddingCharacter
}
let index = thirdByte & 0b00111111
return alphabet[Int(index)]
}

private static let badCharacter: UInt32 = 0x01FF_FFFF

private static let decoding0: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
Expand Down Expand Up @@ -281,7 +369,7 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]

static let decoding1: [UInt32] = [
private static let decoding1: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
Expand Down Expand Up @@ -327,7 +415,7 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]

static let decoding2: [UInt32] = [
private static let decoding2: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
Expand Down Expand Up @@ -373,7 +461,7 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]

static let decoding3: [UInt32] = [
private static let decoding3: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
Expand Down Expand Up @@ -419,7 +507,7 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]

static let decoding0url: [UInt32] = [
private static let decoding0url: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12
Expand Down Expand Up @@ -465,7 +553,7 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]

static let decoding1url: [UInt32] = [
private static let decoding1url: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12
Expand Down Expand Up @@ -511,7 +599,7 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]

static let decoding2url: [UInt32] = [
private static let decoding2url: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12
Expand Down Expand Up @@ -557,7 +645,7 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]

static let decoding3url: [UInt32] = [
private static let decoding3url: [UInt32] = [
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12
Expand Down Expand Up @@ -603,3 +691,42 @@ extension Base64 {
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
]
}

extension String {
/// This is a backport of a proposed String initializer that will allow writing directly into an uninitialized String's backing memory.
///
/// As this API does not exist prior to 5.3 on Linux, or on older Apple platforms, we fake it out with a pointer and accept the extra copy.
init(
backportUnsafeUninitializedCapacity capacity: Int,
initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer<UInt8>) throws -> Int
) rethrows {

// The buffer will store zero terminated C string
let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: capacity + 1)
defer {
buffer.deallocate()
}

let initializedCount = try initializer(buffer)
precondition(initializedCount <= capacity, "Overran buffer in initializer!")

// add zero termination
buffer[initializedCount] = 0

self = String(cString: buffer.baseAddress!)
}

init(
customUnsafeUninitializedCapacity capacity: Int,
initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer<UInt8>) throws -> Int
) rethrows {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) {
try self.init(unsafeUninitializedCapacity: capacity, initializingUTF8With: initializer)
} else {
try self.init(
backportUnsafeUninitializedCapacity: capacity,
initializingUTF8With: initializer
)
}
}
}
11 changes: 11 additions & 0 deletions Sources/GRPCCore/Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ public struct Metadata: Sendable, Hashable {
public enum Value: Sendable, Hashable {
case string(String)
case binary([UInt8])

/// The value as a String. If it was originally stored as a binary, the base64-encoded String version
/// of the binary data will be returned instead.
public func encoded() -> String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To align with other existing API https://developer.apple.com/documentation/foundation/data/2142853-base64encodedstring

Suggested change
public func encoded() -> String {
public func base64EncodedString() -> String {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only a base64 encoded if the underlying value is binary so the base64EncodedString isn't accurate.

switch self {
case .string(let string):
return string
case .binary(let bytes):
return Base64.encode(bytes: bytes)
}
}
}

/// A metadata key-value pair.
Expand Down
52 changes: 52 additions & 0 deletions Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// Supported message compression algorithms.
///
/// These algorithms are indicated in the "grpc-encoding" header. As such, a lack of "grpc-encoding"
/// header indicates that there is no message compression.
public struct CompressionAlgorithm: Hashable, Sendable {
/// Identity compression; "no" compression but indicated via the "grpc-encoding" header.
public static let identity = CompressionAlgorithm(.identity)
public static let deflate = CompressionAlgorithm(.deflate)
public static let gzip = CompressionAlgorithm(.gzip)

// The order here is important: most compression to least.
public static let all: [CompressionAlgorithm] = [.gzip, .deflate, .identity]

public var name: String {
return self.algorithm.rawValue
}

internal enum Algorithm: String {
case identity
case deflate
case gzip
}

internal let algorithm: Algorithm

private init(_ algorithm: Algorithm) {
self.algorithm = algorithm
}

internal init?(rawValue: String) {
guard let algorithm = Algorithm(rawValue: rawValue) else {
return nil
}
self.algorithm = algorithm
}
}
Loading