mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
* Run local packages’ tests as well on CI * BitFoundation module to centralize shared components
809 lines
33 KiB
Swift
809 lines
33 KiB
Swift
//
|
|
// BinaryProtocolTests.swift
|
|
// bitchatTests
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import BitFoundation
|
|
@testable import bitchat
|
|
|
|
struct BinaryProtocolTests {
|
|
|
|
// MARK: - Basic Encoding/Decoding Tests
|
|
|
|
@Test func basicPacketEncodingDecoding() throws {
|
|
let originalPacket = TestHelpers.createTestPacket()
|
|
|
|
let encodedData = try #require(BinaryProtocol.encode(originalPacket), "Failed to encode packet")
|
|
let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode packet")
|
|
|
|
// Verify
|
|
#expect(decodedPacket.type == originalPacket.type)
|
|
#expect(decodedPacket.ttl == originalPacket.ttl)
|
|
#expect(decodedPacket.timestamp == originalPacket.timestamp)
|
|
#expect(decodedPacket.payload == originalPacket.payload)
|
|
|
|
// Sender ID should match (accounting for padding)
|
|
let originalSenderID = originalPacket.senderID.prefix(BinaryProtocol.senderIDSize)
|
|
let decodedSenderID = decodedPacket.senderID.trimmingNullBytes()
|
|
#expect(decodedSenderID == originalSenderID)
|
|
}
|
|
|
|
@Test func trimmingNullBytesReturnsOriginalDataWhenNoNullsPresent() {
|
|
let raw = Data([0x41, 0x42, 0x43])
|
|
#expect(raw.trimmingNullBytes() == raw)
|
|
}
|
|
|
|
@Test func packetWithRecipient() throws {
|
|
let recipientID = PeerID(str: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
|
|
let packet = TestHelpers.createTestPacket(recipientID: recipientID)
|
|
let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with recipient")
|
|
let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode packet with recipient")
|
|
|
|
// Verify recipient
|
|
#expect(decodedPacket.recipientID != nil)
|
|
let decodedRecipientID = decodedPacket.recipientID?.trimmingNullBytes()
|
|
// TODO: Check if this is intended that the decoding only gets the first 8
|
|
#expect(String(data: decodedRecipientID!, encoding: .utf8) == "abcdef01")
|
|
}
|
|
|
|
@Test func packetWithSignature() throws {
|
|
let packet = TestHelpers.createTestPacket(signature: TestConstants.testSignature)
|
|
let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with signature")
|
|
let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode packet with signature")
|
|
|
|
// Verify signature
|
|
#expect(decodedPacket.signature != nil)
|
|
#expect(decodedPacket.signature == TestConstants.testSignature)
|
|
}
|
|
|
|
// MARK: - Source-Based Routing Tests (v2 only)
|
|
|
|
@Test func packetWithRouteRoundTrip() throws {
|
|
let route: [Data] = [
|
|
try #require(Data(hexString: "0102030405060708")),
|
|
try #require(Data(hexString: "1112131415161718")),
|
|
try #require(Data(hexString: "2122232425262728"))
|
|
]
|
|
|
|
// Route is only supported for v2+ packets
|
|
var packet = BitchatPacket(
|
|
type: 0x01,
|
|
senderID: route[0],
|
|
recipientID: route.last,
|
|
timestamp: 1_720_000_000_000,
|
|
payload: Data("route-test".utf8),
|
|
signature: nil,
|
|
ttl: 6,
|
|
version: 2
|
|
)
|
|
packet.route = route
|
|
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with route")
|
|
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
|
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
|
|
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
|
#expect(decoded.version == 2)
|
|
let decodedRoute = try #require(decoded.route)
|
|
#expect(decodedRoute.count == route.count)
|
|
for (expected, actual) in zip(route, decodedRoute) {
|
|
#expect(actual == expected)
|
|
}
|
|
}
|
|
|
|
@Test func packetWithRoutePadsShortHop() throws {
|
|
let sender = try #require(Data(hexString: "0011223344556677"))
|
|
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
|
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
|
|
|
// Route is only supported for v2+ packets
|
|
var packet = BitchatPacket(
|
|
type: 0x02,
|
|
senderID: sender,
|
|
recipientID: destination,
|
|
timestamp: 1_730_000_000_000,
|
|
payload: Data("pad-test".utf8),
|
|
signature: nil,
|
|
ttl: 5,
|
|
version: 2
|
|
)
|
|
packet.route = [shortHop, destination]
|
|
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with short hop route")
|
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with short hop route")
|
|
let decodedRoute = try #require(decoded.route)
|
|
let firstHop = try #require(decodedRoute.first)
|
|
#expect(firstHop.count == BinaryProtocol.senderIDSize)
|
|
#expect(firstHop.prefix(shortHop.count) == shortHop)
|
|
let paddingBytes = firstHop.suffix(firstHop.count - shortHop.count)
|
|
#expect(paddingBytes.allSatisfy { $0 == 0 })
|
|
}
|
|
|
|
@Test func packetWithRouteAndCompressedPayload() throws {
|
|
let route: [Data] = [
|
|
try #require(Data(hexString: "0101010101010101")),
|
|
try #require(Data(hexString: "0202020202020202"))
|
|
]
|
|
let repeatedString = String(repeating: "compress-me", count: 150)
|
|
// Route is only supported for v2+ packets
|
|
var packet = BitchatPacket(
|
|
type: 0x03,
|
|
senderID: route[0],
|
|
recipientID: route.last,
|
|
timestamp: 1_740_000_000_000,
|
|
payload: Data(repeatedString.utf8),
|
|
signature: nil,
|
|
ttl: 7,
|
|
version: 2
|
|
)
|
|
packet.route = route
|
|
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with route and compression")
|
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route and compression")
|
|
#expect(decoded.payload == Data(repeatedString.utf8))
|
|
let decodedRoute = try #require(decoded.route)
|
|
#expect(decodedRoute == route)
|
|
}
|
|
|
|
@Test func v1PacketIgnoresRouteOnEncode() throws {
|
|
// v1 packets should NOT include route even if route is set on the packet object
|
|
let route: [Data] = [
|
|
try #require(Data(hexString: "0102030405060708")),
|
|
try #require(Data(hexString: "1112131415161718"))
|
|
]
|
|
|
|
var packet = BitchatPacket(
|
|
type: 0x01,
|
|
senderID: route[0],
|
|
recipientID: route.last,
|
|
timestamp: 1_720_000_000_000,
|
|
payload: Data("v1-no-route".utf8),
|
|
signature: nil,
|
|
ttl: 6
|
|
// version defaults to 1 (v1 packet)
|
|
)
|
|
packet.route = route // route is set but should be ignored for v1
|
|
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v1 packet")
|
|
|
|
// HAS_ROUTE flag should NOT be set for v1 packets
|
|
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
|
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) == 0, "v1 packet should not have HAS_ROUTE flag set")
|
|
|
|
// Decoded packet should have no route
|
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v1 packet")
|
|
#expect(decoded.version == 1)
|
|
#expect(decoded.route == nil, "v1 packet should decode with nil route")
|
|
#expect(decoded.payload == Data("v1-no-route".utf8))
|
|
}
|
|
|
|
@Test func v2PacketIncludesRouteOnEncode() throws {
|
|
// v2 packets SHOULD include route when route is set
|
|
let route: [Data] = [
|
|
try #require(Data(hexString: "0102030405060708")),
|
|
try #require(Data(hexString: "1112131415161718"))
|
|
]
|
|
|
|
var packet = BitchatPacket(
|
|
type: 0x01,
|
|
senderID: route[0],
|
|
recipientID: route.last,
|
|
timestamp: 1_720_000_000_000,
|
|
payload: Data("v2-with-route".utf8),
|
|
signature: nil,
|
|
ttl: 6,
|
|
version: 2
|
|
)
|
|
packet.route = route
|
|
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v2 packet")
|
|
|
|
// HAS_ROUTE flag SHOULD be set for v2 packets with route
|
|
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
|
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0, "v2 packet should have HAS_ROUTE flag set")
|
|
|
|
// Decoded packet should have route
|
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v2 packet")
|
|
#expect(decoded.version == 2)
|
|
let decodedRoute = try #require(decoded.route, "v2 packet should decode with route")
|
|
#expect(decodedRoute.count == route.count)
|
|
#expect(decoded.payload == Data("v2-with-route".utf8))
|
|
}
|
|
|
|
@Test func v2PacketWithoutRouteDecodesCorrectly() throws {
|
|
// v2 packet without route should still work
|
|
let sender = try #require(Data(hexString: "0011223344556677"))
|
|
let recipient = try #require(Data(hexString: "8899aabbccddeeff"))
|
|
|
|
let packet = BitchatPacket(
|
|
type: 0x02,
|
|
senderID: sender,
|
|
recipientID: recipient,
|
|
timestamp: 1_750_000_000_000,
|
|
payload: Data("v2-no-route".utf8),
|
|
signature: nil,
|
|
ttl: 5,
|
|
version: 2
|
|
)
|
|
// route is nil by default
|
|
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v2 packet without route")
|
|
|
|
// HAS_ROUTE flag should NOT be set when no route
|
|
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
|
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) == 0, "v2 packet without route should not have HAS_ROUTE flag")
|
|
|
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v2 packet without route")
|
|
#expect(decoded.version == 2)
|
|
#expect(decoded.route == nil)
|
|
#expect(decoded.payload == Data("v2-no-route".utf8))
|
|
}
|
|
|
|
@Test func v1AndV2PayloadLengthDifference() throws {
|
|
// Verify that payloadLength does NOT include route bytes
|
|
// by comparing encoded sizes
|
|
let route: [Data] = [
|
|
try #require(Data(hexString: "0102030405060708"))
|
|
]
|
|
let payloadData = Data("test-payload".utf8)
|
|
|
|
// v1 packet (route ignored)
|
|
var v1Packet = BitchatPacket(
|
|
type: 0x01,
|
|
senderID: route[0],
|
|
recipientID: nil,
|
|
timestamp: 1_720_000_000_000,
|
|
payload: payloadData,
|
|
signature: nil,
|
|
ttl: 6
|
|
// version defaults to 1
|
|
)
|
|
v1Packet.route = route // will be ignored for v1
|
|
|
|
// v2 packet with same payload but route included
|
|
var v2Packet = BitchatPacket(
|
|
type: 0x01,
|
|
senderID: route[0],
|
|
recipientID: nil,
|
|
timestamp: 1_720_000_000_000,
|
|
payload: payloadData,
|
|
signature: nil,
|
|
ttl: 6,
|
|
version: 2
|
|
)
|
|
v2Packet.route = route
|
|
|
|
let v1Encoded = try #require(BinaryProtocol.encode(v1Packet, padding: false))
|
|
let v2Encoded = try #require(BinaryProtocol.encode(v2Packet, padding: false))
|
|
|
|
// v2 should be larger by: 2 bytes (header length field difference) + 1 byte (route count) + 8 bytes (one hop)
|
|
// Header: v1=14, v2=16 -> +2 bytes
|
|
// Route: 1 + 8 = 9 bytes
|
|
// Total expected difference: 11 bytes
|
|
let expectedDiff = 2 + 1 + 8 // header diff + route count + one hop
|
|
#expect(v2Encoded.count - v1Encoded.count == expectedDiff,
|
|
"v2 packet should be \(expectedDiff) bytes larger than v1 (actual diff: \(v2Encoded.count - v1Encoded.count))")
|
|
}
|
|
|
|
// MARK: - Compression Tests
|
|
|
|
@Test("Create a large, compressible payload above current threshold (2048B)")
|
|
func payloadCompression() throws {
|
|
let repeatedString = String(repeating: "This is a test message. ", count: 200)
|
|
let largePayload = repeatedString.data(using: .utf8)!
|
|
|
|
let packet = TestHelpers.createTestPacket(payload: largePayload)
|
|
|
|
// Encode (should compress)
|
|
let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with large payload")
|
|
|
|
// The encoded size should be smaller than uncompressed due to compression
|
|
let headerSize = try #require(BinaryProtocol.headerSize(for: packet.version), "Invalid packet version")
|
|
let uncompressedSize = headerSize + BinaryProtocol.senderIDSize + largePayload.count
|
|
#expect(encodedData.count < uncompressedSize, "Compressed packet should be smaller than uncompressed form")
|
|
|
|
// Decode and verify
|
|
let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode compressed packet")
|
|
|
|
#expect(decodedPacket.payload == largePayload)
|
|
}
|
|
|
|
@Test("Small payloads should not be compressed")
|
|
func smallPayloadNoCompression() throws {
|
|
let smallPayload = "Hi".data(using: .utf8)!
|
|
let packet = TestHelpers.createTestPacket(payload: smallPayload)
|
|
let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode small packet")
|
|
let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode small packet")
|
|
#expect(decodedPacket.payload == smallPayload)
|
|
}
|
|
|
|
@Test("Reject payloads larger than the framed file cap")
|
|
func oversizedPayloadIsRejected() throws {
|
|
let targetSize = FileTransferLimits.maxFramedFileBytes + 1
|
|
var oversized = Data()
|
|
oversized.reserveCapacity(targetSize)
|
|
let byteRun = Data((0...255).map { UInt8($0) })
|
|
while oversized.count < targetSize {
|
|
let remaining = targetSize - oversized.count
|
|
if remaining >= byteRun.count {
|
|
oversized.append(byteRun)
|
|
} else {
|
|
oversized.append(byteRun.prefix(remaining))
|
|
}
|
|
}
|
|
let packet = BitchatPacket(
|
|
type: MessageType.message.rawValue,
|
|
senderID: Data(hexString: "0011223344556677") ?? Data(),
|
|
recipientID: nil,
|
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
|
payload: oversized,
|
|
signature: nil,
|
|
ttl: 1,
|
|
version: 2
|
|
)
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode oversized packet")
|
|
#expect(BinaryProtocol.decode(encoded) == nil)
|
|
}
|
|
|
|
// MARK: - Message Padding Tests
|
|
|
|
@Test func messagePadding() throws {
|
|
let payloads = [
|
|
"Short",
|
|
String(repeating: "Medium length message content ", count: 10), // ~300 bytes
|
|
String(repeating: "Long message content that should exceed the 512 byte limit ", count: 20), // ~1200+ bytes
|
|
String(repeating: "Very long message content that should definitely exceed the 2048 byte limit for sure ", count: 30) // ~2700+ bytes
|
|
]
|
|
|
|
var encodedSizes = Set<Int>()
|
|
|
|
for payload in payloads {
|
|
let packet = TestHelpers.createTestPacket(payload: payload.data(using: .utf8)!)
|
|
let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet")
|
|
|
|
// Verify padding creates standard block sizes up to configured limit (no 4096 bucket currently)
|
|
let blockSizes = [256, 512, 1024, 2048]
|
|
if encodedData.count <= 2048 {
|
|
#expect(blockSizes.contains(encodedData.count), "Encoded size \(encodedData.count) is not a standard block size")
|
|
} else {
|
|
// For very large payloads we expect no additional padding beyond raw size
|
|
#expect(encodedData.count > 2048)
|
|
}
|
|
|
|
encodedSizes.insert(encodedData.count)
|
|
|
|
// Verify decoding works
|
|
let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode padded packet")
|
|
#expect(String(data: decodedPacket.payload, encoding: .utf8) == payload)
|
|
}
|
|
|
|
// Different payload sizes (within <=2048) may map to the same bucket depending on compression.
|
|
// Require at least one padded size to be present.
|
|
#expect(encodedSizes.filter { $0 <= 2048 }.count >= 1, "Expected at least one padded size up to 2048, got \(encodedSizes)")
|
|
}
|
|
|
|
@Test func invalidPKCS7PaddingIsRejected() throws {
|
|
let pkt = TestHelpers.createTestPacket(payload: Data(repeating: 0x41, count: 50)) // small
|
|
let enc0 = try #require(BinaryProtocol.encode(pkt), "encode failed")
|
|
// Force padding to known block for test stability
|
|
var enc = MessagePadding.pad(enc0, toSize: 256)
|
|
let unpadded = MessagePadding.unpad(enc)
|
|
let padLen = enc.count - unpadded.count
|
|
if padLen > 0 {
|
|
// Set last pad byte to wrong value (padLen-1) to break PKCS#7
|
|
enc[enc.count - 1] = UInt8((padLen - 1) & 0xFF)
|
|
let maybe = BinaryProtocol.decode(enc)
|
|
// If decode still succeeds (nested pad edge case), at least ensure payload integrity
|
|
if let pkt2 = maybe {
|
|
#expect(pkt2.payload == pkt.payload)
|
|
} else {
|
|
#expect(maybe == nil)
|
|
}
|
|
} else {
|
|
// If no padding was applied, just assert decode succeeds (nothing to test)
|
|
#expect(BinaryProtocol.decode(enc) != nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Message Encoding/Decoding Tests
|
|
|
|
@Test func messageEncodingDecoding() throws {
|
|
let message = TestHelpers.createTestMessage()
|
|
|
|
let payload = try #require(message.toBinaryPayload(), "Failed to encode message to binary")
|
|
|
|
let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode message from binary")
|
|
|
|
#expect(decodedMessage.content == message.content)
|
|
#expect(decodedMessage.sender == message.sender)
|
|
#expect(decodedMessage.senderPeerID == message.senderPeerID)
|
|
#expect(decodedMessage.isPrivate == message.isPrivate)
|
|
|
|
// Timestamp should be close (within 1 second due to conversion)
|
|
let timeDiff = abs(decodedMessage.timestamp.timeIntervalSince(message.timestamp))
|
|
#expect(timeDiff < 1)
|
|
}
|
|
|
|
func testPrivateMessageEncoding() throws {
|
|
let message = TestHelpers.createTestMessage(
|
|
isPrivate: true,
|
|
recipientNickname: TestConstants.testNickname2
|
|
)
|
|
|
|
let payload = try #require(message.toBinaryPayload(), "Failed to encode private message")
|
|
let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode private message")
|
|
|
|
#expect(decodedMessage.isPrivate)
|
|
#expect(decodedMessage.recipientNickname == TestConstants.testNickname2)
|
|
}
|
|
|
|
@Test func messageWithMentions() throws {
|
|
let mentions = [TestConstants.testNickname2, TestConstants.testNickname3]
|
|
let message = TestHelpers.createTestMessage(mentions: mentions)
|
|
let payload = try #require(message.toBinaryPayload(), "Failed to encode message with mentions")
|
|
let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode message with mentions")
|
|
#expect(decodedMessage.mentions == mentions)
|
|
}
|
|
|
|
@Test func relayMessageEncoding() throws {
|
|
let message = BitchatMessage(
|
|
id: UUID().uuidString,
|
|
sender: TestConstants.testNickname1,
|
|
content: TestConstants.testMessage1,
|
|
timestamp: Date(),
|
|
isRelay: true,
|
|
originalSender: TestConstants.testNickname3,
|
|
isPrivate: false,
|
|
recipientNickname: nil,
|
|
mentions: nil
|
|
)
|
|
let payload = try #require(message.toBinaryPayload(), "Failed to encode relay message")
|
|
let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode relay message")
|
|
#expect(decodedMessage.isRelay)
|
|
#expect(decodedMessage.originalSender == TestConstants.testNickname3)
|
|
}
|
|
|
|
// MARK: - Edge Cases and Error Handling
|
|
|
|
@Test("Too small data")
|
|
func invalidDataDecoding() throws {
|
|
let tooSmall = Data(repeating: 0, count: 5)
|
|
#expect(BinaryProtocol.decode(tooSmall) == nil)
|
|
|
|
// Random data
|
|
let random = TestHelpers.generateRandomData(length: 100)
|
|
#expect(BinaryProtocol.decode(random) == nil)
|
|
|
|
// Corrupted header
|
|
let packet = TestHelpers.createTestPacket()
|
|
var encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode test packet")
|
|
|
|
// Corrupt the version byte
|
|
encoded[0] = 0xFF
|
|
#expect(BinaryProtocol.decode(encoded) == nil)
|
|
}
|
|
|
|
@Test("Test maximum size handling")
|
|
func largeMessageHandling() throws {
|
|
let largeContent = String(repeating: "X", count: 65535) // Max uint16
|
|
let message = TestHelpers.createTestMessage(content: largeContent)
|
|
let payload = try #require(message.toBinaryPayload(), "Failed to handle large message")
|
|
let decodedMessage = try #require(BitchatMessage(payload), "Failed to handle large message")
|
|
#expect(decodedMessage.content == largeContent)
|
|
}
|
|
|
|
@Test("Test message with empty content")
|
|
func emptyFieldsHandling() throws {
|
|
let emptyMessage = TestHelpers.createTestMessage(content: "")
|
|
let payload = try #require(emptyMessage.toBinaryPayload(), "Failed to handle empty message")
|
|
let decodedMessage = try #require(BitchatMessage(payload), "Failed to handle empty message")
|
|
#expect(decodedMessage.content.isEmpty)
|
|
}
|
|
|
|
// MARK: - Protocol Version Tests
|
|
|
|
@Test("Test with supported version (version is always 1 in init)")
|
|
func protocolVersionHandling() throws {
|
|
let packet = TestHelpers.createTestPacket()
|
|
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with version")
|
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with version")
|
|
#expect(decoded.version == 1)
|
|
}
|
|
|
|
@Test("Create packet data with unsupported version")
|
|
func unsupportedProtocolVersion() throws {
|
|
let packet = TestHelpers.createTestPacket()
|
|
var encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet")
|
|
|
|
// Manually change version byte to unsupported value
|
|
encoded[0] = 99 // Unsupported version
|
|
|
|
// Should fail to decode
|
|
#expect(BinaryProtocol.decode(encoded) == nil)
|
|
}
|
|
|
|
// MARK: - Bounds Checking Tests (Crash Prevention)
|
|
|
|
@Test("Test the specific crash scenario: payloadLength = 193 (0xc1) but only 30 bytes available")
|
|
func malformedPacketWithInvalidPayloadLength() throws {
|
|
var malformedData = Data()
|
|
|
|
// Valid header (13 bytes)
|
|
malformedData.append(1) // version
|
|
malformedData.append(1) // type
|
|
malformedData.append(10) // ttl
|
|
|
|
// Timestamp (8 bytes)
|
|
for _ in 0..<8 {
|
|
malformedData.append(0)
|
|
}
|
|
|
|
malformedData.append(0) // flags (no recipient, no signature, not compressed)
|
|
|
|
// Invalid payload length: 193 (0x00c1) but we'll only provide 8 bytes total data
|
|
malformedData.append(0x00) // high byte
|
|
malformedData.append(0xc1) // low byte (193)
|
|
|
|
// SenderID (8 bytes) - this brings us to 21 bytes total
|
|
for _ in 0..<8 {
|
|
malformedData.append(0x01)
|
|
}
|
|
|
|
// Only provide 8 more bytes instead of the claimed 193
|
|
for _ in 0..<8 {
|
|
malformedData.append(0x02)
|
|
}
|
|
|
|
// Total data is now 30 bytes, but payloadLength claims 193
|
|
#expect(malformedData.count == 30)
|
|
|
|
// This should not crash - should return nil gracefully
|
|
let result = BinaryProtocol.decode(malformedData)
|
|
#expect(result == nil, "Malformed packet with invalid payload length should return nil, not crash")
|
|
}
|
|
|
|
@Test("Test various truncation scenarios")
|
|
func truncatedPacketHandling() throws {
|
|
let packet = TestHelpers.createTestPacket()
|
|
let validEncoded = try #require(BinaryProtocol.encode(packet), "Failed to encode test packet")
|
|
|
|
// Test truncation at various points
|
|
let truncationPoints = [0, 5, 10, 15, 20, 25]
|
|
|
|
for point in truncationPoints {
|
|
let truncated = validEncoded.prefix(point)
|
|
let result = BinaryProtocol.decode(truncated)
|
|
#expect(result == nil, "Truncated packet at \(point) bytes should return nil, not crash")
|
|
}
|
|
}
|
|
|
|
@Test("Test compressed packet with invalid original size")
|
|
func malformedCompressedPacket() throws {
|
|
var malformedData = Data()
|
|
|
|
// Valid header
|
|
malformedData.append(1) // version
|
|
malformedData.append(1) // type
|
|
malformedData.append(10) // ttl
|
|
|
|
// Timestamp (8 bytes)
|
|
for _ in 0..<8 {
|
|
malformedData.append(0)
|
|
}
|
|
|
|
malformedData.append(0x04) // flags: isCompressed = true
|
|
|
|
// Small payload length that's insufficient for compression
|
|
malformedData.append(0x00) // high byte
|
|
malformedData.append(0x01) // low byte (1 byte - insufficient for 2-byte original size)
|
|
|
|
// SenderID (8 bytes)
|
|
for _ in 0..<8 {
|
|
malformedData.append(0x01)
|
|
}
|
|
|
|
// Only 1 byte of "compressed" data (should need at least 2 for original size)
|
|
malformedData.append(0x99)
|
|
|
|
// Should handle this gracefully
|
|
let result = BinaryProtocol.decode(malformedData)
|
|
#expect(result == nil, "Malformed compressed packet should return nil, not crash")
|
|
}
|
|
|
|
@Test("Test packet claiming extremely large payload")
|
|
func excessivelyLargePayloadLength() throws {
|
|
var malformedData = Data()
|
|
|
|
// Valid header
|
|
malformedData.append(1) // version
|
|
malformedData.append(1) // type
|
|
malformedData.append(10) // ttl
|
|
|
|
// Timestamp (8 bytes)
|
|
for _ in 0..<8 {
|
|
malformedData.append(0)
|
|
}
|
|
|
|
malformedData.append(0) // flags
|
|
|
|
// Maximum payload length (65535)
|
|
malformedData.append(0xFF) // high byte
|
|
malformedData.append(0xFF) // low byte
|
|
|
|
// SenderID (8 bytes)
|
|
for _ in 0..<8 {
|
|
malformedData.append(0x01)
|
|
}
|
|
|
|
// Provide only a tiny amount of actual data
|
|
malformedData.append(contentsOf: [0x01, 0x02, 0x03])
|
|
|
|
// Should handle this gracefully without trying to allocate massive amounts of memory
|
|
let result = BinaryProtocol.decode(malformedData)
|
|
#expect(result == nil, "Packet with excessive payload length should return nil, not crash")
|
|
}
|
|
|
|
@Test("Test compressed packet with unreasonable original size")
|
|
func compressedPacketWithInvalidOriginalSize() throws {
|
|
var malformedData = Data()
|
|
|
|
// Valid header
|
|
malformedData.append(1) // version
|
|
malformedData.append(1) // type
|
|
malformedData.append(10) // ttl
|
|
|
|
// Timestamp (8 bytes)
|
|
for _ in 0..<8 {
|
|
malformedData.append(0)
|
|
}
|
|
|
|
malformedData.append(0x04) // flags: isCompressed = true
|
|
|
|
// Reasonable payload length
|
|
malformedData.append(0x00) // high byte
|
|
malformedData.append(0x10) // low byte (16 bytes)
|
|
|
|
// SenderID (8 bytes)
|
|
for _ in 0..<8 {
|
|
malformedData.append(0x01)
|
|
}
|
|
|
|
// Original size claiming to be extremely large (2MB)
|
|
malformedData.append(0x20) // high byte of original size
|
|
malformedData.append(0x00) // low byte of original size (0x2000 = 8192, but let's make it larger with more bytes)
|
|
|
|
// Add more bytes to make it claim larger size - but this will be invalid
|
|
// because our validation should catch unreasonable sizes
|
|
malformedData.append(contentsOf: [0x01, 0x02, 0x03, 0x04]) // Some compressed data
|
|
|
|
// Pad to match payload length
|
|
while malformedData.count < 21 + 16 { // header + senderID + payload
|
|
malformedData.append(0x00)
|
|
}
|
|
|
|
let result = BinaryProtocol.decode(malformedData)
|
|
#expect(result == nil, "Compressed packet with invalid original size should return nil, not crash")
|
|
}
|
|
|
|
@Test("Test compressed packet with suspicious compression ratio")
|
|
func compressedPacketWithSuspiciousCompressionRatio() {
|
|
var malformedData = Data()
|
|
|
|
malformedData.append(1) // version
|
|
malformedData.append(1) // type
|
|
malformedData.append(10) // ttl
|
|
|
|
for _ in 0..<8 {
|
|
malformedData.append(0)
|
|
}
|
|
|
|
malformedData.append(0x04) // isCompressed
|
|
malformedData.append(0x00)
|
|
malformedData.append(0x03) // payloadLength = 3 (2 original-size bytes + 1 compressed byte)
|
|
|
|
for _ in 0..<8 {
|
|
malformedData.append(0x01)
|
|
}
|
|
|
|
malformedData.append(0xFF)
|
|
malformedData.append(0xFF) // originalSize = 65535
|
|
malformedData.append(0x99) // compressed payload length = 1 => ratio > 50_000
|
|
|
|
#expect(BinaryProtocol.decode(malformedData) == nil)
|
|
}
|
|
|
|
@Test("Test packet designed to cause integer overflow")
|
|
func maliciousPacketWithIntegerOverflow() throws {
|
|
var maliciousData = Data()
|
|
|
|
// Valid header
|
|
maliciousData.append(1) // version
|
|
maliciousData.append(1) // type
|
|
maliciousData.append(10) // ttl
|
|
|
|
// Timestamp (8 bytes)
|
|
for _ in 0..<8 {
|
|
maliciousData.append(0)
|
|
}
|
|
|
|
// Set flags to have recipient and signature (increase expected size)
|
|
maliciousData.append(0x03) // hasRecipient | hasSignature
|
|
|
|
// Very large payload length
|
|
maliciousData.append(0xFF) // high byte
|
|
maliciousData.append(0xFE) // low byte (65534)
|
|
|
|
// SenderID (8 bytes)
|
|
for _ in 0..<8 {
|
|
maliciousData.append(0x01)
|
|
}
|
|
|
|
// RecipientID (8 bytes - required due to flag)
|
|
for _ in 0..<8 {
|
|
maliciousData.append(0x02)
|
|
}
|
|
|
|
// Provide minimal payload data - should trigger bounds check failure
|
|
maliciousData.append(contentsOf: [0x01, 0x02])
|
|
|
|
// Should handle gracefully without integer overflow issues
|
|
let result = BinaryProtocol.decode(maliciousData)
|
|
#expect(result == nil, "Malicious packet designed for integer overflow should return nil, not crash")
|
|
}
|
|
|
|
@Test("Test packets with incomplete headers")
|
|
func partialHeaderData() throws {
|
|
let headerSizes = [0, 1, 5, 10, 12] // Various incomplete header sizes
|
|
|
|
for size in headerSizes {
|
|
let partialData = Data(repeating: 0x01, count: size)
|
|
let result = BinaryProtocol.decode(partialData)
|
|
#expect(result == nil, "Partial header data (\(size) bytes) should return nil, not crash")
|
|
}
|
|
}
|
|
|
|
@Test("Test exact boundary conditions")
|
|
func boundaryConditions() throws {
|
|
let packet = TestHelpers.createTestPacket()
|
|
let validEncoded = try #require(BinaryProtocol.encode(packet), "Failed to encode test packet")
|
|
|
|
// If truncation only removes padding, decode may still succeed. Compute unpadded size.
|
|
let unpadded = MessagePadding.unpad(validEncoded)
|
|
// Truncate within the unpadded frame to guarantee corruption
|
|
let cut = max(1, unpadded.count - 10)
|
|
let truncatedCore = unpadded.prefix(cut)
|
|
let result = BinaryProtocol.decode(truncatedCore)
|
|
#expect(result == nil, "Truncated core frame should return nil, not crash")
|
|
|
|
// Test minimum valid size - create a valid minimal packet
|
|
var minData = Data()
|
|
minData.append(1) // version
|
|
minData.append(1) // type
|
|
minData.append(10) // ttl
|
|
|
|
// Timestamp (8 bytes)
|
|
for _ in 0..<8 {
|
|
minData.append(0)
|
|
}
|
|
|
|
minData.append(0) // flags (no optional fields)
|
|
minData.append(0) // payload length high byte
|
|
minData.append(0) // payload length low byte (0 payload)
|
|
|
|
// SenderID (8 bytes)
|
|
for _ in 0..<8 {
|
|
minData.append(0x01)
|
|
}
|
|
|
|
// This should be exactly the minimum size and should decode without crashing
|
|
_ = BinaryProtocol.decode(minData)
|
|
// The important thing is no crash occurs - result might be nil or valid
|
|
// We don't assert the result, just that no crash happens
|
|
}
|
|
}
|