From e74498aff5f6f2950e6e4832d1567779218e355e Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 16 Oct 2025 22:46:51 +0200 Subject: [PATCH 1/2] feat: Add callback for patch loading exceptions --- api/revanced-patcher.api | 9 +- .../app/revanced/patcher/patch/Patch.kt | 141 ++++++++++++------ 2 files changed, 98 insertions(+), 52 deletions(-) diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index 2f173eb8..b06d6694 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -342,8 +342,12 @@ public final class app/revanced/patcher/patch/PatchKt { public static final fun bytecodePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch; public static synthetic fun bytecodePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; public static final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;)Lapp/revanced/patcher/patch/PatchLoader$Dex; + public static final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;Lkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/PatchLoader$Dex; public static synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;ILjava/lang/Object;)Lapp/revanced/patcher/patch/PatchLoader$Dex; + public static synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/PatchLoader$Dex; public static final fun loadPatchesFromJar (Ljava/util/Set;)Lapp/revanced/patcher/patch/PatchLoader$Jar; + public static final fun loadPatchesFromJar (Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/PatchLoader$Jar; + public static synthetic fun loadPatchesFromJar$default (Ljava/util/Set;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/PatchLoader$Jar; public static final fun rawResourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch; public static synthetic fun rawResourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch; public static final fun resourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/ResourcePatch; @@ -352,7 +356,7 @@ public final class app/revanced/patcher/patch/PatchKt { public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker { public synthetic fun (Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun add (Lapp/revanced/patcher/patch/Patch;)Z public synthetic fun add (Ljava/lang/Object;)Z public fun addAll (Ljava/util/Collection;)Z @@ -373,12 +377,9 @@ public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, ko } public final class app/revanced/patcher/patch/PatchLoader$Dex : app/revanced/patcher/patch/PatchLoader { - public fun (Ljava/util/Set;Ljava/io/File;)V - public synthetic fun (Ljava/util/Set;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class app/revanced/patcher/patch/PatchLoader$Jar : app/revanced/patcher/patch/PatchLoader { - public fun (Ljava/util/Set;)V } public final class app/revanced/patcher/patch/PatchResult { diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 39d27b3e..2bbe1733 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -87,8 +87,7 @@ sealed class Patch>( finalizeBlock?.invoke(context) } - override fun toString() = name ?: - "Patch@${System.identityHashCode(this)}" + override fun toString() = name ?: "Patch@${System.identityHashCode(this)}" } internal fun Patch<*>.anyRecursively( @@ -529,11 +528,11 @@ fun resourcePatch( /** * An exception thrown when patching. * - * @param errorMessage The exception message. - * @param cause The corresponding [Throwable]. + * @param message The exception message. + * @param cause The cause of the exception. */ -class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) { - constructor(errorMessage: String) : this(errorMessage, null) +class PatchException(message: String?, cause: Throwable?) : Exception(message, cause) { + constructor(message: String) : this(message, null) constructor(cause: Throwable) : this(cause.message, cause) } @@ -543,6 +542,7 @@ class PatchException(errorMessage: String?, cause: Throwable?) : Exception(error * @param patch The [Patch] that was executed. * @param exception The [PatchException] thrown, if any. */ +@Deprecated("This class is not used anymore. Instead a callback is used") class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null) /** @@ -553,36 +553,47 @@ class PatchResult internal constructor(val patch: Patch<*>, val exception: Patch * * @param byPatchesFile The patches associated by the patches file they were loaded from. */ -sealed class PatchLoader private constructor( +sealed class PatchLoader( val byPatchesFile: Map>>, ) : Set> by byPatchesFile.values.flatten().toSet() { /** * @param patchesFiles A set of JAR or DEX files to load the patches from. * @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader. * @param classLoader The [ClassLoader] to use for loading the classes. + * @param onLoadPatchesException The callback for patches that could not be loaded. */ - private constructor( + constructor( patchesFiles: Set, getBinaryClassNames: (patchesFile: File) -> List, classLoader: ClassLoader, - ) : this(classLoader.loadPatches(patchesFiles.associateWith { getBinaryClassNames(it).toSet() })) + onLoadPatchesException: (message: String, cause: Throwable) -> Unit + ) : this( + classLoader.loadPatches( + patchesFiles.associateWith { getBinaryClassNames(it).toSet() }, + onLoadPatchesException + ) + ) /** * A [PatchLoader] for JAR files. * * @param patchesFiles The JAR files to load the patches from. + * @param onLoadPatchesException The callback for patches that could not be loaded. * * @constructor Create a new [PatchLoader] for JAR files. */ - class Jar(patchesFiles: Set) : - PatchLoader( - patchesFiles, - { file -> - JarFile(file).entries().toList().filter { it.name.endsWith(".class") } - .map { it.name.substringBeforeLast('.').replace('/', '.') } - }, - URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()), - ) + class Jar internal constructor( + patchesFiles: Set, + onLoadPatchesException: (message: String, cause: Throwable) -> Unit + ) : PatchLoader( + patchesFiles, + { file -> + JarFile(file).entries().toList().filter { it.name.endsWith(".class") } + .map { it.name.substringBeforeLast('.').replace('/', '.') } + }, + URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()), + onLoadPatchesException + ) /** * A [PatchLoader] for [Dex] files. @@ -590,25 +601,30 @@ sealed class PatchLoader private constructor( * @param patchesFiles The DEX files to load the patches from. * @param optimizedDexDirectory The directory to store optimized DEX files in. * This parameter is deprecated and has no effect since API level 26. + * @param onLoadPatchesException The callback for patches that could not be loaded. * * @constructor Create a new [PatchLoader] for [Dex] files. */ - class Dex(patchesFiles: Set, optimizedDexDirectory: File? = null) : - PatchLoader( - patchesFiles, - { patchBundle -> - MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes - .map { classDef -> - classDef.type.substring(1, classDef.length - 1) - } - }, - DexClassLoader( - patchesFiles.joinToString(File.pathSeparator) { it.absolutePath }, - optimizedDexDirectory?.absolutePath, - null, - this::class.java.classLoader, - ), - ) + class Dex internal constructor( + patchesFiles: Set, + optimizedDexDirectory: File? = null, + onLoadPatchesException: (message: String, cause: Throwable) -> Unit + ) : PatchLoader( + patchesFiles, + { patchBundle -> + MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes + .map { classDef -> + classDef.type.substring(1, classDef.length - 1) + } + }, + DexClassLoader( + patchesFiles.joinToString(File.pathSeparator) { it.absolutePath }, + optimizedDexDirectory?.absolutePath, + null, + this::class.java.classLoader, + ), + onLoadPatchesException + ) // Companion object required for unit tests. private companion object { @@ -640,19 +656,24 @@ sealed class PatchLoader private constructor( * * @param binaryClassNamesByPatchesFile The binary class name of the classes to load the patches from * associated by the patches file. + * @param onLoadPatchesException The callback for patches that could not be loaded. * * @return The loaded patches associated by the patches file. */ - private fun ClassLoader.loadPatches(binaryClassNamesByPatchesFile: Map>) = - binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) -> - binaryClassNames.asSequence().map { - loadClass(it) - }.flatMap { - it.patchFields + it.patchMethods - }.filter { - it.name != null - }.toSet() - } + private fun ClassLoader.loadPatches( + binaryClassNamesByPatchesFile: Map>, + onLoadPatchesException: (message: String, cause: Throwable) -> Unit + ) = binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) -> + binaryClassNames.asSequence().mapNotNull { + runCatching { loadClass(it) }.onFailure { exception -> + onLoadPatchesException("Failed to load patch class $it", exception) + }.getOrNull() + }.flatMap { + it.patchFields + it.patchMethods + }.filter { + it.name != null + }.toSet() + } private fun Member.canAccess(): Boolean { if (this is Method && parameterCount != 0) return false @@ -668,11 +689,22 @@ sealed class PatchLoader private constructor( * Patches with no name are not loaded. * * @param patchesFiles The JAR files to load the patches from. + * @param onLoadPatchesException The callback for patches that could not be loaded. * * @return The loaded patches. */ -fun loadPatchesFromJar(patchesFiles: Set) = - PatchLoader.Jar(patchesFiles) +fun loadPatchesFromJar( + patchesFiles: Set, + onLoadPatchesException: ((message: String, cause: Throwable) -> Unit)? = null +) = PatchLoader.Jar(patchesFiles, onLoadPatchesException ?: { message, cause -> }) + +@Deprecated( + "Use the function with the onLoadPatchesException overload", + replaceWith = ReplaceWith("loadPatchesFromJar(patchesFiles, null)") +) +fun loadPatchesFromJar( + patchesFiles: Set +) = loadPatchesFromJar(patchesFiles, null) /** * Loads patches from DEX files declared as public static fields @@ -680,8 +712,21 @@ fun loadPatchesFromJar(patchesFiles: Set) = * Patches with no name are not loaded. * * @param patchesFiles The DEX files to load the patches from. + * @param onLoadPatchesException The callback for patches that could not be loaded. * * @return The loaded patches. */ -fun loadPatchesFromDex(patchesFiles: Set, optimizedDexDirectory: File? = null) = - PatchLoader.Dex(patchesFiles, optimizedDexDirectory) +fun loadPatchesFromDex( + patchesFiles: Set, + optimizedDexDirectory: File? = null, + onLoadPatchesException: ((message: String, cause: Throwable) -> Unit)? = null +) = PatchLoader.Dex(patchesFiles, optimizedDexDirectory, onLoadPatchesException ?: { message, cause -> }) + +@Deprecated( + "Use the function with the onLoadPatchesException overload", + replaceWith = ReplaceWith("loadPatchesFromJar(patchesFiles, optimizedDexDirectory, null)") +) +fun loadPatchesFromDex( + patchesFiles: Set, + optimizedDexDirectory: File? = null, +) = loadPatchesFromDex(patchesFiles, optimizedDexDirectory, null) From 926ec96291efbb70bb20c4e8a06079060a6efafa Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 16 Oct 2025 23:16:43 +0200 Subject: [PATCH 2/2] fix test --- src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt b/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt index 8f90fef3..d68ad04e 100644 --- a/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt +++ b/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt @@ -71,6 +71,7 @@ internal object PatchLoaderTest { patchLoaderCompanionObject, TEST_PATCHES_CLASS_LOADER, mapOf(File("patchesFile") to setOf(TEST_PATCHES_CLASS)), + null ).values.first() assertEquals(