-
-
Notifications
You must be signed in to change notification settings - Fork 260
feat: Match fingerprints by instruction filters #329
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
base: dev
Are you sure you want to change the base?
feat: Match fingerprints by instruction filters #329
Conversation
Still a work in progress. But so far works well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation should be updated to match the new usage.
84739dd
to
98c98e2
Compare
98c98e2
to
7547319
Compare
… `@context` usage, simplify instruction filter block calls.
7ef1357
to
4d38837
Compare
57a5370
to
319a8a7
Compare
src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt
Outdated
Show resolved
Hide resolved
src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt
Outdated
Show resolved
Hide resolved
@@ -0,0 +1,633 @@ | |||
@file:Suppress("unused") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's multiple issues I have with this file and still confused from past discussions.
-
If InstructionFilter filters instructions as it implies, it is not supposed to have context about methods or classes, but instructions, regardless of the reason behind it. If context about classes or methods is needed, "InstructionFilter" needs to have a different name as it is something else than what the current name implies.
-
There are currently some existing filters implemented in Patcher under assumptions for relevance that should not be taken. Today field references, method calls, consts, and object instantiations may be relevant, tomorrow class references, string values, or other things that would require changing this file to adjust to a new assumption. APIs shouldn't be offered based on assumptions. Based on the current assumptions, you will restrict someone to current existing filters to avoid implementing the interface for whatever filter they need. Instead, an universal filter API should be offered that does not make any assumptions of what might or might not be useful and leave this decision to the API consumer. That said, those filters can be implemented somewhere in a separate module, outside of the patcher module, but can't be part of the patcher module just based on assumptions of relevance.
-
Comments should follow the current style. Constructor parameters should be commented as @param in an inline comment for the constructor. Constructors should start with a sentence explaining what the class is/does. Currently, some just jump to examples (such as in LiteralFilter)
-
Currently no DSL api is present, even though the patches & fingerprint API is currently fully DSL. Something like this would be acceptable:
fingerprint { instructions { field(...) Opcode.X() Opcode.Y() "string"() 123() } }
However with the filter API the current simple usage of opcode patterns now involves more boilerplate. Before:
fingerprint { opcodes(Opcode.X, Opcode.Y, ...) }
fingerprint { Opcode.X() Opcode.Y() ...() }
Every filter is added via invocation which has to be done as many times as many opcodes exist, a linear amount of times.
An alternative API would be
fingerprint { instructions { opcodes(Opcode.X, Opcode.Y) // Also works for one field() string("") .... } }
-
Filters look to me more like something suitable in custom block rather than replacing the opcode pattern. As explained in another review comment, fingerprints image a method, filters are not a direct attribute of a method making them suitable at most in custom (where also context about class and method both exist furthermore showing evidence of being a suitable place). Not sure how you'd want to pull that off, but replacing a direct attribute a fingerprint can image is something to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There will always be the usage of method filters, field, and literal constants.
It's up to the patches to declare new filters if desired. Such as ResourceMappingPatch declaring it's own filter that finds decoded resource literals.
Instruction filter no longer has a classDef parameter. The instruction method is passed as a parameter and most filters don't use it, but some require it to check how many indexes the method has and others to parse out the enclosing class.
It's important to note this is not a custom block replacement. It's checking the instruction on a more fine grained scale, and checking the ordering of the instructions, and it produces a list of indexes for those matches that is then used by the patch itself. There should be little to no usage of method.indexOfFirst(previousIndex) { /* do checking here */ }
, as these checks are now part of the fingerprint itself.
With just opcodes you only get patternMatchResults
which is the start/end. But with instruction filters you get indexes of each filter since there can be variable spacing between each filter.
This is an expansion of what opcodes previously did, which is why opcode filters still exist and can still be used.
Previously with only opcode method calls you could only declare invoke_interface
, but there is no way to indicate what it's invoking, especially when it's a non obfuscated class such as List.add()
. Now you can declare more specific usage of these opcodes and not just patterns which are fragile, can match completely unrelated stuff, and frequently break when just a single extra register move opcode is added by the compiler.
DSL style declarations can be added, that's not an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a simple example, where all the index searching was previously in the patch execute:
Before:
internal val shortsBottomBarContainerFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters("Landroid/view/View;", "Landroid/os/Bundle;")
strings("r_pfvc")
literal { bottomBarContainer }
}
shortsBottomBarContainerFingerprint.method.apply {
// Search for indexes after the fact, after the fingerprint already resolved.
// First instruction of interest.
val resourceIndex = indexOfFirstLiteralInstruction(bottomBarContainer)
// Second instruction of interest.
val index = indexOfFirstInstructionOrThrow(resourceIndex) {
getReference<MethodReference>()?.name == "getHeight"
}
// Third instruction of interest.
val heightRegister = getInstruction<OneRegisterInstruction>(index + 1).registerA
addInstructions(
index + 2,
"""
invoke-static { v$heightRegister }, $FILTER_CLASS_DESCRIPTOR->getNavigationBarHeight(I)I
move-result v$heightRegister
"""
)
}
Now the indexOfFirst() logic is in the fingerprint itself:
internal val shortsBottomBarContainerFingerprint by fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters("Landroid/view/View;", "Landroid/os/Bundle;")
strings("r_pfvc")
instructions(
// First instruction of interest.
ResourceMappingFilter("id", "bottom_bar_container"),
// Here lies other unrelated instructions.
// Second instruction of interest.
MethodFilter(methodName = "getHeight"),
// Third instruction of interest.
OpcodeFilter(Opcode.MOVE_RESULT)
)
}
shortsBottomBarContainerFingerprint.let {
it.method.apply {
val index = it.filterMatches.last().index
val heightRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
index + 1,
"""
invoke-static { v$heightRegister }, $FILTER_CLASS_DESCRIPTOR->getNavigationBarHeight(I)I
move-result v$heightRegister
"""
)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
InstructionFilter
can be renamed, but I'm unsure what other name to use.
MethodFilter
might be more appropriately named MethodCallFilter
, since it matches method calls based on specifics of that call.
FieldFilter
could be renamed to something like FieldAccessFilter
since it matches iget
, iput
, sget
, sput
, etc.
Edit: Renamed to MethodCallFilter
and FieldAccessFilter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There will always be the usage of method filters, field, and literal constants.
It's up to the patches to declare new filters if desired. Such as ResourceMappingPatch declaring it's own filter that finds decoded resource literals.
While the current filters and functions like addInstructions
are useful for certain cases, they can't cover every scenario. Assuming that filters will always be necessary is a flawed approach, as there may be situations where none of the existing filters are suitable. These utilities are based on assumptions of common usage, but this can limit flexibility.
The same applies to addInstructions
. Its essentially just adding an item to a list, but introducing this specific functionality as an extension function is an overspecialization that conflicts with the goal of maintaining a generic library. Filters should follow the same principle: while the interface for filters is generic, providing a predefined set of filters creates unnecessary constraints. I’d prefer to move the actual filter implementations to an external module, ideally separate from the patcher repo.
Instruction filter no longer has a classDef parameter. The instruction method is passed as a parameter and most filters don't use it, but some require it to check how many indexes the method has and others to parse out the enclosing class.
An instruction filter should only have context about the instructions. Bringing the method into this context is problematic in terms of abstraction. A filter for instructions relying on a method does not sound right. If somehow it is necessary, it means you need to rethink what "instruction filters" actually are. Perhaps they are more than just that given that you need context about the method.
This is an expansion of what opcodes previously did, which is why opcode filters still exist and can still be used.
There should be one clear way to handle instruction fingerprinting. If we’re moving forward with filters, the old opcode filter approach should be replaced with the new approach and reimplemented in it if necessary.
Now you can declare more specific usage of these opcodes and not just patterns which are fragile, can match completely unrelated stuff, and frequently break when just a single extra register move opcode is added by the compiler.
I think here it also shows that there is a specialization in one direction that is assumed to be likely useful. However, it is nonetheless a specialization that shouldn't happen in a library context that is supposed to be abstract. An example is that you can now filter for method references, but how about filtering for the field type only in field references? You'd now ask to implement the interface to satisfy this situation and would have failed to provide an universal API via the existing filters implementations, because, albeit being likely useful, they are after all specialized for specific usecases.
val index = it.filterMatches.last().index
Regarding the API, it can also be useful, if you can declare a filter so that you can reference it later on. This avoids having to rely on the index of filterMatches. In your example it could look like that:
val opcodeFilter = OpcodeFilter(Opcode.MOVE_RESULT)
internal val shortsBottomBarContainerFingerprint by fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters("Landroid/view/View;", "Landroid/os/Bundle;")
strings("r_pfvc")
instructions(
// First instruction of interest.
ResourceMappingFilter("id", "bottom_bar_container"),
// Here lies other unrelated instructions.
// Second instruction of interest.
MethodFilter(methodName = "getHeight"),
// Third instruction of interest.
opcodeFilter()
)
}
shortsBottomBarContainerFingerprint.let {
it.method.apply {
val index = iopcodeFilter.index // Notice the reference to the val opcodeFilter
val heightRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
index + 1,
"""
invoke-static { v$heightRegister }, $FILTER_CLASS_DESCRIPTOR->getNavigationBarHeight(I)I
move-result v$heightRegister
"""
)
}
}
InstructionFilter can be renamed, but I'm unsure what other name to use.
After all you don't just filter based on the instructions. This is the reason I had initially assumed it to be similar to the custom block of fingerprints. For that reason, a different name is needed. Can you explain why you need anything else than instructions to filter instructions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can already do exactly that with this PR. A field access can declare any part of the access, and leave out the parts that are obfuscated or it's desired to ignore. Such as: fieldAccess(type = "Ljava/lang/String;")
And how do i filter for GET vs PUT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what you mean by "enclosing" or "this":
The enclosing method is needed for methodCall() andfieldAccess() to use the this keyword, since it's impossible to declare an obfuscated class for the method/field access but using this can be used to indicate it's a call to the enclosing class. The declaring class (which is part of the method object) is needed for support the functionality. The declaring method is also used by lastInstruction() since it needs to know how many instructions are in the enclosed method. I don't see any issues with passing along the enclosing method of the instruction as it allows more flexibility such as here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now all use cases of instruction filtering is covered. I can't think of or find any situations where a filter is not provided for a use case.
Because smali opcodes don't change and are pretty much set in stone these days, there won't be any new use cases if what is provided here covers everything.
The only situation I can see where custom filters could be created, is for specific resource filtering (such as looking up resources by name from an embedded JSON file in the app, or any other situation where resources are compiled into code). But even that could still be done with a helper method that returns a literal instruction filter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I deprecated the old opcodes() method, but unless someone wants to spend the possible 10+ hours updating all the old code (I definitely do not want to), then it's much easier and more reliably to deprecate but still support the old patches code.
I dont follow. What do you mean "updating all the old code"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example you list is exactly how this works right now, and the code shown is taken from the patches PR of this change.
I checked the API of the class "InstructionFilter" and could not find a public field index. How do you get it like I showed in the propsed API?
If you mean a FieldReference or some other object, there isn't one and there cannot be a concrete field because it's a filter that matches against a FieldReference. It cannot define an exact field type unless the filter declares everything (name, return type, parameters, access flags, etc). I don't see how this could be useful.
I meant retrieving the matching index. Look closely at the examplary code I provided:

