// // BinaryProtocol.swift // bitchat // // This is free and unencumbered software released into the public domain. // For more information, see // import Foundation extension Data { func trimmingNullBytes() -> Data { // Find the first null byte if let nullIndex = self.firstIndex(of: 0) { return self.prefix(nullIndex) } return self } } // Binary Protocol Format: // Header (Fixed 13 bytes): // - Version: 1 byte // - Type: 1 byte // - TTL: 1 byte // - Timestamp: 8 bytes (UInt64) // - Flags: 1 byte (bit 0: hasRecipient, bit 1: hasSignature) // - PayloadLength: 2 bytes (UInt16) // // Variable sections: // - SenderID: 8 bytes (fixed) // - RecipientID: 8 bytes (if hasRecipient flag set) // - Payload: Variable length // - Signature: 64 bytes (if hasSignature flag set) struct BinaryProtocol { static let headerSize = 13 static let senderIDSize = 8 static let recipientIDSize = 8 static let signatureSize = 64 struct Flags { static let hasRecipient: UInt8 = 0x01 static let hasSignature: UInt8 = 0x02 static let isCompressed: UInt8 = 0x04 } // Encode BitchatPacket to binary format static func encode(_ packet: BitchatPacket) -> Data? { var data = Data() // Try to compress payload if beneficial var payload = packet.payload var originalPayloadSize: UInt16? = nil var isCompressed = false if CompressionUtil.shouldCompress(payload), let compressedPayload = CompressionUtil.compress(payload) { // Store original size for decompression (2 bytes after payload) originalPayloadSize = UInt16(payload.count) payload = compressedPayload isCompressed = true } // Header data.append(packet.version) data.append(packet.type) data.append(packet.ttl) // Timestamp (8 bytes, big-endian) for i in (0..<8).reversed() { data.append(UInt8((packet.timestamp >> (i * 8)) & 0xFF)) } // Flags var flags: UInt8 = 0 if packet.recipientID != nil { flags |= Flags.hasRecipient } if packet.signature != nil { flags |= Flags.hasSignature } if isCompressed { flags |= Flags.isCompressed } data.append(flags) // Payload length (2 bytes, big-endian) - includes original size if compressed let payloadDataSize = payload.count + (isCompressed ? 2 : 0) let payloadLength = UInt16(payloadDataSize) data.append(UInt8((payloadLength >> 8) & 0xFF)) data.append(UInt8(payloadLength & 0xFF)) // SenderID (exactly 8 bytes) let senderBytes = packet.senderID.prefix(senderIDSize) data.append(senderBytes) if senderBytes.count < senderIDSize { data.append(Data(repeating: 0, count: senderIDSize - senderBytes.count)) } // RecipientID (if present) if let recipientID = packet.recipientID { let recipientBytes = recipientID.prefix(recipientIDSize) data.append(recipientBytes) if recipientBytes.count < recipientIDSize { data.append(Data(repeating: 0, count: recipientIDSize - recipientBytes.count)) } } // Payload (with original size prepended if compressed) if isCompressed, let originalSize = originalPayloadSize { // Prepend original size (2 bytes, big-endian) data.append(UInt8((originalSize >> 8) & 0xFF)) data.append(UInt8(originalSize & 0xFF)) } data.append(payload) // Signature (if present) if let signature = packet.signature { data.append(signature.prefix(signatureSize)) } return data } // Decode binary data to BitchatPacket static func decode(_ data: Data) -> BitchatPacket? { guard data.count >= headerSize + senderIDSize else { return nil } var offset = 0 // Header let version = data[offset]; offset += 1 // Only support version 1 guard version == 1 else { return nil } let type = data[offset]; offset += 1 let ttl = data[offset]; offset += 1 // Timestamp let timestampData = data[offset..= expectedSize else { return nil } // SenderID let senderID = data[offset..= 2 else { return nil } let originalSizeData = data[offset.. Data? { var data = Data() // Message format: // - Flags: 1 byte (bit 0: isRelay, bit 1: isPrivate, bit 2: hasOriginalSender, bit 3: hasRecipientNickname, bit 4: hasSenderPeerID, bit 5: hasMentions, bit 6: hasRoom, bit 7: isEncrypted) // - Timestamp: 8 bytes (seconds since epoch) // - ID length: 1 byte // - ID: variable // - Sender length: 1 byte // - Sender: variable // - Content length: 2 bytes // - Content: variable (or encrypted content if isEncrypted) // Optional fields based on flags: // - Original sender length + data // - Recipient nickname length + data // - Sender peer ID length + data // - Mentions array // - Room hashtag var flags: UInt8 = 0 if isRelay { flags |= 0x01 } if isPrivate { flags |= 0x02 } if originalSender != nil { flags |= 0x04 } if recipientNickname != nil { flags |= 0x08 } if senderPeerID != nil { flags |= 0x10 } if mentions != nil && !mentions!.isEmpty { flags |= 0x20 } if room != nil { flags |= 0x40 } if isEncrypted { flags |= 0x80 } data.append(flags) // Timestamp (in milliseconds) let timestampMillis = UInt64(timestamp.timeIntervalSince1970 * 1000) // Encode as 8 bytes, big-endian for i in (0..<8).reversed() { data.append(UInt8((timestampMillis >> (i * 8)) & 0xFF)) } // ID if let idData = id.data(using: .utf8) { data.append(UInt8(min(idData.count, 255))) data.append(idData.prefix(255)) } else { data.append(0) } // Sender if let senderData = sender.data(using: .utf8) { data.append(UInt8(min(senderData.count, 255))) data.append(senderData.prefix(255)) } else { data.append(0) } // Content or encrypted content if isEncrypted, let encryptedContent = encryptedContent { let length = UInt16(min(encryptedContent.count, 65535)) // Encode length as 2 bytes, big-endian data.append(UInt8((length >> 8) & 0xFF)) data.append(UInt8(length & 0xFF)) data.append(encryptedContent.prefix(Int(length))) } else if let contentData = content.data(using: .utf8) { let length = UInt16(min(contentData.count, 65535)) // Encode length as 2 bytes, big-endian data.append(UInt8((length >> 8) & 0xFF)) data.append(UInt8(length & 0xFF)) data.append(contentData.prefix(Int(length))) } else { data.append(contentsOf: [0, 0]) } // Optional fields if let originalSender = originalSender, let origData = originalSender.data(using: .utf8) { data.append(UInt8(min(origData.count, 255))) data.append(origData.prefix(255)) } if let recipientNickname = recipientNickname, let recipData = recipientNickname.data(using: .utf8) { data.append(UInt8(min(recipData.count, 255))) data.append(recipData.prefix(255)) } if let senderPeerID = senderPeerID, let peerData = senderPeerID.data(using: .utf8) { data.append(UInt8(min(peerData.count, 255))) data.append(peerData.prefix(255)) } // Mentions array if let mentions = mentions { data.append(UInt8(min(mentions.count, 255))) // Number of mentions for mention in mentions.prefix(255) { if let mentionData = mention.data(using: .utf8) { data.append(UInt8(min(mentionData.count, 255))) data.append(mentionData.prefix(255)) } else { data.append(0) } } } // Room hashtag if let room = room, let roomData = room.data(using: .utf8) { data.append(UInt8(min(roomData.count, 255))) data.append(roomData.prefix(255)) } return data } static func fromBinaryPayload(_ data: Data) -> BitchatMessage? { // Create an immutable copy to prevent threading issues let dataCopy = Data(data) guard dataCopy.count >= 13 else { return nil } var offset = 0 // Flags guard offset < dataCopy.count else { return nil } let flags = dataCopy[offset]; offset += 1 let isRelay = (flags & 0x01) != 0 let isPrivate = (flags & 0x02) != 0 let hasOriginalSender = (flags & 0x04) != 0 let hasRecipientNickname = (flags & 0x08) != 0 let hasSenderPeerID = (flags & 0x10) != 0 let hasMentions = (flags & 0x20) != 0 let hasRoom = (flags & 0x40) != 0 let isEncrypted = (flags & 0x80) != 0 // Timestamp guard offset + 8 <= dataCopy.count else { return nil } let timestampData = dataCopy[offset.. 0 { mentions = [] for _ in 0..