From abdfdbe148f3265123bb63d698bcde245b0cfe9a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 1 Aug 2022 20:58:36 +0200 Subject: [PATCH 1/4] Detect minified classes and skip instrumentation to avoid build problems --- .../util/ConstantPoolHelpers.kt | 33 +++++++++++-- .../SentryPluginWithMinifiedLibsTest.kt | 1 + .../util/MinifiedClassDetectionTest.kt | 44 ++++++++++++++++++ .../app/testlib/stripe-problematic.jar | Bin 0 -> 3836 bytes 4 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt create mode 100644 plugin-build/src/test/resources/testFixtures/appTestProject/app/testlib/stripe-problematic.jar diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt index 8e8b2bd7..bbf8a440 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt @@ -44,17 +44,25 @@ internal fun ClassWriter.findClassReader(): ClassReader? { return (classReaderField.get(symbolTable) as? ClassReader) } +internal fun ClassReader.getSimpleClassName(): String { + return className.substringAfterLast("/") +} + /** * Looks at the constant pool entries and searches for R8 markers */ internal fun ClassReader.isMinifiedClass(): Boolean { - val charBuffer = CharArray(maxStringLength) + return isR8Minified(this) || classNameLooksMinified(this.getSimpleClassName(), this.className) +} + +private fun isR8Minified(classReader: ClassReader): Boolean { + val charBuffer = CharArray(classReader.maxStringLength) // R8 marker is usually in the first 3-5 entries, so we limit it at 10 to speed it up // (constant pool size can be huge otherwise) - val poolSize = minOf(10, itemCount) + val poolSize = minOf(10, classReader.itemCount) for (i in 1 until poolSize) { try { - val constantPoolEntry = readConst(i, charBuffer) + val constantPoolEntry = classReader.readConst(i, charBuffer) if (constantPoolEntry is String && "~~R8" in constantPoolEntry) { // ~~R8 is a marker in the class' constant pool, which r8 itself is looking at when // parsing a .class file. See here -> https://r8.googlesource.com/r8/+/refs/heads/main/src/main/java/com/android/tools/r8/dex/Marker.java#53 @@ -68,6 +76,25 @@ internal fun ClassReader.isMinifiedClass(): Boolean { return false } +/** + * See https://github.com/getsentry/sentry-android-gradle-plugin/issues/360 + * and https://github.com/getsentry/sentry-android-gradle-plugin/issues/359#issuecomment-1193782500 + */ +/* ktlint-disable max-line-length */ +private val MINIFIED_CLASSNAME_REGEX = """^((([a-zA-z])\3{1,}([0-9]{1,})?(([a-zA-Z])\6{1,})?)|([a-zA-Z]([0-9])?))(${'\\'}${'$'}(((\w)\12{1,}([0-9]{1,})?(([a-zA-Z])\15{1,})?)|(\w([0-9])?)))*${'$'}""".toRegex() + +/** + * See https://github.com/getsentry/sentry/blob/c943de2afc785083554e7fdfb10c67d0c0de0f98/static/app/components/events/eventEntries.tsx#L57-L58 + */ +private val MINIFIED_CLASSNAME_SENTRY_REGEX = + """^(([\w\${'$'}]\/[\w\${'$'}]{1,2})|([\w\${'$'}]{2}\/[\w\${'$'}]\/[\w\${'$'}]))(\/|${'$'})""".toRegex() +/* ktlint-enable max-line-length */ + +fun classNameLooksMinified(simpleClassName: String, fullClassName: String): Boolean { + return MINIFIED_CLASSNAME_REGEX.matches(simpleClassName) || + MINIFIED_CLASSNAME_SENTRY_REGEX.matches(fullClassName) +} + /** * Gets all fields of the given class and its parents (if any). * diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt index b763a2df..24ec3b8d 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt @@ -23,6 +23,7 @@ class SentryPluginWithMinifiedLibsTest : implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.0' implementation 'com.adcolony:sdk:4.7.1' implementation 'com.appboy:android-sdk-ui:19.0.0' + implementation files("testlib/stripe-problematic.jar") } """.trimIndent() ) diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt new file mode 100644 index 00000000..9d22028c --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt @@ -0,0 +1,44 @@ +package io.sentry.android.gradle.instrumentation.util + +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test + +class MinifiedClassDetectionTest { + + @Test + fun `detects minified class names`() { + val classNames = listOf( + "l0", + """a${'$'}a""", + "ccc017zz", + """ccc017zz${'$'}a""", + "aa", + "aa${'$'}a" + ) + + classNames.forEach { + assertTrue(classNameLooksMinified(it, "com/example/$it"), it) + } + } + + @Test + fun `does not consider non minified classes as minified`() { + val classNames = listOf( + "ConstantPoolHelpers", + "FileUtil", + """FileUtil${"$"}Inner""", + "aa${'$'}ab", + "Id" + ) + + classNames.forEach { + assertFalse(classNameLooksMinified(it, "com/example/$it"), it) + } + } + + @Test + fun `tests that something happens`() { + """^\w(\\${'$'}(\w))*${'$'}""".toRegex().matches("a${'$'}a") + } +} diff --git a/plugin-build/src/test/resources/testFixtures/appTestProject/app/testlib/stripe-problematic.jar b/plugin-build/src/test/resources/testFixtures/appTestProject/app/testlib/stripe-problematic.jar new file mode 100644 index 0000000000000000000000000000000000000000..aa9dd558e40c8f57097bea927b13cf990879d0df GIT binary patch literal 3836 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 KZKM?g`R#vSJI06r literal 0 HcmV?d00001 From 14dd6be460a77ff6f96ff824090d77c6206f817d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 1 Aug 2022 22:50:13 +0200 Subject: [PATCH 2/4] Detect more minified classes --- .../util/ConstantPoolHelpers.kt | 2 +- .../util/MinifiedClassDetectionTest.kt | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt index bbf8a440..6d33ac64 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/ConstantPoolHelpers.kt @@ -81,7 +81,7 @@ private fun isR8Minified(classReader: ClassReader): Boolean { * and https://github.com/getsentry/sentry-android-gradle-plugin/issues/359#issuecomment-1193782500 */ /* ktlint-disable max-line-length */ -private val MINIFIED_CLASSNAME_REGEX = """^((([a-zA-z])\3{1,}([0-9]{1,})?(([a-zA-Z])\6{1,})?)|([a-zA-Z]([0-9])?))(${'\\'}${'$'}(((\w)\12{1,}([0-9]{1,})?(([a-zA-Z])\15{1,})?)|(\w([0-9])?)))*${'$'}""".toRegex() +private val MINIFIED_CLASSNAME_REGEX = """^(((([a-zA-z])\4{1,}|[a-zA-Z]{1,2})([0-9]{1,})?(([a-zA-Z])\7{1,})?)|([a-zA-Z]([0-9])?))(${'\\'}${'$'}((((\w)\14{1,}|[a-zA-Z]{1,2})([0-9]{1,})?(([a-zA-Z])\17{1,})?)|(\w([0-9])?)))*${'$'}""".toRegex() /** * See https://github.com/getsentry/sentry/blob/c943de2afc785083554e7fdfb10c67d0c0de0f98/static/app/components/events/eventEntries.tsx#L57-L58 diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt index 9d22028c..46f69ef0 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt @@ -14,7 +14,10 @@ class MinifiedClassDetectionTest { "ccc017zz", """ccc017zz${'$'}a""", "aa", - "aa${'$'}a" + "aa${'$'}a", + "ab", + "aa${'$'}ab", + "ab${'$'}a" ) classNames.forEach { @@ -22,14 +25,24 @@ class MinifiedClassDetectionTest { } } + @Test + fun `detects minified class names with minified package name`() { + val classNames = listOf( + """a${'$'}""", + "aa" + ) + + classNames.forEach { + assertTrue(classNameLooksMinified(it, "a/$it"), it) + } + } + @Test fun `does not consider non minified classes as minified`() { val classNames = listOf( "ConstantPoolHelpers", "FileUtil", - """FileUtil${"$"}Inner""", - "aa${'$'}ab", - "Id" + """FileUtil${"$"}Inner""" ) classNames.forEach { From d1610e5410e433a76783bcdf361945777517e21c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 1 Aug 2022 22:51:35 +0200 Subject: [PATCH 3/4] Add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef1f29c..bc833067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Detect minified classes and skip instrumentation to avoid build problems ([#362](https://github.com/getsentry/sentry-android-gradle-plugin/pull/362)) + ## 3.1.3 ### Features From 6d1854469020e4c57693bf358e143e6659c5d01c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 2 Aug 2022 15:20:13 +0200 Subject: [PATCH 4/4] CR --- .../gradle/SentryPluginWithMinifiedLibsTest.kt | 2 +- .../util/MinifiedClassDetectionTest.kt | 5 ----- .../app/testlib/stripe-problematic.jar | Bin 3836 -> 0 bytes 3 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 plugin-build/src/test/resources/testFixtures/appTestProject/app/testlib/stripe-problematic.jar diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt index 24ec3b8d..246a3df2 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginWithMinifiedLibsTest.kt @@ -23,7 +23,7 @@ class SentryPluginWithMinifiedLibsTest : implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.0' implementation 'com.adcolony:sdk:4.7.1' implementation 'com.appboy:android-sdk-ui:19.0.0' - implementation files("testlib/stripe-problematic.jar") + implementation 'com.stripe:stripeterminal-internal-common:2.12.0' } """.trimIndent() ) diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt index 46f69ef0..7c9b1d94 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/util/MinifiedClassDetectionTest.kt @@ -49,9 +49,4 @@ class MinifiedClassDetectionTest { assertFalse(classNameLooksMinified(it, "com/example/$it"), it) } } - - @Test - fun `tests that something happens`() { - """^\w(\\${'$'}(\w))*${'$'}""".toRegex().matches("a${'$'}a") - } } diff --git a/plugin-build/src/test/resources/testFixtures/appTestProject/app/testlib/stripe-problematic.jar b/plugin-build/src/test/resources/testFixtures/appTestProject/app/testlib/stripe-problematic.jar deleted file mode 100644 index aa9dd558e40c8f57097bea927b13cf990879d0df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3836 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 KZKM?g`R#vSJI06r