From 8595094410fbc1b25aea6b5509847e7fa5e565bf Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 5 Sep 2026 22:31:17 +0530 Subject: [PATCH] fix(sidecar): match a field to its xml node by class as well as id and bounds A wrapper drawn to the same bounds as the untagged field inside it shared the field's key, both were dropped as ambiguous, and every value typed into an untagged field was redacted. The class tells them apart. --- .../dev/sanderling/sidecar/DriverBackend.kt | 7 +++- .../dev/sanderling/sidecar/SecureFactsTest.kt | 38 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index dcd1ad5..fbf9586 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -954,7 +954,7 @@ internal fun treeWithoutKeyboard( // cannot tell a password field from a search box and everything typed anywhere // gets recorded as a credential. The XML the same read produced does carry the // fact, so it is fetched (lazily: a screen with no text field never pays for -// it) and matched back onto the fields by identity and bounds. +// it) and matched back onto the fields by identity, class and bounds. // // A field the XML cannot be matched to is left unstated rather than guessed. // Unstated reads as "may be a credential" downstream, which is the safe way to @@ -976,6 +976,7 @@ internal fun withSecureFacts( for (field in fields) { val key = secureFactKey( nodeAttribute(field, "resource-id"), + nodeAttribute(field, "class"), nodeAttribute(field, "bounds"), ) field.put("secure", facts[key] ?: continue) @@ -1002,6 +1003,7 @@ internal fun secureFactsFromXml(xml: String): Map { val element = nodes.item(index) as? org.w3c.dom.Element ?: continue val key = secureFactKey( element.getAttribute("resource-id"), + element.getAttribute("class"), element.getAttribute("bounds"), ) val password = element.getAttribute("password") == "true" @@ -1011,7 +1013,8 @@ internal fun secureFactsFromXml(xml: String): Map { return facts } -private fun secureFactKey(id: String, bounds: String) = "$id@$bounds" +private fun secureFactKey(id: String, className: String, bounds: String) = + "$id@$className@$bounds" // A text field is what the Go side calls editable off the same two attributes // (internal/hierarchy): stating the fact on a narrower set would leave fields diff --git a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt index 46063d4..f828d53 100644 --- a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt +++ b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt @@ -27,22 +27,21 @@ private const val LOGIN_XML = """ """ private fun secureOf(tree: String, resourceId: String): Boolean? { - val field = jacksonTree(tree, resourceId) ?: return null + val field = jacksonTree(tree, "resource-id", resourceId) ?: return null val secure = field.get("secure") ?: return null return secure.asBoolean() } private fun jacksonTree( tree: String, - resourceId: String, + attribute: String, + value: String, ): com.fasterxml.jackson.databind.JsonNode? { val mapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper() fun walk( node: com.fasterxml.jackson.databind.JsonNode, ): com.fasterxml.jackson.databind.JsonNode? { - if (node.get("attributes")?.get("resource-id")?.asText() == - resourceId - ) { + if (node.get("attributes")?.get(attribute)?.asText() == value) { return node } node.get("children")?.forEach { child -> @@ -111,6 +110,35 @@ class SecureFactsTest { } } + // A wrapper drawn to the same bounds as the untagged field inside it shares + // the field's empty id and its bounds. The class is what still tells them + // apart; without it the two collide, the field answers for nothing, and + // every value typed into an untagged field is redacted. + @Test fun anUntaggedFieldInsideAWrapperOfItsOwnSizeIsStillStated() { + val wrapped = """ + {"attributes":{"bounds":"[0,0,1080,2340]"},"children":[ + {"attributes":{"class":"android.view.View","bounds":"[51,249][429,321]"},"children":[ + {"attributes":{"class":"android.widget.EditText","bounds":"[51,249][429,321]"},"children":[]} + ]} + ]} + """ + val xml = """ + + + + + + """ + + val field = jacksonTree( + withSecureFacts(wrapped) { xml }, + "class", + "android.widget.EditText", + ) + + assertEquals(true, field?.get("secure")?.asBoolean()) + } + // Two nodes sharing a key answer for neither: taking the first would state // "not secure" about a field that may be the other one. @Test fun anAmbiguousMatchIsLeftUnstated() {