refactor: Move ReVanced Library subproject to another repository

This commit removes the subproject ReVanced Library and moves it to another repository. A monorepo turned out to be difficult to work with.
This commit is contained in:
oSumAtrIX 2023-09-23 18:15:10 +02:00
parent 84b24f1b6f
commit 995f2ec99b
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
31 changed files with 73 additions and 1692 deletions

View File

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

View File

@ -1,7 +1,63 @@
plugins { plugins {
kotlin("jvm") version "1.9.0" apply false kotlin("jvm") version "1.9.0"
alias(libs.plugins.shadow)
} }
allprojects { group = "app.revanced"
group = "app.revanced"
dependencies {
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
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 hack gradle-semantic-release-plugin to release this project.
Explanation:
SemVer is a standard for versioning libraries.
For that reason the semantic-release plugin uses the "publish" task to publish libraries.
However, this subproject is not a library, and the "publish" task is not available for this subproject.
Because semantic-release is not designed to handle this case, we need to hack it.
RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
*/
register<DefaultTask>("publish") {
group = "publishing"
description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced CLI"
dependsOn(build)
}
} }

View File

@ -1,27 +1,17 @@
[versions] [versions]
shadow = "8.1.1" shadow = "8.1.1"
apksig = "8.1.1"
bcpkix-jdk18on = "1.76"
jackson-module-kotlin = "2.14.3"
jadb = "2531a28109"
kotlin-reflect = "1.9.0"
kotlin-test = "1.8.20-RC" kotlin-test = "1.8.20-RC"
kotlinx-coroutines-core = "1.7.3" kotlinx-coroutines-core = "1.7.3"
picocli = "4.7.3" picocli = "4.7.3"
revanced-patcher = "15.0.1" revanced-patcher = "15.0.1"
binary-compatibility-validator = "0.13.2" revanced-library = "1.0.0"
[libraries] [libraries]
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcpkix-jdk18on" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" }
jadb = { module = "com.github.revanced:jadb", version.ref = "jadb" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
[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

@ -1,43 +0,0 @@
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)
}
}

View File

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

View File

