mirror of
https://github.com/revanced/revanced-cli.git
synced 2024-12-24 02:55:48 +01:00
feat: use separate command to patch
This commit is contained in:
parent
8a5daab2a3
commit
32da961d57
@ -5,7 +5,7 @@ To use ReVanced CLI, you will need to fulfil specific requirements.
|
||||
## 🤝 Requirements
|
||||
|
||||
- Java SDK 11 (Azul Zulu JDK or OpenJDK)
|
||||
- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to deploy the patched APK file on your device
|
||||
- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device
|
||||
- An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7)
|
||||
- ReVanced Patches
|
||||
- ReVanced Integrations, if the patches require it
|
||||
|
@ -10,7 +10,7 @@ Learn how to ReVanced CLI.
|
||||
adb shell exit
|
||||
```
|
||||
|
||||
If you want to deploy the patched APK file on your device by mounting it on top of the original APK file, you will need root access. This is optional.
|
||||
If you want to install the patched APK file on your device by mounting it on top of the original APK file, you will need root access. This is optional.
|
||||
|
||||
```bash
|
||||
adb shell su -c exit
|
||||
@ -33,8 +33,7 @@ Learn how to ReVanced CLI.
|
||||
- ### 📃 List patches from supplied patch bundles
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar \
|
||||
list-patches \
|
||||
java -jar revanced-cli.jar list-patches \
|
||||
--with-packages \
|
||||
--with-versions \
|
||||
--with-options \
|
||||
@ -49,31 +48,31 @@ Learn how to ReVanced CLI.
|
||||
|
||||
> **Note**: The `options.json` file will be generated at the first time you use ReVanced CLI to patch an APK file for now. This will be changed in the future.
|
||||
|
||||
- ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions
|
||||
- ### 💉 Use ReVanced CLI to patch an APK file but install without root permissions
|
||||
|
||||
This will deploy the patched APK file on your device by installing it.
|
||||
This will install the patched APK file regularly on your device.
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar \
|
||||
-a input.apk \
|
||||
-o patched-output.apk \
|
||||
java -jar revanced-cli.jar patch \
|
||||
-b revanced-patches.jar \
|
||||
-d device-serial
|
||||
-o patched-output.apk \
|
||||
-d device-serial \
|
||||
input-apk
|
||||
```
|
||||
|
||||
- ### 👾 Use ReVanced CLI to patch an APK file but deploy with root permissions
|
||||
- ### 👾 Use ReVanced CLI to patch an APK file but install with root permissions
|
||||
|
||||
This will deploy the patched APK file on your device by mounting it on top of the original APK file.
|
||||
This will install the patched APK file on your device by mounting it on top of the original APK file.
|
||||
|
||||
```bash
|
||||
adb install input.apk
|
||||
java -jar revanced-cli.jar \
|
||||
-a input.apk \
|
||||
java -jar revanced-cli.jar patch \
|
||||
-o patched-output.apk \
|
||||
-b revanced-patches.jar \
|
||||
-e vanced-microg-support \
|
||||
-e some-patch \
|
||||
-d device-serial \
|
||||
--mount
|
||||
--mount \
|
||||
input-apk
|
||||
```
|
||||
|
||||
> **Note**: Some patches from [ReVanced Patches](https://github.com/revanced/revanced-patches) also require [ReVanced Integrations](https://github.com/revanced/revanced-integrations). Supply them with the option `-m`. ReVanced Patcher will merge ReVanced Integrations automatically, depending on if the supplied patches require them.
|
||||
@ -81,8 +80,7 @@ Learn how to ReVanced CLI.
|
||||
|
||||
- ### 🗑️ Uninstall a patched
|
||||
```bash
|
||||
java -jar revanced-cli.jar \
|
||||
uninstall \
|
||||
java -jar revanced-cli.jar uninstall \
|
||||
-p package-name \
|
||||
device-serial
|
||||
```
|
||||
|
@ -1,37 +0,0 @@
|
||||
package app.revanced.cli.aligning
|
||||
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.utils.signing.align.ZipAligner
|
||||
import app.revanced.utils.signing.align.zip.ZipFile
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
||||
import java.io.File
|
||||
|
||||
object Aligning {
|
||||
fun align(result: PatcherResult, inputFile: File, outputFile: File) {
|
||||
logger.info("Aligning ${inputFile.name} to ${outputFile.name}")
|
||||
|
||||
if (outputFile.exists()) outputFile.delete()
|
||||
|
||||
ZipFile(outputFile).use { file ->
|
||||
result.dexFiles.forEach {
|
||||
file.addEntryCompressData(
|
||||
ZipEntry.createWithName(it.name),
|
||||
it.stream.readBytes()
|
||||
)
|
||||
}
|
||||
|
||||
result.resourceFile?.let {
|
||||
file.copyEntriesFromFileAligned(
|
||||
ZipFile(it),
|
||||
ZipAligner::getEntryAlignment
|
||||
)
|
||||
}
|
||||
|
||||
file.copyEntriesFromFileAligned(
|
||||
ZipFile(inputFile),
|
||||
ZipAligner::getEntryAlignment
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
39
src/main/kotlin/app/revanced/cli/command/Main.kt
Normal file
39
src/main/kotlin/app/revanced/cli/command/Main.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package app.revanced.cli.command
|
||||
|
||||
import app.revanced.cli.logging.impl.DefaultCliLogger
|
||||
import app.revanced.patcher.patch.PatchClass
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.IVersionProvider
|
||||
import java.util.*
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
CommandLine(Main).execute(*args)
|
||||
}
|
||||
|
||||
internal typealias PatchList = List<PatchClass>
|
||||
|
||||
internal val logger = DefaultCliLogger()
|
||||
|
||||
object CLIVersionProvider : IVersionProvider {
|
||||
override fun getVersion(): Array<String> {
|
||||
Properties().apply {
|
||||
load(Main::class.java.getResourceAsStream("/app/revanced/cli/version.properties"))
|
||||
}.let {
|
||||
return arrayOf("ReVanced CLI v${it.getProperty("version")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
name = "revanced-cli",
|
||||
description = ["Command line application to use ReVanced"],
|
||||
mixinStandardHelpOptions = true,
|
||||
versionProvider = CLIVersionProvider::class,
|
||||
subcommands = [
|
||||
ListPatchesCommand::class,
|
||||
PatchCommand::class,
|
||||
UninstallCommand::class
|
||||
]
|
||||
)
|
||||
internal object Main
|
412
src/main/kotlin/app/revanced/cli/command/PatchCommand.kt
Normal file
412
src/main/kotlin/app/revanced/cli/command/PatchCommand.kt
Normal file
@ -0,0 +1,412 @@
|
||||
package app.revanced.cli.command
|
||||
|
||||
import app.revanced.cli.patcher.logging.impl.PatcherLogger
|
||||
import app.revanced.patcher.PatchBundleLoader
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||
import app.revanced.patcher.extensions.PatchExtensions.include
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.utils.Options
|
||||
import app.revanced.utils.Options.setOptions
|
||||
import app.revanced.utils.adb.AdbManager
|
||||
import app.revanced.utils.align.ZipAligner
|
||||
import app.revanced.utils.align.zip.ZipFile
|
||||
import app.revanced.utils.align.zip.structures.ZipEntry
|
||||
import app.revanced.utils.signing.ApkSigner
|
||||
import app.revanced.utils.signing.SigningOptions
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||
import java.io.File
|
||||
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "patch",
|
||||
description = ["Patch the supplied APK file with the supplied patches and integrations"]
|
||||
)
|
||||
internal object PatchCommand: Runnable {
|
||||
@CommandLine.Parameters(
|
||||
description = ["APK file to be patched"],
|
||||
arity = "1..1"
|
||||
)
|
||||
lateinit var apk: File
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-b", "--bundle"],
|
||||
description = ["One or more bundles of patches"],
|
||||
required = true
|
||||
)
|
||||
var patchBundles = emptyList<File>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-m", "--merge"],
|
||||
description = ["One or more DEX files or containers to merge into the APK"]
|
||||
)
|
||||
var integrations = listOf<File>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-i", "--include"],
|
||||
description = ["List of patches to include"]
|
||||
)
|
||||
var includedPatches = arrayOf<String>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-e", "--exclude"],
|
||||
description = ["List of patches to exclude"]
|
||||
)
|
||||
var excludedPatches = arrayOf<String>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--options"],
|
||||
description = ["Path to patch options JSON file"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var optionsFile: File = File("options.json")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--exclusive"],
|
||||
description = ["Only include patches that are explicitly specified to be included"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var exclusive = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--experimental"],
|
||||
description = ["Ignore patches incompatibility to versions"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var experimental: Boolean = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-o", "--out"],
|
||||
description = ["Path to save the patched APK file to"],
|
||||
required = true
|
||||
)
|
||||
lateinit var outputFilePath: File
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-d", "--device-serial"],
|
||||
description = ["ADB device serial to install to"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var deviceSerial: String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--mount"],
|
||||
description = ["Install by mounting the patched package"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var mount: Boolean = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--common-name"],
|
||||
description = ["The common name of the signer of the patched APK file"],
|
||||
showDefaultValue = ALWAYS
|
||||
|
||||
)
|
||||
var commonName = "ReVanced"
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--keystore"],
|
||||
description = ["Path to the keystore to sign the patched APK file with"]
|
||||
)
|
||||
var keystorePath: String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--password"],
|
||||
description = ["The password of the keystore to sign the patched APK file with"]
|
||||
)
|
||||
var password = "ReVanced"
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-r", "--resource-cache"],
|
||||
description = ["Path to temporary resource cache directory"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var resourceCachePath = File("revanced-resource-cache")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--custom-aapt2-binary"],
|
||||
description = ["Path to a custom AAPT binary to compile resources with"]
|
||||
)
|
||||
var aaptBinaryPath = File("")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-p", "--purge"],
|
||||
description = ["Purge the temporary resource cache directory after patching"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var purge: Boolean = false
|
||||
|
||||
override fun run() {
|
||||
// region Prepare
|
||||
|
||||
if (!apk.exists()) {
|
||||
logger.error("Input file ${apk.name} does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
val adbManager = deviceSerial?.let { serial ->
|
||||
if (mount) AdbManager.RootAdbManager(serial, logger) else AdbManager.UserAdbManager(
|
||||
serial,
|
||||
logger
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Load patches
|
||||
|
||||
logger.info("Loading patches")
|
||||
|
||||
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
|
||||
val integrations = integrations
|
||||
|
||||
logger.info("Setting up patch options")
|
||||
|
||||
optionsFile.let {
|
||||
if (it.exists()) patches.setOptions(it, logger)
|
||||
else Options.serialize(patches, prettyPrint = true).let(it::writeText)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Patch
|
||||
|
||||
val patcher = Patcher(
|
||||
PatcherOptions(
|
||||
apk,
|
||||
resourceCachePath,
|
||||
aaptBinaryPath.absolutePath,
|
||||
resourceCachePath.absolutePath,
|
||||
PatcherLogger
|
||||
)
|
||||
)
|
||||
|
||||
val result = patcher.apply {
|
||||
acceptIntegrations(integrations)
|
||||
acceptPatches(filterPatchSelection(patches))
|
||||
|
||||
// Execute patches.
|
||||
runBlocking {
|
||||
apply(false).collect { patchResult ->
|
||||
patchResult.exception?.let {
|
||||
logger.error("${patchResult.patchName} failed:\n${patchResult.exception}")
|
||||
} ?: logger.info("${patchResult.patchName} succeeded")
|
||||
}
|
||||
}
|
||||
}.get()
|
||||
|
||||
patcher.close()
|
||||
|
||||
// endregion
|
||||
|
||||
// region Finish
|
||||
|
||||
val alignAndSignedFile = sign(
|
||||
apk.newAlignedFile(
|
||||
result,
|
||||
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
|
||||
)
|
||||
)
|
||||
|
||||
logger.info("Copying to ${outputFilePath.name}")
|
||||
alignAndSignedFile.copyTo(outputFilePath, overwrite = true)
|
||||
|
||||
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
|
||||
|
||||
if (purge) {
|
||||
logger.info("Purging temporary files")
|
||||
outputFilePath.delete()
|
||||
purge(resourceCachePath)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter the patches to be added to the patcher. The filter is based on the following:
|
||||
* - [includedPatches] (explicitly included)
|
||||
* - [excludedPatches] (explicitly excluded)
|
||||
* - [exclusive] (only include patches that are explicitly included)
|
||||
* - [experimental] (ignore patches incompatibility to versions)
|
||||
* - package name and version of the input APK file (if [experimental] is false)
|
||||
*
|
||||
* @param patches The patches to filter.
|
||||
* @return The filtered patches.
|
||||
*/
|
||||
private fun Patcher.filterPatchSelection(patches: PatchList) = buildList {
|
||||
val packageName = context.packageMetadata.packageName
|
||||
val packageVersion = context.packageMetadata.packageVersion
|
||||
|
||||
patches.forEach patch@{ patch ->
|
||||
val formattedPatchName = patch.patchName.lowercase().replace(" ", "-")
|
||||
|
||||
/**
|
||||
* Check if the patch is explicitly excluded.
|
||||
*
|
||||
* Cases:
|
||||
* 1. -e patch.name
|
||||
* 2. -i patch.name -e patch.name
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the patch is explicitly excluded.
|
||||
*
|
||||
* Cases:
|
||||
* 1. -e patch.name
|
||||
* 2. -i patch.name -e patch.name
|
||||
*/
|
||||
|
||||
val excluded = excludedPatches.contains(formattedPatchName)
|
||||
if (excluded) return@patch logger.info("Excluding ${patch.patchName}")
|
||||
|
||||
/**
|
||||
* Check if the patch is constrained to packages.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the patch is constrained to packages.
|
||||
*/
|
||||
|
||||
patch.compatiblePackages?.let { packages ->
|
||||
packages.singleOrNull { it.name == packageName }?.let { `package` ->
|
||||
/**
|
||||
* Check if the package version matches.
|
||||
* If experimental is true, version matching will be skipped.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the package version matches.
|
||||
* If experimental is true, version matching will be skipped.
|
||||
*/
|
||||
|
||||
val matchesVersion = experimental || `package`.versions.let {
|
||||
it.isEmpty() || it.any { version -> version == packageVersion }
|
||||
}
|
||||
|
||||
if (!matchesVersion) return@patch logger.warn(
|
||||
"${patch.patchName} is incompatible with version $packageVersion. " +
|
||||
"This patch is only compatible with version " +
|
||||
packages.joinToString(";") { `package` ->
|
||||
"${`package`.name}: ${`package`.versions.joinToString(", ")}"
|
||||
}
|
||||
)
|
||||
|
||||
} ?: return@patch logger.trace(
|
||||
"${patch.patchName} is incompatible with $packageName. " +
|
||||
"This patch is only compatible with " +
|
||||
packages.joinToString(", ") { `package` -> `package`.name }
|
||||
)
|
||||
|
||||
return@let
|
||||
} ?: logger.trace("$formattedPatchName: No constraint on packages.")
|
||||
|
||||
/**
|
||||
* Check if the patch is explicitly included.
|
||||
*
|
||||
* Cases:
|
||||
* 1. --exclusive
|
||||
* 2. --exclusive -i patch.name
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the patch is explicitly included.
|
||||
*
|
||||
* Cases:
|
||||
* 1. --exclusive
|
||||
* 2. --exclusive -i patch.name
|
||||
*/
|
||||
|
||||
val explicitlyIncluded = includedPatches.contains(formattedPatchName)
|
||||
|
||||
val implicitlyIncluded = !exclusive && patch.include // Case 3.
|
||||
val exclusivelyIncluded = exclusive && explicitlyIncluded // Case 2.
|
||||
|
||||
val included = implicitlyIncluded || exclusivelyIncluded
|
||||
if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1.
|
||||
|
||||
logger.trace("Adding $formattedPatchName")
|
||||
|
||||
add(patch)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new aligned APK file.
|
||||
*
|
||||
* @param result The result of the patching process.
|
||||
* @param outputFile The file to save the aligned APK to.
|
||||
*/
|
||||
private fun File.newAlignedFile(
|
||||
result: PatcherResult,
|
||||
outputFile: File
|
||||
): File {
|
||||
logger.info("Aligning $name to ${outputFile.name}")
|
||||
|
||||
if (outputFile.exists()) outputFile.delete()
|
||||
|
||||
ZipFile(outputFile).use { file ->
|
||||
result.dexFiles.forEach {
|
||||
file.addEntryCompressData(
|
||||
ZipEntry.createWithName(it.name),
|
||||
it.stream.readBytes()
|
||||
)
|
||||
}
|
||||
|
||||
result.resourceFile?.let {
|
||||
file.copyEntriesFromFileAligned(
|
||||
ZipFile(it),
|
||||
ZipAligner::getEntryAlignment
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Do not compress result.doNotCompress
|
||||
|
||||
file.copyEntriesFromFileAligned(
|
||||
ZipFile(this),
|
||||
ZipAligner::getEntryAlignment
|
||||
)
|
||||
}
|
||||
|
||||
return outputFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the APK file.
|
||||
*
|
||||
* @param inputFile The APK file to sign.
|
||||
* @return The signed APK file. If [mount] is true, the input file will be returned.
|
||||
*/
|
||||
private fun sign(inputFile: File) = if (mount)
|
||||
inputFile
|
||||
else {
|
||||
logger.info("Signing ${inputFile.name}")
|
||||
|
||||
val keyStoreFilePath = keystorePath ?: outputFilePath
|
||||
.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath
|
||||
|
||||
val options = SigningOptions(
|
||||
commonName,
|
||||
password,
|
||||
keyStoreFilePath
|
||||
)
|
||||
|
||||
ApkSigner(options)
|
||||
.signApk(
|
||||
inputFile,
|
||||
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
|
||||
)
|
||||
}
|
||||
|
||||
private fun purge(resourceCachePath: File) {
|
||||
val result = if (resourceCachePath.deleteRecursively())
|
||||
"Purged resource cache directory"
|
||||
else
|
||||
"Failed to purge resource cache directory"
|
||||
logger.info(result)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package app.revanced.utils.signing.align
|
||||
package app.revanced.utils.align
|
||||
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
||||
import app.revanced.utils.align.zip.structures.ZipEntry
|
||||
|
||||
internal object ZipAligner {
|
||||
private const val DEFAULT_ALIGNMENT = 4
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.utils.signing.align.zip
|
||||
package app.revanced.utils.align.zip
|
||||
|
||||
import java.io.DataInput
|
||||
import java.io.DataOutput
|
@ -1,7 +1,7 @@
|
||||
package app.revanced.utils.signing.align.zip
|
||||
package app.revanced.utils.align.zip
|
||||
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEndRecord
|
||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
||||
import app.revanced.utils.align.zip.structures.ZipEndRecord
|
||||
import app.revanced.utils.align.zip.structures.ZipEntry
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
@ -11,15 +11,15 @@ import java.util.zip.CRC32
|
||||
import java.util.zip.Deflater
|
||||
|
||||
class ZipFile(file: File) : Closeable {
|
||||
var entries: MutableList<ZipEntry> = mutableListOf()
|
||||
private var entries: MutableList<ZipEntry> = mutableListOf()
|
||||
|
||||
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
|
||||
private var CDNeedsRewrite = false
|
||||
private var centralDirectoryNeedsRewrite = false
|
||||
|
||||
private val compressionLevel = 5
|
||||
|
||||
init {
|
||||
//if file isn't empty try to load entries
|
||||
// If file isn't empty try to load entries.
|
||||
if (file.length() > 0) {
|
||||
val endRecord = findEndRecord()
|
||||
|
||||
@ -29,17 +29,17 @@ class ZipFile(file: File) : Closeable {
|
||||
entries = readEntries(endRecord).toMutableList()
|
||||
}
|
||||
|
||||
//seek back to start for writing
|
||||
// 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
|
||||
// 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
|
||||
// Possible beginning of signature.
|
||||
if (filePointer.readByte() == 0x50.toByte()) {
|
||||
//seek back to get the full int
|
||||
// Seek back to get the full int.
|
||||
filePointer.seek(i)
|
||||
val possibleSignature = filePointer.readUIntLE()
|
||||
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
|
||||
@ -76,7 +76,7 @@ class ZipFile(file: File) : Closeable {
|
||||
}
|
||||
|
||||
private fun writeCD() {
|
||||
val CDStart = filePointer.channel.position().toUInt()
|
||||
val centralDirectoryStartOffset = filePointer.channel.position().toUInt()
|
||||
|
||||
entries.forEach {
|
||||
filePointer.channel.write(it.toCDE())
|
||||
@ -89,8 +89,8 @@ class ZipFile(file: File) : Closeable {
|
||||
0u,
|
||||
entriesCount,
|
||||
entriesCount,
|
||||
filePointer.channel.position().toUInt() - CDStart,
|
||||
CDStart,
|
||||
filePointer.channel.position().toUInt() - centralDirectoryStartOffset,
|
||||
centralDirectoryStartOffset,
|
||||
""
|
||||
)
|
||||
|
||||
@ -98,7 +98,7 @@ class ZipFile(file: File) : Closeable {
|
||||
}
|
||||
|
||||
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
|
||||
CDNeedsRewrite = true
|
||||
centralDirectoryNeedsRewrite = true
|
||||
|
||||
entry.localHeaderOffset = filePointer.channel.position().toUInt()
|
||||
|
||||
@ -114,8 +114,7 @@ class ZipFile(file: File) : Closeable {
|
||||
compressor.finish()
|
||||
|
||||
val uncompressedSize = data.size
|
||||
val compressedData =
|
||||
ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
|
||||
val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger.
|
||||
|
||||
val compressedDataLength = compressor.deflate(compressedData)
|
||||
val compressedBuffer =
|
||||
@ -126,7 +125,7 @@ class ZipFile(file: File) : Closeable {
|
||||
val crc = CRC32()
|
||||
crc.update(data)
|
||||
|
||||
entry.compression = 8u //deflate compression
|
||||
entry.compression = 8u // Deflate compression.
|
||||
entry.uncompressedSize = uncompressedSize.toUInt()
|
||||
entry.compressedSize = compressedDataLength.toUInt()
|
||||
entry.crc32 = crc.value.toUInt()
|
||||
@ -136,14 +135,14 @@ class ZipFile(file: File) : Closeable {
|
||||
|
||||
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
|
||||
alignment?.let {
|
||||
//calculate where data would end up
|
||||
// Calculate where data would end up.
|
||||
val dataOffset = filePointer.filePointer + entry.LFHSize
|
||||
|
||||
val mod = dataOffset % alignment
|
||||
|
||||
//wrong alignment
|
||||
// Wrong alignment.
|
||||
if (mod != 0L) {
|
||||
//add padding at end of extra field
|
||||
// Add padding at end of extra field.
|
||||
entry.localExtraField =
|
||||
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
|
||||
}
|
||||
@ -152,7 +151,7 @@ class ZipFile(file: File) : Closeable {
|
||||
addEntry(entry, data)
|
||||
}
|
||||
|
||||
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
||||
private fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
||||
return filePointer.channel.map(
|
||||
FileChannel.MapMode.READ_ONLY,
|
||||
entry.dataOffset.toLong(),
|
||||
@ -160,9 +159,15 @@ class ZipFile(file: File) : Closeable {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 //don't add duplicates
|
||||
if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates
|
||||
|
||||
val data = file.getDataForEntry(entry)
|
||||
addEntryCopyData(entry, data, entryAlignment(entry))
|
||||
@ -170,7 +175,7 @@ class ZipFile(file: File) : Closeable {
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (CDNeedsRewrite) writeCD()
|
||||
if (centralDirectoryNeedsRewrite) writeCD()
|
||||
filePointer.close()
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package app.revanced.utils.signing.align.zip.structures
|
||||
package app.revanced.utils.align.zip.structures
|
||||
|
||||
import app.revanced.utils.signing.align.zip.putUInt
|
||||
import app.revanced.utils.signing.align.zip.putUShort
|
||||
import app.revanced.utils.signing.align.zip.readUIntLE
|
||||
import app.revanced.utils.signing.align.zip.readUShortLE
|
||||
import app.revanced.utils.align.zip.putUInt
|
||||
import app.revanced.utils.align.zip.putUShort
|
||||
import app.revanced.utils.align.zip.readUIntLE
|
||||
import app.revanced.utils.align.zip.readUShortLE
|
||||
import java.io.DataInput
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
@ -1,6 +1,6 @@
|
||||
package app.revanced.utils.signing.align.zip.structures
|
||||
package app.revanced.utils.align.zip.structures
|
||||
|
||||
import app.revanced.utils.signing.align.zip.*
|
||||
import app.revanced.utils.align.zip.*
|
||||
import java.io.DataInput
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
@ -1,7 +1,6 @@
|
||||
package app.revanced.utils.signing
|
||||
|
||||
import app.revanced.cli.command.MainCommand.logger
|
||||
import app.revanced.cli.signing.SigningOptions
|
||||
import app.revanced.cli.command.logger
|
||||
import com.android.apksig.ApkSigner
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
@ -18,10 +17,40 @@ import java.security.*
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
|
||||
internal class Signer(
|
||||
internal class ApkSigner(
|
||||
private val signingOptions: SigningOptions
|
||||
) {
|
||||
private val signer: ApkSigner.Builder
|
||||
private val passwordCharArray = signingOptions.password.toCharArray()
|
||||
|
||||
init {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
val alias = keyStore.let { store ->
|
||||
FileInputStream(File(signingOptions.keyStoreFilePath).also {
|
||||
if (!it.exists()) {
|
||||
logger.info("Creating keystore at ${it.absolutePath}")
|
||||
newKeystore(it)
|
||||
} else {
|
||||
logger.info("Using keystore at ${it.absolutePath}")
|
||||
}
|
||||
}).use { fis -> store.load(fis, null) }
|
||||
store.aliases().nextElement()
|
||||
}
|
||||
|
||||
with(
|
||||
ApkSigner.SignerConfig.Builder(
|
||||
signingOptions.cn,
|
||||
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
||||
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
||||
).build()
|
||||
) {
|
||||
this@ApkSigner.signer = ApkSigner.Builder(listOf(this))
|
||||
signer.setCreatedBy(signingOptions.cn)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newKeystore(out: File) {
|
||||
val (publicKey, privateKey) = createKey()
|
||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
||||
@ -50,30 +79,12 @@ internal class Signer(
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
||||
}
|
||||
|
||||
fun signApk(input: File, output: File) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
// TODO: keystore should be saved securely
|
||||
val ks = File(signingOptions.keyStoreFilePath)
|
||||
if (!ks.exists()) newKeystore(ks) else {
|
||||
logger.info("Found existing keystore: ${ks.name}")
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
|
||||
val config = ApkSigner.SignerConfig.Builder(
|
||||
signingOptions.cn,
|
||||
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
||||
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
||||
).build()
|
||||
|
||||
val signer = ApkSigner.Builder(listOf(config))
|
||||
signer.setCreatedBy(signingOptions.cn)
|
||||
fun signApk(input: File, output: File): File {
|
||||
signer.setInputApk(input)
|
||||
signer.setOutputApk(output)
|
||||
|
||||
signer.build().sign()
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package app.revanced.utils.signing
|
||||
|
||||
data class SigningOptions(
|
||||
val cn: String,
|
||||
val password: String,
|
||||
val keyStoreFilePath: String
|
||||
)
|
Loading…
Reference in New Issue
Block a user