Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions formats/api/formats.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 21 additions & 2 deletions formats/src/main/kotlin/com/jakewharton/diffuse/format/Dex.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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

class Dex
private constructor(
override val filename: String,
val formatVersion: Int,
val strings: List<String>,
val types: List<String>,
val classes: List<TypeDescriptor>,
Expand All @@ -26,6 +28,7 @@ private constructor(
val mappedReferencedMembers = referencedMembers.map(mapping::get)
return Dex(
filename,
formatVersion,
strings,
types,
mappedClasses,
Expand All @@ -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) =
Expand All @@ -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 <T, R> Pair<T, T>.mapEach(body: (T) -> R): Pair<R, R> = body(first) to body(second)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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('/')
Expand Down
1 change: 1 addition & 0 deletions reports/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ dependencies {

testImplementation libs.junit
testImplementation libs.assertk
testImplementation projects.testHelpers
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import com.jakewharton.picnic.renderText
internal class DexDiff(val oldDexes: List<Dex>, val newDexes: List<Dex>) : 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 }
Expand All @@ -30,7 +34,8 @@ internal class DexDiff(val oldDexes: List<Dex>, val newDexes: List<Dex>) : Binar
val referencedFields =
componentDiff(oldDexes, newDexes) { it.referencedMembers.filterIsInstance<Field>() }

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)
}
Expand Down Expand Up @@ -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)
Expand Down
98 changes: 98 additions & 0 deletions reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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." }
}