diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 710b07ca..a8624a05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ protobufJava = { module = "com.google.protobuf:protobuf-java", version.ref = "pr clikt = "com.github.ajalt.clikt:clikt:4.4.0" junit = "junit:junit:4.13.2" assertk = "com.willowtreeapps.assertk:assertk:0.28.1" +kotlinxHtml = "org.jetbrains.kotlinx:kotlinx-html:0.11.0" okio = "com.squareup.okio:okio:3.9.0" byteunits = "com.jakewharton.byteunits:byteunits:0.9.1" asm = "org.ow2.asm:asm:9.7" diff --git a/reports/build.gradle b/reports/build.gradle index 1e3aa938..a39cceaf 100644 --- a/reports/build.gradle +++ b/reports/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'org.jetbrains.dokka' dependencies { api projects.formats implementation libs.picnic + implementation libs.kotlinxHtml implementation libs.diffUtils testImplementation libs.junit diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AabDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AabDiff.kt index 0d093730..7edadc2b 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AabDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AabDiff.kt @@ -2,6 +2,7 @@ package com.jakewharton.diffuse.diff import com.jakewharton.diffuse.format.Aab import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.AabDiffHtmlReport import com.jakewharton.diffuse.report.text.AabDiffTextReport internal class AabDiff( @@ -33,4 +34,5 @@ internal class AabDiff( } override fun toTextReport(): Report = AabDiffTextReport(this) + override fun toHtmlReport(): Report = AabDiffHtmlReport(this) } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AarDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AarDiff.kt index 7a82a6a5..705a110c 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AarDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/AarDiff.kt @@ -3,6 +3,7 @@ package com.jakewharton.diffuse.diff import com.jakewharton.diffuse.format.Aar import com.jakewharton.diffuse.format.ApiMapping import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.AarDiffHtmlReport import com.jakewharton.diffuse.report.text.AarDiffTextReport internal class AarDiff( @@ -16,4 +17,5 @@ internal class AarDiff( val manifest = ManifestDiff(oldAar.manifest, newAar.manifest) override fun toTextReport(): Report = AarDiffTextReport(this) + override fun toHtmlReport(): Report = AarDiffHtmlReport(this) } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ApkDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ApkDiff.kt index 7a66c94a..787c8a72 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ApkDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ApkDiff.kt @@ -4,6 +4,7 @@ import com.jakewharton.diffuse.diff.lint.resourcesArscCompression import com.jakewharton.diffuse.format.ApiMapping import com.jakewharton.diffuse.format.Apk import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.ApkDiffHtmlReport import com.jakewharton.diffuse.report.text.ApkDiffTextReport internal class ApkDiff( @@ -26,4 +27,5 @@ internal class ApkDiff( ) override fun toTextReport(): Report = ApkDiffTextReport(this) + override fun toHtmlReport(): Report = ApkDiffHtmlReport(this) } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArchiveFilesDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArchiveFilesDiff.kt index 64f124fd..61ffdafa 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArchiveFilesDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArchiveFilesDiff.kt @@ -5,6 +5,7 @@ import com.jakewharton.diffuse.diffuseTable import com.jakewharton.diffuse.format.ArchiveFile.Type import com.jakewharton.diffuse.format.ArchiveFiles import com.jakewharton.diffuse.io.Size +import com.jakewharton.diffuse.report.htmlEncoded import com.jakewharton.diffuse.report.toDiffString import com.jakewharton.picnic.TableSectionDsl import com.jakewharton.picnic.TextAlignment.BottomCenter @@ -12,6 +13,16 @@ import com.jakewharton.picnic.TextAlignment.BottomLeft import com.jakewharton.picnic.TextAlignment.MiddleCenter import com.jakewharton.picnic.TextAlignment.MiddleRight import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.TR +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.tfoot +import kotlinx.html.thead +import kotlinx.html.tr +import kotlinx.html.unsafe internal class ArchiveFilesDiff( val oldFiles: ArchiveFiles, @@ -120,7 +131,7 @@ internal fun ArchiveFilesDiff.toSummaryTable( } } - fun TableSectionDsl.addApkRow(name: String, type: Type? = null) { + fun TableSectionDsl.addArchiveRow(name: String, type: Type? = null) { val old = if (type != null) oldFiles.filterValues { it.type == type } else oldFiles val new = if (type != null) newFiles.filterValues { it.type == type } else newFiles val oldSize = old.values.fold(Size.ZERO) { acc, file -> acc + file.size } @@ -150,7 +161,7 @@ internal fun ArchiveFilesDiff.toSummaryTable( alignment = MiddleRight } for (type in displayTypes) { - addApkRow(type.displayName, type) + addArchiveRow(type.displayName, type) } } @@ -158,7 +169,7 @@ internal fun ArchiveFilesDiff.toSummaryTable( cellStyle { alignment = MiddleRight } - addApkRow("total") + addArchiveRow("total") } }.renderText() @@ -243,3 +254,190 @@ internal fun ArchiveFilesDiff.toDetailReport() = buildString { }.renderText(), ) } + +internal fun FlowContent.toSummaryTable( + name: String, + diff: ArchiveFilesDiff, + displayTypes: List, + skipIfEmptyTypes: Set = emptySet(), +) { + table { + thead { + if (diff.includeCompressed) { + tr { + td { + rowSpan = "2" + style = "text-align: left; vertical-align: bottom;" + +name + } + td { + colSpan = "3" + style = "text-align: center; vertical-align: bottom;" + +"compressed" + } + td { + colSpan = "3" + style = "text-align: center; vertical-align: bottom;" + +"uncompressed" + } + } + tr { + td { +"old" } + td { +"new" } + td { +"diff" } + td { +"old" } + td { +"new" } + td { +"diff" } + } + } else { + tr { + td { + style = "text-align: left; vertical-align: bottom;" + +name + } + td { +"old" } + td { +"new" } + td { +"diff" } + } + } + } + + fun TR.addArchiveRow(name: String, type: Type? = null) { + val old = if (type != null) diff.oldFiles.filterValues { it.type == type } else diff.oldFiles + val new = if (type != null) diff.newFiles.filterValues { it.type == type } else diff.newFiles + val oldSize = old.values.fold(Size.ZERO) { acc, file -> acc + file.size } + val newSize = new.values.fold(Size.ZERO) { acc, file -> acc + file.size } + val oldUncompressedSize = old.values.fold(Size.ZERO) { acc, file -> acc + file.uncompressedSize } + val newUncompressedSize = new.values.fold(Size.ZERO) { acc, file -> acc + file.uncompressedSize } + if (oldSize != Size.ZERO || newSize != Size.ZERO || type !in skipIfEmptyTypes) { + val uncompressedDiff = (newUncompressedSize - oldUncompressedSize).toDiffString() + if (diff.includeCompressed) { + td { +name } + td { +oldSize.toString() } + td { +newSize.toString() } + td { +(newSize - oldSize).toString() } + td { +oldUncompressedSize.toString() } + td { +newUncompressedSize.toString() } + td { +uncompressedDiff } + } else { + td { +name } + td { +oldUncompressedSize.toString() } + td { +newUncompressedSize.toString() } + td { +uncompressedDiff } + } + } + } + + tbody { + style = "text-align: right; vertical-align: middle;" + + for (type in displayTypes) { + tr { + addArchiveRow(type.displayName, type) + } + } + } + + tfoot { + style = "text-align: right; vertical-align: middle;" + tr { addArchiveRow("total") } + } + } +} + +internal fun FlowContent.toDetailReport(diff: ArchiveFilesDiff) { + table { + thead { + if (diff.includeCompressed) { + tr { + td { + style = "text-align: center; vertical-align: middle;" + colSpan = "2" + +"compressed" + } + td { + style = "text-align: center; vertical-align: middle;" + colSpan = "2" + +"uncompressed" + } + + td { + style = "text-align: left; vertical-align: bottom;" + rowSpan = "2" + +"path" + } + } + tr { + td { +"size" } + td { +"diff" } + td { +"size" } + td { +"diff" } + } + } else { + tr { + td { +"size" } + td { +"diff" } + td { + style = "text-align: left; vertical-align: bottom;" + +"path" + } + } + } + } + tfoot { + tr { + if (diff.includeCompressed) { + val totalSize = diff.changes.fold(Size.ZERO) { acc, change -> acc + change.size } + val totalDiff = diff.changes.fold(Size.ZERO) { acc, change -> acc + change.sizeDiff } + td { + style = "text-align: right; vertical-align: middle;" + +totalSize.toString() + } + td { + style = "text-align: right; vertical-align: middle;" + +totalDiff.toDiffString() + } + } + val totalUncompressedSize = diff.changes.fold(Size.ZERO) { acc, change -> acc + change.uncompressedSize } + val totalUncompressedDiff = diff.changes.fold(Size.ZERO) { acc, change -> acc + change.uncompressedSizeDiff } + td { + style = "text-align: right; vertical-align: middle;" + +totalUncompressedSize.toString() + } + td { + style = "text-align: right; vertical-align: middle;" + +totalUncompressedDiff.toDiffString() + } + td { +"(total)" } + } + } + for ((path, size, sizeDiff, uncompressedSize, uncompressedSizeDiff, type) in diff.changes) { + val typeChar = when (type) { + Change.Type.Added -> '+' + Change.Type.Removed -> '-' + Change.Type.Changed -> '∆' + } + tr { + if (diff.includeCompressed) { + td { + style = "text-align: right; vertical-align: middle;" + if (type != Change.Type.Removed) +size.toString() else +"" + } + td { + style = "text-align: right; vertical-align: middle;" + +sizeDiff.toDiffString() + } + } + td { + style = "text-align: right; vertical-align: middle;" + if (type != Change.Type.Removed) +uncompressedSize.toString() else +"" + } + td { + style = "text-align: right; vertical-align: middle;" + +uncompressedSizeDiff.toDiffString() + } + td { unsafe { raw("$typeChar $path".htmlEncoded) } } + } + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArscDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArscDiff.kt index 0c3695f3..fc633069 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArscDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ArscDiff.kt @@ -6,6 +6,17 @@ import com.jakewharton.diffuse.report.toDiffString import com.jakewharton.picnic.TextAlignment.MiddleLeft import com.jakewharton.picnic.TextAlignment.MiddleRight import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.br +import kotlinx.html.div +import kotlinx.html.p +import kotlinx.html.span +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr internal class ArscDiff( val oldArsc: Arsc, @@ -139,3 +150,122 @@ internal fun ArscDiff.toDetailReport() = buildString { appendComponentDiff("CONFIGS", Arsc::configs, configsAdded, configsRemoved) appendComponentDiff("ENTRIES", { it.entries.values }, entriesAdded, entriesRemoved) } + +internal fun FlowContent.toSummaryTable(diff: ArscDiff) { + table { + thead { + tr { + td { +"ARSC" } + td { +"old" } + td { +"new" } + td { colSpan = "2" + "diff" } + } + } + + tbody { + style = "text-align: right; vertical-align: middle;" + + tr { + td { +"configs" } + td { +diff.oldArsc.configs.size } + td { +diff.newArsc.configs.size } + + val configsDelta = diff.configsAdded.size - diff.configsRemoved.size + td { + style = "border-right: none;" + +configsDelta.toDiffString() + } + + val delta = if (diff.configsAdded.isNotEmpty() || diff.configsRemoved.isNotEmpty()) { + val added = diff.configsAdded.size.toDiffString(zeroSign = '+') + val removed = (-diff.configsRemoved.size).toDiffString(zeroSign = '-') + "($added $removed)" + } else { + "" + } + + td { + style = "border-left: none; padding-left: 0; text-align: left; vertical-align: middle;" + +delta + } + } + + tr { + td { +"entries" } + td { +diff.oldArsc.entries.size } + td { +diff.newArsc.entries.size } + + val entriesDelta = diff.entriesAdded.size - diff.entriesRemoved.size + td { + style = "border-right: none;" + +entriesDelta.toDiffString() + } + + val delta = if (diff.entriesAdded.isNotEmpty() || diff.entriesRemoved.isNotEmpty()) { + val added = diff.entriesAdded.size.toDiffString(zeroSign = '+') + val removed = (-diff.entriesRemoved.size).toDiffString(zeroSign = '-') + "($added $removed)" + } else { + "" + } + + td { + style = "border-left: none; padding-left: 0; text-align: left; vertical-align: middle;" + +delta + } + } + } + } +} + +internal fun FlowContent.toDetailReport(diff: ArscDiff) { + fun FlowContent.appendComponentDiff( + name: String, + diff: ArscDiff, + componentSelector: (Arsc) -> Collection<*>, + ) { + if (diff.configsAdded.isNotEmpty() || diff.configsRemoved.isNotEmpty()) { + p { +"$name:" } + + div { + style = "margin-left: 16pt;" + + table { + thead { + tr { + td { +"old" } + td { +"new" } + td { +"diff" } + } + } + tbody { + val diffSize = (diff.configsAdded.size - diff.configsRemoved.size).toDiffString() + val addedSize = diff.configsAdded.size.toDiffString(zeroSign = '+') + val removedSize = (-diff.configsRemoved.size).toDiffString(zeroSign = '-') + tr { + td { +componentSelector(diff.oldArsc).size } + td { +componentSelector(diff.newArsc).size } + td { +"$diffSize ($addedSize $removedSize)" } + } + } + } + + diff.configsAdded.forEach { + span { +"+ $it" } + br() + } + if (diff.configsAdded.isNotEmpty() && diff.configsRemoved.isNotEmpty()) { + br() + br() + } + diff.configsRemoved.forEach { + span { +"- $it" } + br() + } + } + } + } + + appendComponentDiff("CONFIGS", diff, Arsc::configs) + appendComponentDiff("ENTRIES", diff) { it.entries.values } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ComponentDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ComponentDiff.kt index dbf1e85b..70816496 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ComponentDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ComponentDiff.kt @@ -1,8 +1,23 @@ package com.jakewharton.diffuse.diff import com.jakewharton.diffuse.diffuseTable +import com.jakewharton.diffuse.report.htmlEncoded import com.jakewharton.diffuse.report.toDiffString import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.br +import kotlinx.html.details +import kotlinx.html.div +import kotlinx.html.h4 +import kotlinx.html.span +import kotlinx.html.style +import kotlinx.html.summary +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr +import kotlinx.html.unsafe internal class ComponentDiff( val oldRawCount: Int, @@ -71,3 +86,58 @@ internal fun StringBuilder.appendComponentDiff(name: String, diff: ComponentDiff ) } } + +internal fun FlowContent.appendComponentDiff(name: String, diff: ComponentDiff<*>) { + if (diff.changed) { + div { + style = "margin: 24px 0;" + + h4 { +name } + + table { + thead { + tr { + td { +"old" } + td { +"new" } + td { +"diff" } + } + } + tbody { + val diffSize = (diff.added.size - diff.removed.size).toDiffString() + val addedSize = diff.added.size.toDiffString(zeroSign = '+') + val removedSize = (-diff.removed.size).toDiffString(zeroSign = '-') + + tr { + td { +diff.oldCount.toString() } + td { +diff.newCount.toString() } + td { +"$diffSize ($addedSize $removedSize)" } + } + } + } + + details { + summary { +"diff" } + + div { + style = "margin-left: 16pt;" + + if (diff.added.isNotEmpty()) { + br() + diff.added.forEach { + span { unsafe { raw("+ $it".htmlEncoded) } } + br() + } + } + + if (diff.removed.isNotEmpty()) { + br() + diff.removed.forEach { + span { unsafe { raw("- $it".htmlEncoded) } } + br() + } + } + } + } + } + } +} 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 d9692043..ed95b8df 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt @@ -5,6 +5,7 @@ import com.jakewharton.diffuse.format.Dex import com.jakewharton.diffuse.format.Field import com.jakewharton.diffuse.format.Method import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.DexDiffHtmlReport import com.jakewharton.diffuse.report.text.DexDiffTextReport import com.jakewharton.diffuse.report.toDiffString import com.jakewharton.picnic.TextAlignment.BottomCenter @@ -12,6 +13,14 @@ import com.jakewharton.picnic.TextAlignment.BottomLeft import com.jakewharton.picnic.TextAlignment.MiddleLeft import com.jakewharton.picnic.TextAlignment.MiddleRight import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.TBODY +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr internal class DexDiff( val oldDexes: List, @@ -32,6 +41,7 @@ internal class DexDiff( val changed = strings.changed || types.changed || methods.changed || fields.changed override fun toTextReport(): Report = DexDiffTextReport(this) + override fun toHtmlReport(): Report = DexDiffHtmlReport(this) } internal fun DexDiff.toSummaryTable() = diffuseTable { @@ -128,3 +138,103 @@ internal fun DexDiff.toDetailReport() = buildString { appendComponentDiff("METHODS", methods) appendComponentDiff("FIELDS", fields) } + +internal fun FlowContent.toSummaryTable(dexDiff: DexDiff) { + table { + thead { + if (dexDiff.isMultidex) { + tr { + td { + style = "text-align: left; vertical-align: bottom;" + rowSpan = "2" + +"DEX" + } + + td { + style = "text-align: center; vertical-align: bottom;" + colSpan = "3" + +"raw" + } + td { + style = "text-align: center; vertical-align: bottom;" + colSpan = "4" + +"unique" + } + } + } + tr { + if (!dexDiff.isMultidex) { + td { +"DEX" } + } else { + td { +"old" } + td { +"new" } + td { +"diff" } + } + td { +"old" } + td { +"new" } + td { + colSpan = "2" + +"diff" + } + } + } + + tbody { + style = "text-align: right; vertical-align: middle;" + + tr { + td { +"files" } + td { +dexDiff.oldDexes.size } + td { +dexDiff.newDexes.size } + td { + style = "border-right: none;" + +(dexDiff.newDexes.size - dexDiff.oldDexes.size).toDiffString() + } + if (dexDiff.isMultidex) { + // todo: necessary for html? + // Add empty cells to ensure borders get drawn + td { +"" } + td { +"" } + td { colSpan = "2" + "" } + } + } + + fun TBODY.addDexRow(name: String, diff: ComponentDiff<*>) { + tr { + td { +name } + if (dexDiff.isMultidex) { + td { +diff.oldRawCount } + td { +diff.newRawCount } + td { +(diff.newRawCount - diff.oldRawCount).toDiffString() } + } + td { +diff.oldCount } + td { +diff.newCount } + td { + style = "border-right: none;" + +(diff.added.size - diff.removed.size).toDiffString() + } + + val addedSize = diff.added.size.toDiffString(zeroSign = '+') + val removedSize = (-diff.removed.size).toDiffString(zeroSign = '-') + td { + style = "border-left: none; padding-left: 0; text-align: left; vertical-align: middle;" + +"($addedSize $removedSize)" + } + } + } + + addDexRow("strings", dexDiff.strings) + addDexRow("types", dexDiff.types) + addDexRow("classes", dexDiff.classes) + addDexRow("methods", dexDiff.methods) + addDexRow("fields", dexDiff.fields) + } + } +} + +internal fun FlowContent.toDetailReport(diff: DexDiff) { + appendComponentDiff("STRINGS", diff.strings) + appendComponentDiff("TYPES", diff.types) + appendComponentDiff("METHODS", diff.methods) + appendComponentDiff("FIELDS", diff.fields) +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarDiff.kt index 580b4890..6fb9e3c8 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarDiff.kt @@ -3,6 +3,7 @@ package com.jakewharton.diffuse.diff import com.jakewharton.diffuse.format.ApiMapping import com.jakewharton.diffuse.format.Jar import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.JarDiffHtmlReport import com.jakewharton.diffuse.report.text.JarDiffTextReport internal class JarDiff( @@ -17,4 +18,6 @@ internal class JarDiff( val changed = jars.changed || archive.changed override fun toTextReport(): Report = JarDiffTextReport(this) + + override fun toHtmlReport(): Report = JarDiffHtmlReport(this) } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt index eb0d2faa..3e586eb0 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt @@ -10,6 +10,15 @@ import com.jakewharton.diffuse.report.toDiffString import com.jakewharton.picnic.TextAlignment.MiddleLeft import com.jakewharton.picnic.TextAlignment.MiddleRight import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.TBODY +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.th +import kotlinx.html.thead +import kotlinx.html.tr internal class JarsDiff( val oldJars: List, @@ -75,3 +84,53 @@ internal fun JarsDiff.toDetailReport() = buildString { appendComponentDiff("METHODS", methods) appendComponentDiff("FIELDS", fields) } + +internal fun FlowContent.toHtmlSummary(name: String, diff: JarsDiff) { + table { + thead { + tr { + th { +name } + th { +"old" } + th { +"new" } + th { + colSpan = "2" + +"diff" + } + } + } + + tbody { + fun TBODY.addRow(name: String, diff: ComponentDiff<*>) { + tr { + style = "text-align: right; vertical-align: middle;" + td { +name } + td { +diff.oldCount.toString() } + td { +diff.newCount.toString() } + td { + style = "border-right: none;" + +(diff.added.size - diff.removed.size).toDiffString() + } + + val addedSize = diff.added.size.toDiffString(zeroSign = '+') + val removedSize = (-diff.removed.size).toDiffString(zeroSign = '-') + td { + style = "border-left: none; padding-left: 0; text-align: left;" + +"($addedSize $removedSize)" + } + } + } + + // TODO addRow("strings", strings)? + addRow("classes", diff.classes) + addRow("methods", diff.methods) + addRow("fields", diff.fields) + } + } +} + +internal fun FlowContent.toDetailReport(diff: JarsDiff) { + // TODO appendComponentDiff("STRINGS", strings)? + appendComponentDiff("CLASSES", diff.classes) + appendComponentDiff("METHODS", diff.methods) + appendComponentDiff("FIELDS", diff.fields) +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ManifestDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ManifestDiff.kt index 042d7001..99c3d644 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ManifestDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/ManifestDiff.kt @@ -4,6 +4,14 @@ import com.github.difflib.DiffUtils import com.github.difflib.UnifiedDiffUtils import com.jakewharton.diffuse.diffuseTable import com.jakewharton.diffuse.format.AndroidManifest +import kotlinx.html.FlowContent +import kotlinx.html.br +import kotlinx.html.span +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr internal class ManifestDiff( val oldManifest: AndroidManifest, @@ -44,3 +52,44 @@ internal fun ManifestDiff.toDetailReport() = buildString { appendLine() } } + +internal fun FlowContent.toDetailReport(diff: ManifestDiff) { + if (diff.parsedPropertiesChanged) { + table { + thead { + tr { + td { +"" } + td { +"old" } + td { +"new" } + } + } + tbody { + tr { + td { +"package" } + td { +diff.oldManifest.packageName } + td { +diff.newManifest.packageName } + } + + tr { + td { +"version code" } + td { +diff.oldManifest.versionCode.toString() } + td { +diff.newManifest.versionCode.toString() } + } + + tr { + td { +"version name" } + td { +diff.oldManifest.versionName.toString() } + td { +diff.newManifest.versionName.toString() } + } + } + } + } + + if (diff.diff.isNotEmpty()) { + diff.diff.drop(2) // Skip file name headers + .forEach { + span { +it } + br() + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/SignaturesDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/SignaturesDiff.kt index 124ffe55..57d9afdb 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/SignaturesDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/SignaturesDiff.kt @@ -3,6 +3,12 @@ package com.jakewharton.diffuse.diff import com.jakewharton.diffuse.diffuseTable import com.jakewharton.diffuse.format.Signatures import com.jakewharton.picnic.TextAlignment.TopRight +import kotlinx.html.FlowContent +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr import okio.ByteString internal class SignaturesDiff( @@ -58,3 +64,63 @@ internal fun SignaturesDiff.toDetailReport() = buildString { }, ) } + +internal fun FlowContent.toDetailReport(diff: SignaturesDiff) { + table { + thead { + tr { + td { +"SIGNATURES" } + td { +"old" } + td { +"new" } + } + } + + if (diff.oldSignatures.v1.isNotEmpty() || diff.newSignatures.v1.isNotEmpty()) { + tr { + td { + style = "text-align: right; vertical-align: top;" + +"V1" + } + + td { +diff.oldSignatures.v1.joinToString("\n", transform = ByteString::hex) } + td { +diff.newSignatures.v1.joinToString("\n", transform = ByteString::hex) } + } + } + + if (diff.oldSignatures.v2.isNotEmpty() || diff.newSignatures.v2.isNotEmpty()) { + tr { + td { + style = "text-align: right; vertical-align: top;" + +"V2" + } + + td { +diff.oldSignatures.v2.joinToString("\n", transform = ByteString::hex) } + td { +diff.newSignatures.v2.joinToString("\n", transform = ByteString::hex) } + } + } + + if (diff.oldSignatures.v3.isNotEmpty() || diff.newSignatures.v3.isNotEmpty()) { + tr { + td { + style = "text-align: right; vertical-align: top;" + +"V3" + } + + td { +diff.oldSignatures.v3.joinToString("\n", transform = ByteString::hex) } + td { +diff.newSignatures.v3.joinToString("\n", transform = ByteString::hex) } + } + } + + if (diff.oldSignatures.v4.isNotEmpty() || diff.newSignatures.v4.isNotEmpty()) { + tr { + td { + style = "text-align: right; vertical-align: top;" + +"V4" + } + + td { +diff.oldSignatures.v4.joinToString("\n", transform = ByteString::hex) } + td { +diff.newSignatures.v4.joinToString("\n", transform = ByteString::hex) } + } + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/AabInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/AabInfo.kt index c07e5a06..98151467 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/AabInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/AabInfo.kt @@ -3,6 +3,7 @@ package com.jakewharton.diffuse.info import com.jakewharton.diffuse.diff.BinaryDiff import com.jakewharton.diffuse.format.Aab import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.AabInfoHtmlReport import com.jakewharton.diffuse.report.text.AabInfoTextReport class AabInfo( @@ -11,4 +12,8 @@ class AabInfo( override fun toTextReport(): Report { return AabInfoTextReport(aab) } + + override fun toHtmlReport(): Report { + return AabInfoHtmlReport(aab) + } } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/AarInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/AarInfo.kt index 8d288bc8..26bdeb54 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/AarInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/AarInfo.kt @@ -2,6 +2,7 @@ package com.jakewharton.diffuse.info import com.jakewharton.diffuse.format.Aar import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.AarInfoHtmlReport import com.jakewharton.diffuse.report.text.AarInfoTextReport class AarInfo( @@ -10,4 +11,8 @@ class AarInfo( override fun toTextReport(): Report { return AarInfoTextReport(aar) } + + override fun toHtmlReport(): Report { + return AarInfoHtmlReport(aar) + } } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/ApkInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/ApkInfo.kt index a7fa82d0..e996dfc2 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/ApkInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/ApkInfo.kt @@ -2,6 +2,7 @@ package com.jakewharton.diffuse.info import com.jakewharton.diffuse.format.Apk import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.ApkInfoHtmlReport import com.jakewharton.diffuse.report.text.ApkInfoTextReport class ApkInfo( @@ -10,4 +11,8 @@ class ApkInfo( override fun toTextReport(): Report { return ApkInfoTextReport(apk) } + + override fun toHtmlReport(): Report { + return ApkInfoHtmlReport(apk) + } } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArchiveFilesInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArchiveFilesInfo.kt index ebe659ec..8ad29d6d 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArchiveFilesInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArchiveFilesInfo.kt @@ -7,6 +7,16 @@ import com.jakewharton.diffuse.io.Size import com.jakewharton.picnic.TableSectionDsl import com.jakewharton.picnic.TextAlignment import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.TR +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.tfoot +import kotlinx.html.th +import kotlinx.html.thead +import kotlinx.html.tr internal fun ArchiveFiles.toSummaryTable( name: String, @@ -37,7 +47,7 @@ internal fun ArchiveFiles.toSummaryTable( } } - fun TableSectionDsl.addApkRow(name: String, type: ArchiveFile.Type? = null) { + fun TableSectionDsl.addArchiveFileRow(name: String, type: ArchiveFile.Type? = null) { val old = if (type != null) filterValues { it.type == type } else this@toSummaryTable val oldSize = old.values.fold(Size.ZERO) { acc, file -> acc + file.size } val oldUncompressedSize = old.values.fold(Size.ZERO) { acc, file -> acc + file.uncompressedSize } @@ -59,7 +69,7 @@ internal fun ArchiveFiles.toSummaryTable( alignment = TextAlignment.MiddleRight } for (type in displayTypes) { - addApkRow(type.displayName, type) + addArchiveFileRow(type.displayName, type) } } @@ -67,6 +77,83 @@ internal fun ArchiveFiles.toSummaryTable( cellStyle { alignment = TextAlignment.MiddleRight } - addApkRow("total") + addArchiveFileRow("total") } }.renderText() + +internal fun FlowContent.toSummaryTable( + name: String, + files: ArchiveFiles, + displayTypes: List, + skipIfEmptyTypes: Set = emptySet(), + includeCompressed: Boolean = true, +) { + table { + thead { + tr { + if (includeCompressed) { + th { + style = "text-align: left; vertical-align: bottom;" + + +name + } + + th { + style = "text-align: center; vertical-align: bottom;" + + +"compressed" + } + + th { + style = "text-align: center; vertical-align: bottom;" + + +"uncompressed" + } + } else { + th { + style = "text-align: left; vertical-align: bottom;" + + +name + } + + th { + +"size" + } + } + } + } + + fun TR.addArchiveFileRow(name: String, type: ArchiveFile.Type? = null) { + val old = if (type != null) files.filterValues { it.type == type } else files + val oldSize = old.values.fold(Size.ZERO) { acc, file -> acc + file.size } + val oldUncompressedSize = old.values.fold(Size.ZERO) { acc, file -> acc + file.uncompressedSize } + if (oldSize != Size.ZERO || type !in skipIfEmptyTypes) { + if (includeCompressed) { + td { +name } + td { +oldSize.toString() } + td { +oldUncompressedSize.toString() } + } else { + td { +name } + td { +oldUncompressedSize.toString() } + } + } + } + + tbody { + style = "text-align: right; vertical-align: middle;" + + for (type in displayTypes) { + tr { + addArchiveFileRow(type.displayName, type) + } + } + } + + tfoot { + tr { + style = "text-align: right; vertical-align: middle;" + addArchiveFileRow("total") + } + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArscInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArscInfo.kt index 5d6bfe1b..8394d17d 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArscInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/ArscInfo.kt @@ -4,6 +4,13 @@ import com.jakewharton.diffuse.diffuseTable import com.jakewharton.diffuse.format.Arsc import com.jakewharton.picnic.TextAlignment import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr internal fun Arsc.toSummaryTable() = diffuseTable { header { @@ -19,3 +26,28 @@ internal fun Arsc.toSummaryTable() = diffuseTable { row("entries", entries.size) } }.renderText() + +internal fun FlowContent.toSummaryTable(arsc: Arsc) { + table { + thead { + tr { + td { +"ARSC" } + td { +"count" } + } + } + + tbody { + style = "text-align: right; vertical-align: middle;" + + tr { + td { +"configs" } + td { +arsc.configs.size } + } + + tr { + td { +"entries" } + td { +arsc.entries.size } + } + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexInfo.kt index 1a8c8aee..4fbcd9f8 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexInfo.kt @@ -2,6 +2,7 @@ package com.jakewharton.diffuse.info import com.jakewharton.diffuse.format.Dex import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.DexInfoHtmlReport import com.jakewharton.diffuse.report.text.DexInfoTextReport class DexInfo( @@ -10,4 +11,8 @@ class DexInfo( override fun toTextReport(): Report { return DexInfoTextReport(dex) } + + override fun toHtmlReport(): Report { + return DexInfoHtmlReport(dex) + } } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexesInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexesInfo.kt index 33a4def9..359e158c 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexesInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/DexesInfo.kt @@ -6,6 +6,14 @@ import com.jakewharton.diffuse.format.Field import com.jakewharton.diffuse.format.Method import com.jakewharton.picnic.TextAlignment import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.TBODY +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr internal fun List.toSummaryTable() = diffuseTable { val isMultidex = size > 1 @@ -53,3 +61,55 @@ internal fun List.toSummaryTable() = diffuseTable { addDexRow("fields") { it.members.filterIsInstance() } } }.renderText() + +internal fun FlowContent.toSummaryTable(dexList: List) { + val isMultidex = dexList.size > 1 + + table { + thead { + tr { + td { + style = "text-align: left; vertical-align: bottom;" + +"DEX" + } + + if (isMultidex) { + td { +"raw" } + td { +"unique" } + } else { + td { +"count" } + } + } + } + + tbody { + style = "text-align: right; vertical-align: middle;" + + tr { + td { +"files" } + td { +dexList.size.toString() } + + // todo: is this necessary for HTML tables? Kinda think no... + if (isMultidex) { + td { +"" } + } + } + + fun TBODY.addDexRow(name: String, selector: (Dex) -> List) { + tr { + td { +name } + if (isMultidex) { + td { +dexList.sumOf { selector(it).size } } + } + td { +dexList.flatMapTo(LinkedHashSet(), selector).size.toString() } + } + } + + addDexRow("strings") { it.strings } + addDexRow("types") { it.types } + addDexRow("classes") { it.classes } + addDexRow("methods") { it.members.filterIsInstance() } + addDexRow("fields") { it.members.filterIsInstance() } + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarInfo.kt index 2b07bd53..2b320fa3 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarInfo.kt @@ -2,6 +2,7 @@ package com.jakewharton.diffuse.info import com.jakewharton.diffuse.format.Jar import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.html.JarInfoHtmlReport import com.jakewharton.diffuse.report.text.JarInfoTextReport class JarInfo( @@ -10,4 +11,8 @@ class JarInfo( override fun toTextReport(): Report { return JarInfoTextReport(jar) } + + override fun toHtmlReport(): Report { + return JarInfoHtmlReport(jar) + } } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarsInfo.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarsInfo.kt index eebe734e..e419fdba 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarsInfo.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/info/JarsInfo.kt @@ -6,6 +6,13 @@ import com.jakewharton.diffuse.format.Jar import com.jakewharton.diffuse.format.Method import com.jakewharton.picnic.TextAlignment import com.jakewharton.picnic.renderText +import kotlinx.html.FlowContent +import kotlinx.html.style +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.th +import kotlinx.html.thead +import kotlinx.html.tr internal fun List.toSummaryTable(name: String) = diffuseTable { header { @@ -27,3 +34,30 @@ internal fun List.toSummaryTable(name: String) = diffuseTable { addRow("fields") { it.members.filterIsInstance() } } }.renderText() + +internal fun FlowContent.toSummaryTable(name: String, jars: List) { + table { + thead { + tr { + th { +name } + th { +"count" } + } + } + + tbody { + style = "text-align: right; vertical-align: middle;" + + fun addRow(name: String, selector: (Jar) -> List) { + tr { + th { +name } + th { +jars.flatMap(selector).size.toString() } + } + } + + // TODO addRow("strings", strings)? + addRow("classes", Jar::classes) + addRow("methods") { it.members.filterIsInstance() } + addRow("fields") { it.members.filterIsInstance() } + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/Report.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/Report.kt index afe9a4f4..0a9cb5ba 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/report/Report.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/Report.kt @@ -5,8 +5,6 @@ interface Report { interface Factory { fun toTextReport(): Report - fun toHtmlReport(): Report { - TODO("Implement HTML reporting") - } + fun toHtmlReport(): Report } } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AabDiffHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AabDiffHtmlReport.kt new file mode 100644 index 00000000..08647768 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AabDiffHtmlReport.kt @@ -0,0 +1,104 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.diff.AabDiff +import com.jakewharton.diffuse.diff.toDetailReport +import com.jakewharton.diffuse.report.Report +import kotlinx.html.body +import kotlinx.html.br +import kotlinx.html.details +import kotlinx.html.h2 +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.span +import kotlinx.html.stream.appendHTML +import kotlinx.html.style +import kotlinx.html.summary +import kotlinx.html.table +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr + +internal class AabDiffHtmlReport(private val aabDiff: AabDiff) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + span { +"OLD: ${aabDiff.oldAab.filename}" } + span { +"NEW: ${aabDiff.newAab.filename}" } + br() + + table { + style = "text-align: center; vertical-align: middle;" + + thead { + style = "text-align: left; vertical-align: bottom;" + tr { + td { +"MODULES" } + td { +"old" } + td { +"new" } + } + + tr { + td { + style = "text-align: right; vertical-align: middle;" + +"base" + } + td { +"✓" } + td { +"✓" } + } + + for (name in aabDiff.featureModuleNames) { + tr { + td { + style = "text-align: right; vertical-align: middle;" + +name + } + td { if (name in aabDiff.oldAab.featureModules) +"✓" else +"" } + td { if (name in aabDiff.newAab.featureModules) +"✓" else +"" } + } + } + } + } + + h2 { +"base" } + if (aabDiff.baseModule.archive.changed) { + toDetailReport(aabDiff.baseModule.archive) + } + if (aabDiff.baseModule.dex.changed) { + toDetailReport(aabDiff.baseModule.dex) + } + if (aabDiff.baseModule.manifest.changed) { + toDetailReport(aabDiff.baseModule.manifest) + } + + for (name in (aabDiff.featureModuleNames - aabDiff.removedFeatureModules.keys)) { + details { + summary { +name } + + val addedModule = aabDiff.addedFeatureModules[name] + val changedModule = aabDiff.changedFeatureModules[name] + assert((addedModule != null) xor (changedModule != null)) + + if (addedModule != null) { + // TODO + } + if (changedModule != null) { + if (changedModule.archive.changed) { + toDetailReport(changedModule.archive) + } + if (changedModule.dex.changed) { + toDetailReport(changedModule.dex) + } + if (changedModule.manifest.changed) { + toDetailReport(changedModule.manifest) + } + } + } + } + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AabInfoHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AabInfoHtmlReport.kt new file mode 100644 index 00000000..1a5518a3 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AabInfoHtmlReport.kt @@ -0,0 +1,74 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.format.Aab +import com.jakewharton.diffuse.format.ArchiveFile +import com.jakewharton.diffuse.info.toSummaryTable +import com.jakewharton.diffuse.report.Report +import kotlinx.html.BODY +import kotlinx.html.body +import kotlinx.html.details +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.p +import kotlinx.html.stream.appendHTML +import kotlinx.html.summary +import kotlinx.html.table +import kotlinx.html.tbody +import kotlinx.html.td +import kotlinx.html.thead +import kotlinx.html.tr + +class AabInfoHtmlReport(private val aab: Aab) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + p { +aab.filename.toString() } + + table { + thead { + tr { + td { +"MODULES" } + } + } + + tbody { + tr { + td { +"base" } + } + + for (name in aab.featureModules.keys) { + tr { td { +name } } + } + } + } + + appendModule("base", aab.baseModule) + + for ((name, module) in aab.featureModules) { + appendModule(name, module) + } + } + } + } + + override fun toString() = buildString { write(this) } + + private fun BODY.appendModule( + name: String, + module: Aab.Module, + ) { + details { + summary { +name } + toSummaryTable( + "AAB", + module.files, + ArchiveFile.Type.AAB_TYPES, + skipIfEmptyTypes = setOf(ArchiveFile.Type.Native), + ) + + toSummaryTable(module.dexes) + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AarDiffHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AarDiffHtmlReport.kt new file mode 100644 index 00000000..22a5dd46 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AarDiffHtmlReport.kt @@ -0,0 +1,60 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.diff.AarDiff +import com.jakewharton.diffuse.diff.toDetailReport +import com.jakewharton.diffuse.diff.toHtmlSummary +import com.jakewharton.diffuse.diff.toSummaryTable +import com.jakewharton.diffuse.format.ArchiveFile.Type +import com.jakewharton.diffuse.report.Report +import kotlinx.html.body +import kotlinx.html.br +import kotlinx.html.details +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.span +import kotlinx.html.stream.appendHTML +import kotlinx.html.summary + +internal class AarDiffHtmlReport(private val aarDiff: AarDiff) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + span { +"OLD: ${aarDiff.oldAar.filename}" } + br() + span { +"NEW: ${aarDiff.newAar.filename}" } + br() + + toSummaryTable( + "AAR", + aarDiff.archive, + Type.AAR_TYPES, + skipIfEmptyTypes = setOf(Type.JarLibs, Type.ApiJar, Type.LintJar, Type.Native, Type.Res), + ) + + br() + + toHtmlSummary("", aarDiff.jars) + + if (aarDiff.archive.changed) { + br() + details { + summary { +"JAR" } + toDetailReport(aarDiff.archive) + } + } + + if (aarDiff.jars.changed) { + br() + details { + summary { +"CLASSFILES" } + toDetailReport(aarDiff.jars) + } + } + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AarInfoHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AarInfoHtmlReport.kt new file mode 100644 index 00000000..5f9feb3c --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/AarInfoHtmlReport.kt @@ -0,0 +1,38 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.format.Aar +import com.jakewharton.diffuse.format.ArchiveFile.Type +import com.jakewharton.diffuse.info.toSummaryTable +import com.jakewharton.diffuse.report.Report +import kotlinx.html.body +import kotlinx.html.h2 +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.stream.appendHTML + +class AarInfoHtmlReport( + private val aar: Aar, +) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + appendable.apply { + h2 { +aar.filename.toString() } + + toSummaryTable( + "AAR", + aar.files, + Type.AAR_TYPES, + skipIfEmptyTypes = setOf(Type.JarLibs, Type.ApiJar, Type.LintJar, Type.Native, Type.Res), + ) + + toSummaryTable("JAR", aar.jars) + } + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/ApkDiffHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/ApkDiffHtmlReport.kt new file mode 100644 index 00000000..4012db3d --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/ApkDiffHtmlReport.kt @@ -0,0 +1,78 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.diff.ApkDiff +import com.jakewharton.diffuse.diff.toDetailReport +import com.jakewharton.diffuse.diff.toSummaryTable +import com.jakewharton.diffuse.format.ArchiveFile.Type +import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.toSummaryString +import kotlinx.html.body +import kotlinx.html.br +import kotlinx.html.details +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.span +import kotlinx.html.stream.appendHTML +import kotlinx.html.summary + +internal class ApkDiffHtmlReport( + private val diff: ApkDiff, +) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + span { +"OLD: ${diff.oldApk.filename} (signature: ${diff.oldApk.signatures.toSummaryString()})" } + span { +"NEW: ${diff.newApk.filename} (signature: ${diff.newApk.signatures.toSummaryString()})" } + br() + + toSummaryTable("APK", diff.archive, Type.APK_TYPES, skipIfEmptyTypes = setOf(Type.Native)) + br() + + toSummaryTable(diff.dex) + br() + + toSummaryTable(diff.arsc) + br() + + if (diff.archive.changed) { + details { + summary { +"Archive" } + toDetailReport(diff.archive) + } + } + + if (diff.signatures.changed) { + details { + summary { +"Signatures" } + toDetailReport(diff.signatures) + } + } + + if (diff.manifest.changed) { + details { + summary { +"Manifest" } + toDetailReport(diff.manifest) + } + } + + if (diff.dex.changed) { + details { + summary { +"Dex" } + toDetailReport(diff.dex) + } + } + + if (diff.arsc.changed) { + details { + summary { +"ARSC" } + toDetailReport(diff.arsc) + } + } + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/ApkInfoHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/ApkInfoHtmlReport.kt new file mode 100644 index 00000000..dbe5999d --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/ApkInfoHtmlReport.kt @@ -0,0 +1,41 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.format.Apk +import com.jakewharton.diffuse.format.ArchiveFile +import com.jakewharton.diffuse.info.toSummaryTable +import com.jakewharton.diffuse.report.Report +import com.jakewharton.diffuse.report.toSummaryString +import kotlinx.html.body +import kotlinx.html.br +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.p +import kotlinx.html.stream.appendHTML + +internal class ApkInfoHtmlReport( + private val apk: Apk, +) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + p { +"${apk.filename} (signature: ${apk.signatures.toSummaryString()})" } + + toSummaryTable( + "APK", + apk.files, + ArchiveFile.Type.APK_TYPES, + skipIfEmptyTypes = setOf(ArchiveFile.Type.Native), + ) + + br() + toSummaryTable(apk.dexes) + br() + toSummaryTable(apk.arsc) + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/DexDiffHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/DexDiffHtmlReport.kt new file mode 100644 index 00000000..3a88512c --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/DexDiffHtmlReport.kt @@ -0,0 +1,37 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.diff.DexDiff +import com.jakewharton.diffuse.diff.toDetailReport +import com.jakewharton.diffuse.report.Report +import kotlinx.html.body +import kotlinx.html.br +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.span +import kotlinx.html.stream.appendHTML + +internal class DexDiffHtmlReport(private val dexDiff: DexDiff) : Report { + private val oldDex = requireNotNull(dexDiff.oldDexes.singleOrNull()) { + "Dex diff report only supports a single old dex. Found: ${dexDiff.oldDexes}" + } + private val newDex = requireNotNull(dexDiff.newDexes.singleOrNull()) { + "Dex diff report only supports a single new dex. Found: ${dexDiff.newDexes}" + } + + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + span { +"OLD: ${oldDex.filename}" } + br() + span { +"NEW: ${newDex.filename}" } + br() + + toDetailReport(dexDiff) + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/DexInfoHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/DexInfoHtmlReport.kt new file mode 100644 index 00000000..962c1d71 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/DexInfoHtmlReport.kt @@ -0,0 +1,26 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.format.Dex +import com.jakewharton.diffuse.info.toSummaryTable +import com.jakewharton.diffuse.report.Report +import kotlinx.html.body +import kotlinx.html.h2 +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.stream.appendHTML + +internal class DexInfoHtmlReport(private val dex: Dex) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + h2 { +dex.filename } + + toSummaryTable(listOf(dex)) + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/JarDiffHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/JarDiffHtmlReport.kt new file mode 100644 index 00000000..4dd421d4 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/JarDiffHtmlReport.kt @@ -0,0 +1,56 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.diff.JarDiff +import com.jakewharton.diffuse.diff.toDetailReport +import com.jakewharton.diffuse.diff.toHtmlSummary +import com.jakewharton.diffuse.diff.toSummaryTable +import com.jakewharton.diffuse.format.ArchiveFile.Type +import com.jakewharton.diffuse.report.Report +import kotlinx.html.body +import kotlinx.html.br +import kotlinx.html.details +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.span +import kotlinx.html.stream.appendHTML +import kotlinx.html.summary + +internal class JarDiffHtmlReport(private val jarDiff: JarDiff) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + span { +"OLD: ${jarDiff.oldJar.filename}" } + br() + span { +"NEW: ${jarDiff.newJar.filename}" } + br() + br() + + toSummaryTable("JAR", jarDiff.archive, Type.JAR_TYPES) + + br() + + toHtmlSummary("", jarDiff.jars) + + if (jarDiff.archive.changed) { + br() + details { + summary { +"JAR" } + toDetailReport(jarDiff.archive) + } + } + + if (jarDiff.jars.changed) { + br() + details { + summary { +"CLASSFILES" } + toDetailReport(jarDiff.jars) + } + } + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/JarInfoHtmlReport.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/JarInfoHtmlReport.kt new file mode 100644 index 00000000..38270b2d --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/JarInfoHtmlReport.kt @@ -0,0 +1,38 @@ +package com.jakewharton.diffuse.report.html + +import com.jakewharton.diffuse.format.ArchiveFile.Type +import com.jakewharton.diffuse.format.Jar +import com.jakewharton.diffuse.info.toSummaryTable +import com.jakewharton.diffuse.report.Report +import kotlinx.html.body +import kotlinx.html.br +import kotlinx.html.details +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.p +import kotlinx.html.stream.appendHTML +import kotlinx.html.summary + +internal class JarInfoHtmlReport(private val jar: Jar) : Report { + override fun write(appendable: Appendable) { + appendable.appendHTML().html { + head { applyStyles() } + + body { + p { +jar.filename!! } + + toSummaryTable("JAR", jar.files, Type.JAR_TYPES) + + br() + + details { + summary { +"CLASSFILE INFO" } + + toSummaryTable("entity", listOf(jar)) + } + } + } + } + + override fun toString() = buildString { write(this) } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/styles.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/styles.kt new file mode 100644 index 00000000..2e3e46c5 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/html/styles.kt @@ -0,0 +1,35 @@ +package com.jakewharton.diffuse.report.html + +import kotlinx.html.HEAD +import kotlinx.html.link +import kotlinx.html.style +import kotlinx.html.unsafe + +internal fun HEAD.applyStyles() { + link("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css", "stylesheet") { + integrity = "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" + attributes["crossorigin"] = "anonymous" + } + + style("text/css") { + unsafe { + raw( + """ + table { + border-collapse:collapse; + border:1px solid var(--bs-body-color); + } + + table td { + border:1px solid var(--bs-body-color); + padding: 4px; + } + + body { + margin: 0 16pt; + } + """.trimIndent(), + ) + } + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/report/strings.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/report/strings.kt index a7834bb8..08609f40 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/report/strings.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/report/strings.kt @@ -25,3 +25,19 @@ internal fun Size.toDiffString() = buildString { } append(this@toDiffString) } + +internal val String.htmlEncoded: String + get() = buildString { + for (char in this@htmlEncoded) { + when (char) { + '&' -> append("&") + '<' -> append("<") + '>' -> append(">") + '\"' -> append(""") + '\'' -> append("'") + '→' -> append("→") + '∆' -> append("Δ") + else -> append(char) + } + } + }