chore: Merge branch dev to main (#260)

This commit is contained in:
oSumAtrIX 2023-12-01 01:17:47 +01:00 committed by GitHub
commit 288240f163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 8240 additions and 2990 deletions

View File

@ -1,72 +0,0 @@
name: 🐞 Bug report
description: Report a very clearly broken issue.
title: 'bug: <title>'
labels: [bug]
body:
- type: markdown
attributes:
value: |
# ReVanced bug report
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-patcher/labels/bug).
- type: dropdown
attributes:
label: Type
options:
- Crash
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Bug description
description: How did you find the bug? Any additional details that might help?
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Add the steps to reproduce this bug including your environment.
placeholder: Step 1. Download some files. Step 2. ...
validations:
required: true
- type: textarea
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Screenshots or videos
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I filled out all of the requested information in this issue properly.
required: true

108
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -0,0 +1,108 @@
name: 🐞 Bug report
description: Report a bug or an issue.
title: 'bug: '
labels: ['Bug report']
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# ReVanced Patcher bug report
Before creating a new bug report, please keep the following in mind:
- **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-patcher/labels/Bug%20report).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
validations:
required: true
- type: textarea
attributes:
label: Error logs
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
render: shell
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution to the bug.
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: This issue is not a duplicate of an existing bug report.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

View File

@ -1,58 +0,0 @@
name: ⭐ Feature request
description: Create a detailed feature request.
title: 'feat: <title>'
labels: [feature-request]
body:
- type: markdown
attributes:
value: |
# ReVanced feature request
Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead.
Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-patcher/labels/feature-request).
- type: dropdown
attributes:
label: Type
options:
- Functionality
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Issue
description: What is the current problem. Why does it require a feature request?
validations:
required: true
- type: textarea
attributes:
label: Feature
description: Describe your feature in detail. How does it solve the issue?
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Why should your feature should be considered?
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I filled out all of the requested information in this issue properly.
required: true

View File

@ -0,0 +1,106 @@
name: ⭐ Feature request
description: Create a detailed request for a new feature.
title: 'feat: '
labels: ['Feature request']
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# ReVanced Patcher feature request
Before creating a new feature request, please keep the following in mind:
- **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-patcher/labels/Feature%20request).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Feature description
description: |
- Describe your feature in detail
- Add images, videos, links, examples, references, etc. if possible
- Add the target application name in case you request a new patch
- type: textarea
attributes:
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
- What makes this feature important?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- label: This issue is not a duplicate of an existing feature request.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

2
.github/config.yml vendored
View File

@ -1,2 +1,2 @@
firstPRMergeComment: > firstPRMergeComment: >
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role. Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.

View File

@ -16,6 +16,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Open pull request - name: Open pull request
uses: repo-sync/pull-request@v2 uses: repo-sync/pull-request@v2
with: with:

View File

@ -23,22 +23,25 @@ jobs:
# https://github.com/cycjimmy/semantic-release-action#private-packages # https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
- name: Cache
- name: Cache Node modules
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: | path: |
${{ runner.home }}/.gradle/caches
${{ runner.home }}/.gradle/wrapper
.gradle
build
node_modules node_modules
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }} key: npm-${{ hashFiles('package-lock.json') }}
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build with Gradle - name: Build with Gradle
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build clean --no-daemon run: ./gradlew build clean --no-daemon
- name: Setup semantic-release - name: Setup semantic-release
run: npm install run: npm install
- name: Release - name: Release
env: env:
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}

View File

