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

8612
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,11 +39,12 @@ 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].
* *
* @param patches The [Patch]es to add. * @param patches The [Patch]es to add.
*/ */
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
override fun acceptPatches(patches: List<Patch<*>>) { override fun acceptPatches(patches: List<Patch<*>>) {
/** /**
@ -83,7 +83,7 @@ class Patcher(
dependency.visit() dependency.visit()
} }
} }
*/ */
/** /**
* Returns true if at least one patch or its dependencies matches the given predicate. * Returns true if at least one patch or its dependencies matches the given predicate.
@ -127,140 +127,142 @@ 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.
*
* @param patch The [Patch] to execute.
* @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
* @return The result of executing the [Patch].
*/
fun executePatch(
patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult {
val patchName = patch.name ?: patch.toString()
/** executedPatches[patch]?.let { patchResult ->
* Execute a [Patch] and its dependencies recursively. patchResult.exception ?: return patchResult
*
* @param patch The [Patch] to execute.
* @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
* @return The result of executing the [Patch].
*/
fun executePatch(
patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, PatchResult>
): PatchResult {
val patchName = patch.name ?: patch.toString()
executedPatches[patch]?.let { patchResult -> // Return a new result with an exception indicating that the patch was not executed previously,
patchResult.exception ?: return patchResult // because it is a dependency of another patch that failed.
return PatchResult(patch, PatchException("'$patchName' did not succeed previously"))
}
// Return a new result with an exception indicating that the patch was not executed previously, // Recursively execute all dependency patches.
// because it is a dependency of another patch that failed. patch.dependencies?.forEach { dependencyClass ->
return PatchResult(patch, PatchException("'$patchName' did not succeed previously")) val dependency = context.allPatches[dependencyClass]!!
} val result = executePatch(dependency, executedPatches)
// Recursively execute all dependency patches. result.exception?.let {
patch.dependencies?.forEach { dependencyClass -> return PatchResult(
val dependency = context.allPatches[dependencyClass]!! patch,
val result = executePatch(dependency, executedPatches) PatchException(
"'$patchName' depends on '${dependency.name ?: dependency}' " +
result.exception?.let { "that raised an exception:\n${it.stackTraceToString()}",
return PatchResult( ),
patch,
PatchException(
"'$patchName' depends on '${dependency.name ?: dependency}' " +
"that raised an exception:\n${it.stackTraceToString()}"
) )
)
}
}
return try {
// TODO: Implement this in a more polymorphic way.
when (patch) {
is BytecodePatch -> {
patch.fingerprints.resolveUsingLookupMap(context.bytecodeContext)
patch.execute(context.bytecodeContext)
}
is ResourcePatch -> {
patch.execute(context.resourceContext)
} }
} }
PatchResult(patch) return try {
} catch (exception: PatchException) { // TODO: Implement this in a more polymorphic way.
PatchResult(patch, exception) when (patch) {
} catch (exception: Exception) { is BytecodePatch -> {
PatchResult(patch, PatchException(exception)) patch.fingerprints.resolveUsingLookupMap(context.bytecodeContext)
}.also { executedPatches[patch] = it } patch.execute(context.bytecodeContext)
} }
is ResourcePatch -> {
patch.execute(context.resourceContext)
}
}
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() PatchResult(patch)
initializeLookupMaps(context.bytecodeContext)
// Prevent from decoding the app manifest twice if it is not needed.
if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL)
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL)
logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
context.executablePatches.values.sortedBy { it.name }.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
// If the patch failed, emit the result, even if it is closeable.
// Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow
} ?: run {
if (patch is Closeable) return@run
emit(patchResult)
}
}
executedPatches.values
.filter { it.exception == null }
.filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
val patch = executedPatch.patch
val result = try {
(patch as Closeable).close()
executedPatch
} catch (exception: PatchException) { } catch (exception: PatchException) {
PatchResult(patch, exception) PatchResult(patch, exception)
} catch (exception: Exception) { } catch (exception: Exception) {
PatchResult(patch, PatchException(exception)) PatchResult(patch, PatchException(exception))
} }.also { executedPatches[patch] = it }
}
result.exception?.let { if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
emit(
PatchResult( LookupMap.initializeLookupMaps(context.bytecodeContext)
patch,
PatchException( // Prevent from decoding the app manifest twice if it is not needed.
"'${patch.name}' raised an exception while being closed: ${it.stackTraceToString()}", if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) {
result.exception context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL)
) }
)
) logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
context.executablePatches.values.sortedBy { it.name }.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
// If the patch failed, emit the result, even if it is closeable.
// Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow if (returnOnError) return@flow
} ?: run { } ?: run {
patch.name ?: return@run if (patch is Closeable) return@run
emit(result) emit(patchResult)
} }
} }
}
override fun close() = clearLookupMaps() executedPatches.values
.filter { it.exception == null }
.filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
val patch = executedPatch.patch
val result =
try {
(patch as Closeable).close()
executedPatch
} catch (exception: PatchException) {
PatchResult(patch, exception)
} catch (exception: Exception) {
PatchResult(patch, PatchException(exception))
}
result.exception?.let {
emit(
PatchResult(
patch,
PatchException(
"'${patch.name}' raised an exception while being closed: ${it.stackTraceToString()}",
result.exception,
),
),
)
if (returnOnError) return@flow
} ?: run {
patch.name ?: return@run
emit(result)
}
}
}
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() =
context.bytecodeContext.get(), PatcherResult(
context.resourceContext.get(), context.bytecodeContext.get(),
context.packageMetadata.apkInfo.doNotCompress?.toList() context.resourceContext.get(),
) 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,20 +32,23 @@ 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 =
useAapt2 = true Config.getDefaultConfig().apply {
aaptPath = aaptBinaryPath ?: "" useAapt2 = true
frameworkDirectory = frameworkFileDirectory aaptPath = aaptBinaryPath ?: ""
} frameworkDirectory = frameworkFileDirectory
fun recreateResourceCacheDirectory() = resourceCachePath.also {
if (it.exists()) {
logger.info("Deleting existing resource cache directory")
if (!it.deleteRecursively())
logger.severe("Failed to delete existing resource cache directory")
} }
it.mkdirs() fun recreateResourceCacheDirectory() =
} resourceCachePath.also {
if (it.exists()) {
logger.info("Deleting existing resource cache directory")
if (!it.deleteRecursively()) {
logger.severe("Failed to delete existing resource cache directory")
}
}
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

@ -28,141 +28,153 @@ import java.util.logging.Logger
*/ */
class BytecodeContext internal constructor(private val options: PatcherOptions) : class BytecodeContext internal constructor(private val options: PatcherOptions) :
Context<List<PatcherResult.PatchedDexFile>> { Context<List<PatcherResult.PatchedDexFile>> {
private val logger = Logger.getLogger(BytecodeContext::class.java.name) private val logger = Logger.getLogger(BytecodeContext::class.java.name)
/**
* [Opcodes] of the supplied [PatcherOptions.inputFile].
*/
internal lateinit var opcodes: Opcodes
/**
* The list of classes.
*/
val classes by lazy {
ProxyClassList(
MultiDexIO.readDexFile(
true, options.inputFile, BasicDexFileNamer(), null, null
).also { opcodes = it.opcodes }.classes.toMutableSet()
)
}
/**
* The [Integrations] of this [PatcherContext].
*/
internal val integrations = Integrations()
/**
* Find a class by a given class name.
*
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
/**
* Proxy a class.
* This will allow the class to be modified.
*
* @param classDef The class to proxy.
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) = this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
ClassProxy(classDef).also { this.classes.add(it) }
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
/**
* Compile bytecode from the [BytecodeContext].
*
* @return The compiled bytecode.
*/
override fun get(): List<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
val patchedDexFileResults = options.resourceCachePath.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
if (options.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
override fun getOpcodes() = this@BytecodeContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
System.gc()
return patchedDexFileResults
}
/**
* The integrations of a [PatcherContext].
*/
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
/**
* Whether to merge integrations.
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
*/
var merge = false
/** /**
* Merge integrations into the [BytecodeContext] and flush all [Integrations]. * [Opcodes] of the supplied [PatcherOptions.inputFile].
*/ */
override fun flush() { internal lateinit var opcodes: Opcodes
if (!merge) return
logger.info("Merging integrations") /**
* The list of classes.
val classMap = classes.associateBy { it.type } */
val classes by lazy {
this@Integrations.forEach { integrations -> ProxyClassList(
MultiDexIO.readDexFile( MultiDexIO.readDexFile(
true, true,
integrations, BasicDexFileNamer(), options.inputFile,
BasicDexFileNamer(),
null, null,
null null,
).classes.forEach classDef@{ classDef -> ).also { opcodes = it.opcodes }.classes.toMutableSet(),
val existingClass = classMap[classDef.type] ?: run { )
logger.fine("Adding $classDef") }
classes.add(classDef)
return@classDef
}
logger.fine("$classDef exists. Adding missing methods and fields.") /**
* The [Integrations] of this [PatcherContext].
*/
internal val integrations = Integrations()
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass -> /**
// If the class was merged, replace the original class with the merged class. * Find a class by a given class name.
if (mergedClass === existingClass) return@let *
classes.apply { remove(existingClass); add(mergedClass) } * @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) }
?: // else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
/**
* Proxy a class.
* This will allow the class to be modified.
*
* @param classDef The class to proxy.
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) =
this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
ClassProxy(classDef).also { this.classes.add(it) }
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
/**
* Compile bytecode from the [BytecodeContext].
*
* @return The compiled bytecode.
*/
override fun get(): List<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
val patchedDexFileResults =
options.resourceCachePath.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
if (options.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
override fun getOpcodes() = this@BytecodeContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
System.gc()
return patchedDexFileResults
}
/**
* The integrations of a [PatcherContext].
*/
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
/**
* Whether to merge integrations.
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
*/
var merge = false
/**
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
*/
override fun flush() {
if (!merge) return
logger.info("Merging integrations")
val classMap = classes.associateBy { it.type }
this@Integrations.forEach { integrations ->
MultiDexIO.readDexFile(
true,
integrations,
BasicDexFileNamer(),
null,
null,
).classes.forEach classDef@{ classDef ->
val existingClass =
classMap[classDef.type] ?: run {
logger.fine("Adding $classDef")
classes.add(classDef)
return@classDef
}
logger.fine("$classDef exists. Adding missing methods and fields.")
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) return@let
classes.apply {
remove(existingClass)
add(mergedClass)
}
}
} }
} }
clear()
} }
clear()
} }
} }
}

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,52 +37,54 @@ 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) =
// Needed to decode resources. with(context.packageMetadata.apkInfo) {
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this) // Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this)
when (mode) { when (mode) {
ResourceDecodingMode.FULL -> { ResourceDecodingMode.FULL -> {
val outDir = options.recreateResourceCacheDirectory() val outDir = options.recreateResourceCacheDirectory()
logger.info("Decoding resources") logger.info("Decoding resources")
resourcesDecoder.decodeResources(outDir) resourcesDecoder.decodeResources(outDir)
resourcesDecoder.decodeManifest(outDir) resourcesDecoder.decodeManifest(outDir)
// Needed to record uncompressed files. // Needed to record uncompressed files.
val apkDecoder = ApkDecoder(options.resourceConfig, this) val apkDecoder = ApkDecoder(options.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping) apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework = UsesFramework().apply { usesFramework =
ids = resourcesDecoder.resTable.listFramePackages().map { it.id } UsesFramework().apply {
} ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
ResourceDecodingMode.MANIFEST_ONLY -> {
logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
XmlPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer
).decodeManifest(
apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { /* do nothing */
} }
} }
)
// Get the package name and version from the manifest using the XmlPullStreamDecoder. ResourceDecodingMode.MANIFEST_ONLY -> {
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo. logger.info("Decoding app manifest")
context.packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed // Decode manually instead of using resourceDecoder.decodeManifest
versionInfo.let { // because it does not support decoding to an OutputStream.
metadata.packageVersion = it.versionName ?: it.versionCode XmlPullStreamDecoder(
} AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer,
).decodeManifest(
apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { // do nothing
}
},
)
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
context.packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode
}
/* /*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here, The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
@ -92,18 +94,16 @@ class ResourceContext internal constructor(
Set this to false again to prevent the ResTable from being flagged as sparse falsely. Set this to false again to prevent the ResTable from being flagged as sparse falsely.
*/ */
metadata.apkInfo.sparseResources = false metadata.apkInfo.sparseResources = false
}
} }
} }
} }
}
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 =
Files.deleteIfExists(it.toPath()) cacheDirectory.resolve("aapt_temp_file").also {
}.also { resourceFile = it } Files.deleteIfExists(it.toPath())
}.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( targetAnnotationClass: Class<T>,
targetAnnotation: Class<T>, traversed: MutableSet<Annotation> 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))
?: continue
}
return null
} }
return this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf()) // Search the super class.
superclass?.findAnnotationRecursively(
targetAnnotationClass,
searchedClasses,
)?.let { return it }
// Search the interfaces.
interfaces.forEach { superClass ->
return superClass.findAnnotationRecursively(
targetAnnotationClass,
searchedClasses,
) ?: return@forEach
}
return null
} }
/**
* 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,14 +152,15 @@ 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 =
externalLabels.forEach { (name, _) -> StringBuilder(smaliInstructions).also { builder ->
builder.append("\n:$name\nnop") externalLabels.forEach { (name, _) ->
} builder.append("\n:$name\nnop")
}.toString() }
}.toString()
// Compile the instructions with the dummy labels // Compile the instructions with the dummy labels
val compiledInstructions = nopSmali.toInstructions(this) val compiledInstructions = nopSmali.toInstructions(this)
@ -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 ->
i.opcode, BuilderInstruction22t(
i.registerA, i.opcode,
i.registerB, i.registerA,
label i.registerB,
) 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,9 +209,11 @@ 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
// with the new instruction pointing to the real instruction. // with the new instruction pointing to the real instruction.
@ -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,11 +94,12 @@ abstract class MethodFingerprint(
} }
} }
val key = buildString { val key =
append(accessFlags) buildString {
append(returnTypeValue.first()) append(accessFlags)
if (parameters != null) appendParameters(parameters) append(returnTypeValue.first())
} if (parameters != null) appendParameters(parameters)
}
return methodSignatureLookupMap[key] ?: return LookupMap.MethodClassList() return methodSignatureLookupMap[key] ?: return LookupMap.MethodClassList()
} }
@ -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,91 +221,98 @@ abstract class MethodFingerprint(
} }
if (stringsList.isNotEmpty()) return false if (stringsList.isNotEmpty()) return false
} },
) )
} else null } else {
null
val patternScanResult = if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false
fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
) = buildList {
for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
this.add(
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
originalOpcode,
patternOpcode,
instructionIndex,
patternIndex
)
)
}
} }
fun Method.patternScan( val patternScanResult =
fingerprint: MethodFingerprint if (methodFingerprint.opcodes != null) {
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { method.implementation?.instructions ?: return false
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!! fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings(
val instructionLength = instructions.count() pattern: Iterable<Opcode?>,
val patternLength = pattern.count() instructions: Iterable<Instruction>,
) = buildList {
for (index in 0 until instructionLength) { for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) {
var patternIndex = 0 val originalOpcode = instructions.elementAt(instructionIndex).opcode
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex) val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) { this.add(
// if the entire pattern has not been scanned yet MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
// continue the scan originalOpcode,
patternIndex++ patternOpcode,
continue instructionIndex,
} patternIndex,
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod ),
val result = )
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.newWarnings(pattern, instructions)
return result
} }
} }
return null fun Method.patternScan(
fingerprint: MethodFingerprint,
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex,
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.newWarnings(pattern, instructions)
return result
}
}
return null
}
method.patternScan(methodFingerprint) ?: return false
} else {
null
} }
method.patternScan(methodFingerprint) ?: return false methodFingerprint.result =
} else null MethodFingerprintResult(
method,
methodFingerprint.result = MethodFingerprintResult( forClass,
method, MethodFingerprintResult.MethodFingerprintScanResult(
forClass, patternScanResult,
MethodFingerprintResult.MethodFingerprintScanResult( stringsScanResult,
patternScanResult, ),
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,
for (classDef in classes) { classes: Iterable<ClassDef>,
if (fingerprint.resolve(context, classDef)) break ) = forEach { fingerprint ->
} for (classDef in classes) {
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,9 +34,12 @@ 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(
//.fixFieldAccess(otherClass) otherClass: ClassDef,
//.fixMethodAccess(otherClass) context: BytecodeContext,
) = this
// .fixFieldAccess(otherClass)
// .fixMethodAccess(otherClass)
.addMissingFields(otherClass) .addMissingFields(otherClass)
.addMissingMethods(otherClass) .addMissingMethods(otherClass)
.publicize(otherClass, context) .publicize(otherClass, context)
@ -47,13 +50,14 @@ 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 =
methods.filterNot { method -> fromClass.methods.let { fromMethods ->
fromMethods.any { fromMethod -> methods.filterNot { method ->
MethodUtil.methodSignaturesMatch(fromMethod, method) fromMethods.any { fromMethod ->
MethodUtil.methodSignaturesMatch(fromMethod, method)
}
} }
} }
}
if (missingMethods.isEmpty()) return this if (missingMethods.isEmpty()) return this
@ -70,9 +74,10 @@ 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 =
fromField.name == field.name fields.filterNotAny(fromClass.fields) { field, fromField ->
} fromField.name == field.name
}
if (missingFields.isEmpty()) return this if (missingFields.isEmpty()) return this
@ -88,18 +93,22 @@ 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,
this.asMutableClass().apply { context: BytecodeContext,
context.traverseClassHierarchy(this) { ) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
if (accessFlags.isPublic()) return@traverseClassHierarchy this.asMutableClass().apply {
context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy
logger.fine("Publicizing ${this.type}") logger.fine("Publicizing ${this.type}")
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,11 +116,12 @@ 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 =
if (field.name != referenceField.name) return@filterAny false fields.filterAny(reference.fields) { field, referenceField ->
if (field.name != referenceField.name) return@filterAny false
referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic() referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic()
} }
if (brokenFields.isEmpty()) return this if (brokenFields.isEmpty()) return this
@ -135,11 +145,12 @@ 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 =
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false methods.filterAny(reference.methods) { method, referenceMethod ->
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false
referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic() referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic()
} }
if (brokenMethods.isEmpty()) return this if (brokenMethods.isEmpty()) return this
@ -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,9 +31,9 @@ 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 =
.also(Document::normalize) DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.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
@ -58,12 +58,13 @@ 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 =
val isLocked = locks[path]!! > 1 filePath?.let { path ->
// decrease the lock count if the editor was opened for a file val isLocked = locks[path]!! > 1
locks.merge(path, -1, Integer::sum) // decrease the lock count if the editor was opened for a file
isLocked locks.merge(path, -1, Integer::sum)
} ?: false isLocked
} ?: false
// if unlocked, write back to the file // if unlocked, write back to the file
if (!isLocked) { if (!isLocked) {

View File

@ -19,15 +19,16 @@ 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() =
// If the proxy is unused, return false to keep it in the proxies list. proxies.removeIf { proxy ->
// 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
// If it has been used, replace the original class with the mutable class. // If it has been used, replace the original class with the mutable class.
remove(proxy.immutableClass) remove(proxy.immutableClass)
add(proxy.mutableClass) add(proxy.mutableClass)
// Return true to remove the proxy from the proxies list. // Return true to remove the proxy from the proxies list.
return@removeIf true return@removeIf true
} }
} }

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,9 +47,10 @@ 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 =
return@first MethodUtil.methodSignaturesMatch(it, newMethod) methods.first {
} return@first MethodUtil.methodSignaturesMatch(it, newMethod)
}
return this return this
} }
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}") throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")

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,15 +32,23 @@ 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 =
if (forStaticMethod) { METHOD_TEMPLATE.format(
"static" Locale.ENGLISH,
} else { if (forStaticMethod) {
"" "static"
}, parameters, registers, instructions } else {
) ""
},
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)
val tokens = CommonTokenStream(lexer as TokenSource) val tokens = CommonTokenStream(lexer as TokenSource)
@ -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,180 +27,202 @@ private object InstructionExtensionsTest {
private lateinit var testMethodImplementation: MutableMethodImplementation private lateinit var testMethodImplementation: MutableMethodImplementation
@BeforeEach @BeforeEach
fun createTestMethod() = ImmutableMethod( fun createTestMethod() =
"TestClass;", ImmutableMethod(
"testMethod", "TestClass;",
null, "testMethod",
"V", null,
AccessFlags.PUBLIC.value, "V",
null, AccessFlags.PUBLIC.value,
null, null,
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply { null,
repeat(10) { i -> this.addInstruction(TestInstruction(i)) } MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
}, repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
).let { testMethod = it.toMutable() } },
).let { testMethod = it.toMutable() }
@Test @Test
fun addInstructionsToImplementationIndexed() = applyToImplementation { fun addInstructionsToImplementationIndexed() =
addInstructions(5, getTestInstructions(5..6)).also { applyToImplementation {
assertRegisterIs(5, 5) addInstructions(5, getTestInstructions(5..6)).also {
assertRegisterIs(6, 6) assertRegisterIs(5, 5)
assertRegisterIs(6, 6)
assertRegisterIs(5, 7) assertRegisterIs(5, 7)
}
} }
}
@Test @Test
fun addInstructionsToImplementation() = applyToImplementation { fun addInstructionsToImplementation() =
addInstructions(getTestInstructions(10..11)).also { applyToImplementation {
assertRegisterIs(10, 10) addInstructions(getTestInstructions(10..11)).also {
assertRegisterIs(11, 11) assertRegisterIs(10, 10)
assertRegisterIs(11, 11)
}
} }
}
@Test @Test
fun removeInstructionsFromImplementationIndexed() = applyToImplementation { fun removeInstructionsFromImplementationIndexed() =
removeInstructions(5, 5).also { assertRegisterIs(4, 4) } applyToImplementation {
} removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
@Test
fun removeInstructionsFromImplementation() = applyToImplementation {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionsInImplementationIndexed() = applyToImplementation {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
} }
}
@Test @Test
fun addInstructionToMethodIndexed() = applyToMethod { fun removeInstructionsFromImplementation() =
addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) } applyToImplementation {
} removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
@Test removeInstructions(2).also { assertRegisterIs(3, 0) }
fun addInstructionToMethod() = applyToMethod {
addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addSmaliInstructionToMethodIndexed() = applyToMethod {
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun addSmaliInstructionToMethod() = applyToMethod {
addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addInstructionsToMethodIndexed() = applyToMethod {
addInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
} }
}
@Test @Test
fun addInstructionsToMethod() = applyToMethod { fun replaceInstructionsInImplementationIndexed() =
addInstructions(getTestInstructions(0..1)).also { applyToImplementation {
assertRegisterIs(0, 10) replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(1, 11) assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(9, 9) assertRegisterIs(7, 7)
}
} }
}
@Test @Test
fun addSmaliInstructionsToMethodIndexed() = applyToMethod { fun addInstructionToMethodIndexed() =
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also { applyToMethod {
assertRegisterIs(0, 5) addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
} }
}
@Test @Test
fun addSmaliInstructionsToMethod() = applyToMethod { fun addInstructionToMethod() =
addInstructions(getTestSmaliInstructions(0..1)).also { applyToMethod {
assertRegisterIs(0, 10) addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
} }
}
@Test @Test
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() = applyToMethod { fun addSmaliInstructionToMethodIndexed() =
val label = ExternalLabel("testLabel", getInstruction(5)) applyToMethod {
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
addInstructionsWithLabels(
5,
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
label
).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 8)
val gotoTarget = getInstruction<BuilderOffsetInstruction>(7)
.target.location.instruction as OneRegisterInstruction
assertEquals(5, gotoTarget.registerA)
} }
}
@Test @Test
fun removeInstructionFromMethodIndexed() = applyToMethod { fun addSmaliInstructionToMethod() =
removeInstruction(5).also { applyToMethod {
assertRegisterIs(4, 4) addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
assertRegisterIs(6, 5)
} }
}
@Test @Test
fun removeInstructionsFromMethodIndexed() = applyToMethod { fun addInstructionsToMethodIndexed() =
removeInstructions(5, 5).also { assertRegisterIs(4, 4) } applyToMethod {
} addInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
@Test assertRegisterIs(5, 7)
fun removeInstructionsFromMethod() = applyToMethod { }
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionInMethodIndexed() = applyToMethod {
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun replaceInstructionsInMethodIndexed() = applyToMethod {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
} }
}
@Test @Test
fun replaceSmaliInstructionsInMethodIndexed() = applyToMethod { fun addInstructionsToMethod() =
replaceInstructions(5, getTestSmaliInstructions(0..1)).also { applyToMethod {
assertRegisterIs(0, 5) addInstructions(getTestInstructions(0..1)).also {
assertRegisterIs(1, 6) assertRegisterIs(0, 10)
assertRegisterIs(7, 7) assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsToMethodIndexed() =
applyToMethod {
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addSmaliInstructionsToMethod() =
applyToMethod {
addInstructions(getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 10)
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() =
applyToMethod {
val label = ExternalLabel("testLabel", getInstruction(5))
addInstructionsWithLabels(
5,
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
label,
).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 8)
val gotoTarget =
getInstruction<BuilderOffsetInstruction>(7)
.target.location.instruction as OneRegisterInstruction
assertEquals(5, gotoTarget.registerA)
}
}
@Test
fun removeInstructionFromMethodIndexed() =
applyToMethod {
removeInstruction(5).also {
assertRegisterIs(4, 4)
assertRegisterIs(6, 5)
}
}
@Test
fun removeInstructionsFromMethodIndexed() =
applyToMethod {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
}
@Test
fun removeInstructionsFromMethod() =
applyToMethod {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionInMethodIndexed() =
applyToMethod {
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun replaceInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
@Test
fun replaceSmaliInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
} }
}
// region Helper methods // region Helper methods
@ -212,20 +234,27 @@ 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) =
getTestSmaliInstruction(it) range.joinToString("\n") {
} getTestSmaliInstruction(it)
}
// endregion // endregion

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,41 +85,42 @@ 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(
"array",
arrayOf("1", "2")
) )
var stringArrayOption =
stringArrayPatchOption(
"array",
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
@ -12,9 +12,9 @@ object ExampleFingerprint : MethodFingerprint(
listOf("[L"), listOf("[L"),
listOf( listOf(
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 =
.file editor // regular DomFileEditor
.getElementsByTagName("application") .file
.item(0) as Element .getElementsByTagName("application")
.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(
Arrays.fill(it, "const/4 v0, 0x0") arrayOfNulls<String>(insnAmount).also {
}.joinToString("\n")) Arrays.fill(it, "const/4 v0, 0x0")
}.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)