feat: Add ReVanced Library subproject (#265)

This commit is contained in:
oSumAtrIX 2023-09-20 05:11:04 +02:00 committed by GitHub
commit 157278c9ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 875 additions and 566 deletions

View File

@ -31,7 +31,7 @@
{ {
"assets": [ "assets": [
{ {
"path": "build/libs/*all.jar" "path": "revanced-cli/build/libs/*all.jar"
} }
], ],
successComment: false successComment: false

View File

@ -1,57 +1,7 @@
plugins { plugins {
kotlin("jvm") version "1.8.20" kotlin("jvm") version "1.9.0" apply false
alias(libs.plugins.shadow)
} }
group = "app.revanced" allprojects {
group = "app.revanced"
dependencies { }
implementation(libs.revanced.patcher)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.picocli)
implementation(libs.jadb) // Updated fork
implementation(libs.apksig)
implementation(libs.bcpkix.jdk15on)
implementation(libs.jackson.module.kotlin)
testImplementation(libs.kotlin.test)
}
kotlin { jvmToolchain(11) }
tasks {
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
processResources {
expand("projectVersion" to project.version)
}
shadowJar {
manifest {
attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt")
}
minimize {
exclude(dependency("org.jetbrains.kotlin:.*"))
exclude(dependency("org.bouncycastle:.*"))
exclude(dependency("app.revanced:.*"))
}
}
build {
dependsOn(shadowJar)
}
// 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)
}
}

View File

@ -6,9 +6,10 @@ jackson-module-kotlin = "2.14.3"
jadb = "2531a28109" jadb = "2531a28109"
kotlin-reflect = "1.9.0" kotlin-reflect = "1.9.0"
kotlin-test = "1.8.20-RC" kotlin-test = "1.8.20-RC"
kotlinx-coroutines-core = "1.7.1" kotlinx-coroutines-core = "1.7.3"
picocli = "4.7.3" picocli = "4.7.3"
revanced-patcher = "15.0.0-dev.2" revanced-patcher = "15.0.0"
binary-compatibility-validator = "0.13.2"
[libraries] [libraries]
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
@ -23,3 +24,4 @@ revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "re
[plugins] [plugins]
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }

View File

@ -0,0 +1,52 @@
plugins {
kotlin("jvm") version "1.9.0"
alias(libs.plugins.shadow)
}
dependencies {
implementation(project(":revanced-lib"))
implementation(libs.revanced.patcher)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.picocli)
testImplementation(libs.kotlin.test)
}
kotlin { jvmToolchain(11) }
tasks {
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
processResources {
expand("projectVersion" to project.version)
}
shadowJar {
manifest {
attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt")
}
minimize {
exclude(dependency("org.jetbrains.kotlin:.*"))
exclude(dependency("org.bouncycastle:.*"))
exclude(dependency("app.revanced:.*"))
}
}
build {
dependsOn(shadowJar)
}
// 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)
}
}

View File

@ -0,0 +1 @@
rootProject.name = "revanced-cli"

View File

@ -0,0 +1,39 @@
package app.revanced.cli.command
import app.revanced.cli.command.utility.UtilityCommand
import app.revanced.lib.logging.Logger
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.IVersionProvider
import java.util.*
fun main(args: Array<String>) {
Logger.setDefault()
CommandLine(MainCommand).execute(*args)
}
private object CLIVersionProvider : IVersionProvider {
override fun getVersion() = arrayOf(
MainCommand::class.java.getResourceAsStream(
"/app/revanced/cli/version.properties"
)?.use { stream ->
Properties().apply { load(stream) }.let {
"ReVanced CLI v${it.getProperty("version")}"
}
} ?: "ReVanced CLI")
}
@Command(
name = "revanced-cli",
description = ["Command line application to use ReVanced"],
mixinStandardHelpOptions = true,
versionProvider = CLIVersionProvider::class,
subcommands = [
ListPatchesCommand::class,
PatchCommand::class,
OptionsCommand::class,
UtilityCommand::class,
]
)
private object MainCommand

View File

@ -1,8 +1,8 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.lib.Options
import app.revanced.lib.Options.setOptions
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File import java.io.File
@ -37,10 +37,17 @@ internal object OptionsCommand : Runnable {
) )
private var update: Boolean = false private var update: Boolean = false
override fun run() = if (!filePath.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) { override fun run() = try {
if (update && filePath.exists()) setOptions(filePath) PatchBundleLoader.Jar(*patchBundles).let { patches ->
if (!filePath.exists() || overwrite) {
if (update && filePath.exists()) patches.setOptions(filePath)
Options.serialize(this, prettyPrint = true).let(filePath::writeText) Options.serialize(patches, prettyPrint = true).let(filePath::writeText)
} else throw OptionsFileAlreadyExistsException()
}
} catch (ex: OptionsFileAlreadyExistsException) {
logger.severe("Options file already exists, use --overwrite to override it")
} }
else logger.severe("Options file already exists, use --overwrite to override it")
class OptionsFileAlreadyExistsException : Exception()
} }

View File