@ -1,3 +1,16 @@
# [19.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.0.0...v19.1.0-dev.1) (2023-11-29)
### Features
* Add constructor to initialize patches without annotations ([462fbe2](https://github.com/ReVanced/revanced-patcher/commit/462fbe2cadf56d8b0dde33319256021093bd39d5))
* Retrieve annotations in super and interface classes ([7aeae93](https://github.com/ReVanced/revanced-patcher/commit/7aeae93f3d9a13e294fe1bdb2586f79908af60af))
### Performance Improvements
* Use a hash set for fast lookup ([f1de9b3](https://github.com/ReVanced/revanced-patcher/commit/f1de9b39eff1db44c00acd3e41902b3ec6124776))
# [19.0.0](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0...v19.0.0) (2023-10-24) # [19.0.0](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0...v19.0.0) (2023-10-24)

View File

@ -175,6 +175,7 @@ public abstract class app/revanced/patcher/fingerprint/MethodFingerprint {
public fun <init> ()V public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)V public fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getFuzzyPatternScanMethod ()Lapp/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod;
public final fun getResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; public final fun getResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult;
public final fun resolve (Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z public final fun resolve (Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun resolve (Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z public final fun resolve (Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
@ -233,11 +234,14 @@ public abstract interface annotation class app/revanced/patcher/fingerprint/anno
public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public fun <init> ()V public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLjava/util/Set;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLjava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/util/Set;)V public fun <init> (Ljava/util/Set;)V
public synthetic fun <init> (Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
} }
public abstract class app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z public fun equals (Ljava/lang/Object;)Z
public abstract fun execute (Lapp/revanced/patcher/data/Context;)V public abstract fun execute (Lapp/revanced/patcher/data/Context;)V
public final fun getCompatiblePackages ()Ljava/util/Set; public final fun getCompatiblePackages ()Ljava/util/Set;
@ -271,6 +275,8 @@ public final class app/revanced/patcher/patch/PatchResult {
public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
public fun <init> ()V public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
} }
public abstract interface annotation class app/revanced/patcher/patch/annotation/CompatiblePackage : java/lang/annotation/Annotation { public abstract interface annotation class app/revanced/patcher/patch/annotation/CompatiblePackage : java/lang/annotation/Annotation {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 19.0.0 version = 19.1.0-dev.1

View File

@ -1,8 +1,8 @@
[versions] [versions]
android = "4.1.1.4" android = "4.1.1.4"
kotlin-reflect = "1.9.0" kotlin-reflect = "1.9.10"
apktool-lib = "2.9.1" apktool-lib = "2.9.1"
kotlin-test = "1.8.20-RC" kotlin-test = "1.9.10"
kotlinx-coroutines-core = "1.7.3" kotlinx-coroutines-core = "1.7.3"
multidexlib2 = "3.0.3.r3" multidexlib2 = "3.0.3.r3"
smali = "3.0.3" smali = "3.0.3"

View File

@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000 distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dist

8606
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
{ {
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-backmerge": "^3.1.0", "@saithodev/semantic-release-backmerge": "^3.2.1",
"@semantic-release/changelog": "^6.0.2", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.7.6", "gradle-semantic-release-plugin": "^1.8.0",
"semantic-release": "^20.1.0" "semantic-release": "^22.0.8"
} }
} }

View File

@ -1 +1,7 @@
rootProject.name = "revanced-patcher" rootProject.name = "revanced-patcher"
buildCache {
local {
isEnabled = !System.getenv().containsKey("CI")
}
}

View File

@ -12,6 +12,7 @@ import java.util.jar.JarFile
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
* A set of [Patch]es. * A set of [Patch]es.
*/ */
@ -37,7 +38,7 @@ sealed class PatchBundleLoader private constructor(
// This constructor parameter is unfortunately necessary, // This constructor parameter is unfortunately necessary,
// so that a reference to the mutable set is present in the constructor to be able to add patches to it. // so that a reference to the mutable set is present in the constructor to be able to add patches to it.
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter. // because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
private val patchSet: MutableSet<Patch<*>> = mutableSetOf() private val patchSet: MutableSet<Patch<*>> = mutableSetOf(),
) : PatchSet by patchSet { ) : PatchSet by patchSet {
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name) private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
@ -63,22 +64,29 @@ sealed class PatchBundleLoader private constructor(
* @param silent Whether to suppress logging. * @param silent Whether to suppress logging.
* @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated. * @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated.
*/ */
internal fun Class<*>.getInstance(logger: Logger, silent: Boolean = false): Patch<*>? { internal fun Class<*>.getInstance(
logger: Logger,
silent: Boolean = false,
): Patch<*>? {
return try { return try {
getField("INSTANCE").get(null) getField("INSTANCE").get(null)
} catch (exception: NoSuchFieldException) { } catch (exception: NoSuchFieldException) {
if (!silent) logger.fine( if (!silent) {
"Patch class '${name}' has no INSTANCE field, therefor not a singleton. " + logger.fine(
"Will try to instantiate it." "Patch class '$name' has no INSTANCE field, therefor not a singleton. " +
"Attempting to instantiate it.",
) )
}
try { try {
getDeclaredConstructor().newInstance() getDeclaredConstructor().newInstance()
} catch (exception: Exception) { } catch (exception: Exception) {
if (!silent) logger.severe( if (!silent) {
"Patch class '${name}' is not singleton and has no suitable constructor, " + logger.severe(
"therefor cannot be instantiated and will be ignored." "Patch class '$name' is not singleton and has no suitable constructor, " +
"therefor cannot be instantiated and is ignored.",
) )
}
return null return null
} }
@ -97,7 +105,7 @@ sealed class PatchBundleLoader private constructor(
{ patchBundle -> { patchBundle ->
JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") } JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.replace('/', '.').replace(".class", "") } .map { it.name.replace('/', '.').replace(".class", "") }
} },
) )
/** /**
@ -109,9 +117,10 @@ sealed class PatchBundleLoader private constructor(
*/ */
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader( class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
DexClassLoader( DexClassLoader(
patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath, patchBundles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null, null,
PatchBundleLoader::class.java.classLoader PatchBundleLoader::class.java.classLoader,
), ),
patchBundles, patchBundles,
{ patchBundle -> { patchBundle ->
@ -119,7 +128,7 @@ sealed class PatchBundleLoader private constructor(
.map { classDef -> .map { classDef ->
classDef.type.substring(1, classDef.length - 1) classDef.type.substring(1, classDef.length - 1)
} }
} },
) { ) {
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.") @Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null) constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)

View File

@ -2,8 +2,7 @@ package app.revanced.patcher
import app.revanced.patcher.PatchBundleLoader.Utils.getInstance import app.revanced.patcher.PatchBundleLoader.Utils.getInstance
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.fingerprint.LookupMap.Maps.clearLookupMaps import app.revanced.patcher.fingerprint.LookupMap
import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps
import app.revanced.patcher.fingerprint.MethodFingerprint.Companion.resolveUsingLookupMap import app.revanced.patcher.fingerprint.MethodFingerprint.Companion.resolveUsingLookupMap
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.*
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@ -18,7 +17,7 @@ import java.util.logging.Logger
* @param options The options for the patcher. * @param options The options for the patcher.
*/ */
class Patcher( class Patcher(
private val options: PatcherOptions private val options: PatcherOptions,
) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable { ) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable {
private val logger = Logger.getLogger(Patcher::class.java.name) private val logger = Logger.getLogger(Patcher::class.java.name)
@ -40,6 +39,7 @@ class Patcher(
// * @param patches The [Patch]es to add. // * @param patches The [Patch]es to add.
// * @throws PatcherException.CircularDependencyException If a circular dependency is detected. // * @throws PatcherException.CircularDependencyException If a circular dependency is detected.
// */ // */
/** /**
* Add [Patch]es to ReVanced [Patcher]. * Add [Patch]es to ReVanced [Patcher].
* *
@ -127,8 +127,8 @@ class Patcher(
* @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails. * @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails.
* @return A pair of the name of the [Patch] and its [PatchResult]. * @return A pair of the name of the [Patch] and its [PatchResult].
*/ */
override fun apply(returnOnError: Boolean) = flow { override fun apply(returnOnError: Boolean) =
flow {
/** /**
* Execute a [Patch] and its dependencies recursively. * Execute a [Patch] and its dependencies recursively.
* *
@ -138,7 +138,7 @@ class Patcher(
*/ */
fun executePatch( fun executePatch(
patch: Patch<*>, patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, PatchResult> executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult { ): PatchResult {
val patchName = patch.name ?: patch.toString() val patchName = patch.name ?: patch.toString()
@ -160,8 +160,8 @@ class Patcher(
patch, patch,
PatchException( PatchException(
"'$patchName' depends on '${dependency.name ?: dependency}' " + "'$patchName' depends on '${dependency.name ?: dependency}' " +
"that raised an exception:\n${it.stackTraceToString()}" "that raised an exception:\n${it.stackTraceToString()}",
) ),
) )
} }
} }
@ -188,11 +188,12 @@ class Patcher(
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
initializeLookupMaps(context.bytecodeContext) LookupMap.initializeLookupMaps(context.bytecodeContext)
// Prevent from decoding the app manifest twice if it is not needed. // Prevent from decoding the app manifest twice if it is not needed.
if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) {
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL) context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL)
}
logger.info("Executing patches") logger.info("Executing patches")
@ -220,7 +221,8 @@ class Patcher(
.filter { it.patch is Closeable }.asReversed().forEach { executedPatch -> .filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
val patch = executedPatch.patch val patch = executedPatch.patch
val result = try { val result =
try {
(patch as Closeable).close() (patch as Closeable).close()
executedPatch executedPatch
@ -236,9 +238,9 @@ class Patcher(
patch, patch,
PatchException( PatchException(
"'${patch.name}' raised an exception while being closed: ${it.stackTraceToString()}", "'${patch.name}' raised an exception while being closed: ${it.stackTraceToString()}",
result.exception result.exception,
) ),
) ),
) )
if (returnOnError) return@flow if (returnOnError) return@flow
@ -250,17 +252,17 @@ class Patcher(
} }
} }
override fun close() = clearLookupMaps() override fun close() = LookupMap.clearLookupMaps()
/** /**
* Compile and save the patched APK file. * Compile and save the patched APK file.
* *
* @return The [PatcherResult] containing the patched input files. * @return The [PatcherResult] containing the patched input files.
*/ */
override fun get() = PatcherResult( override fun get() =
PatcherResult(
context.bytecodeContext.get(), context.bytecodeContext.get(),
context.resourceContext.get(), context.resourceContext.get(),
context.packageMetadata.apkInfo.doNotCompress?.toList() context.packageMetadata.apkInfo.doNotCompress?.toList(),
) )
} }

View File

@ -9,8 +9,7 @@ package app.revanced.patcher
sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) { sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null) constructor(errorMessage: String) : this(errorMessage, null)
class CircularDependencyException internal constructor(dependant: String) : PatcherException( class CircularDependencyException internal constructor(dependant: String) : PatcherException(
"Patch '$dependant' causes a circular dependency" "Patch '$dependant' causes a circular dependency",
) )
} }

View File

@ -32,19 +32,22 @@ data class PatcherOptions(
/** /**
* The configuration to use for resource decoding and compiling. * The configuration to use for resource decoding and compiling.
*/ */
internal val resourceConfig = Config.getDefaultConfig().apply { internal val resourceConfig =
Config.getDefaultConfig().apply {
useAapt2 = true useAapt2 = true
aaptPath = aaptBinaryPath ?: "" aaptPath = aaptBinaryPath ?: ""
frameworkDirectory = frameworkFileDirectory frameworkDirectory = frameworkFileDirectory
} }
fun recreateResourceCacheDirectory() = resourceCachePath.also { fun recreateResourceCacheDirectory() =
resourceCachePath.also {
if (it.exists()) { if (it.exists()) {
logger.info("Deleting existing resource cache directory") logger.info("Deleting existing resource cache directory")
if (!it.deleteRecursively()) if (!it.deleteRecursively()) {
logger.severe("Failed to delete existing resource cache directory") logger.severe("Failed to delete existing resource cache directory")
} }
}
it.mkdirs() it.mkdirs()
} }

View File

@ -12,7 +12,7 @@ import java.io.InputStream
data class PatcherResult( data class PatcherResult(
val dexFiles: List<PatchedDexFile>, val dexFiles: List<PatchedDexFile>,
val resourceFile: File?, val resourceFile: File?,
val doNotCompress: List<String>? = null val doNotCompress: List<String>? = null,
) { ) {
/** /**
* Wrapper for dex files. * Wrapper for dex files.

View File

@ -41,8 +41,12 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
val classes by lazy { val classes by lazy {
ProxyClassList( ProxyClassList(
MultiDexIO.readDexFile( MultiDexIO.readDexFile(
true, options.inputFile, BasicDexFileNamer(), null, null true,
).also { opcodes = it.opcodes }.classes.toMutableSet() options.inputFile,
BasicDexFileNamer(),
null,
null,
).also { opcodes = it.opcodes }.classes.toMutableSet(),
) )
} }
@ -67,8 +71,8 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
*/ */
fun findClass(predicate: (ClassDef) -> Boolean) = fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate... // if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?: classes.proxies.firstOrNull { predicate(it.immutableClass) }
// else resolve the class to a proxy and return it, if the predicate is matching a class ?: // else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) } classes.find(predicate)?.let { proxy(it) }
/** /**
@ -78,7 +82,8 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
* @param classDef The class to proxy. * @param classDef The class to proxy.
* @return A proxy for the class. * @return A proxy for the class.
*/ */
fun proxy(classDef: ClassDef) = this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let { fun proxy(classDef: ClassDef) =
this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
ClassProxy(classDef).also { this.classes.add(it) } ClassProxy(classDef).also { this.classes.add(it) }
} }
@ -98,7 +103,8 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
override fun get(): List<PatcherResult.PatchedDexFile> { override fun get(): List<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files") logger.info("Compiling patched dex files")
val patchedDexFileResults = options.resourceCachePath.resolve("dex").also { val patchedDexFileResults =
options.resourceCachePath.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty. it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs() it.mkdirs()
}.apply { }.apply {
@ -109,9 +115,10 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
BasicDexFileNamer(), BasicDexFileNamer(),
object : DexFile { object : DexFile {
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses) override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
override fun getOpcodes() = this@BytecodeContext.opcodes override fun getOpcodes() = this@BytecodeContext.opcodes
}, },
DexIO.DEFAULT_MAX_DEX_POOL_SIZE DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info("Compiled $entryName") } ) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) } }.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
@ -143,11 +150,13 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
this@Integrations.forEach { integrations -> this@Integrations.forEach { integrations ->
MultiDexIO.readDexFile( MultiDexIO.readDexFile(
true, true,
integrations, BasicDexFileNamer(), integrations,
BasicDexFileNamer(),
null,
null, null,
null
).classes.forEach classDef@{ classDef -> ).classes.forEach classDef@{ classDef ->
val existingClass = classMap[classDef.type] ?: run { val existingClass =
classMap[classDef.type] ?: run {
logger.fine("Adding $classDef") logger.fine("Adding $classDef")
classes.add(classDef) classes.add(classDef)
return@classDef return@classDef
@ -158,7 +167,10 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass -> existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class. // If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) return@let if (mergedClass === existingClass) return@let
classes.apply { remove(existingClass); add(mergedClass) } classes.apply {
remove(existingClass)
add(mergedClass)
}
} }
} }
} }

View File

@ -26,7 +26,7 @@ import java.util.logging.Logger
*/ */
class ResourceContext internal constructor( class ResourceContext internal constructor(
private val context: PatcherContext, private val context: PatcherContext,
private val options: PatcherOptions private val options: PatcherOptions,
) : Context<File?>, Iterable<File> { ) : Context<File?>, Iterable<File> {
private val logger = Logger.getLogger(ResourceContext::class.java.name) private val logger = Logger.getLogger(ResourceContext::class.java.name)
@ -37,7 +37,8 @@ class ResourceContext internal constructor(
* *
* @param mode The [ResourceDecodingMode] to use when decoding. * @param mode The [ResourceDecodingMode] to use when decoding.
*/ */
internal fun decodeResources(mode: ResourceDecodingMode) = with(context.packageMetadata.apkInfo) { internal fun decodeResources(mode: ResourceDecodingMode) =
with(context.packageMetadata.apkInfo) {
// Needed to decode resources. // Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this) val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this)
@ -54,7 +55,8 @@ class ResourceContext internal constructor(
val apkDecoder = ApkDecoder(options.resourceConfig, this) val apkDecoder = ApkDecoder(options.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping) apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework = UsesFramework().apply { usesFramework =
UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id } ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
} }
} }
@ -66,14 +68,14 @@ class ResourceContext internal constructor(
// because it does not support decoding to an OutputStream. // because it does not support decoding to an OutputStream.
XmlPullStreamDecoder( XmlPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable), AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer resourcesDecoder.resXmlSerializer,
).decodeManifest( ).decodeManifest(
apkFile.directory.getFileInput("AndroidManifest.xml"), apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream() // Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() { object : OutputStream() {
override fun write(b: Int) { /* do nothing */ override fun write(b: Int) { // do nothing
}
} }
},
) )
// Get the package name and version from the manifest using the XmlPullStreamDecoder. // Get the package name and version from the manifest using the XmlPullStreamDecoder.
@ -96,14 +98,12 @@ class ResourceContext internal constructor(
} }
} }
} }
} }
operator fun get(path: String) = options.resourceCachePath.resolve(path) operator fun get(path: String) = options.resourceCachePath.resolve(path)
override fun iterator() = options.resourceCachePath.walkTopDown().iterator() override fun iterator() = options.resourceCachePath.walkTopDown().iterator()
/** /**
* Compile resources from the [ResourceContext]. * Compile resources from the [ResourceContext].
* *
@ -116,14 +116,17 @@ class ResourceContext internal constructor(
logger.info("Compiling modified resources") logger.info("Compiling modified resources")
val cacheDirectory = ExtFile(options.resourceCachePath) val cacheDirectory = ExtFile(options.resourceCachePath)
val aaptFile = cacheDirectory.resolve("aapt_temp_file").also { val aaptFile =
cacheDirectory.resolve("aapt_temp_file").also {
Files.deleteIfExists(it.toPath()) Files.deleteIfExists(it.toPath())
}.also { resourceFile = it } }.also { resourceFile = it }
try { try {
AaptInvoker( AaptInvoker(
options.resourceConfig, context.packageMetadata.apkInfo options.resourceConfig,
).invokeAapt(aaptFile, context.packageMetadata.apkInfo,
).invokeAapt(
aaptFile,
cacheDirectory.resolve("AndroidManifest.xml").also { cacheDirectory.resolve("AndroidManifest.xml").also {
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it) ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
}, },
@ -134,7 +137,8 @@ class ResourceContext internal constructor(
usesFramework.ids.map { id -> usesFramework.ids.map { id ->
Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag) Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag)
}.toTypedArray() }.toTypedArray()
}) },
)
} finally { } finally {
cacheDirectory.close() cacheDirectory.close()
} }
@ -159,12 +163,10 @@ class ResourceContext internal constructor(
} }
inner class XmlFileHolder { inner class XmlFileHolder {
operator fun get(inputStream: InputStream) = operator fun get(inputStream: InputStream) = DomFileEditor(inputStream)
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor { operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path]) return DomFileEditor(this@ResourceContext[path])
} }
} }
} }

View File

@ -1,33 +1,61 @@
@file:Suppress("UNCHECKED_CAST")
package app.revanced.patcher.extensions package app.revanced.patcher.extensions
import kotlin.reflect.KClass import kotlin.reflect.KClass
internal object AnnotationExtensions { internal object AnnotationExtensions {
/** /**
* Recursively find a given annotation on a class. * Search for an annotation recursively.
* *
* @param targetAnnotation The annotation to find. * @param targetAnnotationClass The annotation class to search for.
* @return The annotation. * @param searchedClasses A set of annotations that have already been searched.
* @return The annotation if found, otherwise null.
*/ */
fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: KClass<T>): T? {
fun <T : Annotation> Class<*>.findAnnotationRecursively( fun <T : Annotation> Class<*>.findAnnotationRecursively(
targetAnnotation: Class<T>, traversed: MutableSet<Annotation> targetAnnotationClass: Class<T>,
searchedClasses: HashSet<Annotation> = hashSetOf(),
): T? { ): T? {
val found = this.annotations.firstOrNull { it.annotationClass.java.name == targetAnnotation.name } annotations.forEach { annotation ->
// Terminate if the annotation is already searched.
if (annotation in searchedClasses) return@forEach
searchedClasses.add(annotation)
@Suppress("UNCHECKED_CAST") if (found != null) return found as T // Terminate if the annotation is found.
if (targetAnnotationClass == annotation.annotationClass.java) return annotation as T
for (annotation in this.annotations) { return annotation.annotationClass.java.findAnnotationRecursively(
if (traversed.contains(annotation)) continue targetAnnotationClass,
traversed.add(annotation) searchedClasses,
) ?: return@forEach
}
return (annotation.annotationClass.java.findAnnotationRecursively(targetAnnotation, traversed)) // Search the super class.
?: continue superclass?.findAnnotationRecursively(
targetAnnotationClass,
searchedClasses,
)?.let { return it }
// Search the interfaces.
interfaces.forEach { superClass ->
return superClass.findAnnotationRecursively(
targetAnnotationClass,
searchedClasses,
) ?: return@forEach
} }
return null return null
} }
return this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf()) /**
} * Search for an annotation recursively.
*
* First the annotations, then the annotated classes super class and then it's interfaces
* are searched for the annotation recursively.
*
* @param targetAnnotation The annotation to search for.
* @return The annotation if found, otherwise null.
*/
fun <T : Annotation> KClass<*>.findAnnotationRecursively(targetAnnotation: KClass<T>) =
java.findAnnotationRecursively(targetAnnotation.java)
} }

