From d6438baf2d7a92c2621733752dee36c919e2f8f4 Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Wed, 15 Jul 2026 17:07:41 +0100 Subject: [PATCH 1/9] Add Capsule protocol Motivation: The Capsule protocol is used across a few RFCs. It defines a simple TLC framing. Type and length use the QUIC variable length integer encoding. Modifications: * Add a variable length integer type. * Add types for Capsules. Encoding and decoding APIs are based on spans. Results: Capsules. --- Sources/NetworkTypes/Capsule.swift | 206 ++++++++++++++++++ Sources/NetworkTypes/NetworkTypeError.swift | 18 ++ .../NetworkTypes/VariableLengthInteger.swift | 115 ++++++++++ Tests/NetworkTypesTests/CapsuleTests.swift | 171 +++++++++++++++ .../VariableLengthIntegerTests.swift | 117 ++++++++++ 5 files changed, 627 insertions(+) create mode 100644 Sources/NetworkTypes/Capsule.swift create mode 100644 Sources/NetworkTypes/NetworkTypeError.swift create mode 100644 Sources/NetworkTypes/VariableLengthInteger.swift create mode 100644 Tests/NetworkTypesTests/CapsuleTests.swift create mode 100644 Tests/NetworkTypesTests/VariableLengthIntegerTests.swift diff --git a/Sources/NetworkTypes/Capsule.swift b/Sources/NetworkTypes/Capsule.swift new file mode 100644 index 0000000..3cc2635 --- /dev/null +++ b/Sources/NetworkTypes/Capsule.swift @@ -0,0 +1,206 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A capsule type code, as defined in RFC 9297, Section 3.2. +/// +/// Capsule types occupy an open 62-bit IANA registry. +public struct CapsuleType: Sendable, Hashable { + /// The numeric type code. Must be in the range `0 ... 2^62 - 1`. + public var code: UInt64 + + /// Creates a capsule type from its numeric code. + /// + /// - Precondition: `code` is less than or equal to ``VariableLengthInteger/max``. + public init(_ code: UInt64) { + // This cannot be triggered remotely since we parse these from a `VariableLengthInteger` + // where the upper bits are part of the decoder. + assert(code <= VariableLengthInteger.max, "capsule type \(code) exceeds the variable-length integer maximum") + self.code = code + } + + /// The `DATAGRAM` capsule type (RFC 9297, Section 3.5). + public static var datagram: CapsuleType { 0x00 } + + /// The `ADDRESS_ASSIGN` capusle type (RFC 9484, Section 4.7.1). + public static var addressAssign: CapsuleType { 0x01 } + + /// The `ADDRESS_REQUEST` capusle type (RFC 9484, Section 4.7.2). + public static var addressRequest: CapsuleType { 0x02 } + + /// The `ROUTE_ADVERTISEMENT` capsule type (RFC 9484, Section 4.7.3). + public static var routeAdvertisement: CapsuleType { 0x03 } +} + +// Enable direct initialization from integer literals +extension CapsuleType: ExpressibleByIntegerLiteral { + public init(integerLiteral value: UInt64) { + self.init(value) + } +} + +/// A single capsule containing a type code and its opaque value, per RFC 9297, Section 3.2. +/// +/// ``value`` is a borrowed view into the bytes the capsule was decoded from. +/// As a result, `Capsule` is non-escapable. The value's internal structure is defined by +/// ``type``. +@available(anyAppleOS 26.0, *) +public struct Capsule: ~Escapable { + + public enum CodingError: Swift::Error { + case insufficientBytes + case insufficientCapacity + } + + /// The capsule type code. + public var type: CapsuleType + + /// The value is an opaque byte sequence. Its meaning is defined by ``type``. + public var value: Span + + /// Creates a capsule from a type and a borrowed value. + @_lifetime(copy value) + public init(type: CapsuleType, value: Span) { + self.type = type + self.value = value + } + + /// Header information for a Capsule. + /// + /// Returned by ``decodeHeader(from:)`` when the input contains enough + /// bytes to parse the header. + public struct HeaderInformation { + /// The capsule type code. + public let type: CapsuleType + + /// Length of the capsule's value following the header. + public let valueByteCount: Int + + /// Number of bytes used to encode the capsule type and value length. + public let headerByteCount: Int + } + + /// Decodes a capsule's header (type and length) from the front of + /// `input`, without requiring the value bytes to be present. Mutates + /// the span to start with the value. + /// + /// This lets a caller inspect the header (e.g., to reject an + /// oversized value) before consuming the value. Large + /// values can be parsed separately after consuming the + /// header. + /// + /// - Returns: The `HeaderInformation` for capsule type or + /// `nil` if `input` does not yet contain a complete type and + /// length. + public static func decodeHeader(from input: inout Span) -> HeaderInformation? { + guard let header = Self.peekHeader(from: input) else { + return nil + } + input = input.extracting(header.headerByteCount...) + return header + } + + /// Decodes a capsule's header (type and length) from the front of + /// `input`, without requiring the value bytes to be present. This + /// does not mutate the span. + /// + /// This lets a caller inspect the header (e.g., to reject an + /// oversized value) before consuming the value. + /// + /// - Returns: The `HeaderInformation` for capsule type or + /// `nil` if `input` does not yet contain a complete type and + /// length. + public static func peekHeader(from input: Span) -> HeaderInformation? { + guard let decodedType = VariableLengthInteger.decode(from: input) else { + return nil + } + let remainingBytes = input.extracting(droppingFirst: decodedType.byteCount) + guard let decodedLength = VariableLengthInteger.decode(from: remainingBytes) else { + return nil + } + return HeaderInformation( + type: CapsuleType(decodedType.value), + valueByteCount: Int(decodedLength.value), + headerByteCount: decodedType.byteCount + decodedLength.byteCount + ) + } + + /// Decodes one capsule from the front of `input`, advancing `input` past the + /// bytes it consumed. + /// + /// - Returns: The decoded capsule, or `nil` if `input` does not yet contain a + /// complete capsule. When `nil` is returned, `input` is left unchanged. + @_lifetime(copy input) + public static func decode(from input: inout Span) -> Capsule? { + guard let header = Self.peekHeader(from: input) else { + return nil + } + let capsuleByteCount = header.headerByteCount + header.valueByteCount + guard capsuleByteCount <= input.count else { + return nil + } + + let value = input.extracting(droppingFirst: header.headerByteCount) + let capsule = Capsule(type: header.type, value: value) + + // Drop consumed bytes from span. + input = input.extracting(capsuleByteCount...) + return capsule + } +} + +@available(anyAppleOS 26.0, *) +extension Capsule { + /// The number of bytes ``encode(into:)`` writes for this capsule: the encoded + /// type, the encoded length, and the value. + /// + /// - throws: ``NetworkTypeError`` if the capsule type of count surpass the + /// maximum length of ``VariableLengthInteger``. + public var encodedByteCount: Int { + get throws(NetworkTypeError) { + try VariableLengthInteger.encodedByteCount(self.type.code) + + VariableLengthInteger.encodedByteCount(UInt64(self.value.count)) + + self.value.count + } + } + + /// Encodes the capsule (type, then length, then value) into `output`. + /// + /// - throws: ``NetworkTypeError`` if `output` has does not + /// have at least ``encodedByteCount`` bytes of remaining capacity. + public func encode(into output: inout OutputSpan) throws(NetworkTypeError) { + if try output.capacity < self.encodedByteCount { + throw .insufficientCapacity + } + + // First the type and value length ... + try VariableLengthInteger.encode(self.type.code, into: &output) + try VariableLengthInteger.encode(UInt64(self.value.count), into: &output) + + // ... followed by the value itself. + for index in 0..) throws(NetworkTypeError) { + try VariableLengthInteger.encode(type.code, into: &output) + try VariableLengthInteger.encode(UInt64(valueByteCount), into: &output) + } +} diff --git a/Sources/NetworkTypes/NetworkTypeError.swift b/Sources/NetworkTypes/NetworkTypeError.swift new file mode 100644 index 0000000..e98e9a6 --- /dev/null +++ b/Sources/NetworkTypes/NetworkTypeError.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@nonexhaustive +public enum NetworkTypeError: Error { + case exceedsMaximumValue + case insufficientCapacity +} diff --git a/Sources/NetworkTypes/VariableLengthInteger.swift b/Sources/NetworkTypes/VariableLengthInteger.swift new file mode 100644 index 0000000..33e4d30 --- /dev/null +++ b/Sources/NetworkTypes/VariableLengthInteger.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A QUIC variable-length integer, as defined in RFC 9000, Section 16. +/// +/// The two most significant bits of the first byte select the encoded length +/// (1, 2, 4, or 8 bytes); the remaining bits hold the value in network byte +/// order. +public enum VariableLengthInteger { + /// The largest representable value, `2^62 - 1`. + public static var max: UInt64 { 0x3FFF_FFFF_FFFF_FFFF } + + /// The number of bytes the minimal encoding of `value` occupies (1, 2, 4, or 8). + /// + /// - throws: ``NetworkTypeError.exceedsMaximumValue`` if `value` is greater than ``max``. + public static func encodedByteCount(_ value: UInt64) throws(NetworkTypeError) -> Int { + // 2 bits are reserved to encode the number of bytes. + // N bytes can encode N * 8 - 2 bits. + switch 64 - value.leadingZeroBitCount { + case 0...6: return 1 // fits in 6 bits + case 7...14: return 2 // fits in 14 bits + case 15...30: return 4 // fits in 30 bits + case 31...62: return 8 // fits in 62 bits + default: throw .exceedsMaximumValue + } + } + + /// Write`value` into `output` in network byte order. + /// + /// - Precondition: `output` has enough capcity to write `value`. + private static func write(_ value: T, into output: inout OutputSpan) { + let numBytes = MemoryLayout.size + assert(output.capacity >= numBytes) + let valueBigEndian = value.bigEndian + + withUnsafeBytes(of: valueBigEndian) { bufferPointer in + for unsafe value in unsafe bufferPointer { + output.append(value) + } + } + } + + /// Encodes `value` into `output` using the minimal number of bytes. + /// + /// - throws: ``NetworkTypeError.exceedsMaximumValue`` if `value` is greater than ``max``. + /// - throws: ``NetworkTypeError.insufficientCapacity`` if `output` does not have enough capacity to encode `value`. + @available(anyAppleOS 26.0, *) + public static func encode(_ value: UInt64, into output: inout OutputSpan) throws(NetworkTypeError) { + let byteCount = try Self.encodedByteCount(value) + guard output.capacity >= byteCount else { + throw NetworkTypeError.insufficientCapacity + } + + switch byteCount { + case 1: + let value = UInt8(truncatingIfNeeded: value) + Self.write(value, into: &output) + case 2: + let value = UInt16(truncatingIfNeeded: value | 0x4000) + Self.write(value, into: &output) + case 4: + let value = UInt32(truncatingIfNeeded: value | 0x8000_0000) + Self.write(value, into: &output) + case 8: + let value = UInt64(truncatingIfNeeded: value | 0xC000_0000_0000_0000) + Self.write(value, into: &output) + default: + throw .exceedsMaximumValue + } + } + + /// A decoded value and the bytes read to decode it. + public struct DecodeResult { + /// The decoded value. + public let value: UInt64 + + /// Bytes used for the encoding. This might differ from `encodedByteCount(value)` + /// since it is not required that values are encoded using the minimum number of bytes. + public let byteCount: Int + } + + /// Decodes the variable-length integer at the front of `input`. + /// + /// - Returns: The decoded value and the number of bytes it occupied, or + /// `nil` if `input` holds fewer bytes than its length prefix requires. + @available(anyAppleOS 26.0, *) + public static func decode(from input: Span) -> DecodeResult? { + guard input.count > 0 else { + return nil + } + + let first = input[0] + // Extract bits that signifiy the number bytes used for encoding. + let byteCount = 1 << Int(first >> 6) + guard input.count >= byteCount else { + return nil + } + + var value = UInt64(first & 0x3F) + for index in 1.. [UInt8] { + try [UInt8](capacity: 8) { output in + try VariableLengthInteger.encode(value, into: &output) + } + } + + /// Encodes `value` into a fresh byte array of `capacity` bytes via the OutputSpan initializer. + @available(anyAppleOS 26.0, *) + static func encoded(_ value: UInt64, intoBufferWithCapacity capacity: Int) throws -> [UInt8] { + try [UInt8](capacity: capacity) { output in + try VariableLengthInteger.encode(value, into: &output) + } + } + + @Test + @available(anyAppleOS 26.0, *) + func encodedByteCount() throws { + #expect(try VariableLengthInteger.encodedByteCount(0) == 1) + #expect(try VariableLengthInteger.encodedByteCount(0x3F) == 1) + + #expect(try VariableLengthInteger.encodedByteCount(0x3F + 1) == 2) + #expect(try VariableLengthInteger.encodedByteCount(0x3FFF) == 2) + + #expect(try VariableLengthInteger.encodedByteCount(0x3FFF + 1) == 4) + #expect(try VariableLengthInteger.encodedByteCount(0x3FFF_FFFF) == 4) + + #expect(try VariableLengthInteger.encodedByteCount(0x3FFF_FFFF + 1) == 8) + #expect(try VariableLengthInteger.encodedByteCount(VariableLengthInteger.max) == 8) + + #expect(throws: NetworkTypeError.exceedsMaximumValue.self) { + try VariableLengthInteger.encodedByteCount(VariableLengthInteger.max + 1) + } + } + + @Test + @available(anyAppleOS 26.0, *) + func decodesRFC9000Vectors() { + let eightByte: [UInt8] = [0xc2, 0x19, 0x7c, 0x5e, 0xff, 0x14, 0xe8, 0x8c] + #expect(VariableLengthInteger.decode(from: eightByte.span)?.value == 151_288_809_941_952_652) + let fourByte: [UInt8] = [0x9d, 0x7f, 0x3e, 0x7d] + #expect(VariableLengthInteger.decode(from: fourByte.span)?.value == 494_878_333) + let twoByte: [UInt8] = [0x7b, 0xbd] + #expect(VariableLengthInteger.decode(from: twoByte.span)?.value == 15_293) + let oneByte: [UInt8] = [0x25] + #expect(VariableLengthInteger.decode(from: oneByte.span)?.value == 37) + } + + @Test + @available(anyAppleOS 26.0, *) + func decodesNonMinimalEncoding() { + // 37 encoded in two bytes instead of one; legal to receive (RFC 9297 §1.1). + let bytes: [UInt8] = [0x40, 0x25] + let result = VariableLengthInteger.decode(from: bytes.span) + #expect(result?.value == 37) + #expect(result?.byteCount == 2) + } + + @Test + @available(anyAppleOS 26.0, *) + func decodeReturnsNilWhenTruncated() { + // First byte announces a 4-byte integer, only 2 bytes present. + let truncated: [UInt8] = [0x9d, 0x7f] + #expect(VariableLengthInteger.decode(from: truncated.span) == nil) + let empty: [UInt8] = [] + #expect(VariableLengthInteger.decode(from: empty.span) == nil) + } + + @Test + @available(anyAppleOS 26.0, *) + func encodesToMinimalForm() throws { + #expect(try Self.encoded(63) == [0x3f]) + #expect(try Self.encoded(64) == [0x40, 0x40]) + #expect(try Self.encoded(15_293) == [0x7b, 0xbd]) + } + + @Test(arguments: [ + (UInt64(15_293), 1), + (UInt64(494_878_333), 1), + (UInt64(494_878_333), 2), + (UInt64(151_288_809_941_952_652), 1), + (UInt64(151_288_809_941_952_652), 2), + (UInt64(151_288_809_941_952_652), 3), + ]) + @available(anyAppleOS 26.0, *) + func encodesThrowsWhenNotEnoughCapacity(number: UInt64, capacity: Int) throws { + #expect(throws: NetworkTypeError.self) { try Self.encoded(number, intoBufferWithCapacity: capacity) } + } + + @Test(arguments: [UInt64]([0, 63, 64, 16_383, 16_384, 1_073_741_823, 1_073_741_824, 0x3FFF_FFFF_FFFF_FFFF])) + @available(anyAppleOS 26.0, *) + func roundTripsAllBoundaries(value: UInt64) throws { + let bytes = try Self.encoded(value) + let decoded = VariableLengthInteger.decode(from: bytes.span) + #expect(decoded?.value == value) + #expect(decoded?.byteCount == bytes.count) + } +} From ad52c99ddf61c6cfa635c5eec456033d0abd52f3 Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Wed, 15 Jul 2026 17:32:08 +0100 Subject: [PATCH 2/9] Cleanup a bit --- Sources/NetworkTypes/Capsule.swift | 20 ++++--------------- Sources/NetworkTypes/NetworkTypeError.swift | 5 +++++ .../VariableLengthIntegerTests.swift | 5 +++++ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/NetworkTypes/Capsule.swift b/Sources/NetworkTypes/Capsule.swift index 3cc2635..c8a02c7 100644 --- a/Sources/NetworkTypes/Capsule.swift +++ b/Sources/NetworkTypes/Capsule.swift @@ -22,8 +22,8 @@ public struct CapsuleType: Sendable, Hashable { /// /// - Precondition: `code` is less than or equal to ``VariableLengthInteger/max``. public init(_ code: UInt64) { - // This cannot be triggered remotely since we parse these from a `VariableLengthInteger` - // where the upper bits are part of the decoder. + // This cannot be hit remotely when decoding capsules since + // the `VariableLengthInteger` decoder interprets those bits. assert(code <= VariableLengthInteger.max, "capsule type \(code) exceeds the variable-length integer maximum") self.code = code } @@ -51,16 +51,9 @@ extension CapsuleType: ExpressibleByIntegerLiteral { /// A single capsule containing a type code and its opaque value, per RFC 9297, Section 3.2. /// /// ``value`` is a borrowed view into the bytes the capsule was decoded from. -/// As a result, `Capsule` is non-escapable. The value's internal structure is defined by -/// ``type``. +/// The value's internal structure is defined by ``type``. @available(anyAppleOS 26.0, *) public struct Capsule: ~Escapable { - - public enum CodingError: Swift::Error { - case insufficientBytes - case insufficientCapacity - } - /// The capsule type code. public var type: CapsuleType @@ -75,9 +68,6 @@ public struct Capsule: ~Escapable { } /// Header information for a Capsule. - /// - /// Returned by ``decodeHeader(from:)`` when the input contains enough - /// bytes to parse the header. public struct HeaderInformation { /// The capsule type code. public let type: CapsuleType @@ -94,9 +84,7 @@ public struct Capsule: ~Escapable { /// the span to start with the value. /// /// This lets a caller inspect the header (e.g., to reject an - /// oversized value) before consuming the value. Large - /// values can be parsed separately after consuming the - /// header. + /// oversized value) before consuming the value. /// /// - Returns: The `HeaderInformation` for capsule type or /// `nil` if `input` does not yet contain a complete type and diff --git a/Sources/NetworkTypes/NetworkTypeError.swift b/Sources/NetworkTypes/NetworkTypeError.swift index e98e9a6..33b10fa 100644 --- a/Sources/NetworkTypes/NetworkTypeError.swift +++ b/Sources/NetworkTypes/NetworkTypeError.swift @@ -11,8 +11,13 @@ // //===----------------------------------------------------------------------===// +/// Errors returned from other network types. @nonexhaustive public enum NetworkTypeError: Error { + /// A input value exceeds the maximum by the specification. case exceedsMaximumValue + + /// A value cannot be written to a buffer because the buffer + /// does not have sufficient capacity to hold it. case insufficientCapacity } diff --git a/Tests/NetworkTypesTests/VariableLengthIntegerTests.swift b/Tests/NetworkTypesTests/VariableLengthIntegerTests.swift index e9fd4ba..312c01a 100644 --- a/Tests/NetworkTypesTests/VariableLengthIntegerTests.swift +++ b/Tests/NetworkTypesTests/VariableLengthIntegerTests.swift @@ -35,18 +35,23 @@ struct VariableLengthIntegerTests { @Test @available(anyAppleOS 26.0, *) func encodedByteCount() throws { + // 0 to 6 bits fit into 1 byte. #expect(try VariableLengthInteger.encodedByteCount(0) == 1) #expect(try VariableLengthInteger.encodedByteCount(0x3F) == 1) + // 7 to 14 bits fit into 2 bytes. #expect(try VariableLengthInteger.encodedByteCount(0x3F + 1) == 2) #expect(try VariableLengthInteger.encodedByteCount(0x3FFF) == 2) + // 15 to 30 bits fit into 4 bytes. #expect(try VariableLengthInteger.encodedByteCount(0x3FFF + 1) == 4) #expect(try VariableLengthInteger.encodedByteCount(0x3FFF_FFFF) == 4) + // 31 to 62 bits fit into 8 bytes. #expect(try VariableLengthInteger.encodedByteCount(0x3FFF_FFFF + 1) == 8) #expect(try VariableLengthInteger.encodedByteCount(VariableLengthInteger.max) == 8) + // Larger values cannot be encoded. #expect(throws: NetworkTypeError.exceedsMaximumValue.self) { try VariableLengthInteger.encodedByteCount(VariableLengthInteger.max + 1) } From dcc70a7464f1b3f1dd4b1883c61135a938b8cb7d Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Wed, 15 Jul 2026 17:44:46 +0100 Subject: [PATCH 3/9] Address linting issues --- Sources/NetworkTypes/Capsule.swift | 23 +++++++++++-------- .../NetworkTypes/VariableLengthInteger.swift | 6 +++-- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Sources/NetworkTypes/Capsule.swift b/Sources/NetworkTypes/Capsule.swift index c8a02c7..ccb8e1d 100644 --- a/Sources/NetworkTypes/Capsule.swift +++ b/Sources/NetworkTypes/Capsule.swift @@ -15,7 +15,9 @@ /// /// Capsule types occupy an open 62-bit IANA registry. public struct CapsuleType: Sendable, Hashable { - /// The numeric type code. Must be in the range `0 ... 2^62 - 1`. + /// The numeric type code. + /// + /// Must be in the range `0 ... 2^62 - 1`. public var code: UInt64 /// Creates a capsule type from its numeric code. @@ -57,7 +59,7 @@ public struct Capsule: ~Escapable { /// The capsule type code. public var type: CapsuleType - /// The value is an opaque byte sequence. Its meaning is defined by ``type``. + /// The value is an opaque byte sequence. public var value: Span /// Creates a capsule from a type and a borrowed value. @@ -80,11 +82,12 @@ public struct Capsule: ~Escapable { } /// Decodes a capsule's header (type and length) from the front of - /// `input`, without requiring the value bytes to be present. Mutates - /// the span to start with the value. + /// `input`. /// - /// This lets a caller inspect the header (e.g., to reject an - /// oversized value) before consuming the value. + /// This does not require the value bytes to be present. Mutates + /// the span to start with the value. This lets a caller inspect the + /// header (e.g., to reject an oversized value) before consuming + /// the value. /// /// - Returns: The `HeaderInformation` for capsule type or /// `nil` if `input` does not yet contain a complete type and @@ -98,11 +101,11 @@ public struct Capsule: ~Escapable { } /// Decodes a capsule's header (type and length) from the front of - /// `input`, without requiring the value bytes to be present. This - /// does not mutate the span. + /// `input`. /// - /// This lets a caller inspect the header (e.g., to reject an - /// oversized value) before consuming the value. + /// This does not require the value bytes to be present. This + /// does not mutate the span. This lets a caller inspect the header + /// (e.g., to reject an oversized value) before consuming the value. /// /// - Returns: The `HeaderInformation` for capsule type or /// `nil` if `input` does not yet contain a complete type and diff --git a/Sources/NetworkTypes/VariableLengthInteger.swift b/Sources/NetworkTypes/VariableLengthInteger.swift index 33e4d30..4386e5e 100644 --- a/Sources/NetworkTypes/VariableLengthInteger.swift +++ b/Sources/NetworkTypes/VariableLengthInteger.swift @@ -84,8 +84,10 @@ public enum VariableLengthInteger { /// The decoded value. public let value: UInt64 - /// Bytes used for the encoding. This might differ from `encodedByteCount(value)` - /// since it is not required that values are encoded using the minimum number of bytes. + /// Number of bytes used for the encoding. + /// + /// This might differ from `encodedByteCount(value)` since it is not + /// required that values are encoded using the minimum number of bytes. public let byteCount: Int } From c14f2eae50b74455b571b661e2913e33c7827edf Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Thu, 16 Jul 2026 14:23:36 +0100 Subject: [PATCH 4/9] Start addressing review feedback * Move Capsule and CapsuleType into separate files * Extend CapsuleType docs * Use rawValue instead of code in CapsuleType --- .../{ => CapsuleProtocol}/Capsule.swift | 52 ++--------------- .../CapsuleProtocol/CapsuleType.swift | 57 +++++++++++++++++++ .../NetworkTypes/VariableLengthInteger.swift | 2 - Tests/NetworkTypesTests/CapsuleTests.swift | 2 +- 4 files changed, 64 insertions(+), 49 deletions(-) rename Sources/NetworkTypes/{ => CapsuleProtocol}/Capsule.swift (77%) create mode 100644 Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift diff --git a/Sources/NetworkTypes/Capsule.swift b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift similarity index 77% rename from Sources/NetworkTypes/Capsule.swift rename to Sources/NetworkTypes/CapsuleProtocol/Capsule.swift index ccb8e1d..b0cde33 100644 --- a/Sources/NetworkTypes/Capsule.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift @@ -11,52 +11,12 @@ // //===----------------------------------------------------------------------===// -/// A capsule type code, as defined in RFC 9297, Section 3.2. -/// -/// Capsule types occupy an open 62-bit IANA registry. -public struct CapsuleType: Sendable, Hashable { - /// The numeric type code. - /// - /// Must be in the range `0 ... 2^62 - 1`. - public var code: UInt64 - - /// Creates a capsule type from its numeric code. - /// - /// - Precondition: `code` is less than or equal to ``VariableLengthInteger/max``. - public init(_ code: UInt64) { - // This cannot be hit remotely when decoding capsules since - // the `VariableLengthInteger` decoder interprets those bits. - assert(code <= VariableLengthInteger.max, "capsule type \(code) exceeds the variable-length integer maximum") - self.code = code - } - - /// The `DATAGRAM` capsule type (RFC 9297, Section 3.5). - public static var datagram: CapsuleType { 0x00 } - - /// The `ADDRESS_ASSIGN` capusle type (RFC 9484, Section 4.7.1). - public static var addressAssign: CapsuleType { 0x01 } - - /// The `ADDRESS_REQUEST` capusle type (RFC 9484, Section 4.7.2). - public static var addressRequest: CapsuleType { 0x02 } - - /// The `ROUTE_ADVERTISEMENT` capsule type (RFC 9484, Section 4.7.3). - public static var routeAdvertisement: CapsuleType { 0x03 } -} - -// Enable direct initialization from integer literals -extension CapsuleType: ExpressibleByIntegerLiteral { - public init(integerLiteral value: UInt64) { - self.init(value) - } -} - /// A single capsule containing a type code and its opaque value, per RFC 9297, Section 3.2. /// /// ``value`` is a borrowed view into the bytes the capsule was decoded from. /// The value's internal structure is defined by ``type``. -@available(anyAppleOS 26.0, *) public struct Capsule: ~Escapable { - /// The capsule type code. + /// The capsule type. public var type: CapsuleType /// The value is an opaque byte sequence. @@ -71,7 +31,7 @@ public struct Capsule: ~Escapable { /// Header information for a Capsule. public struct HeaderInformation { - /// The capsule type code. + /// The capsule type. public let type: CapsuleType /// Length of the capsule's value following the header. @@ -119,7 +79,7 @@ public struct Capsule: ~Escapable { return nil } return HeaderInformation( - type: CapsuleType(decodedType.value), + type: .init(decodedType.value), valueByteCount: Int(decodedLength.value), headerByteCount: decodedType.byteCount + decodedLength.byteCount ) @@ -158,7 +118,7 @@ extension Capsule { /// maximum length of ``VariableLengthInteger``. public var encodedByteCount: Int { get throws(NetworkTypeError) { - try VariableLengthInteger.encodedByteCount(self.type.code) + try VariableLengthInteger.encodedByteCount(self.type.rawValue) + VariableLengthInteger.encodedByteCount(UInt64(self.value.count)) + self.value.count } @@ -174,7 +134,7 @@ extension Capsule { } // First the type and value length ... - try VariableLengthInteger.encode(self.type.code, into: &output) + try VariableLengthInteger.encode(self.type.rawValue, into: &output) try VariableLengthInteger.encode(UInt64(self.value.count), into: &output) // ... followed by the value itself. @@ -191,7 +151,7 @@ extension Capsule { /// - throws: ``NetworkTypeError`` if `output` has does not /// have at least ``encodedByteCount`` bytes of remaining capacity public static func encodeHeader(type: CapsuleType, valueByteCount: Int, into output: inout OutputSpan) throws(NetworkTypeError) { - try VariableLengthInteger.encode(type.code, into: &output) + try VariableLengthInteger.encode(type.rawValue, into: &output) try VariableLengthInteger.encode(UInt64(valueByteCount), into: &output) } } diff --git a/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift new file mode 100644 index 0000000..8b3a301 --- /dev/null +++ b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A capsule type, as defined in RFC 9297, Section 3.2. +/// +/// Capsule types occupy an open 62-bit IANA registry. They define the meaning of the value carried +/// in the capsule. As an example, a `DATAGRAM` capsule (0x00, RFC 9297) simply carries the payload +/// of an HTTP datagram with meaning left to the application. In contrast, an `ADDRESS_ASSIGN` +/// capsule (0x01, RFC 9484) defines the structure of the value and the inforamtion it carries. +/// +/// The named capsule types defined here are not exhaustive as new standards can define new capsule +/// types. +public struct CapsuleType: Sendable, Hashable { + /// The numeric type rawValue. + /// + /// Must be in the range `0 ... 2^62 - 1`. + public var rawValue: UInt64 + + /// Creates a capsule type from its numeric rawValue. + /// + /// - Precondition: `rawValue` is less than or equal to ``VariableLengthInteger/max``. + public init(_ rawValue: UInt64) { + // This cannot be hit remotely when decoding capsules since + // the `VariableLengthInteger` decoder interprets those bits. + assert(rawValue <= VariableLengthInteger.max, "capsule type \(rawValue) exceeds the variable-length integer maximum") + self.rawValue = rawValue + } + + /// The `DATAGRAM` capsule type (RFC 9297, Section 3.5). + public static var datagram: Self { 0x00 } + + /// The `ADDRESS_ASSIGN` capusle type (RFC 9484, Section 4.7.1). + public static var addressAssign: Self { 0x01 } + + /// The `ADDRESS_REQUEST` capusle type (RFC 9484, Section 4.7.2). + public static var addressRequest: Self { 0x02 } + + /// The `ROUTE_ADVERTISEMENT` capsule type (RFC 9484, Section 4.7.3). + public static var routeAdvertisement: Self { 0x03 } +} + +// Enable direct initialization from integer literals +extension CapsuleType: ExpressibleByIntegerLiteral { + public init(integerLiteral value: UInt64) { + self.init(value) + } +} diff --git a/Sources/NetworkTypes/VariableLengthInteger.swift b/Sources/NetworkTypes/VariableLengthInteger.swift index 4386e5e..9552d30 100644 --- a/Sources/NetworkTypes/VariableLengthInteger.swift +++ b/Sources/NetworkTypes/VariableLengthInteger.swift @@ -54,7 +54,6 @@ public enum VariableLengthInteger { /// /// - throws: ``NetworkTypeError.exceedsMaximumValue`` if `value` is greater than ``max``. /// - throws: ``NetworkTypeError.insufficientCapacity`` if `output` does not have enough capacity to encode `value`. - @available(anyAppleOS 26.0, *) public static func encode(_ value: UInt64, into output: inout OutputSpan) throws(NetworkTypeError) { let byteCount = try Self.encodedByteCount(value) guard output.capacity >= byteCount else { @@ -95,7 +94,6 @@ public enum VariableLengthInteger { /// /// - Returns: The decoded value and the number of bytes it occupied, or /// `nil` if `input` holds fewer bytes than its length prefix requires. - @available(anyAppleOS 26.0, *) public static func decode(from input: Span) -> DecodeResult? { guard input.count > 0 else { return nil diff --git a/Tests/NetworkTypesTests/CapsuleTests.swift b/Tests/NetworkTypesTests/CapsuleTests.swift index 00ea12d..6a22ba3 100644 --- a/Tests/NetworkTypesTests/CapsuleTests.swift +++ b/Tests/NetworkTypesTests/CapsuleTests.swift @@ -18,7 +18,7 @@ import Testing struct CapsuleTests { @Test func datagramTypeIsZero() { - #expect(CapsuleType.datagram.code == 0) + #expect(CapsuleType.datagram.rawValue == 0) #expect(CapsuleType.datagram == 0) } From 8827342ef1f42142a030a9e0d24686d4febda053 Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Thu, 16 Jul 2026 14:34:07 +0100 Subject: [PATCH 5/9] Use Array instead of Span --- .../NetworkTypes/CapsuleProtocol/Capsule.swift | 18 ++++++++++++------ Tests/NetworkTypesTests/CapsuleTests.swift | 10 +++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift index b0cde33..bf58e78 100644 --- a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift @@ -15,16 +15,15 @@ /// /// ``value`` is a borrowed view into the bytes the capsule was decoded from. /// The value's internal structure is defined by ``type``. -public struct Capsule: ~Escapable { +public struct Capsule { /// The capsule type. public var type: CapsuleType /// The value is an opaque byte sequence. - public var value: Span + public var value: Array /// Creates a capsule from a type and a borrowed value. - @_lifetime(copy value) - public init(type: CapsuleType, value: Span) { + public init(type: CapsuleType, value: Array) { self.type = type self.value = value } @@ -90,7 +89,6 @@ public struct Capsule: ~Escapable { /// /// - Returns: The decoded capsule, or `nil` if `input` does not yet contain a /// complete capsule. When `nil` is returned, `input` is left unchanged. - @_lifetime(copy input) public static func decode(from input: inout Span) -> Capsule? { guard let header = Self.peekHeader(from: input) else { return nil @@ -100,7 +98,15 @@ public struct Capsule: ~Escapable { return nil } - let value = input.extracting(droppingFirst: header.headerByteCount) + // Copy the value. + let valueSpan = input.extracting(droppingFirst: header.headerByteCount) + let value = Array(capacity: valueSpan.count) { outputSpan in + for index in 0.. Date: Thu, 16 Jul 2026 16:40:12 +0100 Subject: [PATCH 6/9] Address more feedback --- .../CapsuleProtocol/Capsule.swift | 32 ++++++++------ .../CapsuleProtocol/CapsuleType.swift | 8 +++- ....swift => QUICVariableLengthInteger.swift} | 43 ++++++++++--------- Tests/NetworkTypesTests/CapsuleTests.swift | 2 +- ...t => QUICVariableLengthIntegerTests.swift} | 40 ++++++++--------- 5 files changed, 70 insertions(+), 55 deletions(-) rename Sources/NetworkTypes/{VariableLengthInteger.swift => QUICVariableLengthInteger.swift} (90%) rename Tests/NetworkTypesTests/{VariableLengthIntegerTests.swift => QUICVariableLengthIntegerTests.swift} (68%) diff --git a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift index bf58e78..472afe5 100644 --- a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift @@ -29,15 +29,23 @@ public struct Capsule { } /// Header information for a Capsule. - public struct HeaderInformation { + public struct HeaderInformation: Sendable, Hashable { + + /// Initialize new HeaderInformation. + public init(type: CapsuleType, valueByteCount: Int, headerByteCount: Int) { + self.type = type + self.valueByteCount = valueByteCount + self.headerByteCount = headerByteCount + } + /// The capsule type. - public let type: CapsuleType + public var type: CapsuleType /// Length of the capsule's value following the header. - public let valueByteCount: Int + public var valueByteCount: Int /// Number of bytes used to encode the capsule type and value length. - public let headerByteCount: Int + public var headerByteCount: Int } /// Decodes a capsule's header (type and length) from the front of @@ -70,11 +78,11 @@ public struct Capsule { /// `nil` if `input` does not yet contain a complete type and /// length. public static func peekHeader(from input: Span) -> HeaderInformation? { - guard let decodedType = VariableLengthInteger.decode(from: input) else { + guard let decodedType = QUICVariableLengthInteger.decode(from: input) else { return nil } let remainingBytes = input.extracting(droppingFirst: decodedType.byteCount) - guard let decodedLength = VariableLengthInteger.decode(from: remainingBytes) else { + guard let decodedLength = QUICVariableLengthInteger.decode(from: remainingBytes) else { return nil } return HeaderInformation( @@ -124,8 +132,8 @@ extension Capsule { /// maximum length of ``VariableLengthInteger``. public var encodedByteCount: Int { get throws(NetworkTypeError) { - try VariableLengthInteger.encodedByteCount(self.type.rawValue) - + VariableLengthInteger.encodedByteCount(UInt64(self.value.count)) + try QUICVariableLengthInteger.encodedByteCount(self.type.rawValue) + + QUICVariableLengthInteger.encodedByteCount(UInt64(self.value.count)) + self.value.count } } @@ -140,8 +148,8 @@ extension Capsule { } // First the type and value length ... - try VariableLengthInteger.encode(self.type.rawValue, into: &output) - try VariableLengthInteger.encode(UInt64(self.value.count), into: &output) + try QUICVariableLengthInteger.encode(self.type.rawValue, into: &output) + try QUICVariableLengthInteger.encode(UInt64(self.value.count), into: &output) // ... followed by the value itself. for index in 0..) throws(NetworkTypeError) { - try VariableLengthInteger.encode(type.rawValue, into: &output) - try VariableLengthInteger.encode(UInt64(valueByteCount), into: &output) + try QUICVariableLengthInteger.encode(type.rawValue, into: &output) + try QUICVariableLengthInteger.encode(UInt64(valueByteCount), into: &output) } } diff --git a/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift index 8b3a301..400e37c 100644 --- a/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift @@ -24,7 +24,11 @@ public struct CapsuleType: Sendable, Hashable { /// The numeric type rawValue. /// /// Must be in the range `0 ... 2^62 - 1`. - public var rawValue: UInt64 + public var rawValue: UInt64 { + didSet { + precondition(rawValue <= QUICVariableLengthInteger.max, "capsule type \(rawValue) exceeds the allowed maximum value") + } + } /// Creates a capsule type from its numeric rawValue. /// @@ -32,7 +36,7 @@ public struct CapsuleType: Sendable, Hashable { public init(_ rawValue: UInt64) { // This cannot be hit remotely when decoding capsules since // the `VariableLengthInteger` decoder interprets those bits. - assert(rawValue <= VariableLengthInteger.max, "capsule type \(rawValue) exceeds the variable-length integer maximum") + assert(rawValue <= QUICVariableLengthInteger.max, "capsule type \(rawValue) exceeds the variable-length integer maximum") self.rawValue = rawValue } diff --git a/Sources/NetworkTypes/VariableLengthInteger.swift b/Sources/NetworkTypes/QUICVariableLengthInteger.swift similarity index 90% rename from Sources/NetworkTypes/VariableLengthInteger.swift rename to Sources/NetworkTypes/QUICVariableLengthInteger.swift index 9552d30..c74d1de 100644 --- a/Sources/NetworkTypes/VariableLengthInteger.swift +++ b/Sources/NetworkTypes/QUICVariableLengthInteger.swift @@ -16,7 +16,7 @@ /// The two most significant bits of the first byte select the encoded length /// (1, 2, 4, or 8 bytes); the remaining bits hold the value in network byte /// order. -public enum VariableLengthInteger { +public enum QUICVariableLengthInteger { /// The largest representable value, `2^62 - 1`. public static var max: UInt64 { 0x3FFF_FFFF_FFFF_FFFF } @@ -35,21 +35,6 @@ public enum VariableLengthInteger { } } - /// Write`value` into `output` in network byte order. - /// - /// - Precondition: `output` has enough capcity to write `value`. - private static func write(_ value: T, into output: inout OutputSpan) { - let numBytes = MemoryLayout.size - assert(output.capacity >= numBytes) - let valueBigEndian = value.bigEndian - - withUnsafeBytes(of: valueBigEndian) { bufferPointer in - for unsafe value in unsafe bufferPointer { - output.append(value) - } - } - } - /// Encodes `value` into `output` using the minimal number of bytes. /// /// - throws: ``NetworkTypeError.exceedsMaximumValue`` if `value` is greater than ``max``. @@ -63,16 +48,16 @@ public enum VariableLengthInteger { switch byteCount { case 1: let value = UInt8(truncatingIfNeeded: value) - Self.write(value, into: &output) + output.write(value) case 2: let value = UInt16(truncatingIfNeeded: value | 0x4000) - Self.write(value, into: &output) + output.write(value) case 4: let value = UInt32(truncatingIfNeeded: value | 0x8000_0000) - Self.write(value, into: &output) + output.write(value) case 8: let value = UInt64(truncatingIfNeeded: value | 0xC000_0000_0000_0000) - Self.write(value, into: &output) + output.write(value) default: throw .exceedsMaximumValue } @@ -113,3 +98,21 @@ public enum VariableLengthInteger { return .init(value: value, byteCount: byteCount) } } + +extension OutputSpan { + /// Write`value` into `output` in network byte order. + /// + /// - Precondition: `output` has enough capcity to write `value`. + mutating func write(_ value: T) { + let numBytes = MemoryLayout.size + assert(self.capacity >= numBytes) + let valueBigEndian = value.bigEndian + + withUnsafeBytes(of: valueBigEndian) { bufferPointer in + for unsafe value in unsafe bufferPointer { + self.append(value) + } + } + } + +} diff --git a/Tests/NetworkTypesTests/CapsuleTests.swift b/Tests/NetworkTypesTests/CapsuleTests.swift index 1a69230..992c1f8 100644 --- a/Tests/NetworkTypesTests/CapsuleTests.swift +++ b/Tests/NetworkTypesTests/CapsuleTests.swift @@ -116,7 +116,7 @@ struct CapsuleTests { func peekAllFCapsuleTypeHeader() { let bytes: [UInt8] = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00] let header = Capsule.peekHeader(from: bytes.span) - #expect(header?.type == CapsuleType(VariableLengthInteger.max)) + #expect(header?.type == CapsuleType(QUICVariableLengthInteger.max)) #expect(header?.valueByteCount == 0) } diff --git a/Tests/NetworkTypesTests/VariableLengthIntegerTests.swift b/Tests/NetworkTypesTests/QUICVariableLengthIntegerTests.swift similarity index 68% rename from Tests/NetworkTypesTests/VariableLengthIntegerTests.swift rename to Tests/NetworkTypesTests/QUICVariableLengthIntegerTests.swift index 312c01a..a7cdd16 100644 --- a/Tests/NetworkTypesTests/VariableLengthIntegerTests.swift +++ b/Tests/NetworkTypesTests/QUICVariableLengthIntegerTests.swift @@ -15,12 +15,12 @@ import NetworkTypes import Testing @Suite -struct VariableLengthIntegerTests { +struct QUICVariableLengthIntegerTests { /// Encodes `value` into a fresh byte array via the OutputSpan initializer. @available(anyAppleOS 26.0, *) static func encoded(_ value: UInt64) throws -> [UInt8] { try [UInt8](capacity: 8) { output in - try VariableLengthInteger.encode(value, into: &output) + try QUICVariableLengthInteger.encode(value, into: &output) } } @@ -28,7 +28,7 @@ struct VariableLengthIntegerTests { @available(anyAppleOS 26.0, *) static func encoded(_ value: UInt64, intoBufferWithCapacity capacity: Int) throws -> [UInt8] { try [UInt8](capacity: capacity) { output in - try VariableLengthInteger.encode(value, into: &output) + try QUICVariableLengthInteger.encode(value, into: &output) } } @@ -36,24 +36,24 @@ struct VariableLengthIntegerTests { @available(anyAppleOS 26.0, *) func encodedByteCount() throws { // 0 to 6 bits fit into 1 byte. - #expect(try VariableLengthInteger.encodedByteCount(0) == 1) - #expect(try VariableLengthInteger.encodedByteCount(0x3F) == 1) + #expect(try QUICVariableLengthInteger.encodedByteCount(0) == 1) + #expect(try QUICVariableLengthInteger.encodedByteCount(0x3F) == 1) // 7 to 14 bits fit into 2 bytes. - #expect(try VariableLengthInteger.encodedByteCount(0x3F + 1) == 2) - #expect(try VariableLengthInteger.encodedByteCount(0x3FFF) == 2) + #expect(try QUICVariableLengthInteger.encodedByteCount(0x3F + 1) == 2) + #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF) == 2) // 15 to 30 bits fit into 4 bytes. - #expect(try VariableLengthInteger.encodedByteCount(0x3FFF + 1) == 4) - #expect(try VariableLengthInteger.encodedByteCount(0x3FFF_FFFF) == 4) + #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF + 1) == 4) + #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF_FFFF) == 4) // 31 to 62 bits fit into 8 bytes. - #expect(try VariableLengthInteger.encodedByteCount(0x3FFF_FFFF + 1) == 8) - #expect(try VariableLengthInteger.encodedByteCount(VariableLengthInteger.max) == 8) + #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF_FFFF + 1) == 8) + #expect(try QUICVariableLengthInteger.encodedByteCount(QUICVariableLengthInteger.max) == 8) // Larger values cannot be encoded. #expect(throws: NetworkTypeError.exceedsMaximumValue.self) { - try VariableLengthInteger.encodedByteCount(VariableLengthInteger.max + 1) + try QUICVariableLengthInteger.encodedByteCount(QUICVariableLengthInteger.max + 1) } } @@ -61,13 +61,13 @@ struct VariableLengthIntegerTests { @available(anyAppleOS 26.0, *) func decodesRFC9000Vectors() { let eightByte: [UInt8] = [0xc2, 0x19, 0x7c, 0x5e, 0xff, 0x14, 0xe8, 0x8c] - #expect(VariableLengthInteger.decode(from: eightByte.span)?.value == 151_288_809_941_952_652) + #expect(QUICVariableLengthInteger.decode(from: eightByte.span)?.value == 151_288_809_941_952_652) let fourByte: [UInt8] = [0x9d, 0x7f, 0x3e, 0x7d] - #expect(VariableLengthInteger.decode(from: fourByte.span)?.value == 494_878_333) + #expect(QUICVariableLengthInteger.decode(from: fourByte.span)?.value == 494_878_333) let twoByte: [UInt8] = [0x7b, 0xbd] - #expect(VariableLengthInteger.decode(from: twoByte.span)?.value == 15_293) + #expect(QUICVariableLengthInteger.decode(from: twoByte.span)?.value == 15_293) let oneByte: [UInt8] = [0x25] - #expect(VariableLengthInteger.decode(from: oneByte.span)?.value == 37) + #expect(QUICVariableLengthInteger.decode(from: oneByte.span)?.value == 37) } @Test @@ -75,7 +75,7 @@ struct VariableLengthIntegerTests { func decodesNonMinimalEncoding() { // 37 encoded in two bytes instead of one; legal to receive (RFC 9297 §1.1). let bytes: [UInt8] = [0x40, 0x25] - let result = VariableLengthInteger.decode(from: bytes.span) + let result = QUICVariableLengthInteger.decode(from: bytes.span) #expect(result?.value == 37) #expect(result?.byteCount == 2) } @@ -85,9 +85,9 @@ struct VariableLengthIntegerTests { func decodeReturnsNilWhenTruncated() { // First byte announces a 4-byte integer, only 2 bytes present. let truncated: [UInt8] = [0x9d, 0x7f] - #expect(VariableLengthInteger.decode(from: truncated.span) == nil) + #expect(QUICVariableLengthInteger.decode(from: truncated.span) == nil) let empty: [UInt8] = [] - #expect(VariableLengthInteger.decode(from: empty.span) == nil) + #expect(QUICVariableLengthInteger.decode(from: empty.span) == nil) } @Test @@ -115,7 +115,7 @@ struct VariableLengthIntegerTests { @available(anyAppleOS 26.0, *) func roundTripsAllBoundaries(value: UInt64) throws { let bytes = try Self.encoded(value) - let decoded = VariableLengthInteger.decode(from: bytes.span) + let decoded = QUICVariableLengthInteger.decode(from: bytes.span) #expect(decoded?.value == value) #expect(decoded?.byteCount == bytes.count) } From e19d4fab68ca899cda1a0674d6c9df51b563d590 Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Fri, 17 Jul 2026 09:51:39 +0100 Subject: [PATCH 7/9] Formatting --- Sources/NetworkTypes/CapsuleProtocol/Capsule.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift index 472afe5..aa43b62 100644 --- a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift @@ -20,10 +20,10 @@ public struct Capsule { public var type: CapsuleType /// The value is an opaque byte sequence. - public var value: Array + public var value: [UInt8] /// Creates a capsule from a type and a borrowed value. - public init(type: CapsuleType, value: Array) { + public init(type: CapsuleType, value: [UInt8]) { self.type = type self.value = value } @@ -108,7 +108,7 @@ public struct Capsule { // Copy the value. let valueSpan = input.extracting(droppingFirst: header.headerByteCount) - let value = Array(capacity: valueSpan.count) { outputSpan in + let value = [UInt8](capacity: valueSpan.count) { outputSpan in for index in 0.. Date: Fri, 17 Jul 2026 15:05:56 +0100 Subject: [PATCH 8/9] Remove (de)serialization --- .../CapsuleProtocol/Capsule.swift | 152 +---------------- .../CapsuleProtocol/CapsuleType.swift | 29 ++-- Sources/NetworkTypes/NetworkTypeError.swift | 23 --- .../QUICVariableLengthInteger.swift | 118 ------------- Tests/NetworkTypesTests/CapsuleTests.swift | 155 +----------------- .../NetworkTypesTests/CapsuleTypeTests.swift | 44 +++++ .../QUICVariableLengthIntegerTests.swift | 122 -------------- 7 files changed, 72 insertions(+), 571 deletions(-) delete mode 100644 Sources/NetworkTypes/NetworkTypeError.swift delete mode 100644 Sources/NetworkTypes/QUICVariableLengthInteger.swift create mode 100644 Tests/NetworkTypesTests/CapsuleTypeTests.swift delete mode 100644 Tests/NetworkTypesTests/QUICVariableLengthIntegerTests.swift diff --git a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift index aa43b62..79382a5 100644 --- a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift @@ -13,159 +13,21 @@ /// A single capsule containing a type code and its opaque value, per RFC 9297, Section 3.2. /// -/// ``value`` is a borrowed view into the bytes the capsule was decoded from. -/// The value's internal structure is defined by ``type``. -public struct Capsule { +/// The Capsule protocol can be used through HTTP upgrade tokens that support it. Both +/// endpoints must agree to use it, e.g., using Extended CONNECT in H2 and H3. +/// +/// A capsule is a TLV type. `value` contains opaque bytes, with a meaning defined +/// by `type`. The length is `value.count`. +public struct Capsule: Sendable { /// The capsule type. public var type: CapsuleType /// The value is an opaque byte sequence. public var value: [UInt8] - /// Creates a capsule from a type and a borrowed value. + /// Creates a capsule from a type and a value. public init(type: CapsuleType, value: [UInt8]) { self.type = type self.value = value } - - /// Header information for a Capsule. - public struct HeaderInformation: Sendable, Hashable { - - /// Initialize new HeaderInformation. - public init(type: CapsuleType, valueByteCount: Int, headerByteCount: Int) { - self.type = type - self.valueByteCount = valueByteCount - self.headerByteCount = headerByteCount - } - - /// The capsule type. - public var type: CapsuleType - - /// Length of the capsule's value following the header. - public var valueByteCount: Int - - /// Number of bytes used to encode the capsule type and value length. - public var headerByteCount: Int - } - - /// Decodes a capsule's header (type and length) from the front of - /// `input`. - /// - /// This does not require the value bytes to be present. Mutates - /// the span to start with the value. This lets a caller inspect the - /// header (e.g., to reject an oversized value) before consuming - /// the value. - /// - /// - Returns: The `HeaderInformation` for capsule type or - /// `nil` if `input` does not yet contain a complete type and - /// length. - public static func decodeHeader(from input: inout Span) -> HeaderInformation? { - guard let header = Self.peekHeader(from: input) else { - return nil - } - input = input.extracting(header.headerByteCount...) - return header - } - - /// Decodes a capsule's header (type and length) from the front of - /// `input`. - /// - /// This does not require the value bytes to be present. This - /// does not mutate the span. This lets a caller inspect the header - /// (e.g., to reject an oversized value) before consuming the value. - /// - /// - Returns: The `HeaderInformation` for capsule type or - /// `nil` if `input` does not yet contain a complete type and - /// length. - public static func peekHeader(from input: Span) -> HeaderInformation? { - guard let decodedType = QUICVariableLengthInteger.decode(from: input) else { - return nil - } - let remainingBytes = input.extracting(droppingFirst: decodedType.byteCount) - guard let decodedLength = QUICVariableLengthInteger.decode(from: remainingBytes) else { - return nil - } - return HeaderInformation( - type: .init(decodedType.value), - valueByteCount: Int(decodedLength.value), - headerByteCount: decodedType.byteCount + decodedLength.byteCount - ) - } - - /// Decodes one capsule from the front of `input`, advancing `input` past the - /// bytes it consumed. - /// - /// - Returns: The decoded capsule, or `nil` if `input` does not yet contain a - /// complete capsule. When `nil` is returned, `input` is left unchanged. - public static func decode(from input: inout Span) -> Capsule? { - guard let header = Self.peekHeader(from: input) else { - return nil - } - let capsuleByteCount = header.headerByteCount + header.valueByteCount - guard capsuleByteCount <= input.count else { - return nil - } - - // Copy the value. - let valueSpan = input.extracting(droppingFirst: header.headerByteCount) - let value = [UInt8](capacity: valueSpan.count) { outputSpan in - for index in 0..) throws(NetworkTypeError) { - if try output.capacity < self.encodedByteCount { - throw .insufficientCapacity - } - - // First the type and value length ... - try QUICVariableLengthInteger.encode(self.type.rawValue, into: &output) - try QUICVariableLengthInteger.encode(UInt64(self.value.count), into: &output) - - // ... followed by the value itself. - for index in 0..) throws(NetworkTypeError) { - try QUICVariableLengthInteger.encode(type.rawValue, into: &output) - try QUICVariableLengthInteger.encode(UInt64(valueByteCount), into: &output) - } } diff --git a/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift index 400e37c..57152f6 100644 --- a/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift @@ -18,15 +18,18 @@ /// of an HTTP datagram with meaning left to the application. In contrast, an `ADDRESS_ASSIGN` /// capsule (0x01, RFC 9484) defines the structure of the value and the inforamtion it carries. /// -/// The named capsule types defined here are not exhaustive as new standards can define new capsule -/// types. +/// The capsule types defined here are not exhaustive. New standards can define new ones. public struct CapsuleType: Sendable, Hashable { + + /// The largest representable value, `2^62 - 1`. + static var maxRawValue: UInt64 { 0x3FFF_FFFF_FFFF_FFFF } + /// The numeric type rawValue. /// /// Must be in the range `0 ... 2^62 - 1`. public var rawValue: UInt64 { didSet { - precondition(rawValue <= QUICVariableLengthInteger.max, "capsule type \(rawValue) exceeds the allowed maximum value") + precondition(rawValue <= Self.maxRawValue, "capsule type \(rawValue) exceeds the allowed maximum value") } } @@ -34,12 +37,19 @@ public struct CapsuleType: Sendable, Hashable { /// /// - Precondition: `rawValue` is less than or equal to ``VariableLengthInteger/max``. public init(_ rawValue: UInt64) { - // This cannot be hit remotely when decoding capsules since - // the `VariableLengthInteger` decoder interprets those bits. - assert(rawValue <= QUICVariableLengthInteger.max, "capsule type \(rawValue) exceeds the variable-length integer maximum") + precondition(rawValue <= Self.maxRawValue, "capsule type \(rawValue) exceeds the variable-length integer maximum") self.rawValue = rawValue } +} +// Enable direct initialization from integer literals +extension CapsuleType: ExpressibleByIntegerLiteral { + public init(integerLiteral value: UInt64) { + self.init(value) + } +} + +extension CapsuleType { /// The `DATAGRAM` capsule type (RFC 9297, Section 3.5). public static var datagram: Self { 0x00 } @@ -52,10 +62,3 @@ public struct CapsuleType: Sendable, Hashable { /// The `ROUTE_ADVERTISEMENT` capsule type (RFC 9484, Section 4.7.3). public static var routeAdvertisement: Self { 0x03 } } - -// Enable direct initialization from integer literals -extension CapsuleType: ExpressibleByIntegerLiteral { - public init(integerLiteral value: UInt64) { - self.init(value) - } -} diff --git a/Sources/NetworkTypes/NetworkTypeError.swift b/Sources/NetworkTypes/NetworkTypeError.swift deleted file mode 100644 index 33b10fa..0000000 --- a/Sources/NetworkTypes/NetworkTypeError.swift +++ /dev/null @@ -1,23 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// Errors returned from other network types. -@nonexhaustive -public enum NetworkTypeError: Error { - /// A input value exceeds the maximum by the specification. - case exceedsMaximumValue - - /// A value cannot be written to a buffer because the buffer - /// does not have sufficient capacity to hold it. - case insufficientCapacity -} diff --git a/Sources/NetworkTypes/QUICVariableLengthInteger.swift b/Sources/NetworkTypes/QUICVariableLengthInteger.swift deleted file mode 100644 index c74d1de..0000000 --- a/Sources/NetworkTypes/QUICVariableLengthInteger.swift +++ /dev/null @@ -1,118 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// A QUIC variable-length integer, as defined in RFC 9000, Section 16. -/// -/// The two most significant bits of the first byte select the encoded length -/// (1, 2, 4, or 8 bytes); the remaining bits hold the value in network byte -/// order. -public enum QUICVariableLengthInteger { - /// The largest representable value, `2^62 - 1`. - public static var max: UInt64 { 0x3FFF_FFFF_FFFF_FFFF } - - /// The number of bytes the minimal encoding of `value` occupies (1, 2, 4, or 8). - /// - /// - throws: ``NetworkTypeError.exceedsMaximumValue`` if `value` is greater than ``max``. - public static func encodedByteCount(_ value: UInt64) throws(NetworkTypeError) -> Int { - // 2 bits are reserved to encode the number of bytes. - // N bytes can encode N * 8 - 2 bits. - switch 64 - value.leadingZeroBitCount { - case 0...6: return 1 // fits in 6 bits - case 7...14: return 2 // fits in 14 bits - case 15...30: return 4 // fits in 30 bits - case 31...62: return 8 // fits in 62 bits - default: throw .exceedsMaximumValue - } - } - - /// Encodes `value` into `output` using the minimal number of bytes. - /// - /// - throws: ``NetworkTypeError.exceedsMaximumValue`` if `value` is greater than ``max``. - /// - throws: ``NetworkTypeError.insufficientCapacity`` if `output` does not have enough capacity to encode `value`. - public static func encode(_ value: UInt64, into output: inout OutputSpan) throws(NetworkTypeError) { - let byteCount = try Self.encodedByteCount(value) - guard output.capacity >= byteCount else { - throw NetworkTypeError.insufficientCapacity - } - - switch byteCount { - case 1: - let value = UInt8(truncatingIfNeeded: value) - output.write(value) - case 2: - let value = UInt16(truncatingIfNeeded: value | 0x4000) - output.write(value) - case 4: - let value = UInt32(truncatingIfNeeded: value | 0x8000_0000) - output.write(value) - case 8: - let value = UInt64(truncatingIfNeeded: value | 0xC000_0000_0000_0000) - output.write(value) - default: - throw .exceedsMaximumValue - } - } - - /// A decoded value and the bytes read to decode it. - public struct DecodeResult { - /// The decoded value. - public let value: UInt64 - - /// Number of bytes used for the encoding. - /// - /// This might differ from `encodedByteCount(value)` since it is not - /// required that values are encoded using the minimum number of bytes. - public let byteCount: Int - } - - /// Decodes the variable-length integer at the front of `input`. - /// - /// - Returns: The decoded value and the number of bytes it occupied, or - /// `nil` if `input` holds fewer bytes than its length prefix requires. - public static func decode(from input: Span) -> DecodeResult? { - guard input.count > 0 else { - return nil - } - - let first = input[0] - // Extract bits that signifiy the number bytes used for encoding. - let byteCount = 1 << Int(first >> 6) - guard input.count >= byteCount else { - return nil - } - - var value = UInt64(first & 0x3F) - for index in 1.. { - /// Write`value` into `output` in network byte order. - /// - /// - Precondition: `output` has enough capcity to write `value`. - mutating func write(_ value: T) { - let numBytes = MemoryLayout.size - assert(self.capacity >= numBytes) - let valueBigEndian = value.bigEndian - - withUnsafeBytes(of: valueBigEndian) { bufferPointer in - for unsafe value in unsafe bufferPointer { - self.append(value) - } - } - } - -} diff --git a/Tests/NetworkTypesTests/CapsuleTests.swift b/Tests/NetworkTypesTests/CapsuleTests.swift index 992c1f8..6e84e39 100644 --- a/Tests/NetworkTypesTests/CapsuleTests.swift +++ b/Tests/NetworkTypesTests/CapsuleTests.swift @@ -17,155 +17,10 @@ import Testing @Suite struct CapsuleTests { @Test - func datagramTypeIsZero() { - #expect(CapsuleType.datagram.rawValue == 0) - #expect(CapsuleType.datagram == 0) - } - - @Test - func capsuleTypeEqualityIsByCode() { - #expect(CapsuleType(5) == CapsuleType(5)) - #expect(CapsuleType(5) != CapsuleType(6)) - } - - @Test - @available(anyAppleOS 26.0, *) - func decodesDatagramCapsule() { - let bytes: [UInt8] = [0x00, 0x04, 0xde, 0xad, 0xbe, 0xef] - var rest = bytes.span - if let capsule = Capsule.decode(from: &rest) { - #expect(capsule.type == .datagram) - #expect(capsule.value.count == 4) - #expect(capsule.value[0] == 0xde) - } else { - Issue.record("expected to decode a capsule") - } - #expect(rest.count == 0) - } - - @Test - @available(anyAppleOS 26.0, *) - func decodesEmptyValue() { - let bytes: [UInt8] = [0x00, 0x00] - var rest = bytes.span - if let capsule = Capsule.decode(from: &rest) { - #expect(capsule.type == .datagram) - #expect(capsule.value.count == 0) - } else { - Issue.record("expected to decode an empty-value capsule") - } - #expect(rest.count == 0) - } - - @Test - @available(anyAppleOS 26.0, *) - func decodesUnknownType() { - // Type 0x21 (unassigned), length 2, value [0xaa, 0xbb]. - let bytes: [UInt8] = [0x21, 0x02, 0xaa, 0xbb] - var rest = bytes.span - if let capsule = Capsule.decode(from: &rest) { - #expect(capsule.type == CapsuleType(0x21)) - #expect(capsule.value.count == 2) - } else { - Issue.record("expected to decode an unknown-type capsule") - } - #expect(rest.count == 0) - } - - @Test - @available(anyAppleOS 26.0, *) - func decodesMultipleCapsulesAndLeavesTruncatedRemainder() { - // Two complete capsules, then a truncated third (type 0x00, length 2, one value byte). - let bytes: [UInt8] = [0x00, 0x00, 0x00, 0x01, 0xaa, 0x00, 0x02, 0xbb] - var rest = bytes.span - var count = 0 - while let capsule = Capsule.decode(from: &rest) { - count += 1 - _ = capsule.type - } - #expect(count == 2) - #expect(rest.count == 3) - } - - @Test - @available(anyAppleOS 26.0, *) - func peekHeaderReadsHeaderWithoutMutation() { - // Type 0x00, length 16384 (a 4-byte varint: 80 00 40 00); no value bytes present. - let bytes: [UInt8] = [0x00, 0x80, 0x00, 0x40, 0x00] - let header = Capsule.peekHeader(from: bytes.span) - #expect(header?.type == .datagram) - #expect(header?.valueByteCount == 16_384) - #expect(header?.headerByteCount == 5) - } - - @Test - @available(anyAppleOS 26.0, *) - func decodeHeaderReadsHeaderChangingSpan() { - // Type 0x00, length 16384 (a 4-byte varint: 80 00 40 00); no value bytes present. - let bytes: [UInt8] = [0x00, 0x80, 0x00, 0x40, 0x00] - var bytesSpan = bytes.span - let header = Capsule.decodeHeader(from: &bytesSpan) - #expect(header?.type == .datagram) - #expect(header?.valueByteCount == 16_384) - #expect(header?.headerByteCount == 5) - #expect(bytesSpan.count == 0) - } - - @Test - @available(anyAppleOS 26.0, *) - func peekAllFCapsuleTypeHeader() { - let bytes: [UInt8] = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00] - let header = Capsule.peekHeader(from: bytes.span) - #expect(header?.type == CapsuleType(QUICVariableLengthInteger.max)) - #expect(header?.valueByteCount == 0) - } - - @Test - @available(anyAppleOS 26.0, *) - func encodedByteCountMatchesLayout() throws { - let payload: [UInt8] = [0xde, 0xad, 0xbe, 0xef] - let capsule = Capsule(type: .datagram, value: payload) - #expect(try capsule.encodedByteCount == 6) // 1 (type) + 1 (length) + 4 (value) - } - - @Test - @available(anyAppleOS 26.0, *) - func encodesDatagramToWireBytes() throws { - let payload: [UInt8] = [0xde, 0xad, 0xbe, 0xef] - let count = try Capsule(type: .datagram, value: payload).encodedByteCount - let encoded = try [UInt8](capacity: count) { output in - try Capsule(type: .datagram, value: payload).encode(into: &output) - } - #expect(encoded == [0x00, 0x04, 0xde, 0xad, 0xbe, 0xef]) - } - - @Test - @available(anyAppleOS 26.0, *) - func encodeHeaderWritesOnlyFraming() throws { - let encoded = try [UInt8](capacity: 16) { output in - try Capsule.encodeHeader(type: .datagram, valueByteCount: 4, into: &output) - } - #expect(encoded == [0x00, 0x04]) - } - - @Test - @available(anyAppleOS 26.0, *) - func roundTripsThroughEncodeAndDecode() throws { - let payload: [UInt8] = [0x01, 0x02, 0x03] - let count = try Capsule(type: CapsuleType(0x1234), value: payload).encodedByteCount - let encoded = try [UInt8](capacity: count) { output in - try Capsule(type: CapsuleType(0x1234), value: payload).encode(into: &output) - } - var rest = encoded.span - if let decoded = Capsule.decode(from: &rest) { - #expect(decoded.type == CapsuleType(0x1234)) - #expect(decoded.value.count == 3) - #expect(decoded.value[0] == 0x01) - #expect(decoded.value[1] == 0x02) - #expect(decoded.value[2] == 0x03) - } else { - Issue.record("expected to decode the round-tripped capsule") - } - #expect(rest.count == 0) + func initialization() { + let capsule = Capsule(type: .datagram, value: [0x01, 0x02]) + #expect(capsule.type == .datagram) + #expect(capsule.value == [0x01, 0x02]) + #expect(capsule.value.count == 2) } } diff --git a/Tests/NetworkTypesTests/CapsuleTypeTests.swift b/Tests/NetworkTypesTests/CapsuleTypeTests.swift new file mode 100644 index 0000000..333254d --- /dev/null +++ b/Tests/NetworkTypesTests/CapsuleTypeTests.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NetworkTypes +import Testing + +@Suite +struct CapsuleTypeTests { + @Test + func datagramTypeIsZero() { + #expect(CapsuleType.datagram.rawValue == 0) + #expect(CapsuleType.datagram == 0) + } + + @Test + func capsuleTypeEqualityIsByCode() { + #expect(CapsuleType(5) == CapsuleType(5)) + #expect(CapsuleType(5) != CapsuleType(6)) + } + + @Test + func hashableConformance() { + let capsuleA = CapsuleType.addressAssign + let capsuleD = CapsuleType.datagram + + // Test that different types have different hash values + // Note: This is not guaranteed by Hashable but is expected in practice + #expect(capsuleA.hashValue != capsuleD.hashValue) + + // Test that same version has same hash value + #expect(capsuleA.hashValue == CapsuleType.addressAssign.hashValue) + #expect(capsuleD.hashValue == CapsuleType.datagram.hashValue) + } +} diff --git a/Tests/NetworkTypesTests/QUICVariableLengthIntegerTests.swift b/Tests/NetworkTypesTests/QUICVariableLengthIntegerTests.swift deleted file mode 100644 index a7cdd16..0000000 --- a/Tests/NetworkTypesTests/QUICVariableLengthIntegerTests.swift +++ /dev/null @@ -1,122 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP API Proposal open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NetworkTypes -import Testing - -@Suite -struct QUICVariableLengthIntegerTests { - /// Encodes `value` into a fresh byte array via the OutputSpan initializer. - @available(anyAppleOS 26.0, *) - static func encoded(_ value: UInt64) throws -> [UInt8] { - try [UInt8](capacity: 8) { output in - try QUICVariableLengthInteger.encode(value, into: &output) - } - } - - /// Encodes `value` into a fresh byte array of `capacity` bytes via the OutputSpan initializer. - @available(anyAppleOS 26.0, *) - static func encoded(_ value: UInt64, intoBufferWithCapacity capacity: Int) throws -> [UInt8] { - try [UInt8](capacity: capacity) { output in - try QUICVariableLengthInteger.encode(value, into: &output) - } - } - - @Test - @available(anyAppleOS 26.0, *) - func encodedByteCount() throws { - // 0 to 6 bits fit into 1 byte. - #expect(try QUICVariableLengthInteger.encodedByteCount(0) == 1) - #expect(try QUICVariableLengthInteger.encodedByteCount(0x3F) == 1) - - // 7 to 14 bits fit into 2 bytes. - #expect(try QUICVariableLengthInteger.encodedByteCount(0x3F + 1) == 2) - #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF) == 2) - - // 15 to 30 bits fit into 4 bytes. - #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF + 1) == 4) - #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF_FFFF) == 4) - - // 31 to 62 bits fit into 8 bytes. - #expect(try QUICVariableLengthInteger.encodedByteCount(0x3FFF_FFFF + 1) == 8) - #expect(try QUICVariableLengthInteger.encodedByteCount(QUICVariableLengthInteger.max) == 8) - - // Larger values cannot be encoded. - #expect(throws: NetworkTypeError.exceedsMaximumValue.self) { - try QUICVariableLengthInteger.encodedByteCount(QUICVariableLengthInteger.max + 1) - } - } - - @Test - @available(anyAppleOS 26.0, *) - func decodesRFC9000Vectors() { - let eightByte: [UInt8] = [0xc2, 0x19, 0x7c, 0x5e, 0xff, 0x14, 0xe8, 0x8c] - #expect(QUICVariableLengthInteger.decode(from: eightByte.span)?.value == 151_288_809_941_952_652) - let fourByte: [UInt8] = [0x9d, 0x7f, 0x3e, 0x7d] - #expect(QUICVariableLengthInteger.decode(from: fourByte.span)?.value == 494_878_333) - let twoByte: [UInt8] = [0x7b, 0xbd] - #expect(QUICVariableLengthInteger.decode(from: twoByte.span)?.value == 15_293) - let oneByte: [UInt8] = [0x25] - #expect(QUICVariableLengthInteger.decode(from: oneByte.span)?.value == 37) - } - - @Test - @available(anyAppleOS 26.0, *) - func decodesNonMinimalEncoding() { - // 37 encoded in two bytes instead of one; legal to receive (RFC 9297 §1.1). - let bytes: [UInt8] = [0x40, 0x25] - let result = QUICVariableLengthInteger.decode(from: bytes.span) - #expect(result?.value == 37) - #expect(result?.byteCount == 2) - } - - @Test - @available(anyAppleOS 26.0, *) - func decodeReturnsNilWhenTruncated() { - // First byte announces a 4-byte integer, only 2 bytes present. - let truncated: [UInt8] = [0x9d, 0x7f] - #expect(QUICVariableLengthInteger.decode(from: truncated.span) == nil) - let empty: [UInt8] = [] - #expect(QUICVariableLengthInteger.decode(from: empty.span) == nil) - } - - @Test - @available(anyAppleOS 26.0, *) - func encodesToMinimalForm() throws { - #expect(try Self.encoded(63) == [0x3f]) - #expect(try Self.encoded(64) == [0x40, 0x40]) - #expect(try Self.encoded(15_293) == [0x7b, 0xbd]) - } - - @Test(arguments: [ - (UInt64(15_293), 1), - (UInt64(494_878_333), 1), - (UInt64(494_878_333), 2), - (UInt64(151_288_809_941_952_652), 1), - (UInt64(151_288_809_941_952_652), 2), - (UInt64(151_288_809_941_952_652), 3), - ]) - @available(anyAppleOS 26.0, *) - func encodesThrowsWhenNotEnoughCapacity(number: UInt64, capacity: Int) throws { - #expect(throws: NetworkTypeError.self) { try Self.encoded(number, intoBufferWithCapacity: capacity) } - } - - @Test(arguments: [UInt64]([0, 63, 64, 16_383, 16_384, 1_073_741_823, 1_073_741_824, 0x3FFF_FFFF_FFFF_FFFF])) - @available(anyAppleOS 26.0, *) - func roundTripsAllBoundaries(value: UInt64) throws { - let bytes = try Self.encoded(value) - let decoded = QUICVariableLengthInteger.decode(from: bytes.span) - #expect(decoded?.value == value) - #expect(decoded?.byteCount == bytes.count) - } -} From 11c0279085e8b0b0afc9d32f0c0a47ceb0b27085 Mon Sep 17 00:00:00 2001 From: Raphael Hiesgen Date: Fri, 17 Jul 2026 15:06:40 +0100 Subject: [PATCH 9/9] Formatting --- Sources/NetworkTypes/CapsuleProtocol/Capsule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift index 79382a5..bbb8b73 100644 --- a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift +++ b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift @@ -17,7 +17,7 @@ /// endpoints must agree to use it, e.g., using Extended CONNECT in H2 and H3. /// /// A capsule is a TLV type. `value` contains opaque bytes, with a meaning defined -/// by `type`. The length is `value.count`. +/// by `type`. The length is `value.count`. public struct Capsule: Sendable { /// The capsule type. public var type: CapsuleType