Reassemble split file content in linear time

decode tolerates a CONTENT TLV split across several TLVs by appending
each chunk with `contentBytes + value`. That reallocates and copies the
whole accumulator every time, so k chunks totalling N bytes cost O(k*N).

The split is the sender's choice, and a CONTENT TLV costs only five
header bytes, so one content byte per chunk maximises the chunk count.
The decoder runs on the mesh handler for any FILE_TRANSFER from a
verified peer — verification binds an identity, it does not authorise
anything, and a peer mints one by signing its own announcement.

Measured on a ~1.2 MB payload split into 200k single-byte chunks, which
is the scale BLE reassembly already admits: 2435ms of copying, repeatable
for the cost of sending the packet again. Collecting the chunks and
joining once brings the same payload to 65ms.

The surrounding decoder was already hardened against exactly this shape
for unknown tags, which are skipped without copying and logged once
rather than per TLV. This closes the same hole on the tag that carries
the bytes.
This commit is contained in:
Taksh 2026-08-16 08:43:12 +05:30
parent 5156f7de89
commit 98a52269e3

View File

@ -104,7 +104,9 @@ data class BitchatFilePacket(
var name: String? = null
var size: Long? = null
var mime: String? = null
var contentBytes: ByteArray? = null
val contentChunks = ArrayList<ByteArray>(1)
var contentLength = 0
var sawContent = false
var skippedUnknownTLVs = 0
while (off < data.size) {
// Every TLV needs at least a type and a 2-byte length.
@ -150,10 +152,18 @@ data class BitchatFilePacket(
}
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)
// Expect a single CONTENT TLV, but tolerate a split one.
//
// `contentBytes + value` reallocates and copies the whole
// accumulator per chunk, so k chunks totalling N bytes cost
// O(k*N). The chunk count is the sender's to choose — a 6-byte
// TLV carries one content byte — so at the 1 MB BLE reassembly
// ceiling one packet buys ~10^10 byte copies on the mesh handler
// thread. Collect the chunks and join once instead.
sawContent = true
if (value.isNotEmpty()) {
contentChunks.add(value)
contentLength += value.size
}
}
}
@ -162,7 +172,18 @@ data class BitchatFilePacket(
android.util.Log.d("BitchatFilePacket", "⏭️ Skipped $skippedUnknownTLVs unknown TLV(s)")
}
val n = name ?: return null
val c = contentBytes ?: return null
if (!sawContent) return null
val c = when (contentChunks.size) {
0 -> ByteArray(0)
1 -> contentChunks[0]
else -> ByteArray(contentLength).also { joined ->
var at = 0
for (chunk in contentChunks) {
System.arraycopy(chunk, 0, joined, at, chunk.size)
at += chunk.size
}
}
}
val s = size ?: c.size.toLong()
val m = mime ?: "application/octet-stream"
val result = BitchatFilePacket(n, s, m, c)