mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:45: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
|
// Check cumulative size before storing this fragment
|
||||||
let currentSize = incomingFragments[key]?.values.reduce(0) { $0 + $1.count } ?? 0
|
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
|
// 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)
|
incomingFragments.removeValue(forKey: key)
|
||||||
fragmentMetadata.removeValue(forKey: key)
|
fragmentMetadata.removeValue(forKey: key)
|
||||||
shouldReassemble = false
|
shouldReassemble = false
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ enum FileTransferLimits {
|
|||||||
static let maxVoiceNoteBytes: Int = 1 * 1024 * 1024 // 1 MiB
|
static let maxVoiceNoteBytes: Int = 1 * 1024 * 1024 // 1 MiB
|
||||||
/// Compressed images after downscaling should comfortably fit under this budget.
|
/// Compressed images after downscaling should comfortably fit under this budget.
|
||||||
static let maxImageBytes: Int = 1 * 1024 * 1024 // 1 MiB
|
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 {
|
static func isValidPayload(_ size: Int) -> Bool {
|
||||||
size <= maxPayloadBytes
|
size <= maxPayloadBytes
|
||||||
|
|||||||
@@ -93,6 +93,62 @@ struct FragmentationTests {
|
|||||||
#expect(capture.publicMessages.first?.content.count == 2048)
|
#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")
|
@Test("Invalid fragment header is ignored")
|
||||||
func invalidFragmentHeaderIsIgnored() async throws {
|
func invalidFragmentHeaderIsIgnored() async throws {
|
||||||
let ble = BLEService(
|
let ble = BLEService(
|
||||||
@@ -142,7 +198,10 @@ struct FragmentationTests {
|
|||||||
extension FragmentationTests {
|
extension FragmentationTests {
|
||||||
private final class CaptureDelegate: BitchatDelegate {
|
private final class CaptureDelegate: BitchatDelegate {
|
||||||
var publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
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 didConnectToPeer(_ peerID: PeerID) {}
|
||||||
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
||||||
func didUpdatePeerList(_ peers: [PeerID]) {}
|
func didUpdatePeerList(_ peers: [PeerID]) {}
|
||||||
@@ -173,8 +232,8 @@ extension FragmentationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper: fragment a packet using the same header format BLEService expects
|
// Helper: fragment a packet using the same header format BLEService expects
|
||||||
private func fragmentPacket(_ packet: BitchatPacket, fragmentSize: Int, fragmentID: Data? = nil) -> [BitchatPacket] {
|
private func fragmentPacket(_ packet: BitchatPacket, fragmentSize: Int, fragmentID: Data? = nil, pad: Bool = true) -> [BitchatPacket] {
|
||||||
let fullData = packet.toBinaryData() ?? Data()
|
guard let fullData = packet.toBinaryData(padding: pad) else { return [] }
|
||||||
let fid = fragmentID ?? Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
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
|
let chunks: [Data] = stride(from: 0, to: fullData.count, by: fragmentSize).map { off in
|
||||||
Data(fullData[off..<min(off + fragmentSize, fullData.count)])
|
Data(fullData[off..<min(off + fragmentSize, fullData.count)])
|
||||||
|
|||||||
Reference in New Issue
Block a user