Compare commits

..

47 Commits

Author SHA1 Message Date
semantic-release-bot
1526f98f53 chore(release): 2.1.0-dev.1 [skip ci]
# [2.1.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v2.0.0...v2.1.0-dev.1) (2024-02-15)

### Features

* Mention APK file name when logging aligning ([244ebc2](244ebc2186))
2024-02-15 03:46:55 +00:00
oSumAtrIX
244ebc2186 feat: Mention APK file name when logging aligning 2024-02-15 02:54:31 +01:00
oSumAtrIX
486a7acff2 ci: Create a GitHub release with changelogs 2024-02-15 02:29:58 +01:00
semantic-release-bot
f0b8bc5438 chore(release): 2.0.0 [skip ci]
# [2.0.0](https://github.com/ReVanced/revanced-library/compare/v1.5.0...v2.0.0) (2024-02-15)

### Bug Fixes

* Map dependencies from `KClass` into `String` to fix serialization ([57e36ab](57e36ab5c1))
* Use the JVM name instead of the value from `KClass#toString` ([d18e436](d18e436de1))

### Features

* Use `apkzlib` instead of own implementations and bump ReVanced Patcher ([3aa6dc2](3aa6dc223a))

### BREAKING CHANGES

* This commit removes deprecated APIs and bumps ReVanced Patcher. Because of it's changes, `apkzlib` is now used instead of own implementations of `ZipFile`
2024-02-15 01:24:23 +00:00
oSumAtrIX
806f45c77f chore: Merge branch dev to main (#24) 2024-02-15 02:22:42 +01:00
semantic-release-bot
259bcd04c9 chore(release): 2.0.0-dev.1 [skip ci]
# [2.0.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.5.1-dev.2...v2.0.0-dev.1) (2024-02-14)

### Features

* Use `apkzlib` instead of own implementations and bump ReVanced Patcher ([3aa6dc2](3aa6dc223a))

### BREAKING CHANGES

* This commit removes deprecated APIs and bumps ReVanced Patcher. Because of it's changes, `apkzlib` is now used instead of own implementations of `ZipFile`
2024-02-14 03:25:29 +00:00
oSumAtrIX
0d040dcfd0 chore: Fix builds 2024-02-14 04:23:49 +01:00
oSumAtrIX
13af960de1 chore: Dump API 2024-02-14 04:21:57 +01:00
oSumAtrIX
d21fd5798c build: Bump Gradle 2024-02-14 02:51:09 +01:00
oSumAtrIX
1c348f3621 build: Bump dependencies 2024-02-14 02:51:09 +01:00
oSumAtrIX
0c02680b70 build: Publish to GitHub Packages
Because Jitpack can not sign artifacts.
2024-02-14 02:51:09 +01:00
oSumAtrIX
b86b8aacea build: Sign publication artifacts 2024-02-14 02:51:08 +01:00
oSumAtrIX
3aa6dc223a feat: Use apkzlib instead of own implementations and bump ReVanced Patcher
BREAKING CHANGE: This commit removes deprecated APIs and bumps ReVanced Patcher. Because of it's changes, `apkzlib` is now used instead of own implementations of `ZipFile`
2024-02-14 02:31:44 +01:00
semantic-release-bot
c664a6eaed chore(release): 1.5.1-dev.2 [skip ci]
## [1.5.1-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.5.1-dev.1...v1.5.1-dev.2) (2024-02-08)

### Bug Fixes

* Use the JVM name instead of the value from `KClass#toString` ([d18e436](d18e436de1))
2024-02-08 02:51:47 +00:00
oSumAtrIX
d18e436de1 fix: Use the JVM name instead of the value from KClass#toString 2024-02-08 03:50:15 +01:00
semantic-release-bot
9ff06a2f1b chore(release): 1.5.1-dev.1 [skip ci]
## [1.5.1-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.5.0...v1.5.1-dev.1) (2024-02-08)

### Bug Fixes

* Map dependencies from `KClass` into `String` to fix serialization ([57e36ab](57e36ab5c1))
2024-02-08 02:10:11 +00:00
oSumAtrIX
57e36ab5c1 fix: Map dependencies from KClass into String to fix serialization 2024-02-08 03:08:10 +01:00
oSumAtrIX
8d934bde00 ci: Use latest Node.js LTS version to fix builds 2024-01-27 00:47:29 +01:00
oSumAtrIX
d1a7699256 ci: Add dependabot 2024-01-26 01:46:20 +01:00
oSumAtrIX
e08d0ad8f3 build: Bump dependencies 2024-01-26 01:46:20 +01:00
oSumAtrIX
4a14c5ccaa build: Bump dependencies 2024-01-10 09:32:58 +01:00
semantic-release-bot
4bbe2fa150 chore(release): 1.5.0 [skip ci]
# [1.5.0](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0) (2023-12-28)

### Features

* Add JSON de- and serialization of patches ([ecff6fe](ecff6fe0d3))
* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](9fda407441))
2023-12-28 21:35:25 +00:00
oSumAtrIX
938eac53b1 chore: Merge branch dev to main (#20) 2023-12-28 22:34:05 +01:00
Pun Butrach
ab004b91f0 docs: Update to latest branding changes (#23) 2023-12-10 13:31:20 +01:00
semantic-release-bot
b637e0d7d7 chore(release): 1.5.0-dev.2 [skip ci]
# [1.5.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.5.0-dev.1...v1.5.0-dev.2) (2023-12-07)

### Features

* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](9fda407441))
2023-12-07 15:41:07 +00:00
oSumAtrIX
9fda407441 feat: Improve mount reliability by unmounting existing mounts and killing running apps 2023-12-07 16:39:51 +01:00
semantic-release-bot
44f7c13a77 chore(release): 1.5.0-dev.1 [skip ci]
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0-dev.1) (2023-12-07)

### Features

* Add JSON de- and serialization of patches ([ecff6fe](ecff6fe0d3))
2023-12-07 14:16:31 +00:00
oSumAtrIX
ecff6fe0d3 feat: Add JSON de- and serialization of patches 2023-12-07 15:14:59 +01:00
oSumAtrIX
3515e331ac refactor: Simplify code 2023-12-07 02:45:49 +01:00
oSumAtrIX
a921c40306 build: Bump dependencies 2023-12-07 02:44:36 +01:00
oSumAtrIX
b6e1de5eaf chore: Use correct variable keyword 2023-12-07 02:38:52 +01:00
oSumAtrIX
bf5780e2f7 chore: Improve wording 2023-12-07 02:29:46 +01:00
oSumAtrIX
99e90c02ed refactor: Remove unused typealias 2023-12-01 23:06:29 +01:00
semantic-release-bot
ac3917fecc chore(release): 1.4.0 [skip ci]
# [1.4.0](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0) (2023-11-27)

### Bug Fixes

* Differentiate no package compatibility to any version compatibility ([762b7e3](762b7e3bc0))
* Sort the version maps by the most common version ([e4be6db](e4be6dbccd))

### Features

* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](c5f3536cbb))
* Allow getting most common compatible versions for all packages ([96845ba](96845ba265))
2023-11-27 21:50:58 +00:00
oSumAtrIX
98b7b347cb chore: Merge branch dev to main (#19) 2023-11-27 22:49:52 +01:00
semantic-release-bot
cdf94ffeca chore(release): 1.4.0-dev.2 [skip ci]
# [1.4.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2023-11-27)

### Bug Fixes

* Differentiate no package compatibility to any version compatibility ([762b7e3](762b7e3bc0))
* Sort the version maps by the most common version ([e4be6db](e4be6dbccd))

### Features

* Allow getting most common compatible versions for all packages ([96845ba](96845ba265))
2023-11-27 21:43:22 +00:00
oSumAtrIX
37434cf4a4 build: Bump dependencies 2023-11-27 22:40:07 +01:00
oSumAtrIX
73c97abedd chore: Lint code 2023-11-27 22:40:00 +01:00
oSumAtrIX
762b7e3bc0 fix: Differentiate no package compatibility to any version compatibility 2023-11-27 22:30:59 +01:00
oSumAtrIX
e4be6dbccd fix: Sort the version maps by the most common version 2023-11-27 21:21:25 +01:00
oSumAtrIX
96845ba265 feat: Allow getting most common compatible versions for all packages 2023-11-27 21:03:23 +01:00
semantic-release-bot
34171b534b chore(release): 1.4.0-dev.1 [skip ci]
# [1.4.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0-dev.1) (2023-11-27)

### Features

* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](c5f3536cbb))
2023-11-27 01:26:17 +00:00
oSumAtrIX
c5f3536cbb feat: Add PatchUtils#getMostCommonCompatibleVersions utility function 2023-11-27 02:24:55 +01:00
oSumAtrIX
893274074b refactor: Do not escape unnecessary 2023-11-27 02:24:55 +01:00
semantic-release-bot
b6c09d42ae chore(release): 1.3.0 [skip ci]
# [1.3.0](https://github.com/ReVanced/revanced-library/compare/v1.2.0...v1.3.0) (2023-11-26)

### Bug Fixes

* Add missing log when calling `UserAdbManager#install` ([90b612b](90b612bee8))
* Delete mount script ([4fe0fb0](4fe0fb0a61))

### Features

* Increase certainty of the possibility to mount ([10f8cd1](10f8cd1470))
* Select first Adb device, if none supplied automatically ([1a5f868](1a5f868ecd))
2023-11-26 04:35:19 +00:00
oSumAtrIX
fe8a2334e6 chore: Merge branch dev to main (#18) 2023-11-26 05:33:52 +01:00
oSumAtrIX
a9e5966145 chore: Lint code 2023-11-26 05:27:29 +01:00
27 changed files with 1542 additions and 5675 deletions

3
.editorconfig Normal file
View File

@@ -0,0 +1,3 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled

22
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: npm
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: gradle
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly

View File

@@ -1,4 +1,4 @@
name: Publish ReVanced Library name: Release
on: on:
workflow_dispatch: workflow_dispatch:
@@ -24,25 +24,30 @@ jobs:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
- name: Cache Node modules
uses: actions/cache@v3
with:
path: |
node_modules
key: npm-${{ hashFiles('package-lock.json') }}
- name: Cache Gradle - name: Cache Gradle
uses: burrunan/gradle-cache-action@v1 uses: burrunan/gradle-cache-action@v1
- name: Build - name: Build
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Cleaning is necessary to avoid uploading two identical artifacts with different versions run: ./gradlew build clean
run: ./gradlew clean --no-daemon
- name: Setup semantic-release - name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: 'npm'
- name: Install dependencies
run: npm install run: npm install
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ env.GPG_FINGERPRINT }}
- name: Release - name: Release
env: env:
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}

