mirror of
https://github.com/apple/swift-nio.git
synced 2026-05-20 20:30:36 +00:00
Add functions for reading and writing length-prefixed data with customizable encodings for the length, particularly for quic variable-length integers (RFC 9000) ### Motivation: Many protocols require us to write data and then prefix that data with its length. But each protocol has a different way of encoding the length. This PR introduces general purpose functions which can be extended for different encoding strategies ### Modifications: Create a new protocol which defines how to encode an integer Implement this protocol for QUIC Provide functions on bytebuffer for writing length-prefixed buffers, strings or bytes
333 lines
12 KiB
Swift
333 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import NIOCore
|
|
import XCTest
|
|
|
|
/// A strategy which just writes integers as UInt8. Enforces the integer must be a particular number to aid testing. Forbids reads
|
|
struct UInt8WritingTestStrategy: NIOBinaryIntegerEncodingStrategy {
|
|
let expectedWrite: Int
|
|
|
|
func readInteger<IntegerType: FixedWidthInteger>(
|
|
as: IntegerType.Type,
|
|
from buffer: inout ByteBuffer
|
|
) -> IntegerType? {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger<IntegerType: FixedWidthInteger>(_ integer: IntegerType, to buffer: inout ByteBuffer) -> Int {
|
|
XCTAssertEqual(Int(integer), self.expectedWrite)
|
|
return buffer.writeInteger(UInt8(integer))
|
|
}
|
|
|
|
func writeInteger(_ integer: Int, reservedCapacity: Int, to buffer: inout ByteBuffer) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// A which reads a single UInt8 for the length. Forbids writes
|
|
struct UInt8ReadingTestStrategy: NIOBinaryIntegerEncodingStrategy {
|
|
let expectedRead: UInt8
|
|
|
|
func readInteger<IntegerType: FixedWidthInteger>(
|
|
as: IntegerType.Type,
|
|
from buffer: inout ByteBuffer
|
|
) -> IntegerType? {
|
|
let value = buffer.readInteger(as: UInt8.self)
|
|
XCTAssertEqual(value, self.expectedRead)
|
|
return value.flatMap(IntegerType.init)
|
|
}
|
|
|
|
func writeInteger<IntegerType: FixedWidthInteger>(_ integer: IntegerType, to buffer: inout ByteBuffer) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger(_ integer: Int, reservedCapacity: Int, to buffer: inout ByteBuffer) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
var requiredBytesHint: Int { 1 }
|
|
}
|
|
|
|
final class ByteBufferBinaryEncodedLengthPrefixTests: XCTestCase {
|
|
// MARK: - simple readEncodedInteger and writeEncodedInteger tests
|
|
|
|
func testReadWriteEncodedInteger() {
|
|
struct TestStrategy: NIOBinaryIntegerEncodingStrategy {
|
|
func readInteger<IntegerType: FixedWidthInteger>(
|
|
as: IntegerType.Type,
|
|
from buffer: inout ByteBuffer
|
|
) -> IntegerType? {
|
|
10
|
|
}
|
|
|
|
func writeInteger<IntegerType: FixedWidthInteger>(
|
|
_ integer: IntegerType,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTAssertEqual(integer, 10)
|
|
return 1
|
|
}
|
|
|
|
func writeInteger(
|
|
_ integer: Int,
|
|
reservedCapacity: Int,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// This should just call down to the strategy function
|
|
var buffer = ByteBuffer()
|
|
XCTAssertEqual(buffer.readEncodedInteger(strategy: TestStrategy()), 10)
|
|
XCTAssertEqual(buffer.writeEncodedInteger(10, strategy: TestStrategy()), 1)
|
|
}
|
|
|
|
// MARK: - writeLengthPrefixed tests
|
|
|
|
func testWriteLengthPrefixedFitsInReservedCapacity() {
|
|
struct TestStrategy: NIOBinaryIntegerEncodingStrategy {
|
|
func readInteger<IntegerType: FixedWidthInteger>(
|
|
as: IntegerType.Type,
|
|
from buffer: inout ByteBuffer
|
|
) -> IntegerType? {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger<IntegerType: FixedWidthInteger>(
|
|
_ integer: IntegerType,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger(
|
|
_ integer: Int,
|
|
reservedCapacity: Int,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTAssertEqual(Int(integer), 4)
|
|
XCTAssertEqual(reservedCapacity, 1)
|
|
return buffer.writeInteger(UInt8(integer))
|
|
}
|
|
|
|
var requiredBytesHint: Int { 1 }
|
|
}
|
|
|
|
var buffer = ByteBuffer()
|
|
buffer.writeLengthPrefixed(strategy: TestStrategy()) { writer in
|
|
writer.writeString("test")
|
|
}
|
|
|
|
XCTAssertEqual(buffer.readableBytes, 5)
|
|
XCTAssertEqual(buffer.readBytes(length: 5), [4] + "test".utf8)
|
|
XCTAssertTrue(buffer.readableBytesView.isEmpty)
|
|
}
|
|
|
|
func testWriteLengthPrefixedNeedsMoreThanReservedCapacity() {
|
|
struct TestStrategy: NIOBinaryIntegerEncodingStrategy {
|
|
func readInteger<IntegerType: FixedWidthInteger>(
|
|
as: IntegerType.Type,
|
|
from buffer: inout ByteBuffer
|
|
) -> IntegerType? {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger<IntegerType: FixedWidthInteger>(
|
|
_ integer: IntegerType,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger(
|
|
_ integer: Int,
|
|
reservedCapacity: Int,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTAssertEqual(Int(integer), 4)
|
|
XCTAssertEqual(reservedCapacity, 1)
|
|
// We use 8 bytes, but only one was reserved
|
|
return buffer.writeInteger(UInt64(integer))
|
|
}
|
|
|
|
var requiredBytesHint: Int { 1 }
|
|
}
|
|
|
|
var buffer = ByteBuffer()
|
|
buffer.writeLengthPrefixed(strategy: TestStrategy()) { writer in
|
|
writer.writeString("test")
|
|
}
|
|
|
|
// The strategy above uses 8 bytes for encoding the length. The data is 4, making a total of 12 bytes written
|
|
XCTAssertEqual(buffer.readableBytes, 12)
|
|
XCTAssertEqual(buffer.readBytes(length: 12), [0, 0, 0, 0, 0, 0, 0, 4] + "test".utf8)
|
|
XCTAssertTrue(buffer.readableBytesView.isEmpty)
|
|
}
|
|
|
|
func testWriteLengthPrefixedNeedsLessThanReservedCapacity() {
|
|
struct TestStrategy: NIOBinaryIntegerEncodingStrategy {
|
|
func readInteger<IntegerType: FixedWidthInteger>(
|
|
as: IntegerType.Type,
|
|
from buffer: inout ByteBuffer
|
|
) -> IntegerType? {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger<IntegerType: FixedWidthInteger>(
|
|
_ integer: IntegerType,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger(
|
|
_ integer: Int,
|
|
reservedCapacity: Int,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTAssertEqual(Int(integer), 4)
|
|
XCTAssertEqual(reservedCapacity, 8)
|
|
return buffer.writeInteger(UInt8(integer))
|
|
}
|
|
|
|
var requiredBytesHint: Int { 8 }
|
|
}
|
|
|
|
var buffer = ByteBuffer()
|
|
buffer.writeLengthPrefixed(strategy: TestStrategy()) { writer in
|
|
writer.writeString("test")
|
|
}
|
|
|
|
// The strategy above reserves 8 bytes, but only uses 1
|
|
// The implementation will take care of removing the 7 spare bytes for us
|
|
XCTAssertEqual(buffer.readableBytes, 5)
|
|
XCTAssertEqual(buffer.readBytes(length: 5), [4] + "test".utf8)
|
|
XCTAssertTrue(buffer.readableBytesView.isEmpty)
|
|
}
|
|
|
|
func testWriteLengthPrefixedThrowing() {
|
|
// A strategy which fails the test if anything is called
|
|
struct NeverCallStrategy: NIOBinaryIntegerEncodingStrategy {
|
|
func readInteger<IntegerType: FixedWidthInteger>(
|
|
as: IntegerType.Type,
|
|
from buffer: inout ByteBuffer
|
|
) -> IntegerType? {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger<IntegerType: FixedWidthInteger>(
|
|
_ integer: IntegerType,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
func writeInteger(
|
|
_ integer: Int,
|
|
reservedCapacity: Int,
|
|
to buffer: inout ByteBuffer
|
|
) -> Int {
|
|
XCTFail("This should not be called")
|
|
return 1
|
|
}
|
|
|
|
var requiredBytesHint: Int { 1 }
|
|
}
|
|
|
|
struct TestError: Error {}
|
|
|
|
var buffer = ByteBuffer()
|
|
do {
|
|
try buffer.writeLengthPrefixed(strategy: NeverCallStrategy()) { _ in
|
|
throw TestError()
|
|
}
|
|
XCTFail("Expected call to throw")
|
|
} catch {
|
|
// Nothing should have happened, buffer should still be empty
|
|
XCTAssertTrue(buffer.readableBytesView.isEmpty)
|
|
}
|
|
}
|
|
|
|
// MARK: - readLengthPrefixed tests
|
|
|
|
func testReadLengthPrefixedSlice() {
|
|
var buffer = ByteBuffer()
|
|
buffer.writeBytes([5, 1, 2, 3, 4, 5])
|
|
let slice = buffer.readLengthPrefixedSlice(strategy: UInt8ReadingTestStrategy(expectedRead: 5))
|
|
XCTAssertEqual(slice?.readableBytesView, [1, 2, 3, 4, 5])
|
|
}
|
|
|
|
func testReadLengthPrefixedSliceInsufficientBytes() {
|
|
var buffer = ByteBuffer()
|
|
buffer.writeBytes([5, 1, 2, 3]) // We put a length of 5, followed by only 3 bytes
|
|
let slice = buffer.readLengthPrefixedSlice(strategy: UInt8ReadingTestStrategy(expectedRead: 5))
|
|
XCTAssertNil(slice)
|
|
// The original buffer reader index should NOT move
|
|
XCTAssertEqual(buffer.readableBytesView, [5, 1, 2, 3])
|
|
}
|
|
|
|
// MARK: - writeLengthPrefixed* tests
|
|
|
|
func testWriteVariableLengthPrefixedString() {
|
|
var buffer = ByteBuffer()
|
|
let strategy = UInt8WritingTestStrategy(expectedWrite: 11)
|
|
let testString = "Hello World" // length = 11
|
|
let bytesWritten = buffer.writeLengthPrefixedString(testString, strategy: strategy)
|
|
XCTAssertEqual(bytesWritten, 11 + 1) // we use 1 byte to write the length
|
|
|
|
XCTAssertEqual(buffer.readableBytes, 12)
|
|
XCTAssertEqual(buffer.readBytes(length: 12), [11] + testString.utf8)
|
|
XCTAssertTrue(buffer.readableBytesView.isEmpty)
|
|
}
|
|
|
|
func testWriteVariableLengthPrefixedBytes() {
|
|
var buffer = ByteBuffer()
|
|
let strategy = UInt8WritingTestStrategy(expectedWrite: 10)
|
|
let testBytes = [UInt8](repeating: 1, count: 10)
|
|
let bytesWritten = buffer.writeLengthPrefixedBytes(testBytes, strategy: strategy)
|
|
XCTAssertEqual(bytesWritten, 10 + 1) // we use 1 byte to write the length
|
|
|
|
XCTAssertEqual(buffer.readableBytes, 11)
|
|
XCTAssertEqual(buffer.readBytes(length: 11), [10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
|
XCTAssertTrue(buffer.readableBytesView.isEmpty)
|
|
}
|
|
|
|
func testWriteVariableLengthPrefixedBuffer() {
|
|
var buffer = ByteBuffer()
|
|
let strategy = UInt8WritingTestStrategy(expectedWrite: 4)
|
|
let testBuffer = ByteBuffer(string: "test")
|
|
let bytesWritten = buffer.writeLengthPrefixedBuffer(testBuffer, strategy: strategy)
|
|
XCTAssertEqual(bytesWritten, 4 + 1) // we use 1 byte to write the length
|
|
|
|
XCTAssertEqual(buffer.readableBytes, 5)
|
|
XCTAssertEqual(buffer.readBytes(length: 5), [4] + "test".utf8)
|
|
XCTAssertTrue(buffer.readableBytesView.isEmpty)
|
|
}
|
|
}
|