diff --git a/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift new file mode 100644 index 0000000..bbb8b73 --- /dev/null +++ b/Sources/NetworkTypes/CapsuleProtocol/Capsule.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 single capsule containing a type code and its opaque value, per RFC 9297, Section 3.2. +/// +/// 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 value. + public init(type: CapsuleType, value: [UInt8]) { + self.type = type + self.value = value + } +} diff --git a/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift new file mode 100644 index 0000000..57152f6 --- /dev/null +++ b/Sources/NetworkTypes/CapsuleProtocol/CapsuleType.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// 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 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 <= Self.maxRawValue, "capsule type \(rawValue) exceeds the allowed maximum value") + } + } + + /// Creates a capsule type from its numeric rawValue. + /// + /// - Precondition: `rawValue` is less than or equal to ``VariableLengthInteger/max``. + public init(_ rawValue: UInt64) { + 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 } + + /// 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 } +} diff --git a/Tests/NetworkTypesTests/CapsuleTests.swift b/Tests/NetworkTypesTests/CapsuleTests.swift new file mode 100644 index 0000000..6e84e39 --- /dev/null +++ b/Tests/NetworkTypesTests/CapsuleTests.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 CapsuleTests { + @Test + 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) + } +}