diff --git a/CHANGELOG.md b/CHANGELOG.md index 90166cc8..d086d556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add `--summary-only` flag. - Support diffing bytecode versions for classes. - Support diffing Kotlin metadata versions for classes. +- Support diffing format versions for DEX files. **Changed** - Replace `com.jakewharton.diffuse.io.Size` with `me.saket.bytesize.ByteSize` in the APIs. diff --git a/formats/api/formats.api b/formats/api/formats.api index 02e95be6..fdaf4b23 100644 --- a/formats/api/formats.api +++ b/formats/api/formats.api @@ -243,6 +243,7 @@ public final class com/jakewharton/diffuse/format/Dex : com/jakewharton/diffuse/ public final fun getClasses ()Ljava/util/List; public fun getDeclaredMembers ()Ljava/util/List; public fun getFilename ()Ljava/lang/String; + public final fun getFormatVersion ()I public fun getMembers ()Ljava/util/List; public fun getReferencedMembers ()Ljava/util/List; public final fun getStrings ()Ljava/util/List; diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Dex.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Dex.kt index 38e82daa..fa297439 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Dex.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Dex.kt @@ -2,6 +2,7 @@ package com.jakewharton.diffuse.format import com.android.dex.ClassDef import com.android.dex.Dex as AndroidDex +import com.android.dex.DexFormat as AndroidDexFormat import com.android.dex.FieldId import com.android.dex.MethodId import com.jakewharton.diffuse.io.Input @@ -9,6 +10,7 @@ import com.jakewharton.diffuse.io.Input class Dex private constructor( override val filename: String, + val formatVersion: Int, val strings: List, val types: List, val classes: List, @@ -26,6 +28,7 @@ private constructor( val mappedReferencedMembers = referencedMembers.map(mapping::get) return Dex( filename, + formatVersion, strings, types, mappedClasses, @@ -38,7 +41,15 @@ private constructor( @JvmStatic @JvmName("parse") fun Input.toDex(): Dex { - val dex = AndroidDex(toByteArray()) + val bytes = toByteArray() + val dex = AndroidDex(bytes) + val formatVersion = + String( + bytes, + offset = AndroidDexFormat.MAGIC_PREFIX.length, + length = AndroidDexFormat.VERSION_CURRENT.length, + ) + .toInt() val classes = dex.classDefs().map { TypeDescriptor(dex.typeNames()[it.typeIndex]) } val declaredTypeIndices = dex.classDefs().map(ClassDef::getTypeIndex).toSet() val (declaredMethods, referencedMethods) = @@ -53,7 +64,15 @@ private constructor( .mapEach { it.map(dex::getField) } val declaredMembers = declaredMethods + declaredFields val referencedMembers = referencedMethods + referencedFields - return Dex(name, dex.strings(), dex.typeNames(), classes, declaredMembers, referencedMembers) + return Dex( + name, + formatVersion, + dex.strings(), + dex.typeNames(), + classes, + declaredMembers, + referencedMembers, + ) } private fun Pair.mapEach(body: (T) -> R): Pair = body(first) to body(second) diff --git a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt index 35d0927c..4decc793 100644 --- a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt +++ b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt @@ -7,6 +7,7 @@ import assertk.assertions.hasSize import assertk.assertions.index import assertk.assertions.isEqualTo import com.jakewharton.diffuse.format.Class.Companion.toClass +import com.jakewharton.diffuse.testing.requireResource import java.util.function.Function import org.junit.Test diff --git a/formats/src/test/kotlin/com/jakewharton/diffuse/format/DexTest.kt b/formats/src/test/kotlin/com/jakewharton/diffuse/format/DexTest.kt index 0281f589..56845972 100644 --- a/formats/src/test/kotlin/com/jakewharton/diffuse/format/DexTest.kt +++ b/formats/src/test/kotlin/com/jakewharton/diffuse/format/DexTest.kt @@ -2,14 +2,38 @@ package com.jakewharton.diffuse.format import assertk.assertThat import assertk.assertions.hasSize +import assertk.assertions.isEqualTo import assertk.assertions.prop import com.jakewharton.diffuse.format.Dex.Companion.toDex +import com.jakewharton.diffuse.io.Input.Companion.asInput +import com.jakewharton.diffuse.testing.requireResource +import okio.ByteString.Companion.toByteString import org.junit.Test class DexTest { @Test fun parameterTypeIsUnsigned() { - val dex = DexTest::class.java.getResource("/params32769.dex").asInput().toDex() + val dex = DexTest::class.java.requireResource("/params32769.dex").asInput().toDex() assertThat(dex).prop(Dex::declaredMembers).hasSize(32769) } + + @Test + fun formatVersion() { + val bytes = DexTest::class.java.requireResource("/params32769.dex").asInput().toByteArray() + val dex35 = bytes.toByteString().asInput("dex35.dex").toDex() + assertThat(dex35.formatVersion).isEqualTo(35) + + val dex38 = + bytes + .clone() + .apply { + set(4, '0'.code.toByte()) + set(5, '3'.code.toByte()) + set(6, '8'.code.toByte()) + } + .toByteString() + .asInput("dex38.dex") + .toDex() + assertThat(dex38.formatVersion).isEqualTo(38) + } } diff --git a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ResourceInput.kt b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ResourceInput.kt index 581429a7..bc1b3a23 100644 --- a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ResourceInput.kt +++ b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ResourceInput.kt @@ -1,17 +1,12 @@ package com.jakewharton.diffuse.format import com.jakewharton.diffuse.io.Input -import java.lang.Class import java.net.URL import okio.buffer import okio.source fun URL.asInput(): Input = ResourceInput(this) -fun Class<*>.requireResource(name: String): URL { - return checkNotNull(getResource(name)) { "Resource $name not found." } -} - private class ResourceInput(private val url: URL) : Input { override val name get() = url.path.substringAfterLast('/') diff --git a/reports/build.gradle b/reports/build.gradle index 1e3aa938..8cab5047 100644 --- a/reports/build.gradle +++ b/reports/build.gradle @@ -9,4 +9,5 @@ dependencies { testImplementation libs.junit testImplementation libs.assertk + testImplementation projects.testHelpers } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt index 887f654b..678d68a1 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt @@ -16,6 +16,10 @@ import com.jakewharton.picnic.renderText internal class DexDiff(val oldDexes: List, val newDexes: List) : BinaryDiff { val isMultidex = oldDexes.size > 1 || newDexes.size > 1 + val formatVersions = + componentDiff(oldDexes, newDexes) { dex -> + listOf("${dex.filename}: ${dex.formatVersion}") + } val strings = componentDiff(oldDexes, newDexes) { it.strings } val types = componentDiff(oldDexes, newDexes) { it.types } val classes = componentDiff(oldDexes, newDexes) { it.classes } @@ -30,7 +34,8 @@ internal class DexDiff(val oldDexes: List, val newDexes: List) : Binar val referencedFields = componentDiff(oldDexes, newDexes) { it.referencedMembers.filterIsInstance() } - val changed = strings.changed || types.changed || methods.changed || fields.changed + val changed = + formatVersions.changed || strings.changed || types.changed || methods.changed || fields.changed override fun toTextReport(summaryOnly: Boolean): Report = DexDiffTextReport(this, summaryOnly) } @@ -117,6 +122,7 @@ internal fun DexDiff.toSummaryTable() = diffuseTable { .renderText() internal fun DexDiff.toDetailReport() = buildString { + appendComponentDiff("FORMAT VERSIONS", formatVersions) appendComponentDiff("STRINGS", strings) appendComponentDiff("TYPES", types) appendComponentDiff("METHODS", methods) diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt new file mode 100644 index 00000000..e5921d6e --- /dev/null +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt @@ -0,0 +1,98 @@ +package com.jakewharton.diffuse + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jakewharton.diffuse.diff.DexDiff +import com.jakewharton.diffuse.diff.toDetailReport +import com.jakewharton.diffuse.format.Dex +import com.jakewharton.diffuse.format.Dex.Companion.toDex +import com.jakewharton.diffuse.io.Input.Companion.asInput +import com.jakewharton.diffuse.testing.requireResource +import okio.ByteString.Companion.toByteString +import org.junit.Test + +class DexDiffTest { + @Test + fun formatVersionsSingleDexSame() { + val diff = + DexDiff( + oldDexes = listOf(Dex("classes.dex", 35)), + newDexes = listOf(Dex("classes.dex", 35)), + ) + + assertThat(diff.changed).isEqualTo(false) + assertThat(diff.toDetailReport()).isEqualTo("") + } + + @Test + fun formatVersionsSingleDexChanged() { + val diff = + DexDiff( + oldDexes = listOf(Dex("classes.dex", 35)), + newDexes = listOf(Dex("classes.dex", 38)), + ) + + assertThat(diff.changed).isEqualTo(true) + assertThat(diff.toDetailReport()) + .isEqualTo( + """ + | + |FORMAT VERSIONS: + | + | old │ new │ diff + | ─────┼─────┼─────────── + | 1 │ 1 │ 0 (+1 -1) + | + | + classes.dex: 38 + | + | - classes.dex: 35 + | + |""" + .trimMargin() + ) + } + + @Test + fun formatVersionsMultidexChanged() { + val diff = + DexDiff( + oldDexes = listOf(Dex("classes.dex", 35), Dex("classes2.dex", 35)), + newDexes = listOf(Dex("classes.dex", 35), Dex("classes2.dex", 38)), + ) + + assertThat(diff.changed).isEqualTo(true) + assertThat(diff.toDetailReport()) + .isEqualTo( + """ + | + |FORMAT VERSIONS: + | + | old │ new │ diff + | ─────┼─────┼─────────── + | 2 │ 2 │ 0 (+1 -1) + | + | + classes2.dex: 38 + | + | - classes2.dex: 35 + | + |""" + .trimMargin() + ) + } +} + +private fun Dex(name: String, version: Int): Dex { + val (tens, ones) = version.toString().toCharArray() + return DexDiffTest::class + .java + .requireResource("/params32769.dex") + .readBytes() + .apply { + set(4, '0'.code.toByte()) + set(5, tens.code.toByte()) + set(6, ones.code.toByte()) + } + .toByteString() + .asInput(name) + .toDex() +} diff --git a/test-helpers/src/main/kotlin/com/jakewharton/diffuse/testing/requireResource.kt b/test-helpers/src/main/kotlin/com/jakewharton/diffuse/testing/requireResource.kt new file mode 100644 index 00000000..1efd7677 --- /dev/null +++ b/test-helpers/src/main/kotlin/com/jakewharton/diffuse/testing/requireResource.kt @@ -0,0 +1,7 @@ +package com.jakewharton.diffuse.testing + +import java.net.URL + +fun Class<*>.requireResource(name: String): URL { + return checkNotNull(getResource(name)) { "Resource $name not found." } +} diff --git a/formats/src/test/resources/params32769.dex b/test-helpers/src/main/resources/params32769.dex similarity index 100% rename from formats/src/test/resources/params32769.dex rename to test-helpers/src/main/resources/params32769.dex