You've already forked revanced-cli
mirror of
https://github.com/revanced/revanced-cli
synced 2025-11-19 15:33:27 +01:00
Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5af2f10e14 | ||
|
|
40c8500e38 | ||
|
|
9add2f9e82 | ||
|
|
a942a57364 | ||
|
|
718df54bdc | ||
|
|
507b6f3409 | ||
|
|
c183c38f2a | ||
|
|
d9c5a179c5 | ||
|
|
6447311b0a | ||
|
|
2015c2a1dc | ||
|
|
99f123d1ca | ||
|
|
6ccbec6c95 | ||
|
|
9c30eac7c1 | ||
|
|
a26b0ea64d | ||
|
|
ab4c81965e | ||
|
|
eb83cabfff | ||
|
|
125fa06ca6 | ||
|
|
131100ef00 | ||
|
|
8e91c12c5e | ||
|
|
93a4787780 | ||
|
|
ac7c7a9a1a | ||
|
|
cd3ded1fbd | ||
|
|
bac8c67d6f | ||
|
|
387afd6bd5 | ||
|
|
ef2019ceba | ||
|
|
c1333d87f4 | ||
|
|
00fec2508a | ||
|
|
387b6bd016 | ||
|
|
5e7785fd8f | ||
|
|
19dfa2e2f1 | ||
|
|
0ddc2b54b7 | ||
|
|
3a51ce71a5 | ||
|
|
52b316150d | ||
|
|
75e9c49b72 | ||
|
|
31a05b6768 | ||
|
|
57a1e7c27f | ||
|
|
585d77ce80 | ||
|
|
b4d0ce52ea | ||
|
|
ee26a8d233 | ||
|
|
be3abdda30 | ||
|
|
1849d570f8 | ||
|
|
20fa17957e | ||
|
|
0d58ef14ae | ||
|
|
20e15defc2 | ||
|
|
9f91f63220 | ||
|
|
8782cdef67 | ||
|
|
58fa0774c4 | ||
|
|
dc5ff36058 | ||
|
|
27c28fab5e | ||
|
|
88852a45ac | ||
|
|
8dd9293cb9 | ||
|
|
499ce0a6fb | ||
|
|
27457e0c7d | ||
|
|
7418573c6c | ||
|
|
df85fa37ef | ||
|
|
04805e45fe | ||
|
|
61d3b99313 | ||
|
|
5b8537e6b7 | ||
|
|
7d8a61c3ba | ||
|
|
1dcdbc9fe9 | ||
|
|
4cc2f5269f | ||
|
|
46056956fe | ||
|
|
8e3d147690 | ||
|
|
87ffaa4bdb | ||
|
|
8a49dcc110 | ||
|
|
4a23cb69bc | ||
|
|
8c325af0f9 | ||
|
|
a26ab2a2c3 | ||
|
|
d335846202 | ||
|
|
6fc3ae67c8 | ||
|
|
0df936e99b | ||
|
|
83743929c8 | ||
|
|
51e091ce40 | ||
|
|
e5a37e0a5f | ||
|
|
c94471f464 | ||
|
|
bfd50a43b9 | ||
|
|
2c8a106151 | ||
|
|
ea7efd2afc | ||
|
|
6c4c1924ee | ||
|
|
ce78b245d1 | ||
|
|
d7cffea99c | ||
|
|
9331594706 | ||
|
|
07f6bdf330 | ||
|
|
a48c0860e3 | ||
|
|
4d5efab8bd | ||
|
|
b30c7375a7 | ||
|
|
f694542d64 | ||
|
|
6f54af5963 | ||
|
|
0a52180431 | ||
|
|
af32572f29 | ||
|
|
e126436f3d | ||
|
|
80c11fef73 | ||
|
|
debf0116fb | ||
|
|
29105bab3d | ||
|
|
31853fe539 | ||
|
|
21747d5552 | ||
|
|
ee6aff8fe7 | ||
|
|
f3a3e935a2 | ||
|
|
c272d55e2d | ||
|
|
1781612789 | ||
|
|
3829136c49 | ||
|
|
00145f2bb6 | ||
|
|
7dabd53109 | ||
|
|
98bd6f3f4b | ||
|
|
1a3db77c21 | ||
|
|
430de23856 | ||
|
|
c7d72c4d1c | ||
|
|
09b9027e5e | ||
|
|
3cc98efaa6 | ||
|
|
76da6c1fa6 | ||
|
|
b977d7039f | ||
|
|
75c3776498 | ||
|
|
6b9751448c | ||
|
|
496f821218 | ||
|
|
755f2b5afc | ||
|
|
d7353cd27a | ||
|
|
072d9e15d7 | ||
|
|
b768c06d90 | ||
|
|
055c282dd3 | ||
|
|
d1400548ef | ||
|
|
b25236c072 | ||
|
|
227d8d94c8 | ||
|
|
afff4c8418 | ||
|
|
c9716be205 | ||
|
|
6ceb449cf8 | ||
|
|
6c6abafe95 | ||
|
|
e1c7d1082a | ||
|
|
7444e4e67d | ||
|
|
48102c6607 | ||
|
|
70258e251c | ||
|
|
836892df7b | ||
|
|
ca8e1ba6af | ||
|
|
00e9e53df6 | ||
|
|
cdc4e9c8ac | ||
|
|
32589c88e4 | ||
|
|
3878532688 | ||
|
|
ee4a83bef9 | ||
|
|
775e2fd906 | ||
|
|
2975a47d0f | ||
|
|
0d7581ad75 | ||
|
|
d55ca86e5c | ||
|
|
2bfbbc2eb9 | ||
|
|
d1ceab45c8 | ||
|
|
cf1d512f4b | ||
|
|
22d2535af8 | ||
|
|
a0644c7045 | ||
|
|
ef01bb2016 | ||
|
|
6e82418958 | ||
|
|
9da4f707ac | ||
|
|
45171dd4b0 | ||
|
|
bd3773798d | ||
|
|
8665661ed7 | ||
|
|
1c5e5e249d | ||
|
|
1b645c67db | ||
|
|
4758289d68 | ||
|
|
87c0e05d9c | ||
|
|
54c0a03d44 | ||
|
|
bd185133bc | ||
|
|
ace70e417f | ||
|
|
d6f2609cfa | ||
|
|
735dbc9149 | ||
|
|
e756ac9920 | ||
|
|
a4ef47a285 | ||
|
|
c69170535e | ||
|
|
a1bcd1fdaf | ||
|
|
206f2029d7 | ||
|
|
04e0027c08 | ||
|
|
e76983e01c | ||
|
|
11d67bc1ea | ||
|
|
cabd32fda4 |
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report on the CLI. Do not submit suggestions for patches here.
|
||||
title: 'problem: some problem'
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 🐞 Issue
|
||||
|
||||
<!-- Describe your issue in detail here -->
|
||||
|
||||
## ⚙ Reproduce
|
||||
|
||||
<!-- Include your environment and steps to reproduce the issue as detailed as possible -->
|
||||
|
||||
## 🛠 Solution
|
||||
|
||||
<!-- If applicable, add a possible solution -->
|
||||
|
||||
## ⚠ Additional context
|
||||
|
||||
<!-- Add any other context about the problem here -->
|
||||
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a change to the CLI. Do not submit suggestions for patches here.
|
||||
title: 'feat: some feature'
|
||||
labels: feature-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 🐞 Issue
|
||||
|
||||
<!-- Describe your issue in detail here -->
|
||||
|
||||
## ⚙ Reproduce
|
||||
|
||||
<!-- Include your environment and steps to reproduce the issue as detailed as possible -->
|
||||
|
||||
## 🛠 Solution
|
||||
|
||||
<!-- If applicable, add a possible solution -->
|
||||
|
||||
## ⚠ Additional context
|
||||
|
||||
<!-- Add any other context about the problem here -->
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -27,12 +28,10 @@ jobs:
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build
|
||||
run: ./gradlew build clean
|
||||
- name: Setup semantic-release
|
||||
run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog gradle-semantic-release-plugin -D
|
||||
- name: Release
|
||||
|
||||
19
.releaserc
19
.releaserc
@@ -7,7 +7,11 @@
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
["@semantic-release/commit-analyzer", {
|
||||
"releaseRules": [
|
||||
{"type": "build", "release": "patch"}
|
||||
]
|
||||
}],
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
@@ -20,6 +24,15 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "build/libs/*all.jar"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
390
CHANGELOG.md
390
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,42 +1,36 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.6.21"
|
||||
kotlin("jvm") version "1.7.0"
|
||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||
java
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
|
||||
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") // DO NOT CHANGE!
|
||||
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
|
||||
username = githubUsername
|
||||
password = githubPassword
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.21")
|
||||
implementation("app.revanced:revanced-patcher:+")
|
||||
implementation("app.revanced:revanced-patches:+")
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(kotlin("reflect"))
|
||||
|
||||
implementation("app.revanced:revanced-patcher:2.4.0")
|
||||
implementation("info.picocli:picocli:4.6.3")
|
||||
|
||||
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
|
||||
implementation("com.android.tools.build:apksig:7.2.1")
|
||||
implementation("com.github.revanced:jadb:master-SNAPSHOT") // updated fork
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.21")
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
tasks {
|
||||
@@ -45,27 +39,17 @@ tasks {
|
||||
}
|
||||
shadowJar {
|
||||
manifest {
|
||||
attributes("Main-Class" to "app.revanced.cli.MainKt")
|
||||
attributes("Main-Class" to "app.revanced.cli.main.MainKt")
|
||||
attributes("Implementation-Title" to project.name)
|
||||
attributes("Implementation-Version" to project.version)
|
||||
}
|
||||
}
|
||||
// Dummy task to fix the Gradle semantic-release plugin.
|
||||
// Remove this if you forked it to support building only.
|
||||
// Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
|
||||
register<DefaultTask>("publish") {
|
||||
group = "publish"
|
||||
description = "Dummy task"
|
||||
dependsOn(build)
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-cli")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
publications {
|
||||
register<MavenPublication>("gpr") {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
kotlin.code.style = official
|
||||
version = 1.1.3
|
||||
version = 2.6.0
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package app.revanced.cli
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.extensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.util.patch.PatchLoader
|
||||
import app.revanced.utils.adb.Adb
|
||||
import app.revanced.utils.patcher.addPatchesFiltered
|
||||
import app.revanced.utils.signature.Signature
|
||||
import picocli.CommandLine.*
|
||||
import java.io.File
|
||||
|
||||
@Command(
|
||||
name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true
|
||||
)
|
||||
internal object MainCommand : Runnable {
|
||||
@Parameters(
|
||||
paramLabel = "INCLUDE",
|
||||
description = ["Which patches to include. If none is specified, all compatible patches will be included"]
|
||||
)
|
||||
internal var includedPatches = arrayOf<String>()
|
||||
|
||||
@Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"])
|
||||
internal var patchBundles = arrayOf<File>()
|
||||
|
||||
@Option(names = ["-t", "--temp-dir"], description = ["Temporal resource cache directory"], required = true)
|
||||
internal lateinit var cacheDirectory: String
|
||||
|
||||
@Option(names = ["-r", "--resource-patcher"], description = ["Enable patching resources"])
|
||||
internal var patchResources: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-c", "--clean"],
|
||||
description = ["Clean the temporal resource cache directory. This will be done anyways when running the patcher"]
|
||||
)
|
||||
internal var clean: Boolean = false
|
||||
|
||||
@Option(names = ["-l", "--list"], description = ["List patches only"])
|
||||
internal var listOnly: Boolean = false
|
||||
|
||||
@Option(names = ["-s", "--signature-checker"], description = ["Check signatures of all patches"])
|
||||
internal var signatureCheck: Boolean = false
|
||||
|
||||
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
|
||||
internal var mergeFiles = listOf<File>()
|
||||
|
||||
@Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true)
|
||||
internal lateinit var inputFile: File
|
||||
|
||||
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
|
||||
internal lateinit var outputPath: String
|
||||
|
||||
@Option(names = ["-d", "--deploy-on"], description = ["If specified, deploy to adb device with given name"])
|
||||
internal var deploy: String? = null
|
||||
|
||||
override fun run() {
|
||||
if (listOnly) {
|
||||
for (patchBundle in patchBundles)
|
||||
for (it in PatchLoader.loadFromFile(patchBundle))
|
||||
println(
|
||||
"[available] ${
|
||||
it.javaClass.findAnnotationRecursively(
|
||||
Name::class.java
|
||||
)?.name ?: Name::class.java.name
|
||||
}"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val patcher = app.revanced.patcher.Patcher(
|
||||
inputFile, cacheDirectory, patchResources
|
||||
)
|
||||
|
||||
if (signatureCheck) {
|
||||
patcher.addPatchesFiltered()
|
||||
Signature.checkSignatures(patcher)
|
||||
return
|
||||
}
|
||||
|
||||
val outputFile = File(outputPath)
|
||||
|
||||
var adb: Adb? = null
|
||||
deploy?.let {
|
||||
adb = Adb(
|
||||
outputFile, patcher.packageName, deploy!!
|
||||
)
|
||||
}
|
||||
|
||||
Patcher.start(patcher)
|
||||
|
||||
if (clean) File(cacheDirectory).deleteRecursively()
|
||||
|
||||
adb?.deploy()
|
||||
|
||||
if (clean) outputFile.delete()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package app.revanced.cli
|
||||
|
||||
import app.revanced.utils.filesystem.FileSystemUtils
|
||||
import app.revanced.utils.patcher.addPatchesFiltered
|
||||
import app.revanced.utils.patcher.applyPatchesPrint
|
||||
import app.revanced.utils.patcher.mergeFiles
|
||||
import app.revanced.utils.signing.Signer
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
|
||||
internal class Patcher {
|
||||
internal companion object {
|
||||
internal fun start(patcher: app.revanced.patcher.Patcher) {
|
||||
// merge files like necessary integrations
|
||||
patcher.mergeFiles()
|
||||
// add patches, but filter incompatible or excluded patches
|
||||
patcher.addPatchesFiltered(includeFilter = MainCommand.includedPatches.isNotEmpty())
|
||||
// apply patches
|
||||
patcher.applyPatchesPrint()
|
||||
|
||||
// write output file
|
||||
val outFile = File(MainCommand.outputPath)
|
||||
if (outFile.exists()) outFile.delete()
|
||||
MainCommand.inputFile.copyTo(outFile)
|
||||
|
||||
val zipFileSystem = FileSystemUtils(outFile)
|
||||
|
||||
// replace all dex files
|
||||
for ((name, data) in patcher.save()) {
|
||||
zipFileSystem.replaceFile(name, data.data)
|
||||
}
|
||||
|
||||
if (MainCommand.patchResources) {
|
||||
for (file in File(MainCommand.cacheDirectory).resolve("build/").listFiles(FileFilter { it.isDirectory })?.first()?.listFiles()!!) {
|
||||
if (!file.isDirectory) {
|
||||
zipFileSystem.replaceFile(file.name, file.readBytes())
|
||||
continue
|
||||
}
|
||||
zipFileSystem.replaceDirectory(file)
|
||||
}
|
||||
}
|
||||
|
||||
// finally close the stream
|
||||
zipFileSystem.close()
|
||||
|
||||
// and sign the apk file
|
||||
Signer.signApk(outFile)
|
||||
|
||||
println("[done]")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
219
src/main/kotlin/app/revanced/cli/command/MainCommand.kt
Normal file
219
src/main/kotlin/app/revanced/cli/command/MainCommand.kt
Normal file
@@ -0,0 +1,219 @@
|
||||
package app.revanced.cli.command
|
||||
|
||||
import app.revanced.cli.logging.impl.DefaultCliLogger
|
||||
import app.revanced.cli.patcher.Patcher
|
||||
import app.revanced.cli.patcher.logging.impl.PatcherLogger
|
||||
import app.revanced.cli.signing.Signing
|
||||
import app.revanced.cli.signing.SigningOptions
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
import app.revanced.patcher.extensions.PatchExtensions.description
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.util.patch.implementation.JarPatchBundle
|
||||
import app.revanced.utils.adb.Adb
|
||||
import picocli.CommandLine.*
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
private class CLIVersionProvider : IVersionProvider {
|
||||
override fun getVersion() = arrayOf(
|
||||
MainCommand::class.java.`package`.implementationVersion ?: "unknown"
|
||||
)
|
||||
}
|
||||
|
||||
@Command(
|
||||
name = "ReVanced-CLI",
|
||||
mixinStandardHelpOptions = true,
|
||||
versionProvider = CLIVersionProvider::class
|
||||
)
|
||||
internal object MainCommand : Runnable {
|
||||
val logger = DefaultCliLogger()
|
||||
|
||||
@ArgGroup(exclusive = false, multiplicity = "1")
|
||||
lateinit var args: Args
|
||||
|
||||
class Args {
|
||||
@Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true)
|
||||
lateinit var inputFile: File
|
||||
|
||||
@Option(names = ["--uninstall"], description = ["Uninstall the mount variant"])
|
||||
var uninstall: Boolean = false
|
||||
|
||||
@Option(names = ["-d", "--deploy-on"], description = ["If specified, deploy to adb device with given name"])
|
||||
var deploy: String? = null
|
||||
|
||||
@ArgGroup(exclusive = false)
|
||||
var sArgs: StartPatcherArgs? = null
|
||||
}
|
||||
|
||||
class StartPatcherArgs {
|
||||
@Option(names = ["-b", "--bundles"], description = ["One or more bundles of patches"], required = true)
|
||||
var patchBundles = arrayOf<String>()
|
||||
|
||||
@ArgGroup(exclusive = false)
|
||||
var lArgs: ListingArgs? = null
|
||||
|
||||
@ArgGroup(exclusive = false)
|
||||
var pArgs: PatchingArgs? = null
|
||||
}
|
||||
|
||||
class ListingArgs {
|
||||
@Option(names = ["-l", "--list"], description = ["List patches only"], required = true)
|
||||
var listOnly: Boolean = false
|
||||
|
||||
@Option(names = ["--with-versions"], description = ["List patches with compatible versions"])
|
||||
var withVersions: Boolean = false
|
||||
|
||||
@Option(names = ["--with-packages"], description = ["List patches with compatible packages"])
|
||||
var withPackages: Boolean = false
|
||||
|
||||
@Option(names = ["--with-descriptions"], description = ["List patches with their descriptions"])
|
||||
var withDescriptions: Boolean = true
|
||||
}
|
||||
|
||||
class PatchingArgs {
|
||||
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
|
||||
lateinit var outputPath: String
|
||||
|
||||
@Option(names = ["-e", "--exclude"], description = ["Explicitly exclude patches"])
|
||||
var excludedPatches = arrayOf<String>()
|
||||
|
||||
@Option(names = ["--exclusive"], description = ["Only installs the patches you include, not including any patch by default"])
|
||||
var defaultExclude = false
|
||||
|
||||
@Option(names = ["-i", "--include"], description = ["Include patches"])
|
||||
var includedPatches = arrayOf<String>()
|
||||
|
||||
@Option(names = ["-r", "--resource-patcher"], description = ["Disable patching resources"])
|
||||
var disableResourcePatching: Boolean = false
|
||||
|
||||
@Option(names = ["--experimental"], description = ["Disable patch version compatibility patch"])
|
||||
var experimental: Boolean = false
|
||||
|
||||
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
|
||||
var mergeFiles = listOf<File>()
|
||||
|
||||
@Option(names = ["--mount"], description = ["If specified, instead of installing, mount"])
|
||||
var mount: Boolean = false
|
||||
|
||||
@Option(names = ["--cn"], description = ["Overwrite the default CN for the signed file"])
|
||||
var cn = "ReVanced"
|
||||
|
||||
@Option(names = ["--keystore"], description = ["File path to your keystore"])
|
||||
var keystorePath: String? = null
|
||||
|
||||
@Option(names = ["-p", "--password"], description = ["Overwrite the default password for the signed file"])
|
||||
var password = "ReVanced"
|
||||
|
||||
@Option(names = ["-t", "--temp-dir"], description = ["Temporal resource cache directory"])
|
||||
var cacheDirectory = "revanced-cache"
|
||||
|
||||
@Option(
|
||||
names = ["-c", "--clean"],
|
||||
description = ["Clean the temporal resource cache directory. This will be done anyways when running the patcher"]
|
||||
)
|
||||
var clean: Boolean = false
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (args.sArgs?.lArgs?.listOnly == true) {
|
||||
printListOfPatches()
|
||||
return
|
||||
}
|
||||
|
||||
if (args.uninstall) {
|
||||
// temporarily get package name using Patcher method
|
||||
// fix: abstract options in patcher
|
||||
val patcher = app.revanced.patcher.Patcher(
|
||||
PatcherOptions(
|
||||
args.inputFile,
|
||||
"uninstaller-cache",
|
||||
false
|
||||
)
|
||||
)
|
||||
File("uninstaller-cache").deleteRecursively()
|
||||
|
||||
val adb: Adb? = args.deploy?.let {
|
||||
Adb(File("placeholder_file"), patcher.data.packageMetadata.packageName, args.deploy!!, false)
|
||||
}
|
||||
adb?.uninstall()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val _args = args
|
||||
val args = args.sArgs?.pArgs ?: return
|
||||
|
||||
val patcher = app.revanced.patcher.Patcher(
|
||||
PatcherOptions(
|
||||
_args.inputFile,
|
||||
args.cacheDirectory,
|
||||
!args.disableResourcePatching,
|
||||
logger = PatcherLogger
|
||||
)
|
||||
)
|
||||
|
||||
val outputFile = File(args.outputPath)
|
||||
|
||||
val adb: Adb? = _args.deploy?.let {
|
||||
Adb(outputFile, patcher.data.packageMetadata.packageName, _args.deploy!!, !args.mount)
|
||||
}
|
||||
val patchedFile = if (args.mount) outputFile
|
||||
else File(args.cacheDirectory).resolve("${outputFile.nameWithoutExtension}_raw.apk")
|
||||
|
||||
Patcher.start(patcher, patchedFile)
|
||||
|
||||
if (!args.mount) {
|
||||
Signing.start(
|
||||
patchedFile,
|
||||
outputFile,
|
||||
SigningOptions(
|
||||
args.cn,
|
||||
args.password,
|
||||
args.keystorePath ?: outputFile.absoluteFile.parentFile
|
||||
.resolve("${outputFile.nameWithoutExtension}.keystore")
|
||||
.canonicalPath
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (args.clean) File(args.cacheDirectory).deleteRecursively()
|
||||
|
||||
adb?.deploy()
|
||||
|
||||
if (args.clean && _args.deploy != null) Files.delete(outputFile.toPath())
|
||||
|
||||
logger.info("Finished")
|
||||
}
|
||||
|
||||
private fun printListOfPatches() {
|
||||
for (patchBundlePath in args.sArgs?.patchBundles!!) for (patch in JarPatchBundle(patchBundlePath).loadPatches()) {
|
||||
for (compatiblePackage in patch.compatiblePackages!!) {
|
||||
val packageEntryStr = buildString {
|
||||
// Add package if flag is set
|
||||
if (args.sArgs?.lArgs?.withPackages == true) {
|
||||
val packageName = compatiblePackage.name.substringAfterLast(".").padStart(10)
|
||||
append(packageName)
|
||||
append("\t")
|
||||
}
|
||||
// Add patch name
|
||||
val patchName = patch.patchName.padStart(25)
|
||||
append(patchName)
|
||||
// Add description if flag is set.
|
||||
if (args.sArgs?.lArgs?.withDescriptions == true) {
|
||||
append("\t")
|
||||
append(patch.description)
|
||||
}
|
||||
// Add compatible versions, if flag is set
|
||||
if (args.sArgs?.lArgs?.withVersions == true) {
|
||||
val compatibleVersions = compatiblePackage.versions.joinToString(separator = ", ")
|
||||
append("\t")
|
||||
append(compatibleVersions)
|
||||
}
|
||||
|
||||
}
|
||||
logger.info(packageEntryStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/main/kotlin/app/revanced/cli/logging/CliLogger.kt
Normal file
8
src/main/kotlin/app/revanced/cli/logging/CliLogger.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package app.revanced.cli.logging
|
||||
|
||||
internal interface CliLogger {
|
||||
fun error(msg: String)
|
||||
fun info(msg: String)
|
||||
fun trace(msg: String)
|
||||
fun warn(msg: String)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package app.revanced.cli.logging.impl
|
||||
|
||||
import app.revanced.cli.command.MainCommand
|
||||
import app.revanced.cli.logging.CliLogger
|
||||
import java.util.logging.Logger
|
||||
import java.util.logging.SimpleFormatter
|
||||
|
||||
internal class DefaultCliLogger(
|
||||
private val logger: Logger = Logger.getLogger(MainCommand::class.java.name),
|
||||
private val errorLogger: Logger = Logger.getLogger(logger.name + "Err")
|
||||
) : CliLogger {
|
||||
|
||||
init {
|
||||
logger.useParentHandlers = false
|
||||
if (logger.handlers.isEmpty()) {
|
||||
logger.addHandler(FlushingStreamHandler(System.out, SimpleFormatter()))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
|
||||
}
|
||||
}
|
||||
|
||||
override fun error(msg: String) = errorLogger.severe(msg)
|
||||
override fun info(msg: String) = logger.info(msg)
|
||||
override fun trace(msg: String) = logger.finest(msg)
|
||||
override fun warn(msg: String) = errorLogger.warning(msg)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.cli.logging.impl
|
||||
|
||||
import java.io.OutputStream
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.StreamHandler
|
||||
|
||||
internal class FlushingStreamHandler(out: OutputStream, format: Formatter) : StreamHandler(out, format) {
|
||||
override fun publish(record: LogRecord) {
|
||||
super.publish(record)
|
||||
flush()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.cli
|
||||
package app.revanced.cli.main
|
||||
|
||||
import app.revanced.cli.command.MainCommand
|
||||
import picocli.CommandLine
|
||||
|
||||
internal fun main(args: Array<String>) {
|
||||
48
src/main/kotlin/app/revanced/cli/patcher/Patcher.kt
Normal file
48
src/main/kotlin/app/revanced/cli/patcher/Patcher.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
package app.revanced.cli.patcher
|
||||
|
||||
import app.revanced.cli.command.MainCommand.args
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.utils.filesystem.ZipFileSystemUtils
|
||||
import app.revanced.utils.patcher.addPatchesFiltered
|
||||
import app.revanced.utils.patcher.applyPatchesVerbose
|
||||
import app.revanced.utils.patcher.mergeFiles
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
internal object Patcher {
|
||||
internal fun start(patcher: app.revanced.patcher.Patcher, output: File) {
|
||||
val inputFile = args.inputFile
|
||||
val args = args.sArgs?.pArgs!!
|
||||
|
||||
// merge files like necessary integrations
|
||||
patcher.mergeFiles()
|
||||
// add patches, but filter incompatible or excluded patches
|
||||
patcher.addPatchesFiltered()
|
||||
// apply patches
|
||||
patcher.applyPatchesVerbose()
|
||||
|
||||
// write output file
|
||||
if (output.exists()) Files.delete(output.toPath())
|
||||
inputFile.copyTo(output)
|
||||
|
||||
val result = patcher.save()
|
||||
ZipFileSystemUtils(output).use { outputFileSystem ->
|
||||
// replace all dex files
|
||||
result.dexFiles.forEach {
|
||||
logger.info("Writing dex file ${it.name}")
|
||||
outputFileSystem.write(it.name, it.dexFileInputStream.readAllBytes())
|
||||
}
|
||||
|
||||
if (!args.disableResourcePatching) {
|
||||
logger.info("Writing resources...")
|
||||
|
||||
ZipFileSystemUtils(result.resourceFile!!).use { resourceFileSystem ->
|
||||
val resourceFiles = resourceFileSystem.getFile(File.separator)
|
||||
outputFileSystem.writePathRecursively(resourceFiles)
|
||||
}
|
||||
}
|
||||
|
||||
result.doNotCompress?.let { outputFileSystem.uncompress(*it.toTypedArray()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.cli.patcher.logging.impl
|
||||
|
||||
import app.revanced.cli.logging.impl.DefaultCliLogger
|
||||
import java.util.logging.Logger
|
||||
|
||||
internal object PatcherLogger : app.revanced.patcher.logging.Logger{
|
||||
private val logger = DefaultCliLogger(Logger.getLogger(app.revanced.patcher.Patcher::class.java.name))
|
||||
|
||||
override fun error(msg: String) = logger.error(msg)
|
||||
override fun info(msg: String) = logger.info(msg)
|
||||
override fun warn(msg: String)= logger.warn(msg)
|
||||
override fun trace(msg: String)= logger.trace(msg)
|
||||
}
|
||||
28
src/main/kotlin/app/revanced/cli/signing/Signing.kt
Normal file
28
src/main/kotlin/app/revanced/cli/signing/Signing.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package app.revanced.cli.signing
|
||||
|
||||
import app.revanced.cli.command.MainCommand.args
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.utils.signing.Signer
|
||||
import app.revanced.utils.signing.align.ZipAligner
|
||||
import java.io.File
|
||||
|
||||
object Signing {
|
||||
fun start(inputFile: File, outputFile: File, signingOptions: SigningOptions) {
|
||||
val cacheDirectory = File(args.sArgs?.pArgs?.cacheDirectory)
|
||||
val alignedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_aligned.apk")
|
||||
val signedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_signed.apk")
|
||||
|
||||
// align the inputFile and write to alignedOutput
|
||||
logger.info("Aligning ${inputFile.name}")
|
||||
ZipAligner.align(inputFile, alignedOutput)
|
||||
// sign the alignedOutput and write to signedOutput
|
||||
// the reason is, in case the signer fails
|
||||
// it does not damage the output file
|
||||
logger.info("Signing ${alignedOutput.name}")
|
||||
Signer(signingOptions).signApk(alignedOutput, signedOutput)
|
||||
|
||||
// afterwards copy over the file to the output
|
||||
logger.info("Copying ${signedOutput.name} to ${outputFile.name}")
|
||||
signedOutput.copyTo(outputFile, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.revanced.cli.signing
|
||||
|
||||
data class SigningOptions(
|
||||
val cn: String,
|
||||
val password: String,
|
||||
val keyStoreFilePath: String
|
||||
)
|
||||
@@ -1,14 +1,17 @@
|
||||
package app.revanced.utils.adb
|
||||
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import se.vidstige.jadb.JadbConnection
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.managers.PackageManager
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
internal class Adb(
|
||||
private val apk: File,
|
||||
private val file: File,
|
||||
private val packageName: String,
|
||||
deviceName: String,
|
||||
private val modeInstall: Boolean = false,
|
||||
private val logging: Boolean = true
|
||||
) {
|
||||
private val device: JadbDevice
|
||||
@@ -17,49 +20,65 @@ internal class Adb(
|
||||
device = JadbConnection().devices.find { it.serial == deviceName }
|
||||
?: throw IllegalArgumentException("No such device with name $deviceName")
|
||||
|
||||
if (device.run("su -h", false) != 0)
|
||||
throw IllegalArgumentException("Root required on $deviceName. Deploying failed.")
|
||||
if (!modeInstall && device.run("su -h", false) != 0)
|
||||
throw IllegalArgumentException("Root required on $deviceName. Task failed")
|
||||
}
|
||||
|
||||
private fun String.replacePlaceholder(): String {
|
||||
return this.replace(Constants.PLACEHOLDER, packageName)
|
||||
private fun String.replacePlaceholder(with: String? = null): String {
|
||||
return this.replace(Constants.PLACEHOLDER, with ?: packageName)
|
||||
}
|
||||
|
||||
internal fun deploy() {
|
||||
// create revanced path
|
||||
device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}")
|
||||
if (modeInstall) {
|
||||
logger.info("Installing without mounting")
|
||||
|
||||
// push patched file
|
||||
device.copy(Constants.PATH_INIT_PUSH, apk)
|
||||
// install apk
|
||||
device.run(Constants.COMMAND_INSTALL_APK.replacePlaceholder())
|
||||
PackageManager(device).install(file)
|
||||
} else {
|
||||
logger.info("Installing by mounting")
|
||||
|
||||
// push mount script
|
||||
device.createFile(
|
||||
Constants.PATH_INIT_PUSH,
|
||||
Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder()
|
||||
)
|
||||
// install mount script
|
||||
device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder())
|
||||
// push patched file
|
||||
device.copy(Constants.PATH_INIT_PUSH, file)
|
||||
|
||||
// push umount script
|
||||
device.createFile(
|
||||
Constants.PATH_INIT_PUSH,
|
||||
Constants.CONTENT_UMOUNT_SCRIPT.replacePlaceholder()
|
||||
)
|
||||
// install mount script
|
||||
device.run(Constants.COMMAND_INSTALL_UMOUNT.replacePlaceholder())
|
||||
// create revanced folder path
|
||||
device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}")
|
||||
|
||||
// unmount the apk for sanity
|
||||
device.run(Constants.PATH_UMOUNT.replacePlaceholder())
|
||||
// mount the apk
|
||||
device.run(Constants.PATH_MOUNT.replacePlaceholder())
|
||||
// prepare mounting the apk
|
||||
device.run(Constants.COMMAND_PREPARE_MOUNT_APK.replacePlaceholder())
|
||||
|
||||
// relaunch app
|
||||
device.run(Constants.COMMAND_RESTART.replacePlaceholder())
|
||||
// push mount script
|
||||
device.createFile(
|
||||
Constants.PATH_INIT_PUSH,
|
||||
Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder()
|
||||
)
|
||||
// install mount script
|
||||
device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder())
|
||||
|
||||
// log the app
|
||||
log()
|
||||
// unmount the apk for sanity
|
||||
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
|
||||
// mount the apk
|
||||
device.run(Constants.PATH_MOUNT.replacePlaceholder())
|
||||
|
||||
// relaunch app
|
||||
device.run(Constants.COMMAND_RESTART.replacePlaceholder())
|
||||
|
||||
// log the app
|
||||
log()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun uninstall() {
|
||||
logger.info("Uninstalling by unmounting")
|
||||
|
||||
// unmount the apk
|
||||
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
|
||||
|
||||
// delete revanced app
|
||||
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_REVANCED_APP).replacePlaceholder())
|
||||
|
||||
// delete mount script
|
||||
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_MOUNT).replacePlaceholder())
|
||||
|
||||
logger.info("Finished uninstalling")
|
||||
}
|
||||
|
||||
private fun log() {
|
||||
@@ -84,11 +103,11 @@ internal class Adb(
|
||||
}
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("An error occurred while monitoring state of app", e)
|
||||
throw RuntimeException("An error occurred while monitoring the state of app", e)
|
||||
}
|
||||
}
|
||||
println("App closed, continuing.")
|
||||
logger.info("Stopped logging because the app was closed")
|
||||
process.destroy()
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,37 +15,31 @@ internal object Constants {
|
||||
private const val NAME_MOUNT_SCRIPT = "mount_revanced_$PLACEHOLDER.sh"
|
||||
|
||||
// initial directory to push files to via adb push
|
||||
internal const val PATH_INIT_PUSH = "/sdcard/revanced.delete"
|
||||
internal const val PATH_INIT_PUSH = "/data/local/tmp/revanced.delete"
|
||||
|
||||
// revanced path
|
||||
internal const val PATH_REVANCED = "/data/adb/revanced/"
|
||||
|
||||
// revanced apk path
|
||||
private const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
|
||||
internal const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
|
||||
|
||||
// (un)mount script paths
|
||||
// delete command
|
||||
internal const val COMMAND_DELETE = "rm -rf $PLACEHOLDER"
|
||||
|
||||
// mount script path
|
||||
internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT"
|
||||
internal const val PATH_UMOUNT = "/data/adb/post-fs-data.d/un$NAME_MOUNT_SCRIPT"
|
||||
|
||||
// move to revanced apk path & set permissions
|
||||
internal const val COMMAND_INSTALL_APK =
|
||||
internal const val COMMAND_PREPARE_MOUNT_APK =
|
||||
"base_path=\"$PATH_REVANCED_APP\" && mv $PATH_INIT_PUSH ${'$'}base_path && chmod 644 ${'$'}base_path && chown system:system ${'$'}base_path && chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
|
||||
|
||||
// unmount command
|
||||
internal const val COMMAND_UMOUNT =
|
||||
"stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) && umount -l ${'$'}stock_path"
|
||||
|
||||
// install mount script & set permissions
|
||||
internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT"
|
||||
|
||||
// install umount script & set permissions
|
||||
internal const val COMMAND_INSTALL_UMOUNT = "mv $PATH_INIT_PUSH $PATH_UMOUNT && $COMMAND_CHMOD_MOUNT $PATH_UMOUNT"
|
||||
|
||||
// unmount script
|
||||
internal val CONTENT_UMOUNT_SCRIPT =
|
||||
"""
|
||||
#!/system/bin/sh
|
||||
|
||||
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
||||
umount -l ${'$'}stock_path
|
||||
""".trimIndent()
|
||||
|
||||
// mount script
|
||||
internal val CONTENT_MOUNT_SCRIPT =
|
||||
"""
|
||||
@@ -54,6 +48,8 @@ internal object Constants {
|
||||
|
||||
base_path="$PATH_REVANCED_APP"
|
||||
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
||||
|
||||
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
||||
mount -o bind ${'$'}base_path ${'$'}stock_path
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package app.revanced.utils.filesystem
|
||||
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
|
||||
internal class FileSystemUtils(
|
||||
file: File
|
||||
) : Closeable {
|
||||
private var fileSystem: FileSystem
|
||||
|
||||
init {
|
||||
fileSystem = FileSystems.newFileSystem(file.toPath(), null as ClassLoader?)
|
||||
}
|
||||
|
||||
private fun deleteDirectory(dirPath: String) {
|
||||
val files = Files.walk(fileSystem.getPath("$dirPath/"))
|
||||
|
||||
files
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach {
|
||||
|
||||
Files.delete(it)
|
||||
}
|
||||
|
||||
files.close()
|
||||
}
|
||||
|
||||
|
||||
internal fun replaceDirectory(replacement: File) {
|
||||
if (!replacement.isDirectory) throw Exception("${replacement.name} is not a directory.")
|
||||
|
||||
// FIXME: make this delete the directory recursively
|
||||
//deleteDirectory(replacement.name)
|
||||
//val path = Files.createDirectory(fileSystem.getPath(replacement.name))
|
||||
|
||||
val excludeFromPath = replacement.path.removeSuffix(replacement.name)
|
||||
for (path in Files.walk(replacement.toPath())) {
|
||||
val file = path.toFile()
|
||||
if (file.isDirectory) {
|
||||
val relativePath = path.toString().removePrefix(excludeFromPath)
|
||||
val fileSystemPath = fileSystem.getPath(relativePath)
|
||||
if (!Files.exists(fileSystemPath)) Files.createDirectory(fileSystemPath)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
replaceFile(path.toString().removePrefix(excludeFromPath), file.readBytes())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun replaceFile(sourceFile: String, content: ByteArray) {
|
||||
val path = fileSystem.getPath(sourceFile)
|
||||
Files.deleteIfExists(path)
|
||||
Files.write(path, content)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
fileSystem.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package app.revanced.utils.filesystem
|
||||
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.zip.ZipEntry
|
||||
|
||||
internal class ZipFileSystemUtils(
|
||||
file: File
|
||||
) : Closeable {
|
||||
private var zipFileSystem = FileSystems.newFileSystem(file.toPath(), mapOf("noCompression" to true))
|
||||
|
||||
private fun Path.deleteRecursively() {
|
||||
if (!Files.exists(this)) {
|
||||
throw IllegalStateException("File exists in real folder but not in zip file system")
|
||||
}
|
||||
|
||||
if (Files.isDirectory(this)) {
|
||||
Files.list(this).forEach { path ->
|
||||
path.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
Files.delete(this)
|
||||
}
|
||||
|
||||
internal fun getFile(path: String) = zipFileSystem.getPath(path)
|
||||
|
||||
internal fun writePathRecursively(path: Path) {
|
||||
Files.list(path).use { fileStream ->
|
||||
fileStream.forEach { filePath ->
|
||||
val fileSystemPath = filePath.getRelativePath(path)
|
||||
fileSystemPath.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
Files.walk(path).use { fileStream ->
|
||||
// don't include build directory
|
||||
// by skipping the root node.
|
||||
fileStream.skip(1).forEach { filePath ->
|
||||
val relativePath = filePath.getRelativePath(path)
|
||||
|
||||
if (Files.isDirectory(filePath)) {
|
||||
Files.createDirectory(relativePath)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
Files.copy(filePath, relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun write(path: String, content: ByteArray) = Files.write(zipFileSystem.getPath(path), content)
|
||||
|
||||
private fun Path.getRelativePath(path: Path): Path = zipFileSystem.getPath(path.relativize(this).toString())
|
||||
|
||||
// TODO: figure out why the file system is uncompressed by default and how to fix it
|
||||
internal fun uncompress(vararg paths: String) =
|
||||
paths.forEach { Files.setAttribute(zipFileSystem.getPath(it), "zip:method", ZipEntry.STORED) }
|
||||
|
||||
override fun close() = zipFileSystem.close()
|
||||
}
|
||||
@@ -1,71 +1,78 @@
|
||||
package app.revanced.utils.patcher
|
||||
|
||||
import app.revanced.cli.MainCommand
|
||||
import app.revanced.cli.command.MainCommand
|
||||
import app.revanced.cli.command.MainCommand.args
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.annotation.Compatibility
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.data.base.Data
|
||||
import app.revanced.patcher.extensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import app.revanced.patcher.util.patch.PatchLoader
|
||||
import app.revanced.patcher.data.Data
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
import app.revanced.patcher.extensions.PatchExtensions.include
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.patch.implementation.JarPatchBundle
|
||||
|
||||
fun Patcher.addPatchesFiltered(
|
||||
packageCompatibilityFilter: Boolean = true,
|
||||
packageVersionCompatibilityFilter: Boolean = true,
|
||||
includeFilter: Boolean = false
|
||||
) {
|
||||
val packageName = this.packageName
|
||||
val packageVersion = this.packageVersion
|
||||
fun Patcher.addPatchesFiltered() {
|
||||
val packageName = this.data.packageMetadata.packageName
|
||||
val packageVersion = this.data.packageMetadata.packageVersion
|
||||
|
||||
MainCommand.patchBundles.forEach { bundle ->
|
||||
val includedPatches = mutableListOf<Patch<Data>>()
|
||||
PatchLoader.loadFromFile(bundle).forEach patch@{ p ->
|
||||
val patch = p.getDeclaredConstructor().newInstance()
|
||||
args.sArgs?.patchBundles!!.forEach { bundle ->
|
||||
val includedPatches = mutableListOf<Class<out Patch<Data>>>()
|
||||
JarPatchBundle(bundle).loadPatches().forEach patch@{ patch ->
|
||||
val compatiblePackages = patch.compatiblePackages
|
||||
val patchName = patch.patchName
|
||||
|
||||
val compatibilityAnnotation = patch.javaClass.findAnnotationRecursively(Compatibility::class.java)
|
||||
val prefix = "Skipping $patchName"
|
||||
|
||||
val patchName = patch.javaClass.findAnnotationRecursively(Name::class.java)?.name ?: Name::class.java.name
|
||||
val args = MainCommand.args.sArgs?.pArgs!!
|
||||
|
||||
val prefix = "[skipped] $patchName"
|
||||
|
||||
if (includeFilter && !MainCommand.includedPatches.contains(patchName)) {
|
||||
println(prefix)
|
||||
if (args.excludedPatches.contains(patchName)) {
|
||||
logger.info("$prefix: Explicitely excluded")
|
||||
return@patch
|
||||
} else if ((!patch.include || args.defaultExclude) && !args.includedPatches.contains(patchName)) {
|
||||
logger.info("$prefix: Not explicitly included")
|
||||
return@patch
|
||||
}
|
||||
|
||||
if (packageVersionCompatibilityFilter || packageCompatibilityFilter) {
|
||||
|
||||
if (compatibilityAnnotation == null) {
|
||||
println("$prefix: Missing compatibility annotation.")
|
||||
if (compatiblePackages == null) logger.warn("$prefix: Missing compatibility annotation. Continuing.")
|
||||
else {
|
||||
if (!compatiblePackages.any { it.name == packageName }) {
|
||||
logger.warn("$prefix: Incompatible with $packageName. This patch is only compatible with ${
|
||||
compatiblePackages.joinToString(
|
||||
", "
|
||||
) { it.name }
|
||||
}")
|
||||
return@patch
|
||||
}
|
||||
|
||||
|
||||
for (compatiblePackage in compatibilityAnnotation.compatiblePackages) {
|
||||
if (packageCompatibilityFilter && compatiblePackage.name != packageName) {
|
||||
println("$prefix: Package name not matching ${compatiblePackage.name}.")
|
||||
return@patch
|
||||
}
|
||||
|
||||
if (!packageVersionCompatibilityFilter || compatiblePackage.versions.any { it == packageVersion }) continue
|
||||
println("$prefix: Unsupported version.")
|
||||
if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) {
|
||||
val compatibleWith = compatiblePackages.map { _package ->
|
||||
"${_package.name}: ${_package.versions.joinToString(", ")}"
|
||||
}.joinToString(";")
|
||||
logger.warn("$prefix: Incompatible with version $packageVersion. This patch is only compatible with version $compatibleWith")
|
||||
return@patch
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace("Adding $patchName")
|
||||
includedPatches.add(patch)
|
||||
println("[added] $patchName")
|
||||
}
|
||||
this.addPatches(includedPatches)
|
||||
}
|
||||
}
|
||||
|
||||
fun Patcher.applyPatchesPrint() {
|
||||
for ((patch, result) in this.applyPatches()) {
|
||||
println("[${if (result.isFailure) "error" else "success"}] $patch")
|
||||
fun Patcher.applyPatchesVerbose() {
|
||||
this.applyPatches().forEach { (patch, result) ->
|
||||
if (result.isSuccess) {
|
||||
logger.info("$patch succeeded")
|
||||
return@forEach
|
||||
}
|
||||
logger.error("$patch failed:")
|
||||
result.exceptionOrNull()!!.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun Patcher.mergeFiles() {
|
||||
this.addFiles(MainCommand.mergeFiles)
|
||||
this.addFiles(args.sArgs?.pArgs!!.mergeFiles) { file ->
|
||||
logger.info("Merging $file")
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package app.revanced.utils.signature
|
||||
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.extensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.signature.implementation.method.annotation.FuzzyPatternScanMethod
|
||||
import app.revanced.patcher.signature.implementation.method.annotation.MatchingMethod
|
||||
import org.jf.dexlib2.iface.Method
|
||||
|
||||
object Signature {
|
||||
|
||||
fun checkSignatures(patcher: Patcher) {
|
||||
val failed = mutableListOf<String>()
|
||||
for (signature in patcher.resolveSignatures()) {
|
||||
val signatureClass = signature::class.java
|
||||
val signatureName =
|
||||
signatureClass.findAnnotationRecursively(app.revanced.patcher.annotation.Name::class.java)?.name
|
||||
?: signatureClass.name
|
||||
if (!signature.resolved) {
|
||||
failed.add(signatureName)
|
||||
continue
|
||||
}
|
||||
|
||||
val method = signature.result!!.method
|
||||
val matchingMethod =
|
||||
signatureClass.findAnnotationRecursively(MatchingMethod::class.java) ?: MatchingMethod()
|
||||
|
||||
println(
|
||||
"""
|
||||
[Signature] $signatureName
|
||||
[Method] ${matchingMethod.definingClass}->${matchingMethod.name}
|
||||
[Match] ${method.definingClass}->${method.toStr()}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
signatureClass.findAnnotationRecursively(FuzzyPatternScanMethod::class.java)?.let {
|
||||
val warnings = signature.result!!.scanResult.warnings!!
|
||||
println(
|
||||
"""
|
||||
[Warnings: ${warnings.count()}]
|
||||
${warnings.joinToString(separator = "\n") { warning -> "${warning.instructionIndex} / ${warning.patternIndex}: ${warning.wrongOpcode} (expected: ${warning.correctOpcode})" }}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
println(
|
||||
"""
|
||||
${"=".repeat(50)}
|
||||
[Failed signatures: ${failed.size}]
|
||||
${failed.joinToString(separator = "\n") { it }}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Method.toStr(): String {
|
||||
return "${this.name}(${this.parameterTypes.joinToString("")})${this.returnType}"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package app.revanced.utils.signing
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
data class KeySet(
|
||||
val publicKey: X509Certificate,
|
||||
val privateKey: PrivateKey
|
||||
)
|
||||
@@ -1,65 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Juby210 & Vendicated
|
||||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
package app.revanced.utils.signing
|
||||
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.cli.signing.SigningOptions
|
||||
import com.android.apksig.ApkSigner
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cms.*
|
||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import org.bouncycastle.operator.DigestCalculatorProvider
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
|
||||
import org.bouncycastle.util.encoders.Base64
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.*
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.Manifest
|
||||
import java.util.regex.Pattern
|
||||
|
||||
|
||||
const val CN = "ReVanced"
|
||||
val PASSWORD = "revanced".toCharArray() // TODO: make it secure; random password should be enough
|
||||
|
||||
/**
|
||||
* APK Signer.
|
||||
* @author Aliucord authors
|
||||
* @author ReVanced team
|
||||
*/
|
||||
object Signer {
|
||||
internal class Signer(
|
||||
private val signingOptions: SigningOptions
|
||||
) {
|
||||
private val passwordCharArray = signingOptions.password.toCharArray()
|
||||
private fun newKeystore(out: File) {
|
||||
val key = createKey()
|
||||
val (publicKey, privateKey) = createKey()
|
||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
||||
privateKS.load(null, PASSWORD)
|
||||
privateKS.setKeyEntry("alias", key.privateKey, PASSWORD, arrayOf(key.publicKey))
|
||||
privateKS.store(FileOutputStream(out), PASSWORD)
|
||||
privateKS.load(null, passwordCharArray)
|
||||
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
|
||||
privateKS.store(FileOutputStream(out), passwordCharArray)
|
||||
}
|
||||
|
||||
private fun createKey(): KeySet {
|
||||
private fun createKey(): Pair<X509Certificate, PrivateKey> {
|
||||
val gen = KeyPairGenerator.getInstance("RSA")
|
||||
gen.initialize(2048)
|
||||
val pair = gen.generateKeyPair()
|
||||
var serialNumber: BigInteger
|
||||
do serialNumber =
|
||||
BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
|
||||
val x500Name = X500Name("CN=$CN")
|
||||
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
|
||||
val x500Name = X500Name("CN=${signingOptions.cn}")
|
||||
val builder = X509v3CertificateBuilder(
|
||||
x500Name,
|
||||
serialNumber,
|
||||
@@ -69,143 +46,34 @@ object Signer {
|
||||
x500Name,
|
||||
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
|
||||
)
|
||||
val signer: ContentSigner = JcaContentSignerBuilder("SHA1withRSA").build(pair.private)
|
||||
return KeySet(JcaX509CertificateConverter().getCertificate(builder.build(signer)), pair.private)
|
||||
val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
||||
}
|
||||
|
||||
private val stripPattern: Pattern = Pattern.compile("^META-INF/(.*)[.](MF|SF|RSA|DSA)$")
|
||||
|
||||
// based on https://gist.github.com/mmuszkow/10288441
|
||||
// and https://github.com/fornwall/apksigner/blob/master/src/main/java/net/fornwall/apksigner/ZipSigner.java
|
||||
fun signApk(apkFile: File) {
|
||||
fun signApk(input: File, output: File) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val ks = File(apkFile.parent, "revanced-cli.keystore")
|
||||
if (!ks.exists()) newKeystore(ks)
|
||||
// TODO: keystore should be saved securely
|
||||
val ks = File(signingOptions.keyStoreFilePath)
|
||||
if (!ks.exists()) newKeystore(ks) else {
|
||||
logger.info("Found existing keystore: ${ks.nameWithoutExtension}")
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
val keySet = KeySet(
|
||||
(keyStore.getCertificate(alias) as X509Certificate),
|
||||
(keyStore.getKey(alias, PASSWORD) as PrivateKey)
|
||||
)
|
||||
|
||||
val zip = FileSystems.newFileSystem(apkFile.toPath(), null as ClassLoader?)
|
||||
val config = ApkSigner.SignerConfig.Builder(
|
||||
signingOptions.cn,
|
||||
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
||||
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
||||
).build()
|
||||
|
||||
val dig = MessageDigest.getInstance("SHA1")
|
||||
val digests: MutableMap<String, String> = LinkedHashMap()
|
||||
val signer = ApkSigner.Builder(listOf(config))
|
||||
signer.setCreatedBy(signingOptions.cn)
|
||||
signer.setInputApk(input)
|
||||
signer.setOutputApk(output)
|
||||
|
||||
for (entry in zip.allEntries) {
|
||||
val name = entry.toString()
|
||||
if (stripPattern.matcher(name).matches()) {
|
||||
Files.delete(entry)
|
||||
} else {
|
||||
digests[name] = toBase64(dig.digest(Files.readAllBytes(entry)))
|
||||
}
|
||||
}
|
||||
|
||||
val sectionDigests: MutableMap<String, String> = LinkedHashMap()
|
||||
var manifest = Manifest()
|
||||
var attrs = manifest.mainAttributes
|
||||
attrs[Attributes.Name.MANIFEST_VERSION] = "1.0"
|
||||
attrs[Attributes.Name("Created-By")] = CN
|
||||
|
||||
val digestAttr = Attributes.Name("SHA1-Digest")
|
||||
for ((name, value) in digests) {
|
||||
val attributes = Attributes()
|
||||
attributes[digestAttr] = value
|
||||
manifest.entries[name] = attributes
|
||||
sectionDigests[name] = hashEntrySection(name, attributes, dig)
|
||||
}
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
zip.writeFile(JarFile.MANIFEST_NAME, baos.toByteArray())
|
||||
}
|
||||
|
||||
val manifestHash = getManifestHash(manifest, dig)
|
||||
val tmpManifest = Manifest()
|
||||
tmpManifest.mainAttributes.putAll(attrs)
|
||||
val manifestMainHash = getManifestHash(tmpManifest, dig)
|
||||
|
||||
manifest = Manifest()
|
||||
attrs = manifest.mainAttributes
|
||||
attrs[Attributes.Name.SIGNATURE_VERSION] = "1.0"
|
||||
attrs[Attributes.Name("Created-By")] = CN
|
||||
attrs[Attributes.Name("SHA1-Digest-Manifest")] = manifestHash
|
||||
attrs[Attributes.Name("SHA1-Digest-Manifest-Main-Attributes")] = manifestMainHash
|
||||
|
||||
for ((key, value) in sectionDigests) {
|
||||
val attributes = Attributes()
|
||||
attributes[digestAttr] = value
|
||||
manifest.entries[key] = attributes
|
||||
}
|
||||
var sigBytes: ByteArray
|
||||
ByteArrayOutputStream().use { sigStream ->
|
||||
manifest.write(sigStream)
|
||||
sigBytes = sigStream.toByteArray()
|
||||
zip.writeFile("META-INF/CERT.SF", sigBytes)
|
||||
}
|
||||
|
||||
val signature = signSigFile(keySet, sigBytes)
|
||||
zip.writeFile("META-INF/CERT.RSA", signature)
|
||||
|
||||
zip.close()
|
||||
signer.build().sign()
|
||||
}
|
||||
|
||||
private fun hashEntrySection(name: String, attrs: Attributes, dig: MessageDigest): String {
|
||||
val manifest = Manifest()
|
||||
manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
val emptyLen = baos.toByteArray().size
|
||||
manifest.entries[name] = attrs
|
||||
baos.reset()
|
||||
manifest.write(baos)
|
||||
var ob = baos.toByteArray()
|
||||
ob = Arrays.copyOfRange(ob, emptyLen, ob.size)
|
||||
return toBase64(dig.digest(ob))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getManifestHash(manifest: Manifest, dig: MessageDigest): String {
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
return toBase64(dig.digest(baos.toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun signSigFile(keySet: KeySet, content: ByteArray): ByteArray {
|
||||
val msg: CMSTypedData = CMSProcessableByteArray(content)
|
||||
val certs = JcaCertStore(Collections.singletonList(keySet.publicKey))
|
||||
val gen = CMSSignedDataGenerator()
|
||||
val jcaContentSignerBuilder = JcaContentSignerBuilder("SHA1withRSA")
|
||||
val sha1Signer: ContentSigner = jcaContentSignerBuilder.build(keySet.privateKey)
|
||||
val jcaDigestCalculatorProviderBuilder = JcaDigestCalculatorProviderBuilder()
|
||||
val digestCalculatorProvider: DigestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build()
|
||||
val jcaSignerInfoGeneratorBuilder = JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
|
||||
jcaSignerInfoGeneratorBuilder.setDirectSignature(true)
|
||||
val signerInfoGenerator: SignerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey)
|
||||
gen.addSignerInfoGenerator(signerInfoGenerator)
|
||||
gen.addCertificates(certs)
|
||||
val sigData: CMSSignedData = gen.generate(msg, false)
|
||||
return sigData.toASN1Structure().getEncoded("DER")
|
||||
}
|
||||
|
||||
private fun toBase64(data: ByteArray): String {
|
||||
return String(Base64.encode(data))
|
||||
}
|
||||
}
|
||||
|
||||
private val java.nio.file.FileSystem.allEntries: List<Path>
|
||||
get() = buildList {
|
||||
this@allEntries.rootDirectories.forEach { dir ->
|
||||
Files.walk(dir).filter(Files::isRegularFile).forEach { file ->
|
||||
this@buildList.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun java.nio.file.FileSystem.writeFile(path: String, bytes: ByteArray) {
|
||||
Files.write(this.getPath("/$path"), bytes)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package app.revanced.utils.signing.align
|
||||
|
||||
import app.revanced.utils.signing.align.zip.ZipFile
|
||||
import java.io.File
|
||||
|
||||
internal object ZipAligner {
|
||||
private const val DEFAULT_ALIGNMENT = 4
|
||||
private const val LIBRARY_ALIGNMENT = 4096
|
||||
|
||||
fun align(input: File, output: File) {
|
||||
val inputZip = ZipFile(input)
|
||||
val outputZip = ZipFile(output)
|
||||
|
||||
for (entry in inputZip.entries) {
|
||||
val data = inputZip.getDataForEntry(entry)
|
||||
|
||||
if (entry.compression == 0.toUShort()) {
|
||||
val alignment = if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
|
||||
|
||||
outputZip.addEntryAligned(entry, data, alignment)
|
||||
} else {
|
||||
outputZip.addEntry(entry, data)
|
||||
}
|
||||
}
|
||||
|
||||
outputZip.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.revanced.utils.signing.align.zip
|
||||
|
||||
import java.io.DataInput
|
||||
import java.io.DataOutput
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
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()
|
||||
|
||||
fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
|
||||
|
||||
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()
|
||||
|
||||
fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
|
||||
|
||||
fun ByteBuffer.getUShort() = this.getShort().toUShort()
|
||||
fun ByteBuffer.getUInt() = this.getInt().toUInt()
|
||||
|
||||
fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
|
||||
fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
|
||||
|
||||
fun DataInput.readUShort() = this.readShort().toUShort()
|
||||
fun DataInput.readUInt() = this.readInt().toUInt()
|
||||
|
||||
fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
|
||||
fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
|
||||
|
||||
fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
|
||||
fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
|
||||
|
||||
fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
|
||||
fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())
|
||||
130
src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt
Normal file
130
src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt
Normal file
@@ -0,0 +1,130 @@
|
||||
package app.revanced.utils.signing.align.zip
|
||||
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEndRecord
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
class ZipFile(val file: File) {
|
||||
var entries: MutableList<ZipEntry> = mutableListOf()
|
||||
|
||||
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
|
||||
|
||||
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 writeCDE() {
|
||||
val CDEStart = 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() - CDEStart,
|
||||
CDEStart,
|
||||
""
|
||||
)
|
||||
|
||||
filePointer.channel.write(endRecord.toECD())
|
||||
}
|
||||
|
||||
fun addEntry(entry: ZipEntry, data: ByteBuffer) {
|
||||
entry.localHeaderOffset = filePointer.channel.position().toUInt()
|
||||
|
||||
filePointer.channel.write(entry.toLFH())
|
||||
filePointer.channel.write(data)
|
||||
|
||||
entries.add(entry)
|
||||
}
|
||||
|
||||
fun addEntryAligned(entry: ZipEntry, data: ByteBuffer, alignment: Int) {
|
||||
//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)
|
||||
}
|
||||
|
||||
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
||||
return filePointer.channel.map(
|
||||
FileChannel.MapMode.READ_ONLY,
|
||||
entry.dataOffset.toLong(),
|
||||
entry.compressedSize.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
writeCDE()
|
||||
filePointer.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package app.revanced.utils.signing.align.zip.structures
|
||||
|
||||
import app.revanced.utils.signing.align.zip.putUInt
|
||||
import app.revanced.utils.signing.align.zip.putUShort
|
||||
import app.revanced.utils.signing.align.zip.readUIntLE
|
||||
import app.revanced.utils.signing.align.zip.readUShortLE
|
||||
import java.io.DataInput
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
data 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
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user