mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-10-25 07:40:36 +00:00
Cover split-content reassembly
Pins ordering across chunks, the single-TLV round trip, an empty CONTENT TLV, rejection when CONTENT is absent, and correct reassembly at 200k chunks. Deliberately no wall-clock assertion: a threshold wide enough to be stable on slow CI is too wide to fail on the quadratic path, so it would only add flake. The 2435ms/65ms comparison was measured directly and is in the pull request instead.
This commit is contained in:
parent
98a52269e3
commit
61fa6ee9cb
@ -0,0 +1,108 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* A FILE_TRANSFER payload arrives from any verified peer, and the sender chooses how many CONTENT
|
||||
* TLVs it is split into. Reassembly therefore has to be linear in the payload, not in the chunk
|
||||
* count times the payload.
|
||||
*/
|
||||
class BitchatFilePacketContentTest {
|
||||
|
||||
private fun shortTlv(tag: Int, value: ByteArray): ByteArray =
|
||||
byteArrayOf(
|
||||
tag.toByte(),
|
||||
((value.size ushr 8) and 0xFF).toByte(),
|
||||
(value.size and 0xFF).toByte()
|
||||
) + value
|
||||
|
||||
/** CONTENT carries a 4-byte length, unlike the other tags. */
|
||||
private fun contentTlv(value: ByteArray): ByteArray =
|
||||
byteArrayOf(
|
||||
0x04,
|
||||
((value.size ushr 24) and 0xFF).toByte(),
|
||||
((value.size ushr 16) and 0xFF).toByte(),
|
||||
((value.size ushr 8) and 0xFF).toByte(),
|
||||
(value.size and 0xFF).toByte()
|
||||
) + value
|
||||
|
||||
private fun payload(contentChunks: List<ByteArray>): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(shortTlv(0x01, "photo.jpg".toByteArray()))
|
||||
out.write(shortTlv(0x03, "image/jpeg".toByteArray()))
|
||||
contentChunks.forEach { out.write(contentTlv(it)) }
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a split content tlv is reassembled in order`() {
|
||||
val chunks = listOf("abc".toByteArray(), "de".toByteArray(), "fgh".toByteArray())
|
||||
|
||||
val decoded = BitchatFilePacket.decode(payload(chunks))
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertArrayEquals("abcdefgh".toByteArray(), decoded!!.content)
|
||||
assertEquals("photo.jpg", decoded.fileName)
|
||||
assertEquals("image/jpeg", decoded.mimeType)
|
||||
// No FILE_SIZE TLV, so the size falls back to the reassembled length.
|
||||
assertEquals(8L, decoded.fileSize)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a single content tlv round trips through encode`() {
|
||||
val original = BitchatFilePacket(
|
||||
fileName = "note.txt",
|
||||
fileSize = 5L,
|
||||
mimeType = "text/plain",
|
||||
content = "hello".toByteArray()
|
||||
)
|
||||
|
||||
val decoded = BitchatFilePacket.decode(original.encode()!!)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertArrayEquals(original.content, decoded!!.content)
|
||||
assertEquals(original.fileName, decoded.fileName)
|
||||
assertEquals(original.fileSize, decoded.fileSize)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an empty content tlv still decodes to an empty file`() {
|
||||
val decoded = BitchatFilePacket.decode(payload(listOf(ByteArray(0))))
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals(0, decoded!!.content.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a payload with no content tlv is rejected`() {
|
||||
assertNull(BitchatFilePacket.decode(payload(emptyList())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `heavily split content reassembles correctly at scale`() {
|
||||
// A CONTENT TLV costs 5 header bytes, so one content byte per chunk is the sender's
|
||||
// cheapest way to maximise the chunk count. 200k chunks is ~1.2 MB on the wire, the
|
||||
// scale BLE reassembly already lets through.
|
||||
//
|
||||
// No timing assertion here on purpose: a wall-clock threshold wide enough to stay stable
|
||||
// on slow CI is too wide to fail on the quadratic path, so it would only add flake. The
|
||||
// cost was measured directly instead and is recorded in the pull request.
|
||||
val chunkCount = 200_000
|
||||
val data = payload(List(chunkCount) { byteArrayOf(it.toByte()) })
|
||||
|
||||
val decoded = BitchatFilePacket.decode(data)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals(chunkCount, decoded!!.content.size)
|
||||
assertTrue(
|
||||
"every chunk must land at its own offset",
|
||||
decoded.content.withIndex().all { (i, b) -> b == i.toByte() }
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user