@ -1,17 +1,19 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.patcher.* import app.revanced.lib.ApkUtils
import app.revanced.utils.Options import app.revanced.lib.Options
import app.revanced.utils.Options.setOptions import app.revanced.lib.Options.setOptions
import app.revanced.utils.adb.AdbManager import app.revanced.lib.adb.AdbManager
import app.revanced.utils.align.ZipAligner import app.revanced.lib.signing.SigningOptions
import app.revanced.utils.align.zip.ZipFile import app.revanced.patcher.PatchBundleLoader
import app.revanced.utils.align.zip.structures.ZipEntry import app.revanced.patcher.PatchSet
import app.revanced.utils.signing.ApkSigner import app.revanced.patcher.Patcher
import app.revanced.utils.signing.SigningOptions import app.revanced.patcher.PatcherOptions
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import picocli.CommandLine.Model.CommandSpec
import picocli.CommandLine.Spec
import java.io.File import java.io.File
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
@ -24,21 +26,15 @@ import java.util.logging.Logger
internal object PatchCommand : Runnable { internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(PatchCommand::class.java.name) private val logger = Logger.getLogger(PatchCommand::class.java.name)
@CommandLine.Parameters( @Spec
description = ["APK file to be patched"], arity = "1..1" lateinit var spec: CommandSpec // injected by picocli
)
private lateinit var apk: File private lateinit var apk: File
@CommandLine.Option(
names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
)
private var patchBundles = emptyList<File>()
@CommandLine.Option(
names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
)
private var integrations = listOf<File>() private var integrations = listOf<File>()
private var patchBundles = emptyList<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["-i", "--include"], description = ["List of patches to include"] names = ["-i", "--include"], description = ["List of patches to include"]
) )
@ -94,7 +90,7 @@ internal object PatchCommand : Runnable {
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"] names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]
) )
private var keystorePath: String? = null private var keystoreFilePath: File? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"] names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"]
@ -108,10 +104,7 @@ internal object PatchCommand : Runnable {
) )
private var resourceCachePath = File("revanced-resource-cache") private var resourceCachePath = File("revanced-resource-cache")
@CommandLine.Option( private var aaptBinaryPath: File? = null
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
)
private var aaptBinaryPath = File("")
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--purge"], names = ["-p", "--purge"],
@ -120,36 +113,60 @@ internal object PatchCommand : Runnable {
) )
private var purge: Boolean = false private var purge: Boolean = false
@CommandLine.Parameters(
description = ["APK file to be patched"], arity = "1..1"
)
@Suppress("unused")
private fun setApk(apk: File) {
if (!apk.exists()) throw CommandLine.ParameterException(
spec.commandLine(),
"APK file ${apk.name} does not exist"
)
this.apk = apk
}
@CommandLine.Option(
names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
)
@Suppress("unused")
private fun setIntegrations(integrations: Array<File>) {
integrations.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "Integrations file ${it.name} does not exist")
}
this.integrations += integrations
}
@CommandLine.Option(
names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
)
@Suppress("unused")
private fun setPatchBundles(patchBundles: Array<File>) {
patchBundles.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "Patch bundle ${it.name} does not exist")
}
this.patchBundles = patchBundles.toList()
}
@CommandLine.Option(
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
)
@Suppress("unused")
private fun setAaptBinaryPath(aaptBinaryPath: File) {
if (!aaptBinaryPath.exists()) throw CommandLine.ParameterException(
spec.commandLine(),
"AAPT binary ${aaptBinaryPath.name} does not exist"
)
this.aaptBinaryPath = aaptBinaryPath
}
override fun run() { override fun run() {
// region Prepare val adbManager = deviceSerial?.let { serial -> AdbManager.getAdbManager(serial, mount) }
if (!apk.exists()) {
logger.severe("APK file ${apk.name} does not exist")
return
}
integrations.filter { !it.exists() }.let {
if (it.isEmpty()) return@let
it.forEach { integration ->
logger.severe("Integration file ${integration.name} does not exist")
}
return
}
val adbManager = deviceSerial?.let { serial ->
if (mount) AdbManager.RootAdbManager(serial)
else AdbManager.UserAdbManager(serial)
}
// endregion
// region Load patches // region Load patches
logger.info("Loading patches") logger.info("Loading patches")
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
val integrations = integrations
logger.info("Setting patch options") logger.info("Setting patch options")
@ -160,57 +177,63 @@ internal object PatchCommand : Runnable {
// endregion // endregion
// region Patch Patcher(
val patcher = Patcher(
PatcherOptions( PatcherOptions(
apk, apk,
resourceCachePath, resourceCachePath,
aaptBinaryPath.path, aaptBinaryPath?.path,
resourceCachePath.absolutePath, resourceCachePath.absolutePath,
) )
) ).use { patcher ->
// region Patch
val result = patcher.apply { val patcherResult = patcher.apply {
acceptIntegrations(integrations) acceptIntegrations(integrations)
acceptPatches(filterPatchSelection(patches)) acceptPatches(filterPatchSelection(patches))
// Execute patches. // Execute patches.
runBlocking { runBlocking {
apply(false).collect { patchResult -> apply(false).collect { patchResult ->
patchResult.exception?.let { patchResult.exception?.let {
StringWriter().use { writer -> StringWriter().use { writer ->
it.printStackTrace(PrintWriter(writer)) it.printStackTrace(PrintWriter(writer))
logger.severe("${patchResult.patch.name} failed:\n$writer") logger.severe("${patchResult.patch.name} failed:\n$writer")
} }
} ?: logger.info("${patchResult.patch.name} succeeded") } ?: logger.info("${patchResult.patch.name} succeeded")
}
} }
} }.get()
}.get()
patcher.close() // endregion
// endregion // region Save
// region Finish val tempFile = resourceCachePath.resolve(apk.name)
ApkUtils.copyAligned(apk, tempFile, patcherResult)
val alignAndSignedFile = sign( if (!mount) ApkUtils.sign(
apk.newAlignedFile( tempFile,
result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk") outputFilePath,
SigningOptions(
commonName,
password,
keystoreFilePath ?: outputFilePath.absoluteFile.parentFile
.resolve("${outputFilePath.nameWithoutExtension}.keystore"),
)
) )
)
logger.info("Copying to ${outputFilePath.name}") // endregion
alignAndSignedFile.copyTo(outputFilePath, overwrite = true)
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) // region Install
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
// endregion
}
if (purge) { if (purge) {
logger.info("Purging temporary files") logger.info("Purging temporary files")
purge(resourceCachePath) purge(resourceCachePath)
} }
// endregion
} }
@ -279,64 +302,6 @@ internal object PatchCommand : Runnable {
} }
} }
/**
* Create a new aligned APK file.
*
* @param result The result of the patching process.
* @param outputFile The file to save the aligned APK to.
*/
private fun File.newAlignedFile(
result: PatcherResult, outputFile: File
): File {
logger.info("Aligning $name")
if (outputFile.exists()) outputFile.delete()
ZipFile(outputFile).use { file ->
result.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry.createWithName(it.name), it.stream.readBytes()
)
}
result.resourceFile?.let {
file.copyEntriesFromFileAligned(
ZipFile(it), ZipAligner::getEntryAlignment
)
}
// TODO: Do not compress result.doNotCompress
file.copyEntriesFromFileAligned(
ZipFile(this), ZipAligner::getEntryAlignment
)
}
return outputFile
}
/**
* Sign the APK file.
*
* @param inputFile The APK file to sign.
* @return The signed APK file. If [mount] is true, the input file will be returned.
*/
private fun sign(inputFile: File) = if (mount) inputFile
else {
logger.info("Signing ${inputFile.name}")
val keyStoreFilePath = keystorePath
?: outputFilePath.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath
val options = SigningOptions(
commonName, password, keyStoreFilePath
)
ApkSigner(options).signApk(
inputFile, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
)
}
private fun purge(resourceCachePath: File) { private fun purge(resourceCachePath: File) {
val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory" val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory"
else "Failed to purge resource cache directory" else "Failed to purge resource cache directory"

View File

@ -1,6 +1,6 @@
package app.revanced.cli.command.utility package app.revanced.cli.command.utility
import app.revanced.utils.adb.AdbManager import app.revanced.lib.adb.AdbManager
import picocli.CommandLine.* import picocli.CommandLine.*
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
@ -28,15 +28,11 @@ internal object InstallCommand : Runnable {
) )
private var packageName: String? = null private var packageName: String? = null
override fun run() = try { override fun run() = deviceSerials.forEach { deviceSerial ->
deviceSerials.forEach { deviceSerial -> try {
if (packageName != null) { AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName))
AdbManager.RootAdbManager(deviceSerial) } catch (e: AdbManager.DeviceNotFoundException) {
} else { logger.severe(e.toString())
AdbManager.UserAdbManager(deviceSerial)
}.install(AdbManager.Apk(apk, packageName))
} }
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
} }
} }

