From c5f3536cbb6997766076595dc0b2b5d2e861ca73 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 27 Nov 2023 02:23:47 +0100 Subject: [PATCH] feat: Add `PatchUtils#getMostCommonCompatibleVersions` utility function --- api/revanced-library.api | 3 + .../kotlin/app/revanced/library/PatchUtils.kt | 46 +++++++ .../app/revanced/library/PatchUtilsTest.kt | 113 +++++++++++++++++- 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/api/revanced-library.api b/api/revanced-library.api index 2d0cddb..e020e1e 100644 --- a/api/revanced-library.api +++ b/api/revanced-library.api @@ -63,6 +63,8 @@ public final class app/revanced/library/Options$Patch$Option { public final class app/revanced/library/PatchUtils { public static final field INSTANCE Lapp/revanced/library/PatchUtils; public final fun getMostCommonCompatibleVersion (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String; + public final fun getMostCommonCompatibleVersions (Ljava/util/Set;Ljava/util/Set;Z)Ljava/util/Map; + public static synthetic fun getMostCommonCompatibleVersions$default (Lapp/revanced/library/PatchUtils;Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map; } public abstract class app/revanced/library/adb/AdbManager { @@ -87,6 +89,7 @@ public final class app/revanced/library/adb/AdbManager$Companion { } public final class app/revanced/library/adb/AdbManager$DeviceNotFoundException : java/lang/Exception { + public fun ()V } public final class app/revanced/library/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception { diff --git a/src/main/kotlin/app/revanced/library/PatchUtils.kt b/src/main/kotlin/app/revanced/library/PatchUtils.kt index 716814c..de04dcd 100644 --- a/src/main/kotlin/app/revanced/library/PatchUtils.kt +++ b/src/main/kotlin/app/revanced/library/PatchUtils.kt @@ -1,6 +1,14 @@ package app.revanced.library import app.revanced.patcher.PatchSet +import java.util.* + +private typealias PackageName = String +private typealias Version = String +private typealias Count = Int + +private typealias VersionMap = SortedMap +internal typealias PackageNameMap = Map /** * Utility functions for working with patches. @@ -14,6 +22,13 @@ object PatchUtils { * @param packageName The name of the compatible package. * @return The most common version of. */ + @Deprecated( + "Use getMostCommonCompatibleVersions instead.", + ReplaceWith( + "getMostCommonCompatibleVersions(patches, setOf(packageName))" + + ".entries.firstOrNull()?.value?.keys?.firstOrNull()", + ), + ) fun getMostCommonCompatibleVersion( patches: PatchSet, packageName: String, @@ -28,4 +43,35 @@ object PatchUtils { .groupingBy { it } .eachCount() .maxByOrNull { it.value }?.key + + /** + * Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version. + * + * @param patches The set of patches to check. + * @param packageNames The names of the compatible packages. + * @param countUnusedPatches Whether to count patches that are not used. + * @return A map of package names to a map of versions to their count. + */ + fun getMostCommonCompatibleVersions( + patches: PatchSet, + packageNames: Set, + countUnusedPatches: Boolean = false, + ): PackageNameMap { + val wantedPackages = packageNames.toHashSet() + return buildMap { + patches + .filter { it.use || countUnusedPatches } + .flatMap { it.compatiblePackages ?: emptyList() } + .filter { it.name in wantedPackages } + .forEach { compatiblePackage -> + compatiblePackage.versions?.let { versions -> + val versionMap = getOrPut(compatiblePackage.name) { sortedMapOf() } + + versions.forEach { version -> + versionMap[version] = versionMap.getOrDefault(version, 0) + 1 + } + } + } + } + } } diff --git a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt b/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt index 9b8ada2..669c214 100644 --- a/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt +++ b/src/test/kotlin/app/revanced/library/PatchUtilsTest.kt @@ -8,6 +8,83 @@ import org.junit.jupiter.api.Test import kotlin.test.assertEquals internal object PatchUtilsTest { + private val patches = + arrayOf( + newPatch("some.package", "a"), + newPatch("some.package", "a", "b", use = false), + newPatch("some.package", "a", "b", "c", use = false), + newPatch("some.other.package", "b", use = false), + newPatch("some.other.package", "b", "c"), + newPatch("some.other.package", "b", "c", "d"), + newPatch("some.other.other.package"), + newPatch("some.other.other.package", "a"), + newPatch("some.other.other.package", "b"), + newPatch("some.other.other.other.package", use = false), + newPatch("some.other.other.other.package", use = false), + ).toSet() + + @Test + fun `return common versions correctly ordered for each package`() { + assertEqualsVersions( + expected = + mapOf( + "some.package" to sortedMapOf("a" to 3, "b" to 2, "c" to 1), + "some.other.package" to sortedMapOf("b" to 3, "c" to 2, "d" to 1), + "some.other.other.package" to sortedMapOf("a" to 1, "b" to 1), + "some.other.other.other.package" to sortedMapOf(), + ), + patches, + compatiblePackageNames = + setOf( + "some.package", + "some.other.package", + "some.other.other.package", + "some.other.other.other.package", + ), + countUnusedPatches = true, + ) + } + + @Test + fun `return common versions correctly ordered for each package without counting unused patches`() { + assertEqualsVersions( + expected = + mapOf( + "some.package" to sortedMapOf("a" to 1), + "some.other.package" to sortedMapOf("b" to 2, "c" to 2, "d" to 1), + "some.other.other.package" to sortedMapOf("a" to 1, "b" to 1), + ), + patches, + compatiblePackageNames = + setOf( + "some.package", + "some.other.package", + "some.other.other.package", + "some.other.other.other.package", + ), + countUnusedPatches = false, + ) + } + + @Test + fun `return an empty map because no known package was supplied`() { + assertEqualsVersions( + expected = emptyMap(), + patches, + compatiblePackageNames = setOf("unknown.package"), + ) + } + + @Test + fun `return empty set of versions because no compatible package is constrained to a version`() { + assertEqualsVersions( + expected = mapOf("some.package" to sortedMapOf()), + patches = setOf(newPatch("some.package")), + compatiblePackageNames = setOf("some.package"), + countUnusedPatches = true, + ) + } + @Test fun `return 'a' because it is the most common version`() { val patches = @@ -31,7 +108,7 @@ internal object PatchUtilsTest { } @Test - fun `return null because no patch compatible package is constrained to a version`() { + fun `return null because no compatible package is constrained to a version`() { val patches = setOf( newPatch("other.package"), @@ -41,15 +118,39 @@ internal object PatchUtilsTest { assertEqualsVersion(null, patches, "other.package") } + private fun assertEqualsVersions( + expected: PackageNameMap, + patches: PatchSet, + compatiblePackageNames: Set, + countUnusedPatches: Boolean = false, + ) = assertEquals( + expected, + PatchUtils.getMostCommonCompatibleVersions(patches, compatiblePackageNames, countUnusedPatches), + ) + private fun assertEqualsVersion( expected: String?, patches: PatchSet, compatiblePackageName: String, - ) = assertEquals(expected, PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName)) + ) { + // Test both the deprecated and the new method. + + assertEquals( + expected, + PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName), + ) + + assertEquals( + expected, + PatchUtils.getMostCommonCompatibleVersions(patches, setOf(compatiblePackageName)) + .entries.firstOrNull()?.value?.keys?.firstOrNull(), + ) + } private fun newPatch( packageName: String, vararg versions: String, + use: Boolean = true, ) = object : BytecodePatch() { init { // Set the compatible packages field to the supplied package name and versions reflectively, @@ -58,8 +159,16 @@ internal object PatchUtilsTest { compatiblePackagesField.isAccessible = true compatiblePackagesField.set(this, setOf(CompatiblePackage(packageName, versions.toSet()))) + + val useField = Patch::class.java.getDeclaredField("use") + + useField.isAccessible = true + useField.set(this, use) } override fun execute(context: BytecodeContext) {} + + // Needed to make the patches unique. + override fun equals(other: Any?) = false } }