diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2d6d258 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{kt,kts}] +ktlint_code_style = intellij_idea +ktlint_standard_no-wildcard-imports = disabled \ No newline at end of file diff --git a/api/revanced-library.api b/api/revanced-library.api index ba73750..bc811e1 100644 --- a/api/revanced-library.api +++ b/api/revanced-library.api @@ -1,14 +1,13 @@ public final class app/revanced/library/ApkSigner { public static final field INSTANCE Lapp/revanced/library/ApkSigner; - public final fun newApkSignerBuilder (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; - public final fun 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/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V - public final fun newKeyStore (Ljava/util/List;)Ljava/security/KeyStore; + public final fun newApkSigner (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer; + public final fun newApkSigner (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$Signer; + public final fun newKeyStore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Set;)V + public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore; public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/library/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; - public final fun signApk (Lcom/android/apksig/ApkSigner$Builder;Ljava/io/File;Ljava/io/File;)V } public final class app/revanced/library/ApkSigner$KeyStoreEntry { @@ -25,10 +24,16 @@ public final class app/revanced/library/ApkSigner$PrivateKeyCertificatePair { public final fun getPrivateKey ()Ljava/security/PrivateKey; } +public final class app/revanced/library/ApkSigner$Signer { + public final fun getSigningExtension ()Lcom/android/tools/build/apkzlib/sign/SigningExtension; + public final fun signApk (Lcom/android/tools/build/apkzlib/zip/ZFile;)V + public final fun signApk (Ljava/io/File;)V +} + public final class app/revanced/library/ApkUtils { public static final field INSTANCE Lapp/revanced/library/ApkUtils; - public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V - public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V + public final fun sign (Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V + public final fun writePatcherResult (Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V } public final class app/revanced/library/ApkUtils$SigningOptions { @@ -162,23 +167,3 @@ public final class app/revanced/library/logging/Logger { public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V } -public final class app/revanced/library/zip/ZipFile : java/io/Closeable { - public static final field ApkZipFile Lapp/revanced/library/zip/ZipFile$ApkZipFile; - public fun (Ljava/io/File;)V - public final fun addEntryCompressData (Lapp/revanced/library/zip/structures/ZipEntry;[B)V - public fun close ()V - public final fun copyEntriesFromFileAligned (Lapp/revanced/library/zip/ZipFile;Lkotlin/jvm/functions/Function1;)V -} - -public final class app/revanced/library/zip/ZipFile$ApkZipFile { - public final fun getApkZipEntryAlignment ()Lkotlin/jvm/functions/Function1; -} - -public final class app/revanced/library/zip/structures/ZipEntry { - public static final field Companion Lapp/revanced/library/zip/structures/ZipEntry$Companion; - public fun (Ljava/lang/String;)V -} - -public final class app/revanced/library/zip/structures/ZipEntry$Companion { -} - diff --git a/build.gradle.kts b/build.gradle.kts index 4136d7a..c21fa79 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,10 +16,11 @@ repositories { dependencies { implementation(libs.revanced.patcher) implementation(libs.kotlin.reflect) - implementation(libs.jadb) // Updated fork - implementation(libs.apksig) - implementation(libs.bcpkix.jdk18on) + implementation(libs.jadb) // Fork with Shell v2 support. implementation(libs.jackson.module.kotlin) + implementation(libs.apkzlib) + implementation(libs.bcpkix.jdk15on) + implementation(libs.guava) testImplementation(libs.revanced.patcher) testImplementation(libs.kotlin.test) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37286d1..dd635c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,23 @@ [versions] -apksig = "8.1.4" -bcpkix-jdk18on = "1.76" -jackson-module-kotlin = "2.14.3" +jackson-module-kotlin = "2.15.0" jadb = "1.2.1" -kotlin-reflect = "1.9.20" -kotlin-test = "1.9.20" -revanced-patcher = "19.2.0" -binary-compatibility-validator = "0.13.2" +kotlin-reflect = "1.9.22" +kotlin-test = "1.9.22" +revanced-patcher = "19.3.1" +binary-compatibility-validator = "0.14.0" +apkzlib = "8.2.2" +bcpkix-jdk15on = "1.70" +guava = "33.0.0-jre" [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 = "app.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" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } +apkzlib = { module = "com.android.tools.build:apkzlib", version.ref = "apkzlib" } +bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } [plugins] binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } diff --git a/src/main/kotlin/app/revanced/library/ApkSigner.kt b/src/main/kotlin/app/revanced/library/ApkSigner.kt index 3908422..75ce9fb 100644 --- a/src/main/kotlin/app/revanced/library/ApkSigner.kt +++ b/src/main/kotlin/app/revanced/library/ApkSigner.kt @@ -1,11 +1,12 @@ package app.revanced.library -import com.android.apksig.ApkSigner +import com.android.tools.build.apkzlib.sign.SigningExtension +import com.android.tools.build.apkzlib.sign.SigningOptions +import com.android.tools.build.apkzlib.zip.ZFile import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.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 @@ -19,23 +20,18 @@ 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. + * Utility class for reading or writing keystore files and entries as well as signing APK files. */ @Suppress("MemberVisibilityCanBePrivate", "unused") object ApkSigner { - private val logger = Logger.getLogger(app.revanced.library.ApkSigner::class.java.name) - - init { - if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { - Security.addProvider(BouncyCastleProvider()) - } - } + private val logger = Logger.getLogger(Signer::class.java.name) /** * 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( @@ -79,7 +75,9 @@ object ApkSigner { * @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( @@ -111,13 +109,15 @@ object ApkSigner { * 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): KeyStore { + fun newKeyStore(entries: Set): KeyStore { logger.fine("Creating keystore") - return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { + return KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null) entries.forEach { entry -> @@ -142,18 +142,20 @@ object ApkSigner { fun newKeyStore( keyStoreOutputStream: OutputStream, keyStorePassword: String, - entries: List, + entries: Set, ) = 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( @@ -162,7 +164,7 @@ object ApkSigner { ): KeyStore { logger.fine("Reading keystore") - return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { + return KeyStore.getInstance(KeyStore.getDefaultType()).apply { try { load(keyStoreInputStream, keyStorePassword?.toCharArray()) } catch (exception: IOException) { @@ -176,75 +178,45 @@ object ApkSigner { } /** - * Create a new [ApkSigner.Builder]. + * Create a new [Signer]. * * @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. + * + * @return The new [Signer]. + * + * @see PrivateKeyCertificatePair + * @see Signer */ - 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", + fun newApkSigner(privateKeyCertificatePair: PrivateKeyCertificatePair) = + Signer( + SigningExtension( + SigningOptions.builder() + .setMinSdkVersion(21) // TODO: Extracting from the target APK would be ideal. + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setCertificates(privateKeyCertificatePair.certificate) + .setKey(privateKeyCertificatePair.privateKey) + .build(), + ), ) - // Create the signer config. - val signerConfig = - ApkSigner.SignerConfig.Builder( - signer, - privateKeyCertificatePair.privateKey, - listOf(privateKeyCertificatePair.certificate), - ).build() - - // Create the signer. - return ApkSigner.Builder(listOf(signerConfig)).apply { - setCreatedBy(createdBy) - } - } - /** - * Create a new [ApkSigner.Builder]. + * Create a new [Signer]. * * @param keyStore The keystore to use for signing. * @param 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 + * + * @return The new [Signer]. + * + * @see KeyStore + * @see Signer */ - fun newApkSignerBuilder( + fun newApkSigner( 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() - } + ) = newApkSigner(readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword)) /** * An entry in a keystore. @@ -252,6 +224,7 @@ object ApkSigner { * @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( @@ -270,4 +243,24 @@ object ApkSigner { val privateKey: PrivateKey, val certificate: X509Certificate, ) + + class Signer internal constructor(val signingExtension: SigningExtension) { + /** + * Sign an APK file. + * + * @param apkFile The APK file to sign. + */ + fun signApk(apkFile: File) = ZFile.openReadWrite(apkFile).use { signApk(it) } + + /** + * Sign an APK file. + * + * @param apkZFile The APK [ZFile] to sign. + */ + fun signApk(apkZFile: ZFile) { + logger.info("Signing ${apkZFile.file.name}") + + signingExtension.register(apkZFile) + } + } } diff --git a/src/main/kotlin/app/revanced/library/ApkUtils.kt b/src/main/kotlin/app/revanced/library/ApkUtils.kt index 542d2db..f2e43f1 100644 --- a/src/main/kotlin/app/revanced/library/ApkUtils.kt +++ b/src/main/kotlin/app/revanced/library/ApkUtils.kt @@ -1,96 +1,124 @@ package app.revanced.library -import app.revanced.library.ApkSigner.signApk -import app.revanced.library.zip.ZipFile -import app.revanced.library.zip.structures.ZipEntry import app.revanced.patcher.PatcherResult +import com.android.tools.build.apkzlib.zip.AlignmentRules +import com.android.tools.build.apkzlib.zip.StoredEntry +import com.android.tools.build.apkzlib.zip.ZFile +import com.android.tools.build.apkzlib.zip.ZFileOptions import java.io.File import java.util.logging.Logger -import kotlin.io.path.deleteIfExists /** - * Utility functions for working with apks. + * Utility functions to work with APK files. */ @Suppress("MemberVisibilityCanBePrivate", "unused") object ApkUtils { private val logger = Logger.getLogger(ApkUtils::class.java.name) + private const val LIBRARY_EXTENSION = ".so" + + // Alignment for native libraries. + private const val LIBRARY_ALIGNMENT = 1024 * 4 + + // Alignment for all other files. + private const val DEFAULT_ALIGNMENT = 4 + + // Prefix for resources. + private const val RES_PREFIX = "res/" + + private val zFileOptions = + ZFileOptions().setAlignmentRule( + AlignmentRules.compose( + AlignmentRules.constantForSuffix(LIBRARY_EXTENSION, LIBRARY_ALIGNMENT), + AlignmentRules.constant(DEFAULT_ALIGNMENT), + ), + ) + /** - * Creates a new apk from [apkFile] and [patchedEntriesSource] and writes it to [outputFile]. + * Applies the [PatcherResult] to the given [apkFile]. * - * @param apkFile The apk to copy entries from. - * @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. + * The order of operation is as follows: + * 1. Write patched dex files. + * 2. Delete all resources in the target APK + * 3. Merge resources.apk compiled by AAPT. + * 4. Write raw resources. + * 5. Delete resources staged for deletion. + * 6. Realign the APK. + * + * @param apkFile The file to apply the patched files to. */ - fun copyAligned( - apkFile: File, - outputFile: File, - patchedEntriesSource: PatcherResult, - ) { - logger.info("Aligning ${apkFile.name}") - - outputFile.toPath().deleteIfExists() - - ZipFile(outputFile).use { file -> - patchedEntriesSource.dexFiles.forEach { - file.addEntryCompressData( - ZipEntry(it.name), - it.stream.readBytes(), - ) + fun PatcherResult.applyTo(apkFile: File) { + ZFile.openReadWrite(apkFile, zFileOptions).use { targetApkZFile -> + dexFiles.forEach { dexFile -> + targetApkZFile.add(dexFile.name, dexFile.stream) } - patchedEntriesSource.resourceFile?.let { - file.copyEntriesFromFileAligned( - ZipFile(it), - ZipFile.apkZipEntryAlignment, - ) + resources?.let { resources -> + // Add resources compiled by AAPT. + resources.resourcesApk?.let { resourcesApk -> + ZFile.openReadOnly(resourcesApk).use { resourcesApkZFile -> + // Delete all resources in the target APK before merging the new ones. + // This is necessary because the resources.apk renames resources. + // So unless, the old resources are deleted, there will be orphaned resources in the APK. + // It is not necessary, but for the sake of cleanliness, it is done. + targetApkZFile.entries().filter { entry -> + entry.centralDirectoryHeader.name.startsWith(RES_PREFIX) + }.forEach(StoredEntry::delete) + + targetApkZFile.mergeFrom(resourcesApkZFile) { false } + } + } + + // Add resources not compiled by AAPT. + resources.otherResources?.let { otherResources -> + targetApkZFile.addAllRecursively(otherResources) { file -> + file.relativeTo(otherResources).invariantSeparatorsPath !in resources.doNotCompress + } + } + + // Delete resources that were staged for deletion. + if (resources.deleteResources.isNotEmpty()) { + targetApkZFile.entries().filter { entry -> + resources.deleteResources.any { shouldDelete -> shouldDelete(entry.centralDirectoryHeader.name) } + }.forEach(StoredEntry::delete) + } } - // TODO: Do not compress result.doNotCompress + logger.info("Aligning APK") - // TODO: Fix copying resources that are not needed anymore. - file.copyEntriesFromFileAligned( - ZipFile(apkFile), - ZipFile.apkZipEntryAlignment, - ) + targetApkZFile.realign() + + logger.fine("Writing changes") } } /** - * Signs the [apk] file and writes it to [output]. + * Signs the apk file with the given options. * - * @param apk The apk to sign. - * @param output The apk to write the signed apk to. * @param signingOptions The options to use for signing. */ - fun sign( - apk: File, - output: File, - signingOptions: SigningOptions, - ) { + fun File.sign(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) + ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword ?: "") } else { - val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password) + val entries = setOf(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( + ApkSigner.newKeyStore(entries).apply { + store( signingOptions.keyStore.outputStream(), signingOptions.keyStorePassword?.toCharArray(), ) } } - ApkSigner.newApkSignerBuilder( + ApkSigner.newApkSigner( keyStore, signingOptions.alias, signingOptions.password, - signingOptions.signer, - signingOptions.signer, - ).signApk(apk, output) + ).signApk(this) } /** diff --git a/src/main/kotlin/app/revanced/library/PatchUtils.kt b/src/main/kotlin/app/revanced/library/PatchUtils.kt index ff5c9b3..fc26311 100644 --- a/src/main/kotlin/app/revanced/library/PatchUtils.kt +++ b/src/main/kotlin/app/revanced/library/PatchUtils.kt @@ -20,35 +20,6 @@ typealias PackageNameMap = Map */ @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. - */ - @Deprecated( - "Use getMostCommonCompatibleVersions instead.", - ReplaceWith( - "getMostCommonCompatibleVersions(patches, setOf(packageName))" + - ".entries.firstOrNull()?.value?.keys?.firstOrNull()", - ), - ) - 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 - /** * Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version. * diff --git a/src/main/kotlin/app/revanced/library/zip/Extensions.kt b/src/main/kotlin/app/revanced/library/zip/Extensions.kt deleted file mode 100644 index 403e899..0000000 --- a/src/main/kotlin/app/revanced/library/zip/Extensions.kt +++ /dev/null @@ -1,42 +0,0 @@ -package app.revanced.library.zip - -import java.io.DataInput -import java.io.DataOutput -import java.nio.ByteBuffer - -internal fun UInt.toLittleEndian() = - (((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt() - -internal fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort() - -internal fun UInt.toBigEndian() = - ( - ((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8) - or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24) - ).toUInt() - -internal fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort() - -internal fun ByteBuffer.getUShort() = this.getShort().toUShort() - -internal fun ByteBuffer.getUInt() = this.getInt().toUInt() - -internal fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort()) - -internal fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt()) - -internal fun DataInput.readUShort() = this.readShort().toUShort() - -internal fun DataInput.readUInt() = this.readInt().toUInt() - -internal fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt()) - -internal fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt()) - -internal fun DataInput.readUShortLE() = this.readUShort().toBigEndian() - -internal fun DataInput.readUIntLE() = this.readUInt().toBigEndian() - -internal fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian()) - -internal fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian()) diff --git a/src/main/kotlin/app/revanced/library/zip/ZipFile.kt b/src/main/kotlin/app/revanced/library/zip/ZipFile.kt deleted file mode 100644 index 2063543..0000000 --- a/src/main/kotlin/app/revanced/library/zip/ZipFile.kt +++ /dev/null @@ -1,218 +0,0 @@ -package app.revanced.library.zip - -import app.revanced.library.zip.structures.ZipEndRecord -import app.revanced.library.zip.structures.ZipEntry -import java.io.Closeable -import java.io.File -import java.io.RandomAccessFile -import java.nio.ByteBuffer -import java.nio.channels.FileChannel -import java.util.zip.CRC32 -import java.util.zip.Deflater - -class ZipFile(file: File) : Closeable { - private var entries: MutableList = mutableListOf() - - // Open file for writing if it doesn't exist (because the intention is to write) or is writable. - private val filePointer: RandomAccessFile = - RandomAccessFile( - file, - if (!file.exists() || file.canWrite()) "rw" else "r", - ) - - private var centralDirectoryNeedsRewrite = false - - private val compressionLevel = 5 - - init { - // If file isn't empty try to load entries. - if (file.length() > 0) { - val endRecord = findEndRecord() - - if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries) { - throw IllegalArgumentException("Multi-file archives are not supported") - } - - entries = readEntries(endRecord).toMutableList() - } - - // Seek back to start for writing. - filePointer.seek(0) - } - - private fun findEndRecord(): ZipEndRecord { - // Look from end to start since end record is at the end. - for (i in filePointer.length() - 1 downTo 0) { - filePointer.seek(i) - // Possible beginning of signature. - if (filePointer.readByte() == 0x50.toByte()) { - // Seek back to get the full int. - filePointer.seek(i) - val possibleSignature = filePointer.readUIntLE() - if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { - filePointer.seek(i) - return ZipEndRecord.fromECD(filePointer) - } - } - } - - throw Exception("Couldn't find end record") - } - - private fun readEntries(endRecord: ZipEndRecord): List { - 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 - } - } - } -} diff --git a/src/main/kotlin/app/revanced/library/zip/structures/ZipEndRecord.kt b/src/main/kotlin/app/revanced/library/zip/structures/ZipEndRecord.kt deleted file mode 100644 index d7fe06d..0000000 --- a/src/main/kotlin/app/revanced/library/zip/structures/ZipEndRecord.kt +++ /dev/null @@ -1,77 +0,0 @@ -package app.revanced.library.zip.structures - -import app.revanced.library.zip.putUInt -import app.revanced.library.zip.putUShort -import app.revanced.library.zip.readUIntLE -import app.revanced.library.zip.readUShortLE -import java.io.DataInput -import java.nio.ByteBuffer -import java.nio.ByteOrder - -internal class ZipEndRecord( - val diskNumber: UShort, - val startingDiskNumber: UShort, - val diskEntries: UShort, - val totalEntries: UShort, - val centralDirectorySize: UInt, - val centralDirectoryStartOffset: UInt, - val fileComment: String, -) { - companion object { - const val ECD_HEADER_SIZE = 22 - const val ECD_SIGNATURE = 0x06054b50u - - fun fromECD(input: DataInput): ZipEndRecord { - val signature = input.readUIntLE() - - if (signature != ECD_SIGNATURE) { - throw IllegalArgumentException("Input doesn't start with end record signature") - } - - val diskNumber = input.readUShortLE() - val startingDiskNumber = input.readUShortLE() - val diskEntries = input.readUShortLE() - val totalEntries = input.readUShortLE() - val centralDirectorySize = input.readUIntLE() - val centralDirectoryStartOffset = input.readUIntLE() - val fileCommentLength = input.readUShortLE() - var fileComment = "" - - if (fileCommentLength > 0u) { - val fileCommentBytes = ByteArray(fileCommentLength.toInt()) - input.readFully(fileCommentBytes) - fileComment = fileCommentBytes.toString(Charsets.UTF_8) - } - - return ZipEndRecord( - diskNumber, - startingDiskNumber, - diskEntries, - totalEntries, - centralDirectorySize, - centralDirectoryStartOffset, - fileComment, - ) - } - } - - fun toECD(): ByteBuffer { - val commentBytes = fileComment.toByteArray(Charsets.UTF_8) - - val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size).also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(ECD_SIGNATURE) - buffer.putUShort(diskNumber) - buffer.putUShort(startingDiskNumber) - buffer.putUShort(diskEntries) - buffer.putUShort(totalEntries) - buffer.putUInt(centralDirectorySize) - buffer.putUInt(centralDirectoryStartOffset) - buffer.putUShort(commentBytes.size.toUShort()) - - buffer.put(commentBytes) - - buffer.flip() - return buffer - } -} diff --git a/src/main/kotlin/app/revanced/library/zip/structures/ZipEntry.kt b/src/main/kotlin/app/revanced/library/zip/structures/ZipEntry.kt deleted file mode 100644 index fb04f1b..0000000 --- a/src/main/kotlin/app/revanced/library/zip/structures/ZipEntry.kt +++ /dev/null @@ -1,192 +0,0 @@ -package app.revanced.library.zip.structures - -import app.revanced.library.zip.* -import java.io.DataInput -import java.nio.ByteBuffer -import java.nio.ByteOrder - -class ZipEntry private constructor( - internal val version: UShort, - internal val versionNeeded: UShort, - internal val flags: UShort, - internal var compression: UShort, - internal val modificationTime: UShort, - internal val modificationDate: UShort, - internal var crc32: UInt, - internal var compressedSize: UInt, - internal var uncompressedSize: UInt, - internal val diskNumber: UShort, - internal val internalAttributes: UShort, - internal val externalAttributes: UInt, - internal var localHeaderOffset: UInt, - internal val fileName: String, - internal val extraField: ByteArray, - internal val fileComment: String, - internal var localExtraField: ByteArray = ByteArray(0), // separate for alignment -) { - internal val LFHSize: Int - get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size - - internal val dataOffset: UInt - get() = localHeaderOffset + LFHSize.toUInt() - - constructor(fileName: String) : this( - 0x1403u, // made by unix, version 20 - 0u, - 0u, - 0u, - 0x0821u, // seems to be static time google uses, no idea - 0x0221u, // same as above - 0u, - 0u, - 0u, - 0u, - 0u, - 0u, - 0u, - fileName, - ByteArray(0), - "", - ) - - companion object { - internal const val CDE_HEADER_SIZE = 46 - internal const val CDE_SIGNATURE = 0x02014b50u - - internal const val LFH_HEADER_SIZE = 30 - internal const val LFH_SIGNATURE = 0x04034b50u - - internal fun fromCDE(input: DataInput): ZipEntry { - val signature = input.readUIntLE() - - if (signature != CDE_SIGNATURE) { - throw IllegalArgumentException("Input doesn't start with central directory entry signature") - } - - val version = input.readUShortLE() - val versionNeeded = input.readUShortLE() - var flags = input.readUShortLE() - val compression = input.readUShortLE() - val modificationTime = input.readUShortLE() - val modificationDate = input.readUShortLE() - val crc32 = input.readUIntLE() - val compressedSize = input.readUIntLE() - val uncompressedSize = input.readUIntLE() - val fileNameLength = input.readUShortLE() - var fileName = "" - val extraFieldLength = input.readUShortLE() - val extraField = ByteArray(extraFieldLength.toInt()) - val fileCommentLength = input.readUShortLE() - var fileComment = "" - val diskNumber = input.readUShortLE() - val internalAttributes = input.readUShortLE() - val externalAttributes = input.readUIntLE() - val localHeaderOffset = input.readUIntLE() - - val variableFieldsLength = - fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt() - - if (variableFieldsLength > 0) { - val fileNameBytes = ByteArray(fileNameLength.toInt()) - input.readFully(fileNameBytes) - fileName = fileNameBytes.toString(Charsets.UTF_8) - - input.readFully(extraField) - - val fileCommentBytes = ByteArray(fileCommentLength.toInt()) - input.readFully(fileCommentBytes) - fileComment = fileCommentBytes.toString(Charsets.UTF_8) - } - - flags = ( - flags and - 0b1000u.inv() - .toUShort() - ) // disable data descriptor flag as they are not used - - return ZipEntry( - version, - versionNeeded, - flags, - compression, - modificationTime, - modificationDate, - crc32, - compressedSize, - uncompressedSize, - diskNumber, - internalAttributes, - externalAttributes, - localHeaderOffset, - fileName, - extraField, - fileComment, - ) - } - } - - internal fun readLocalExtra(buffer: ByteBuffer) { - buffer.order(ByteOrder.LITTLE_ENDIAN) - localExtraField = ByteArray(buffer.getUShort().toInt()) - } - - internal fun toLFH(): ByteBuffer { - val nameBytes = fileName.toByteArray(Charsets.UTF_8) - - val buffer = - ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size) - .also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(LFH_SIGNATURE) - buffer.putUShort(versionNeeded) - buffer.putUShort(flags) - buffer.putUShort(compression) - buffer.putUShort(modificationTime) - buffer.putUShort(modificationDate) - buffer.putUInt(crc32) - buffer.putUInt(compressedSize) - buffer.putUInt(uncompressedSize) - buffer.putUShort(nameBytes.size.toUShort()) - buffer.putUShort(localExtraField.size.toUShort()) - - buffer.put(nameBytes) - buffer.put(localExtraField) - - buffer.flip() - return buffer - } - - internal fun toCDE(): ByteBuffer { - val nameBytes = fileName.toByteArray(Charsets.UTF_8) - val commentBytes = fileComment.toByteArray(Charsets.UTF_8) - - val buffer = - ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size) - .also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(CDE_SIGNATURE) - buffer.putUShort(version) - buffer.putUShort(versionNeeded) - buffer.putUShort(flags) - buffer.putUShort(compression) - buffer.putUShort(modificationTime) - buffer.putUShort(modificationDate) - buffer.putUInt(crc32) - buffer.putUInt(compressedSize) - buffer.putUInt(uncompressedSize) - buffer.putUShort(nameBytes.size.toUShort()) - buffer.putUShort(extraField.size.toUShort()) - buffer.putUShort(commentBytes.size.toUShort()) - buffer.putUShort(diskNumber) - buffer.putUShort(internalAttributes) - buffer.putUInt(externalAttributes) - buffer.putUInt(localHeaderOffset) - - buffer.put(nameBytes) - buffer.put(extraField) - buffer.put(commentBytes) - - buffer.flip() - return buffer - } -} diff --git a/src/test/kotlin/app/revanced/library/PatchOptionsTest.kt b/src/test/kotlin/app/revanced/library/PatchOptionsTest.kt index 3a9ba0a..c0c03be 100644 --- a/src/test/kotlin/app/revanced/library/PatchOptionsTest.kt +++ b/src/test/kotlin/app/revanced/library/PatchOptionsTest.kt @@ -37,7 +37,7 @@ internal object PatchOptionsTest { "[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]" @Patch("PatchOptionsTestPatch") - object PatchOptionsTestPatch : BytecodePatch() { + object PatchOptionsTestPatch : BytecodePatch(emptySet()) { var option1 by stringPatchOption("key1", null, null, "title1", "description1") var option2 by booleanPatchOption("key2", true, null, "title2", "description2")