mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
Validate input and correct navigation and sensor services
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user