diff --git a/bitchat/Protocols/BinaryProtocol.swift b/bitchat/Protocols/BinaryProtocol.swift index 00e8bb0c..f79f931d 100644 --- a/bitchat/Protocols/BinaryProtocol.swift +++ b/bitchat/Protocols/BinaryProtocol.swift @@ -161,7 +161,9 @@ struct BinaryProtocol { } let lengthFieldBytes = lengthFieldSize(for: version) - let originalRoute = packet.route ?? [] + + // Route is only supported for v2+ packets (per SOURCE_ROUTING.md spec) + let originalRoute = (version >= 2) ? (packet.route ?? []) : [] if originalRoute.contains(where: { $0.isEmpty }) { return nil } let sanitizedRoute: [Data] = originalRoute.map { hop in if hop.count == senderIDSize { return hop } @@ -175,14 +177,15 @@ struct BinaryProtocol { let hasRoute = !sanitizedRoute.isEmpty let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0 let originalSizeFieldBytes = isCompressed ? lengthFieldBytes : 0 - let payloadDataSize = routeLength + payload.count + originalSizeFieldBytes + // payloadLength in header is payload-only (does NOT include route bytes) + let payloadDataSize = payload.count + originalSizeFieldBytes if version == 1 && payloadDataSize > Int(UInt16.max) { return nil } if version == 2 && payloadDataSize > Int(UInt32.max) { return nil } guard let headerSize = headerSize(for: version) else { return nil } let estimatedHeader = headerSize + senderIDSize + (packet.recipientID == nil ? 0 : recipientIDSize) - let estimatedPayload = payloadDataSize + let estimatedPayload = payloadDataSize + routeLength let estimatedSignature = (packet.signature == nil ? 0 : signatureSize) var data = Data() data.reserveCapacity(estimatedHeader + estimatedPayload + estimatedSignature + 255) @@ -199,7 +202,8 @@ struct BinaryProtocol { if packet.recipientID != nil { flags |= Flags.hasRecipient } if packet.signature != nil { flags |= Flags.hasSignature } if isCompressed { flags |= Flags.isCompressed } - if hasRoute { flags |= Flags.hasRoute } + // HAS_ROUTE is only valid for v2+ packets + if hasRoute && version >= 2 { flags |= Flags.hasRoute } data.append(flags) if version == 2 { @@ -323,6 +327,8 @@ struct BinaryProtocol { let hasRecipient = (flags & Flags.hasRecipient) != 0 let hasSignature = (flags & Flags.hasSignature) != 0 let isCompressed = (flags & Flags.isCompressed) != 0 + // HAS_ROUTE is only valid for v2+ packets; ignore the flag for v1 + let hasRoute = (version >= 2) && (flags & Flags.hasRoute) != 0 let payloadLength: Int if version == 2 { @@ -343,27 +349,24 @@ struct BinaryProtocol { if recipientID == nil { return nil } } + // Route (optional, v2+ only): route bytes are NOT included in payloadLength var route: [Data]? = nil - var remainingPayloadBytes = payloadLength - - if (flags & Flags.hasRoute) != 0 { - guard remainingPayloadBytes >= 1, let routeCount = read8() else { return nil } - remainingPayloadBytes -= 1 + if hasRoute { + guard let routeCount = read8() else { return nil } if routeCount > 0 { var hops: [Data] = [] for _ in 0..= senderIDSize, - let hop = readData(senderIDSize) else { return nil } - remainingPayloadBytes -= senderIDSize + guard let hop = readData(senderIDSize) else { return nil } hops.append(hop) } route = hops } } + // Payload: payloadLength is exactly the payload size (+ compression preamble if compressed) let payload: Data if isCompressed { - guard remainingPayloadBytes >= lengthFieldBytes else { return nil } + guard payloadLength >= lengthFieldBytes else { return nil } let originalSize: Int if version == 2 { guard let rawSize = read32() else { return nil } @@ -372,11 +375,9 @@ struct BinaryProtocol { guard let rawSize = read16() else { return nil } originalSize = Int(rawSize) } - remainingPayloadBytes -= lengthFieldBytes guard originalSize >= 0 && originalSize <= FileTransferLimits.maxFramedFileBytes else { return nil } - let compressedSize = remainingPayloadBytes + let compressedSize = payloadLength - lengthFieldBytes guard compressedSize > 0, let compressed = readData(compressedSize) else { return nil } - remainingPayloadBytes = 0 let compressionRatio = Double(originalSize) / Double(compressedSize) guard compressionRatio <= 50_000.0 else { @@ -388,9 +389,7 @@ struct BinaryProtocol { decompressed.count == originalSize else { return nil } payload = decompressed } else { - guard remainingPayloadBytes >= 0, - let rawPayload = readData(remainingPayloadBytes) else { return nil } - remainingPayloadBytes = 0 + guard let rawPayload = readData(payloadLength) else { return nil } payload = rawPayload } diff --git a/bitchatTests/Protocol/BinaryProtocolTests.swift b/bitchatTests/Protocol/BinaryProtocolTests.swift index 17d014ff..72def4c8 100644 --- a/bitchatTests/Protocol/BinaryProtocolTests.swift +++ b/bitchatTests/Protocol/BinaryProtocolTests.swift @@ -55,6 +55,8 @@ struct BinaryProtocolTests { #expect(decodedPacket.signature == TestConstants.testSignature) } + // MARK: - Source-Based Routing Tests (v2 only) + @Test func packetWithRouteRoundTrip() throws { let route: [Data] = [ try #require(Data(hexString: "0102030405060708")), @@ -62,6 +64,7 @@ struct BinaryProtocolTests { try #require(Data(hexString: "2122232425262728")) ] + // Route is only supported for v2+ packets var packet = BitchatPacket( type: 0x01, senderID: route[0], @@ -69,7 +72,8 @@ struct BinaryProtocolTests { timestamp: 1_720_000_000_000, payload: Data("route-test".utf8), signature: nil, - ttl: 6 + ttl: 6, + version: 2 ) packet.route = route @@ -78,6 +82,7 @@ struct BinaryProtocolTests { #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) { @@ -90,6 +95,7 @@ struct BinaryProtocolTests { 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, @@ -97,7 +103,8 @@ struct BinaryProtocolTests { timestamp: 1_730_000_000_000, payload: Data("pad-test".utf8), signature: nil, - ttl: 5 + ttl: 5, + version: 2 ) packet.route = [shortHop, destination] @@ -117,6 +124,7 @@ struct BinaryProtocolTests { 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], @@ -124,7 +132,8 @@ struct BinaryProtocolTests { timestamp: 1_740_000_000_000, payload: Data(repeatedString.utf8), signature: nil, - ttl: 7 + ttl: 7, + version: 2 ) packet.route = route @@ -135,6 +144,146 @@ struct BinaryProtocolTests { #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)")