…r from 0.15.1 to 0.18.1 (ReVanced#362)
…1.8.1 to 1.10.2 (ReVanced#351) build(deps): bump org.jetbrains.kotlinx:kotlinx-coroutines-core --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
) Bumps [burrunan/gradle-cache-action](https://github.com/burrunan/gradle-cache-action) from 1 to 3. - [Release notes](https://github.com/burrunan/gradle-cache-action/releases) - [Changelog](https://github.com/burrunan/gradle-cache-action/blob/main/CHANGELOG.md) - [Commits](burrunan/gradle-cache-action@v1...v3) --- updated-dependencies: - dependency-name: burrunan/gradle-cache-action dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…eVanced#353) --- updated-dependencies: - dependency-name: com.android.tools.smali:smali dependency-version: 3.0.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
--- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.jetbrains.kotlin:kotlin-test dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.jetbrains.kotlin.jvm dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
opcodes(Opcode.RETURN) | ||
strings("pro") | ||
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" } | ||
// Declared parameters are matched using String.startsWith() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be better to match with "contains". Sometimes packages are minified, but classes not. E.g. Lp/SomeClass;.
I think for flexibility reasons it may be useful to make the matcher parametrized. Every possible place where "matching" can be done in different ways should be configurable. Make it possible to let the user choose the matching type. E.g.
return eq "Z"
or
return startsWith "Z"
As you can see the API is strongly typed and domain specific. At this point we have to decide. Does this level of DSL make sense?
If we have such fidelity, why not simply provide the return type and ask for a lambda? E.g:
fingerprint { method.returnType == "Z" }
What kind of abstraction do we want? What can we benefit of it? Performance? With the DSL we can have hashmaps, with a lambda, maybe? It is possible if you expose the maps:
fingerprint { "Z" in returnsTypes }
As of right now, we have strong limitations everywhere in the API. The parameters API is hardcoded to match with startsWith. Its a clear opinionated limitation and as such we have to think about the above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've never seen proguard obfuscate a package but not the class name in a package.
So starts with always works because if the class name isn't obfuscated, then the package also isn't obfuscated.
What I think could be helpful, is to allow multiple values for access flags, return type, and especially parameters.
There are times where access flags change between app targets, and other times where parameters change slightly (commonly an additional parameter is added).
It would simplify some YT fingerprints where parameters could be multiple parameter declarations, and any parameter list can match.
I didn't add this because it's only useful for YT since most apps only care about patching the latest, and it adds more complexity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because it's only useful for YT since most apps
This can not be assumed. You cant force matching to just startsWith. My personal and private app is obfuscating only packages and already this should proof the need of e.g. endsWith. So if you just implement startsWith you have a conceptual limitation. With the proposed API this clear problem is solved precisely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But is it stripping packages, or obfuscating package names and keeping the class names?
If it's stripping packages (everything is always moved to default or a specific obfuscated package), then starts with still works.
But this could be changed to support both "ends with" and "start with". I'll try adding this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But is it stripping packages, or obfuscating package names and keeping the class names?
Of course it will be the one where startsWith doesnt work for demonstrative purposes.
But this could be changed to support both "ends with" and "start with". I'll try adding this.
What if my obfuscator obfuscates the end and the start?
Lp/Class->obj;LType becomes Lp/ABC->obj;LCDE
As you can see, as long as you dont provide full coverage of the api, and try to solve one special case with a special solution, another special case will still remain unsolved.
The API must somehow provide a way where you can let the user "choose the matcher".
E.g.
fun string(s: String, matcher: IMatcher)
IMatcher can be anything in this case and automatically covers any case you can imagine, not just cases where startsWith works. The example is not clean but it proofs theres a way to cover all cases. Now we just have to figure out a clean DSL/API.
E.g. the one i proposed
return eq "Z"
(eq would be a builtin prefix function which is emits an Instruction filter in the this
receiver)
So it would basically translate to
instructions {
eq(return, "s") // emits a string filter with equals matching
}
if you generalize merely this api provides full coverage:
instructions {
instruction {
// "this" is the current instruction
}
}
This API provides FULL coverage because instruction becomes a lambda where you can define anything, e.g.
instructions {
instruction {
it.getReference().string == "A"
}
}
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The source files are not meant for navigation. As for such they should not conform a name that is supposed to enable or aid that. Removing the prefix entirely is even okay as there's no specific reason for the prefix to be in the name outside for the unwanted purpose of navigation through the source files. Instead, navigation will be enabled once the files are rendered by some parser into a user-facing document, e.g. the site, a pdf or similar.
execute { | ||
// Fingerprint to find the method to patch. | ||
val showAdsFingerprint = fingerprint { | ||
val showAdsFingerprint by fingerprint { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a purpose of the field name being visible in the stack trace when there's already the LoC. In this case I can see it occurred in line 63 which is unambiguous. This spares the necessity for non-intuitive-on-first-sight API which you have to look up to understand just for a dininishing return of now understanding its minor use.
// Method implementation: | ||
instructions( | ||
// Filter 1. | ||
fieldAccess( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these apis should follow the opcode names, e.g.
GetObject
PutObject
Once again, with the other review comment above, these are limited apis. What if I want to match for iGet instead of Get. This flexibility issue needs to be fixed regardless of how likely or unlikely you will need it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can match any specific get/put instruction by specifying one or more opcodes with the filter. Then you can define if it's a static get/put or it's accessing a primitive/object/array/etc from an object field. So the filter can be as wide as any kind of field access or as specific as matching an exact field type that is static or not static.
I agree it could be two different field access filters, one for get and another for put, because it's always one or the other and never do you want to match either or.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
specifying one or more opcodes with the filter
Then i cant match on the operand. How do I match "iput & operand = SomeField"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm away from my computer, but that is possible right now.
Something like:
fieldAccess(opcode = Opcodes.IPUT, name = "someField")
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" } | ||
unrelatedMethod(value1); | ||
|
||
// Filter 2, 3, 4 target instructions, and the instructions to modify. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, "filter" is terminology we can get rid of before we introduce it. We also do not call the returns api a filter, so consistency must be retained somehow
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"clearMatch" is weird api. I recall there was something being said about good api that an api should not just be callable once, e.g. that there should not be a "state", unless it is explicit, and where it acts as a deconstructor for some class/state
Examples where it is allowed is "close" in streams, because it is a kind of deconstructor.
We can fix the issue with performance when multiple patches rely on a fingerprint by moving the match out of the fingerprint to the designated bytecode context.
E.g.
with(someBytecodeContext) {
fingerprint.match() // matches in relation to the context
}
with(otherBytecodeContext) {
fingerprint.match() // will match again
fingerprint.match() // wont match again
}
with(someBytecodeContext) {
fingerprint.match() // early returns since in the current context it already has matched
}
Code implementation wise we might not want to move the "match" field to the context. But keeping track of contexts via a map in the fingerprint class also is weird. So implementation wise this has to be looked into first. Right now I only have:
class BytecodeContext {
matches: Map<Fingerprint, Match>
}
or
class Fingerprint {
matches: Map<BytecodeContext, Match>
}
- `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. If no match is found, an exception is raised. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might wanna rename to "immutableClassDef". This would make the API naming consistent with dexlib (the type is called ImmutableClassDef)
```kt | ||
execute { | ||
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } | ||
val adsLoaderClass = classBy("Lcom/some/app/ads/Loader;") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should get rid of this abstraction. It is overspecialized and redundant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exposing "classes" is enough as you can then simply use classes.single
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can simplify this api by replacing recursion with iteration.
This would spare us the odd api "stop".
Instead we could have
fun to(vararg indices: Int, mutable: boolean)
Chaining can be allowed with another overload:
fun Method.to(vararg indices: Int, mutable: boolean)
this way it will work with:
navigate(someMethod).to(1,2,3).to(4, mutable=true)
We could maybe even get rid of more abstraction and just provide:
navigate(someMethod, 1,2,3, mutable)
and maybe even normalize the name for example with
method(from=someMethod, at=1, mutable=true) (or getMethod etc)
/** | ||
* All classes for the target app and any extension classes. | ||
*/ | ||
class PatchClasses internal constructor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats the motivation for the name?
Not a full review, but advancing iteratively |
Adds instruction filters to support more flexible instruction fingerprinting.
Changes
Fingerprints can still use opcode patterns, or they can use instruction filters that allow more precise matching.
Basic support exists for matching instructions using:
Projects can define their own custom instruction filters, such as ResourceMappingPatch with it's own kind of LiteralFilter that matches resource literal values (no more mucking about with using a ResourcePatches to first set a resource value a fingerprint then uses).
Variable space allowed between instruction filters
By default, all filters allow any amount of space between them. But if filters are always immediately after each other, or there is a rough estimate of the maximum number of indexes until the next instruction, then a maximum distance can be set. An example is using an opcode filter of
MOVE_RESULT
orMOVE_RESULT_OBJECT
after a method call, where the max instruction spacing is always 0.Breaking changes
Fuzzy pattern match is now obsolete, as it's functionality is now part of the filtering itself. Variable spacing is allowed between instruction filters, and non important instructions are now ignored by simply not defining instruction filters for them.
Fingerprints are now declared using
by
semantics.Before:
internal val shortsBottomBarContainerFingerprint = fingerprint {
After:
internal val shortsBottomBarContainerFingerprint by fingerprint {
If a fingerprint fails to resolve, the stack traces now includes the fingerprint name.
Example fingerprint before and after this change
Before:
Now the indexOfFirst() logic is in the fingerprint itself: