mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-26 11:05:22 +00:00
refactor
This commit is contained in:
@@ -4,6 +4,7 @@ package com.bitchat.android.mesh
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -37,8 +38,8 @@ class PacketRelayManagerTest {
|
||||
private fun createPacket(route: List<ByteArray>?, recipient: String? = null): BitchatPacket {
|
||||
return BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = hexStringToPeerBytes(otherPeerID),
|
||||
recipientID = recipient?.let { hexStringToPeerBytes(it) },
|
||||
senderID = PeerId.toBytes(otherPeerID),
|
||||
recipientID = recipient?.let { PeerId.toBytes(it) },
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = "hello".toByteArray(),
|
||||
ttl = 5u,
|
||||
@@ -49,8 +50,8 @@ class PacketRelayManagerTest {
|
||||
@Test
|
||||
fun `packet with duplicate hops is dropped`() = runTest {
|
||||
val route = listOf(
|
||||
hexStringToPeerBytes(nextHopPeerID),
|
||||
hexStringToPeerBytes(nextHopPeerID)
|
||||
PeerId.toBytes(nextHopPeerID),
|
||||
PeerId.toBytes(nextHopPeerID)
|
||||
)
|
||||
val packet = createPacket(route)
|
||||
val routedPacket = RoutedPacket(packet, otherPeerID)
|
||||
@@ -64,8 +65,8 @@ class PacketRelayManagerTest {
|
||||
@Test
|
||||
fun `valid source-routed packet is relayed to next hop`() = runTest {
|
||||
val route = listOf(
|
||||
hexStringToPeerBytes(myPeerID),
|
||||
hexStringToPeerBytes(nextHopPeerID)
|
||||
PeerId.toBytes(myPeerID),
|
||||
PeerId.toBytes(nextHopPeerID)
|
||||
)
|
||||
val packet = createPacket(route, finalRecipientID)
|
||||
val routedPacket = RoutedPacket(packet, otherPeerID)
|
||||
@@ -80,7 +81,7 @@ class PacketRelayManagerTest {
|
||||
@Test
|
||||
fun `last hop does not relay further`() = runTest {
|
||||
val route = listOf(
|
||||
hexStringToPeerBytes(myPeerID)
|
||||
PeerId.toBytes(myPeerID)
|
||||
)
|
||||
val packet = createPacket(route, finalRecipientID)
|
||||
val routedPacket = RoutedPacket(packet, otherPeerID)
|
||||
@@ -103,15 +104,4 @@ class PacketRelayManagerTest {
|
||||
verify(delegate).broadcastPacket(any())
|
||||
}
|
||||
|
||||
private fun hexStringToPeerBytes(hex: String): ByteArray {
|
||||
val result = ByteArray(8)
|
||||
var idx = 0
|
||||
var out = 0
|
||||
while (idx + 1 < hex.length && out < 8) {
|
||||
val b = hex.substring(idx, idx + 2).toIntOrNull(16)?.toByte() ?: 0
|
||||
result[out++] = b
|
||||
idx += 2
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.services.meshgraph.GossipTLV
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class TlvPacketRegressionTest {
|
||||
|
||||
@Test
|
||||
fun `identity announcement skips unknown tlv fields`() {
|
||||
val noiseKey = ByteArray(32) { 0x11 }
|
||||
val signingKey = ByteArray(32) { 0x22 }
|
||||
val encoded = TlvWriter()
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x01, "alice".toByteArray(), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x02, noiseKey, TlvLengthSize.ONE_BYTE)
|
||||
.put(0x03, signingKey, TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
|
||||
val decoded = IdentityAnnouncement.decode(encoded)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals("alice", decoded!!.nickname)
|
||||
assertArrayEquals(noiseKey, decoded.noisePublicKey)
|
||||
assertArrayEquals(signingKey, decoded.signingPublicKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `private message rejects unknown tlv fields`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x00, "msg-1".toByteArray(), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x01, "hello".toByteArray(), TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
|
||||
assertNull(PrivateMessagePacket.decode(encoded))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request sync skips unknown tlv fields and enforces payload cap`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x01, byteArrayOf(4), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x02, byteArrayOf(0, 0, 0, 16), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, byteArrayOf(1, 2, 3), TlvLengthSize.TWO_BYTES)
|
||||
.toByteArray()
|
||||
val tooLarge = TlvWriter()
|
||||
.put(0x01, byteArrayOf(4), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x02, byteArrayOf(0, 0, 0, 16), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, ByteArray(RequestSyncPacket.MAX_ACCEPT_FILTER_BYTES + 1), TlvLengthSize.TWO_BYTES)
|
||||
.toByteArray()
|
||||
|
||||
val decoded = RequestSyncPacket.decode(encoded)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals(4, decoded!!.p)
|
||||
assertEquals(16L, decoded.m)
|
||||
assertArrayEquals(byteArrayOf(1, 2, 3), decoded.data)
|
||||
assertNull(RequestSyncPacket.decode(tooLarge))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `file packet uses four byte content length for large payloads`() {
|
||||
val content = ByteArray(70_000) { (it % 251).toByte() }
|
||||
val packet = BitchatFilePacket(
|
||||
fileName = "large.bin",
|
||||
fileSize = content.size.toLong(),
|
||||
mimeType = "application/octet-stream",
|
||||
content = content
|
||||
)
|
||||
|
||||
val encoded = packet.encode()
|
||||
val decoded = BitchatFilePacket.decode(encoded!!)
|
||||
val contentTypeOffset = encoded.indexOf(0x04.toByte())
|
||||
val contentLength =
|
||||
((encoded[contentTypeOffset + 1].toInt() and 0xFF) shl 24) or
|
||||
((encoded[contentTypeOffset + 2].toInt() and 0xFF) shl 16) or
|
||||
((encoded[contentTypeOffset + 3].toInt() and 0xFF) shl 8) or
|
||||
(encoded[contentTypeOffset + 4].toInt() and 0xFF)
|
||||
|
||||
assertEquals(content.size, contentLength)
|
||||
assertNotNull(decoded)
|
||||
assertArrayEquals(content, decoded!!.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `file packet rejects unknown tlv fields`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x01, "x.bin".toByteArray(), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x02, byteArrayOf(0, 0, 0, 1), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, "application/octet-stream".toByteArray(), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x04, byteArrayOf(1), TlvLengthSize.FOUR_BYTES)
|
||||
.toByteArray()
|
||||
|
||||
assertNull(BitchatFilePacket.decode(encoded))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `gossip tlv uses shared peer id and tlv helpers`() {
|
||||
val encoded = GossipTLV.encodeNeighbors(listOf("0102030405060708", "aabbcc"))
|
||||
|
||||
assertEquals(listOf("0102030405060708", "aabbcc0000000000"), GossipTLV.decodeNeighborsFromAnnouncementPayload(encoded))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.bitchat.android.protocol
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class TlvTest {
|
||||
|
||||
@Test
|
||||
fun `writer and reader support one two and four byte lengths`() {
|
||||
val oneByteValue = byteArrayOf(0x7f)
|
||||
val twoByteValue = ByteArray(256) { it.toByte() }
|
||||
val fourByteValue = ByteArray(70_000) { (it % 251).toByte() }
|
||||
|
||||
val encoded = TlvWriter()
|
||||
.put(0x01, oneByteValue, TlvLengthSize.ONE_BYTE)
|
||||
.put(0x02, twoByteValue, TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, fourByteValue, TlvLengthSize.FOUR_BYTES)
|
||||
.toByteArray()
|
||||
|
||||
val fields = TlvReader.decode(encoded, TlvLengthSize.ONE_BYTE) { type ->
|
||||
when (type) {
|
||||
0x02 -> TlvLengthSize.TWO_BYTES
|
||||
0x03 -> TlvLengthSize.FOUR_BYTES
|
||||
else -> TlvLengthSize.ONE_BYTE
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(3, fields!!.size)
|
||||
assertArrayEquals(oneByteValue, fields[0].value)
|
||||
assertArrayEquals(twoByteValue, fields[1].value)
|
||||
assertArrayEquals(fourByteValue, fields[2].value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown tlv policy skips or fails consistently`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x01, byteArrayOf(1), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x7f, byteArrayOf(9), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x02, byteArrayOf(2), TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
|
||||
val skipped = TlvReader.decode(
|
||||
data = encoded,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes = setOf(0x01, 0x02)
|
||||
)
|
||||
val failed = TlvReader.decode(
|
||||
data = encoded,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.FAIL,
|
||||
knownTypes = setOf(0x01, 0x02)
|
||||
)
|
||||
|
||||
assertEquals(listOf(0x01, 0x02), skipped!!.map { it.type })
|
||||
assertNull(failed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reader rejects truncated values and writer rejects oversized values`() {
|
||||
val truncated = byteArrayOf(0x01, 0x03, 0x41)
|
||||
|
||||
assertNull(TlvReader.decode(truncated, TlvLengthSize.ONE_BYTE))
|
||||
|
||||
try {
|
||||
TlvWriter().put(0x01, ByteArray(256), TlvLengthSize.ONE_BYTE)
|
||||
throw AssertionError("Expected oversized one-byte TLV value to fail")
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Expected.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.bitchat.android.ui.debug
|
||||
|
||||
import android.os.Build
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
|
||||
class DebugSettingsManagerTest {
|
||||
|
||||
private lateinit var manager: DebugSettingsManager
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
manager = DebugSettingsManager.getInstance()
|
||||
manager.resetForTesting()
|
||||
manager.setVerboseLoggingEnabled(true)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
manager.resetForTesting()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logIncoming emits one typed packet trace`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 7u,
|
||||
senderID = "aaaabbbbccccdddd",
|
||||
payload = byteArrayOf(1, 2, 3)
|
||||
)
|
||||
|
||||
manager.logIncoming(
|
||||
packet = packet,
|
||||
fromPeerID = "aaaabbbbccccdddd",
|
||||
fromNickname = "alice",
|
||||
fromDeviceAddress = "AA:BB:CC:DD:EE:FF",
|
||||
myPeerID = "1111222233334444"
|
||||
)
|
||||
|
||||
val packetMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.PacketEvent>()
|
||||
|
||||
assertEquals("Incoming packet should create one packet event", 1, packetMessages.size)
|
||||
assertTrue(packetMessages.single().content.contains("Incoming"))
|
||||
assertTrue(packetMessages.single().content.contains("MESSAGE"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logOutgoing relay emits one relay event without packet duplicate`() {
|
||||
manager.logOutgoing(
|
||||
packetType = "MESSAGE",
|
||||
toPeerID = "bbbbccccddddeeee",
|
||||
toNickname = "bob",
|
||||
toDeviceAddress = "11:22:33:44:55:66",
|
||||
previousHopPeerID = "aaaabbbbccccdddd",
|
||||
packetVersion = 1u,
|
||||
routeInfo = "routed: 2 hops",
|
||||
ttl = 6u
|
||||
)
|
||||
|
||||
val packetMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.PacketEvent>()
|
||||
val relayMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.RelayEvent>()
|
||||
|
||||
assertEquals("Relay send should not also create a packet event", 0, packetMessages.size)
|
||||
assertEquals("Relay send should create one relay event", 1, relayMessages.size)
|
||||
assertTrue(relayMessages.single().content.contains("Relay"))
|
||||
assertTrue(relayMessages.single().content.contains("MESSAGE"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validation failures are hidden when verbose logging is disabled`() {
|
||||
manager.setVerboseLoggingEnabled(false)
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 7u,
|
||||
senderID = "aaaabbbbccccdddd",
|
||||
payload = byteArrayOf(1)
|
||||
)
|
||||
|
||||
manager.logPacketValidationFailure(packet, "aaaabbbbccccdddd", "invalid signature")
|
||||
|
||||
val validationMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.ValidationEvent>()
|
||||
assertEquals(0, validationMessages.size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class HexPeerHashingTest {
|
||||
|
||||
@Test
|
||||
fun `hex encodes lowercase and decodes mixed case`() {
|
||||
val bytes = byteArrayOf(0x00, 0x0f, 0x10, 0x7f, 0xff.toByte())
|
||||
|
||||
assertEquals("000f107fff", Hex.encode(bytes))
|
||||
assertArrayEquals(bytes, Hex.decode("000F107fFF"))
|
||||
assertEquals("000f107fff", bytes.toHexString())
|
||||
assertArrayEquals(bytes, "000f107fff".hexToByteArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hex decode rejects malformed input unless odd length is explicitly allowed`() {
|
||||
assertNull(Hex.decode("abc"))
|
||||
assertArrayEquals(byteArrayOf(0x0a, 0xbc.toByte()), Hex.decode("abc", allowOddLength = true))
|
||||
assertNull(Hex.decode("00xx"))
|
||||
assertNull("00xx".hexToByteArrayOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `peer id parsing is fixed width with truncation and zero fill`() {
|
||||
assertArrayEquals(
|
||||
byteArrayOf(0x11, 0x22, 0x00, 0x44, 0, 0, 0, 0),
|
||||
PeerId.toBytes("1122zz44")
|
||||
)
|
||||
assertArrayEquals(
|
||||
byteArrayOf(0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77),
|
||||
PeerId.toBytes("00112233445566778899")
|
||||
)
|
||||
assertEquals("1122004400000000", PeerId.normalize("1122zz44"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `peer id strict parse requires exactly eight valid bytes`() {
|
||||
val bytes = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7)
|
||||
|
||||
assertArrayEquals(bytes, PeerId.parse("0001020304050607"))
|
||||
assertEquals("0001020304050607", PeerId.fromBytes(bytes))
|
||||
assertNull(PeerId.parse("000102"))
|
||||
assertNull(PeerId.parse("00010203040506xx"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hashing sha256 hex matches known vector`() {
|
||||
assertEquals(
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
||||
Hashing.sha256Hex("abc")
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user