Files
bitchat/bitchat/Utils/CompressionUtil.swift
T
a30b73dd99 Feature/fragmentation fixes (#453)
* Fix fragmentation + BLE long-write + padding

- Accumulate CBATTRequest long writes by offset and decode once per central
- Decode original packet after fragment reassembly (preserve flags/compression)
- Switch MessagePadding to strict PKCS#7 and validate before unpadding
- Make BinaryProtocol.decode robust: try raw first, then unpad fallback
- Unpad frames before BLE notify; fragment when exceeding centrals' max update length
- Skip notify path when max update length < 21 bytes (protocol minimum)

Verified large PMs and announces route without decode errors and peers show reliably.

* Tests: fix weak delegate lifetime, legacy constants, and unused vars; fragment unpadded frames

- BLEServiceTests: hold strong reference to MockBitchatDelegate
- IntegrationTests: fix inline comment braces; replace removed types with test-safe values; use numeric 0x06 for legacy handshake resp checks
- BinaryProtocolTests: remove unused minResult variable
- BLEService: fragment the unpadded frame so fragments are efficient

* tests: add Noise rehandshake recovery test; document in-memory test bus and autoFlood; stabilize large-network broadcast

* tests: align suite with current behavior (compression, padding, routing, nonce); deflake and stabilize

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
2025-08-19 01:22:21 +02:00

76 lines
2.9 KiB
Swift

//
// CompressionUtil.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import Compression
struct CompressionUtil {
// Compression threshold - don't compress if data is smaller than this
static let compressionThreshold = 100 // bytes
// Compress data using zlib algorithm (most compatible)
static func compress(_ data: Data) -> Data? {
// Skip compression for small data
guard data.count >= compressionThreshold else { return nil }
let maxCompressedSize = data.count + (data.count / 255) + 16
let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxCompressedSize)
defer { destinationBuffer.deallocate() }
let compressedSize = data.withUnsafeBytes { sourceBuffer in
guard let sourcePtr = sourceBuffer.bindMemory(to: UInt8.self).baseAddress else { return 0 }
return compression_encode_buffer(
destinationBuffer, data.count,
sourcePtr, data.count,
nil, COMPRESSION_ZLIB
)
}
guard compressedSize > 0 && compressedSize < data.count else { return nil }
return Data(bytes: destinationBuffer, count: compressedSize)
}
// Decompress zlib compressed data
static func decompress(_ compressedData: Data, originalSize: Int) -> Data? {
let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: originalSize)
defer { destinationBuffer.deallocate() }
let decompressedSize = compressedData.withUnsafeBytes { sourceBuffer in
guard let sourcePtr = sourceBuffer.bindMemory(to: UInt8.self).baseAddress else { return 0 }
return compression_decode_buffer(
destinationBuffer, originalSize,
sourcePtr, compressedData.count,
nil, COMPRESSION_ZLIB
)
}
guard decompressedSize > 0 else { return nil }
return Data(bytes: destinationBuffer, count: decompressedSize)
}
// Helper to check if compression is worth it
static func shouldCompress(_ data: Data) -> Bool {
// Don't compress if:
// 1. Data is too small
// 2. Data appears to be already compressed (high entropy)
guard data.count >= compressionThreshold else { return false }
// Simple entropy check - count unique bytes
var byteFrequency = [UInt8: Int]()
for byte in data {
byteFrequency[byte, default: 0] += 1
}
// If we have very high byte diversity, data is likely already compressed
let uniqueByteRatio = Double(byteFrequency.count) / Double(min(data.count, 256))
return uniqueByteRatio < 0.9 // Compress if less than 90% unique bytes
}
}