Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Match fingerprints by instruction filters #329

Open
wants to merge 67 commits into
base: dev
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
31f6122
feat: Match fingerprints by instruction filters
LisoUseInAIKyrios Jan 1, 2025
1965e84
refactor
LisoUseInAIKyrios Jan 2, 2025
d02aad0
refactor: Use 'by' semantic to capture fingerprint name
LisoUseInAIKyrios Jan 3, 2025
a160101
Allow using a fingerprint result inside another fingerprint
LisoUseInAIKyrios Jan 3, 2025
7547319
fix 'by' syntax causing multiple resolves. Add debug resolving perfor…
LisoUseInAIKyrios Jan 3, 2025
14335e8
Add helper method
LisoUseInAIKyrios Jan 3, 2025
3386fd9
make match result fields consistent with fingerprint accessor methods
LisoUseInAIKyrios Jan 3, 2025
e2707e1
fix: Retain existing null opcode behavior
LisoUseInAIKyrios Jan 3, 2025
9779e50
refactor: Change `ByteCodePatchContext` to a singleton object, remove…
LisoUseInAIKyrios Jan 3, 2025
4d38837
feat: Add 'classFingerprint' (parent fingerprint)
LisoUseInAIKyrios Jan 3, 2025
a3852a6
fix: Temporarily turn off failing tests
LisoUseInAIKyrios Jan 4, 2025
2b6e437
add `NewInstanceFilter`, add JVM method string parsing, add basic uni…
LisoUseInAIKyrios Jan 4, 2025
319a8a7
code documentation
LisoUseInAIKyrios Jan 4, 2025
1199e21
refactor
LisoUseInAIKyrios Jan 5, 2025
111d6ca
comments
LisoUseInAIKyrios Jan 5, 2025
3dad1b0
refactor
LisoUseInAIKyrios Jan 5, 2025
6c80a20
refactor: remove performance logging
LisoUseInAIKyrios Jan 5, 2025
cfb873a
Revert "refactor: Change `ByteCodePatchContext` to a singleton object…
LisoUseInAIKyrios Jan 5, 2025
c37ecb8
fix: delete test that is now too clunky since a context must be passed
LisoUseInAIKyrios Jan 5, 2025
231378e
refactor: Rename to `MethodCallFilter` and `FieldCallFilter`
LisoUseInAIKyrios Jan 5, 2025
502ea98
Revert "feat: Add 'classFingerprint' (parent fingerprint)"
LisoUseInAIKyrios Jan 5, 2025
f5a1b26
perf: Copy strings only if strings are found
LisoUseInAIKyrios Jan 5, 2025
1166096
Restore instruction filter test
LisoUseInAIKyrios Jan 5, 2025
cdb986d
Comments. Will update .md examples after DSL is figured out.
LisoUseInAIKyrios Jan 5, 2025
0e85451
refactor: Rename FieldCallFilter -> FieldAccessFilter
LisoUseInAIKyrios Jan 6, 2025
b3b77ac
docs: Update fingerprinting examples
LisoUseInAIKyrios Jan 6, 2025
47e8086
refactor: Use DSL style constructor functions
LisoUseInAIKyrios Jan 7, 2025
0ca165d
refactor
LisoUseInAIKyrios Jan 7, 2025
f54efb1
add String literal instruction filter
LisoUseInAIKyrios Jan 7, 2025
a572771
docs: Cleanup examples
LisoUseInAIKyrios Jan 8, 2025
2faba7f
Comments
LisoUseInAIKyrios Jan 9, 2025
b74d51b
refactor: Comments, consistency
LisoUseInAIKyrios Jan 9, 2025
df7bc88
refactor: Allow partial matches of string literals
LisoUseInAIKyrios Jan 9, 2025
2c91090
refactor
LisoUseInAIKyrios Jan 9, 2025
a0a0306
perf: Skip return type check if access flags include constructor
LisoUseInAIKyrios Jan 9, 2025
329dfbd
add 'checkCast' instruction filter
LisoUseInAIKyrios Jan 10, 2025
b117dba
Add field access smali parsing for consistency
LisoUseInAIKyrios Jan 10, 2025
bb06381
fix: Improve smali regex filter
LisoUseInAIKyrios Jan 10, 2025
e1930ea
refactor: Match class types using endsWith
LisoUseInAIKyrios Jan 10, 2025
f981d1c
refactor: Throw exception on bad new instance type
LisoUseInAIKyrios Jan 10, 2025
20b4900
Add more details to example
LisoUseInAIKyrios Jan 12, 2025
132fa00
refactor: Add sub version to show files in correct order
LisoUseInAIKyrios Jan 12, 2025
f93f870
refactor: Deprecate pure opcode declarations
LisoUseInAIKyrios Jan 15, 2025
955ceb6
Move instruction filters to fingerprint.kt file
LisoUseInAIKyrios Jan 15, 2025
c74c1b8
rename parameter to be more clear
LisoUseInAIKyrios Jan 15, 2025
5885984
Revert "refactor: Deprecate pure opcode declarations" It's still use…
LisoUseInAIKyrios Jan 21, 2025
57d8087
add debugging code
LisoUseInAIKyrios Jan 27, 2025
1dacd3d
Revert "add debugging code"
LisoUseInAIKyrios Jan 27, 2025
b08ef19
Work in progress fix for wrong fingerprint indexes found when multipl…
LisoUseInAIKyrios Jan 27, 2025
0f198c4
fix: Replace original classdef with proxy class immediately
LisoUseInAIKyrios Jan 28, 2025
142aa71
refactor: Remove ClassProxy wrapper that's no longer needed. All exi…
LisoUseInAIKyrios Jan 28, 2025
8951990
refactor: Change ProxyList to a map, remove now redundant class looku…
LisoUseInAIKyrios Jan 28, 2025
772c0e6
Update documentation
LisoUseInAIKyrios Jan 28, 2025
8f7911e
Refactor: Simplify
LisoUseInAIKyrios Jan 28, 2025
567feef
refactor: Add `mutableClassByOrNull()`
LisoUseInAIKyrios Jan 28, 2025
d7974ef
refactor: Pre-size the class map
LisoUseInAIKyrios Jan 28, 2025
d0e0a80
Update examples
LisoUseInAIKyrios Jan 28, 2025
44a1424
refactor
LisoUseInAIKyrios Jan 30, 2025
fc319f4
refactor, remove `lastInstruction()` filter
LisoUseInAIKyrios Feb 2, 2025
6c58248
fix: Validate 1 or more instructions or opcodes
LisoUseInAIKyrios Feb 6, 2025
6898576
fix floating point literals
LisoUseInAIKyrios Feb 13, 2025
e4bfbce
refactor: Move instruction filters to their own file
LisoUseInAIKyrios Feb 13, 2025
d1d5557
fix: Add clear match method for shared fingerprints
LisoUseInAIKyrios Feb 13, 2025
5628204
refactor
LisoUseInAIKyrios Feb 13, 2025
f32cc90
Comments and cleanup
LisoUseInAIKyrios Feb 18, 2025
f38f5a7
fix default value
LisoUseInAIKyrios Feb 22, 2025
22db561
Merge remote-tracking branch 'upstream/dev' into feat/instruction_fil…
LisoUseInAIKyrios Mar 16, 2025
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
228 changes: 181 additions & 47 deletions api/revanced-patcher.api

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/1_patcher_intro.md
Original file line number Diff line number Diff line change
@@ -108,4 +108,4 @@ val resources = patcherResult.resources

