| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- /*
- * Copyright 2021, 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.
- */
- import NIOCore
- import NIOHPACK
- import NIOHTTP1
- import NIOHTTP2
- import XCTest
- @testable import GRPC
- final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase {
- fileprivate typealias StateMachine = GRPCWebToHTTP2ServerCodec.StateMachine
- private let allocator = ByteBufferAllocator()
- private func makeStateMachine(scheme: String = "http") -> StateMachine {
- return StateMachine(scheme: scheme)
- }
- private func makeRequestHead(
- version: HTTPVersion = .http1_1,
- method: HTTPMethod = .POST,
- uri: String,
- headers: HTTPHeaders = [:]
- ) -> HTTPServerRequestPart {
- return .head(.init(version: version, method: method, uri: uri, headers: headers))
- }
- // MARK: - grpc-web
- func test_gRPCWeb_requestHeaders() {
- var state = self.makeStateMachine(scheme: "http")
- let head = self.makeRequestHead(method: .POST, uri: "foo", headers: ["host": "localhost"])
- let action = state.processInbound(serverRequestPart: head, allocator: self.allocator)
- action.assertRead { payload in
- payload.assertHeaders { payload in
- XCTAssertFalse(payload.endStream)
- XCTAssertEqual(payload.headers[canonicalForm: ":path"], ["foo"])
- XCTAssertEqual(payload.headers[canonicalForm: ":method"], ["POST"])
- XCTAssertEqual(payload.headers[canonicalForm: ":scheme"], ["http"])
- XCTAssertEqual(payload.headers[canonicalForm: ":authority"], ["localhost"])
- }
- }
- }
- func test_gRPCWeb_requestBody() {
- var state = self.makeStateMachine()
- let head = self.makeRequestHead(
- uri: "foo",
- headers: ["content-type": "application/grpc-web"]
- )
- state.processInbound(serverRequestPart: head, allocator: self.allocator).assertRead {
- $0.assertHeaders()
- }
- let b1 = ByteBuffer(string: "hello")
- for _ in 0 ..< 5 {
- state.processInbound(serverRequestPart: .body(b1), allocator: self.allocator).assertRead {
- $0.assertData {
- XCTAssertFalse($0.endStream)
- $0.data.assertByteBuffer { buffer in
- var buffer = buffer
- XCTAssertEqual(buffer.readString(length: buffer.readableBytes), "hello")
- }
- }
- }
- }
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead {
- $0.assertEmptyDataWithEndStream()
- }
- }
- private func checkResponseHeaders(
- from state: StateMachine,
- expectConnectionCloseHeader: Bool,
- line: UInt = #line
- ) {
- var state = state
- state.processOutbound(
- framePayload: .headers(.init(headers: [":status": "200"])),
- promise: nil,
- allocator: self.allocator
- ).assertWrite { write in
- write.part.assertHead {
- XCTAssertEqual($0.status, .ok, line: line)
- XCTAssertFalse($0.headers.contains(name: ":status"), line: line)
- if expectConnectionCloseHeader {
- XCTAssertEqual($0.headers[canonicalForm: "connection"], ["close"], line: line)
- } else {
- XCTAssertFalse($0.headers.contains(name: "connection"), line: line)
- }
- }
- XCTAssertNil(write.additionalPart, line: line)
- XCTAssertFalse(write.closeChannel, line: line)
- }
- }
- func test_gRPCWeb_responseHeaders() {
- for connectionClose in [true, false] {
- let headers: HTTPHeaders = connectionClose ? ["connection": "close"] : [:]
- let requestHead = self.makeRequestHead(uri: "/echo", headers: headers)
- var state = self.makeStateMachine()
- state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead()
- self.checkResponseHeaders(from: state, expectConnectionCloseHeader: connectionClose)
- // Do it again with the request stream closed.
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead()
- self.checkResponseHeaders(from: state, expectConnectionCloseHeader: connectionClose)
- }
- }
- private func checkTrailersOnlyResponse(
- from state: StateMachine,
- expectConnectionCloseHeader: Bool,
- line: UInt = #line
- ) {
- var state = state
- state.processOutbound(
- framePayload: .headers(.init(headers: [":status": "415"], endStream: true)),
- promise: nil,
- allocator: self.allocator
- ).assertWrite { write in
- write.part.assertHead {
- XCTAssertEqual($0.status, .unsupportedMediaType, line: line)
- XCTAssertFalse($0.headers.contains(name: ":status"), line: line)
- if expectConnectionCloseHeader {
- XCTAssertEqual($0.headers[canonicalForm: "connection"], ["close"], line: line)
- } else {
- XCTAssertFalse($0.headers.contains(name: "connection"), line: line)
- }
- }
- // Should also send end.
- write.additionalPart.assertSome { $0.assertEnd() }
- XCTAssertEqual(write.closeChannel, expectConnectionCloseHeader, line: line)
- }
- }
- func test_gRPCWeb_responseTrailersOnly() {
- for connectionClose in [true, false] {
- let headers: HTTPHeaders = connectionClose ? ["connection": "close"] : [:]
- let requestHead = self.makeRequestHead(uri: "/echo", headers: headers)
- var state = self.makeStateMachine()
- state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead()
- self.checkTrailersOnlyResponse(from: state, expectConnectionCloseHeader: connectionClose)
- // Do it again with the request stream closed.
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead()
- self.checkTrailersOnlyResponse(from: state, expectConnectionCloseHeader: connectionClose)
- }
- }
- private func checkGRPCWebResponseData(from state: StateMachine, line: UInt = #line) {
- var state = state
- for i in 0 ..< 10 {
- let buffer = ByteBuffer(string: "foo-\(i)")
- state.processOutbound(
- framePayload: .data(.init(data: .byteBuffer(buffer))),
- promise: nil,
- allocator: self.allocator
- ).assertWrite { write in
- write.part.assertBody {
- XCTAssertEqual($0, buffer, line: line)
- }
- XCTAssertNil(write.additionalPart, line: line)
- XCTAssertFalse(write.closeChannel, line: line)
- }
- }
- }
- func test_gRPCWeb_responseData() {
- var state = self.makeStateMachine()
- let requestHead = self.makeRequestHead(
- uri: "/echo",
- headers: ["content-type": "application/grpc-web"]
- )
- state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead()
- state.processOutbound(
- framePayload: .headers(.init(headers: [":status": "200"])),
- promise: nil,
- allocator: self.allocator
- ).assertWrite()
- // Request stream is open.
- self.checkGRPCWebResponseData(from: state)
- // Close request stream and test again.
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead()
- self.checkGRPCWebResponseData(from: state)
- }
- private func checkGRPCWebResponseTrailers(
- from state: StateMachine,
- expectChannelClose: Bool,
- line: UInt = #line
- ) {
- var state = state
- state.processOutbound(
- framePayload: .headers(.init(headers: ["grpc-status": "0"], endStream: true)),
- promise: nil,
- allocator: self.allocator
- ).assertWrite { write in
- write.part.assertBody { buffer in
- var buffer = buffer
- let trailers = buffer.readLengthPrefixedMessage().map { String(buffer: $0) }
- XCTAssertEqual(trailers, "grpc-status: 0\r\n")
- }
- XCTAssertEqual(write.closeChannel, expectChannelClose)
- }
- }
- func test_gRPCWeb_responseTrailers() {
- for connectionClose in [true, false] {
- let headers: HTTPHeaders = connectionClose ? ["connection": "close"] : [:]
- let requestHead = self.makeRequestHead(uri: "/echo", headers: headers)
- var state = self.makeStateMachine()
- state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead()
- state.processOutbound(
- framePayload: .headers(.init(headers: [":status": "200"])),
- promise: nil,
- allocator: self.allocator
- ).assertWrite()
- // Request stream is open.
- self.checkGRPCWebResponseTrailers(from: state, expectChannelClose: connectionClose)
- // Check again with request stream closed.
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead()
- self.checkGRPCWebResponseTrailers(from: state, expectChannelClose: connectionClose)
- }
- }
- // MARK: - grpc-web-text
- func test_gRPCWebText_requestBody() {
- var state = self.makeStateMachine()
- let head = self.makeRequestHead(
- uri: "foo",
- headers: ["content-type": "application/grpc-web-text"]
- )
- state.processInbound(serverRequestPart: head, allocator: self.allocator).assertRead {
- $0.assertHeaders()
- }
- let expected = ["hel", "lo"]
- let buffers = [ByteBuffer(string: "aGVsb"), ByteBuffer(string: "G8=")]
- for (buffer, expected) in zip(buffers, expected) {
- state.processInbound(serverRequestPart: .body(buffer), allocator: self.allocator).assertRead {
- $0.assertData {
- XCTAssertFalse($0.endStream)
- $0.data.assertByteBuffer { buffer in
- var buffer = buffer
- XCTAssertEqual(buffer.readString(length: buffer.readableBytes), expected)
- }
- }
- }
- }
- // If there's not enough to decode, there's nothing to do.
- let buffer = ByteBuffer(string: "a")
- state.processInbound(serverRequestPart: .body(buffer), allocator: self.allocator).assertNone()
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead {
- $0.assertEmptyDataWithEndStream()
- }
- }
- private func checkResponseDataAndTrailersForGRPCWebText(
- from state: StateMachine,
- line: UInt = #line
- ) {
- var state = state
- state.processOutbound(
- framePayload: .headers(.init(headers: [":status": "200"])),
- promise: nil,
- allocator: self.allocator
- ).assertWrite()
- // Write some bytes.
- for text in ["hello", ", world!"] {
- let buffer = ByteBuffer(string: text)
- state.processOutbound(
- framePayload: .data(.init(data: .byteBuffer(buffer))),
- promise: nil,
- allocator: self.allocator
- ).assertCompletePromise { error in
- XCTAssertNil(error)
- }
- }
- state.processOutbound(
- framePayload: .headers(.init(headers: ["grpc-status": "0"], endStream: true)),
- promise: nil,
- allocator: self.allocator
- ).assertWrite { write in
- // The response is encoded by:
- // - accumulating the bytes of request messages (these would normally be gRPC length prefixed
- // messages)
- // - appending a 'trailers' byte (0x80)
- // - appending the UInt32 length of the trailers when encoded as HTTP/1 header lines
- // - the encoded headers
- write.part.assertBody { buffer in
- var buffer = buffer
- let base64Encoded = buffer.readString(length: buffer.readableBytes)!
- XCTAssertEqual(base64Encoded, "aGVsbG8sIHdvcmxkIYAAAAAQZ3JwYy1zdGF0dXM6IDANCg==")
- let data = Data(base64Encoded: base64Encoded)!
- buffer.writeData(data)
- XCTAssertEqual(buffer.readString(length: 13), "hello, world!")
- XCTAssertEqual(buffer.readInteger(), UInt8(0x80))
- XCTAssertEqual(buffer.readInteger(), UInt32(16))
- XCTAssertEqual(buffer.readString(length: 16), "grpc-status: 0\r\n")
- XCTAssertEqual(buffer.readableBytes, 0)
- }
- // There should be an end now.
- write.additionalPart.assertSome { $0.assertEnd() }
- XCTAssertFalse(write.closeChannel)
- }
- }
- func test_gRPCWebText_responseDataAndTrailers() {
- var state = self.makeStateMachine()
- let requestHead = self.makeRequestHead(
- uri: "/echo",
- headers: ["content-type": "application/grpc-web-text"]
- )
- state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead()
- // Request stream is still open.
- self.checkResponseDataAndTrailersForGRPCWebText(from: state)
- // Check again with request stream closed.
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead()
- self.checkResponseDataAndTrailersForGRPCWebText(from: state)
- }
- // MARK: - General
- func test_requestPartsAfterServerClosed() {
- var state = self.makeStateMachine()
- let requestHead = self.makeRequestHead(uri: "/echo")
- state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead()
- // Close the response stream.
- state.processOutbound(
- framePayload: .headers(.init(headers: [":status": "415"], endStream: true)),
- promise: nil,
- allocator: self.allocator
- ).assertWrite()
- state.processInbound(
- serverRequestPart: .body(ByteBuffer(string: "hello world")),
- allocator: self.allocator
- ).assertRead {
- $0.assertData {
- XCTAssertFalse($0.endStream)
- $0.data.assertByteBuffer { buffer in
- XCTAssertTrue(buffer.readableBytesView.elementsEqual("hello world".utf8))
- }
- }
- }
- state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead {
- $0.assertEmptyDataWithEndStream()
- }
- }
- func test_responsePartsAfterServerClosed() {
- var state = self.makeStateMachine()
- let requestHead = self.makeRequestHead(uri: "/echo")
- state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead()
- // Close the response stream.
- state.processOutbound(
- framePayload: .headers(.init(headers: [":status": "415"], endStream: true)),
- promise: nil,
- allocator: self.allocator
- ).assertWrite()
- // More writes should be told to fail their promise.
- state.processOutbound(
- framePayload: .headers(.init(headers: .init())),
- promise: nil,
- allocator: self.allocator
- ).assertCompletePromise { error in
- XCTAssertNotNil(error)
- }
- state.processOutbound(
- framePayload: .data(.init(data: .byteBuffer(.init()))),
- promise: nil,
- allocator: self.allocator
- ).assertCompletePromise { error in
- XCTAssertNotNil(error)
- }
- }
- func test_handleMultipleRequests() {
- func sendRequestHead(
- _ state: inout StateMachine,
- contentType: ContentType
- )
- -> StateMachine
- .Action
- {
- let requestHead = self.makeRequestHead(
- uri: "/echo",
- headers: ["content-type": contentType.canonicalValue]
- )
- return state.processInbound(serverRequestPart: requestHead, allocator: self.allocator)
- }
- func sendRequestBody(_ state: inout StateMachine, buffer: ByteBuffer) -> StateMachine.Action {
- return state.processInbound(serverRequestPart: .body(buffer), allocator: self.allocator)
- }
- func sendRequestEnd(_ state: inout StateMachine) -> StateMachine.Action {
- return state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator)
- }
- func sendResponseHeaders(
- _ state: inout StateMachine,
- headers: HPACKHeaders,
- endStream: Bool = false
- ) -> StateMachine.Action {
- return state.processOutbound(
- framePayload: .headers(.init(headers: headers, endStream: endStream)),
- promise: nil,
- allocator: self.allocator
- )
- }
- func sendResponseData(
- _ state: inout StateMachine,
- buffer: ByteBuffer
- ) -> StateMachine.Action {
- return state.processOutbound(
- framePayload: .data(.init(data: .byteBuffer(buffer))),
- promise: nil,
- allocator: self.allocator
- )
- }
- var state = self.makeStateMachine()
- // gRPC-Web, all request parts then all response parts.
- sendRequestHead(&state, contentType: .webProtobuf).assertRead()
- sendRequestBody(&state, buffer: .init(string: "hello")).assertRead()
- sendRequestEnd(&state).assertRead()
- sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite()
- sendResponseData(&state, buffer: .init(string: "bye")).assertWrite()
- sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite()
- // gRPC-Web text, all requests then all response parts.
- sendRequestHead(&state, contentType: .webTextProtobuf).assertRead()
- sendRequestBody(&state, buffer: .init(string: "hello")).assertRead()
- sendRequestEnd(&state).assertRead()
- sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite()
- // nothing; buffered and sent with end.
- sendResponseData(&state, buffer: .init(string: "bye")).assertCompletePromise()
- sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite()
- // gRPC-Web, interleaving
- sendRequestHead(&state, contentType: .webProtobuf).assertRead()
- sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite()
- sendRequestBody(&state, buffer: .init(string: "hello")).assertRead()
- sendResponseData(&state, buffer: .init(string: "bye")).assertWrite()
- sendRequestEnd(&state).assertRead()
- sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite()
- // gRPC-Web text, interleaving
- sendRequestHead(&state, contentType: .webTextProtobuf).assertRead()
- sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite()
- sendRequestBody(&state, buffer: .init(string: "hello")).assertRead()
- sendResponseData(&state, buffer: .init(string: "bye")).assertCompletePromise()
- sendRequestEnd(&state).assertRead()
- sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite()
- // gRPC-Web, server closes immediately.
- sendRequestHead(&state, contentType: .webProtobuf).assertRead()
- sendResponseHeaders(&state, headers: [":status": "415"], endStream: true).assertWrite()
- sendRequestBody(&state, buffer: .init(string: "hello")).assertRead()
- sendRequestEnd(&state).assertRead()
- // gRPC-Web text, server closes immediately.
- sendRequestHead(&state, contentType: .webTextProtobuf).assertRead()
- sendResponseHeaders(&state, headers: [":status": "415"], endStream: true).assertWrite()
- sendRequestBody(&state, buffer: .init(string: "hello")).assertRead()
- sendRequestEnd(&state).assertRead()
- }
- }
- // MARK: - Assertions
- extension GRPCWebToHTTP2ServerCodec.StateMachine.Action {
- func assertRead(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (HTTP2Frame.FramePayload) -> Void = { _ in }
- ) {
- if case let .fireChannelRead(payload) = self {
- verify(payload)
- } else {
- XCTFail("Expected '.fireChannelRead' but got '\(self)'", file: file, line: line)
- }
- }
- func assertWrite(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (Write) -> Void = { _ in }
- ) {
- if case let .write(write) = self {
- verify(write)
- } else {
- XCTFail("Expected '.write' but got '\(self)'", file: file, line: line)
- }
- }
- func assertCompletePromise(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (Error?) -> Void = { _ in }
- ) {
- if case let .completePromise(_, result) = self {
- do {
- try result.get()
- verify(nil)
- } catch {
- verify(error)
- }
- } else {
- XCTFail("Expected '.completePromise' but got '\(self)'", file: file, line: line)
- }
- }
- func assertNone(
- file: StaticString = #filePath,
- line: UInt = #line
- ) {
- if case .none = self {
- ()
- } else {
- XCTFail("Expected '.none' but got '\(self)'", file: file, line: line)
- }
- }
- }
- extension HTTP2Frame.FramePayload {
- func assertHeaders(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (Headers) -> Void = { _ in }
- ) {
- if case let .headers(headers) = self {
- verify(headers)
- } else {
- XCTFail("Expected '.headers' but got '\(self)'", file: file, line: line)
- }
- }
- func assertData(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (Data) -> Void = { _ in }
- ) {
- if case let .data(data) = self {
- verify(data)
- } else {
- XCTFail("Expected '.data' but got '\(self)'", file: file, line: line)
- }
- }
- func assertEmptyDataWithEndStream(
- file: StaticString = #filePath,
- line: UInt = #line
- ) {
- self.assertData(file: file, line: line) {
- XCTAssertTrue($0.endStream)
- $0.data.assertByteBuffer { buffer in
- XCTAssertEqual(buffer.readableBytes, 0)
- }
- }
- }
- }
- extension HTTPServerResponsePart {
- func assertHead(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (HTTPResponseHead) -> Void = { _ in }
- ) {
- if case let .head(head) = self {
- verify(head)
- } else {
- XCTFail("Expected '.head' but got '\(self)'", file: file, line: line)
- }
- }
- func assertBody(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (ByteBuffer) -> Void = { _ in }
- ) {
- if case let .body(.byteBuffer(buffer)) = self {
- verify(buffer)
- } else {
- XCTFail("Expected '.body(.byteBuffer)' but got '\(self)'", file: file, line: line)
- }
- }
- func assertEnd(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (HTTPHeaders?) -> Void = { _ in }
- ) {
- if case let .end(trailers) = self {
- verify(trailers)
- } else {
- XCTFail("Expected '.end' but got '\(self)'", file: file, line: line)
- }
- }
- }
- extension IOData {
- func assertByteBuffer(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (ByteBuffer) -> Void = { _ in }
- ) {
- if case let .byteBuffer(buffer) = self {
- verify(buffer)
- } else {
- XCTFail("Expected '.byteBuffer' but got '\(self)'", file: file, line: line)
- }
- }
- }
- extension Optional {
- func assertSome(
- file: StaticString = #filePath,
- line: UInt = #line,
- verify: (Wrapped) -> Void = { _ in }
- ) {
- switch self {
- case let .some(wrapped):
- verify(wrapped)
- case .none:
- XCTFail("Expected '.some' but got 'nil'", file: file, line: line)
- }
- }
- }
- extension ByteBuffer {
- mutating func readLengthPrefixedMessage() -> ByteBuffer? {
- // Read off and ignore the compression byte.
- if self.readInteger(as: UInt8.self) == nil {
- return nil
- }
- return self.readLengthPrefixedSlice(as: UInt32.self)
- }
- }
|