View File

@ -11,13 +11,6 @@ import com.android.tools.smali.dexlib2.AccessFlags
*/ */
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index) fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
/**
* Perform a bitwise OR operation between two [AccessFlags].
*
* @param other The other [AccessFlags] to perform the operation with.
*/
infix fun AccessFlags.or(other: AccessFlags) = value or other.value
/** /**
* Perform a bitwise OR operation between an [AccessFlags] and an [Int]. * Perform a bitwise OR operation between an [AccessFlags] and an [Int].
* *
@ -25,6 +18,13 @@ infix fun AccessFlags.or(other: AccessFlags) = value or other.value
*/ */
infix fun Int.or(other: AccessFlags) = this or other.value infix fun Int.or(other: AccessFlags) = this or other.value
/**
* Perform a bitwise OR operation between two [AccessFlags].
*
* @param other The other [AccessFlags] to perform the operation with.
*/
infix fun AccessFlags.or(other: AccessFlags) = value or other.value
/** /**
* Perform a bitwise OR operation between an [Int] and an [AccessFlags]. * Perform a bitwise OR operation between an [Int] and an [AccessFlags].
* *

View File

@ -12,7 +12,6 @@ import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.Instruction
object InstructionExtensions { object InstructionExtensions {
/** /**
* Add instructions to a method at the given index. * Add instructions to a method at the given index.
* *
@ -21,7 +20,7 @@ object InstructionExtensions {
*/ */
fun MutableMethodImplementation.addInstructions( fun MutableMethodImplementation.addInstructions(
index: Int, index: Int,
instructions: List<BuilderInstruction> instructions: List<BuilderInstruction>,
) = instructions.asReversed().forEach { addInstruction(index, it) } ) = instructions.asReversed().forEach { addInstruction(index, it) }
/** /**
@ -39,7 +38,10 @@ object InstructionExtensions {
* @param index The index to remove the instructions at. * @param index The index to remove the instructions at.
* @param count The amount of instructions to remove. * @param count The amount of instructions to remove.
*/ */
fun MutableMethodImplementation.removeInstructions(index: Int, count: Int) = repeat(count) { fun MutableMethodImplementation.removeInstructions(
index: Int,
count: Int,
) = repeat(count) {
removeInstruction(index) removeInstruction(index)
} }
@ -57,7 +59,10 @@ object InstructionExtensions {
* @param index The index to replace the instructions at. * @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with. * @param instructions The instructions to replace the instructions with.
*/ */
fun MutableMethodImplementation.replaceInstructions(index: Int, instructions: List<BuilderInstruction>) { fun MutableMethodImplementation.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) {
// Remove the instructions at the given index. // Remove the instructions at the given index.
removeInstructions(index, instructions.size) removeInstructions(index, instructions.size)
@ -71,16 +76,17 @@ object InstructionExtensions {
* @param index The index to add the instruction at. * @param index The index to add the instruction at.
* @param instruction The instruction to add. * @param instruction The instruction to add.
*/ */
fun MutableMethod.addInstruction(index: Int, instruction: BuilderInstruction) = fun MutableMethod.addInstruction(
implementation!!.addInstruction(index, instruction) index: Int,
instruction: BuilderInstruction,
) = implementation!!.addInstruction(index, instruction)
/** /**
* Add an instruction to a method. * Add an instruction to a method.
* *
* @param instruction The instructions to add. * @param instruction The instructions to add.
*/ */
fun MutableMethod.addInstruction(instruction: BuilderInstruction) = fun MutableMethod.addInstruction(instruction: BuilderInstruction) = implementation!!.addInstruction(instruction)
implementation!!.addInstruction(instruction)
/** /**
* Add an instruction to a method at the given index. * Add an instruction to a method at the given index.
@ -88,17 +94,17 @@ object InstructionExtensions {
* @param index The index to add the instruction at. * @param index The index to add the instruction at.
* @param smaliInstructions The instruction to add. * @param smaliInstructions The instruction to add.
*/ */
fun MutableMethod.addInstruction(index: Int, smaliInstructions: String) = fun MutableMethod.addInstruction(
implementation!!.addInstruction(index, smaliInstructions.toInstruction(this)) index: Int,
smaliInstructions: String,
) = implementation!!.addInstruction(index, smaliInstructions.toInstruction(this))
/** /**
* Add an instruction to a method. * Add an instruction to a method.
* *
* @param smaliInstructions The instruction to add. * @param smaliInstructions The instruction to add.
*/ */
fun MutableMethod.addInstruction(smaliInstructions: String) = fun MutableMethod.addInstruction(smaliInstructions: String) = implementation!!.addInstruction(smaliInstructions.toInstruction(this))
implementation!!.addInstruction(smaliInstructions.toInstruction(this))
/** /**
* Add instructions to a method at the given index. * Add instructions to a method at the given index.
@ -106,32 +112,34 @@ object InstructionExtensions {
* @param index The index to add the instructions at. * @param index The index to add the instructions at.
* @param instructions The instructions to add. * @param instructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(index: Int, instructions: List<BuilderInstruction>) = fun MutableMethod.addInstructions(
implementation!!.addInstructions(index, instructions) index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.addInstructions(index, instructions)
/** /**
* Add instructions to a method. * Add instructions to a method.
* *
* @param instructions The instructions to add. * @param instructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) = fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) = implementation!!.addInstructions(instructions)
implementation!!.addInstructions(instructions)
/** /**
* Add instructions to a method. * Add instructions to a method.
* *
* @param smaliInstructions The instructions to add. * @param smaliInstructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(index: Int, smaliInstructions: String) = fun MutableMethod.addInstructions(
implementation!!.addInstructions(index, smaliInstructions.toInstructions(this)) index: Int,
smaliInstructions: String,
) = implementation!!.addInstructions(index, smaliInstructions.toInstructions(this))
/** /**
* Add instructions to a method. * Add instructions to a method.
* *
* @param smaliInstructions The instructions to add. * @param smaliInstructions The instructions to add.
*/ */
fun MutableMethod.addInstructions(smaliInstructions: String) = fun MutableMethod.addInstructions(smaliInstructions: String) = implementation!!.addInstructions(smaliInstructions.toInstructions(this))
implementation!!.addInstructions(smaliInstructions.toInstructions(this))
/** /**
* Add instructions to a method at the given index. * Add instructions to a method at the given index.
@ -144,10 +152,11 @@ object InstructionExtensions {
fun MutableMethod.addInstructionsWithLabels( fun MutableMethod.addInstructionsWithLabels(
index: Int, index: Int,
smaliInstructions: String, smaliInstructions: String,
vararg externalLabels: ExternalLabel vararg externalLabels: ExternalLabel,
) { ) {
// Create reference dummy instructions for the instructions. // Create reference dummy instructions for the instructions.
val nopSmali = StringBuilder(smaliInstructions).also { builder -> val nopSmali =
StringBuilder(smaliInstructions).also { builder ->
externalLabels.forEach { (name, _) -> externalLabels.forEach { (name, _) ->
builder.append("\n:$name\nnop") builder.append("\n:$name\nnop")
} }
@ -159,7 +168,7 @@ object InstructionExtensions {
// Add the compiled list of instructions to the method. // Add the compiled list of instructions to the method.
addInstructions( addInstructions(
index, index,
compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size) compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size),
) )
implementation!!.apply { implementation!!.apply {
@ -174,22 +183,24 @@ object InstructionExtensions {
*/ */
fun Instruction.makeNewLabel() { fun Instruction.makeNewLabel() {
fun replaceOffset( fun replaceOffset(
i: BuilderOffsetInstruction, label: Label i: BuilderOffsetInstruction,
label: Label,
): BuilderOffsetInstruction { ): BuilderOffsetInstruction {
return when (i) { return when (i) {
is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label) is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label)
is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label) is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label)
is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label) is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label)
is BuilderInstruction22t -> BuilderInstruction22t( is BuilderInstruction22t ->
BuilderInstruction22t(
i.opcode, i.opcode,
i.registerA, i.registerA,
i.registerB, i.registerB,
label label,
) )
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label) is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label) is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
else -> throw IllegalStateException( else -> throw IllegalStateException(
"A non-offset instruction was given, this should never happen!" "A non-offset instruction was given, this should never happen!",
) )
} }
} }
@ -198,8 +209,10 @@ object InstructionExtensions {
val label = newLabelForIndex(this@apply.instructions.indexOf(this)) val label = newLabelForIndex(this@apply.instructions.indexOf(this))
// Create the final instruction with the new label. // Create the final instruction with the new label.
val newInstruction = replaceOffset( val newInstruction =
compiledInstruction, label replaceOffset(
compiledInstruction,
label,
) )
// Replace the instruction pointing to the dummy label // Replace the instruction pointing to the dummy label
@ -233,8 +246,7 @@ object InstructionExtensions {
* *
* @param index The index to remove the instruction at. * @param index The index to remove the instruction at.
*/ */
fun MutableMethod.removeInstruction(index: Int) = fun MutableMethod.removeInstruction(index: Int) = implementation!!.removeInstruction(index)
implementation!!.removeInstruction(index)
/** /**
* Remove instructions at the given index. * Remove instructions at the given index.
@ -242,16 +254,17 @@ object InstructionExtensions {
* @param index The index to remove the instructions at. * @param index The index to remove the instructions at.
* @param count The amount of instructions to remove. * @param count The amount of instructions to remove.
*/ */
fun MutableMethod.removeInstructions(index: Int, count: Int) = fun MutableMethod.removeInstructions(
implementation!!.removeInstructions(index, count) index: Int,
count: Int,
) = implementation!!.removeInstructions(index, count)
/** /**
* Remove instructions at the given index. * Remove instructions at the given index.
* *
* @param count The amount of instructions to remove. * @param count The amount of instructions to remove.
*/ */
fun MutableMethod.removeInstructions(count: Int) = fun MutableMethod.removeInstructions(count: Int) = implementation!!.removeInstructions(count)
implementation!!.removeInstructions(count)
/** /**
* Replace an instruction at the given index. * Replace an instruction at the given index.
@ -259,8 +272,10 @@ object InstructionExtensions {
* @param index The index to replace the instruction at. * @param index The index to replace the instruction at.
* @param instruction The instruction to replace the instruction with. * @param instruction The instruction to replace the instruction with.
*/ */
fun MutableMethod.replaceInstruction(index: Int, instruction: BuilderInstruction) = fun MutableMethod.replaceInstruction(
implementation!!.replaceInstruction(index, instruction) index: Int,
instruction: BuilderInstruction,
) = implementation!!.replaceInstruction(index, instruction)
/** /**
* Replace an instruction at the given index. * Replace an instruction at the given index.
@ -268,8 +283,10 @@ object InstructionExtensions {
* @param index The index to replace the instruction at. * @param index The index to replace the instruction at.
* @param smaliInstruction The smali instruction to replace the instruction with. * @param smaliInstruction The smali instruction to replace the instruction with.
*/ */
fun MutableMethod.replaceInstruction(index: Int, smaliInstruction: String) = fun MutableMethod.replaceInstruction(
implementation!!.replaceInstruction(index, smaliInstruction.toInstruction(this)) index: Int,
smaliInstruction: String,
) = implementation!!.replaceInstruction(index, smaliInstruction.toInstruction(this))
/** /**
* Replace instructions at the given index. * Replace instructions at the given index.
@ -277,8 +294,10 @@ object InstructionExtensions {
* @param index The index to replace the instructions at. * @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with. * @param instructions The instructions to replace the instructions with.
*/ */
fun MutableMethod.replaceInstructions(index: Int, instructions: List<BuilderInstruction>) = fun MutableMethod.replaceInstructions(
implementation!!.replaceInstructions(index, instructions) index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.replaceInstructions(index, instructions)
/** /**
* Replace instructions at the given index. * Replace instructions at the given index.
@ -286,8 +305,10 @@ object InstructionExtensions {
* @param index The index to replace the instructions at. * @param index The index to replace the instructions at.
* @param smaliInstructions The smali instructions to replace the instructions with. * @param smaliInstructions The smali instructions to replace the instructions with.
*/ */
fun MutableMethod.replaceInstructions(index: Int, smaliInstructions: String) = fun MutableMethod.replaceInstructions(
implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this)) index: Int,
smaliInstructions: String,
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
/** /**
* Get an instruction at the given index. * Get an instruction at the given index.

View File

@ -1,14 +1,16 @@
package app.revanced.patcher.extensions package app.revanced.patcher.extensions
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.MethodFingerprint import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
object MethodFingerprintExtensions { object MethodFingerprintExtensions {
// TODO: Make this a property.
/** /**
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint]. * The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
*/ */
@Deprecated(
message = "Use the property instead.",
replaceWith = ReplaceWith("this.fuzzyPatternScanMethod"),
)
val MethodFingerprint.fuzzyPatternScanMethod val MethodFingerprint.fuzzyPatternScanMethod
get() = javaClass.findAnnotationRecursively(FuzzyPatternScanMethod::class) get() = this.fuzzyPatternScanMethod
} }