The next page teaches the fundamentals of ReVanced Patches.

Continue: [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
Continue: [🧩 Introduction to ReVanced Patches](2_0_0_patches_intro.md)
2 changes: 1 addition & 1 deletion docs/2_patches_intro.md → docs/2_0_0_patches_intro.md
Original file line number Diff line number Diff line change
@@ -123,4 +123,4 @@ val resourcePatch = resourcePatch {

The next page will guide you through creating a development environment for creating patches.

Continue: [👶 Setting up a development environment](2_1_setup.md)
Continue: [👨‍💻 Setting up a development environment](2_1_0_setup.md)
4 changes: 2 additions & 2 deletions docs/2_1_setup.md → docs/2_1_0_setup.md
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@
Continuing the legacy of Vanced
</p>

# 👶 Setting up a development environment
# 👨‍💻 Setting up a development environment

To start developing patches with ReVanced Patcher, you must prepare a development environment.

@@ -109,4 +109,4 @@ Throughout the documentation, [ReVanced Patches](https://github.com/revanced/rev
The next page will go into details about a ReVanced patch.
Continue: [🧩 Anatomy of a patch](2_2_patch_anatomy.md)
Continue: [🧩 Anatomy of a patch](2_2_0_patch_anatomy.md)
2 changes: 1 addition & 1 deletion docs/2_2_patch_anatomy.md → docs/2_2_0_patch_anatomy.md
Original file line number Diff line number Diff line change
@@ -85,7 +85,7 @@ val disableAdsPatch = bytecodePatch(
// Business logic of the patch to disable ads in the app.
execute {
// Fingerprint to find the method to patch.
val showAdsFingerprint = fingerprint {
val showAdsFingerprint by fingerprint {
// More about fingerprints on the next page of the documentation.
}

301 changes: 185 additions & 116 deletions docs/2_2_1_fingerprinting.md
Original file line number Diff line number Diff line change
@@ -65,116 +65,225 @@ It is used to uniquely match a method by its characteristics.
Fingerprinting is used to match methods with a limited amount of known information.
Methods with obfuscated names that change with each update are primary candidates for fingerprinting.
The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type,
access flags, an opcode pattern, strings, and more.
access flags, instructions, strings, and more.

## ⛳️ Example fingerprint
## 🔎 Example target Java code and bytecode

An example fingerprint is shown below:
```java
package com.some.app.ads;

```kt
class AdsLoader {
private final static Map<String, String> a = new HashMap<>();

package app.revanced.patches.ads.fingerprints
// Method to fingerprint
public final boolean obfuscatedMethod(String parameter1, int parameter2, ObfuscatedClass parameter3) {
// Filter 1 target instruction.
String value1 = a.get(parameter1);

fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Z")
parameters("Z")
opcodes(Opcode.RETURN)
strings("pro")
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
unrelatedMethod(value1);

// Filter 2, 3, 4 target instructions, and the instructions to modify.
if ("showBannerAds".equals(value1)) {
showBannerAds();
}

// Filter 5 and 6 target instructions.
return parameter2 != 1337;
}

private void showBannerAds() {
// ...
}

private void unrelatedMethod(String parameter) {
// ...
}
}
```

## 🔎 Reconstructing the original code from the example fingerprint from above
```asm
# Method to fingerprint
.method public final obfuscatedMethod(Ljava/lang/String;ILObfuscatedClass;)Z
.registers 4
The following code is reconstructed from the fingerprint to understand how a fingerprint is created.
# Filter 1 target instruction.
sget-object v0, Lcom/some/app/ads/AdsLoader;->a:Ljava/util/Map;
The fingerprint contains the following information:
invoke-interface {v0, p1}, Ljava/util/Map;->get(Ljava/lang/Object;)Ljava/lang/Object;
- Method signature:
move-result-object p1
```kt
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Z")
parameters("Z")
```
check-cast p1, Ljava/lang/String;
- Method implementation:
invoke-direct {p0, p1}, Lcom/some/app/ads/AdsLoader;->unrelatedMethod(Ljava/lang/String;)V
```kt
opcodes(Opcode.RETURN)
strings("pro")
```
# Filter 2 target instruction.
const-string v0, "showBannerAds"
- Package and class name:
# Filter 3 target instruction.
invoke-virtual {v0, p1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
```kt
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
```
# Filter 4 target instruction.
move-result p1
With this information, the original code can be reconstructed:
if-eqz p1, :cond_16
```java
package com.some.app.ads;
invoke-direct {p0}, Lcom/some/app/ads/AdsLoader;->showBannerAds()V
<accessFlags>
# Filter 5 target instruction.
:cond_16
const/16 p1, 0x539
class AdsLoader {
public final boolean <methodName>(boolean <parameter>)
# Filter 6 target instruction.
if-eq p2, p1, :cond_1c
const/4 p1, 0x1
goto :goto_1d
{
// ...
:cond_1c
const/4 p1, 0x0
var userStatus = "pro";
:goto_1d
return p1
.end method
```

// ...
## ⛳️ Example fingerprint

return <returnValue >;
```kt
val hideAdsFingerprint by fingerprint {
// Method signature:
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Z")
// Last parameter is simply `L` since it's an obfuscated class.
parameters("Ljava/lang/String;", "I", "L")

// Method implementation:
instructions(
// Filter 1
fieldAccess(
definingClass = "this",
type = "Ljava/util/Map;"
),

// Filter 2
string("showBannerAds"),

// Filter 3
methodCall(
definingClass = "Ljava/lang/String;",
name = "equals",
),

// Filter 4
// maxAfter = 0 means this must match immediately after the last filter.
opcode(Opcode.MOVE_RESULT, maxAfter = 0),

// Filter 5
literal(1337),

// Filter 6
opcode(Opcode.IF_EQ),
)
custom { method, classDef ->
classDef.type == "Lcom/some/app/ads/AdsLoader;"
}
}
```

Using that fingerprint, this method can be matched uniquely from all other methods.
Notice the instruction filters do not declare every instruction in the target method,
and between each filter can exist 0 or more other instructions. Instruction filters
must be declared in the same order as the instructions appear in the target method.

If the distance between each instruction declaration can be approximated,
then the `maxAfter` parameter can be used to restrict the instruction match to
a maximum distance from the last instruction. A value of 0 for the first instruction filter
means the filter must be the first instruction of the target method.

If a single instruction varies slightly between different app targets but otherwise the fingerprint
is still the same, the `anyInstruction()` wrapper can be used to specify variations of the
same instruction. Such as:
`anyInstruction(string("string in early app target"), string("updated string in latest app target"))`

If a method cannot be uniquely identified using the built in filters, but a fixed
pattern of opcodes can identify the method, then the opcode pattern can be
defined using the fingerprint `opcodes()` declaration. Opcode patterns do not
allow variable spacing between each opcode, and all opcodes all must appear exactly as declared.
Opcode patterns should be avoided whenever possible due to their fragility and
possibility of matching completely unrelated code.

> [!TIP]
> A fingerprint should contain information about a method likely to remain the same across updates.
> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated
> app.
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the
> same.
> A fingerprint should contain information about a method likely to remain stable across updates.
> Names of obfuscated classes and methods should not be used since they can change between app updates.
## 🔨 How to use fingerprints

After declaring a fingerprint, it can be used in a patch to find the method it matches to:
After declaring a fingerprint it can be used in a patch to find the method it matches to:

```kt
val fingerprint = fingerprint {
// ...
execute {
hideAdsFingerprint.let {
// Changes the target code to:
// if (false) {
// showBannerAds();
// }
val filter4 = it.instructionMatches[3]
val moveResultIndex = filter3.index
val moveResultRegister = filter3.getInstruction<OneRegisterInstruction>().registerA

it.method.addInstructions(moveResultIndex + 1, "const/4 v$moveResultRegister, 0x0")
}
}
```

val patch = bytecodePatch {
execute {
fingerprint.method
}
Be careful if making more than 1 modification to the same method. Adding/removing instructions to
a method can cause fingerprint match indexes to no longer be correct. The simplest solution is
to modify the target method from the last match index to the first. Another solution is after modifying
the target method to then call `clearMatch()` followed by `match()`, and then the instruction match indexes
are up to date and correct.

Modifying the example above to also change the code `return parameter2 != 1337;` into: `return false;`:

```kt
execute {
appFingerprint.let {
// Modify method from last indexes to first to preserve the correct fingerprint indexes.

// Remove conditional branch and always return false.
val filter6 = it.instructionMatches[5]
it.method.removeInstruction(filter6.index)


// Changes the target code to:
// if (false) {
// showBannerAds();
// }
val filter4 = it.instructionMatches[3]
val moveResultIndex = filter3.index
val moveResultRegister = filter3.getInstruction<OneRegisterInstruction>().registerA

it.method.addInstructions(moveResultIndex + 1, "const/4 v$moveResultRegister, 0x0")
}
}
```

The fingerprint won't be matched again, if it has already been matched once, for performance reasons.
This makes it useful, to share fingerprints between multiple patches,
For performance reasons, a fingerprint will always match only once.
This makes it useful to share fingerprints between multiple patches,
and let the first executing patch match the fingerprint:

```kt
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
val mainActivityPatch1 = bytecodePatch {
execute {
mainActivityOnCreateFingerprint.method
}
execute {
mainActivityOnCreateFingerprint.method
}
}

val mainActivityPatch2 = bytecodePatch {
execute {
mainActivityOnCreateFingerprint.method
}
execute {
mainActivityOnCreateFingerprint.method
}
}
```

@@ -183,32 +292,16 @@ val mainActivityPatch2 = bytecodePatch {
> accessing certain properties of the fingerprint will raise an exception.
> Instead, the `orNull` properties can be used to return `null` if no match is found.
> [!TIP]
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
> function to fuzzy match the pattern.
> `null` can be used as a wildcard to match any opcode:
>
> ```kt
> fingerprint(fuzzyPatternScanThreshhold = 2) {
> opcodes(
> Opcode.ICONST_0,
> null,
> Opcode.ICONST_1,
> Opcode.IRETURN,
> )
>}
> ```
The following properties can be accessed in a fingerprint:

- `originalClassDef`: The original class definition the fingerprint matches to.
- `originalClassDefOrNull`: The original class definition the fingerprint matches to.
- `originalMethod`: The original method the fingerprint matches to.
- `originalMethodOrNull`: The original method the fingerprint matches to.
- `classDef`: The class the fingerprint matches to.
- `classDefOrNull`: The class the fingerprint matches to.
- `method`: The method the fingerprint matches to. If no match is found, an exception is raised.
- `methodOrNull`: The method the fingerprint matches to.
- `originalClassDef`: The immutable class definition the fingerprint matches to.
- `originalClassDefOrNull`: The immutable class definition the fingerprint matches to, or null.
- `originalMethod`: The immutable method the fingerprint matches to.
- `originalMethodOrNull`: The immutable method the fingerprint matches to, or null.
- `classDef`: The mutable class the fingerprint matches to.
- `classDefOrNull`: The mutable class the fingerprint matches to, or null.
- `method`: The mutable method the fingerprint matches to. If no match is found, an exception is raised.
- `methodOrNull`: The mutable method the fingerprint matches to, or null.

The difference between the `original` and non-`original` properties is that the `original` properties return the
original class or method definition, while the non-`original` properties return a mutable copy of the class or method.
@@ -234,7 +327,7 @@ Instead, the fingerprint can be matched manually using various overloads of a fi

```kt
execute {
val match = showAdsFingerprint(classes)
val match = showAdsFingerprint.match(classes)
}
```

@@ -250,41 +343,17 @@ Instead, the fingerprint can be matched manually using various overloads of a fi
}
```

Another common usecase is to use a fingerprint to reduce the search space of a method to a single class.

```kt
execute {
// Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint.
val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef)
}
```

- Match a **single method**, to extract certain information about it

The match of a fingerprint contains useful information about the method,
such as the start and end index of an opcode pattern or the indices of the instructions with certain string
references.
A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:
Another common use case is to find the class of the target code by finger printing an easy
to identify method in that class (especially a method with string constants), then use the class
found to match a second fingerprint that finds the target method.

```kt
execute {
val currentPlanFingerprint = fingerprint {
strings("free", "trial")
}

currentPlanFingerprint.match(adsFingerprint.method).let { match ->
match.stringMatches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}")
}
}
// Match showAdsFingerprint to the class of the ads loader found by adsLoaderClassFingerprint.
val match = showAdsFingerprint.match(adsLoaderClassFingerprint.originalClassDef)
}
```

> [!WARNING]
> If the fingerprint can not be matched to any method, calling `match` will raise an
> exception.
> Instead, the `orNull` overloads can be used to return `null` if no match is found.
> [!TIP]
> To see real-world examples of fingerprints,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
58 changes: 33 additions & 25 deletions docs/4_apis.md
Original file line number Diff line number Diff line change
@@ -4,59 +4,67 @@ A handful of APIs are available to make patch development easier and more effici

## 📙 Overview

1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
1. 🔍 Find immutable classes with `classBy(String)`
2. 👹 Create mutable replacements of classes with `mutableClassBy(ClassDef)`
3. 🏃‍ Navigate method calls recursively by index with `navigate(Method)`
4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)`
5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)`

### 🧰 APIs

#### 👹 `proxy(ClassDef)`
#### 🔍 `classBy(String)`

By default, the classes are immutable, meaning they cannot be modified.
To make a class mutable, use the `proxy(ClassDef)` function.
This function creates a lazy mutable copy of the class definition.
Accessing the property will replace the original class definition with the mutable copy,
thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.
The `classBy(String)` function is an alternative to finding immutable classes
from a constant string or from a String field of a fingerprint match.

```kt
execute {
val mutableClass = proxy(classDef)
mutableClass.methods.add(Method())
// Find the superclass of a fingerprint return type
val superClassOfReturnType = classBy(match().originalMethod.returnType).superclass
}
```

#### 🔍 `classBy(Predicate)`
#### 👹 `mutableClassBy(ClassDef)`

The `classBy(Predicate)` function is an alternative to finding and creating mutable classes by a predicate.
It automatically proxies the class definition, making it mutable.
By default, the classes are immutable and they cannot be modified.
To make a class mutable use the `mutableClassBy(ClassDef)` function.
Accessing the property will replace the original class definition with the mutable copy,
thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.

```kt
execute {
// Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef
val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef
// Find a class by the return type of a fingerprint
val superClassOfReturnType = classBy(match().originalMethod.returnType).superclass

val mutableClass = mutableClassBy(superClassOfReturnType)
mutableClass.methods.add(Method())
}
```

#### 🏃‍ `navigate(Method).at(index)`

The `navigate(Method)` function allows you to navigate method calls recursively by index.
The `navigate(Method)` function allows navigating method calls by index,
and provides an easier way to parse the method call classes in code.

```kt
execute {
// Sequentially navigate to the instructions at index 1 within 'someMethod'.
val method = navigate(someMethod).to(1).original() // original() returns the original immutable method.
// Navigate to the method at index 5 within 'someMethod'.
// original() returns the original immutable method.
val original = navigate(someMethod).to(5).original()

// Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'.
// Further navigate to the second occurrence of the opcode 'INVOKE_VIRTUAL'.
// stop() returns the mutable copy of the method.
val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()

// Alternatively, to stop(), you can delegate the method to a variable.
val method by navigate(someMethod).to(1)
val mutable = navigate(someMethod).to(2) {
instruction -> instruction.opcode == Opcode.INVOKE_VIRTUAL
}.stop()

// You can chain multiple calls to at() to navigate deeper into the method.
val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
// You can chain multiple to() calls together navigate multiple calls across different methods and classes.
//
// Navigate to:
// A. the method of the 5th instruction
// B. the method of the 10th instruction in method A
// C. the method of 2nd instruction of method B
val mutableDeep = navigate(someMethod).to(5, 10, 2).stop() // Mutable method Method C
}
```

6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -65,9 +65,9 @@ This documentation contains the fundamentals of ReVanced Patcher and how to use
## 📖 Table of content

1. [💉 Introduction to ReVanced Patcher](1_patcher_intro.md)
2. [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
1. [👶 Setting up a development environment](2_1_setup.md)
2. [🧩 Anatomy of a ReVanced patch](2_2_patch_anatomy.md)
2. [🧩 Introduction to ReVanced Patches](2_0_0_patches_intro)
1. [👨‍💻 Setting up a development environment](2_1_0_setup)
2. [🧩 Anatomy of a ReVanced patch](2_2_0_patch_anatomy)
1. [🔎 Fingerprinting](2_2_1_fingerprinting.md)
3. [📜 Project structure and conventions](3_structure_and_conventions.md)
4. [💪 Advanced APIs](4_apis.md)
573 changes: 384 additions & 189 deletions src/main/kotlin/app/revanced/patcher/Fingerprint.kt

Large diffs are not rendered by default.

882 changes: 882 additions & 0 deletions src/main/kotlin/app/revanced/patcher/InstructionFilter.kt

Large diffs are not rendered by default.

127 changes: 89 additions & 38 deletions src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt
Original file line number Diff line number Diff line change
@@ -6,8 +6,7 @@ import app.revanced.patcher.PatcherResult
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
import app.revanced.patcher.util.ProxyClassList
import app.revanced.patcher.util.proxy.ClassProxy
import app.revanced.patcher.util.PatchClasses
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.ClassDef
@@ -42,22 +41,22 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal val opcodes: Opcodes

/**
* The list of classes.
* All classes for the target app and any extension classes.
*/
val classes = ProxyClassList(
val classes = PatchClasses(
MultiDexIO.readDexFile(
true,
config.apkFile,
BasicDexFileNamer(),
null,
null,
).also { opcodes = it.opcodes }.classes.toMutableList(),
).also { opcodes = it.opcodes }.classes
)

/**
* The lookup maps for methods and the class they are a member of from the [classes].
*/
internal val lookupMaps by lazy { LookupMaps(classes) }
internal val lookupMaps by lazy { LookupMaps(classes.pool.values) }

/**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
@@ -68,11 +67,10 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
val existingClass = classes.classByOrNull(classDef.type) ?: run {
logger.fine { "Adding class \"$classDef\"" }

classes += classDef
lookupMaps.classesByType[classDef.type] = classDef
classes.addClass(classDef)

return@forEach
}
@@ -85,32 +83,93 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
return@let
}

classes -= existingClass
classes += mergedClass
classes.addClass(mergedClass)
}
}
} ?: logger.fine("No extension to merge")
}

/**
* Find a class with a predicate.
*
* @param classType The full classname.
* @return An immutable instance of the class type.
* @see mutableClassBy
*/
fun classBy(classType: String) = classes.classBy(classType)

/**
* Find a class with a predicate.
*
* @param predicate A predicate to match the class.
* @return An immutable instance of the class type.
* @see mutableClassBy
*/
fun classBy(predicate: (ClassDef) -> Boolean) = classes.classBy(predicate)

/**
* Find a class with a predicate.
*
* @param classType The full classname.
* @return An immutable instance of the class type.
* @see mutableClassBy
*/
fun classByOrNull(classType: String) = classes.classByOrNull(classType)

/**
* Find a class with a predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
* @return An immutable instance of the class type.
*/
fun classBy(predicate: (ClassDef) -> Boolean) =
classes.proxyPool.find { predicate(it.immutableClass) } ?: classes.find(predicate)?.let { proxy(it) }
fun classByOrNull(predicate: (ClassDef) -> Boolean) = classes.classByOrNull(predicate)

/**
* Proxy the class to allow mutation.
* Find a class with a predicate.
*
* @param classDef The class to proxy.
* @param classType The full classname.
* @return A mutable version of the class type.
*/
fun mutableClassBy(classType: String) = classes.mutableClassBy(classType)

/**
* Find a class with a predicate.
*
* @return A proxy for the class.
* @param classDef An immutable class.
* @return A mutable version of the class definition.
*/
fun proxy(classDef: ClassDef) = classes.proxyPool.find {
it.immutableClass.type == classDef.type
} ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
fun mutableClassBy(classDef: ClassDef) = classes.mutableClassBy(classDef)

/**
* Find a class with a predicate.
*
* @param predicate A predicate to match the class.
* @return A mutable class that matches the predicate.
*/
fun mutableClassBy(predicate: (ClassDef) -> Boolean) = classes.mutableClassBy(predicate)

/**
* Mutable class from a full class name.
* Returns `null` if class is not available, such as a built in Android or Java library.
*
* @param classType The full classname.
* @return A mutable version of the class type.
*/
fun mutableClassByOrNull(classType: String) = classes.mutableClassByOrNull(classType)

/**
* Find a mutable class with a predicate.
*
* @param predicate A predicate to match the class.
* @return A mutable class that matches the predicate.
*/
fun mutableClassByOrNull(predicate: (ClassDef) -> Boolean) = classes.mutableClassByOrNull(predicate)

/**
* @return The mutable instance of an immutable class.
*/
@Deprecated("Instead use `mutableClassBy(String)`, `mutableClassBy(ClassDef)`, or `mutableClassBy(predicate)`")
fun proxy(classDef: ClassDef) = classes.mutableClassBy(classDef)

/**
* Navigate a method.
@@ -144,8 +203,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() =
this@BytecodePatchContext.classes.also(ProxyClassList::replaceClasses).toSet()
override fun getClasses() = this@BytecodePatchContext.classes.pool.values.toSet()

override fun getOpcodes() = this@BytecodePatchContext.opcodes
},
@@ -161,52 +219,45 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
}

/**
* A lookup map for methods and the class they are a member of and classes.
* A lookup map for strings and the methods they are a member of.
*
* @param classes The list of classes to create the lookup maps from.
*/
internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable {
internal class LookupMaps internal constructor(classes: Collection<ClassDef>) : Closeable {
/**
* Methods associated by strings referenced in it.
* Methods associated by strings referenced in them.
*/
internal val methodsByStrings = MethodClassPairsLookupMap()

// Lookup map for fast checking if a class exists by its type.
val classesByType = mutableMapOf<String, ClassDef>().apply {
classes.forEach { classDef -> put(classDef.type, classDef) }
}

init {
classes.forEach { classDef ->
classDef.methods.forEach { method ->
val methodClassPair: MethodClassPair = method to classDef
val methodClassPair: MethodClassPair by lazy {
method to classDef
}

// Add strings contained in the method as the key.
method.instructionsOrNull?.forEach instructions@{ instruction ->
method.instructionsOrNull?.forEach { instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
return@instructions
return@forEach
}

val string = ((instruction as ReferenceInstruction).reference as StringReference).string

methodsByStrings[string] = methodClassPair
}

// In the future, the class type could be added to the lookup map.
// This would require MethodFingerprint to be changed to include the class type.
}
}
}

override fun close() {
methodsByStrings.clear()
classesByType.clear()
}
}

override fun close() {
lookupMaps.close()
classes.clear()
classes.close()
}
}

2 changes: 1 addition & 1 deletion src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt
Original file line number Diff line number Diff line change
@@ -181,7 +181,7 @@ internal object ClassMerger {
callback(targetClass)

targetClass.superclass ?: return
this.classBy { targetClass.superclass == it.type }?.mutableClass?.let {
mutableClassByOrNull(targetClass.superclass!!)?.let {
traverseClassHierarchy(it, callback)
}
}
11 changes: 2 additions & 9 deletions src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ class MethodNavigator internal constructor(
*
* @return The last navigated method mutably.
*/
fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
fun stop() = mutableClassBy(lastNavigatedMethodReference.definingClass).firstMethodBySignature
as MutableMethod

/**
@@ -95,14 +95,7 @@ class MethodNavigator internal constructor(
*
* @return The last navigated method immutably.
*/
fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature

/**
* Predicate to match the class defining the current method reference.
*/
private val matchesCurrentMethodReferenceDefiningClass = { classDef: ClassDef ->
classDef.type == lastNavigatedMethodReference.definingClass
}
fun original(): Method = classes.classBy(lastNavigatedMethodReference.definingClass).firstMethodBySignature

/**
* Find the first [lastNavigatedMethodReference] in the class.
127 changes: 127 additions & 0 deletions src/main/kotlin/app/revanced/patcher/util/PatchClasses.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package app.revanced.patcher.util

import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import com.android.tools.smali.dexlib2.iface.ClassDef

@Deprecated("Instead use PatchClasses")
typealias ProxyClassList = PatchClasses

/**
* All classes for the target app and any extension classes.
*/
class PatchClasses internal constructor(
/**
* Pool of both immutable and mutable classes.
*/
internal val pool: MutableMap<String, ClassDef>
) {

internal constructor(set: Set<ClassDef>) :
this(set.associateByTo(HashMap<String, ClassDef>(set.size * 3 / 2)) { it.type })

internal fun addClass(classDef: ClassDef) {
pool[classDef.type] = classDef
}

internal fun close() {
pool.clear()
}

/**
* Iterate over all classes.
*/
fun forEach(action: (ClassDef) -> Unit) {
pool.values.forEach(action)
}

/**
* Find a class with a predicate.
*
* @param classType The full classname.
* @return An immutable instance of the class type.
* @see mutableClassBy
*/
fun classByOrNull(classType: String) = pool[classType]

/**
* Find a class with a predicate.
*
* @param predicate A predicate to match the class.
* @return An immutable instance of the class type, or null if not found.
*/
fun classByOrNull(predicate: (ClassDef) -> Boolean) = pool.values.find(predicate)

/**
* Find a class with a predicate.
*
* @param predicate A predicate to match the class.
* @return An immutable instance of the class type.
*/
fun classBy(predicate: (ClassDef) -> Boolean) = classByOrNull(predicate)
?: throw PatchException("Could not find any class match")

/**
* Find a class with a predicate.
*
* @param classType The full classname.
* @return An immutable instance of the class type.
* @see mutableClassBy
*/
fun classBy(classType: String) = classByOrNull(classType)
?: throw PatchException("Could not find class: $classType")

/**
* Mutable class from a full class name.
* Returns `null` if class is not available, such as a built in Android or Java library.
*
* @param classDefType The full classname.
* @return A mutable version of the class type.
*/
fun mutableClassByOrNull(classDefType: String) : MutableClass? {
var classDef = pool[classDefType]
if (classDef == null) return null
if (classDef is MutableClass) return classDef

classDef = MutableClass(classDef)
pool[classDefType] = classDef
return classDef
}

/**
* Find a class with a predicate.
*
* @param classDefType The full classname.
* @return A mutable version of the class type.
*/
fun mutableClassBy(classDefType: String) = mutableClassByOrNull(classDefType)
?: throw PatchException("Could not find class: $classDefType")

/**
* Find a mutable class with a predicate.
*
* @param predicate A predicate to match the class.
* @return A mutable class that matches the predicate.
*/
fun mutableClassByOrNull(predicate: (ClassDef) -> Boolean) =
classByOrNull(predicate)?.let {
if (it is MutableClass) it else mutableClassBy(it.type)
}

/**
* @param classDef An immutable class.
* @return A mutable version of the class definition.
*/
fun mutableClassBy(classDef: ClassDef) =
if (classDef is MutableClass) classDef else mutableClassBy(classDef.type)

/**
* Find a mutable class with a predicate.
*
* @param predicate A predicate to match the class.
* @return A mutable class that matches the predicate.
*/
fun mutableClassBy(predicate: (ClassDef) -> Boolean) = mutableClassByOrNull(predicate)
?: throw PatchException("Could not find any class match")

}
29 changes: 0 additions & 29 deletions src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt

This file was deleted.

35 changes: 0 additions & 35 deletions src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt

This file was deleted.

261 changes: 250 additions & 11 deletions src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Original file line number Diff line number Diff line change
@@ -2,14 +2,15 @@ package app.revanced.patcher

import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
import app.revanced.patcher.util.ProxyClassList
import app.revanced.patcher.util.PatchClasses
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.*
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertThrows
import java.util.logging.Logger
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -39,6 +40,7 @@ internal object PatcherTest {
}
}


@Test
fun `executes patches in correct order`() {
val executed = mutableListOf<String>()
@@ -70,6 +72,7 @@ internal object PatcherTest {
)
}


@Test
fun `handles execution of patches correctly when exceptions occur`() {
val executed = mutableListOf<String>()
@@ -150,7 +153,7 @@ internal object PatcherTest {
val patch = bytecodePatch {
execute {
// Fingerprint can never match.
val fingerprint = fingerprint { }
val fingerprint by fingerprint { }

// Throws, because the fingerprint can't be matched.
fingerprint.patternMatch
@@ -165,9 +168,9 @@ internal object PatcherTest {

@Test
fun `matches fingerprint`() {
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
mutableListOf(
ImmutableClassDef(
every { patcher.context.bytecodeContext.classes } returns PatchClasses(
mutableMapOf(
"class" to ImmutableClassDef(
"class",
0,
null,
@@ -191,15 +194,15 @@ internal object PatcherTest {
),
)

val fingerprint = fingerprint { returns("V") }
val fingerprint2 = fingerprint { returns("V") }
val fingerprint3 = fingerprint { returns("V") }
val fingerprint by fingerprint { returns("V") }
val fingerprint2 by fingerprint { returns("V") }
val fingerprint3 by fingerprint { returns("V") }

val patches = setOf(
bytecodePatch {
execute {
fingerprint.match(classes.first().methods.first())
fingerprint2.match(classes.first())
fingerprint.match(classes.pool.values.first().methods.first())
fingerprint2.match(classes.pool.values.first())
fingerprint3.originalClassDef
}
},
@@ -217,9 +220,245 @@ internal object PatcherTest {
}
}

@Test
fun `MethodCallFilter smali parsing`() {
with(patcher.context.bytecodeContext) {
var definingClass = "Landroid/view/View;"
var name = "inflate"
var parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
var returnType = "Landroid/view/View;"
var methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

var filter = MethodCallFilter.parseJvmMethodCall(methodSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(parameters == filter.parameters!!()) },
{ assertTrue(returnType == filter.returnType!!()) },
)


definingClass = "Landroid/view/View\$InnerClass;"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "Landroid/view/ViewGroup\$ViewInnerClass;", "J")
returnType = "V"
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

filter = MethodCallFilter.parseJvmMethodCall(methodSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(parameters == filter.parameters!!()) },
{ assertTrue(returnType == filter.returnType!!()) },
)

definingClass = "Landroid/view/View\$InnerClass;"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "Landroid/view/ViewGroup\$ViewInnerClass;", "J")
returnType = "I"
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

filter = MethodCallFilter.parseJvmMethodCall(methodSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(parameters == filter.parameters!!()) },
{ assertTrue(returnType == filter.returnType!!()) },
)

definingClass = "Landroid/view/View\$InnerClass;"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "Landroid/view/ViewGroup\$ViewInnerClass;", "J")
returnType = "[I"
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

filter = MethodCallFilter.parseJvmMethodCall(methodSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(parameters == filter.parameters!!()) },
{ assertTrue(returnType == filter.returnType!!()) },
)
}
}

@Test
fun `MethodCallFilter smali bad input`() {
with(patcher.context.bytecodeContext) {
var definingClass = "Landroid/view/View;"
var name = "inflate"
var parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
var returnType = "Landroid/view/View;"
var methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

assertThrows<IllegalArgumentException>("Bad max instructions before") {
MethodCallFilter.parseJvmMethodCall(methodSignature, null, -1)
}


definingClass = "Landroid/view/View"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
returnType = "Landroid/view/View;"
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

assertThrows<IllegalArgumentException>("Defining class missing semicolon") {
MethodCallFilter.parseJvmMethodCall(methodSignature)
}


definingClass = "Landroid/view/View;"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup")
returnType = "Landroid/view/View"
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

assertThrows<IllegalArgumentException>("Return type missing semicolon") {
MethodCallFilter.parseJvmMethodCall(methodSignature)
}


definingClass = "Landroid/view/View;"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
returnType = ""
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

assertThrows<IllegalArgumentException>("Empty return type") {
MethodCallFilter.parseJvmMethodCall(methodSignature)
}


definingClass = "Landroid/view/View;"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
returnType = "Landroid/view/View"
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

assertThrows<IllegalArgumentException>("Return type class missing semicolon") {
MethodCallFilter.parseJvmMethodCall(methodSignature)
}


definingClass = "Landroid/view/View;"
name = "inflate"
parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
returnType = "Q"
methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"

assertThrows<IllegalArgumentException>("Bad primitive type") {
MethodCallFilter.parseJvmMethodCall(methodSignature)
}
}
}

@Test
fun `FieldAccess smali parsing`() {
with(patcher.context.bytecodeContext) {
var definingClass = "Ljava/lang/Boolean;"
var name = "TRUE"
var type = "Ljava/lang/Boolean;"
var fieldSignature = "$definingClass->$name:$type"

var filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(type == filter.type!!()) },
)


definingClass = "Landroid/view/View\$InnerClass;"
name = "arrayField"
type = "[Ljava/lang/Boolean;"
fieldSignature = "$definingClass->$name:$type"

filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(type == filter.type!!()) },
)


definingClass = "Landroid/view/View\$InnerClass;"
name = "primitiveField"
type = "I"
fieldSignature = "$definingClass->$name:$type"

filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(type == filter.type!!()) },
)


definingClass = "Landroid/view/View\$InnerClass;"
name = "primitiveField"
type = "[I"
fieldSignature = "$definingClass->$name:$type"

filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)

assertAll(
{ assertTrue(definingClass == filter.definingClass!!()) },
{ assertTrue(name == filter.name!!()) },
{ assertTrue(type == filter.type!!()) },
)
}
}

@Test
fun `FieldAccess smali bad input`() {
with(patcher.context.bytecodeContext) {
assertThrows<IllegalArgumentException>("Defining class missing semicolon") {
FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View->fieldName:Landroid/view/View;")
}

assertThrows<IllegalArgumentException>("Type class missing semicolon") {
FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View;->fieldName:Landroid/view/View")
}

assertThrows<IllegalArgumentException>("Empty field name") {
FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View;->:Landroid/view/View;")
}

assertThrows<IllegalArgumentException>("Invalid primitive type") {
FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View;->fieldName:Q")
}
}
}

@Test
fun `NewInstance bad input`() {
with(patcher.context.bytecodeContext) {
assertThrows<IllegalArgumentException>("Defining class missing semicolon") {
newInstance("Lcom/whatever/BadClassType")
}
}
}


@Test
fun `CheckCast bad input`() {
with(patcher.context.bytecodeContext) {
assertThrows<IllegalArgumentException>("Defining class missing semicolon") {
checkCast("Lcom/whatever/BadClassType")
}
}
}

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 { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes.pool.values)
every { with(patcher.context.bytecodeContext) { mergeExtension(any<BytecodePatch>()) } } just runs

return runBlocking { patcher().toList() }