Skip to content

Commit 77d9173

Browse files
committed
feat: Add function to get the most common compatible version
This adds a function to get the version that is most common for a given package name in a supplied set of patches.
1 parent 3846f72 commit 77d9173

File tree

6 files changed

+165
-78
lines changed

6 files changed

+165
-78
lines changed

revanced-lib/api/revanced-lib.api

+32-27
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
public final class app/revanced/lib/ApkSigner {
2+
public static final field INSTANCE Lapp/revanced/lib/ApkSigner;
3+
public final fun newApkSignerBuilder (Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder;
4+
public final fun newApkSignerBuilder (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder;
5+
public final fun newKeyStore (Ljava/util/List;)Ljava/security/KeyStore;
6+
public final fun newKeystore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V
7+
public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
8+
public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/lib/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
9+
public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
10+
public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore;
11+
public final fun signApk (Lcom/android/apksig/ApkSigner$Builder;Ljava/io/File;Ljava/io/File;)V
12+
}
13+
14+
public final class app/revanced/lib/ApkSigner$KeyStoreEntry {
15+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;)V
16+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
17+
public final fun getAlias ()Ljava/lang/String;
18+
public final fun getPassword ()Ljava/lang/String;
19+
public final fun getPrivateKeyCertificatePair ()Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
20+
}
21+
22+
public final class app/revanced/lib/ApkSigner$PrivateKeyCertificatePair {
23+
public fun <init> (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V
24+
public final fun getCertificate ()Ljava/security/cert/X509Certificate;
25+
public final fun getPrivateKey ()Ljava/security/PrivateKey;
26+
}
27+
128
public final class app/revanced/lib/ApkUtils {
229
public static final field INSTANCE Lapp/revanced/lib/ApkUtils;
330
public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V
@@ -33,6 +60,11 @@ public final class app/revanced/lib/Options$Patch$Option {
3360
public final fun getValue ()Ljava/lang/Object;
3461
}
3562

63+
public final class app/revanced/lib/PatchUtils {
64+
public static final field INSTANCE Lapp/revanced/lib/PatchUtils;
65+
public final fun getMostCommonCompatibleVersion (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String;
66+
}
67+
3668
public abstract class app/revanced/lib/adb/AdbManager {
3769
public static final field Companion Lapp/revanced/lib/adb/AdbManager$Companion;
3870
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -86,33 +118,6 @@ public final class app/revanced/lib/logging/Logger {
86118
public static synthetic fun setFormat$default (Lapp/revanced/lib/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V
87119
}
88120

89-
public final class app/revanced/lib/signing/ApkSigner {
90-
public static final field INSTANCE Lapp/revanced/lib/signing/ApkSigner;
91-
public final fun newApkSignerBuilder (Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder;
92-
public final fun newApkSignerBuilder (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder;
93-
public final fun newKeyStore (Ljava/util/List;)Ljava/security/KeyStore;
94-
public final fun newKeystore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V
95-
public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;
96-
public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/lib/signing/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;
97-
public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;
98-
public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore;
99-
public final fun signApk (Lcom/android/apksig/ApkSigner$Builder;Ljava/io/File;Ljava/io/File;)V
100-
}
101-
102-
public final class app/revanced/lib/signing/ApkSigner$KeyStoreEntry {
103-
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;)V
104-
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
105-
public final fun getAlias ()Ljava/lang/String;
106-
public final fun getPassword ()Ljava/lang/String;
107-
public final fun getPrivateKeyCertificatePair ()Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;
108-
}
109-
110-
public final class app/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair {
111-
public fun <init> (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V
112-
public final fun getCertificate ()Ljava/security/cert/X509Certificate;
113-
public final fun getPrivateKey ()Ljava/security/PrivateKey;
114-
}
115-
116121
public final class app/revanced/lib/zip/ZipFile : java/io/Closeable {
117122
public static final field ApkZipFile Lapp/revanced/lib/zip/ZipFile$ApkZipFile;
118123
public fun <init> (Ljava/io/File;)V

revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt renamed to revanced-lib/src/main/kotlin/app/revanced/lib/ApkSigner.kt

+39-35
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package app.revanced.lib.signing
1+
package app.revanced.lib
22

33
import com.android.apksig.ApkSigner
44
import org.bouncycastle.asn1.x500.X500Name
@@ -18,9 +18,12 @@ import java.util.*
1818
import java.util.logging.Logger
1919
import kotlin.time.Duration.Companion.days
2020

21-
@Suppress("unused", "MemberVisibilityCanBePrivate")
21+
/**
22+
* Utility class for writing or reading keystore files and entries as well as signing APK files.
23+
*/
24+
@Suppress("MemberVisibilityCanBePrivate", "unused")
2225
object ApkSigner {
23-
private val logger = Logger.getLogger(app.revanced.lib.signing.ApkSigner::class.java.name)
26+
private val logger = Logger.getLogger(app.revanced.lib.ApkSigner::class.java.name)
2427

2528
init {
2629
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
@@ -67,6 +70,39 @@ object ApkSigner {
6770
return PrivateKeyCertificatePair(keyPair.private, certificate)
6871
}
6972

73+
74+
/**
75+
* Read a [PrivateKeyCertificatePair] from a keystore entry.
76+
*
77+
* @param keyStore The keystore to read the entry from.
78+
* @param keyStoreEntryAlias The alias of the key store entry to read.
79+
* @param keyStoreEntryPassword The password for recovering the signing key.
80+
* @return The read [PrivateKeyCertificatePair].
81+
* @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid.
82+
*/
83+
fun readKeyCertificatePair(
84+
keyStore: KeyStore,
85+
keyStoreEntryAlias: String,
86+
keyStoreEntryPassword: String,
87+
): PrivateKeyCertificatePair {
88+
logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias")
89+
90+
if (!keyStore.containsAlias(keyStoreEntryAlias))
91+
throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias")
92+
93+
// Read the private key and certificate from the keystore.
94+
95+
val privateKey = try {
96+
keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey
97+
} catch (exception: UnrecoverableKeyException) {
98+
throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias")
99+
}
100+
101+
val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate
102+
103+
return PrivateKeyCertificatePair(privateKey, certificate)
104+
}
105+
70106
/**
71107
* Create a new keystore with a new keypair.
72108
*
@@ -167,38 +203,6 @@ object ApkSigner {
167203
}
168204
}
169205

170-
/**
171-
* Read a [PrivateKeyCertificatePair] from a keystore entry.
172-
*
173-
* @param keyStore The keystore to read the entry from.
174-
* @param keyStoreEntryAlias The alias of the key store entry to read.
175-
* @param keyStoreEntryPassword The password for recovering the signing key.
176-
* @return The read [PrivateKeyCertificatePair].
177-
* @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid.
178-
*/
179-
fun readKeyCertificatePair(
180-
keyStore: KeyStore,
181-
keyStoreEntryAlias: String,
182-
keyStoreEntryPassword: String,
183-
): PrivateKeyCertificatePair {
184-
logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias")
185-
186-
if (!keyStore.containsAlias(keyStoreEntryAlias))
187-
throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias")
188-
189-
// Read the private key and certificate from the keystore.
190-
191-
val privateKey = try {
192-
keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey
193-
} catch (exception: UnrecoverableKeyException) {
194-
throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias")
195-
}
196-
197-
val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate
198-
199-
return PrivateKeyCertificatePair(privateKey, certificate)
200-
}
201-
202206
/**
203207
* Create a new [ApkSigner.Builder].
204208
*

revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package app.revanced.lib
22

3-
import app.revanced.lib.signing.ApkSigner
4-
import app.revanced.lib.signing.ApkSigner.signApk
3+
import app.revanced.lib.ApkSigner.signApk
54
import app.revanced.lib.zip.ZipFile
65
import app.revanced.lib.zip.structures.ZipEntry
76
import app.revanced.patcher.PatcherResult
87
import java.io.File
98
import java.util.logging.Logger
109
import kotlin.io.path.deleteIfExists
1110

11+
/**
12+
* Utility functions for working with apks.
13+
*/
1214
@Suppress("MemberVisibilityCanBePrivate", "unused")
1315
object ApkUtils {
1416
private val logger = Logger.getLogger(ApkUtils::class.java.name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package app.revanced.lib
2+
3+
import app.revanced.patcher.PatchSet
4+
5+
/**
6+
* Utility functions for working with patches.
7+
*/
8+
@Suppress("MemberVisibilityCanBePrivate", "unused")
9+
object PatchUtils {
10+
/**
11+
* Get the version that is most common for [packageName] in the supplied set of [patches].
12+
*
13+
* @param patches The set of patches to check.
14+
* @param packageName The name of the compatible package.
15+
* @return The most common version of.
16+
*/
17+
fun getMostCommonCompatibleVersion(patches: PatchSet, packageName: String) = patches
18+
.mapNotNull {
19+
// Map all patches to their compatible packages with version constraints.
20+
it.compatiblePackages?.firstOrNull { compatiblePackage ->
21+
compatiblePackage.name == packageName && compatiblePackage.versions?.isNotEmpty() == true
22+
}
23+
}
24+
.flatMap { it.versions!! }
25+
.groupingBy { it }
26+
.eachCount()
27+
.maxByOrNull { it.value }?.key
28+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
package app.revanced.patcher.options
1+
package app.revanced.lib
22

3-
import app.revanced.lib.Options
43
import app.revanced.lib.Options.setOptions
54
import app.revanced.patcher.data.BytecodeContext
65
import app.revanced.patcher.patch.BytecodePatch
@@ -11,16 +10,6 @@ import org.junit.jupiter.api.Order
1110
import org.junit.jupiter.api.Test
1211
import org.junit.jupiter.api.TestMethodOrder
1312

14-
15-
object PatchOptionsTestPatch : BytecodePatch(name = "PatchOptionsTestPatch") {
16-
var key1 by stringPatchOption("key1", null, "title1", "description1")
17-
var key2 by booleanPatchOption("key2", true, "title2", "description2")
18-
19-
override fun execute(context: BytecodeContext) {
20-
// Do nothing
21-
}
22-
}
23-
2413
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
2514
internal object PatchOptionsTest {
2615
private var patches = setOf(PatchOptionsTestPatch)
@@ -36,13 +25,22 @@ internal object PatchOptionsTest {
3625
fun loadOptionsTest() {
3726
patches.setOptions(CHANGED_JSON)
3827

39-
assert(PatchOptionsTestPatch.key1 == "test")
40-
assert(PatchOptionsTestPatch.key2 == false)
28+
assert(PatchOptionsTestPatch.option1 == "test")
29+
assert(PatchOptionsTestPatch.option2 == false)
4130
}
4231

4332
private const val SERIALIZED_JSON =
4433
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":null},{\"key\":\"key2\",\"value\":true}]}]"
4534

4635
private const val CHANGED_JSON =
4736
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]"
37+
38+
object PatchOptionsTestPatch : BytecodePatch(name = "PatchOptionsTestPatch") {
39+
var option1 by stringPatchOption("key1", null, "title1", "description1")
40+
var option2 by booleanPatchOption("key2", true, "title2", "description2")
41+
42+
override fun execute(context: BytecodeContext) {
43+
// Do nothing
44+
}
45+
}
4846
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package app.revanced.lib
2+
3+
import app.revanced.patcher.PatchSet
4+
import app.revanced.patcher.data.BytecodeContext
5+
import app.revanced.patcher.patch.BytecodePatch
6+
import org.junit.jupiter.api.Test
7+
import kotlin.test.assertEquals
8+
9+
internal object PatchUtilsTest {
10+
@Test
11+
fun `return 'a' because it is the most common version`() {
12+
val patches = arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d")
13+
.map { version -> newPatch("some.package", version) }
14+
.toSet()
15+
16+
assertEqualsVersion("a", patches, "some.package")
17+
}
18+
19+
@Test
20+
fun `return null because no patches were supplied`() {
21+
assertEqualsVersion(null, emptySet<BytecodePatch>(), "some.package")
22+
}
23+
24+
@Test
25+
fun `return null because no patch is compatible with the supplied package name`() {
26+
val patches = setOf(newPatch("other.package", "a"))
27+
28+
assertEqualsVersion(null, patches, "other.package")
29+
}
30+
31+
@Test
32+
fun `return null because no patch compatible package is constrained to a version`() {
33+
val patches = setOf(
34+
newPatch("other.package"),
35+
newPatch("other.package"),
36+
)
37+
38+
assertEqualsVersion(null, patches, "other.package")
39+
}
40+
41+
private fun assertEqualsVersion(
42+
expected: String?, patches: PatchSet, compatiblePackageName: String
43+
) = assertEquals(expected, PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName))
44+
45+
private fun newPatch(packageName: String, vararg versions: String) = object : BytecodePatch(
46+
compatiblePackages = setOf(CompatiblePackage(packageName, versions.toSet()))
47+
) {
48+
override fun execute(context: BytecodeContext) {}
49+
}
50+
}

0 commit comments

Comments
 (0)