View File

@ -20,7 +20,7 @@ internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by muta
*/ */
fun add( fun add(
key: String, key: String,
methodClassPair: MethodClassPair methodClassPair: MethodClassPair,
) { ) {
getOrPut(key) { MethodClassList() }.add(methodClassPair) getOrPut(key) { MethodClassList() }.add(methodClassPair)
} }
@ -73,13 +73,14 @@ internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by muta
append(accessFlagsReturnKey) append(accessFlagsReturnKey)
appendParameters(method.parameterTypes) appendParameters(method.parameterTypes)
}, },
methodClassPair methodClassPair,
) )
// Add strings contained in the method as the key. // Add strings contained in the method as the key.
method.implementation?.instructions?.forEach instructions@{ instruction -> method.implementation?.instructions?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
return@instructions return@instructions
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string val string = ((instruction as ReferenceInstruction).reference as StringReference).string
@ -93,7 +94,7 @@ internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by muta
} }
/** /**
* Clears the internal lookup maps created in [initializeLookupMaps] * Clears the internal lookup maps created in [initializeLookupMaps].
*/ */
internal fun clearLookupMaps() { internal fun clearLookupMaps() {
methods.clear() methods.clear()
@ -120,6 +121,5 @@ internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by muta
append(parameter.first()) append(parameter.first())
} }
} }
} }
} }

View File

@ -1,7 +1,7 @@
package app.revanced.patcher.fingerprint package app.revanced.patcher.fingerprint
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.fingerprint.LookupMap.Maps.appendParameters import app.revanced.patcher.fingerprint.LookupMap.Maps.appendParameters
import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps
import app.revanced.patcher.fingerprint.LookupMap.Maps.methodSignatureLookupMap import app.revanced.patcher.fingerprint.LookupMap.Maps.methodSignatureLookupMap
@ -28,13 +28,14 @@ import com.android.tools.smali.dexlib2.iface.reference.StringReference
* @param strings A list of the method's strings compared each using [String.contains]. * @param strings A list of the method's strings compared each using [String.contains].
* @param customFingerprint A custom condition for this fingerprint. * @param customFingerprint A custom condition for this fingerprint.
*/ */
@Suppress("MemberVisibilityCanBePrivate")
abstract class MethodFingerprint( abstract class MethodFingerprint(
internal val returnType: String? = null, internal val returnType: String? = null,
internal val accessFlags: Int? = null, internal val accessFlags: Int? = null,
internal val parameters: Iterable<String>? = null, internal val parameters: Iterable<String>? = null,
internal val opcodes: Iterable<Opcode?>? = null, internal val opcodes: Iterable<Opcode?>? = null,
internal val strings: Iterable<String>? = null, internal val strings: Iterable<String>? = null,
internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null,
) { ) {
/** /**
* The result of the [MethodFingerprint]. * The result of the [MethodFingerprint].
@ -42,6 +43,13 @@ abstract class MethodFingerprint(
var result: MethodFingerprintResult? = null var result: MethodFingerprintResult? = null
private set private set
/**
* The [FuzzyPatternScanMethod] annotation of the [MethodFingerprint].
*
* If the annotation is not present, this property is null.
*/
val fuzzyPatternScanMethod = this::class.findAnnotationRecursively(FuzzyPatternScanMethod::class)
/** /**
* Resolve a [MethodFingerprint] using the lookup map built by [initializeLookupMaps]. * Resolve a [MethodFingerprint] using the lookup map built by [initializeLookupMaps].
* *
@ -86,7 +94,8 @@ abstract class MethodFingerprint(
} }
} }
val key = buildString { val key =
buildString {
append(accessFlags) append(accessFlags)
append(returnTypeValue.first()) append(returnTypeValue.first())
if (parameters != null) appendParameters(parameters) if (parameters != null) appendParameters(parameters)
@ -107,7 +116,11 @@ abstract class MethodFingerprint(
} }
val methodsWithSameStrings = methodStringsLookup() val methodsWithSameStrings = methodStringsLookup()
if (methodsWithSameStrings != null) if (resolveUsingMethodClassPair(methodsWithSameStrings)) return true if (methodsWithSameStrings != null) {
if (resolveUsingMethodClassPair(methodsWithSameStrings)) {
return true
}
}
// No strings declared or none matched (partial matches are allowed). // No strings declared or none matched (partial matches are allowed).
// Use signature matching. // Use signature matching.
@ -121,10 +134,14 @@ abstract class MethodFingerprint(
* @param context The [BytecodeContext] to host proxies. * @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise. * @return True if the resolution was successful, false otherwise.
*/ */
fun resolve(context: BytecodeContext, forClass: ClassDef): Boolean { fun resolve(
context: BytecodeContext,
forClass: ClassDef,
): Boolean {
for (method in forClass.methods) for (method in forClass.methods)
if (resolve(context, method, forClass)) if (resolve(context, method, forClass)) {
return true return true
}
return false return false
} }
@ -136,20 +153,26 @@ abstract class MethodFingerprint(
* @param context The [BytecodeContext] to host proxies. * @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
*/ */
fun resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean { fun resolve(
context: BytecodeContext,
method: Method,
forClass: ClassDef,
): Boolean {
val methodFingerprint = this val methodFingerprint = this
if (methodFingerprint.result != null) return true if (methodFingerprint.result != null) return true
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) {
return false return false
}
if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) {
return false return false
}
fun parametersEqual( fun parametersEqual(
parameters1: Iterable<CharSequence>, parameters2: Iterable<CharSequence> parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): Boolean { ): Boolean {
if (parameters1.count() != parameters2.count()) return false if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator() val iterator1 = parameters1.iterator()
@ -159,15 +182,19 @@ abstract class MethodFingerprint(
return true return true
} }
if (methodFingerprint.parameters != null && !parametersEqual( if (methodFingerprint.parameters != null &&
!parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters() methodFingerprint.parameters, // TODO: parseParameters()
method.parameterTypes method.parameterTypes,
) )
) return false ) {
return false
}
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass)) if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass)) {
return false return false
}
val stringsScanResult: StringsScanResult? = val stringsScanResult: StringsScanResult? =
if (methodFingerprint.strings != null) { if (methodFingerprint.strings != null) {
@ -181,7 +208,9 @@ abstract class MethodFingerprint(
if ( if (
instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO instruction.opcode != Opcode.CONST_STRING_JUMBO
) return@forEachIndexed ) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains) val index = stringsList.indexOfFirst(string::contains)
@ -192,15 +221,19 @@ abstract class MethodFingerprint(
} }
if (stringsList.isNotEmpty()) return false if (stringsList.isNotEmpty()) return false
} },
) )
} else null } else {
null
}
val patternScanResult = if (methodFingerprint.opcodes != null) { val patternScanResult =
if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false method.implementation?.instructions ?: return false
fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings( fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction> pattern: Iterable<Opcode?>,
instructions: Iterable<Instruction>,
) = buildList { ) = buildList {
for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) { for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode val originalOpcode = instructions.elementAt(instructionIndex).opcode
@ -213,14 +246,14 @@ abstract class MethodFingerprint(
originalOpcode, originalOpcode,
patternOpcode, patternOpcode,
instructionIndex, instructionIndex,
patternIndex patternIndex,
) ),
) )
} }
} }
fun Method.patternScan( fun Method.patternScan(
fingerprint: MethodFingerprint fingerprint: MethodFingerprint,
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { ): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0 val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
@ -253,7 +286,7 @@ abstract class MethodFingerprint(
val result = val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult( MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index, index,
index + patternIndex index + patternIndex,
) )
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.newWarnings(pattern, instructions) result.warnings = result.newWarnings(pattern, instructions)
@ -266,16 +299,19 @@ abstract class MethodFingerprint(
} }
method.patternScan(methodFingerprint) ?: return false method.patternScan(methodFingerprint) ?: return false
} else null } else {
null
}
methodFingerprint.result = MethodFingerprintResult( methodFingerprint.result =
MethodFingerprintResult(
method, method,
forClass, forClass,
MethodFingerprintResult.MethodFingerprintScanResult( MethodFingerprintResult.MethodFingerprintScanResult(
patternScanResult, patternScanResult,
stringsScanResult stringsScanResult,
), ),
context context,
) )
return true return true
@ -309,12 +345,13 @@ abstract class MethodFingerprint(
* @param context The [BytecodeContext] to host proxies. * @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise. * @return True if the resolution was successful, false otherwise.
*/ */
fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) = fun Iterable<MethodFingerprint>.resolve(
forEach { fingerprint -> context: BytecodeContext,
classes: Iterable<ClassDef>,
) = forEach { fingerprint ->
for (classDef in classes) { for (classDef in classes) {
if (fingerprint.resolve(context, classDef)) break if (fingerprint.resolve(context, classDef)) break
} }
} }
} }
} }