@ -1,140 +0,0 @@
public final class app/revanced/lib/ApkSigner {
public static final field INSTANCE Lapp/revanced/lib/ApkSigner;
public final fun newApkSignerBuilder (Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder;
public final fun newApkSignerBuilder (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder;
public final fun newKeyStore (Ljava/util/List;)Ljava/security/KeyStore;
public final fun newKeystore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V
public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/lib/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore;
public final fun signApk (Lcom/android/apksig/ApkSigner$Builder;Ljava/io/File;Ljava/io/File;)V
}
public final class app/revanced/lib/ApkSigner$KeyStoreEntry {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getAlias ()Ljava/lang/String;
public final fun getPassword ()Ljava/lang/String;
public final fun getPrivateKeyCertificatePair ()Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;
}
public final class app/revanced/lib/ApkSigner$PrivateKeyCertificatePair {
public fun <init> (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V
public final fun getCertificate ()Ljava/security/cert/X509Certificate;
public final fun getPrivateKey ()Ljava/security/PrivateKey;
}
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/ApkUtils$SigningOptions;)V
}
public final class app/revanced/lib/ApkUtils$SigningOptions {
public fun <init> (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getAlias ()Ljava/lang/String;
public final fun getKeyStore ()Ljava/io/File;
public final fun getKeyStorePassword ()Ljava/lang/String;
public final fun getPassword ()Ljava/lang/String;
public final fun getSigner ()Ljava/lang/String;
}
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 final class app/revanced/lib/PatchUtils {
public static final field INSTANCE Lapp/revanced/lib/PatchUtils;
public final fun getMostCommonCompatibleVersion (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String;
}
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/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

@ -1,78 +0,0 @@
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.jdk18on)
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

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

View File

@ -1,265 +0,0 @@
package app.revanced.lib
import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigInteger
import java.security.*
import java.security.cert.X509Certificate
import java.util.*
import java.util.logging.Logger
import kotlin.time.Duration.Companion.days
/**
* Utility class for writing or reading keystore files and entries as well as signing APK files.
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
object ApkSigner {
private val logger = Logger.getLogger(app.revanced.lib.ApkSigner::class.java.name)
init {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
Security.addProvider(BouncyCastleProvider())
}
/**
* Create a new [PrivateKeyCertificatePair].
*
* @param commonName The common name of the certificate.
* @param validUntil The date until the certificate is valid.
* @return The created [PrivateKeyCertificatePair].
*/
fun newPrivateKeyCertificatePair(
commonName: String = "ReVanced",
validUntil: Date = Date(System.currentTimeMillis() + 356.days.inWholeMilliseconds * 24)
): PrivateKeyCertificatePair {
logger.fine("Creating certificate for $commonName")
// Generate a new key pair.
val keyPair = KeyPairGenerator.getInstance("RSA").apply {
initialize(2048)
}.generateKeyPair()
var serialNumber: BigInteger
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong())
while (serialNumber < BigInteger.ZERO)
val name = X500Name("CN=$commonName")
// Create a new certificate.
val certificate = JcaX509CertificateConverter().getCertificate(
X509v3CertificateBuilder(
name,
serialNumber,
Date(System.currentTimeMillis()),
validUntil,
Locale.ENGLISH,
name,
SubjectPublicKeyInfo.getInstance(keyPair.public.encoded)
).build(JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private))
)
return PrivateKeyCertificatePair(keyPair.private, certificate)
}
/**
* Read a [PrivateKeyCertificatePair] from a keystore entry.
*
* @param keyStore The keystore to read the entry from.
* @param keyStoreEntryAlias The alias of the key store entry to read.
* @param keyStoreEntryPassword The password for recovering the signing key.
* @return The read [PrivateKeyCertificatePair].
* @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid.
*/
fun readKeyCertificatePair(
keyStore: KeyStore,
keyStoreEntryAlias: String,
keyStoreEntryPassword: String,
): PrivateKeyCertificatePair {
logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias")
if (!keyStore.containsAlias(keyStoreEntryAlias))
throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias")
// Read the private key and certificate from the keystore.
val privateKey = try {
keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey
} catch (exception: UnrecoverableKeyException) {
throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias")
}
val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate
return PrivateKeyCertificatePair(privateKey, certificate)
}
/**
* Create a new keystore with a new keypair.
*
* @param entries The entries to add to the keystore.
* @return The created keystore.
* @see KeyStoreEntry
*/
fun newKeyStore(
entries: List<KeyStoreEntry>
): KeyStore {
logger.fine("Creating keystore")
return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply {
entries.forEach { entry ->
load(null)
// Add all entries to the keystore.
setKeyEntry(
entry.alias,
entry.privateKeyCertificatePair.privateKey,
entry.password.toCharArray(),
arrayOf(entry.privateKeyCertificatePair.certificate)
)
}
}
}
/**
* Create a new keystore with a new keypair and saves it to the given [keyStoreOutputStream].
*
* @param keyStoreOutputStream The stream to write the keystore to.
* @param keyStorePassword The password for the keystore.
* @param entries The entries to add to the keystore.
*/
fun newKeystore(
keyStoreOutputStream: OutputStream,
keyStorePassword: String,
entries: List<KeyStoreEntry>
) = newKeyStore(entries).store(
keyStoreOutputStream,
keyStorePassword.toCharArray()
) // Save the keystore.
/**
* Read a keystore from the given [keyStoreInputStream].
*
* @param keyStoreInputStream The stream to read the keystore from.
* @param keyStorePassword The password for the keystore.
* @return The keystore.
* @throws IllegalArgumentException If the keystore password is invalid.
*/
fun readKeyStore(
keyStoreInputStream: InputStream,
keyStorePassword: String?
): KeyStore {
logger.fine("Reading keystore")
return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply {
try {
load(keyStoreInputStream, keyStorePassword?.toCharArray())
} catch (exception: IOException) {
if (exception.cause is UnrecoverableKeyException)
throw IllegalArgumentException("Invalid keystore password")
else
throw exception
}
}
}
/**
* Create a new [ApkSigner.Builder].
*
* @param privateKeyCertificatePair The private key and certificate pair to use for signing.
* @param signer The name of the signer.
* @param createdBy The value for the `Created-By` attribute in the APK's manifest.
* @return The created [ApkSigner.Builder] instance.
*/
fun newApkSignerBuilder(
privateKeyCertificatePair: PrivateKeyCertificatePair,
signer: String,
createdBy: String
): ApkSigner.Builder {
logger.fine(
"Creating new ApkSigner " +
"with $signer as signer and " +
"$createdBy as Created-By attribute in the APK's manifest"
)
// Create the signer config.
val signerConfig = ApkSigner.SignerConfig.Builder(
signer,
privateKeyCertificatePair.privateKey,
listOf(privateKeyCertificatePair.certificate)
).build()
// Create the signer.
return ApkSigner.Builder(listOf(signerConfig)).apply {
setCreatedBy(createdBy)
}
}
/**
* Create a new [ApkSigner.Builder].
*
* @param keyStore The keystore to use for signing.
* @param keyStoreEntryAlias The alias of the key store entry to use for signing.
* @param keyStoreEntryPassword The password for recovering the signing key.
* @param signer The name of the signer.
* @param createdBy The value for the `Created-By` attribute in the APK's manifest.
* @return The created [ApkSigner.Builder] instance.
* @see KeyStoreEntry
* @see PrivateKeyCertificatePair
* @see ApkSigner.Builder.setCreatedBy
* @see ApkSigner.Builder.signApk
*/
fun newApkSignerBuilder(
keyStore: KeyStore,
keyStoreEntryAlias: String,
keyStoreEntryPassword: String,
signer: String,
createdBy: String,
) = newApkSignerBuilder(
readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword),
signer,
createdBy
)
fun ApkSigner.Builder.signApk(input: File, output: File) {
logger.info("Signing ${input.name}")
setInputApk(input)
setOutputApk(output)
build().sign()
}
/**
* An entry in a keystore.
*
* @param alias The alias of the entry.
* @param password The password for recovering the signing key.
* @param privateKeyCertificatePair The private key and certificate pair.
* @see PrivateKeyCertificatePair
*/
class KeyStoreEntry(
val alias: String,
val password: String,
val privateKeyCertificatePair: PrivateKeyCertificatePair = newPrivateKeyCertificatePair()
)
/**
* A private key and certificate pair.
*
* @param privateKey The private key.
* @param certificate The certificate.
*/
class PrivateKeyCertificatePair(
val privateKey: PrivateKey,
val certificate: X509Certificate,
)
}

