Align REQUEST_SYNC filters with Android

This commit is contained in:
Codex
2026-06-25 23:09:35 +02:00
parent f688e529f6
commit 842d8f2cd3
5 changed files with 295 additions and 34 deletions
+101
View File
@@ -279,6 +279,107 @@ struct GossipSyncManagerTests {
#expect(sentPackets.count == 1)
#expect(sentPackets[0].type == MessageType.fragment.rawValue)
}
@Test func handleRequestSyncHonorsSinceTimestamp() async throws {
var config = GossipSyncManager.Config()
config.seenCapacity = 5
config.fragmentCapacity = 0
config.fileTransferCapacity = 0
config.messageSyncIntervalSeconds = 0
config.fragmentSyncIntervalSeconds = 0
config.fileTransferSyncIntervalSeconds = 0
let requestSyncManager = RequestSyncManager()
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
let delegate = RecordingDelegate()
manager.delegate = delegate
let sender = try #require(Data(hexString: "aabbccddeeff0011"))
let threshold = UInt64(Date().timeIntervalSince1970 * 1000)
let oldMessage = BitchatPacket(
type: MessageType.message.rawValue,
senderID: sender,
recipientID: nil,
timestamp: threshold - 1,
payload: Data([0x10]),
signature: nil,
ttl: 1
)
let freshMessage = BitchatPacket(
type: MessageType.message.rawValue,
senderID: sender,
recipientID: nil,
timestamp: threshold,
payload: Data([0x20]),
signature: nil,
ttl: 1
)
manager.onPublicPacketSeen(oldMessage)
manager.onPublicPacketSeen(freshMessage)
let peer = PeerID(str: "FFFFFFFFFFFFFFFF")
let request = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .message, sinceTimestamp: threshold)
manager.handleRequestSync(from: peer, request: request)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
let sentPacket = try #require(delegate.packets.first)
#expect(sentPacket.payload == Data([0x20]))
}
@Test func buildGcsPayloadHonorsSinceTimestamp() throws {
var config = GossipSyncManager.Config()
config.seenCapacity = 5
config.fragmentCapacity = 0
config.fileTransferCapacity = 0
config.messageSyncIntervalSeconds = 0
config.fragmentSyncIntervalSeconds = 0
config.fileTransferSyncIntervalSeconds = 0
config.gcsTargetFpr = 0.000001
let requestSyncManager = RequestSyncManager()
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
let sender = try #require(Data(hexString: "1122334455667788"))
let threshold = UInt64(Date().timeIntervalSince1970 * 1000)
let oldMessage = BitchatPacket(
type: MessageType.message.rawValue,
senderID: sender,
recipientID: nil,
timestamp: threshold - 1,
payload: Data([0xA0]),
signature: nil,
ttl: 1
)
let freshMessage = BitchatPacket(
type: MessageType.message.rawValue,
senderID: sender,
recipientID: nil,
timestamp: threshold + 1,
payload: Data([0xB0]),
signature: nil,
ttl: 1
)
manager.onPublicPacketSeen(oldMessage)
manager.onPublicPacketSeen(freshMessage)
manager._performMaintenanceSynchronously()
let payload = manager._buildGcsPayloadSynchronously(for: .message, sinceTimestamp: threshold)
let request = try #require(RequestSyncPacket.decode(from: payload))
let sorted = GCSFilter.decodeToSortedSet(p: request.p, m: request.m, data: request.data)
let oldBucket = GCSFilter.bucket(for: PacketIdUtil.computeId(oldMessage), modulus: request.m)
let freshBucket = GCSFilter.bucket(for: PacketIdUtil.computeId(freshMessage), modulus: request.m)
#expect(request.types?.contains(.message) == true)
#expect(request.sinceTimestamp == threshold)
#expect(GCSFilter.contains(sortedValues: sorted, candidate: freshBucket))
#expect(GCSFilter.contains(sortedValues: sorted, candidate: oldBucket) == false)
}
}
private final class RecordingDelegate: GossipSyncManager.Delegate {
@@ -0,0 +1,118 @@
//
// RequestSyncPacketTests.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import Testing
import BitFoundation
@testable import bitchat
struct RequestSyncPacketTests {
@Test func baseFieldsRoundTrip() throws {
let original = RequestSyncPacket(
p: 7,
m: 12_800,
data: Data([1, 2, 3, 4, 5])
)
let decoded = try #require(RequestSyncPacket.decode(from: original.encode()))
#expect(decoded.p == 7)
#expect(decoded.m == 12_800)
#expect(decoded.data == Data([1, 2, 3, 4, 5]))
#expect(decoded.types == nil)
#expect(decoded.sinceTimestamp == nil)
}
@Test func upgradedFieldsRoundTripAsAndroidWantedTypes() throws {
let original = RequestSyncPacket(
p: 8,
m: 25_600,
data: Data([10, 20, 30]),
types: .publicMessages,
sinceTimestamp: 1_700_000_000_000
)
let encoded = original.encode()
let wantedTypes = try #require(tlvValue(type: 0x04, in: encoded))
let decoded = try #require(RequestSyncPacket.decode(from: encoded))
#expect(wantedTypes == Data([MessageType.announce.rawValue, MessageType.message.rawValue]))
#expect(decoded.p == 8)
#expect(decoded.m == 25_600)
#expect(decoded.data == Data([10, 20, 30]))
#expect(decoded.types?.contains(.announce) == true)
#expect(decoded.types?.contains(.message) == true)
#expect(decoded.sinceTimestamp == 1_700_000_000_000)
}
@Test func decodesLegacyPayloadWithoutUpgradeFields() throws {
let payload = Data([
0x01, 0x00, 0x01, 0x07,
0x02, 0x00, 0x04, 0x00, 0x00, 0x32, 0x00,
0x03, 0x00, 0x03, 0x01, 0x02, 0x03
])
let decoded = try #require(RequestSyncPacket.decode(from: payload))
#expect(decoded.p == 7)
#expect(decoded.m == 12_800)
#expect(decoded.data == Data([1, 2, 3]))
#expect(decoded.types == nil)
#expect(decoded.sinceTimestamp == nil)
}
@Test func decodesAndroidWantedTypesAndMinTimestamp() throws {
let payload = Data([
0x01, 0x00, 0x01, 0x08,
0x02, 0x00, 0x04, 0x00, 0x00, 0x64, 0x00,
0x03, 0x00, 0x02, 0xAA, 0xBB,
0x04, 0x00, 0x02, MessageType.announce.rawValue, MessageType.message.rawValue,
0x05, 0x00, 0x08, 0x00, 0x00, 0x01, 0x8B, 0xCF, 0xE5, 0x68, 0x00
])
let decoded = try #require(RequestSyncPacket.decode(from: payload))
#expect(decoded.p == 8)
#expect(decoded.m == 25_600)
#expect(decoded.data == Data([0xAA, 0xBB]))
#expect(decoded.types?.contains(.announce) == true)
#expect(decoded.types?.contains(.message) == true)
#expect(decoded.sinceTimestamp == 1_700_000_000_000)
}
@Test func decodesLegacyIOSPublicMessageBitfield() throws {
let payload = Data([
0x01, 0x00, 0x01, 0x08,
0x02, 0x00, 0x04, 0x00, 0x00, 0x64, 0x00,
0x03, 0x00, 0x00,
0x04, 0x00, 0x01, 0x03
])
let decoded = try #require(RequestSyncPacket.decode(from: payload))
#expect(decoded.types?.contains(.announce) == true)
#expect(decoded.types?.contains(.message) == true)
}
private func tlvValue(type: UInt8, in data: Data) -> Data? {
var offset = 0
while offset + 3 <= data.count {
let currentType = data[offset]
offset += 1
let length = (Int(data[offset]) << 8) | Int(data[offset + 1])
offset += 2
guard offset + length <= data.count else { return nil }
let value = data.subdata(in: offset..<(offset + length))
offset += length
if currentType == type {
return value
}
}
return nil
}
}