View File

@ -20,7 +20,7 @@ class MethodFingerprintResult(
val method: Method, val method: Method,
val classDef: ClassDef, val classDef: ClassDef,
val scanResult: MethodFingerprintScanResult, val scanResult: MethodFingerprintScanResult,
internal val context: BytecodeContext internal val context: BytecodeContext,
) { ) {
/** /**
* Returns a mutable clone of [classDef] * Returns a mutable clone of [classDef]
@ -50,7 +50,7 @@ class MethodFingerprintResult(
*/ */
class MethodFingerprintScanResult( class MethodFingerprintScanResult(
val patternScanResult: PatternScanResult?, val patternScanResult: PatternScanResult?,
val stringsScanResult: StringsScanResult? val stringsScanResult: StringsScanResult?,
) { ) {
/** /**
* The result of scanning strings on the [MethodFingerprint]. * The result of scanning strings on the [MethodFingerprint].
@ -74,7 +74,7 @@ class MethodFingerprintResult(
class PatternScanResult( class PatternScanResult(
val startIndex: Int, val startIndex: Int,
val endIndex: Int, val endIndex: Int,
var warnings: List<Warning>? = null var warnings: List<Warning>? = null,
) { ) {
/** /**
* Represents warnings of the pattern scan. * Represents warnings of the pattern scan.

View File

@ -8,5 +8,5 @@ import app.revanced.patcher.fingerprint.MethodFingerprint
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class FuzzyPatternScanMethod( annotation class FuzzyPatternScanMethod(
val threshold: Int = 1 val threshold: Int = 1,
) )

View File

@ -1,13 +1,61 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass
import app.revanced.patcher.Patcher
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.fingerprint.MethodFingerprint import app.revanced.patcher.fingerprint.MethodFingerprint
import java.io.Closeable
/** /**
* A ReVanced [Patch] that works on [BytecodeContext]. * A ReVanced [Patch] that accesses a [BytecodeContext].
* *
* @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed. * If an implementation of [Patch] also implements [Closeable]
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
*/ */
abstract class BytecodePatch( @Suppress("unused")
internal val fingerprints : Set<MethodFingerprint> = emptySet(), abstract class BytecodePatch : Patch<BytecodeContext> {
) : Patch<BytecodeContext>() /**
* The fingerprints to resolve before executing the patch.
*/
internal val fingerprints: Set<MethodFingerprint>
/**
* Create a new [BytecodePatch].
*
* @param fingerprints The fingerprints to resolve before executing the patch.
*/
constructor(fingerprints: Set<MethodFingerprint> = emptySet()) {
this.fingerprints = fingerprints
}
/**
* Create a new [BytecodePatch].
*
* @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies Other patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
*/
constructor(
name: String? = null,
description: String? = null,
compatiblePackages: Set<CompatiblePackage>? = null,
dependencies: Set<PatchClass>? = null,
use: Boolean = true,
requiresIntegrations: Boolean = false,
fingerprints: Set<MethodFingerprint> = emptySet(),
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations) {
this.fingerprints = fingerprints
}
/**
* Create a new [BytecodePatch].
*/
@Deprecated(
"Use the constructor with fingerprints instead.",
ReplaceWith("BytecodePatch(emptySet())"),
)
constructor() : this(emptySet())
}

View File

@ -5,12 +5,13 @@ package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass import app.revanced.patcher.PatchClass
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.data.Context import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.patch.options.PatchOptions import app.revanced.patcher.patch.options.PatchOptions
import java.io.Closeable import java.io.Closeable
import kotlin.reflect.full.findAnnotation
/** /**
* A ReVanced patch. * A ReVanced patch.
*
* If an implementation of [Patch] also implements [Closeable] * If an implementation of [Patch] also implements [Closeable]
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher]. * it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
* *
@ -47,7 +48,6 @@ sealed class Patch<out T : Context<*>> {
var use = true var use = true
private set private set
// TODO: Remove this property, once integrations are coupled with patches. // TODO: Remove this property, once integrations are coupled with patches.
/** /**
* Weather or not the patch requires integrations. * Weather or not the patch requires integrations.
@ -55,24 +55,41 @@ sealed class Patch<out T : Context<*>> {
var requiresIntegrations = false var requiresIntegrations = false
private set private set
constructor(
name: String?,
description: String?,
compatiblePackages: Set<CompatiblePackage>?,
dependencies: Set<PatchClass>?,
use: Boolean,
requiresIntegrations: Boolean,
) {
this.name = name
this.description = description
this.compatiblePackages = compatiblePackages
this.dependencies = dependencies
this.use = use
this.requiresIntegrations = requiresIntegrations
}
constructor() {
this::class.findAnnotationRecursively(app.revanced.patcher.patch.annotation.Patch::class)?.let { annotation ->
this.name = annotation.name.ifEmpty { null }
this.description = annotation.description.ifEmpty { null }
this.compatiblePackages =
annotation.compatiblePackages
.map { CompatiblePackage(it.name, it.versions.toSet().ifEmpty { null }) }
.toSet().ifEmpty { null }
this.dependencies = annotation.dependencies.toSet().ifEmpty { null }
this.use = annotation.use
this.requiresIntegrations = annotation.requiresIntegrations
}
}
/** /**
* The options of the patch associated by the options key. * The options of the patch associated by the options key.
*/ */
val options = PatchOptions() val options = PatchOptions()
init {
this::class.findAnnotation<app.revanced.patcher.patch.annotation.Patch>()?.let { annotation ->
name = annotation.name.ifEmpty { null }
description = annotation.description.ifEmpty { null }
compatiblePackages = annotation.compatiblePackages
.map { CompatiblePackage(it.name, it.versions.toSet().ifEmpty { null }) }
.toSet().ifEmpty { null }
dependencies = annotation.dependencies.toSet().ifEmpty { null }
use = annotation.use
requiresIntegrations = annotation.requiresIntegrations
}
}
/** /**
* The execution function of the patch. * The execution function of the patch.
* *

View File

@ -1,8 +1,38 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass
import app.revanced.patcher.Patcher
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.data.ResourceContext
import java.io.Closeable
/** /**
* A ReVanced [Patch] that works on [ResourceContext]. * A ReVanced [Patch] that accesses a [ResourceContext].
*
* If an implementation of [Patch] also implements [Closeable]
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
*/ */
abstract class ResourcePatch : Patch<ResourceContext>() abstract class ResourcePatch : Patch<ResourceContext> {
/**
* Create a new [ResourcePatch].
*/
constructor()
/**
* Create a new [ResourcePatch].
*
* @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies Other patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
*/
constructor(
name: String? = null,
description: String? = null,
compatiblePackages: Set<CompatiblePackage>? = null,
dependencies: Set<PatchClass>? = null,
use: Boolean = true,
requiresIntegrations: Boolean = false,
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
}

View File

@ -25,7 +25,7 @@ open class PatchOption<T>(
val description: String?, val description: String?,
val required: Boolean, val required: Boolean,
val valueType: String, val valueType: String,
val validator: PatchOption<T>.(T?) -> Boolean val validator: PatchOption<T>.(T?) -> Boolean,
) { ) {
/** /**
* The value of the [PatchOption]. * The value of the [PatchOption].
@ -45,6 +45,7 @@ open class PatchOption<T>(
uncheckedValue = value uncheckedValue = value
} }
/** /**
* Get the value of the [PatchOption]. * Get the value of the [PatchOption].
* *
@ -81,9 +82,16 @@ open class PatchOption<T>(
override fun toString() = value.toString() override fun toString() = value.toString()
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value operator fun getValue(
thisRef: Any?,
property: KProperty<*>,
) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { operator fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T?,
) {
this.value = value this.value = value
} }
@ -111,9 +119,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<String>.(String?) -> Boolean = { true } validator: PatchOption<String>.(String?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "String", validator key,
default,
values,
title,
description,
required,
"String",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -138,9 +153,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Int?>.(Int?) -> Boolean = { true } validator: PatchOption<Int?>.(Int?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "Int", validator key,
default,
values,
title,
description,
required,
"Int",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -165,7 +187,7 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Boolean?>.(Boolean?) -> Boolean = { true } validator: PatchOption<Boolean?>.(Boolean?) -> Boolean = { true },
) = PatchOption(key, default, values, title, description, required, "Boolean", validator).also { ) = PatchOption(key, default, values, title, description, required, "Boolean", validator).also {
registerOption(it) registerOption(it)
} }
@ -192,9 +214,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Float?>.(Float?) -> Boolean = { true } validator: PatchOption<Float?>.(Float?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "Float", validator key,
default,
values,
title,
description,
required,
"Float",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -219,9 +248,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Long?>.(Long?) -> Boolean = { true } validator: PatchOption<Long?>.(Long?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "Long", validator key,
default,
values,
title,
description,
required,
"Long",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -246,9 +282,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<String>?>.(Array<String>?) -> Boolean = { true } validator: PatchOption<Array<String>?>.(Array<String>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "StringArray", validator key,
default,
values,
title,
description,
required,
"StringArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -273,9 +316,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Int>?>.(Array<Int>?) -> Boolean = { true } validator: PatchOption<Array<Int>?>.(Array<Int>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "IntArray", validator key,
default,
values,
title,
description,
required,
"IntArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -300,9 +350,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Boolean>?>.(Array<Boolean>?) -> Boolean = { true } validator: PatchOption<Array<Boolean>?>.(Array<Boolean>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "BooleanArray", validator key,
default,
values,
title,
description,
required,
"BooleanArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -327,9 +384,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Float>?>.(Array<Float>?) -> Boolean = { true } validator: PatchOption<Array<Float>?>.(Array<Float>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "FloatArray", validator key,
default,
values,
title,
description,
required,
"FloatArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
/** /**
@ -354,9 +418,16 @@ open class PatchOption<T>(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: PatchOption<Array<Long>?>.(Array<Long>?) -> Boolean = { true } validator: PatchOption<Array<Long>?>.(Array<Long>?) -> Boolean = { true },
) = PatchOption( ) = PatchOption(
key, default, values, title, description, required, "LongArray", validator key,
default,
values,
title,
description,
required,
"LongArray",
validator,
).also { registerOption(it) } ).also { registerOption(it) }
private fun <P : Patch<*>> P.registerOption(option: PatchOption<*>) = option.also { options.register(it) } private fun <P : Patch<*>> P.registerOption(option: PatchOption<*>) = option.also { options.register(it) }

View File

@ -36,6 +36,6 @@ sealed class PatchOptionException(errorMessage: String) : Exception(errorMessage
* *
* @param key The key of the [PatchOption]. * @param key The key of the [PatchOption].
*/ */
class PatchOptionNotFoundException(key: String) class PatchOptionNotFoundException(key: String) :
: PatchOptionException("No option with key $key") PatchOptionException("No option with key $key")
} }

View File

@ -1,13 +1,12 @@
package app.revanced.patcher.patch.options package app.revanced.patcher.patch.options
/** /**
* A map of [PatchOption]s associated by their keys. * A map of [PatchOption]s associated by their keys.
* *
* @param options The [PatchOption]s to initialize with. * @param options The [PatchOption]s to initialize with.
*/ */
class PatchOptions internal constructor( class PatchOptions internal constructor(
private val options: MutableMap<String, PatchOption<*>> = mutableMapOf() private val options: MutableMap<String, PatchOption<*>> = mutableMapOf(),
) : MutableMap<String, PatchOption<*>> by options { ) : MutableMap<String, PatchOption<*>> by options {
/** /**
* Register a [PatchOption]. Acts like [MutableMap.put]. * Register a [PatchOption]. Acts like [MutableMap.put].
@ -23,7 +22,10 @@ class PatchOptions internal constructor(
* @param value The value. * @param value The value.
* @throws PatchOptionException.PatchOptionNotFoundException If the option does not exist. * @throws PatchOptionException.PatchOptionNotFoundException If the option does not exist.
*/ */
operator fun <T : Any> set(key: String, value: T?) { operator fun <T : Any> set(
key: String,
value: T?,
) {
val option = this[key] val option = this[key]
try { try {
@ -40,6 +42,5 @@ class PatchOptions internal constructor(
/** /**
* Get an option. * Get an option.
*/ */
override operator fun get(key: String) = override operator fun get(key: String) = options[key] ?: throw PatchOptionException.PatchOptionNotFoundException(key)
options[key] ?: throw PatchOptionException.PatchOptionNotFoundException(key)
} }

View File

@ -34,7 +34,10 @@ internal object ClassMerger {
* @param context The context to traverse the class hierarchy in. * @param context The context to traverse the class hierarchy in.
* @return The merged class or the original class if no merge was needed. * @return The merged class or the original class if no merge was needed.
*/ */
fun ClassDef.merge(otherClass: ClassDef, context: BytecodeContext) = this fun ClassDef.merge(
otherClass: ClassDef,
context: BytecodeContext,
) = this
// .fixFieldAccess(otherClass) // .fixFieldAccess(otherClass)
// .fixMethodAccess(otherClass) // .fixMethodAccess(otherClass)
.addMissingFields(otherClass) .addMissingFields(otherClass)
@ -47,7 +50,8 @@ internal object ClassMerger {
* @param fromClass The class to add missing methods from. * @param fromClass The class to add missing methods from.
*/ */
private fun ClassDef.addMissingMethods(fromClass: ClassDef): ClassDef { private fun ClassDef.addMissingMethods(fromClass: ClassDef): ClassDef {
val missingMethods = fromClass.methods.let { fromMethods -> val missingMethods =
fromClass.methods.let { fromMethods ->
methods.filterNot { method -> methods.filterNot { method ->
fromMethods.any { fromMethod -> fromMethods.any { fromMethod ->
MethodUtil.methodSignaturesMatch(fromMethod, method) MethodUtil.methodSignaturesMatch(fromMethod, method)
@ -70,7 +74,8 @@ internal object ClassMerger {
* @param fromClass The class to add missing fields from. * @param fromClass The class to add missing fields from.
*/ */
private fun ClassDef.addMissingFields(fromClass: ClassDef): ClassDef { private fun ClassDef.addMissingFields(fromClass: ClassDef): ClassDef {
val missingFields = fields.filterNotAny(fromClass.fields) { field, fromField -> val missingFields =
fields.filterNotAny(fromClass.fields) { field, fromField ->
fromField.name == field.name fromField.name == field.name
} }
@ -88,8 +93,10 @@ internal object ClassMerger {
* @param reference The class to check the [AccessFlags] of. * @param reference The class to check the [AccessFlags] of.
* @param context The context to traverse the class hierarchy in. * @param context The context to traverse the class hierarchy in.
*/ */
private fun ClassDef.publicize(reference: ClassDef, context: BytecodeContext) = private fun ClassDef.publicize(
if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) reference: ClassDef,
context: BytecodeContext,
) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
this.asMutableClass().apply { this.asMutableClass().apply {
context.traverseClassHierarchy(this) { context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy if (accessFlags.isPublic()) return@traverseClassHierarchy
@ -99,7 +106,9 @@ internal object ClassMerger {
accessFlags = accessFlags.toPublic() accessFlags = accessFlags.toPublic()
} }
} }
else this } else {
this
}
/** /**
* Publicize fields if they are public in [reference]. * Publicize fields if they are public in [reference].
@ -107,7 +116,8 @@ internal object ClassMerger {
* @param reference The class to check the [AccessFlags] of the fields in. * @param reference The class to check the [AccessFlags] of the fields in.
*/ */
private fun ClassDef.fixFieldAccess(reference: ClassDef): ClassDef { private fun ClassDef.fixFieldAccess(reference: ClassDef): ClassDef {
val brokenFields = fields.filterAny(reference.fields) { field, referenceField -> val brokenFields =
fields.filterAny(reference.fields) { field, referenceField ->
if (field.name != referenceField.name) return@filterAny false if (field.name != referenceField.name) return@filterAny false
referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic() referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic()
@ -135,7 +145,8 @@ internal object ClassMerger {
* @param reference The class to check the [AccessFlags] of the methods in. * @param reference The class to check the [AccessFlags] of the methods in.
*/ */
private fun ClassDef.fixMethodAccess(reference: ClassDef): ClassDef { private fun ClassDef.fixMethodAccess(reference: ClassDef): ClassDef {
val brokenMethods = methods.filterAny(reference.methods) { method, referenceMethod -> val brokenMethods =
methods.filterAny(reference.methods) { method, referenceMethod ->
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false
referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic() referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic()
@ -164,7 +175,10 @@ internal object ClassMerger {
* @param targetClass the class to start traversing the class hierarchy from * @param targetClass the class to start traversing the class hierarchy from
* @param callback function that is called for every class in the hierarchy * @param callback function that is called for every class in the hierarchy
*/ */
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) { fun BytecodeContext.traverseClassHierarchy(
targetClass: MutableClass,
callback: MutableClass.() -> Unit,
) {
callback(targetClass) callback(targetClass)
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let { this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback) traverseClassHierarchy(it, callback)
@ -195,7 +209,8 @@ internal object ClassMerger {
* @return The [this] filtered on [needles] matching the given [predicate]. * @return The [this] filtered on [needles] matching the given [predicate].
*/ */
fun <HayType, NeedleType> Iterable<HayType>.filterAny( fun <HayType, NeedleType> Iterable<HayType>.filterAny(
needles: Iterable<NeedleType>, predicate: (HayType, NeedleType) -> Boolean needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean,
) = Iterable<HayType>::filter.any(this, needles, predicate) ) = Iterable<HayType>::filter.any(this, needles, predicate)
/** /**
@ -206,13 +221,14 @@ internal object ClassMerger {
* @return The [this] filtered on [needles] not matching the given [predicate]. * @return The [this] filtered on [needles] not matching the given [predicate].
*/ */
fun <HayType, NeedleType> Iterable<HayType>.filterNotAny( fun <HayType, NeedleType> Iterable<HayType>.filterNotAny(
needles: Iterable<NeedleType>, predicate: (HayType, NeedleType) -> Boolean needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean,
) = Iterable<HayType>::filterNot.any(this, needles, predicate) ) = Iterable<HayType>::filterNot.any(this, needles, predicate)
fun <HayType, NeedleType> KFunction2<Iterable<HayType>, (HayType) -> Boolean, List<HayType>>.any( fun <HayType, NeedleType> KFunction2<Iterable<HayType>, (HayType) -> Boolean, List<HayType>>.any(
haystack: Iterable<HayType>, haystack: Iterable<HayType>,
needles: Iterable<NeedleType>, needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean predicate: (HayType, NeedleType) -> Boolean,
) = this(haystack) { hay -> ) = this(haystack) { hay ->
needles.any { needle -> needles.any { needle ->
predicate(hay, needle) predicate(hay, needle)

View File

@ -31,10 +31,10 @@ class DomFileEditor internal constructor(
/** /**
* The document of the xml file * The document of the xml file
*/ */
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream) val file: Document =
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize) .also(Document::normalize)
// lazily open an output stream // lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed // this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor // the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
@ -58,7 +58,8 @@ class DomFileEditor internal constructor(
outputStream?.let { outputStream?.let {
// prevent writing to same file, if it is being locked // prevent writing to same file, if it is being locked
// isLocked will be false if the editor was created through a stream // isLocked will be false if the editor was created through a stream
val isLocked = filePath?.let { path -> val isLocked =
filePath?.let { path ->
val isLocked = locks[path]!! > 1 val isLocked = locks[path]!! > 1
// decrease the lock count if the editor was opened for a file // decrease the lock count if the editor was opened for a file
locks.merge(path, -1, Integer::sum) locks.merge(path, -1, Integer::sum)

View File

@ -19,7 +19,8 @@ class ProxyClassList internal constructor(classes: MutableSet<ClassDef>) : Mutab
/** /**
* Replace all classes with their mutated versions. * Replace all classes with their mutated versions.
*/ */
internal fun replaceClasses() = proxies.removeIf { proxy -> internal fun replaceClasses() =
proxies.removeIf { proxy ->
// If the proxy is unused, return false to keep it in the proxies list. // If the proxy is unused, return false to keep it in the proxies list.
if (!proxy.resolved) return@removeIf false if (!proxy.resolved) return@removeIf false

View File

@ -14,7 +14,7 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
*/ */
class MethodWalker internal constructor( class MethodWalker internal constructor(
private val bytecodeContext: BytecodeContext, private val bytecodeContext: BytecodeContext,
private var currentMethod: Method private var currentMethod: Method,
) { ) {
/** /**
* Get the method which was walked last. * Get the method which was walked last.
@ -36,7 +36,10 @@ class MethodWalker internal constructor(
* @param walkMutable If this is true, the class of the method will be resolved mutably. * @param walkMutable If this is true, the class of the method will be resolved mutably.
* @return The same [MethodWalker] instance with the method at [offset]. * @return The same [MethodWalker] instance with the method at [offset].
*/ */
fun nextMethod(offset: Int, walkMutable: Boolean = false): MethodWalker { fun nextMethod(
offset: Int,
walkMutable: Boolean = false,
): MethodWalker {
currentMethod.implementation?.instructions?.let { instructions -> currentMethod.implementation?.instructions?.let { instructions ->
val instruction = instructions.elementAt(offset) val instruction = instructions.elementAt(offset)
@ -44,7 +47,8 @@ class MethodWalker internal constructor(
val proxy = bytecodeContext.findClass(newMethod.definingClass)!! val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
currentMethod = methods.first { currentMethod =
methods.first {
return@first MethodUtil.methodSignaturesMatch(it, newMethod) return@first MethodUtil.methodSignaturesMatch(it, newMethod)
} }
return this return this

View File

@ -27,7 +27,8 @@ class ClassProxy internal constructor(
resolved = true resolved = true
if (immutableClass is MutableClass) { if (immutableClass is MutableClass) {
immutableClass immutableClass
} else } else {
MutableClass(immutableClass) MutableClass(immutableClass)
} }
} }
}

View File

@ -3,11 +3,11 @@ package app.revanced.patcher.util.proxy.mutableTypes
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.google.common.collect.Iterables
import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference
import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.util.FieldUtil import com.android.tools.smali.dexlib2.util.FieldUtil
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
import com.google.common.collect.Iterables
class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() { class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() {
// Class // Class

View File

@ -5,7 +5,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
import com.android.tools.smali.dexlib2.iface.AnnotationElement import com.android.tools.smali.dexlib2.iface.AnnotationElement
import com.android.tools.smali.dexlib2.iface.value.AnnotationEncodedValue import com.android.tools.smali.dexlib2.iface.value.AnnotationEncodedValue
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) : BaseAnnotationEncodedValue(), class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
BaseAnnotationEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var type = annotationEncodedValue.type private var type = annotationEncodedValue.type

View File

@ -3,7 +3,8 @@ package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue
import com.android.tools.smali.dexlib2.iface.value.BooleanEncodedValue import com.android.tools.smali.dexlib2.iface.value.BooleanEncodedValue
class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) : BaseBooleanEncodedValue(), class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) :
BaseBooleanEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = booleanEncodedValue.value private var value = booleanEncodedValue.value

View File

@ -3,7 +3,8 @@ package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue
import com.android.tools.smali.dexlib2.iface.value.DoubleEncodedValue import com.android.tools.smali.dexlib2.iface.value.DoubleEncodedValue
class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) : BaseDoubleEncodedValue(), class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) :
BaseDoubleEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = doubleEncodedValue.value private var value = doubleEncodedValue.value

View File

@ -4,7 +4,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseMethodEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.value.MethodEncodedValue import com.android.tools.smali.dexlib2.iface.value.MethodEncodedValue
class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) : BaseMethodEncodedValue(), class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) :
BaseMethodEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = methodEncodedValue.value private var value = methodEncodedValue.value

View File

@ -22,6 +22,4 @@ class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEnco
return MutableMethodHandleEncodedValue(this) return MutableMethodHandleEncodedValue(this)
} }
} }
} }

View File

@ -4,7 +4,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseMethodTypeEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference
import com.android.tools.smali.dexlib2.iface.value.MethodTypeEncodedValue import com.android.tools.smali.dexlib2.iface.value.MethodTypeEncodedValue
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(), class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) :
BaseMethodTypeEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = methodTypeEncodedValue.value private var value = methodTypeEncodedValue.value
@ -21,6 +22,4 @@ class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedVal
return MutableMethodTypeEncodedValue(this) return MutableMethodTypeEncodedValue(this)
} }
} }
} }

View File

@ -4,7 +4,8 @@ import com.android.tools.smali.dexlib2.base.value.BaseStringEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue
class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) : BaseStringEncodedValue(), class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) :
BaseStringEncodedValue(),
MutableEncodedValue { MutableEncodedValue {
private var value = stringEncodedValue.value private var value = stringEncodedValue.value

View File

@ -1,9 +1,6 @@
package app.revanced.patcher.util.smali package app.revanced.patcher.util.smali
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcodes import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction import com.android.tools.smali.dexlib2.builder.BuilderInstruction
@ -12,6 +9,9 @@ import com.android.tools.smali.smali.LexerErrorInterface
import com.android.tools.smali.smali.smaliFlexLexer import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser import com.android.tools.smali.smali.smaliParser
import com.android.tools.smali.smali.smaliTreeWalker import com.android.tools.smali.smali.smaliTreeWalker
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.Locale import java.util.Locale
@ -32,14 +32,22 @@ class InlineSmaliCompiler {
* if the parameters and registers of the method are passed. * if the parameters and registers of the method are passed.
*/ */
fun compile( fun compile(
instructions: String, parameters: String, registers: Int, forStaticMethod: Boolean instructions: String,
parameters: String,
registers: Int,
forStaticMethod: Boolean,
): List<BuilderInstruction> { ): List<BuilderInstruction> {
val input = METHOD_TEMPLATE.format(Locale.ENGLISH, val input =
METHOD_TEMPLATE.format(
Locale.ENGLISH,
if (forStaticMethod) { if (forStaticMethod) {
"static" "static"
} else { } else {
"" ""
}, parameters, registers, instructions },
parameters,
registers,
instructions,
) )
val reader = InputStreamReader(input.byteInputStream()) val reader = InputStreamReader(input.byteInputStream())
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15) val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
@ -48,7 +56,7 @@ class InlineSmaliCompiler {
val result = parser.smali_file() val result = parser.smali_file()
if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) { if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) {
throw IllegalStateException( throw IllegalStateException(
"Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!" "Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!",
) )
} }
val treeStream = CommonTreeNodeStream(result.tree) val treeStream = CommonTreeNodeStream(result.tree)
@ -70,10 +78,11 @@ class InlineSmaliCompiler {
* @returns A list of instructions. * @returns A list of instructions.
*/ */
fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> { fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> {
return InlineSmaliCompiler.compile(this, return InlineSmaliCompiler.compile(
this,
method?.parameters?.joinToString("") { it } ?: "", method?.parameters?.joinToString("") { it } ?: "",
method?.implementation?.registerCount ?: 1, method?.implementation?.registerCount ?: 1,
method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true,
) )
} }

View File

@ -0,0 +1,50 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
private object AnnotationExtensionsTest {
@Test
fun `find annotation in annotated class`() {
assertNotNull(TestClasses.Annotation2::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `find annotation`() {
assertNotNull(TestClasses.AnnotatedClass::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `find annotation recursively in super class`() {
assertNotNull(TestClasses.AnnotatedClass2::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `find annotation recursively in super class with annotation`() {
assertNotNull(TestClasses.AnnotatedTestClass3::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `don't find unknown annotation in annotated class`() {
assertNull(TestClasses.AnnotatedClass::class.findAnnotationRecursively(TestClasses.UnknownAnnotation::class))
}
object TestClasses {
annotation class Annotation
@Annotation
annotation class Annotation2
annotation class UnknownAnnotation
@Annotation
abstract class AnnotatedClass
@Annotation2
class AnnotatedTestClass3
abstract class AnnotatedClass2 : AnnotatedClass()
}
}

View File

@ -27,7 +27,8 @@ private object InstructionExtensionsTest {
private lateinit var testMethodImplementation: MutableMethodImplementation private lateinit var testMethodImplementation: MutableMethodImplementation
@BeforeEach @BeforeEach
fun createTestMethod() = ImmutableMethod( fun createTestMethod() =
ImmutableMethod(
"TestClass;", "TestClass;",
"testMethod", "testMethod",
null, null,
@ -41,7 +42,8 @@ private object InstructionExtensionsTest {
).let { testMethod = it.toMutable() } ).let { testMethod = it.toMutable() }
@Test @Test
fun addInstructionsToImplementationIndexed() = applyToImplementation { fun addInstructionsToImplementationIndexed() =
applyToImplementation {
addInstructions(5, getTestInstructions(5..6)).also { addInstructions(5, getTestInstructions(5..6)).also {
assertRegisterIs(5, 5) assertRegisterIs(5, 5)
assertRegisterIs(6, 6) assertRegisterIs(6, 6)
@ -51,7 +53,8 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun addInstructionsToImplementation() = applyToImplementation { fun addInstructionsToImplementation() =
applyToImplementation {
addInstructions(getTestInstructions(10..11)).also { addInstructions(getTestInstructions(10..11)).also {
assertRegisterIs(10, 10) assertRegisterIs(10, 10)
assertRegisterIs(11, 11) assertRegisterIs(11, 11)
@ -59,19 +62,22 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun removeInstructionsFromImplementationIndexed() = applyToImplementation { fun removeInstructionsFromImplementationIndexed() =
applyToImplementation {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) } removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
} }
@Test @Test
fun removeInstructionsFromImplementation() = applyToImplementation { fun removeInstructionsFromImplementation() =
applyToImplementation {
removeInstructions(0).also { assertRegisterIs(9, 9) } removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) } removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) } removeInstructions(2).also { assertRegisterIs(3, 0) }
} }
@Test @Test
fun replaceInstructionsInImplementationIndexed() = applyToImplementation { fun replaceInstructionsInImplementationIndexed() =
applyToImplementation {
replaceInstructions(5, getTestInstructions(0..1)).also { replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5) assertRegisterIs(0, 5)
assertRegisterIs(1, 6) assertRegisterIs(1, 6)
@ -80,27 +86,32 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun addInstructionToMethodIndexed() = applyToMethod { fun addInstructionToMethodIndexed() =
applyToMethod {
addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) } addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
} }
@Test @Test
fun addInstructionToMethod() = applyToMethod { fun addInstructionToMethod() =
applyToMethod {
addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) } addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
} }
@Test @Test
fun addSmaliInstructionToMethodIndexed() = applyToMethod { fun addSmaliInstructionToMethodIndexed() =
applyToMethod {
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) } addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
} }
@Test @Test
fun addSmaliInstructionToMethod() = applyToMethod { fun addSmaliInstructionToMethod() =
applyToMethod {
addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) } addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
} }
@Test @Test
fun addInstructionsToMethodIndexed() = applyToMethod { fun addInstructionsToMethodIndexed() =
applyToMethod {
addInstructions(5, getTestInstructions(0..1)).also { addInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5) assertRegisterIs(0, 5)
assertRegisterIs(1, 6) assertRegisterIs(1, 6)
@ -110,7 +121,8 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun addInstructionsToMethod() = applyToMethod { fun addInstructionsToMethod() =
applyToMethod {
addInstructions(getTestInstructions(0..1)).also { addInstructions(getTestInstructions(0..1)).also {
assertRegisterIs(0, 10) assertRegisterIs(0, 10)
assertRegisterIs(1, 11) assertRegisterIs(1, 11)
@ -120,7 +132,8 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun addSmaliInstructionsToMethodIndexed() = applyToMethod { fun addSmaliInstructionsToMethodIndexed() =
applyToMethod {
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also { addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5) assertRegisterIs(0, 5)
assertRegisterIs(1, 6) assertRegisterIs(1, 6)
@ -130,7 +143,8 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun addSmaliInstructionsToMethod() = applyToMethod { fun addSmaliInstructionsToMethod() =
applyToMethod {
addInstructions(getTestSmaliInstructions(0..1)).also { addInstructions(getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 10) assertRegisterIs(0, 10)
assertRegisterIs(1, 11) assertRegisterIs(1, 11)
@ -140,19 +154,21 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() = applyToMethod { fun addSmaliInstructionsWithExternalLabelToMethodIndexed() =
applyToMethod {
val label = ExternalLabel("testLabel", getInstruction(5)) val label = ExternalLabel("testLabel", getInstruction(5))
addInstructionsWithLabels( addInstructionsWithLabels(
5, 5,
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"), getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
label label,
).also { ).also {
assertRegisterIs(0, 5) assertRegisterIs(0, 5)
assertRegisterIs(1, 6) assertRegisterIs(1, 6)
assertRegisterIs(5, 8) assertRegisterIs(5, 8)
val gotoTarget = getInstruction<BuilderOffsetInstruction>(7) val gotoTarget =
getInstruction<BuilderOffsetInstruction>(7)
.target.location.instruction as OneRegisterInstruction .target.location.instruction as OneRegisterInstruction
assertEquals(5, gotoTarget.registerA) assertEquals(5, gotoTarget.registerA)
@ -160,7 +176,8 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun removeInstructionFromMethodIndexed() = applyToMethod { fun removeInstructionFromMethodIndexed() =
applyToMethod {
removeInstruction(5).also { removeInstruction(5).also {
assertRegisterIs(4, 4) assertRegisterIs(4, 4)
assertRegisterIs(6, 5) assertRegisterIs(6, 5)
@ -168,24 +185,28 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun removeInstructionsFromMethodIndexed() = applyToMethod { fun removeInstructionsFromMethodIndexed() =
applyToMethod {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) } removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
} }
@Test @Test
fun removeInstructionsFromMethod() = applyToMethod { fun removeInstructionsFromMethod() =
applyToMethod {
removeInstructions(0).also { assertRegisterIs(9, 9) } removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) } removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) } removeInstructions(2).also { assertRegisterIs(3, 0) }
} }
@Test @Test
fun replaceInstructionInMethodIndexed() = applyToMethod { fun replaceInstructionInMethodIndexed() =
applyToMethod {
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) } replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
} }
@Test @Test
fun replaceInstructionsInMethodIndexed() = applyToMethod { fun replaceInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestInstructions(0..1)).also { replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5) assertRegisterIs(0, 5)
assertRegisterIs(1, 6) assertRegisterIs(1, 6)
@ -194,7 +215,8 @@ private object InstructionExtensionsTest {
} }
@Test @Test
fun replaceSmaliInstructionsInMethodIndexed() = applyToMethod { fun replaceSmaliInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestSmaliInstructions(0..1)).also { replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5) assertRegisterIs(0, 5)
assertRegisterIs(1, 6) assertRegisterIs(1, 6)
@ -212,18 +234,25 @@ private object InstructionExtensionsTest {
testMethod.apply(block) testMethod.apply(block)
} }
private fun MutableMethodImplementation.assertRegisterIs(register: Int, atIndex: Int) = assertEquals( private fun MutableMethodImplementation.assertRegisterIs(
register, getInstruction<OneRegisterInstruction>(atIndex).registerA register: Int,
atIndex: Int,
) = assertEquals(
register,
getInstruction<OneRegisterInstruction>(atIndex).registerA,
) )
private fun MutableMethod.assertRegisterIs(register: Int, atIndex: Int) = private fun MutableMethod.assertRegisterIs(
implementation!!.assertRegisterIs(register, atIndex) register: Int,
atIndex: Int,
) = implementation!!.assertRegisterIs(register, atIndex)
private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) } private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) }
private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0" private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0"
private fun getTestSmaliInstructions(range: IntRange) = range.joinToString("\n") { private fun getTestSmaliInstructions(range: IntRange) =
range.joinToString("\n") {
getTestSmaliInstruction(it) getTestSmaliInstruction(it)
} }

View File

@ -0,0 +1,29 @@
package app.revanced.patcher.patch
import app.revanced.patcher.data.ResourceContext
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import app.revanced.patcher.patch.annotation.Patch as PatchAnnotation
object PatchInitializationTest {
@Test
fun `initialize using constructor`() {
val patch =
object : ResourcePatch(name = "Resource patch test") {
override fun execute(context: ResourceContext) {}
}
assert(patch.name == "Resource patch test")
}
@Test
fun `initialize using annotation`() {
val patch =
@PatchAnnotation("Resource patch test")
object : ResourcePatch() {
override fun execute(context: ResourceContext) {}
}
assert(patch.name == "Resource patch test")
}
}

View File

@ -67,8 +67,7 @@ internal class PatchOptionsTest {
} }
@Test @Test
fun `should allow setting custom value`() = fun `should allow setting custom value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = "unknown" }
assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = "unknown" }
@Test @Test
fun `should allow resetting value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = null } fun `should allow resetting value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = null }
@ -86,40 +85,41 @@ internal class PatchOptionsTest {
} }
@Test @Test
fun `option types should be known`() = fun `option types should be known`() = assertTrue(OptionsTestPatch.options["array"].valueType == "StringArray")
assertTrue(OptionsTestPatch.options["array"].valueType == "StringArray")
@Test @Test
fun `getting default value should work`() = fun `getting default value should work`() = assertDoesNotThrow { assertNull(OptionsTestPatch.resettableOption.default) }
assertDoesNotThrow { assertNull(OptionsTestPatch.resettableOption.default) }
@Suppress("DEPRECATION")
private object OptionsTestPatch : BytecodePatch() { private object OptionsTestPatch : BytecodePatch() {
var booleanOption by booleanPatchOption( var booleanOption by booleanPatchOption(
"bool", "bool",
true true,
) )
var requiredStringOption by stringPatchOption( var requiredStringOption by stringPatchOption(
"required", "required",
"default", "default",
required = true required = true,
) )
var stringArrayOption = stringArrayPatchOption( var stringArrayOption =
stringArrayPatchOption(
"array", "array",
arrayOf("1", "2") arrayOf("1", "2"),
) )
var stringOptionWithChoices by stringPatchOption( var stringOptionWithChoices by stringPatchOption(
"choices", "choices",
"value", "value",
values = mapOf("Valid option value" to "valid") values = mapOf("Valid option value" to "valid"),
) )
var validatedOption by stringPatchOption( var validatedOption by stringPatchOption(
"validated", "validated",
"default" "default",
) { it == "valid" } ) { it == "valid" }
var resettableOption = stringPatchOption( var resettableOption =
"resettable", null, stringPatchOption(
required = true "resettable",
null,
required = true,
) )
override fun execute(context: BytecodeContext) {} override fun execute(context: BytecodeContext) {}

View File

@ -32,7 +32,7 @@ import com.google.common.collect.ImmutableList
name = "Example bytecode patch", name = "Example bytecode patch",
description = "Example demonstration of a bytecode patch.", description = "Example demonstration of a bytecode patch.",
dependencies = [ExampleResourcePatch::class], dependencies = [ExampleResourcePatch::class],
compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))] compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))],
) )
object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) { object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
// Entry point of a patch. Supplied fingerprints are resolved at this point. // Entry point of a patch. Supplied fingerprints are resolved at this point.
@ -60,7 +60,7 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String; invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
move-result-object v1 move-result-object v1
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
""" """,
) )
} }
@ -82,14 +82,14 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
BuilderInstruction21c( BuilderInstruction21c(
Opcode.CONST_STRING, Opcode.CONST_STRING,
0, 0,
ImmutableStringReference("Hello, ReVanced! Adding bytecode.") ImmutableStringReference("Hello, ReVanced! Adding bytecode."),
), ),
BuilderInstruction11x(Opcode.RETURN_OBJECT, 0) BuilderInstruction11x(Opcode.RETURN_OBJECT, 0),
), ),
null, null,
null null,
) ),
).toMutable() ).toMutable(),
) )
// Add a field in the main class. // Add a field in the main class.
@ -105,12 +105,12 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
ImmutableFieldReference( ImmutableFieldReference(
"Ljava/lang/System;", "Ljava/lang/System;",
"out", "out",
"Ljava/io/PrintStream;" "Ljava/io/PrintStream;",
) ),
), ),
null, null,
null null,
).toMutable() ).toMutable(),
) )
} }
} ?: throw PatchException("Fingerprint failed to resolve.") } ?: throw PatchException("Fingerprint failed to resolve.")
@ -121,7 +121,10 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
* @param index The index of the instruction to replace. * @param index The index of the instruction to replace.
* @param string The replacement string. * @param string The replacement string.
*/ */
private fun MutableMethod.replaceStringAt(index: Int, string: String) { private fun MutableMethod.replaceStringAt(
index: Int,
string: String,
) {
val instruction = getInstruction(index) val instruction = getInstruction(index)
// Utility method of dexlib2. // Utility method of dexlib2.
@ -139,8 +142,7 @@ object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
// At last, use the method replaceInstruction to replace it at the given index startIndex. // At last, use the method replaceInstruction to replace it at the given index startIndex.
replaceInstruction( replaceInstruction(
index, index,
"const-string ${strInstruction.registerA}, ${ImmutableStringReference(string)}" "const-string ${strInstruction.registerA}, ${ImmutableStringReference(string)}",
) )
} }
} }

View File

@ -1,7 +1,7 @@
package app.revanced.patcher.patch.usage package app.revanced.patcher.patch.usage
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.MethodFingerprint import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@ -14,7 +14,7 @@ object ExampleFingerprint : MethodFingerprint(
Opcode.SGET_OBJECT, Opcode.SGET_OBJECT,
null, // Matching unknown opcodes. null, // Matching unknown opcodes.
Opcode.INVOKE_STATIC, // This is intentionally wrong to test fuzzy matching. Opcode.INVOKE_STATIC, // This is intentionally wrong to test fuzzy matching.
Opcode.RETURN_VOID Opcode.RETURN_VOID,
), ),
null null,
) )

View File

@ -4,18 +4,18 @@ import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch import app.revanced.patcher.patch.ResourcePatch
import org.w3c.dom.Element import org.w3c.dom.Element
class ExampleResourcePatch : ResourcePatch() { class ExampleResourcePatch : ResourcePatch() {
override fun execute(context: ResourceContext) { override fun execute(context: ResourceContext) {
context.xmlEditor["AndroidManifest.xml"].use { editor -> context.xmlEditor["AndroidManifest.xml"].use { editor ->
val element = editor // regular DomFileEditor val element =
editor // regular DomFileEditor
.file .file
.getElementsByTagName("application") .getElementsByTagName("application")
.item(0) as Element .item(0) as Element
element element
.setAttribute( .setAttribute(
"exampleAttribute", "exampleAttribute",
"exampleValue" "exampleValue",
) )
} }
} }

View File

@ -33,16 +33,18 @@ internal class InlineSmaliCompilerTest {
val insnIndex = insnAmount - 2 val insnIndex = insnAmount - 2
val targetIndex = insnIndex - 1 val targetIndex = insnIndex - 1
method.addInstructions(arrayOfNulls<String>(insnAmount).also { method.addInstructions(
arrayOfNulls<String>(insnAmount).also {
Arrays.fill(it, "const/4 v0, 0x0") Arrays.fill(it, "const/4 v0, 0x0")
}.joinToString("\n")) }.joinToString("\n"),
)
method.addInstructionsWithLabels( method.addInstructionsWithLabels(
targetIndex, targetIndex,
""" """
:test :test
const/4 v0, 0x1 const/4 v0, 0x1
if-eqz v0, :test if-eqz v0, :test
""" """,
) )
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex) val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
@ -59,7 +61,7 @@ internal class InlineSmaliCompilerTest {
""" """
const/4 v0, 0x1 const/4 v0, 0x1
const/4 v0, 0x0 const/4 v0, 0x0
""" """,
) )
assertEquals(labelIndex, method.newLabel(labelIndex).location.index) assertEquals(labelIndex, method.newLabel(labelIndex).location.index)
@ -71,7 +73,7 @@ internal class InlineSmaliCompilerTest {
if-eqz v0, :test if-eqz v0, :test
return-void return-void
""", """,
ExternalLabel("test", method.getInstruction(1)) ExternalLabel("test", method.getInstruction(1)),
) )
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex) val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
@ -93,10 +95,13 @@ internal class InlineSmaliCompilerTest {
accessFlags, accessFlags,
emptySet(), emptySet(),
emptySet(), emptySet(),
MutableMethodImplementation(registerCount) MutableMethodImplementation(registerCount),
).toMutable() ).toMutable()
private fun instructionEquals(want: BuilderInstruction, have: BuilderInstruction) { private fun instructionEquals(
want: BuilderInstruction,
have: BuilderInstruction,
) {
assertEquals(want.opcode, have.opcode) assertEquals(want.opcode, have.opcode)
assertEquals(want.format, have.format) assertEquals(want.format, have.format)
assertEquals(want.codeUnits, have.codeUnits) assertEquals(want.codeUnits, have.codeUnits)