View File

@ -1,104 +0,0 @@
package app.revanced.lib
import app.revanced.lib.ApkSigner.signApk
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
/**
* Utility functions for working with apks.
*/
@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] file 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,
) {
// Get the keystore from the file or create a new one.
val keyStore = if (signingOptions.keyStore.exists()) {
ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword)
} else {
val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password)
// Create a new keystore with a new keypair and saves it.
ApkSigner.newKeyStore(listOf(entry)).also { keyStore ->
keyStore.store(
signingOptions.keyStore.outputStream(),
signingOptions.keyStorePassword?.toCharArray()
)
}
}
ApkSigner.newApkSignerBuilder(
keyStore,
signingOptions.alias,
signingOptions.password,
signingOptions.signer,
signingOptions.signer
).signApk(apk, output)
}
/**
* Options for signing an apk.
*
* @param keyStore The keystore to use for signing.
* @param keyStorePassword The password for the keystore.
* @param alias The alias of the key store entry to use for signing.
* @param password The password for recovering the signing key.
* @param signer The name of the signer.
*/
class SigningOptions(
val keyStore: File,
val keyStorePassword: String?,
val alias: String = "ReVanced Key",
val password: String = "",
val signer: String = "ReVanced",
)
}

View File

@ -1,107 +0,0 @@
@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 com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File
import java.util.logging.Logger
private typealias PatchList = List<PatchClass>
object Options {
private val logger = Logger.getLogger(Options::class.java.name)
private var mapper = jacksonObjectMapper()
/**
* Serializes the options for the patches in the list.
*
* @param patches The list of patches to serialize.
* @param prettyPrint Whether to pretty print the JSON.
* @return The JSON string containing the options.
*/
fun serialize(patches: PatchSet, prettyPrint: Boolean = false): String = patches
.filter { it.options.any() }
.map { patch ->
Patch(
patch.name!!,
patch.options.values.map { option -> Option(option.key, option.value) }
)
}
// See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7.
.distinctBy { it.patchName }
.let {
if (prettyPrint)
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it)
else
mapper.writeValueAsString(it)
}
/**
* Deserializes the options for the patches in the list.
*
* @param json The JSON string containing the options.
* @return The list of [Patch]s.
* @see Patch
* @see PatchList
*/
fun deserialize(json: String): Array<Patch> = mapper.readValue(json, Array<Patch>::class.java)
/**
* Sets the options for the patches in the list.
*
* @param json The JSON string containing the options.
*/
fun PatchSet.setOptions(json: String) {
filter { it.options.any() }.let { patches ->
if (patches.isEmpty()) return
val patchOptions = deserialize(json)
patches.forEach patch@{ patch ->
patchOptions.find { option -> option.patchName == patch.name!! }?.let {
it.options.forEach { option ->
try {
patch.options[option.key] = option.value
} catch (e: PatchOptionException) {
logger.severe(e.toString())
}
}
}
}
}
}
/**
* Sets the options for the patches in the list.
*
* @param file The file containing the JSON string containing the options.
* @see setOptions
*/
fun PatchSet.setOptions(file: File) = setOptions(file.readText())
/**
* Data class for a patch and its [Option]s.
*
* @property patchName The name of the patch.
* @property options The [Option]s for the patch.
*/
class Patch internal constructor(
val patchName: String,
val options: List<Option>
) {
/**
* Data class for patch option.
*
* @property key The name of the option.
* @property value The value of the option.
*/
class Option internal constructor(val key: String, val value: Any?)
}
}