View File

@@ -32,6 +32,12 @@
backmergeBranches: [{"from": "main", "to": "dev"}], backmergeBranches: [{"from": "main", "to": "dev"}],
clearWorkspace: true clearWorkspace: true
} }
],
[
"@semantic-release/github",
{
successComment: false
}
] ]
] ]
} }

View File

@@ -1,3 +1,124 @@
# [2.1.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v2.0.0...v2.1.0-dev.1) (2024-02-15)
### Features
* Mention APK file name when logging aligning ([244ebc2](https://github.com/ReVanced/revanced-library/commit/244ebc21868c07d1852857f6858c1a53a5561155))
# [2.0.0](https://github.com/ReVanced/revanced-library/compare/v1.5.0...v2.0.0) (2024-02-15)
### Bug Fixes
* Map dependencies from `KClass` into `String` to fix serialization ([57e36ab](https://github.com/ReVanced/revanced-library/commit/57e36ab5c15a5fa7c50fb689ee43ad4eb9a4a515))
* Use the JVM name instead of the value from `KClass#toString` ([d18e436](https://github.com/ReVanced/revanced-library/commit/d18e436de1df14452ecaa7d827be5e6596ba8a2d))
### Features
* Use `apkzlib` instead of own implementations and bump ReVanced Patcher ([3aa6dc2](https://github.com/ReVanced/revanced-library/commit/3aa6dc223a9a1a2f735eda407917548ecbd366aa))
### BREAKING CHANGES
* This commit removes deprecated APIs and bumps ReVanced Patcher. Because of it's changes, `apkzlib` is now used instead of own implementations of `ZipFile`
# [2.0.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.5.1-dev.2...v2.0.0-dev.1) (2024-02-14)
### Features
* Use `apkzlib` instead of own implementations and bump ReVanced Patcher ([3aa6dc2](https://github.com/ReVanced/revanced-library/commit/3aa6dc223a9a1a2f735eda407917548ecbd366aa))
### BREAKING CHANGES
* This commit removes deprecated APIs and bumps ReVanced Patcher. Because of it's changes, `apkzlib` is now used instead of own implementations of `ZipFile`
## [1.5.1-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.5.1-dev.1...v1.5.1-dev.2) (2024-02-08)
### Bug Fixes
* Use the JVM name instead of the value from `KClass#toString` ([d18e436](https://github.com/ReVanced/revanced-library/commit/d18e436de1df14452ecaa7d827be5e6596ba8a2d))
## [1.5.1-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.5.0...v1.5.1-dev.1) (2024-02-08)
### Bug Fixes
* Map dependencies from `KClass` into `String` to fix serialization ([57e36ab](https://github.com/ReVanced/revanced-library/commit/57e36ab5c15a5fa7c50fb689ee43ad4eb9a4a515))
# [1.5.0](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0) (2023-12-28)
### Features
* Add JSON de- and serialization of patches ([ecff6fe](https://github.com/ReVanced/revanced-library/commit/ecff6fe0d3889d729a0badcfa28b89610bd27d48))
* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](https://github.com/ReVanced/revanced-library/commit/9fda40744173669c84b0c2599ae5ac5d39591798))
# [1.5.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.5.0-dev.1...v1.5.0-dev.2) (2023-12-07)
### Features
* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](https://github.com/ReVanced/revanced-library/commit/9fda40744173669c84b0c2599ae5ac5d39591798))
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0-dev.1) (2023-12-07)
### Features
* Add JSON de- and serialization of patches ([ecff6fe](https://github.com/ReVanced/revanced-library/commit/ecff6fe0d3889d729a0badcfa28b89610bd27d48))
# [1.4.0](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0) (2023-11-27)
### Bug Fixes
* Differentiate no package compatibility to any version compatibility ([762b7e3](https://github.com/ReVanced/revanced-library/commit/762b7e3bc01e2ca33dfcdbb1b5028d60ef6e0a48))
* Sort the version maps by the most common version ([e4be6db](https://github.com/ReVanced/revanced-library/commit/e4be6dbccd86700ffafe7cd8395e845bbd3d5138))
### Features
* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](https://github.com/ReVanced/revanced-library/commit/c5f3536cbb6997766076595dc0b2b5d2e861ca73))
* Allow getting most common compatible versions for all packages ([96845ba](https://github.com/ReVanced/revanced-library/commit/96845ba265e6dc208c7ac96f5e58734209cd1720))
# [1.4.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2023-11-27)
### Bug Fixes
* Differentiate no package compatibility to any version compatibility ([762b7e3](https://github.com/ReVanced/revanced-library/commit/762b7e3bc01e2ca33dfcdbb1b5028d60ef6e0a48))
* Sort the version maps by the most common version ([e4be6db](https://github.com/ReVanced/revanced-library/commit/e4be6dbccd86700ffafe7cd8395e845bbd3d5138))
### Features
* Allow getting most common compatible versions for all packages ([96845ba](https://github.com/ReVanced/revanced-library/commit/96845ba265e6dc208c7ac96f5e58734209cd1720))
# [1.4.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0-dev.1) (2023-11-27)
### Features
* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](https://github.com/ReVanced/revanced-library/commit/c5f3536cbb6997766076595dc0b2b5d2e861ca73))
# [1.3.0](https://github.com/ReVanced/revanced-library/compare/v1.2.0...v1.3.0) (2023-11-26)
### Bug Fixes
* Add missing log when calling `UserAdbManager#install` ([90b612b](https://github.com/ReVanced/revanced-library/commit/90b612bee8591c01b8befabde4147c7de7a2a09f))
* Delete mount script ([4fe0fb0](https://github.com/ReVanced/revanced-library/commit/4fe0fb0a617082b24199331671193e4fa7f485e2))
### Features
* Increase certainty of the possibility to mount ([10f8cd1](https://github.com/ReVanced/revanced-library/commit/10f8cd1470fd29cfefe53bf00a4a014f71a3f706))
* Select first Adb device, if none supplied automatically ([1a5f868](https://github.com/ReVanced/revanced-library/commit/1a5f868ecd0d278d574c12664ee95139c2423c17))
# [1.3.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.2.1-dev.1...v1.3.0-dev.1) (2023-11-26) # [1.3.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.2.1-dev.1...v1.3.0-dev.1) (2023-11-26)

View File

@@ -6,34 +6,53 @@
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg" srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
> >
<img <img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg" src="assets/revanced-headline/revanced-headline-vertical-light.svg"
> >
</picture> </picture>
<br> <br>
<a href="https://revanced.app/"> <a href="https://revanced.app/">
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" /> <picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp; </a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/revanced"> <a href="https://github.com/ReVanced">
<picture> <picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" /> <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" /> <img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture> </picture>
</a>&nbsp;&nbsp;&nbsp; </a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord"> <a href="http://revanced.app/discord">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" /> <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>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp"> <a href="https://reddit.com/r/revancedapp">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" /> <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>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced"> <a href="https://t.me/app_revanced">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" /> <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>&nbsp;&nbsp;&nbsp;
<a href="https://twitter.com/revancedapp"> <a href="https://x.com/revancedapp">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032018-6da37214-7474-4641-a1da-7af7db3a31cd.png" /> <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>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced"> <a href="https://www.youtube.com/@ReVanced">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" /> <picture>
</a>&nbsp;&nbsp;&nbsp; <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>
<br> <br>
Continuing the legacy of Vanced Continuing the legacy of Vanced
@@ -54,7 +73,7 @@ This document describes how to contribute to ReVanced Library.
Features can be requested by opening an issue using the Features can be requested by opening an issue using the
[Feature request issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+). [Feature request issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+).
> **Note** > [!NOTE]
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Library. > Requests can be accepted or rejected at the discretion of maintainers of ReVanced Library.
> Good motivation has to be provided for a request to be accepted. > Good motivation has to be provided for a request to be accepted.

View File

@@ -1,14 +1,13 @@
public final class app/revanced/library/ApkSigner { public final class app/revanced/library/ApkSigner {
public static final field INSTANCE Lapp/revanced/library/ApkSigner; public static final field INSTANCE Lapp/revanced/library/ApkSigner;
public final fun newApkSignerBuilder (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; public final fun newApkSigner (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer;
public final fun newApkSignerBuilder (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; public final fun newApkSigner (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$Signer;
public final fun newKeyStore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V public final fun newKeyStore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Set;)V
public final fun newKeyStore (Ljava/util/List;)Ljava/security/KeyStore; public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore;
public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;
public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/library/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/library/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;
public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;
public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore;
public final fun signApk (Lcom/android/apksig/ApkSigner$Builder;Ljava/io/File;Ljava/io/File;)V
} }
public final class app/revanced/library/ApkSigner$KeyStoreEntry { public final class app/revanced/library/ApkSigner$KeyStoreEntry {
@@ -25,10 +24,16 @@ public final class app/revanced/library/ApkSigner$PrivateKeyCertificatePair {
public final fun getPrivateKey ()Ljava/security/PrivateKey; public final fun getPrivateKey ()Ljava/security/PrivateKey;
} }
public final class app/revanced/library/ApkSigner$Signer {
public final fun getSigningExtension ()Lcom/android/tools/build/apkzlib/sign/SigningExtension;
public final fun signApk (Lcom/android/tools/build/apkzlib/zip/ZFile;)V
public final fun signApk (Ljava/io/File;)V
}
public final class app/revanced/library/ApkUtils { public final class app/revanced/library/ApkUtils {
public static final field INSTANCE Lapp/revanced/library/ApkUtils; public static final field INSTANCE Lapp/revanced/library/ApkUtils;
public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V public final fun applyTo (Lapp/revanced/patcher/PatcherResult;Ljava/io/File;)V
public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V public final fun sign (Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V
} }
public final class app/revanced/library/ApkUtils$SigningOptions { public final class app/revanced/library/ApkUtils$SigningOptions {
@@ -62,7 +67,49 @@ public final class app/revanced/library/Options$Patch$Option {
public final class app/revanced/library/PatchUtils { public final class app/revanced/library/PatchUtils {
public static final field INSTANCE Lapp/revanced/library/PatchUtils; public static final field INSTANCE Lapp/revanced/library/PatchUtils;
public final fun getMostCommonCompatibleVersion (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String; public final fun getMostCommonCompatibleVersions (Ljava/util/Set;Ljava/util/Set;Z)Ljava/util/Map;
public static synthetic fun getMostCommonCompatibleVersions$default (Lapp/revanced/library/PatchUtils;Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map;
}
public final class app/revanced/library/PatchUtils$Json {
public static final field INSTANCE Lapp/revanced/library/PatchUtils$Json;
public final fun deserialize (Ljava/io/InputStream;Ljava/lang/Class;)Ljava/util/Set;
public final fun serialize (Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;)V
public static synthetic fun serialize$default (Lapp/revanced/library/PatchUtils$Json;Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;ILjava/lang/Object;)V
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch : app/revanced/library/PatchUtils$Json$JsonPatch {
public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$Companion;
public final fun getCompatiblePackages ()Ljava/util/Set;
public final fun getDependencies ()Ljava/util/Set;
public final fun getDescription ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getOptions ()Ljava/util/Map;
public final fun getRequiresIntegrations ()Z
public final fun getUse ()Z
public final fun setRequiresIntegrations (Z)V
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$Companion {
public final fun fromPatch (Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch;
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption {
public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion;
public final fun getDefault ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String;
public final fun getKey ()Ljava/lang/String;
public final fun getRequired ()Z
public final fun getTitle ()Ljava/lang/String;
public final fun getValueType ()Ljava/lang/String;
public final fun getValues ()Ljava/util/Map;
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion {
public final fun fromPatchOption (Lapp/revanced/patcher/patch/options/PatchOption;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption;
}
public abstract interface class app/revanced/library/PatchUtils$Json$JsonPatch {
} }
public abstract class app/revanced/library/adb/AdbManager { public abstract class app/revanced/library/adb/AdbManager {
@@ -87,6 +134,7 @@ public final class app/revanced/library/adb/AdbManager$Companion {
} }
public final class app/revanced/library/adb/AdbManager$DeviceNotFoundException : java/lang/Exception { public final class app/revanced/library/adb/AdbManager$DeviceNotFoundException : java/lang/Exception {
public fun <init> ()V
} }
public final class app/revanced/library/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception { public final class app/revanced/library/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception {
@@ -118,23 +166,3 @@ public final class app/revanced/library/logging/Logger {
public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V
} }
public final class app/revanced/library/zip/ZipFile : java/io/Closeable {
public static final field ApkZipFile Lapp/revanced/library/zip/ZipFile$ApkZipFile;
public fun <init> (Ljava/io/File;)V
public final fun addEntryCompressData (Lapp/revanced/library/zip/structures/ZipEntry;[B)V
public fun close ()V
public final fun copyEntriesFromFileAligned (Lapp/revanced/library/zip/ZipFile;Lkotlin/jvm/functions/Function1;)V
}
public final class app/revanced/library/zip/ZipFile$ApkZipFile {
public final fun getApkZipEntryAlignment ()Lkotlin/jvm/functions/Function1;
}
public final class app/revanced/library/zip/structures/ZipEntry {
public static final field Companion Lapp/revanced/library/zip/structures/ZipEntry$Companion;
public fun <init> (Ljava/lang/String;)V
}
public final class app/revanced/library/zip/structures/ZipEntry$Companion {
}

View File

@@ -1,7 +1,8 @@
plugins { plugins {
kotlin("jvm") version "1.9.10" alias(libs.plugins.kotlin)
alias(libs.plugins.binary.compatibility.validator) alias(libs.plugins.binary.compatibility.validator)
`maven-publish` `maven-publish`
signing
} }
group = "app.revanced" group = "app.revanced"
@@ -9,17 +10,25 @@ group = "app.revanced"
repositories { repositories {
mavenCentral() mavenCentral()
mavenLocal() mavenLocal()
maven { url = uri("https://jitpack.io") }
google() google()
maven {
// A repository must be speficied for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials {
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
}
}
} }
dependencies { dependencies {
implementation(libs.revanced.patcher) implementation(libs.revanced.patcher)
implementation(libs.kotlin.reflect) implementation(libs.kotlin.reflect)
implementation(libs.jadb) // Updated fork implementation(libs.jadb) // Fork with Shell v2 support.
implementation(libs.apksig)
implementation(libs.bcpkix.jdk18on)
implementation(libs.jackson.module.kotlin) implementation(libs.jackson.module.kotlin)
implementation(libs.apkzlib)
implementation(libs.bcpkix.jdk15on)
implementation(libs.guava)
testImplementation(libs.revanced.patcher) testImplementation(libs.revanced.patcher)
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)
@@ -41,8 +50,19 @@ java {
} }
publishing { publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-library")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications { publications {
create<MavenPublication>("gpr") { create<MavenPublication>("revanced-library-publication") {
from(components["java"]) from(components["java"])
version = project.version.toString() version = project.version.toString()
@@ -76,3 +96,8 @@ publishing {
} }
} }
} }
signing {
useGpgCmd()
sign(publishing.publications["revanced-library-publication"])
}

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 = 1.3.0-dev.1 version = 2.1.0-dev.1

View File

@@ -1,21 +1,23 @@
[versions] [versions]
apksig = "8.1.4" jackson-module-kotlin = "2.15.0"
bcpkix-jdk18on = "1.76"
jackson-module-kotlin = "2.14.3"
jadb = "1.2.1" jadb = "1.2.1"
kotlin-reflect = "1.9.10" kotlin = "1.9.22"
kotlin-test = "1.9.10" revanced-patcher = "19.3.1"
revanced-patcher = "19.0.0" binary-compatibility-validator = "0.14.0"
binary-compatibility-validator = "0.13.2" apkzlib = "8.2.2"
bcpkix-jdk15on = "1.70"
guava = "33.0.0-jre"
[libraries] [libraries]
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcpkix-jdk18on" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" }
jadb = { module = "app.revanced:jadb", version.ref = "jadb" } jadb = { module = "app.revanced:jadb", version.ref = "jadb" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
apkzlib = { module = "com.android.tools.build:apkzlib", version.ref = "apkzlib" }
bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
[plugins] [plugins]
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dist zipStorePath=wrapper/dist

5433
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.2.1", "@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.8.0", "gradle-semantic-release-plugin": "^1.9.1",
"semantic-release": "^22.0.8" "semantic-release": "^23.0.2"
} }
} }

View File

@@ -1,11 +1,12 @@
package app.revanced.library package app.revanced.library
import com.android.apksig.ApkSigner import com.android.tools.build.apkzlib.sign.SigningExtension
import com.android.tools.build.apkzlib.sign.SigningOptions
import com.android.tools.build.apkzlib.zip.ZFile
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -19,34 +20,31 @@ import java.util.logging.Logger
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
/** /**
* Utility class for writing or reading keystore files and entries as well as signing APK files. * Utility class for reading or writing keystore files and entries as well as signing APK files.
*/ */
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
object ApkSigner { object ApkSigner {
private val logger = Logger.getLogger(app.revanced.library.ApkSigner::class.java.name) private val logger = Logger.getLogger(Signer::class.java.name)
init {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
Security.addProvider(BouncyCastleProvider())
}
/** /**
* Create a new [PrivateKeyCertificatePair]. * Create a new [PrivateKeyCertificatePair].
* *
* @param commonName The common name of the certificate. * @param commonName The common name of the certificate.
* @param validUntil The date until the certificate is valid. * @param validUntil The date until the certificate is valid.
*
* @return The created [PrivateKeyCertificatePair]. * @return The created [PrivateKeyCertificatePair].
*/ */
fun newPrivateKeyCertificatePair( fun newPrivateKeyCertificatePair(
commonName: String = "ReVanced", commonName: String = "ReVanced",
validUntil: Date = Date(System.currentTimeMillis() + 356.days.inWholeMilliseconds * 24) validUntil: Date = Date(System.currentTimeMillis() + 356.days.inWholeMilliseconds * 24),
): PrivateKeyCertificatePair { ): PrivateKeyCertificatePair {
logger.fine("Creating certificate for $commonName") logger.fine("Creating certificate for $commonName")
// Generate a new key pair. // Generate a new key pair.
val keyPair = KeyPairGenerator.getInstance("RSA").apply { val keyPair =
initialize(4096) KeyPairGenerator.getInstance("RSA").apply {
}.generateKeyPair() initialize(4096)
}.generateKeyPair()
var serialNumber: BigInteger var serialNumber: BigInteger
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) do serialNumber = BigInteger.valueOf(SecureRandom().nextLong())
@@ -55,29 +53,31 @@ object ApkSigner {
val name = X500Name("CN=$commonName") val name = X500Name("CN=$commonName")
// Create a new certificate. // Create a new certificate.
val certificate = JcaX509CertificateConverter().getCertificate( val certificate =
X509v3CertificateBuilder( JcaX509CertificateConverter().getCertificate(
name, X509v3CertificateBuilder(
serialNumber, name,
Date(System.currentTimeMillis()), serialNumber,
validUntil, Date(System.currentTimeMillis()),
Locale.ENGLISH, validUntil,
name, Locale.ENGLISH,
SubjectPublicKeyInfo.getInstance(keyPair.public.encoded) name,
).build(JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private)) SubjectPublicKeyInfo.getInstance(keyPair.public.encoded),
) ).build(JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private)),
)
return PrivateKeyCertificatePair(keyPair.private, certificate) return PrivateKeyCertificatePair(keyPair.private, certificate)
} }
/** /**
* Read a [PrivateKeyCertificatePair] from a keystore entry. * Read a [PrivateKeyCertificatePair] from a keystore entry.
* *
* @param keyStore The keystore to read the entry from. * @param keyStore The keystore to read the entry from.
* @param keyStoreEntryAlias The alias of the key store entry to read. * @param keyStoreEntryAlias The alias of the key store entry to read.
* @param keyStoreEntryPassword The password for recovering the signing key. * @param keyStoreEntryPassword The password for recovering the signing key.
*
* @return The read [PrivateKeyCertificatePair]. * @return The read [PrivateKeyCertificatePair].
*
* @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid. * @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid.
*/ */
fun readKeyCertificatePair( fun readKeyCertificatePair(
@@ -87,16 +87,18 @@ object ApkSigner {
): PrivateKeyCertificatePair { ): PrivateKeyCertificatePair {
logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias") logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias")
if (!keyStore.containsAlias(keyStoreEntryAlias)) if (!keyStore.containsAlias(keyStoreEntryAlias)) {
throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias") throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias")
}
// Read the private key and certificate from the keystore. // Read the private key and certificate from the keystore.
val privateKey = try { val privateKey =
keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey try {
} catch (exception: UnrecoverableKeyException) { keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey
throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias") } catch (exception: UnrecoverableKeyException) {
} throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias")
}
val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate
@@ -107,15 +109,15 @@ object ApkSigner {
* Create a new keystore with a new keypair. * Create a new keystore with a new keypair.
* *
* @param entries The entries to add to the keystore. * @param entries The entries to add to the keystore.
*
* @return The created keystore. * @return The created keystore.
*
* @see KeyStoreEntry * @see KeyStoreEntry
*/ */
fun newKeyStore( fun newKeyStore(entries: Set<KeyStoreEntry>): KeyStore {
entries: List<KeyStoreEntry>
): KeyStore {
logger.fine("Creating keystore") logger.fine("Creating keystore")
return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { return KeyStore.getInstance(KeyStore.getDefaultType()).apply {
load(null) load(null)
entries.forEach { entry -> entries.forEach { entry ->
@@ -124,7 +126,7 @@ object ApkSigner {
entry.alias, entry.alias,
entry.privateKeyCertificatePair.privateKey, entry.privateKeyCertificatePair.privateKey,
entry.password.toCharArray(), entry.password.toCharArray(),
arrayOf(entry.privateKeyCertificatePair.certificate) arrayOf(entry.privateKeyCertificatePair.certificate),
) )
} }
} }
@@ -140,104 +142,81 @@ object ApkSigner {
fun newKeyStore( fun newKeyStore(
keyStoreOutputStream: OutputStream, keyStoreOutputStream: OutputStream,
keyStorePassword: String, keyStorePassword: String,
entries: List<KeyStoreEntry> entries: Set<KeyStoreEntry>,
) = newKeyStore(entries).store( ) = newKeyStore(entries).store(
keyStoreOutputStream, keyStoreOutputStream,
keyStorePassword.toCharArray() keyStorePassword.toCharArray(),
) // Save the keystore. )
/** /**
* Read a keystore from the given [keyStoreInputStream]. * Read a keystore from the given [keyStoreInputStream].
* *
* @param keyStoreInputStream The stream to read the keystore from. * @param keyStoreInputStream The stream to read the keystore from.
* @param keyStorePassword The password for the keystore. * @param keyStorePassword The password for the keystore.
*
* @return The keystore. * @return The keystore.
*
* @throws IllegalArgumentException If the keystore password is invalid. * @throws IllegalArgumentException If the keystore password is invalid.
*/ */
fun readKeyStore( fun readKeyStore(
keyStoreInputStream: InputStream, keyStoreInputStream: InputStream,
keyStorePassword: String? keyStorePassword: String?,
): KeyStore { ): KeyStore {
logger.fine("Reading keystore") logger.fine("Reading keystore")
return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { return KeyStore.getInstance(KeyStore.getDefaultType()).apply {
try { try {
load(keyStoreInputStream, keyStorePassword?.toCharArray()) load(keyStoreInputStream, keyStorePassword?.toCharArray())
} catch (exception: IOException) { } catch (exception: IOException) {
if (exception.cause is UnrecoverableKeyException) if (exception.cause is UnrecoverableKeyException) {
throw IllegalArgumentException("Invalid keystore password") throw IllegalArgumentException("Invalid keystore password")
else } else {
throw exception throw exception
}
} }
} }
} }
/** /**
* Create a new [ApkSigner.Builder]. * Create a new [Signer].
* *
* @param privateKeyCertificatePair The private key and certificate pair to use for signing. * @param privateKeyCertificatePair The private key and certificate pair to use for signing.
* @param signer The name of the signer. *
* @param createdBy The value for the `Created-By` attribute in the APK's manifest. * @return The new [Signer].
* @return The created [ApkSigner.Builder] instance. *
* @see PrivateKeyCertificatePair
* @see Signer
*/ */
fun newApkSignerBuilder( fun newApkSigner(privateKeyCertificatePair: PrivateKeyCertificatePair) =
privateKeyCertificatePair: PrivateKeyCertificatePair, Signer(
signer: String, SigningExtension(
createdBy: String SigningOptions.builder()
): ApkSigner.Builder { .setMinSdkVersion(21) // TODO: Extracting from the target APK would be ideal.
logger.fine( .setV1SigningEnabled(true)
"Creating new ApkSigner " + .setV2SigningEnabled(true)
"with $signer as signer and " + .setCertificates(privateKeyCertificatePair.certificate)
"$createdBy as Created-By attribute in the APK's manifest" .setKey(privateKeyCertificatePair.privateKey)
.build(),
),
) )
// Create the signer config.
val signerConfig = ApkSigner.SignerConfig.Builder(
signer,
privateKeyCertificatePair.privateKey,
listOf(privateKeyCertificatePair.certificate)
).build()
// Create the signer.
return ApkSigner.Builder(listOf(signerConfig)).apply {
setCreatedBy(createdBy)
}
}
/** /**
* Create a new [ApkSigner.Builder]. * Create a new [Signer].
* *
* @param keyStore The keystore to use for signing. * @param keyStore The keystore to use for signing.
* @param keyStoreEntryAlias The alias of the key store entry to use for signing. * @param keyStoreEntryAlias The alias of the key store entry to use for signing.
* @param keyStoreEntryPassword The password for recovering the signing key. * @param keyStoreEntryPassword The password for recovering the signing key.
* @param signer The name of the signer. *
* @param createdBy The value for the `Created-By` attribute in the APK's manifest. * @return The new [Signer].
* @return The created [ApkSigner.Builder] instance. *
* @see KeyStoreEntry * @see KeyStore
* @see PrivateKeyCertificatePair * @see Signer
* @see ApkSigner.Builder.setCreatedBy
* @see ApkSigner.Builder.signApk
*/ */
fun newApkSignerBuilder( fun newApkSigner(
keyStore: KeyStore, keyStore: KeyStore,
keyStoreEntryAlias: String, keyStoreEntryAlias: String,
keyStoreEntryPassword: String, keyStoreEntryPassword: String,
signer: String, ) = newApkSigner(readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword))
createdBy: String,
) = newApkSignerBuilder(
readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword),
signer,
createdBy
)
fun ApkSigner.Builder.signApk(input: File, output: File) {
logger.info("Signing ${input.name}")
setInputApk(input)
setOutputApk(output)
build().sign()
}
/** /**
* An entry in a keystore. * An entry in a keystore.
@@ -245,12 +224,13 @@ object ApkSigner {
* @param alias The alias of the entry. * @param alias The alias of the entry.
* @param password The password for recovering the signing key. * @param password The password for recovering the signing key.
* @param privateKeyCertificatePair The private key and certificate pair. * @param privateKeyCertificatePair The private key and certificate pair.
*
* @see PrivateKeyCertificatePair * @see PrivateKeyCertificatePair
*/ */
class KeyStoreEntry( class KeyStoreEntry(
val alias: String, val alias: String,
val password: String, val password: String,
val privateKeyCertificatePair: PrivateKeyCertificatePair = newPrivateKeyCertificatePair() val privateKeyCertificatePair: PrivateKeyCertificatePair = newPrivateKeyCertificatePair(),
) )
/** /**
@@ -263,4 +243,24 @@ object ApkSigner {
val privateKey: PrivateKey, val privateKey: PrivateKey,
val certificate: X509Certificate, val certificate: X509Certificate,
) )
class Signer internal constructor(val signingExtension: SigningExtension) {
/**
* Sign an APK file.
*
* @param apkFile The APK file to sign.
*/
fun signApk(apkFile: File) = ZFile.openReadWrite(apkFile).use { signApk(it) }
/**
* Sign an APK file.
*
* @param apkZFile The APK [ZFile] to sign.
*/
fun signApk(apkZFile: ZFile) {
logger.info("Signing ${apkZFile.file.name}")
signingExtension.register(apkZFile)
}
}
} }

View File

@@ -1,88 +1,124 @@
package app.revanced.library package app.revanced.library
import app.revanced.library.ApkSigner.signApk
import app.revanced.library.zip.ZipFile
import app.revanced.library.zip.structures.ZipEntry
import app.revanced.patcher.PatcherResult import app.revanced.patcher.PatcherResult
import com.android.tools.build.apkzlib.zip.AlignmentRules
import com.android.tools.build.apkzlib.zip.StoredEntry
import com.android.tools.build.apkzlib.zip.ZFile
import com.android.tools.build.apkzlib.zip.ZFileOptions
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.io.path.deleteIfExists
/** /**
* Utility functions for working with apks. * Utility functions to work with APK files.
*/ */
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
object ApkUtils { object ApkUtils {
private val logger = Logger.getLogger(ApkUtils::class.java.name) private val logger = Logger.getLogger(ApkUtils::class.java.name)
private const val LIBRARY_EXTENSION = ".so"
// Alignment for native libraries.
private const val LIBRARY_ALIGNMENT = 1024 * 4
// Alignment for all other files.
private const val DEFAULT_ALIGNMENT = 4
// Prefix for resources.
private const val RES_PREFIX = "res/"
private val zFileOptions =
ZFileOptions().setAlignmentRule(
AlignmentRules.compose(
AlignmentRules.constantForSuffix(LIBRARY_EXTENSION, LIBRARY_ALIGNMENT),
AlignmentRules.constant(DEFAULT_ALIGNMENT),
),
)
/** /**
* Creates a new apk from [apkFile] and [patchedEntriesSource] and writes it to [outputFile]. * Applies the [PatcherResult] to the given [apkFile].
* *
* @param apkFile The apk to copy entries from. * The order of operation is as follows:
* @param outputFile The apk to write the new entries to. * 1. Write patched dex files.
* @param patchedEntriesSource The result of the patcher to add the patched dex files and resources. * 2. Delete all resources in the target APK
* 3. Merge resources.apk compiled by AAPT.
* 4. Write raw resources.
* 5. Delete resources staged for deletion.
* 6. Realign the APK.
*
* @param apkFile The file to apply the patched files to.
*/ */
fun copyAligned(apkFile: File, outputFile: File, patchedEntriesSource: PatcherResult) { fun PatcherResult.applyTo(apkFile: File) {
logger.info("Aligning ${apkFile.name}") ZFile.openReadWrite(apkFile, zFileOptions).use { targetApkZFile ->
dexFiles.forEach { dexFile ->
outputFile.toPath().deleteIfExists() targetApkZFile.add(dexFile.name, dexFile.stream)
ZipFile(outputFile).use { file ->
patchedEntriesSource.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry(it.name), it.stream.readBytes()
)
} }
patchedEntriesSource.resourceFile?.let { resources?.let { resources ->
file.copyEntriesFromFileAligned( // Add resources compiled by AAPT.
ZipFile(it), ZipFile.apkZipEntryAlignment resources.resourcesApk?.let { resourcesApk ->
) ZFile.openReadOnly(resourcesApk).use { resourcesApkZFile ->
// Delete all resources in the target APK before merging the new ones.
// This is necessary because the resources.apk renames resources.
// So unless, the old resources are deleted, there will be orphaned resources in the APK.
// It is not necessary, but for the sake of cleanliness, it is done.
targetApkZFile.entries().filter { entry ->
entry.centralDirectoryHeader.name.startsWith(RES_PREFIX)
}.forEach(StoredEntry::delete)
targetApkZFile.mergeFrom(resourcesApkZFile) { false }
}
}
// Add resources not compiled by AAPT.
resources.otherResources?.let { otherResources ->
targetApkZFile.addAllRecursively(otherResources) { file ->
file.relativeTo(otherResources).invariantSeparatorsPath !in resources.doNotCompress
}
}
// Delete resources that were staged for deletion.
if (resources.deleteResources.isNotEmpty()) {
targetApkZFile.entries().filter { entry ->
resources.deleteResources.any { shouldDelete -> shouldDelete(entry.centralDirectoryHeader.name) }
}.forEach(StoredEntry::delete)
}
} }
// TODO: Do not compress result.doNotCompress logger.info("Aligning ${apkFile.name}")
// TODO: Fix copying resources that are not needed anymore. targetApkZFile.realign()
file.copyEntriesFromFileAligned(
ZipFile(apkFile), ZipFile.apkZipEntryAlignment logger.fine("Writing changes")
)
} }
} }
/** /**
* Signs the [apk] file and writes it to [output]. * Signs the apk file with the given options.
* *
* @param apk The apk to sign.
* @param output The apk to write the signed apk to.
* @param signingOptions The options to use for signing. * @param signingOptions The options to use for signing.
*/ */
fun sign( fun File.sign(signingOptions: SigningOptions) {
apk: File,
output: File,
signingOptions: SigningOptions,
) {
// Get the keystore from the file or create a new one. // Get the keystore from the file or create a new one.
val keyStore = if (signingOptions.keyStore.exists()) { val keyStore =
ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword) if (signingOptions.keyStore.exists()) {
} else { ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword ?: "")
val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password) } else {
val entries = setOf(ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password))
// Create a new keystore with a new keypair and saves it. // Create a new keystore with a new keypair and saves it.
ApkSigner.newKeyStore(listOf(entry)).also { keyStore -> ApkSigner.newKeyStore(entries).apply {
keyStore.store( store(
signingOptions.keyStore.outputStream(), signingOptions.keyStore.outputStream(),
signingOptions.keyStorePassword?.toCharArray() signingOptions.keyStorePassword?.toCharArray(),
) )
}
} }
}
ApkSigner.newApkSignerBuilder( ApkSigner.newApkSigner(
keyStore, keyStore,
signingOptions.alias, signingOptions.alias,
signingOptions.password, signingOptions.password,
signingOptions.signer, ).signApk(this)
signingOptions.signer
).signApk(apk, output)
} }
/** /**

View File

@@ -2,67 +2,69 @@
package app.revanced.library package app.revanced.library
import app.revanced.library.Options.Patch.Option import app.revanced.library.Options.Patch.Option
import app.revanced.patcher.PatchClass
import app.revanced.patcher.PatchSet import app.revanced.patcher.PatchSet
import app.revanced.patcher.patch.options.PatchOptionException import app.revanced.patcher.patch.options.PatchOptionException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
private typealias PatchList = List<PatchClass> @Suppress("unused")
object Options { object Options {
private val logger = Logger.getLogger(Options::class.java.name) private val logger = Logger.getLogger(Options::class.java.name)
private var mapper = jacksonObjectMapper() private val mapper = jacksonObjectMapper()
/** /**
* Serializes the options for the patches in the list. * Serializes the options for a set of patches.
* *
* @param patches The list of patches to serialize. * @param patches The set of patches to serialize.
* @param prettyPrint Whether to pretty print the JSON. * @param prettyPrint Whether to pretty print the JSON.
* @return The JSON string containing the options. * @return The JSON string containing the options.
*/ */
fun serialize(patches: PatchSet, prettyPrint: Boolean = false): String = patches fun serialize(
.filter { it.options.any() } patches: PatchSet,
.map { patch -> prettyPrint: Boolean = false,
Patch( ): String =
patch.name!!, patches
patch.options.values.map { option -> .filter { it.options.any() }
val optionValue = try { .map { patch ->
option.value Patch(
} catch (e: PatchOptionException) { patch.name!!,
logger.warning("Using default option value for the ${patch.name} patch: ${e.message}") patch.options.values.map { option ->
option.default val optionValue =
} try {
option.value
} catch (e: PatchOptionException) {
logger.warning("Using default option value for the ${patch.name} patch: ${e.message}")
option.default
}
Option(option.key, optionValue) Option(option.key, optionValue)
},
)
}
// See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7.
.distinctBy { it.patchName }
.let {
if (prettyPrint) {
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it)
} else {
mapper.writeValueAsString(it)
} }
) }
}
// See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7.
.distinctBy { it.patchName }
.let {
if (prettyPrint)
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it)
else
mapper.writeValueAsString(it)
}
/** /**
* Deserializes the options for the patches in the list. * Deserializes the options to a set of patches.
* *
* @param json The JSON string containing the options. * @param json The JSON string containing the options.
* @return The list of [Patch]s. * @return A set of [Patch]s.
* @see Patch * @see Patch
* @see PatchList
*/ */
fun deserialize(json: String): Array<Patch> = mapper.readValue(json, Array<Patch>::class.java) fun deserialize(json: String): Array<Patch> = mapper.readValue(json, Array<Patch>::class.java)
/** /**
* Sets the options for the patches in the list. * Sets the options for a set of patches.
* *
* @param json The JSON string containing the options. * @param json The JSON string containing the options.
*/ */
@@ -70,9 +72,10 @@ object Options {
filter { it.options.any() }.let { patches -> filter { it.options.any() }.let { patches ->
if (patches.isEmpty()) return if (patches.isEmpty()) return
val jsonPatches = deserialize(json).associate { val jsonPatches =
it.patchName to it.options.associate { option -> option.key to option.value } deserialize(json).associate {
} it.patchName to it.options.associate { option -> option.key to option.value }
}
patches.forEach { patch -> patches.forEach { patch ->
jsonPatches[patch.name]?.let { jsonPatchOptions -> jsonPatches[patch.name]?.let { jsonPatchOptions ->
@@ -89,7 +92,7 @@ object Options {
} }
/** /**
* Sets the options for the patches in the list. * Sets the options for a set of patches.
* *
* @param file The file containing the JSON string containing the options. * @param file The file containing the JSON string containing the options.
* @see setOptions * @see setOptions
@@ -104,9 +107,8 @@ object Options {
*/ */
class Patch internal constructor( class Patch internal constructor(
val patchName: String, val patchName: String,
val options: List<Option> val options: List<Option>,
) { ) {
/** /**
* Data class for patch option. * Data class for patch option.
* *

View File

@@ -1,6 +1,19 @@
package app.revanced.library package app.revanced.library
import app.revanced.patcher.PatchSet import app.revanced.patcher.PatchSet
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.options.PatchOption
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.InputStream
import java.io.OutputStream
import kotlin.reflect.jvm.jvmName
typealias PackageName = String
typealias Version = String
typealias Count = Int
typealias VersionMap = LinkedHashMap<Version, Count>
typealias PackageNameMap = Map<PackageName, VersionMap>
/** /**
* Utility functions for working with patches. * Utility functions for working with patches.
@@ -8,21 +21,149 @@ import app.revanced.patcher.PatchSet
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
object PatchUtils { object PatchUtils {
/** /**
* Get the version that is most common for [packageName] in the supplied set of [patches]. * Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version.
* *
* @param patches The set of patches to check. * @param patches The set of patches to check.
* @param packageName The name of the compatible package. * @param packageNames The names of the compatible packages to include. If null, all packages will be included.
* @return The most common version of. * @param countUnusedPatches Whether to count patches that are not used.
* @return A map of package names to a map of versions to their count.
*/ */
fun getMostCommonCompatibleVersion(patches: PatchSet, packageName: String) = patches fun getMostCommonCompatibleVersions(
.mapNotNull { patches: PatchSet,
// Map all patches to their compatible packages with version constraints. packageNames: Set<String>? = null,
it.compatiblePackages?.firstOrNull { compatiblePackage -> countUnusedPatches: Boolean = false,
compatiblePackage.name == packageName && compatiblePackage.versions?.isNotEmpty() == true ): PackageNameMap =
buildMap {
fun filterWantedPackages(compatiblePackages: Iterable<Patch.CompatiblePackage>): Iterable<Patch.CompatiblePackage> {
val wantedPackages = packageNames?.toHashSet() ?: return compatiblePackages
return compatiblePackages.filter { it.name in wantedPackages }
}
patches
.filter { it.use || countUnusedPatches }
.flatMap { it.compatiblePackages ?: emptyList() }
.let(::filterWantedPackages)
.forEach { compatiblePackage ->
if (compatiblePackage.versions?.isEmpty() == true) {
return@forEach
}
val versionMap = getOrPut(compatiblePackage.name) { linkedMapOf() }
compatiblePackage.versions?.let { versions ->
versions.forEach { version ->
versionMap[version] = versionMap.getOrDefault(version, 0) + 1
}
}
}
// Sort the version maps by the most common version.
forEach { (packageName, versionMap) ->
this[packageName] =
versionMap
.asIterable()
.sortedWith(compareByDescending { it.value })
.associate { it.key to it.value } as VersionMap
} }
} }
.flatMap { it.versions!! }
.groupingBy { it } object Json {
.eachCount() private val mapper = jacksonObjectMapper()
.maxByOrNull { it.value }?.key
/**
* Serializes a set of [Patch]es to a JSON string and writes it to an output stream.
*
* @param patches The set of [Patch]es to serialize.
* @param transform A function to transform the [Patch]es to [JsonPatch]es.
* @param prettyPrint Whether to pretty print the JSON.
* @param outputStream The output stream to write the JSON to.
*/
fun serialize(
patches: PatchSet,
transform: (Patch<*>) -> JsonPatch = { patch -> FullJsonPatch.fromPatch(patch) },
prettyPrint: Boolean = false,
outputStream: OutputStream,
) {
patches.map(transform).let { transformed ->
if (prettyPrint) {
mapper.writerWithDefaultPrettyPrinter().writeValue(outputStream, transformed)
} else {
mapper.writeValue(outputStream, transformed)
}
}
}
/**
* Deserializes a JSON string to a set of [FullJsonPatch]es from an input stream.
*
* @param inputStream The input stream to read the JSON from.
* @param jsonPatchElementClass The class of the [JsonPatch]es to deserialize.
* @return A set of [JsonPatch]es.
* @see FullJsonPatch
*/
fun <T : JsonPatch> deserialize(
inputStream: InputStream,
jsonPatchElementClass: Class<T>,
): Set<T> =
mapper.readValue(
inputStream,
mapper.typeFactory.constructCollectionType(Set::class.java, jsonPatchElementClass),
)
interface JsonPatch
/**
* A JSON representation of a [Patch].
* @see Patch
*/
class FullJsonPatch internal constructor(
val name: String?,
val description: String?,
val compatiblePackages: Set<Patch.CompatiblePackage>?,
val dependencies: Set<String>?,
val use: Boolean,
var requiresIntegrations: Boolean,
val options: Map<String, FullJsonPatchOption<*>>,
) : JsonPatch {
companion object {
fun fromPatch(patch: Patch<*>) =
FullJsonPatch(
patch.name,
patch.description,
patch.compatiblePackages,
buildSet { patch.dependencies?.forEach { add(it.jvmName) } },
patch.use,
patch.requiresIntegrations,
patch.options.mapValues { FullJsonPatchOption.fromPatchOption(it.value) },
)
}
/**
* A JSON representation of a [PatchOption].
* @see PatchOption
*/
class FullJsonPatchOption<T> internal constructor(
val key: String,
val default: T?,
val values: Map<String, T?>?,
val title: String?,
val description: String?,
val required: Boolean,
val valueType: String,
) {
companion object {
fun fromPatchOption(option: PatchOption<*>) =
FullJsonPatchOption(
option.key,
option.default,
option.values,
option.title,
option.description,
option.required,
option.valueType,
)
}
}
}
}
} }

View File

@@ -5,10 +5,11 @@ import app.revanced.library.adb.Constants.CREATE_DIR
import app.revanced.library.adb.Constants.DELETE import app.revanced.library.adb.Constants.DELETE
import app.revanced.library.adb.Constants.GET_INSTALLED_PATH import app.revanced.library.adb.Constants.GET_INSTALLED_PATH
import app.revanced.library.adb.Constants.INSTALLATION_PATH import app.revanced.library.adb.Constants.INSTALLATION_PATH
import app.revanced.library.adb.Constants.INSTALL_MOUNT import app.revanced.library.adb.Constants.INSTALL_MOUNT_SCRIPT
import app.revanced.library.adb.Constants.INSTALL_PATCHED_APK import app.revanced.library.adb.Constants.INSTALL_PATCHED_APK
import app.revanced.library.adb.Constants.MOUNT_PATH import app.revanced.library.adb.Constants.KILL
import app.revanced.library.adb.Constants.MOUNT_SCRIPT import app.revanced.library.adb.Constants.MOUNT_SCRIPT
import app.revanced.library.adb.Constants.MOUNT_SCRIPT_PATH
import app.revanced.library.adb.Constants.PATCHED_APK_PATH import app.revanced.library.adb.Constants.PATCHED_APK_PATH
import app.revanced.library.adb.Constants.PLACEHOLDER import app.revanced.library.adb.Constants.PLACEHOLDER
import app.revanced.library.adb.Constants.RESTART import app.revanced.library.adb.Constants.RESTART
@@ -29,15 +30,16 @@ import java.util.logging.Logger
sealed class AdbManager private constructor(deviceSerial: String?) { sealed class AdbManager private constructor(deviceSerial: String?) {
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name) protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
protected val device = with(JadbConnection().devices) { protected val device =
if (isEmpty()) throw DeviceNotFoundException() with(JadbConnection().devices) {
if (isEmpty()) throw DeviceNotFoundException()
deviceSerial?.let { deviceSerial?.let {
firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException(deviceSerial) firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException(deviceSerial)
} ?: first().also { } ?: first().also {
logger.warning("No device serial supplied. Using device with serial ${it.serial}") logger.warning("No device serial supplied. Using device with serial ${it.serial}")
} }
}!! }!!
init { init {
logger.fine("Connected to ${device.serial}") logger.fine("Connected to ${device.serial}")
@@ -70,8 +72,10 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
* @return The [AdbManager]. * @return The [AdbManager].
* @throws DeviceNotFoundException If the device can not be found. * @throws DeviceNotFoundException If the device can not be found.
*/ */
fun getAdbManager(deviceSerial: String? = null, root: Boolean = false): AdbManager = fun getAdbManager(
if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial) deviceSerial: String? = null,
root: Boolean = false,
): AdbManager = if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial)
} }
/** /**
@@ -101,9 +105,8 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement(packageName)) device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement(packageName))
device.run(INSTALL_MOUNT, packageName).waitFor() device.run(INSTALL_MOUNT_SCRIPT, packageName).waitFor()
device.run(UMOUNT, packageName).waitFor() // Sanity check. device.run(MOUNT_SCRIPT_PATH, packageName).waitFor()
device.run(MOUNT_PATH, packageName).waitFor()
device.run(RESTART, packageName) device.run(RESTART, packageName)
device.run(DELETE, TMP_PATH) device.run(DELETE, TMP_PATH)
@@ -115,15 +118,19 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
device.run(UMOUNT, packageName) device.run(UMOUNT, packageName)
device.run(DELETE.applyReplacement(PATCHED_APK_PATH), packageName) device.run(DELETE.applyReplacement(PATCHED_APK_PATH), packageName)
device.run(DELETE, MOUNT_PATH.applyReplacement(packageName)) device.run(DELETE, MOUNT_SCRIPT_PATH.applyReplacement(packageName))
device.run(DELETE, TMP_PATH) device.run(DELETE, TMP_PATH)
device.run(RESTART, packageName) device.run(KILL, packageName)
super.uninstall(packageName) super.uninstall(packageName)
} }
companion object Utils { companion object Utils {
private fun JadbDevice.run(command: String, with: String) = run(command.applyReplacement(with)) private fun JadbDevice.run(
command: String,
with: String,
) = run(command.applyReplacement(with))
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with) private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
} }
} }
@@ -162,9 +169,11 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
class Apk(val file: File, val packageName: String? = null) class Apk(val file: File, val packageName: String? = null)
class DeviceNotFoundException internal constructor(deviceSerial: String? = null) : class DeviceNotFoundException internal constructor(deviceSerial: String? = null) :
Exception(deviceSerial?.let { Exception(
"The device with the ADB device serial \"$deviceSerial\" can not be found" deviceSerial?.let {
} ?: "No ADB device found") "The device with the ADB device serial \"$deviceSerial\" can not be found"
} ?: "No ADB device found",
)
class FailedToFindInstalledPackageException internal constructor(packageName: String) : class FailedToFindInstalledPackageException internal constructor(packageName: String) :
Exception("Failed to find installed package \"$packageName\" because no activity was found") Exception("Failed to find installed package \"$packageName\" because no activity was found")

View File

@@ -5,8 +5,10 @@ import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder import se.vidstige.jadb.ShellProcessBuilder
import java.io.File import java.io.File
internal fun JadbDevice.buildCommand(
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { command: String,
su: Boolean = true,
): ShellProcessBuilder {
if (su) return shellProcessBuilder("su -c \'$command\'") if (su) return shellProcessBuilder("su -c \'$command\'")
val args = command.split(" ") as ArrayList<String> val args = command.split(" ") as ArrayList<String>
@@ -15,14 +17,19 @@ internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): Shell
return shellProcessBuilder(cmd, *args.toTypedArray()) return shellProcessBuilder(cmd, *args.toTypedArray())
} }
internal fun JadbDevice.run(command: String, su: Boolean = true) = internal fun JadbDevice.run(
this.buildCommand(command, su).start() command: String,
su: Boolean = true,
) = this.buildCommand(command, su).start()
internal fun JadbDevice.hasSu() = internal fun JadbDevice.hasSu() = this.run("whoami", true).waitFor() == 0
this.run("whoami", true).waitFor() == 0
internal fun JadbDevice.push(file: File, targetFilePath: String) = internal fun JadbDevice.push(
push(file, RemoteFile(targetFilePath)) file: File,
targetFilePath: String,
) = push(file, RemoteFile(targetFilePath))
internal fun JadbDevice.createFile(targetFile: String, content: String) = internal fun JadbDevice.createFile(
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) targetFile: String,
content: String,
) = push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))

View File

@@ -6,37 +6,45 @@ internal object Constants {
internal const val TMP_PATH = "/data/local/tmp/revanced.tmp" internal const val TMP_PATH = "/data/local/tmp/revanced.tmp"
internal const val INSTALLATION_PATH = "/data/adb/revanced/" internal const val INSTALLATION_PATH = "/data/adb/revanced/"
internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk" internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk"
internal const val MOUNT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" internal const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
internal const val DELETE = "rm -rf $PLACEHOLDER" internal const val DELETE = "rm -rf $PLACEHOLDER"
internal const val CREATE_DIR = "mkdir -p" internal const val CREATE_DIR = "mkdir -p"
internal const val RESTART = "am start -S $PLACEHOLDER" internal const val RESTART = "am start -S $PLACEHOLDER"
internal const val KILL = "am force-stop $PLACEHOLDER"
internal const val GET_INSTALLED_PATH = "pm path $PLACEHOLDER" internal const val GET_INSTALLED_PATH = "pm path $PLACEHOLDER"
internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " + internal const val INSTALL_PATCHED_APK =
"base_path=\"$PATCHED_APK_PATH\" && " +
"mv $TMP_PATH ${'$'}base_path && " + "mv $TMP_PATH ${'$'}base_path && " +
"chmod 644 ${'$'}base_path && " + "chmod 644 ${'$'}base_path && " +
"chown system:system ${'$'}base_path && " + "chown system:system ${'$'}base_path && " +
"chcon u:object_r:apk_data_file:s0 ${'$'}base_path" "chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
internal const val UMOUNT = internal const val UMOUNT =
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done" "grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d ' ' -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH" internal const val INSTALL_MOUNT_SCRIPT = "mv $TMP_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH"
internal val MOUNT_SCRIPT = internal val MOUNT_SCRIPT =
""" """
#!/system/bin/sh #!/system/bin/sh
MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin MAGISKTMP="$( magisk --path )" || MAGISKTMP=/sbin
MIRROR="${'$'}MAGISKTMP/.magisk/mirror" MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
until [ "${'$'}(getprop sys.boot_completed)" = 1 ]; do sleep 3; done until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done
until [ -d "/sdcard/Android" ]; do sleep 1; done until [ -d "/sdcard/Android" ]; do sleep 1; done
base_path="$PATCHED_APK_PATH" # Unmount any existing mount as a safety measure
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) $UMOUNT
chcon u:object_r:apk_data_file:s0 ${'$'}base_path base_path="$PATCHED_APK_PATH"
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path stock_path=$( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
# Kill the app to force it to restart the mounted APK in case it's already running
$KILL
""".trimIndent() """.trimIndent()
} }

View File

@@ -10,10 +10,11 @@ object Logger {
/** /**
* Rules for allowed loggers. * Rules for allowed loggers.
*/ */
private val allowedLoggersRules = arrayOf<String.() -> Boolean>( private val allowedLoggersRules =
{ startsWith("app.revanced") }, // ReVanced loggers. arrayOf<String.() -> Boolean>(
{ this == "" } // Logs warnings when compiling resources (Logger in class brut.util.OS). { startsWith("app.revanced") }, // ReVanced loggers.
) { this == "" }, // Logs warnings when compiling resources (Logger in class brut.util.OS).
)
private val rootLogger = java.util.logging.Logger.getLogger("") private val rootLogger = java.util.logging.Logger.getLogger("")
@@ -48,13 +49,14 @@ object Logger {
fun addHandler( fun addHandler(
publishHandler: (log: String, level: Level, loggerName: String?) -> Unit, publishHandler: (log: String, level: Level, loggerName: String?) -> Unit,
flushHandler: () -> Unit, flushHandler: () -> Unit,
closeHandler: () -> Unit closeHandler: () -> Unit,
) = object : Handler() { ) = object : Handler() {
override fun publish(record: LogRecord) = publishHandler( override fun publish(record: LogRecord) =
formatter.format(record), publishHandler(
record.level, formatter.format(record),
record.loggerName record.level,
) record.loggerName,
)
override fun flush() = flushHandler() override fun flush() = flushHandler()
@@ -77,10 +79,11 @@ object Logger {
} }
log.toByteArray().let { log.toByteArray().let {
if (level.intValue() > Level.WARNING.intValue()) if (level.intValue() > Level.WARNING.intValue()) {
System.err.write(it) System.err.write(it)
else } else {
System.out.write(it) System.out.write(it)
}
} }
} }

View File

@@ -1,33 +0,0 @@
package app.revanced.library.zip
import java.io.DataInput
import java.io.DataOutput
import java.nio.ByteBuffer
internal fun UInt.toLittleEndian() =
(((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt()
internal fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
internal fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt()
internal fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
internal fun ByteBuffer.getUShort() = this.getShort().toUShort()
internal fun ByteBuffer.getUInt() = this.getInt().toUInt()
internal fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
internal fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
internal fun DataInput.readUShort() = this.readShort().toUShort()
internal fun DataInput.readUInt() = this.readInt().toUInt()
internal fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
internal fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
internal fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
internal fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
internal fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
internal fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())

View File

@@ -1,197 +0,0 @@
package app.revanced.library.zip
import app.revanced.library.zip.structures.ZipEndRecord
import app.revanced.library.zip.structures.ZipEntry
import java.io.Closeable
import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.zip.CRC32
import java.util.zip.Deflater
class ZipFile(file: File) : Closeable {
private var entries: MutableList<ZipEntry> = mutableListOf()
// Open file for writing if it doesn't exist (because the intention is to write) or is writable.
private val filePointer: RandomAccessFile = RandomAccessFile(
file,
if (!file.exists() || file.canWrite()) "rw" else "r"
)
private var centralDirectoryNeedsRewrite = false
private val compressionLevel = 5
init {
// If file isn't empty try to load entries.
if (file.length() > 0) {
val endRecord = findEndRecord()
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries)
throw IllegalArgumentException("Multi-file archives are not supported")
entries = readEntries(endRecord).toMutableList()
}
// Seek back to start for writing.
filePointer.seek(0)
}
private fun findEndRecord(): ZipEndRecord {
// Look from end to start since end record is at the end.
for (i in filePointer.length() - 1 downTo 0) {
filePointer.seek(i)
// Possible beginning of signature.
if (filePointer.readByte() == 0x50.toByte()) {
// Seek back to get the full int.
filePointer.seek(i)
val possibleSignature = filePointer.readUIntLE()
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
filePointer.seek(i)
return ZipEndRecord.fromECD(filePointer)
}
}
}
throw Exception("Couldn't find end record")
}
private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> {
filePointer.seek(endRecord.centralDirectoryStartOffset.toLong())
val numberOfEntries = endRecord.diskEntries.toInt()
return buildList(numberOfEntries) {
for (i in 1..numberOfEntries) {
add(
ZipEntry.fromCDE(filePointer).also
{
//for some reason the local extra field can be different from the central one
it.readLocalExtra(
filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
it.localHeaderOffset.toLong() + 28,
2
)
)
})
}
}
}
private fun writeCD() {
val centralDirectoryStartOffset = filePointer.channel.position().toUInt()
entries.forEach {
filePointer.channel.write(it.toCDE())
}
val entriesCount = entries.size.toUShort()
val endRecord = ZipEndRecord(
0u,
0u,
entriesCount,
entriesCount,
filePointer.channel.position().toUInt() - centralDirectoryStartOffset,
centralDirectoryStartOffset,
""
)
filePointer.channel.write(endRecord.toECD())
}
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
centralDirectoryNeedsRewrite = true
entry.localHeaderOffset = filePointer.channel.position().toUInt()
filePointer.channel.write(entry.toLFH())
filePointer.channel.write(data)
entries.add(entry)
}
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
val compressor = Deflater(compressionLevel, true)
compressor.setInput(data)
compressor.finish()
val uncompressedSize = data.size
val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger.
val compressedDataLength = compressor.deflate(compressedData)
val compressedBuffer =
ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray())
compressor.end()
val crc = CRC32()
crc.update(data)
entry.compression = 8u // Deflate compression.
entry.uncompressedSize = uncompressedSize.toUInt()
entry.compressedSize = compressedDataLength.toUInt()
entry.crc32 = crc.value.toUInt()
addEntry(entry, compressedBuffer)
}
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
alignment?.let {
// Calculate where data would end up.
val dataOffset = filePointer.filePointer + entry.LFHSize
val mod = dataOffset % alignment
// Wrong alignment.
if (mod != 0L) {
// Add padding at end of extra field.
entry.localExtraField =
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
}
}
addEntry(entry, data)
}
private fun getDataForEntry(entry: ZipEntry): ByteBuffer {
return filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
entry.dataOffset.toLong(),
entry.compressedSize.toLong()
)
}
/**
* Copies all entries from [file] to this file but skip already existing entries.
*
* @param file The file to copy entries from.
* @param entryAlignment A function that returns the alignment for a given entry.
*/
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates
val data = file.getDataForEntry(entry)
addEntryCopyData(entry, data, entryAlignment(entry))
}
}
override fun close() {
if (centralDirectoryNeedsRewrite) writeCD()
filePointer.close()
}
companion object ApkZipFile {
private const val DEFAULT_ALIGNMENT = 4
private const val LIBRARY_ALIGNMENT = 4096
val apkZipEntryAlignment = { entry: ZipEntry ->
if (entry.compression.toUInt() != 0u) null
else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT
else DEFAULT_ALIGNMENT
}
}
}

View File

@@ -1,77 +0,0 @@
package app.revanced.library.zip.structures
import app.revanced.library.zip.putUInt
import app.revanced.library.zip.putUShort
import app.revanced.library.zip.readUIntLE
import app.revanced.library.zip.readUShortLE
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
internal class ZipEndRecord(
val diskNumber: UShort,
val startingDiskNumber: UShort,
val diskEntries: UShort,
val totalEntries: UShort,
val centralDirectorySize: UInt,
val centralDirectoryStartOffset: UInt,
val fileComment: String,
) {
companion object {
const val ECD_HEADER_SIZE = 22
const val ECD_SIGNATURE = 0x06054b50u
fun fromECD(input: DataInput): ZipEndRecord {
val signature = input.readUIntLE()
if (signature != ECD_SIGNATURE)
throw IllegalArgumentException("Input doesn't start with end record signature")
val diskNumber = input.readUShortLE()
val startingDiskNumber = input.readUShortLE()
val diskEntries = input.readUShortLE()
val totalEntries = input.readUShortLE()
val centralDirectorySize = input.readUIntLE()
val centralDirectoryStartOffset = input.readUIntLE()
val fileCommentLength = input.readUShortLE()
var fileComment = ""
if (fileCommentLength > 0u) {
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
input.readFully(fileCommentBytes)
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
}
return ZipEndRecord(
diskNumber,
startingDiskNumber,
diskEntries,
totalEntries,
centralDirectorySize,
centralDirectoryStartOffset,
fileComment
)
}
}
fun toECD(): ByteBuffer {
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size).also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(ECD_SIGNATURE)
buffer.putUShort(diskNumber)
buffer.putUShort(startingDiskNumber)
buffer.putUShort(diskEntries)
buffer.putUShort(totalEntries)
buffer.putUInt(centralDirectorySize)
buffer.putUInt(centralDirectoryStartOffset)
buffer.putUShort(commentBytes.size.toUShort())
buffer.put(commentBytes)
buffer.flip()
return buffer
}
}

View File

@@ -1,187 +0,0 @@
package app.revanced.library.zip.structures
import app.revanced.library.zip.*
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
class ZipEntry private constructor(
internal val version: UShort,
internal val versionNeeded: UShort,
internal val flags: UShort,
internal var compression: UShort,
internal val modificationTime: UShort,
internal val modificationDate: UShort,
internal var crc32: UInt,
internal var compressedSize: UInt,
internal var uncompressedSize: UInt,
internal val diskNumber: UShort,
internal val internalAttributes: UShort,
internal val externalAttributes: UInt,
internal var localHeaderOffset: UInt,
internal val fileName: String,
internal val extraField: ByteArray,
internal val fileComment: String,
internal var localExtraField: ByteArray = ByteArray(0), //separate for alignment
) {
internal val LFHSize: Int
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
internal val dataOffset: UInt
get() = localHeaderOffset + LFHSize.toUInt()
constructor(fileName: String) : this(
0x1403u, //made by unix, version 20
0u,
0u,
0u,
0x0821u, //seems to be static time google uses, no idea
0x0221u, //same as above
0u,
0u,
0u,
0u,
0u,
0u,
0u,
fileName,
ByteArray(0),
""
)
companion object {
internal const val CDE_HEADER_SIZE = 46
internal const val CDE_SIGNATURE = 0x02014b50u
internal const val LFH_HEADER_SIZE = 30
internal const val LFH_SIGNATURE = 0x04034b50u
internal fun fromCDE(input: DataInput): ZipEntry {
val signature = input.readUIntLE()
if (signature != CDE_SIGNATURE)
throw IllegalArgumentException("Input doesn't start with central directory entry signature")
val version = input.readUShortLE()
val versionNeeded = input.readUShortLE()
var flags = input.readUShortLE()
val compression = input.readUShortLE()
val modificationTime = input.readUShortLE()
val modificationDate = input.readUShortLE()
val crc32 = input.readUIntLE()
val compressedSize = input.readUIntLE()
val uncompressedSize = input.readUIntLE()
val fileNameLength = input.readUShortLE()
var fileName = ""
val extraFieldLength = input.readUShortLE()
val extraField = ByteArray(extraFieldLength.toInt())
val fileCommentLength = input.readUShortLE()
var fileComment = ""
val diskNumber = input.readUShortLE()
val internalAttributes = input.readUShortLE()
val externalAttributes = input.readUIntLE()
val localHeaderOffset = input.readUIntLE()
val variableFieldsLength =
fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt()
if (variableFieldsLength > 0) {
val fileNameBytes = ByteArray(fileNameLength.toInt())
input.readFully(fileNameBytes)
fileName = fileNameBytes.toString(Charsets.UTF_8)
input.readFully(extraField)
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
input.readFully(fileCommentBytes)
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
}
flags = (flags and 0b1000u.inv()
.toUShort()) //disable data descriptor flag as they are not used
return ZipEntry(
version,
versionNeeded,
flags,
compression,
modificationTime,
modificationDate,
crc32,
compressedSize,
uncompressedSize,
diskNumber,
internalAttributes,
externalAttributes,
localHeaderOffset,
fileName,
extraField,
fileComment,
)
}
}
internal fun readLocalExtra(buffer: ByteBuffer) {
buffer.order(ByteOrder.LITTLE_ENDIAN)
localExtraField = ByteArray(buffer.getUShort().toInt())
}
internal fun toLFH(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(LFH_SIGNATURE)
buffer.putUShort(versionNeeded)
buffer.putUShort(flags)
buffer.putUShort(compression)
buffer.putUShort(modificationTime)
buffer.putUShort(modificationDate)
buffer.putUInt(crc32)
buffer.putUInt(compressedSize)
buffer.putUInt(uncompressedSize)
buffer.putUShort(nameBytes.size.toUShort())
buffer.putUShort(localExtraField.size.toUShort())
buffer.put(nameBytes)
buffer.put(localExtraField)
buffer.flip()
return buffer
}
internal fun toCDE(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
val buffer =
ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(CDE_SIGNATURE)
buffer.putUShort(version)
buffer.putUShort(versionNeeded)
buffer.putUShort(flags)
buffer.putUShort(compression)
buffer.putUShort(modificationTime)
buffer.putUShort(modificationDate)
buffer.putUInt(crc32)
buffer.putUInt(compressedSize)
buffer.putUInt(uncompressedSize)
buffer.putUShort(nameBytes.size.toUShort())
buffer.putUShort(extraField.size.toUShort())
buffer.putUShort(commentBytes.size.toUShort())
buffer.putUShort(diskNumber)
buffer.putUShort(internalAttributes)
buffer.putUInt(externalAttributes)
buffer.putUInt(localHeaderOffset)
buffer.put(nameBytes)
buffer.put(extraField)
buffer.put(commentBytes)
buffer.flip()
return buffer
}
}

View File

@@ -37,7 +37,7 @@ internal object PatchOptionsTest {
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]" "[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]"
@Patch("PatchOptionsTestPatch") @Patch("PatchOptionsTestPatch")
object PatchOptionsTestPatch : BytecodePatch() { object PatchOptionsTestPatch : BytecodePatch(emptySet()) {
var option1 by stringPatchOption("key1", null, null, "title1", "description1") var option1 by stringPatchOption("key1", null, null, "title1", "description1")
var option2 by booleanPatchOption("key2", true, null, "title2", "description2") var option2 by booleanPatchOption("key2", true, null, "title2", "description2")

View File

@@ -4,15 +4,116 @@ import app.revanced.patcher.PatchSet
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.intArrayPatchOption
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import kotlin.test.assertEquals import kotlin.test.assertEquals
internal object PatchUtilsTest { internal object PatchUtilsTest {
private val patches =
arrayOf(
newPatch("some.package", setOf("a")) { stringPatchOption("string", "value") },
newPatch("some.package", setOf("a", "b"), use = false),
newPatch("some.package", setOf("a", "b", "c"), use = false),
newPatch("some.other.package", setOf("b"), use = false),
newPatch("some.other.package", setOf("b", "c")) { booleanPatchOption("bool", true) },
newPatch("some.other.package", setOf("b", "c", "d")),
newPatch("some.other.other.package") { intArrayPatchOption("intArray", arrayOf(1, 2, 3)) },
newPatch("some.other.other.package", setOf("a")),
newPatch("some.other.other.package", setOf("b")),
newPatch("some.other.other.other.package", use = false),
newPatch("some.other.other.other.package", use = false),
).toSet()
@Test
fun `empty because package is incompatible with any version`() {
assertEqualsVersions(
expected = emptyMap(),
patches = setOf(newPatch("some.package", emptySet(), use = true)),
compatiblePackageNames = setOf("some.package"),
)
}
@Test
fun `empty list of versions because package is unconstrained to any version`() {
assertEqualsVersions(
expected = mapOf("some.package" to linkedMapOf()),
patches = setOf(newPatch("some.package")),
compatiblePackageNames = setOf("some.package"),
countUnusedPatches = true,
)
}
@Test
fun `empty because no known package was supplied`() {
assertEqualsVersions(
expected = emptyMap(),
patches,
compatiblePackageNames = setOf("unknown.package"),
)
}
@Test
fun `common versions correctly ordered for each package`() {
fun assertEqualsExpected(compatiblePackageNames: Set<String>?) =
assertEqualsVersions(
expected =
mapOf(
"some.package" to linkedMapOf("a" to 3, "b" to 2, "c" to 1),
"some.other.package" to linkedMapOf("b" to 3, "c" to 2, "d" to 1),
"some.other.other.package" to linkedMapOf("a" to 1, "b" to 1),
"some.other.other.other.package" to linkedMapOf(),
),
patches,
compatiblePackageNames,
countUnusedPatches = true,
)
assertEqualsExpected(
compatiblePackageNames =
setOf(
"some.package",
"some.other.package",
"some.other.other.package",
"some.other.other.other.package",
),
)
assertEqualsExpected(
compatiblePackageNames = null,
)
}
@Test
fun `common versions correctly ordered for each package without counting unused patches`() {
assertEqualsVersions(
expected =
mapOf(
"some.package" to linkedMapOf("a" to 1),
"some.other.package" to linkedMapOf("b" to 2, "c" to 2, "d" to 1),
"some.other.other.package" to linkedMapOf("a" to 1, "b" to 1),
),
patches,
compatiblePackageNames =
setOf(
"some.package",
"some.other.package",
"some.other.other.package",
"some.other.other.other.package",
),
countUnusedPatches = false,
)
}
@Test @Test
fun `return 'a' because it is the most common version`() { fun `return 'a' because it is the most common version`() {
val patches = arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d") val patches =
.map { version -> newPatch("some.package", version) } arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d")
.toSet() .map { version -> newPatch("some.package", setOf(version)) }
.toSet()
assertEqualsVersion("a", patches, "some.package") assertEqualsVersion("a", patches, "some.package")
} }
@@ -24,35 +125,75 @@ internal object PatchUtilsTest {
@Test @Test
fun `return null because no patch is compatible with the supplied package name`() { fun `return null because no patch is compatible with the supplied package name`() {
val patches = setOf(newPatch("some.package", "a")) val patches = setOf(newPatch("some.package", setOf("a")))
assertEqualsVersion(null, patches, "other.package") assertEqualsVersion(null, patches, "other.package")
} }
@Test @Test
fun `return null because no patch compatible package is constrained to a version`() { fun `return null because no compatible package is constrained to a version`() {
val patches = setOf( val patches =
newPatch("other.package"), setOf(
newPatch("other.package"), newPatch("other.package"),
) newPatch("other.package"),
)
assertEqualsVersion(null, patches, "other.package") assertEqualsVersion(null, patches, "other.package")
} }
@Test
fun `serializes to and deserializes from JSON string correctly`() {
val out = ByteArrayOutputStream()
PatchUtils.Json.serialize(patches, outputStream = out)
val deserialized =
PatchUtils.Json.deserialize(
ByteArrayInputStream(out.toByteArray()),
PatchUtils.Json.FullJsonPatch::class.java,
)
assert(patches.size == deserialized.size)
}
private fun assertEqualsVersions(
expected: PackageNameMap,
patches: PatchSet,
compatiblePackageNames: Set<String>?,
countUnusedPatches: Boolean = false,
) = assertEquals(
expected,
PatchUtils.getMostCommonCompatibleVersions(patches, compatiblePackageNames, countUnusedPatches),
)
private fun assertEqualsVersion( private fun assertEqualsVersion(
expected: String?, patches: PatchSet, compatiblePackageName: String expected: String?,
) = assertEquals(expected, PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName)) patches: PatchSet,
compatiblePackageName: String,
) {
assertEquals(
expected,
PatchUtils.getMostCommonCompatibleVersions(patches, setOf(compatiblePackageName))
.entries.firstOrNull()?.value?.keys?.firstOrNull(),
)
}
private fun newPatch(packageName: String, vararg versions: String) = object : BytecodePatch() { private fun newPatch(
packageName: String,
versions: Set<String>? = null,
use: Boolean = true,
options: Patch<*>.() -> Unit = {},
) = object : BytecodePatch(
name = "test",
compatiblePackages = setOf(CompatiblePackage(packageName, versions?.toSet())),
use = use,
) {
init { init {
// Set the compatible packages field to the supplied package name and versions reflectively, options()
// because the setter is private but needed for testing.
val compatiblePackagesField = Patch::class.java.getDeclaredField("compatiblePackages")
compatiblePackagesField.isAccessible = true
compatiblePackagesField.set(this, setOf(CompatiblePackage(packageName, versions.toSet())))
} }
override fun execute(context: BytecodeContext) {} override fun execute(context: BytecodeContext) {}
// Needed to make the patches unique.
override fun equals(other: Any?) = false
} }
} }