Validate input and correct navigation and sensor services

This commit is contained in:
Iscle authored and Seb3thehacker committed 2026-10-21 12:30:38 -07:00
1 parent c04c35b5cd
commit 6efdad5e53
11 files changed
+355 -138

No files matched your search

@@ -1,5 +1,6 @@
package app.seb3thehacker.gearslip
import android.view.Surface
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
@@ -229,4 +230,27 @@ class ConnectionLifecycleTest {
GearslipRunner::class.java.getDeclaredField(name).apply { isAccessible = true }.set(runner, value)
}
@Test fun `touchpad reports are not injected as touchscreen taps`() {
var touches = 0
val projection = object : Projection {
override fun onSurfaceReady(surface: Surface, width: Int, height: Int, densityDpi: Int) = Unit
override fun onTouch(action: Int, actionIndex: Int, points: List<TouchPoint>) { touches++ }
override fun onProjectionStopped() = Unit
}
val runner = GearslipRunner(ByteArrayInputStream(byteArrayOf()), ByteArrayOutputStream(), ::identity, projection)
val input = GearslipRunner::class.java.getDeclaredMethod("onInputReport", ByteArray::class.java)
.apply { isAccessible = true }
val touch = Protobuf.bytesField(1, Protobuf.varintField(1, 100) +
Protobuf.varintField(2, 200) + Protobuf.varintField(3, 0))
try {
input.invoke(runner, Protobuf.varintField(1, 1000) + Protobuf.bytesField(7, touch))
assertEquals(0, touches)
input.invoke(runner, Protobuf.varintField(1, 1000) + Protobuf.bytesField(3, touch))
assertEquals(1, touches)
} finally {
runner.stop()
}
}
}
@@ -0,0 +1,36 @@
package app.seb3thehacker.gearslip
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class ServiceDiscoveryChannelTest {
private fun service(id: Long, field: Int, payload: ByteArray = byteArrayOf()) =
Protobuf.bytesField(1, Protobuf.varintField(1, id) + Protobuf.bytesField(field, payload))
@Test fun `invalid channel IDs cannot alias the control or another service channel`() {
val config = Protobuf.varintField(1, 16000) + Protobuf.varintField(2, 16) + Protobuf.varintField(3, 1)
val audio = Protobuf.bytesField(3, config)
val microphone = Protobuf.bytesField(2, config)
val sensor = Protobuf.bytesField(1, Protobuf.varintField(1, 1))
for (id in listOf(-1L, 0L, 256L, 0x1_0000_0001L)) {
assertNull(ServiceDiscovery.findMediaStatusChannel(service(id, 9)))
assertNull(ServiceDiscovery.findNavStatusService(service(id, 8, Protobuf.varintField(1, 1000))))
assertNull(ServiceDiscovery.findVideoService(service(id, 3, Protobuf.bytesField(4, byteArrayOf()))))
assertNull(ServiceDiscovery.findInputService(service(id, 4)))
assertNull(ServiceDiscovery.findMicService(service(id, 5, microphone)))
assertNull(ServiceDiscovery.findSensorService(service(id, 2, sensor)))
assertTrue(ServiceDiscovery.findAudioServices(service(id, 3, audio)).isEmpty())
}
assertEquals(255, ServiceDiscovery.findMediaStatusChannel(service(255, 9)))
}
@Test fun `navigation discovery preserves the requested update interval`() {
val navigation = requireNotNull(ServiceDiscovery.findNavStatusService(service(6, 8, Protobuf.varintField(1, 1500))))
assertEquals(6, navigation.serviceId)
assertEquals(1500, navigation.minimumIntervalMs)
assertNull(ServiceDiscovery.findNavStatusService(service(6, 8)))
assertNull(ServiceDiscovery.findNavStatusService(service(6, 8, Protobuf.varintField(1, -1))))
}
}
@@ -0,0 +1,61 @@
package app.seb3thehacker.gearslip.car
import app.seb3thehacker.gearslip.Protobuf
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ClusterNavTest {
@Test fun `head unit minimum interval limits repeated navigation updates`() {
val first = CountDownLatch(1)
val next = CountDownLatch(1)
val count = AtomicInteger()
val nav = ClusterNav(6, 1000) { _, _ ->
if (count.incrementAndGet() == 1) first.countDown() else next.countDown()
}
try {
nav.onMessage(8, Protobuf.varintField(1, 0))
assertTrue(first.await(2, TimeUnit.SECONDS))
nav.onMessage(32769, byteArrayOf())
assertFalse("the fixed 500ms timer must not override the head unit", next.await(700, TimeUnit.MILLISECONDS))
assertTrue(next.await(2, TimeUnit.SECONDS))
} finally {
nav.stop()
}
}
@Test fun `head unit stop pauses updates and start resends current status`() {
val messages = mutableListOf<Int>()
val nav = ClusterNav(6) { id, _ -> messages += id }
// Drive polling synchronously so the test does not depend on a background timer.
val running = ClusterNav::class.java.getDeclaredField("running").apply { isAccessible = true }
val tick = ClusterNav::class.java.getDeclaredMethod("tick").apply { isAccessible = true }
running.set(nav, true)
try {
tick.invoke(nav)
assertEquals(listOf(32771), messages)
nav.onMessage(32770, byteArrayOf())
tick.invoke(nav)
assertEquals(1, messages.size)
nav.onMessage(32769, byteArrayOf())
tick.invoke(nav)
assertEquals(listOf(32771, 32771), messages)
nav.stop()
tick.invoke(nav)
assertEquals(2, messages.size)
} finally {
nav.stop()
}
}
@Test fun `missing channel open status does not start publishing`() {
val nav = ClusterNav(6) { _, _ -> error("channel was not accepted") }
nav.onMessage(8, byteArrayOf())
val running = ClusterNav::class.java.getDeclaredField("running").apply { isAccessible = true }
assertFalse(running.getBoolean(nav))
}
}
@@ -0,0 +1,52 @@
package app.seb3thehacker.gearslip.protocol
import app.seb3thehacker.gearslip.Protobuf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class InputProtocolTest {
private fun pointer(id: Int) = Protobuf.bytesField(1,
Protobuf.varintField(1, 100) + Protobuf.varintField(2, 200) + Protobuf.varintField(3, id.toLong()))
@Test fun `multitouch preserves pointer IDs and action index`() {
val body = pointer(7) + pointer(3) + Protobuf.varintField(2, 1) + Protobuf.varintField(3, 5)
val touch = requireNotNull(InputProtocol.touch(body, 800 to 480))
assertEquals(5, touch.action)
assertEquals(1, touch.actionIndex)
assertEquals(listOf(7, 3), touch.points.map { it.id })
assertEquals(100f, touch.points[1].x)
}
@Test fun `invalid pointer index is rejected instead of targeting a different finger`() {
assertNull(InputProtocol.touch(pointer(0) + Protobuf.varintField(2, 1), null))
assertNull(InputProtocol.touch(pointer(0) + Protobuf.varintField(2, -1), null))
}
@Test fun `missing required pointer fields and duplicate IDs are rejected`() {
assertNull(InputProtocol.touch(Protobuf.bytesField(1, Protobuf.varintField(1, 20)), null))
assertNull(InputProtocol.touch(pointer(1) + pointer(1), null))
}
@Test fun `pointer IDs must fit the Android motion event range`() {
for (id in listOf(-1L, 32L, 0x1_0000_0000L)) {
val body = Protobuf.bytesField(1, Protobuf.varintField(1, 100) +
Protobuf.varintField(2, 200) + Protobuf.varintField(3, id))
assertNull(InputProtocol.touch(body, null))
}
assertEquals(31, requireNotNull(InputProtocol.touch(pointer(31), null)).points.single().id)
}
@Test fun `oversized pointer sets are rejected before constructing motion events`() {
val body = (0..32).fold(byteArrayOf()) { bytes, id -> bytes + pointer(id) }
assertNull(InputProtocol.touch(body, null))
}
@Test fun `optional touch fields use down and first pointer defaults`() {
val touch = requireNotNull(InputProtocol.touch(pointer(0), 50 to 60))
assertEquals(0, touch.action)
assertEquals(0, touch.actionIndex)
assertEquals(49f, touch.points.single().x)
assertEquals(59f, touch.points.single().y)
}
}