adjust TLV sizes

This commit is contained in:
callebtc
2025-09-18 17:19:51 +02:00
parent ade700e7f0
commit b196a55c37
4 changed files with 77 additions and 155 deletions
+64 -24
View File
@@ -2,8 +2,8 @@
// BitchatFilePacket.swift
// bitchat
//
// TLV encoder/decoder for file transfer payloads (audio/images)
// Types: 0x01 FILE_NAME (utf8), 0x02 FILE_SIZE (8 bytes, BE), 0x03 MIME_TYPE (utf8), 0x04 CONTENT (raw)
// TLV encoder/decoder for file transfer payloads (images, audio, and generic files)
// v2 Spec: 0x01 FILE_NAME (utf8), 0x02 FILE_SIZE (4 bytes, BE), 0x03 MIME_TYPE (utf8), 0x04 CONTENT (4-byte len)
//
import Foundation
@@ -18,29 +18,51 @@ struct BitchatFilePacket {
}
let fileName: String
let fileSize: UInt64
let fileSize: UInt32
let mimeType: String
let content: Data
func encode() -> Data? {
var out = Data()
func appendTLV(_ type: TLVType, _ value: Data) {
// Standard TLV helper for FILE_NAME and MIME_TYPE (2-byte length)
func appendStandardTLV(_ type: TLVType, _ value: Data) {
out.append(type.rawValue)
let len = UInt16(min(value.count, 0xFFFF))
out.append(UInt8((len >> 8) & 0xFF))
out.append(UInt8(len & 0xFF))
out.append(value.prefix(Int(len)))
}
// FILE_NAME
if let nameData = fileName.data(using: .utf8) { appendTLV(.fileName, nameData) }
// FILE_SIZE (8 bytes, BE)
var sizeBE = Data()
for i in (0..<8).reversed() { sizeBE.append(UInt8((fileSize >> (i * 8)) & 0xFF)) }
appendTLV(.fileSize, sizeBE)
if let nameData = fileName.data(using: .utf8) {
appendStandardTLV(.fileName, nameData)
}
// FILE_SIZE (4 bytes, UInt32 BE) - v2 spec
out.append(TLVType.fileSize.rawValue)
out.append(UInt8(0)) // Length high byte = 0
out.append(UInt8(4)) // Length low byte = 4
let size32 = UInt32(min(fileSize, UInt32.max))
out.append(UInt8((size32 >> 24) & 0xFF))
out.append(UInt8((size32 >> 16) & 0xFF))
out.append(UInt8((size32 >> 8) & 0xFF))
out.append(UInt8(size32 & 0xFF))
// MIME_TYPE
if let mimeData = mimeType.data(using: .utf8) { appendTLV(.mimeType, mimeData) }
// CONTENT
appendTLV(.content, content)
if let mimeData = mimeType.data(using: .utf8) {
appendStandardTLV(.mimeType, mimeData)
}
// CONTENT (4-byte length) - v2 spec
out.append(TLVType.content.rawValue)
let contentLen = UInt32(content.count)
out.append(UInt8((contentLen >> 24) & 0xFF))
out.append(UInt8((contentLen >> 16) & 0xFF))
out.append(UInt8((contentLen >> 8) & 0xFF))
out.append(UInt8(contentLen & 0xFF))
out.append(content)
return out
}
@@ -56,37 +78,55 @@ struct BitchatFilePacket {
guard let d = read(2) else { return nil }
return (UInt16(d[0]) << 8) | UInt16(d[1])
}
func read32() -> UInt32? {
guard let d = read(4) else { return nil }
return (UInt32(d[0]) << 24) | (UInt32(d[1]) << 16) | (UInt32(d[2]) << 8) | UInt32(d[3])
}
var name: String?
var size: UInt64?
var size: UInt32?
var mime: String?
var content = Data()
var contentData = Data()
while idx < data.count {
guard let t = read8(), let tlvType = TLVType(rawValue: t), let len = read16() else { return nil }
let l = Int(len)
guard let val = read(l) else { return nil }
guard let t = read8(), let tlvType = TLVType(rawValue: t) else { return nil }
// CONTENT uses 4-byte length; others use 2-byte length
let len: Int
if tlvType == .content {
guard let len32 = read32() else { return nil }
len = Int(len32)
} else {
guard let len16 = read16() else { return nil }
len = Int(len16)
}
guard len >= 0, idx + len <= data.count else { return nil }
let val = data.subdata(in: idx..<(idx+len))
idx += len
switch tlvType {
case .fileName:
name = String(data: val, encoding: .utf8)
case .fileSize:
guard l == 8 else { return nil }
var s: UInt64 = 0
for b in val { s = (s << 8) | UInt64(b) }
guard len == 4 else { return nil } // v2 spec: 4 bytes
var s: UInt32 = 0
for b in val { s = (s << 8) | UInt32(b) }
size = s
case .mimeType:
mime = String(data: val, encoding: .utf8)
case .content:
content = val
// Expect single CONTENT TLV; if multiple, concatenate defensively
contentData.append(val)
}
}
// Validate
guard !content.isEmpty else { return nil }
let finalSize = size ?? UInt64(content.count)
guard !contentData.isEmpty else { return nil }
let finalSize = size ?? UInt32(contentData.count)
let finalName = name ?? "file"
let finalMime = mime ?? "application/octet-stream"
return BitchatFilePacket(fileName: finalName, fileSize: finalSize, mimeType: finalMime, content: content)
return BitchatFilePacket(fileName: finalName, fileSize: finalSize, mimeType: finalMime, content: contentData)
}
}
@@ -25,7 +25,7 @@ extension ChatViewModel {
let name = fileURL.lastPathComponent
let mime = "image/jpeg"
let tlv = BitchatFilePacket(fileName: name, fileSize: UInt64(data.count), mimeType: mime, content: data)
let tlv = BitchatFilePacket(fileName: name, fileSize: UInt32(data.count), mimeType: mime, content: data)
guard let payload = tlv.encode() else { return }
let transferId = payload.sha256Hex()
+3 -97
View File
@@ -3771,7 +3771,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
@MainActor
private func peerColor(for message: BitchatMessage, isDark: Bool) -> Color {
func peerColor(for message: BitchatMessage, isDark: Bool) -> Color {
if let spid = message.senderPeerID {
if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") {
let bare: String = {
@@ -6060,7 +6060,7 @@ private func checkForMentions(_ message: BitchatMessage) {
guard let data = try? Data(contentsOf: fileURL) else { return }
let name = fileURL.lastPathComponent
let mime = "audio/mp4"
let tlv = BitchatFilePacket(fileName: name, fileSize: UInt64(data.count), mimeType: mime, content: data)
let tlv = BitchatFilePacket(fileName: name, fileSize: UInt32(data.count), mimeType: mime, content: data)
guard let payload = tlv.encode() else { return }
let transferId = payload.sha256Hex()
@@ -6105,101 +6105,7 @@ private func checkForMentions(_ message: BitchatMessage) {
}
}
// MARK: - Images (Send)
#if os(iOS)
@MainActor
func sendImage(_ image: UIImage) {
// Downscale and encode JPEG
guard let data = downscaleJPEG(image, maxDimension: 512, quality: 0.85) else { return }
// Persist to app files (outgoing)
guard let fileURL = saveOutgoingImage(data: data) else { return }
let name = fileURL.lastPathComponent
let mime = "image/jpeg"
let tlv = BitchatFilePacket(fileName: name, fileSize: UInt64(data.count), mimeType: mime, content: data)
guard let payload = tlv.encode() else { return }
let transferId = payload.sha256Hex()
let contentMarker = "[image] \(fileURL.path)"
let now = Date()
if let peer = selectedPrivateChatPeer {
let messageID = UUID().uuidString
let msg = BitchatMessage(
id: messageID,
sender: nickname,
content: contentMarker,
timestamp: now,
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: meshService.peerNickname(peerID: peer),
senderPeerID: meshService.myPeerID,
mentions: nil,
deliveryStatus: .sending
)
appendToPrivateChat(peerID: peer, message: msg)
meshService.sendFileTransferTLV(payload, recipientPeerID: peer, transferId: transferId, messageID: messageID)
} else {
let messageID = UUID().uuidString
let msg = BitchatMessage(
id: messageID,
sender: nickname,
content: contentMarker,
timestamp: now,
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: meshService.myPeerID,
mentions: nil,
deliveryStatus: .sending
)
publicBuffer.append(msg)
schedulePublicFlush()
meshService.sendFileTransferTLV(payload, recipientPeerID: nil, transferId: transferId, messageID: messageID)
}
}
#endif
#if os(iOS)
private func downscaleJPEG(_ image: UIImage, maxDimension: CGFloat, quality: CGFloat) -> Data? {
let size = image.size
guard size.width > 0 && size.height > 0 else { return image.jpegData(compressionQuality: quality) }
let maxSide = max(size.width, size.height)
let scale = min(1.0, maxDimension / maxSide)
let target = CGSize(width: floor(size.width * scale), height: floor(size.height * scale))
let rendererFormat = UIGraphicsImageRendererFormat.default()
rendererFormat.scale = 1
let renderer = UIGraphicsImageRenderer(size: target, format: rendererFormat)
let scaled = renderer.image { ctx in
UIColor.clear.setFill()
ctx.fill(CGRect(origin: .zero, size: target))
image.draw(in: CGRect(origin: .zero, size: target))
}
return scaled.jpegData(compressionQuality: quality)
}
private func saveOutgoingImage(data: Data) -> URL? {
do {
let fm = FileManager.default
let base = try fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let folder = base.appendingPathComponent("bitchat_images/outgoing", isDirectory: true)
if !fm.fileExists(atPath: folder.path) {
try fm.createDirectory(at: folder, withIntermediateDirectories: true)
}
let ts = Int(Date().timeIntervalSince1970)
let fileURL = folder.appendingPathComponent("img_\(ts)_\(UUID().uuidString.prefix(8)).jpg")
try data.write(to: fileURL, options: .atomic)
return fileURL
} catch {
SecureLogger.error("❌ Failed to save outgoing image: \(error)", category: .session)
return nil
}
}
#endif
// Images code moved to ChatViewModel+Images.swift
@MainActor
private func appendToPrivateChat(peerID: String, message: BitchatMessage) {
+8 -32
View File
@@ -328,6 +328,9 @@ struct ContentView: View {
let isImage = message.content.hasPrefix("[image] ")
if isVoice {
// Header line above media: sender + timestamp
MessageHeaderLine(message: message)
.environmentObject(viewModel)
// Hide the raw marker text; render custom voice view
HStack(alignment: .center, spacing: 8) {
let path = String(message.content.dropFirst("[voice] ".count))
@@ -349,40 +352,13 @@ struct ContentView: View {
.padding(.leading, 4)
}
} else if isImage {
// Header line above image: sender + timestamp
MessageHeaderLine(message: message)
.environmentObject(viewModel)
// Hide the raw marker text; render image view
let path = String(message.content.dropFirst("[image] ".count))
#if os(iOS)
if let uiImage = UIImage(contentsOfFile: path) {
VStack(alignment: .leading, spacing: 4) {
// Image with rounded corners and left alignment
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 200, maxHeight: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 2)
// Delivery status for our own private images
if message.isPrivate && message.sender == viewModel.nickname,
let status = message.deliveryStatus {
DeliveryStatusView(status: status, colorScheme: colorScheme)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
} else {
// Fallback for invalid image files
Text("⚠️ Image unavailable")
.font(.system(size: 14, design: .monospaced))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
#else
// macOS: show path for now since NSImage loading is different
Text("📷 Image: \(path)")
.font(.system(size: 14, design: .monospaced))
.foregroundColor(textColor)
.frame(maxWidth: .infinity, alignment: .leading)
#endif
ImageMessageRow(path: path, message: message)
.environmentObject(viewModel)
} else {
HStack(alignment: .top, spacing: 0) {
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuTokens.isEmpty