View File

@ -1,28 +0,0 @@
package app.revanced.lib
import app.revanced.patcher.PatchSet
/**
* Utility functions for working with patches.
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
object PatchUtils {
/**
* Get the version that is most common for [packageName] in the supplied set of [patches].
*
* @param patches The set of patches to check.
* @param packageName The name of the compatible package.
* @return The most common version of.
*/
fun getMostCommonCompatibleVersion(patches: PatchSet, packageName: String) = patches
.mapNotNull {
// Map all patches to their compatible packages with version constraints.
it.compatiblePackages?.firstOrNull { compatiblePackage ->
compatiblePackage.name == packageName && compatiblePackage.versions?.isNotEmpty() == true
}
}
.flatMap { it.versions!! }
.groupingBy { it }
.eachCount()
.maxByOrNull { it.value }?.key
}

View File

@ -1,154 +0,0 @@
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,34 +0,0 @@
package app.revanced.lib.adb
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcess
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
if (su) return shellProcessBuilder("su -c \'$command\'")
val args = command.split(" ") as ArrayList<String>
val cmd = args.removeFirst()
return shellProcessBuilder(cmd, *args.toTypedArray())
}
internal fun JadbDevice.run(command: String, su: Boolean = true): ShellProcess {
return this.buildCommand(command, su).start()!!
}
internal fun JadbDevice.hasSu() =
this.startCommand("su -h", false).waitFor() == 0
internal fun JadbDevice.push(file: File, targetFilePath: String) =
push(file, RemoteFile(targetFilePath))
internal fun JadbDevice.createFile(targetFile: String, content: String) =
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
private fun JadbDevice.startCommand(command: String, su: Boolean) =
shellProcessBuilder(if (su) "su -c '$command'" else command).start()

View File

