mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
BLEService's per-packet-type orchestration moves into owned, tested components (BLEAnnounceHandler, BLEPublicMessageHandler, BLENoisePacketHandler, BLEFileTransferHandler, BLEFragmentHandler), each taking an environment struct of closures so every queue hop stays in BLEService and the handlers are synchronously testable. Behavior is preserved verbatim, including Noise session recovery on decrypt failure and single-block UI event ordering. handleLeave/handleRequestSync stay in place as already-thin delegations. BLEService drops to 3393 lines. Four coordinators (delivery, private conversation, Nostr, public conversation) drop their unowned/weak ChatViewModel back-references for narrow @MainActor context protocols, with ChatViewModel conformances as single shared witnesses for overlapping members. Their true coupling is now an explicit, reviewable surface, and each gains a mock-context test suite covering flows previously testable only through the full view model. Delivery/read acks now also clear the router's retained-send outbox via the delivery context. New LargeTopologyTests exercise production-shaped meshes with the in-memory harness: an 8-peer relay chain with per-hop TTL decay, a 14-peer cyclic mesh with exactly-once delivery, partition/heal, and topology churn. App-layer runtime/model files updated alongside. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
4.3 KiB
Swift
95 lines
4.3 KiB
Swift
import BitFoundation
|
|
import BitLogger
|
|
import Foundation
|
|
|
|
/// Narrow environment for `BLEFragmentHandler`.
|
|
///
|
|
/// All queue hops (the message-queue entry hop and the collections barrier
|
|
/// around the assembly buffer) live on the `BLEService` side — the entry hop
|
|
/// in `BLEService.handleFragment`, the barrier inside the supplied closures —
|
|
/// keeping the handler queue-agnostic and synchronously testable.
|
|
struct BLEFragmentHandlerEnvironment {
|
|
/// Local peer identity at the time the fragment is handled.
|
|
let localPeerID: () -> PeerID
|
|
/// Tracks broadcast fragments for gossip sync.
|
|
let trackPacketSeen: (BitchatPacket) -> Void
|
|
/// Appends the fragment to the assembly buffer (collections barrier write).
|
|
let appendFragment: (BLEFragmentHeader) -> BLEFragmentAssemblyBuffer.AppendResult
|
|
/// Ingress acceptance check for the reassembled inner packet.
|
|
let isAcceptedIngressPayload: (_ packet: BitchatPacket, _ innerSender: PeerID) -> Bool
|
|
/// Re-enters the receive pipeline with the reassembled packet (TTL already zeroed).
|
|
let processReassembledPacket: (_ packet: BitchatPacket, _ from: PeerID) -> Void
|
|
}
|
|
|
|
/// Orchestrates inbound fragments: self-fragment suppression, gossip tracking,
|
|
/// assembly-buffer appends, and reassembled-packet validation and re-injection
|
|
/// into the receive pipeline.
|
|
final class BLEFragmentHandler {
|
|
private let environment: BLEFragmentHandlerEnvironment
|
|
|
|
init(environment: BLEFragmentHandlerEnvironment) {
|
|
self.environment = environment
|
|
}
|
|
|
|
func handle(_ packet: BitchatPacket, from peerID: PeerID) {
|
|
let env = environment
|
|
// Don't process our own fragments
|
|
if peerID == env.localPeerID() {
|
|
return
|
|
}
|
|
|
|
guard let header = BLEFragmentHeader(packet: packet) else { return }
|
|
|
|
if header.isBroadcastFragment {
|
|
env.trackPacketSeen(packet)
|
|
}
|
|
|
|
let assemblyResult = env.appendFragment(header)
|
|
|
|
logFragmentAssemblyResult(assemblyResult)
|
|
|
|
guard case let .complete(completedHeader, reassembled, _) = assemblyResult else { return }
|
|
|
|
// Decode the original packet bytes we reassembled, so flags/compression are preserved
|
|
if var originalPacket = BinaryProtocol.decode(reassembled) {
|
|
|
|
// Reassembled packet validation
|
|
let innerSender = PeerID(hexData: originalPacket.senderID)
|
|
if !env.isAcceptedIngressPayload(originalPacket, innerSender) {
|
|
// Cleanup below
|
|
} else {
|
|
SecureLogger.debug("✅ Reassembled packet id=\(completedHeader.idLogString) type=\(originalPacket.type) bytes=\(reassembled.count)", category: .session)
|
|
originalPacket.ttl = 0
|
|
env.processReassembledPacket(originalPacket, peerID)
|
|
}
|
|
} else {
|
|
SecureLogger.error("❌ Failed to decode reassembled packet (type=\(completedHeader.originalType), total=\(completedHeader.total))", category: .session)
|
|
}
|
|
}
|
|
|
|
private func logFragmentAssemblyResult(_ result: BLEFragmentAssemblyBuffer.AppendResult) {
|
|
func logStartedIfNeeded(header: BLEFragmentHeader, started: Bool) {
|
|
if started {
|
|
SecureLogger.debug("📦 Started fragment assembly id=\(header.idLogString) total=\(header.total)", category: .session)
|
|
}
|
|
}
|
|
|
|
switch result {
|
|
case let .stored(header, started):
|
|
logStartedIfNeeded(header: header, started: started)
|
|
SecureLogger.debug("📦 Fragment \(header.index + 1)/\(header.total) (len=\(header.fragmentData.count)) for id=\(header.idLogString)", category: .session)
|
|
|
|
case let .complete(header, _, started):
|
|
logStartedIfNeeded(header: header, started: started)
|
|
SecureLogger.debug("📦 Fragment \(header.index + 1)/\(header.total) (len=\(header.fragmentData.count)) for id=\(header.idLogString)", category: .session)
|
|
|
|
case let .oversized(header, projectedSize, limit, started):
|
|
logStartedIfNeeded(header: header, started: started)
|
|
SecureLogger.warning(
|
|
"🚫 Fragment assembly exceeds size limit (\(projectedSize) bytes > \(limit)), evicting. Type=\(header.originalType) Index=\(header.index)/\(header.total)",
|
|
category: .security
|
|
)
|
|
}
|
|
}
|
|
}
|