View File

@ -1,6 +1,6 @@
package app.revanced.cli.command.utility package app.revanced.cli.command.utility
import app.revanced.utils.adb.AdbManager import app.revanced.lib.adb.AdbManager
import picocli.CommandLine.* import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import java.util.logging.Logger import java.util.logging.Logger
@ -26,15 +26,11 @@ internal object UninstallCommand : Runnable {
) )
private var unmount: Boolean = false private var unmount: Boolean = false
override fun run() = try { override fun run() = deviceSerials.forEach { deviceSerial ->
deviceSerials.forEach { deviceSerial -> try {
if (unmount) { AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName)
AdbManager.RootAdbManager(deviceSerial) } catch (e: AdbManager.DeviceNotFoundException) {
} else { logger.severe(e.toString())
AdbManager.UserAdbManager(deviceSerial)
}.uninstall(packageName)
} }
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
} }
} }

View File

@ -0,0 +1,118 @@
public final class app/revanced/lib/ApkUtils {
public static final field INSTANCE Lapp/revanced/lib/ApkUtils;
public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V
public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/lib/signing/SigningOptions;)V
}
public final class app/revanced/lib/Options {
public static final field INSTANCE Lapp/revanced/lib/Options;
public final fun deserialize (Ljava/lang/String;)[Lapp/revanced/lib/Options$Patch;
public final fun serialize (Ljava/util/Set;Z)Ljava/lang/String;
public static synthetic fun serialize$default (Lapp/revanced/lib/Options;Ljava/util/Set;ZILjava/lang/Object;)Ljava/lang/String;
public final fun setOptions (Ljava/util/Set;Ljava/io/File;)V
public final fun setOptions (Ljava/util/Set;Ljava/lang/String;)V
}
public final class app/revanced/lib/Options$Patch {
public final fun getOptions ()Ljava/util/List;
public final fun getPatchName ()Ljava/lang/String;
}
public final class app/revanced/lib/Options$Patch$Option {
public final fun getKey ()Ljava/lang/String;
public final fun getValue ()Ljava/lang/Object;
}
public abstract class app/revanced/lib/adb/AdbManager {
public static final field Companion Lapp/revanced/lib/adb/AdbManager$Companion;
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
protected final fun getDevice ()Lse/vidstige/jadb/JadbDevice;
protected final fun getLogger ()Ljava/util/logging/Logger;
public fun install (Lapp/revanced/lib/adb/AdbManager$Apk;)V
public fun uninstall (Ljava/lang/String;)V
}
public final class app/revanced/lib/adb/AdbManager$Apk {
public fun <init> (Ljava/io/File;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getFile ()Ljava/io/File;
public final fun getPackageName ()Ljava/lang/String;
}
public final class app/revanced/lib/adb/AdbManager$Companion {
public final fun getAdbManager (Ljava/lang/String;Z)Lapp/revanced/lib/adb/AdbManager;
public static synthetic fun getAdbManager$default (Lapp/revanced/lib/adb/AdbManager$Companion;Ljava/lang/String;ZILjava/lang/Object;)Lapp/revanced/lib/adb/AdbManager;
}
public final class app/revanced/lib/adb/AdbManager$DeviceNotFoundException : java/lang/Exception {
}
public final class app/revanced/lib/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception {
}
public final class app/revanced/lib/adb/AdbManager$PackageNameRequiredException : java/lang/Exception {
}
public final class app/revanced/lib/adb/AdbManager$RootAdbManager : app/revanced/lib/adb/AdbManager {
public static final field Utils Lapp/revanced/lib/adb/AdbManager$RootAdbManager$Utils;
public fun install (Lapp/revanced/lib/adb/AdbManager$Apk;)V
public fun uninstall (Ljava/lang/String;)V
}
public final class app/revanced/lib/adb/AdbManager$RootAdbManager$Utils {
}
public final class app/revanced/lib/adb/AdbManager$UserAdbManager : app/revanced/lib/adb/AdbManager {
public fun install (Lapp/revanced/lib/adb/AdbManager$Apk;)V
public fun uninstall (Ljava/lang/String;)V
}
public final class app/revanced/lib/logging/Logger {
public static final field INSTANCE Lapp/revanced/lib/logging/Logger;
public final fun addHandler (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V
public final fun removeAllHandlers ()V
public final fun setDefault ()V
public final fun setFormat (Ljava/lang/String;)V
public static synthetic fun setFormat$default (Lapp/revanced/lib/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V
}
public final class app/revanced/lib/signing/ApkSigner {
public fun <init> (Lapp/revanced/lib/signing/SigningOptions;)V
public final fun signApk (Ljava/io/File;Ljava/io/File;)V
}
public final class app/revanced/lib/signing/SigningOptions {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/io/File;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)Lapp/revanced/lib/signing/SigningOptions;
public static synthetic fun copy$default (Lapp/revanced/lib/signing/SigningOptions;Ljava/lang/String;Ljava/lang/String;Ljava/io/File;ILjava/lang/Object;)Lapp/revanced/lib/signing/SigningOptions;
public fun equals (Ljava/lang/Object;)Z
public final fun getCommonName ()Ljava/lang/String;
public final fun getKeyStoreOutputFilePath ()Ljava/io/File;
public final fun getPassword ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/lib/zip/ZipFile : java/io/Closeable {
public static final field ApkZipFile Lapp/revanced/lib/zip/ZipFile$ApkZipFile;
public fun <init> (Ljava/io/File;)V
public final fun addEntryCompressData (Lapp/revanced/lib/zip/structures/ZipEntry;[B)V
public fun close ()V
public final fun copyEntriesFromFileAligned (Lapp/revanced/lib/zip/ZipFile;Lkotlin/jvm/functions/Function1;)V
}
public final class app/revanced/lib/zip/ZipFile$ApkZipFile {
public final fun getApkZipEntryAlignment ()Lkotlin/jvm/functions/Function1;
}
public final class app/revanced/lib/zip/structures/ZipEntry {
public static final field Companion Lapp/revanced/lib/zip/structures/ZipEntry$Companion;
public fun <init> (Ljava/lang/String;)V
}
public final class app/revanced/lib/zip/structures/ZipEntry$Companion {
}

View File

@ -0,0 +1,78 @@
plugins {
kotlin("jvm") version "1.9.0"
alias(libs.plugins.binary.compatibility.validator)
`maven-publish`
}
dependencies {
implementation(libs.revanced.patcher)
implementation(libs.kotlin.reflect)
implementation(libs.jadb) // Updated fork
implementation(libs.apksig)
implementation(libs.bcpkix.jdk15on)
implementation(libs.jackson.module.kotlin)
testImplementation(libs.revanced.patcher)
testImplementation(libs.kotlin.test)
}
tasks {
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
}
kotlin { jvmToolchain(11) }
java {
withSourcesJar()
}
publishing {
repositories {
mavenLocal()
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 {
create<MavenPublication>("gpr") {
from(components["java"])
version = project.version.toString()
pom {
name = "ReVanced Library"
description = "Library containing common utilities for ReVanced"
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-cli.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-cli.git"
url = "https://github.com/revanced/revanced-cli"
}
}
}
}
}

View File

@ -0,0 +1 @@
rootProject.name = "revanced-lib"

View File

@ -0,0 +1,67 @@
package app.revanced.lib
import app.revanced.lib.signing.ApkSigner
import app.revanced.lib.signing.SigningOptions
import app.revanced.lib.zip.ZipFile
import app.revanced.lib.zip.structures.ZipEntry
import app.revanced.patcher.PatcherResult
import java.io.File
import java.util.logging.Logger
import kotlin.io.path.deleteIfExists
@Suppress("MemberVisibilityCanBePrivate", "unused")
object ApkUtils {
private val logger = Logger.getLogger(ApkUtils::class.java.name)
/**
* Creates a new apk from [apkFile] and [patchedEntriesSource] and writes it to [outputFile].
*
* @param apkFile The apk to copy entries from.
* @param outputFile The apk to write the new entries to.
* @param patchedEntriesSource The result of the patcher to add the patched dex files and resources.
*/
fun copyAligned(apkFile: File, outputFile: File, patchedEntriesSource: PatcherResult) {
logger.info("Aligning ${apkFile.name}")
outputFile.toPath().deleteIfExists()
ZipFile(outputFile).use { file ->
patchedEntriesSource.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry(it.name), it.stream.readBytes()
)
}
patchedEntriesSource.resourceFile?.let {
file.copyEntriesFromFileAligned(
ZipFile(it), ZipFile.apkZipEntryAlignment
)
}
// TODO: Do not compress result.doNotCompress
// TODO: Fix copying resources that are not needed anymore.
file.copyEntriesFromFileAligned(
ZipFile(apkFile), ZipFile.apkZipEntryAlignment
)
}
}
/**
* Signs the apk at [apk] and writes it to [output].
*
* @param apk The apk to sign.
* @param output The apk to write the signed apk to.
* @param signingOptions The options to use for signing.
*/
fun sign(
apk: File,
output: File,
signingOptions: SigningOptions,
) {
logger.info("Signing ${apk.name}")
ApkSigner(signingOptions).signApk(apk, output)
}
}

View File

@ -1,15 +1,19 @@
package app.revanced.utils @file:Suppress("MemberVisibilityCanBePrivate")
package app.revanced.lib
import app.revanced.lib.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 app.revanced.utils.Options.PatchOption.Option
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>
internal 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 var mapper = jacksonObjectMapper()
@ -24,7 +28,7 @@ internal object Options {
fun serialize(patches: PatchSet, prettyPrint: Boolean = false): String = patches fun serialize(patches: PatchSet, prettyPrint: Boolean = false): String = patches
.filter { it.options.any() } .filter { it.options.any() }
.map { patch -> .map { patch ->
PatchOption( Patch(
patch.name!!, patch.name!!,
patch.options.values.map { option -> Option(option.key, option.value) } patch.options.values.map { option -> Option(option.key, option.value) }
) )
@ -42,12 +46,11 @@ internal object Options {
* Deserializes the options for the patches in the list. * Deserializes the options for the patches in the list.
* *
* @param json The JSON string containing the options. * @param json The JSON string containing the options.
* @return The list of [PatchOption]s. * @return The list of [Patch]s.
* @see PatchOption * @see Patch
* @see PatchList * @see PatchList
*/ */
@Suppress("MemberVisibilityCanBePrivate") fun deserialize(json: String): Array<Patch> = mapper.readValue(json, Array<Patch>::class.java)
fun deserialize(json: String): Array<PatchOption> = mapper.readValue(json, Array<PatchOption>::class.java)
/** /**
* Sets the options for the patches in the list. * Sets the options for the patches in the list.
@ -88,7 +91,7 @@ internal object Options {
* @property patchName The name of the patch. * @property patchName The name of the patch.
* @property options The [Option]s for the patch. * @property options The [Option]s for the patch.
*/ */
internal data class PatchOption( class Patch internal constructor(
val patchName: String, val patchName: String,
val options: List<Option> val options: List<Option>
) { ) {
@ -99,6 +102,6 @@ internal object Options {
* @property key The name of the option. * @property key The name of the option.
* @property value The value of the option. * @property value The value of the option.
*/ */
internal data class Option(val key: String, val value: Any?) class Option internal constructor(val key: String, val value: Any?)
} }
} }

View File

@ -0,0 +1,154 @@
package app.revanced.lib.adb
import app.revanced.lib.adb.AdbManager.Apk
import app.revanced.lib.adb.Constants.CREATE_DIR
import app.revanced.lib.adb.Constants.DELETE
import app.revanced.lib.adb.Constants.INSTALLATION_PATH
import app.revanced.lib.adb.Constants.INSTALL_MOUNT
import app.revanced.lib.adb.Constants.INSTALL_PATCHED_APK
import app.revanced.lib.adb.Constants.MOUNT_PATH
import app.revanced.lib.adb.Constants.MOUNT_SCRIPT
import app.revanced.lib.adb.Constants.PATCHED_APK_PATH
import app.revanced.lib.adb.Constants.PLACEHOLDER
import app.revanced.lib.adb.Constants.RESOLVE_ACTIVITY
import app.revanced.lib.adb.Constants.RESTART
import app.revanced.lib.adb.Constants.TMP_PATH
import app.revanced.lib.adb.Constants.UMOUNT
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.managers.Package
import se.vidstige.jadb.managers.PackageManager
import java.io.File
import java.util.logging.Logger
/**
* Adb manager. Used to install and uninstall [Apk] files.
*
* @param deviceSerial The serial of the device.
*/
sealed class AdbManager private constructor(deviceSerial: String? = null) {
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial }
?: throw DeviceNotFoundException(deviceSerial)
init {
logger.fine("Established connection to $deviceSerial")
}
/**
* Installs the [Apk] file.
*
* @param apk The [Apk] file.
*/
open fun install(apk: Apk) {
logger.info("Finished installing ${apk.file.name}")
}
/**
* Uninstalls the package.
*
* @param packageName The package name.
*/
open fun uninstall(packageName: String) {
logger.info("Finished uninstalling $packageName")
}
companion object {
/**
* Gets an [AdbManager] for the supplied device serial.
*
* @param deviceSerial The device serial.
* @param root Whether to use root or not.
* @return The [AdbManager].
* @throws DeviceNotFoundException If the device can not be found.
*/
fun getAdbManager(deviceSerial: String, root: Boolean = false): AdbManager =
if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial)
}
class RootAdbManager internal constructor(deviceSerial: String) : AdbManager(deviceSerial) {
init {
if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed")
}
override fun install(apk: Apk) {
logger.info("Installing by mounting")
val packageName = apk.packageName ?: throw PackageNameRequiredException()
device.run(RESOLVE_ACTIVITY, packageName).inputStream.bufferedReader().readLine().let { line ->
if (line != "No activity found") return@let
throw throw FailedToFindInstalledPackageException(packageName)
}
device.push(apk.file, TMP_PATH)
device.run("$CREATE_DIR $INSTALLATION_PATH")
device.run(INSTALL_PATCHED_APK, packageName)
device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement(packageName))
device.run(INSTALL_MOUNT, packageName)
device.run(UMOUNT, packageName) // Sanity check.
device.run(MOUNT_PATH, packageName)
device.run(RESTART, packageName)
device.run(DELETE, TMP_PATH)
super.install(apk)
}
override fun uninstall(packageName: String) {
logger.info("Uninstalling $packageName by unmounting")
device.run(UMOUNT, packageName)
device.run(DELETE.applyReplacement(PATCHED_APK_PATH), packageName)
device.run(DELETE, MOUNT_PATH)
device.run(DELETE, TMP_PATH)
super.uninstall(packageName)
}
companion object Utils {
private fun JadbDevice.run(command: String, with: String) = run(command.applyReplacement(with))
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
}
}
class UserAdbManager internal constructor(deviceSerial: String) : AdbManager(deviceSerial) {
private val packageManager = PackageManager(device)
override fun install(apk: Apk) {
PackageManager(device).install(apk.file)
super.install(apk)
}
override fun uninstall(packageName: String) {
logger.info("Uninstalling $packageName")
packageManager.uninstall(Package(packageName))
super.uninstall(packageName)
}
}
/**
* Apk file for [AdbManager].
*
* @param file The [Apk] file.
* @param packageName The package name of the [Apk] file.
*/
class Apk(val file: File, val packageName: String? = null)
class DeviceNotFoundException internal constructor(deviceSerial: String?) :
Exception(deviceSerial?.let {
"The device with the ADB device serial \"$deviceSerial\" can not be found"
} ?: "No ADB device found")
class FailedToFindInstalledPackageException internal constructor(packageName: String) :
Exception("Failed to find installed package \"$packageName\" because no activity was found")
class PackageNameRequiredException internal constructor() :
Exception("Package name is required")
}

View File

@ -1,7 +1,8 @@
package app.revanced.utils.adb package app.revanced.lib.adb
import se.vidstige.jadb.JadbDevice import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcess
import se.vidstige.jadb.ShellProcessBuilder import se.vidstige.jadb.ShellProcessBuilder
import java.io.File import java.io.File
@ -15,8 +16,8 @@ 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): Int { internal fun JadbDevice.run(command: String, su: Boolean = true): ShellProcess {
return this.buildCommand(command, su).start().waitFor() return this.buildCommand(command, su).start()!!
} }
internal fun JadbDevice.hasSu() = internal fun JadbDevice.hasSu() =

View File

@ -1,4 +1,4 @@
package app.revanced.utils.adb package app.revanced.lib.adb
internal object Constants { internal object Constants {
internal const val PLACEHOLDER = "PLACEHOLDER" internal const val PLACEHOLDER = "PLACEHOLDER"
@ -10,7 +10,8 @@ internal object Constants {
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 = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " + internal const val RESOLVE_ACTIVITY = "pm resolve-activity --brief $PLACEHOLDER"
internal const val RESTART = "$RESOLVE_ACTIVITY | tail -n 1 | " +
"xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)" "xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)"
internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " + internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " +

View File

@ -0,0 +1,84 @@
package app.revanced.lib.logging
import java.util.logging.Handler
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.SimpleFormatter
@Suppress("MemberVisibilityCanBePrivate")
object Logger {
private val rootLogger = java.util.logging.Logger.getLogger("")
/**
* Sets the format for the logger.
*
* @param format The format to use.
*/
fun setFormat(format: String = "%4\$s: %5\$s %n") {
System.setProperty("java.util.logging.SimpleFormatter.format", format)
}
/**
* Removes all handlers from the logger.
*/
fun removeAllHandlers() {
rootLogger.let { logger ->
logger.handlers.forEach { handler ->
handler.close()
logger.removeHandler(handler)
}
}
}
/**
* Adds a handler to the logger.
*
* @param publishHandler The handler for publishing the log.
* @param flushHandler The handler for flushing the log.
* @param closeHandler The handler for closing the log.
*/
fun addHandler(
publishHandler: (log: String, level: Level, loggerName: String?) -> Unit,
flushHandler: () -> Unit,
closeHandler: () -> Unit
) = object : Handler() {
override fun publish(record: LogRecord) = publishHandler(
formatter.format(record),
record.level,
record.loggerName
)
override fun flush() = flushHandler()
override fun close() = closeHandler()
}.also {
it.level = Level.ALL
it.formatter = SimpleFormatter()
}.let(rootLogger::addHandler)
/**
* Log to "standard" (error) output streams.
*/
fun setDefault() {
setFormat()
removeAllHandlers()
val publishHandler = handler@{ log: String, level: Level, loggerName: String? ->
if (loggerName?.startsWith("app.revanced") != true) return@handler
log.toByteArray().let {
if (level.intValue() > Level.WARNING.intValue())
System.err.write(it)
else
System.out.write(it)
}
}
val flushHandler = {
System.out.flush()
System.err.flush()
}
addHandler(publishHandler, flushHandler, flushHandler)
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.utils.signing package app.revanced.lib.signing
import com.android.apksig.ApkSigner import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
@ -17,10 +17,10 @@ import java.security.cert.X509Certificate
import java.util.* import java.util.*
import java.util.logging.Logger import java.util.logging.Logger
internal class ApkSigner( class ApkSigner(
private val signingOptions: SigningOptions private val signingOptions: SigningOptions
) { ) {
private val logger = Logger.getLogger(ApkSigner::class.java.name) private val logger = Logger.getLogger(app.revanced.lib.signing.ApkSigner::class.java.name)
private val signer: ApkSigner.Builder private val signer: ApkSigner.Builder
private val passwordCharArray = signingOptions.password.toCharArray() private val passwordCharArray = signingOptions.password.toCharArray()
@ -30,12 +30,12 @@ internal class ApkSigner(
val keyStore = KeyStore.getInstance("BKS", "BC") val keyStore = KeyStore.getInstance("BKS", "BC")
val alias = keyStore.let { store -> val alias = keyStore.let { store ->
FileInputStream(File(signingOptions.keyStoreFilePath).also { FileInputStream(signingOptions.keyStoreOutputFilePath.also {
if (!it.exists()) { if (!it.exists()) {
logger.info("Creating keystore at ${it.absolutePath}") logger.info("Creating keystore at ${it.absolutePath}")
newKeystore(it) newKeystore(it)
} else { } else {
logger.info("Using keystore at ${it.absolutePath}") logger.info("Using keystore ${it.absolutePath}")
} }
}).use { fis -> store.load(fis, null) } }).use { fis -> store.load(fis, null) }
store.aliases().nextElement() store.aliases().nextElement()
@ -43,13 +43,13 @@ internal class ApkSigner(
with( with(
ApkSigner.SignerConfig.Builder( ApkSigner.SignerConfig.Builder(
signingOptions.cn, signingOptions.commonName,
keyStore.getKey(alias, passwordCharArray) as PrivateKey, keyStore.getKey(alias, passwordCharArray) as PrivateKey,
listOf(keyStore.getCertificate(alias) as X509Certificate) listOf(keyStore.getCertificate(alias) as X509Certificate)
).build() ).build()
) { ) {
this@ApkSigner.signer = ApkSigner.Builder(listOf(this)) this@ApkSigner.signer = ApkSigner.Builder(listOf(this))
signer.setCreatedBy(signingOptions.cn) signer.setCreatedBy(signingOptions.commonName)
} }
} }
@ -67,7 +67,7 @@ internal class ApkSigner(
val pair = gen.generateKeyPair() val pair = gen.generateKeyPair()
var serialNumber: BigInteger var serialNumber: BigInteger
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO) do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
val x500Name = X500Name("CN=${signingOptions.cn}") val x500Name = X500Name("CN=${signingOptions.commonName}")
val builder = X509v3CertificateBuilder( val builder = X509v3CertificateBuilder(
x500Name, x500Name,
serialNumber, serialNumber,
@ -81,12 +81,10 @@ internal class ApkSigner(
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
} }
fun signApk(input: File, output: File): File { fun signApk(input: File, output: File) {
signer.setInputApk(input) signer.setInputApk(input)
signer.setOutputApk(output) signer.setOutputApk(output)
signer.build().sign() signer.build().sign()
return output
} }
} }

View File

@ -0,0 +1,9 @@
package app.revanced.lib.signing
import java.io.File
data class SigningOptions(
val commonName: String,
val password: String,
val keyStoreOutputFilePath: File
)

View File

@ -0,0 +1,33 @@
package app.revanced.lib.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,7 +1,7 @@
package app.revanced.utils.align.zip package app.revanced.lib.zip
import app.revanced.utils.align.zip.structures.ZipEndRecord import app.revanced.lib.zip.structures.ZipEndRecord
import app.revanced.utils.align.zip.structures.ZipEntry import app.revanced.lib.zip.structures.ZipEntry
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.RandomAccessFile import java.io.RandomAccessFile
@ -178,4 +178,15 @@ class ZipFile(file: File) : Closeable {
if (centralDirectoryNeedsRewrite) writeCD() if (centralDirectoryNeedsRewrite) writeCD()
filePointer.close() 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,14 +1,14 @@
package app.revanced.utils.align.zip.structures package app.revanced.lib.zip.structures
import app.revanced.utils.align.zip.putUInt import app.revanced.lib.zip.putUInt
import app.revanced.utils.align.zip.putUShort import app.revanced.lib.zip.putUShort
import app.revanced.utils.align.zip.readUIntLE import app.revanced.lib.zip.readUIntLE
import app.revanced.utils.align.zip.readUShortLE import app.revanced.lib.zip.readUShortLE
import java.io.DataInput import java.io.DataInput
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
data class ZipEndRecord( internal class ZipEndRecord(
val diskNumber: UShort, val diskNumber: UShort,
val startingDiskNumber: UShort, val startingDiskNumber: UShort,
val diskEntries: UShort, val diskEntries: UShort,

View File

@ -1,64 +1,62 @@
package app.revanced.utils.align.zip.structures package app.revanced.lib.zip.structures
import app.revanced.utils.align.zip.* import app.revanced.lib.zip.*
import java.io.DataInput import java.io.DataInput
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
data class ZipEntry( class ZipEntry private constructor(
val version: UShort, internal val version: UShort,
val versionNeeded: UShort, internal val versionNeeded: UShort,
val flags: UShort, internal val flags: UShort,
var compression: UShort, internal var compression: UShort,
val modificationTime: UShort, internal val modificationTime: UShort,
val modificationDate: UShort, internal val modificationDate: UShort,
var crc32: UInt, internal var crc32: UInt,
var compressedSize: UInt, internal var compressedSize: UInt,
var uncompressedSize: UInt, internal var uncompressedSize: UInt,
val diskNumber: UShort, internal val diskNumber: UShort,
val internalAttributes: UShort, internal val internalAttributes: UShort,
val externalAttributes: UInt, internal val externalAttributes: UInt,
var localHeaderOffset: UInt, internal var localHeaderOffset: UInt,
val fileName: String, internal val fileName: String,
val extraField: ByteArray, internal val extraField: ByteArray,
val fileComment: String, internal val fileComment: String,
var localExtraField: ByteArray = ByteArray(0), //separate for alignment internal var localExtraField: ByteArray = ByteArray(0), //separate for alignment
) { ) {
val LFHSize: Int internal val LFHSize: Int
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
val dataOffset: UInt internal val dataOffset: UInt
get() = localHeaderOffset + LFHSize.toUInt() 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 { companion object {
const val CDE_HEADER_SIZE = 46 internal const val CDE_HEADER_SIZE = 46
const val CDE_SIGNATURE = 0x02014b50u internal const val CDE_SIGNATURE = 0x02014b50u
const val LFH_HEADER_SIZE = 30 internal const val LFH_HEADER_SIZE = 30
const val LFH_SIGNATURE = 0x04034b50u internal const val LFH_SIGNATURE = 0x04034b50u
fun createWithName(fileName: String): ZipEntry { internal fun fromCDE(input: DataInput): ZipEntry {
return ZipEntry(
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),
""
)
}
fun fromCDE(input: DataInput): ZipEntry {
val signature = input.readUIntLE() val signature = input.readUIntLE()
if (signature != CDE_SIGNATURE) if (signature != CDE_SIGNATURE)
@ -123,12 +121,12 @@ data class ZipEntry(
} }
} }
fun readLocalExtra(buffer: ByteBuffer) { internal fun readLocalExtra(buffer: ByteBuffer) {
buffer.order(ByteOrder.LITTLE_ENDIAN) buffer.order(ByteOrder.LITTLE_ENDIAN)
localExtraField = ByteArray(buffer.getUShort().toInt()) localExtraField = ByteArray(buffer.getUShort().toInt())
} }
fun toLFH(): ByteBuffer { internal fun toLFH(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8) val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size) val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
@ -153,7 +151,7 @@ data class ZipEntry(
return buffer return buffer
} }
fun toCDE(): ByteBuffer { internal fun toCDE(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8) val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val commentBytes = fileComment.toByteArray(Charsets.UTF_8) val commentBytes = fileComment.toByteArray(Charsets.UTF_8)

View File

@ -1,11 +1,11 @@
package app.revanced.patcher.options package app.revanced.patcher.options
import app.revanced.lib.Options
import app.revanced.lib.Options.setOptions
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.options.types.BooleanPatchOption.Companion.booleanPatchOption import app.revanced.patcher.patch.options.types.BooleanPatchOption.Companion.booleanPatchOption
import app.revanced.patcher.patch.options.types.StringPatchOption.Companion.stringPatchOption import app.revanced.patcher.patch.options.types.StringPatchOption.Companion.stringPatchOption
import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions
import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -22,7 +22,7 @@ object PatchOptionsTestPatch : BytecodePatch(name = "PatchOptionsTestPatch") {
} }
@TestMethodOrder(MethodOrderer.OrderAnnotation::class) @TestMethodOrder(MethodOrderer.OrderAnnotation::class)
internal object PatchOptionOptionsTest { internal object PatchOptionsTest {
private var patches = setOf(PatchOptionsTestPatch) private var patches = setOf(PatchOptionsTestPatch)
@Test @Test

View File

@ -20,4 +20,4 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "revanced-cli" include("revanced-cli", "revanced-lib")

View File

@ -1,64 +0,0 @@
package app.revanced.cli.command
import app.revanced.cli.command.utility.UtilityCommand
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.IVersionProvider
import java.util.*
import java.util.logging.*
fun main(args: Array<String>) {
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
object : Handler() {
override fun publish(record: LogRecord) = formatter.format(record).toByteArray().let {
if (record.level.intValue() > Level.INFO.intValue())
System.err.write(it)
else
System.out.write(it)
}
override fun flush() {
System.out.flush()
System.err.flush()
}
override fun close() = flush()
}.also {
it.level = Level.ALL
it.formatter = SimpleFormatter()
}.let(::addHandler)
}
CommandLine(MainCommand).execute(*args)
}
private object CLIVersionProvider : IVersionProvider {
override fun getVersion(): Array<String> {
Properties().apply {
load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties"))
}.let {
return arrayOf("ReVanced CLI v${it.getProperty("version")}")
}
}
}
@Command(
name = "revanced-cli",
description = ["Command line application to use ReVanced"],
mixinStandardHelpOptions = true,
versionProvider = CLIVersionProvider::class,
subcommands = [
ListPatchesCommand::class,
PatchCommand::class,
OptionsCommand::class,
UtilityCommand::class,
]
)
private object MainCommand

View File

@ -1,140 +0,0 @@
package app.revanced.utils.adb
import app.revanced.utils.adb.AdbManager.Apk
import app.revanced.utils.adb.Constants.CREATE_DIR
import app.revanced.utils.adb.Constants.DELETE
import app.revanced.utils.adb.Constants.INSTALLATION_PATH
import app.revanced.utils.adb.Constants.INSTALL_MOUNT
import app.revanced.utils.adb.Constants.INSTALL_PATCHED_APK
import app.revanced.utils.adb.Constants.MOUNT_PATH
import app.revanced.utils.adb.Constants.MOUNT_SCRIPT
import app.revanced.utils.adb.Constants.PATCHED_APK_PATH
import app.revanced.utils.adb.Constants.PLACEHOLDER
import app.revanced.utils.adb.Constants.RESTART
import app.revanced.utils.adb.Constants.TMP_PATH
import app.revanced.utils.adb.Constants.UMOUNT
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.managers.Package
import se.vidstige.jadb.managers.PackageManager
import java.io.Closeable
import java.io.File
import java.util.logging.Logger
/**
* Adb manager. Used to install and uninstall [Apk] files.
*
* @param deviceSerial The serial of the device.
*/
internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial }
?: throw DeviceNotFoundException(deviceSerial)
init {
logger.fine("Established connection to $deviceSerial")
}
/**
* Installs the [Apk] file.
*
* @param apk The [Apk] file.
*/
open fun install(apk: Apk) {
logger.info("Finished installing ${apk.file.name}")
}
/**
* Uninstalls the package.
*
* @param packageName The package name.
*/
open fun uninstall(packageName: String) {
logger.info("Finished uninstalling $packageName")
}
/**
* Closes the [AdbManager] instance.
*/
override fun close() {
logger.fine("Closed")
}
class RootAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
init {
if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed")
}
override fun install(apk: Apk) {
logger.info("Installing by mounting")
val applyReplacement = getPlaceholderReplacement(
apk.packageName ?: throw IllegalArgumentException("Package name is required")
)
device.push(apk.file, TMP_PATH)
device.run("$CREATE_DIR $INSTALLATION_PATH")
device.run(INSTALL_PATCHED_APK.applyReplacement())
device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement())
device.run(INSTALL_MOUNT.applyReplacement())
device.run(UMOUNT.applyReplacement()) // Sanity check.
device.run(MOUNT_PATH.applyReplacement())
device.run(RESTART.applyReplacement())
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
super.install(apk)
}
override fun uninstall(packageName: String) {
logger.info("Uninstalling $packageName by unmounting")
val applyReplacement = getPlaceholderReplacement(packageName)
device.run(UMOUNT.applyReplacement(packageName))
device.run(DELETE.applyReplacement(PATCHED_APK_PATH).applyReplacement())
device.run(DELETE.applyReplacement(MOUNT_PATH).applyReplacement())
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
super.uninstall(packageName)
}
companion object Utils {
private fun getPlaceholderReplacement(with: String): String.() -> String = { replace(PLACEHOLDER, with) }
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
}
}
class UserAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
private val packageManager = PackageManager(device)
override fun install(apk: Apk) {
PackageManager(device).install(apk.file)
super.install(apk)
}
override fun uninstall(packageName: String) {
logger.info("Uninstalling $packageName")
packageManager.uninstall(Package(packageName))
super.uninstall(packageName)
}
}
/**
* Apk file for [AdbManager].
*
* @param file The [Apk] file.
* @param packageName The package name of the [Apk] file.
*/
internal class Apk(val file: File, val packageName: String? = null)
internal class DeviceNotFoundException(deviceSerial: String?) :
Exception(deviceSerial?.let {
"The device with the ADB device serial \"$deviceSerial\" can not be found"
} ?: "No ADB device found")
}

View File

@ -1,11 +0,0 @@
package app.revanced.utils.align
import app.revanced.utils.align.zip.structures.ZipEntry
internal object ZipAligner {
private const val DEFAULT_ALIGNMENT = 4
private const val LIBRARY_ALIGNMENT = 4096
fun getEntryAlignment(entry: ZipEntry): Int? =
if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
}

View File

@ -1,33 +0,0 @@
package app.revanced.utils.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())

View File

@ -1,7 +0,0 @@
package app.revanced.utils.signing
data class SigningOptions(
val cn: String,
val password: String,
val keyStoreFilePath: String
)