diff --git a/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift b/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift index 09a65616..316a232f 100644 --- a/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift +++ b/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift @@ -94,9 +94,14 @@ enum BLEOutboundFragmentPlanner { calculatedChunk = max(minimumChunkSize, bleMaxMTU - overhead) } + let linkBoundedChunk = min( + requestedMaxChunk ?? calculatedChunk, + calculatedChunk + ) + return ( fragmentVersion: fragmentVersion, - chunkSize: max(minimumChunkSize, requestedMaxChunk ?? calculatedChunk) + chunkSize: max(minimumChunkSize, linkBoundedChunk) ) } diff --git a/bitchat/Services/BLE/BLEOutboundPacketPolicy.swift b/bitchat/Services/BLE/BLEOutboundPacketPolicy.swift index ddcc4abc..e79980bc 100644 --- a/bitchat/Services/BLE/BLEOutboundPacketPolicy.swift +++ b/bitchat/Services/BLE/BLEOutboundPacketPolicy.swift @@ -2,7 +2,11 @@ import BitFoundation import Foundation enum BLEOutboundPacketPolicy { - private static let fragmentFrameOverhead = 13 + 8 + 8 + 13 + private static let fragmentFrameOverhead = + BinaryProtocol.v1HeaderSize + + BinaryProtocol.senderIDSize + + BinaryProtocol.recipientIDSize + + 13 static func messageID(for packet: BitchatPacket) -> String { BLEIngressLinkRegistry.messageID(for: packet) diff --git a/bitchatTests/Services/BLEOutboundFragmentPlannerTests.swift b/bitchatTests/Services/BLEOutboundFragmentPlannerTests.swift index 9e3db51e..0c60e09a 100644 --- a/bitchatTests/Services/BLEOutboundFragmentPlannerTests.swift +++ b/bitchatTests/Services/BLEOutboundFragmentPlannerTests.swift @@ -78,6 +78,34 @@ struct BLEOutboundFragmentPlannerTests { #expect(plan.fragmentPackets.allSatisfy { $0.recipientID == Data(hexString: directedPeer.id) }) } + @Test("link-derived chunks keep every directed fragment within the link limit") + func linkDerivedChunksFitTheLinkLimit() throws { + let linkLimit = 512 + let directedPeer = PeerID(str: "8877665544332211") + let packet = makePacket(payload: makePayload(count: 1_024)) + let request = BLEOutboundFragmentTransferRequest( + packet: packet, + pad: false, + maxChunk: BLEOutboundPacketPolicy.fragmentChunkSize( + forLinkLimit: linkLimit + ), + directedPeer: directedPeer, + transferId: nil + ) + + let plan = try #require(BLEOutboundFragmentPlanner.makePlan( + for: request, + defaultChunkSize: TransportConfig.bleDefaultFragmentSize, + bleMaxMTU: linkLimit, + fragmentID: Data(repeating: 0xB3, count: 8) + )) + let encodedFragments = try plan.fragmentPackets.map { + try #require($0.toBinaryData(padding: false)) + } + + #expect(encodedFragments.allSatisfy { $0.count <= linkLimit }) + } + @Test("route-aware fragments use version two and route-sized chunking") func routeAwareFragmentsUseVersionTwoAndRouteSizedChunking() throws { let route = [ @@ -107,6 +135,43 @@ struct BLEOutboundFragmentPlannerTests { #expect(plan.fragmentPackets.allSatisfy { $0.route == route && $0.isRSR }) } + @Test("link-derived chunks cannot override route-safe fragment sizing") + func linkDerivedChunksRespectRouteOverhead() throws { + let linkLimit = 512 + let route = [ + Data([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]), + Data([0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27]) + ] + let directedPeer = PeerID(str: "8877665544332211") + let packet = makePacket( + payload: makePayload(count: 1_024), + route: route + ) + let requestedChunk = BLEOutboundPacketPolicy.fragmentChunkSize( + forLinkLimit: linkLimit + ) + let request = BLEOutboundFragmentTransferRequest( + packet: packet, + pad: false, + maxChunk: requestedChunk, + directedPeer: directedPeer, + transferId: nil + ) + + let plan = try #require(BLEOutboundFragmentPlanner.makePlan( + for: request, + defaultChunkSize: TransportConfig.bleDefaultFragmentSize, + bleMaxMTU: linkLimit, + fragmentID: Data(repeating: 0xC4, count: 8) + )) + let encodedFragments = try plan.fragmentPackets.map { + try #require($0.toBinaryData(padding: false)) + } + + #expect(plan.chunkSize < requestedChunk) + #expect(encodedFragments.allSatisfy { $0.count <= linkLimit }) + } + @Test("invalid fragment IDs do not produce a plan") func invalidFragmentIDReturnsNil() { let packet = makePacket(payload: makePayload(count: 128))