Files
bitchat-android/app/src/main/java/com/bitchat/android/model/BitchatFilePacket.kt
T
2026-06-10 14:14:28 +02:00

105 lines
4.1 KiB
Kotlin

package com.bitchat.android.model
import com.bitchat.android.protocol.TlvLengthSize
import com.bitchat.android.protocol.TlvReader
import com.bitchat.android.protocol.TlvWriter
import com.bitchat.android.protocol.UnknownTlvPolicy
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* BitchatFilePacket: TLV-encoded file transfer payload for BLE mesh.
* TLVs:
* - 0x01: filename (UTF-8)
* - 0x02: file size (4 bytes, UInt32)
* - 0x03: mime type (UTF-8)
* - 0x04: content (bytes)
*
* FILE_NAME, FILE_SIZE, and MIME_TYPE use 2-byte big-endian lengths.
* CONTENT uses a 4-byte big-endian length so the outer v2 packet can carry
* payloads larger than 64 KiB before transport fragmentation.
*/
data class BitchatFilePacket(
val fileName: String,
val fileSize: Long,
val mimeType: String,
val content: ByteArray
) {
private enum class TLVType(val v: UByte) {
FILE_NAME(0x01u), FILE_SIZE(0x02u), MIME_TYPE(0x03u), CONTENT(0x04u);
companion object { fun from(value: UByte) = values().find { it.v == value } }
}
fun encode(): ByteArray? {
return try {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val mimeBytes = mimeType.toByteArray(Charsets.UTF_8)
if (nameBytes.size > 0xFFFF || mimeBytes.size > 0xFFFF) {
return null
}
val sizeBytes = ByteBuffer.allocate(4)
.order(ByteOrder.BIG_ENDIAN)
.putInt(fileSize.toInt())
.array()
TlvWriter()
.put(TLVType.FILE_NAME.v.toInt(), nameBytes, TlvLengthSize.TWO_BYTES)
.put(TLVType.FILE_SIZE.v.toInt(), sizeBytes, TlvLengthSize.TWO_BYTES)
.put(TLVType.MIME_TYPE.v.toInt(), mimeBytes, TlvLengthSize.TWO_BYTES)
.put(TLVType.CONTENT.v.toInt(), content, TlvLengthSize.FOUR_BYTES)
.toByteArray()
} catch (e: Exception) {
null
}
}
companion object {
fun decode(data: ByteArray): BitchatFilePacket? {
try {
val knownTypes = TLVType.values().map { it.v.toInt() }.toSet()
val fields = TlvReader.decode(
data = data,
defaultLengthSize = TlvLengthSize.TWO_BYTES,
unknownPolicy = UnknownTlvPolicy.FAIL,
knownTypes = knownTypes
) { type ->
if (type == TLVType.CONTENT.v.toInt()) TlvLengthSize.FOUR_BYTES else TlvLengthSize.TWO_BYTES
} ?: return null
var name: String? = null
var size: Long? = null
var mime: String? = null
var contentBytes: ByteArray? = null
for (field in fields) {
val t = TLVType.from(field.type.toUByte()) ?: return null
val value = field.value
when (t) {
TLVType.FILE_NAME -> name = String(value, Charsets.UTF_8)
TLVType.FILE_SIZE -> {
if (value.size != 4) return null
val bb = ByteBuffer.wrap(value).order(ByteOrder.BIG_ENDIAN)
size = bb.int.toLong()
}
TLVType.MIME_TYPE -> mime = String(value, Charsets.UTF_8)
TLVType.CONTENT -> {
// Expect a single CONTENT TLV
if (contentBytes == null) contentBytes = value else {
// If multiple CONTENT TLVs appear, concatenate for tolerance
contentBytes = (contentBytes!! + value)
}
}
}
}
val n = name ?: return null
val c = contentBytes ?: return null
val s = size ?: c.size.toLong()
val m = mime ?: "application/octet-stream"
return BitchatFilePacket(n, s, m, c)
} catch (e: Exception) {
return null
}
}
}
}