mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:25:19 +00:00
Allow file fragments to account for protocol overhead
This commit is contained in:
@@ -1142,9 +1142,19 @@ final class BLEService: NSObject {
|
||||
|
||||
// Check cumulative size before storing this fragment
|
||||
let currentSize = incomingFragments[key]?.values.reduce(0) { $0 + $1.count } ?? 0
|
||||
guard currentSize + fragmentData.count <= FileTransferLimits.maxPayloadBytes else {
|
||||
let assemblyLimit: Int = {
|
||||
if originalType == MessageType.fileTransfer.rawValue {
|
||||
// Allow headroom for TLV metadata and binary framing overhead.
|
||||
return FileTransferLimits.maxFramedFileBytes
|
||||
}
|
||||
return FileTransferLimits.maxPayloadBytes
|
||||
}()
|
||||
guard currentSize + fragmentData.count <= assemblyLimit else {
|
||||
// Exceeds size limit - evict this assembly
|
||||
SecureLogger.warning("🚫 Fragment assembly exceeds size limit (\(currentSize + fragmentData.count) bytes), evicting", category: .security)
|
||||
SecureLogger.warning(
|
||||
"🚫 Fragment assembly exceeds size limit (\(currentSize + fragmentData.count) bytes > \(assemblyLimit)), evicting",
|
||||
category: .security
|
||||
)
|
||||
incomingFragments.removeValue(forKey: key)
|
||||
fragmentMetadata.removeValue(forKey: key)
|
||||
shouldReassemble = false
|
||||
|
||||
@@ -8,6 +8,16 @@ enum FileTransferLimits {
|
||||
static let maxVoiceNoteBytes: Int = 1 * 1024 * 1024 // 1 MiB
|
||||
/// Compressed images after downscaling should comfortably fit under this budget.
|
||||
static let maxImageBytes: Int = 1 * 1024 * 1024 // 1 MiB
|
||||
/// Worst-case size once TLV metadata and binary packet framing are included for the largest payloads.
|
||||
static let maxFramedFileBytes: Int = {
|
||||
let maxMetadataBytes = Int(UInt16.max) * 2 // fileName + mimeType TLVs
|
||||
let tlvEnvelopeOverhead = 18 + maxMetadataBytes // TLV tags + lengths + metadata bytes
|
||||
let binaryEnvelopeOverhead = BinaryProtocol.v2HeaderSize
|
||||
+ BinaryProtocol.senderIDSize
|
||||
+ BinaryProtocol.recipientIDSize
|
||||
+ BinaryProtocol.signatureSize
|
||||
return maxPayloadBytes + tlvEnvelopeOverhead + binaryEnvelopeOverhead
|
||||
}()
|
||||
|
||||
static func isValidPayload(_ size: Int) -> Bool {
|
||||
size <= maxPayloadBytes
|
||||
|
||||
@@ -92,6 +92,62 @@ struct FragmentationTests {
|
||||
#expect(capture.publicMessages.count == 1)
|
||||
#expect(capture.publicMessages.first?.content.count == 2048)
|
||||
}
|
||||
|
||||
@Test("Max-sized file transfer survives reassembly")
|
||||
func maxSizedFileTransferSurvivesReassembly() async throws {
|
||||
let ble = BLEService(
|
||||
keychain: mockKeychain,
|
||||
idBridge: idBridge,
|
||||
identityManager: mockIdentityManager
|
||||
)
|
||||
let capture = CaptureDelegate()
|
||||
ble.delegate = capture
|
||||
|
||||
let remoteID = PeerID(str: "CAFEBABECAFEBABE")
|
||||
let fileContent = Data(repeating: 0x42, count: FileTransferLimits.maxPayloadBytes)
|
||||
let filePacket = BitchatFilePacket(
|
||||
fileName: "limit.bin",
|
||||
fileSize: UInt64(fileContent.count),
|
||||
mimeType: "application/octet-stream",
|
||||
content: fileContent
|
||||
)
|
||||
let encoded = try #require(filePacket.encode(), "File packet encoding failed")
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.fileTransfer.rawValue,
|
||||
senderID: Data(hexString: remoteID.id) ?? Data(),
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: encoded,
|
||||
signature: nil,
|
||||
ttl: 7,
|
||||
version: 2
|
||||
)
|
||||
|
||||
let fragments = fragmentPacket(packet, fragmentSize: 4096, pad: false)
|
||||
#expect(!fragments.isEmpty)
|
||||
|
||||
for (i, fragment) in fragments.enumerated() {
|
||||
let delay = 5 * Double(i) * 0.001
|
||||
Task {
|
||||
try await sleep(delay)
|
||||
ble._test_handlePacket(fragment, fromPeerID: remoteID)
|
||||
}
|
||||
}
|
||||
|
||||
try await sleep(1.0)
|
||||
|
||||
let message = try #require(capture.receivedMessages.first, "Expected file transfer message")
|
||||
#expect(message.content.hasPrefix("[file]"))
|
||||
|
||||
if let fileName = message.content.split(separator: " ").last {
|
||||
let base = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
let filesRoot = base.appendingPathComponent("files", isDirectory: true)
|
||||
let incoming = filesRoot.appendingPathComponent("files/incoming", isDirectory: true)
|
||||
let url = incoming.appendingPathComponent(String(fileName))
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Invalid fragment header is ignored")
|
||||
func invalidFragmentHeaderIsIgnored() async throws {
|
||||
@@ -142,7 +198,10 @@ struct FragmentationTests {
|
||||
extension FragmentationTests {
|
||||
private final class CaptureDelegate: BitchatDelegate {
|
||||
var publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
||||
func didReceiveMessage(_ message: BitchatMessage) {}
|
||||
var receivedMessages: [BitchatMessage] = []
|
||||
func didReceiveMessage(_ message: BitchatMessage) {
|
||||
receivedMessages.append(message)
|
||||
}
|
||||
func didConnectToPeer(_ peerID: PeerID) {}
|
||||
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
||||
func didUpdatePeerList(_ peers: [PeerID]) {}
|
||||
@@ -173,8 +232,8 @@ extension FragmentationTests {
|
||||
}
|
||||
|
||||
// Helper: fragment a packet using the same header format BLEService expects
|
||||
private func fragmentPacket(_ packet: BitchatPacket, fragmentSize: Int, fragmentID: Data? = nil) -> [BitchatPacket] {
|
||||
let fullData = packet.toBinaryData() ?? Data()
|
||||
private func fragmentPacket(_ packet: BitchatPacket, fragmentSize: Int, fragmentID: Data? = nil, pad: Bool = true) -> [BitchatPacket] {
|
||||
guard let fullData = packet.toBinaryData(padding: pad) else { return [] }
|
||||
let fid = fragmentID ?? Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||
let chunks: [Data] = stride(from: 0, to: fullData.count, by: fragmentSize).map { off in
|
||||
Data(fullData[off..<min(off + fragmentSize, fullData.count)])
|
||||
|
||||
Reference in New Issue
Block a user