feat: Use ReVanced Library in ReVanced CLI

This commit is contained in:
oSumAtrIX 2023-09-16 19:42:36 +02:00
parent 2b77608651
commit 7794327a11
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
26 changed files with 587 additions and 367 deletions

View File

@ -8,7 +8,8 @@ kotlin-reflect = "1.9.0"
kotlin-test = "1.8.20-RC"
kotlinx-coroutines-core = "1.7.3"
picocli = "4.7.3"
revanced-patcher = "15.0.0-dev.2"
revanced-patcher = "15.0.0-dev.4"
binary-compatibility-validator = "0.13.2"
[libraries]
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]
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

@ -5,15 +5,10 @@ plugins {
dependencies {
implementation(project(":revanced-lib"))
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)
}

View File

@ -1,52 +1,27 @@
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.*
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)
}
Logger.setDefault()
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")}")
}
}
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(

View File

@ -1,8 +1,8 @@
package app.revanced.cli.command
import app.revanced.lib.Options
import app.revanced.lib.Options.setOptions
import app.revanced.patcher.PatchBundleLoader
import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions
import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File
@ -37,10 +37,17 @@ internal object OptionsCommand : Runnable {
)
private var update: Boolean = false
override fun run() = if (!filePath.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) {
if (update && filePath.exists()) setOptions(filePath)
override fun run() = try {
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
import app.revanced.patcher.*
import app.revanced.utils.Options
import app.revanced.utils.Options.setOptions
import app.revanced.utils.adb.AdbManager
import app.revanced.utils.align.ZipAligner
import app.revanced.utils.align.zip.ZipFile
import app.revanced.utils.align.zip.structures.ZipEntry
import app.revanced.utils.signing.ApkSigner
import app.revanced.utils.signing.SigningOptions
import app.revanced.lib.ApkUtils
import app.revanced.lib.Options
import app.revanced.lib.Options.setOptions
import app.revanced.lib.adb.AdbManager
import app.revanced.lib.signing.SigningOptions
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import kotlinx.coroutines.runBlocking
import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS
import picocli.CommandLine.Model.CommandSpec
import picocli.CommandLine.Spec
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
@ -24,21 +26,15 @@ import java.util.logging.Logger
internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(PatchCommand::class.java.name)
@CommandLine.Parameters(
description = ["APK file to be patched"], arity = "1..1"
)
@Spec
lateinit var spec: CommandSpec // injected by picocli
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 patchBundles = emptyList<File>()
@CommandLine.Option(
names = ["-i", "--include"], description = ["List of patches to include"]
)
@ -94,7 +90,7 @@ internal object PatchCommand : Runnable {
@CommandLine.Option(
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(
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")
@CommandLine.Option(
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
)
private var aaptBinaryPath = File("")
private var aaptBinaryPath: File? = null
@CommandLine.Option(
names = ["-p", "--purge"],
@ -120,36 +113,58 @@ internal object PatchCommand : Runnable {
)
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
)
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"]
)
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() {
// region Prepare
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
val adbManager = deviceSerial?.let { serial -> AdbManager.getAdbManager(serial, mount) }
// region Load patches
logger.info("Loading patches")
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
val integrations = integrations
logger.info("Setting patch options")
@ -160,57 +175,62 @@ internal object PatchCommand : Runnable {
// endregion
// region Patch
val patcher = Patcher(
Patcher(
PatcherOptions(
apk,
resourceCachePath,
aaptBinaryPath.path,
aaptBinaryPath?.path,
resourceCachePath.absolutePath,
)
)
).use { patcher ->
// region Patch
val result = patcher.apply {
acceptIntegrations(integrations)
acceptPatches(filterPatchSelection(patches))
val patcherResult = patcher.apply {
acceptIntegrations(integrations)
acceptPatches(filterPatchSelection(patches))
// Execute patches.
runBlocking {
apply(false).collect { patchResult ->
patchResult.exception?.let {
StringWriter().use { writer ->
it.printStackTrace(PrintWriter(writer))
logger.severe("${patchResult.patch.name} failed:\n$writer")
}
} ?: logger.info("${patchResult.patch.name} succeeded")
// Execute patches.
runBlocking {
apply(false).collect { patchResult ->
patchResult.exception?.let {
StringWriter().use { writer ->
it.printStackTrace(PrintWriter(writer))
logger.severe("${patchResult.patch.name} failed:\n$writer")
}
} ?: logger.info("${patchResult.patch.name} succeeded")
}
}
}
}.get()
}.get()
patcher.close()
// endregion
// endregion
// region Save
// region Finish
val alignAndSignedFile = sign(
apk.newAlignedFile(
result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
ApkUtils.alignAndSign(
apk,
outputFilePath,
SigningOptions(
commonName,
password,
keystoreFilePath ?: outputFilePath.parentFile
.resolve("${outputFilePath.nameWithoutExtension}.keystore"),
),
patcherResult
)
)
logger.info("Copying to ${outputFilePath.name}")
alignAndSignedFile.copyTo(outputFilePath, overwrite = true)
// endregion
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
// region Install
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
// endregion
}
if (purge) {
logger.info("Purging temporary files")
purge(resourceCachePath)
}
// endregion
}
@ -279,64 +299,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) {
val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory"
else "Failed to purge resource cache directory"

View File

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

View File

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

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
)

View File

@ -0,0 +1,111 @@
public final class app/revanced/lib/ApkUtils {
public static final field INSTANCE Lapp/revanced/lib/ApkUtils;
public final fun alignAndSign (Ljava/io/File;Ljava/io/File;Lapp/revanced/lib/signing/SigningOptions;Lapp/revanced/patcher/PatcherResult;)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$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/Function2;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/ZipAligner {
public static final field INSTANCE Lapp/revanced/lib/zip/ZipAligner;
public final fun getEntryAlignment (Lapp/revanced/lib/zip/structures/ZipEntry;)Ljava/lang/Integer;
}
public final class app/revanced/lib/zip/ZipFile : java/io/Closeable {
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/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

@ -1,18 +1,21 @@
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)
}
kotlin { jvmToolchain(11) }
java {
withSourcesJar()
}
tasks {
test {
useJUnitPlatform()
@ -22,6 +25,12 @@ tasks {
}
}
kotlin { jvmToolchain(11) }
java {
withSourcesJar()
}
publishing {
repositories {
mavenLocal()
@ -42,7 +51,7 @@ publishing {
pom {
name = "ReVanced Library"
description = "Library for ReVanced"
description = "Library containing common utilities for ReVanced"
url = "https://revanced.app"
licenses {

View File

@ -0,0 +1,84 @@
package app.revanced.lib
import app.revanced.lib.signing.ApkSigner
import app.revanced.lib.signing.SigningOptions
import app.revanced.lib.zip.ZipAligner
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
object ApkUtils {
private val logger = Logger.getLogger(ApkUtils::class.java.name)
/**
* Aligns and signs the apk at [apkFile] and writes it to [outputFile].
*
* @param apkFile The apk to align and sign.
* @param outputFile The apk to write the aligned and signed apk to.
* @param signingOptions The options to use for signing.
* @param patchedEntriesSource The result of the patcher to add the patched dex files and resources.
*/
fun alignAndSign(
apkFile: File,
outputFile: File,
signingOptions: SigningOptions,
patchedEntriesSource: PatcherResult
) {
if (outputFile.exists()) outputFile.delete()
align(apkFile, outputFile, patchedEntriesSource)
sign(outputFile, outputFile, signingOptions)
}
/**
* 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.
*/
private fun align(apkFile: File, outputFile: File, patchedEntriesSource: PatcherResult) {
logger.info("Aligning ${apkFile.name}")
ZipFile(outputFile).use { file ->
patchedEntriesSource.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry(it.name), it.stream.readBytes()
)
}
patchedEntriesSource.resourceFile?.let {
file.copyEntriesFromFileAligned(
ZipFile(it), ZipAligner::getEntryAlignment
)
}
// TODO: Do not compress result.doNotCompress
// TODO: Fix copying resources that are not needed anymore.
file.copyEntriesFromFileAligned(
ZipFile(apkFile), ZipAligner::getEntryAlignment
)
}
}
/**
* 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.
*/
private 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.patch.options.PatchOptionException
import app.revanced.utils.Options.PatchOption.Option
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File
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 var mapper = jacksonObjectMapper()
@ -24,7 +28,7 @@ internal object Options {
fun serialize(patches: PatchSet, prettyPrint: Boolean = false): String = patches
.filter { it.options.any() }
.map { patch ->
PatchOption(
Patch(
patch.name!!,
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.
*
* @param json The JSON string containing the options.
* @return The list of [PatchOption]s.
* @see PatchOption
* @return The list of [Patch]s.
* @see Patch
* @see PatchList
*/
@Suppress("MemberVisibilityCanBePrivate")
fun deserialize(json: String): Array<PatchOption> = mapper.readValue(json, Array<PatchOption>::class.java)
fun deserialize(json: String): Array<Patch> = mapper.readValue(json, Array<Patch>::class.java)
/**
* Sets the options for the patches in the list.
@ -88,7 +91,7 @@ internal object Options {
* @property patchName The name of the patch.
* @property options The [Option]s for the patch.
*/
internal data class PatchOption(
class Patch internal constructor(
val patchName: String,
val options: List<Option>
) {
@ -99,6 +102,6 @@ internal object Options {
* @property key The name 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

@ -1,22 +1,21 @@
package app.revanced.utils.adb
package app.revanced.lib.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 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.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.managers.Package
import se.vidstige.jadb.managers.PackageManager
import java.io.Closeable
import java.io.File
import java.util.logging.Logger
@ -25,7 +24,7 @@ import java.util.logging.Logger
*
* @param deviceSerial The serial of the device.
*/
internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
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 }
@ -53,14 +52,20 @@ internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
logger.info("Finished uninstalling $packageName")
}
/**
* Closes the [AdbManager] instance.
*/
override fun close() {
logger.fine("Closed")
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(deviceSerial: String) : AdbManager(deviceSerial) {
class RootAdbManager internal constructor(deviceSerial: String) : AdbManager(deviceSerial) {
init {
if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed")
}
@ -107,7 +112,7 @@ internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
}
}
class UserAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
class UserAdbManager internal constructor(deviceSerial: String) : AdbManager(deviceSerial) {
private val packageManager = PackageManager(device)
override fun install(apk: Apk) {
@ -131,9 +136,9 @@ internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
* @param file The [Apk] file.
* @param packageName The package name of the [Apk] file.
*/
internal class Apk(val file: File, val packageName: String? = null)
class Apk(val file: File, val packageName: String? = null)
internal class DeviceNotFoundException(deviceSerial: String?) :
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")

View File

@ -1,4 +1,4 @@
package app.revanced.utils.adb
package app.revanced.lib.adb
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile

View File

@ -1,4 +1,4 @@
package app.revanced.utils.adb
package app.revanced.lib.adb
internal object Constants {
internal const val PLACEHOLDER = "PLACEHOLDER"

View File

@ -0,0 +1,78 @@
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) -> Unit,
flushHandler: () -> Unit,
closeHandler: () -> Unit
) = object : Handler() {
override fun publish(record: LogRecord) = publishHandler(formatter.format(record), record.level)
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 = { log: String, level: Level ->
log.toByteArray().let {
if (level.intValue() > Level.INFO.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 org.bouncycastle.asn1.x500.X500Name
@ -17,10 +17,10 @@ import java.security.cert.X509Certificate
import java.util.*
import java.util.logging.Logger
internal class ApkSigner(
class ApkSigner(
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 passwordCharArray = signingOptions.password.toCharArray()
@ -30,7 +30,7 @@ internal class ApkSigner(
val keyStore = KeyStore.getInstance("BKS", "BC")
val alias = keyStore.let { store ->
FileInputStream(File(signingOptions.keyStoreFilePath).also {
FileInputStream(signingOptions.keyStoreOutputFilePath.also {
if (!it.exists()) {
logger.info("Creating keystore at ${it.absolutePath}")
newKeystore(it)
@ -43,13 +43,13 @@ internal class ApkSigner(
with(
ApkSigner.SignerConfig.Builder(
signingOptions.cn,
signingOptions.commonName,
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
listOf(keyStore.getCertificate(alias) as X509Certificate)
).build()
) {
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()
var serialNumber: BigInteger
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(
x500Name,
serialNumber,
@ -81,12 +81,10 @@ internal class ApkSigner(
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.setOutputApk(output)
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,8 +1,8 @@
package app.revanced.utils.align
package app.revanced.lib.zip
import app.revanced.utils.align.zip.structures.ZipEntry
import app.revanced.lib.zip.structures.ZipEntry
internal object ZipAligner {
object ZipAligner {
private const val DEFAULT_ALIGNMENT = 4
private const val LIBRARY_ALIGNMENT = 4096

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.utils.align.zip.structures.ZipEntry
import app.revanced.lib.zip.structures.ZipEndRecord
import app.revanced.lib.zip.structures.ZipEntry
import java.io.Closeable
import java.io.File
import java.io.RandomAccessFile

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.utils.align.zip.putUShort
import app.revanced.utils.align.zip.readUIntLE
import app.revanced.utils.align.zip.readUShortLE
import app.revanced.lib.zip.putUInt
import app.revanced.lib.zip.putUShort
import app.revanced.lib.zip.readUIntLE
import app.revanced.lib.zip.readUShortLE
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
data class ZipEndRecord(
internal class ZipEndRecord(
val diskNumber: UShort,
val startingDiskNumber: 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.nio.ByteBuffer
import java.nio.ByteOrder
data class ZipEntry(
val version: UShort,
val versionNeeded: UShort,
val flags: UShort,
var compression: UShort,
val modificationTime: UShort,
val modificationDate: UShort,
var crc32: UInt,
var compressedSize: UInt,
var uncompressedSize: UInt,
val diskNumber: UShort,
val internalAttributes: UShort,
val externalAttributes: UInt,
var localHeaderOffset: UInt,
val fileName: String,
val extraField: ByteArray,
val fileComment: String,
var localExtraField: ByteArray = ByteArray(0), //separate for alignment
class ZipEntry private constructor(
internal val version: UShort,
internal val versionNeeded: UShort,
internal val flags: UShort,
internal var compression: UShort,
internal val modificationTime: UShort,
internal val modificationDate: UShort,
internal var crc32: UInt,
internal var compressedSize: UInt,
internal var uncompressedSize: UInt,
internal val diskNumber: UShort,
internal val internalAttributes: UShort,
internal val externalAttributes: UInt,
internal var localHeaderOffset: UInt,
internal val fileName: String,
internal val extraField: ByteArray,
internal val fileComment: String,
internal var localExtraField: ByteArray = ByteArray(0), //separate for alignment
) {
val LFHSize: Int
internal val LFHSize: Int
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
val dataOffset: UInt
internal val dataOffset: UInt
get() = localHeaderOffset + LFHSize.toUInt()
constructor(fileName: String) : this(
0x1403u, //made by unix, version 20
0u,
0u,
0u,
0x0821u, //seems to be static time google uses, no idea
0x0221u, //same as above
0u,
0u,
0u,
0u,
0u,
0u,
0u,
fileName,
ByteArray(0),
""
)
companion object {
const val CDE_HEADER_SIZE = 46
const val CDE_SIGNATURE = 0x02014b50u
internal const val CDE_HEADER_SIZE = 46
internal const val CDE_SIGNATURE = 0x02014b50u
const val LFH_HEADER_SIZE = 30
const val LFH_SIGNATURE = 0x04034b50u
internal const val LFH_HEADER_SIZE = 30
internal const val LFH_SIGNATURE = 0x04034b50u
fun createWithName(fileName: String): 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 {
internal fun fromCDE(input: DataInput): ZipEntry {
val signature = input.readUIntLE()
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)
localExtraField = ByteArray(buffer.getUShort().toInt())
}
fun toLFH(): ByteBuffer {
internal fun toLFH(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
@ -153,7 +151,7 @@ data class ZipEntry(
return buffer
}
fun toCDE(): ByteBuffer {
internal fun toCDE(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)

View File

@ -1 +0,0 @@
version=${projectVersion}

View File

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