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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/main/kotlin/app/revanced/patcher/Patcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class Patcher(private val config: PatcherConfig) : Closeable {
* @param patches The patches to add.
*/
operator fun plusAssign(patches: Set<Patch<*>>) {
// Filter out bytecode patches if bytecodeContext is null
val patches = patches.filterNotTo(mutableSetOf()) { (context.bytecodeContext == null && it is BytecodePatch) }

// Add all patches to the executablePatches set.
context.executablePatches += patches

Expand Down Expand Up @@ -96,10 +99,12 @@ class Patcher(private val config: PatcherConfig) : Closeable {
context.resourceContext.decodeResources(config.resourceMode)
}

logger.info("Initializing lookup maps")
if (context.bytecodeContext != null) {
logger.info("Initializing lookup maps")
}

// Accessing the lazy lookup maps to initialize them.
context.bytecodeContext.lookupMaps
context.bytecodeContext?.lookupMaps

logger.info("Executing patches")

Expand Down Expand Up @@ -156,5 +161,5 @@ class Patcher(private val config: PatcherConfig) : Closeable {
* @return The [PatcherResult] containing the patched APK files.
*/
@OptIn(InternalApi::class)
fun get() = PatcherResult(context.bytecodeContext.get(), context.resourceContext.get())
fun get() = PatcherResult(context.bytecodeContext?.get() ?: setOf(), context.resourceContext.get())
}
13 changes: 11 additions & 2 deletions src/main/kotlin/app/revanced/patcher/PatcherContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import app.revanced.patcher.patch.ResourcePatchContext
import brut.androlib.apk.ApkInfo
import brut.directory.ExtFile
import java.io.Closeable
import lanchon.multidexlib2.EmptyMultiDexContainerException
import java.util.logging.Logger

/**
* A context for the patcher containing the current state of the patcher.
Expand All @@ -14,6 +16,8 @@ import java.io.Closeable
*/
@Suppress("MemberVisibilityCanBePrivate")
class PatcherContext internal constructor(config: PatcherConfig): Closeable {
private val logger = Logger.getLogger(this::class.java.name)

/**
* [PackageMetadata] of the supplied [PatcherConfig.apkFile].
*/
Expand All @@ -37,7 +41,12 @@ class PatcherContext internal constructor(config: PatcherConfig): Closeable {
/**
* The context for patches containing the current state of the bytecode.
*/
internal val bytecodeContext = BytecodePatchContext(config)
internal val bytecodeContext : BytecodePatchContext? = try {
BytecodePatchContext(config)
} catch (_: EmptyMultiDexContainerException) {
logger.info("The APK contains no DEX files. Skipping bytecode patches")
null
}

override fun close() = bytecodeContext.close()
override fun close() = bytecodeContext?.close() ?: Unit
}
4 changes: 2 additions & 2 deletions src/main/kotlin/app/revanced/patcher/patch/Patch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,12 @@ class BytecodePatch internal constructor(
executeBlock,
finalizeBlock,
) {
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
override fun execute(context: PatcherContext) = with(context.bytecodeContext!!) {
mergeExtension(this@BytecodePatch)
execute(this)
}

override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext)
override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext!!)

override fun toString() = name ?: "Bytecode${super.toString()}"
}
Expand Down
70 changes: 70 additions & 0 deletions src/test/kotlin/app/revanced/patcher/PatcherDexlessTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package app.revanced.patcher

import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.ResourcePatchContext
import app.revanced.patcher.patch.bytecodePatch
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test
import java.util.logging.Logger
import kotlin.test.assertEquals

object PatcherDexlessTest {
private lateinit var patcher: Patcher

@BeforeEach
fun setUp() {
patcher = mockk<Patcher> {
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
setPrivateField(
"config",
mockk<PatcherConfig> {
every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE
},
)
setPrivateField(
"logger",
Logger.getAnonymousLogger(),
)

every { context.bytecodeContext } returns null
every { this@mockk() } answers { callOriginal() }
}
}

@Test
fun `doesn't execute bytecode patches`() {
val executed = mutableListOf<String>()

val patch = bytecodePatch { execute { executed += "1" } }

assert(executed.isEmpty())

patch()

assertEquals(
emptyList(),
executed,
"Expected no bytecode patches to be executed.",
)
}

private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()

return runBlocking { patcher().toList() }
}

private operator fun Patch<*>.invoke() = setOf(this)().first()

private fun Any.setPrivateField(field: String, value: Any) {
this::class.java.getDeclaredField(field).apply {
this.isAccessible = true
set(this@setPrivateField, value)
}
}
}
10 changes: 5 additions & 5 deletions src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal object PatcherTest {
Logger.getAnonymousLogger(),
)

every { context.bytecodeContext.classes } returns mockk(relaxed = true)
every { context.bytecodeContext!!.classes } returns mockk(relaxed = true)
every { this@mockk() } answers { callOriginal() }
}
}
Expand Down Expand Up @@ -165,7 +165,7 @@ internal object PatcherTest {

@Test
fun `matches fingerprint`() {
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
every { patcher.context.bytecodeContext!!.classes } returns ProxyClassList(
mutableListOf(
ImmutableClassDef(
"class",
Expand Down Expand Up @@ -207,7 +207,7 @@ internal object PatcherTest {

patches()

with(patcher.context.bytecodeContext) {
with(patcher.context.bytecodeContext!!) {
assertAll(
"Expected fingerprints to match.",
{ assertNotNull(fingerprint.originalClassDefOrNull) },
Expand All @@ -219,8 +219,8 @@ internal object PatcherTest {

private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
every { with(patcher.context.bytecodeContext) { mergeExtension(any<BytecodePatch>()) } } just runs
every { patcher.context.bytecodeContext!!.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext!!.classes)
every { with(patcher.context.bytecodeContext!!) { mergeExtension(any<BytecodePatch>()) } } just runs

return runBlocking { patcher().toList() }
}
Expand Down