@ -1,41 +0,0 @@
package app.revanced.lib.adb
internal object Constants {
internal const val PLACEHOLDER = "PLACEHOLDER"
internal const val TMP_PATH = "/data/local/tmp/revanced.tmp"
internal const val INSTALLATION_PATH = "/data/adb/revanced/"
internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk"
internal const val MOUNT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
internal const val DELETE = "rm -rf $PLACEHOLDER"
internal const val CREATE_DIR = "mkdir -p"
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)"
internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " +
"mv $TMP_PATH ${'$'}base_path && " +
"chmod 644 ${'$'}base_path && " +
"chown system:system ${'$'}base_path && " +
"chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
internal const val UMOUNT =
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH"
internal val MOUNT_SCRIPT =
"""
#!/system/bin/sh
MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin
MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
base_path="$PATCHED_APK_PATH"
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
""".trimIndent()
}

View File

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

View File

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

View File

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

View File

@ -1,46 +0,0 @@
package app.revanced.lib
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 org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
internal object PatchOptionsTest {
private var patches = setOf(PatchOptionsTestPatch)
@Test
@Order(1)
fun serializeTest() {
assert(SERIALIZED_JSON == Options.serialize(patches))
}
@Test
@Order(2)
fun loadOptionsTest() {
patches.setOptions(CHANGED_JSON)
assert(PatchOptionsTestPatch.option1 == "test")
assert(PatchOptionsTestPatch.option2 == false)
}
private const val SERIALIZED_JSON =
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":null},{\"key\":\"key2\",\"value\":true}]}]"
private const val CHANGED_JSON =
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]"
object PatchOptionsTestPatch : BytecodePatch(name = "PatchOptionsTestPatch") {
var option1 by stringPatchOption("key1", null, "title1", "description1")
var option2 by booleanPatchOption("key2", true, "title2", "description2")
override fun execute(context: BytecodeContext) {
// Do nothing
}
}
}

View File

@ -1,50 +0,0 @@
package app.revanced.lib
import app.revanced.patcher.PatchSet
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
internal object PatchUtilsTest {
@Test
fun `return 'a' because it is the most common version`() {
val patches = arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d")
.map { version -> newPatch("some.package", version) }
.toSet()
assertEqualsVersion("a", patches, "some.package")
}
@Test
fun `return null because no patches were supplied`() {
assertEqualsVersion(null, emptySet<BytecodePatch>(), "some.package")
}
@Test
fun `return null because no patch is compatible with the supplied package name`() {
val patches = setOf(newPatch("some.package", "a"))
assertEqualsVersion(null, patches, "other.package")
}
@Test
fun `return null because no patch compatible package is constrained to a version`() {
val patches = setOf(
newPatch("other.package"),
newPatch("other.package"),
)
assertEqualsVersion(null, patches, "other.package")
}
private fun assertEqualsVersion(
expected: String?, patches: PatchSet, compatiblePackageName: String
) = assertEquals(expected, PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName))
private fun newPatch(packageName: String, vararg versions: String) = object : BytecodePatch(
compatiblePackages = setOf(CompatiblePackage(packageName, versions.toSet()))
) {
override fun execute(context: BytecodeContext) {}
}
}

View File

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

View File

@ -1,7 +1,7 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.cli.command.utility.UtilityCommand import app.revanced.cli.command.utility.UtilityCommand
import app.revanced.lib.logging.Logger import app.revanced.library.logging.Logger
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
import picocli.CommandLine.IVersionProvider import picocli.CommandLine.IVersionProvider

View File

@ -1,7 +1,7 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.lib.Options import app.revanced.library.Options
import app.revanced.lib.Options.setOptions import app.revanced.library.Options.setOptions
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS

View File

@ -1,9 +1,9 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.lib.ApkUtils import app.revanced.library.ApkUtils
import app.revanced.lib.Options import app.revanced.library.Options
import app.revanced.lib.Options.setOptions import app.revanced.library.Options.setOptions
import app.revanced.lib.adb.AdbManager import app.revanced.library.adb.AdbManager
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher

View File

@ -1,6 +1,6 @@
package app.revanced.cli.command.utility package app.revanced.cli.command.utility
import app.revanced.lib.adb.AdbManager import app.revanced.library.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

View File

@ -1,6 +1,6 @@
package app.revanced.cli.command.utility package app.revanced.cli.command.utility
import app.revanced.lib.adb.AdbManager import app.revanced.library.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