diff --git a/Sources/SwiftNetwork/Protocols/Frame.swift b/Sources/SwiftNetwork/Protocols/Frame.swift index f4b8500..fd8e045 100644 --- a/Sources/SwiftNetwork/Protocols/Frame.swift +++ b/Sources/SwiftNetwork/Protocols/Frame.swift @@ -52,12 +52,12 @@ public struct Frame: ~Copyable { set { _endOffset = UInt32(newValue) } } private var _effectiveBufferLength: UInt32 = 0 - var effectiveBufferLength: Int { + @usableFromInline var effectiveBufferLength: Int { get { Int(_effectiveBufferLength) } set { _effectiveBufferLength = UInt32(newValue) } } private var _aggregateBufferLength: UInt32 = 0 - var aggregateBufferLength: Int { + @usableFromInline var aggregateBufferLength: Int { get { Int(_aggregateBufferLength) } set { _aggregateBufferLength = UInt32(newValue) } } @@ -261,6 +261,7 @@ public struct Frame: ~Copyable { } } + @inline(__always) public mutating func claim(fromStart: Int, fromEnd: Int = 0, adjustSingleIPAggregate: Bool = true) -> Bool { if adjustSingleIPAggregate && isSingleIPAggregate { guard fromEnd == 0 else { @@ -786,6 +787,7 @@ extension Frame { @available(Network 0.1.0, *) extension Frame { + // Copy length bytes from offset in this Frame into destination Frame. // checking the source offset, length and destination fit. // Return the length that it was able to copy into destination. diff --git a/Sources/SwiftNetwork/QUIC/PacketParser.swift b/Sources/SwiftNetwork/QUIC/PacketParser.swift index be2e0ed..f36a497 100644 --- a/Sources/SwiftNetwork/QUIC/PacketParser.swift +++ b/Sources/SwiftNetwork/QUIC/PacketParser.swift @@ -319,11 +319,11 @@ struct PacketParser: ~Copyable, PrefixedLoggable { private func parseHeader(frame: inout Frame, dcidLength: Int) throws(QUICError) -> Packet { var firstOctet: UInt8 = 0 let originalLength = frame.unclaimedLength - let result = Deserializer.deserialize(&frame, claim: true) { read throws(DeserializationError) in - try read.uint8(&firstOctet) + do throws(DeserializationError) { + firstOctet = try FrameDeserializer.uint8(frame: &frame, claim: true) + } catch { + throw QUICError.packet(QUICPacketError.deserializationError) } - try validateDeserializationResult(result) - // Common short/long header bits let longHeader = (firstOctet & 0x80) != 0 @@ -342,7 +342,6 @@ struct PacketParser: ~Copyable, PrefixedLoggable { originalLength: originalLength ) } - packet.framesReceived.reserveCapacity(1) return packet } @@ -523,16 +522,19 @@ struct PacketParser: ~Copyable, PrefixedLoggable { log.error("Short header fixed bit is zero") throw QUICError.packet(QUICPacketError.deserializationError) } - var dcidStorage = QUICConnectionIDStorage.empty - let result = Deserializer.deserialize(&frame, claim: true) { read throws(DeserializationError) in - try read.connectionID(&dcidStorage, length: dcidLength) + do throws(DeserializationError) { + try FrameDeserializer.connectionID( + frame: &frame, + storage: &dcidStorage, + length: Int(dcidLength), + claim: true + ) + } catch { + throw QUICError.packet(QUICPacketError.deserializationError) } - try validateDeserializationResult(result) - - let destinationConnectionID = QUICConnectionID(storage: dcidStorage, size: Int(dcidLength)) return Packet( - destinationConnectionID: destinationConnectionID, + destinationConnectionID: QUICConnectionID(storage: dcidStorage, size: Int(dcidLength)), headerLength: UInt16(originalLength - frame.unclaimedLength), spin: spinValue ) diff --git a/Sources/SwiftNetwork/QUIC/QUICConnectionID.swift b/Sources/SwiftNetwork/QUIC/QUICConnectionID.swift index cbc9766..c0ead4e 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnectionID.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnectionID.swift @@ -88,8 +88,10 @@ public struct QUICConnectionID: Sendable, Equatable, CustomStringConvertible { // Creates a QUICConnectionID from an array. public init?(_ connectionID: [UInt8]) { guard connectionID.count <= QUICConnectionID.maximumSize else { + #if !DisableErrorLogging let connectionIDCount = connectionID.count - Logger.proto.fault("Invalid QUICConnectionID length \(connectionIDCount)") + Logger.proto.error("Invalid QUICConnectionID length \(connectionIDCount)") + #endif return nil } actualLength = connectionID.count @@ -101,15 +103,19 @@ public struct QUICConnectionID: Sendable, Equatable, CustomStringConvertible { if size <= QUICConnectionID.maximumSize { actualLength = size } else { - Logger.proto.fault("Invalid QUICConnectionID length \(size)") + #if !DisableErrorLogging + Logger.proto.error("Invalid QUICConnectionID length \(size)") + #endif actualLength = QUICConnectionID.maximumSize } } public init?(_ connectionID: Span) { guard connectionID.count <= QUICConnectionID.maximumSize else { + #if !DisableErrorLogging let connectionIDCount = connectionID.count - Logger.proto.fault("Invalid QUICConnectionID length \(connectionIDCount)") + Logger.proto.error("Invalid QUICConnectionID length \(connectionIDCount)") + #endif return nil } actualLength = connectionID.count @@ -120,7 +126,9 @@ public struct QUICConnectionID: Sendable, Equatable, CustomStringConvertible { public init(_ size: Int) { var size = size if size > QUICConnectionID.maximumSize { - Logger.proto.fault("Invalid QUICConnectionID length \(size)") + #if !DisableErrorLogging + Logger.proto.error("Invalid QUICConnectionID length \(size)") + #endif size = QUICConnectionID.maximumSize } if size != 0 && size < 4 { @@ -133,7 +141,9 @@ public struct QUICConnectionID: Sendable, Equatable, CustomStringConvertible { // Creates a QUICConnectionID from a buffer with a specific size. init?(_ buffer: [UInt8], size: Int) { guard size <= QUICConnectionID.maximumSize, buffer.count >= size else { - Logger.proto.fault("Invalid QUICConnectionID length \(size)") + #if !DisableErrorLogging + Logger.proto.error("Invalid QUICConnectionID length \(size)") + #endif return nil } let cidBytes = Array(buffer[0.. UInt8 { + guard frame._bytes.count > 0 else { + throw DeserializationError.bufferTooShort + } + let value: UInt8 = frame._bytes[frame.startOffset] + if claim { + guard frame.claim(fromStart: 1) else { + throw DeserializationError.bufferTooShort + } + } + return value + } + + @inline(__always) + static func uint16(frame: inout Frame, claim: Bool = false) throws(DeserializationError) -> UInt16 { + guard frame.startOffset + 2 <= frame._bytes.count else { + throw DeserializationError.bufferTooShort + } + let value = frame._bytes.span.bytes.unsafeLoadUnaligned( + fromByteOffset: frame.startOffset, + as: UInt16.self + ) + if claim { + guard frame.claim(fromStart: 2) else { + throw DeserializationError.bufferTooShort + } + } + return value + } + + @inline(__always) + static func uint16NetworkByteOrder( + frame: inout Frame, + claim: Bool = false + ) throws(DeserializationError) -> UInt16 { + UInt16(bigEndian: try uint16(frame: &frame, claim: claim)) + } + + @inline(__always) + static func uint32(frame: inout Frame, claim: Bool = false) throws(DeserializationError) -> UInt32 { + guard frame.startOffset + 4 <= frame._bytes.count else { + throw DeserializationError.bufferTooShort + } + let value = frame._bytes.span.bytes.unsafeLoadUnaligned( + fromByteOffset: frame.startOffset, + as: UInt32.self + ) + if claim { + guard frame.claim(fromStart: 4) else { + throw DeserializationError.bufferTooShort + } + } + return value + } + + @inline(__always) + static func uint32NetworkByteOrder( + frame: inout Frame, + claim: Bool = false + ) throws(DeserializationError) -> UInt32 { + UInt32(bigEndian: try uint32(frame: &frame, claim: claim)) + } + + @inline(__always) + static func uint64(frame: inout Frame, claim: Bool = false) throws(DeserializationError) -> UInt64 { + guard frame.startOffset + 8 <= frame._bytes.count else { + throw DeserializationError.bufferTooShort + } + let value = frame._bytes.span.bytes.unsafeLoadUnaligned( + fromByteOffset: frame.startOffset, + as: UInt64.self + ) + if claim { + guard frame.claim(fromStart: 8) else { + throw DeserializationError.bufferTooShort + } + } + return value + } + + @inline(__always) + static func uint64NetworkByteOrder( + frame: inout Frame, + claim: Bool = false + ) throws(DeserializationError) -> UInt64 { + UInt64(bigEndian: try uint64(frame: &frame, claim: claim)) + } + + @inline(__always) + static func connectionID( + frame: inout Frame, + storage: inout [20 of UInt8], + length: Int, + claim: Bool = false + ) throws(DeserializationError) { + guard frame.startOffset + length <= frame._bytes.count else { + return + } + for i in 0.. Bool { + frame.claim(fromStart: length) + } +} + @_spi(ProtocolProvider) @available(Network 0.1.0, *) public struct Deserializer: ~Copyable, ~Escapable { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkFrameDeserializerTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkFrameDeserializerTests.swift new file mode 100644 index 0000000..07b7d1a --- /dev/null +++ b/Tests/SwiftNetworkTests/SwiftNetworkFrameDeserializerTests.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import XCTest + +#if canImport(SwiftNetwork) +@_spi(Essentials) @_spi(ProtocolProvider) @testable import SwiftNetwork +#elseif canImport(Network) +@_spi(Essentials) @_spi(ProtocolProvider) @testable import Network +#endif + +@available(Network 0.1.0, *) +final class SwiftNetworkFrameDeserializerTests: NetTestCase { + + func testUInt8InlineValue() throws { + var frame = Frame(copyBuffer: [0xAB] as [UInt8]) + defer { frame.finalize(success: false) } + do throws(DeserializationError) { + let value = try FrameDeserializer.uint8(frame: &frame, claim: true) + XCTAssertEqual(value, 0xAB) + } catch { + XCTFail("Unexpected deserialization error: \(error)") + } + } + + func testUInt8PeekDoesNotAdvanceOffset() throws { + var frame = Frame(copyBuffer: [0xCD, 0xEF] as [UInt8]) + defer { frame.finalize(success: false) } + do throws(DeserializationError) { + let firstUnclaimed = try FrameDeserializer.uint8(frame: &frame, claim: false) + let nextClaimed = try FrameDeserializer.uint8(frame: &frame, claim: true) + XCTAssertEqual(firstUnclaimed, 0xCD) + XCTAssertEqual(nextClaimed, 0xCD) + XCTAssertEqual(frame.unclaimedLength, 1) + } catch { + XCTFail("Unexpected deserialization error: \(error)") + } + } + + func testUInt64InlineValue() throws { + let bytes: [UInt8] = [0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41] + var frame = Frame(copyBuffer: bytes) + defer { frame.finalize(success: false) } + do throws(DeserializationError) { + let value = try FrameDeserializer.uint64(frame: &frame, claim: true) + XCTAssertEqual(value, 0x4141_4141_4141_4141) + XCTAssertEqual(frame.unclaimedLength, 0) + } catch { + XCTFail("Unexpected deserialization error: \(error)") + } + } + + func testUInt64NetworkByteOrderInlineValue() throws { + let bytes: [UInt8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + var frame = Frame(copyBuffer: bytes) + defer { frame.finalize(success: false) } + do throws(DeserializationError) { + let value = try FrameDeserializer.uint64NetworkByteOrder(frame: &frame, claim: true) + XCTAssertEqual(value, 0x0102_0304_0506_0708) + XCTAssertEqual(frame.unclaimedLength, 0) + } catch { + XCTFail("Unexpected deserialization error: \(error)") + } + } + + func testUInt64NetworkByteOrderThenUInt8Sequential() throws { + let bytes: [UInt8] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x42] + var frame = Frame(copyBuffer: bytes) + defer { frame.finalize(success: false) } + do throws(DeserializationError) { + let high = try FrameDeserializer.uint64NetworkByteOrder(frame: &frame, claim: true) + let low = try FrameDeserializer.uint8(frame: &frame, claim: true) + XCTAssertEqual(high, 0x0000_0000_0000_00FF) + XCTAssertEqual(low, 0x42) + XCTAssertEqual(frame.unclaimedLength, 0) + } catch { + XCTFail("Unexpected deserialization error: \(error)") + } + } + +}