From a2662623e49d66ccd431427f990e5fe386bc3e42 Mon Sep 17 00:00:00 2001 From: Eric Ahn Date: Sun, 11 Jan 2026 01:35:20 -0800 Subject: [PATCH 1/5] feat: Add ability to write patching results to JSON file --- build.gradle.kts | 2 + gradle/libs.versions.toml | 6 +- .../app/morphe/cli/command/PatchCommand.kt | 50 +++++++++-- .../morphe/cli/command/model/FailedPatch.kt | 11 +++ .../cli/command/model/PatchingResult.kt | 13 +++ .../cli/command/model/SerializablePatch.kt | 89 +++++++++++++++++++ 6 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/app/morphe/cli/command/model/FailedPatch.kt create mode 100644 src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt create mode 100644 src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt diff --git a/build.gradle.kts b/build.gradle.kts index b99f5df..6474d12 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlin) + alias(libs.plugins.kotlin.serialization) alias(libs.plugins.shadow) application `maven-publish` @@ -35,6 +36,7 @@ dependencies { implementation(libs.morphe.patcher) implementation(libs.morphe.library) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) implementation(libs.picocli) testImplementation(libs.kotlin.test) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b911af..0cfbcf7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] shadow = "8.3.9" -kotlin = "2.0.21" -kotlinx = "1.8.1" +kotlin = "2.3.0" +kotlinx = "1.9.0" picocli = "4.7.7" morphe-patcher = "1.0.1" morphe-library = "1.0.1" @@ -9,6 +9,7 @@ morphe-library = "1.0.1" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } morphe-patcher = { module = "app.morphe:morphe-patcher", version.ref = "morphe-patcher" } morphe-library = { module = "app.morphe:morphe-library-jvm", version.ref = "morphe-library" } @@ -16,3 +17,4 @@ morphe-library = { module = "app.morphe:morphe-library-jvm", version.ref = "morp [plugins] shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 44ffe51..6d4baee 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -1,5 +1,8 @@ package app.morphe.cli.command +import app.morphe.cli.command.model.FailedPatch +import app.morphe.cli.command.model.PatchingResult +import app.morphe.cli.command.model.toSerializablePatch import app.morphe.library.ApkUtils import app.morphe.library.ApkUtils.applyTo import app.morphe.library.installation.installer.* @@ -9,6 +12,9 @@ import app.morphe.patcher.PatcherConfig import app.morphe.patcher.patch.Patch import app.morphe.patcher.patch.loadPatchesFromJar import kotlinx.coroutines.runBlocking +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream import picocli.CommandLine import picocli.CommandLine.ArgGroup import picocli.CommandLine.Help.Visibility.ALWAYS @@ -19,6 +25,7 @@ import java.io.PrintWriter import java.io.StringWriter import java.util.logging.Logger +@OptIn(ExperimentalSerializationApi::class) @CommandLine.Command( name = "patch", description = ["Patch an APK file."], @@ -115,6 +122,17 @@ internal object PatchCommand : Runnable { this.outputFilePath = outputFilePath?.absoluteFile } + private var patchingResultOutputFilePath: File? = null + + @CommandLine.Option( + names = ["-r", "--result-file"], + description = ["Path to save the patching result file to"], + ) + @Suppress("unused") + private fun setPatchingResultOutputFilePath(outputFilePath: File?) { + this.patchingResultOutputFilePath = outputFilePath?.absoluteFile + } + @CommandLine.Option( names = ["-i", "--install"], description = ["Serial of the ADB device to install to. If not specified, the first connected device will be used."], @@ -233,7 +251,7 @@ internal object PatchCommand : Runnable { description = ["Disable signing of the final apk."], ) private var unsigned: Boolean = false - + override fun run() { // region Setup @@ -316,20 +334,36 @@ internal object PatchCommand : Runnable { patcher += filteredPatches + val patchingResult = PatchingResult() + // Execute patches. runBlocking { patcher().collect { patchResult -> - val exception = patchResult.exception - ?: return@collect logger.info("\"${patchResult.patch}\" succeeded") - - StringWriter().use { writer -> - exception.printStackTrace(PrintWriter(writer)) - - logger.severe("\"${patchResult.patch}\" failed:\n$writer") + patchResult.exception?.let { exception -> + StringWriter().use { writer -> + exception.printStackTrace(PrintWriter(writer)) + + logger.severe("\"${patchResult.patch}\" failed:\n$writer") + + patchingResult.failedPatches.add( + FailedPatch( + patchResult.patch.toSerializablePatch(), + writer.toString() + ) + ) + } + } ?: patchResult.patch.let { + patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) + logger.info("\"${patchResult.patch}\" succeeded") } } } + patchingResultOutputFilePath?.let { outputFile -> + Json.encodeToStream(patchingResult, outputFile.outputStream()) + logger.info("Patching result saved to $outputFile") + } + patcher.context.packageMetadata.packageName to patcher.get() } diff --git a/src/main/kotlin/app/morphe/cli/command/model/FailedPatch.kt b/src/main/kotlin/app/morphe/cli/command/model/FailedPatch.kt new file mode 100644 index 0000000..3b80ce4 --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/model/FailedPatch.kt @@ -0,0 +1,11 @@ +package app.morphe.cli.command.model + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable + +@ExperimentalSerializationApi +@Serializable +data class FailedPatch( + val patch: SerializablePatch, + val reason: String +) diff --git a/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt new file mode 100644 index 0000000..98c5966 --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt @@ -0,0 +1,13 @@ +package app.morphe.cli.command.model + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable + +@ExperimentalSerializationApi +@Serializable +data class PatchingResult( + @EncodeDefault val appliedPatches: MutableList = mutableListOf(), + @EncodeDefault val failedPatches: MutableList = mutableListOf() + // Maybe add results for compilation, APK aligning, signing? +) diff --git a/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt b/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt new file mode 100644 index 0000000..d1cb15f --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt @@ -0,0 +1,89 @@ +package app.morphe.cli.command.model + +import app.morphe.patcher.patch.Patch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject + +@ExperimentalSerializationApi +@Serializable(with = PatchSerializer::class) +data class SerializablePatch( + val name: String? = null, + val index: Int? = null, + val options: Map = mutableMapOf() +) + +@ExperimentalSerializationApi +fun Patch<*>.toSerializablePatch(): SerializablePatch { + return SerializablePatch( + name = this.name, + options = this.options.mapValues { PatchSerializer.serializeValue(it.value.value) } + ) +} + +@ExperimentalSerializationApi +object PatchSerializer : KSerializer { + fun serializeValue(value: Any?): JsonElement { + return when (value) { + null -> JsonNull + is JsonElement -> value + is List<*> -> { + buildJsonArray { + value.forEach { item -> + add(serializeValue(item)) + } + } + } + else -> JsonPrimitive(value.toString()) + } + } + + override fun serialize(encoder: Encoder, value: SerializablePatch) { + require(encoder is JsonEncoder) + + val jsonElement = buildJsonObject { + require(value.name != null || value.index != null) { + "Either name or index must be provided for a Patch." + } + + if (value.name != null) { + put("name", JsonPrimitive(value.name)) + } else { + put("index", JsonPrimitive(value.index)) + } + + if (value.options.isNotEmpty()) { + put("options", buildJsonArray { + value.options.forEach { (key, optionValue) -> + add(buildJsonObject { + put("key", JsonPrimitive(key)) + put("value", serializeValue(optionValue)) + }) + } + }) + } + } + encoder.encodeJsonElement(jsonElement) + } + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Patch") { + element("name") + element("index") + element>("options") + } + + override fun deserialize(decoder: Decoder): SerializablePatch { + TODO("Not yet implemented") + } +} From fd5af7941a7adef9e870d24c75c2acb6a8727531 Mon Sep 17 00:00:00 2001 From: Eric Ahn Date: Sun, 11 Jan 2026 01:58:25 -0800 Subject: [PATCH 2/5] chore: Handle primitive types explicitly --- .../morphe/cli/command/model/SerializablePatch.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt b/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt index d1cb15f..88cd340 100644 --- a/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt +++ b/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt @@ -38,6 +38,9 @@ object PatchSerializer : KSerializer { return when (value) { null -> JsonNull is JsonElement -> value + is Boolean -> JsonPrimitive(value) + is Number -> JsonPrimitive(value) + is String -> JsonPrimitive(value) is List<*> -> { buildJsonArray { value.forEach { item -> @@ -45,6 +48,16 @@ object PatchSerializer : KSerializer { } } } + is Map<*, *> -> { + buildJsonObject { + value.forEach { + require(it.key is String) { + "Map keys must be of type String for serialization, but found: ${it.key?.let { k -> k::class }}" + } + put(it.key as String, serializeValue(it.value)) + } + } + } else -> JsonPrimitive(value.toString()) } } From 838f7dab96e408f72673e29ad14473aa868f74f3 Mon Sep 17 00:00:00 2001 From: Eric Ahn Date: Sun, 11 Jan 2026 13:41:24 -0800 Subject: [PATCH 3/5] chore: Add package information and status of individual steps to results JSON --- .../app/morphe/cli/command/PatchCommand.kt | 200 ++++++++++-------- .../cli/command/model/PatchingResult.kt | 33 ++- .../morphe/cli/command/model/PatchingStep.kt | 8 + .../cli/command/model/PatchingStepResult.kt | 12 ++ 4 files changed, 168 insertions(+), 85 deletions(-) create mode 100644 src/main/kotlin/app/morphe/cli/command/model/PatchingStep.kt create mode 100644 src/main/kotlin/app/morphe/cli/command/model/PatchingStepResult.kt diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 6d4baee..5174e6c 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -2,6 +2,9 @@ package app.morphe.cli.command import app.morphe.cli.command.model.FailedPatch import app.morphe.cli.command.model.PatchingResult +import app.morphe.cli.command.model.PatchingStep +import app.morphe.cli.command.model.PatchingStepResult +import app.morphe.cli.command.model.addStepResult import app.morphe.cli.command.model.toSerializablePatch import app.morphe.library.ApkUtils import app.morphe.library.ApkUtils.applyTo @@ -309,104 +312,133 @@ internal object PatchCommand : Runnable { val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") - val (packageName, patcherResult) = Patcher( - PatcherConfig( - apk, - patcherTemporaryFilesPath, - aaptBinaryPath?.path, - patcherTemporaryFilesPath.absolutePath, - ), - ).use { patcher -> - val packageName = patcher.context.packageMetadata.packageName - val packageVersion = patcher.context.packageMetadata.packageVersion - - val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) - - logger.info("Setting patch options") - - val patchesList = patches.toList() - selection.filter { it.enabled != null }.associate { - val enabledSelection = it.enabled!! - - (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to - enabledSelection.options - }.let(filteredPatches::setOptions) - - patcher += filteredPatches - - val patchingResult = PatchingResult() + var patchingResult = PatchingResult() + + try { + val (packageName, patcherResult) = Patcher( + PatcherConfig( + apk, + patcherTemporaryFilesPath, + aaptBinaryPath?.path, + patcherTemporaryFilesPath.absolutePath, + ), + ).use { patcher -> + val packageName = patcher.context.packageMetadata.packageName + val packageVersion = patcher.context.packageMetadata.packageVersion + + patchingResult.packageName = packageName + patchingResult.packageVersion = packageVersion + + val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) + + logger.info("Setting patch options") + + val patchesList = patches.toList() + selection.filter { it.enabled != null }.associate { + val enabledSelection = it.enabled!! + + (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to + enabledSelection.options + }.let(filteredPatches::setOptions) + + patcher += filteredPatches + + // Execute patches. + patchingResult.addStepResult( + PatchingStep.PATCHING, + this, + { + runBlocking { + patcher().collect { patchResult -> + patchResult.exception?.let { exception -> + StringWriter().use { writer -> + exception.printStackTrace(PrintWriter(writer)) + + logger.severe("\"${patchResult.patch}\" failed:\n$writer") + + patchingResult.failedPatches.add( + FailedPatch( + patchResult.patch.toSerializablePatch(), + writer.toString() + ) + ) + patchingResult.success = false + } + } ?: patchResult.patch.let { + patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) + logger.info("\"${patchResult.patch}\" succeeded") + } + } + } + } + ) - // Execute patches. - runBlocking { - patcher().collect { patchResult -> - patchResult.exception?.let { exception -> - StringWriter().use { writer -> - exception.printStackTrace(PrintWriter(writer)) + patcher.context.packageMetadata.packageName to patcher.get() + } - logger.severe("\"${patchResult.patch}\" failed:\n$writer") + // region Save. - patchingResult.failedPatches.add( - FailedPatch( - patchResult.patch.toSerializablePatch(), - writer.toString() - ) + apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply { + patchingResult.addStepResult( + PatchingStep.REBUILDING, + this, + { + patcherResult.applyTo(this) + } + ) + }.let { patchedApkFile -> + if (!mount && !unsigned) { + patchingResult.addStepResult( + PatchingStep.SIGNING, + this, + { + ApkUtils.signApk( + patchedApkFile, + outputFilePath, + signer, + ApkUtils.KeyStoreDetails( + keystoreFilePath, + keyStorePassword, + keyStoreEntryAlias, + keyStoreEntryPassword, + ), ) } - } ?: patchResult.patch.let { - patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) - logger.info("\"${patchResult.patch}\" succeeded") - } + ) + } else { + patchedApkFile.copyTo(outputFilePath, overwrite = true) } } + logger.info("Saved to $outputFilePath") + + // endregion + + // region Install. + + deviceSerial?.let { + patchingResult.addStepResult( + PatchingStep.INSTALLING, + this, + { + runBlocking { + when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) { + RootInstallerResult.FAILURE -> logger.severe("Failed to mount the patched APK file") + is AdbInstallerResult.Failure -> logger.severe(result.exception.toString()) + else -> logger.info("Installed the patched APK file") + } + } + } + ) + } + // endregion + } finally { patchingResultOutputFilePath?.let { outputFile -> Json.encodeToStream(patchingResult, outputFile.outputStream()) logger.info("Patching result saved to $outputFile") } - - patcher.context.packageMetadata.packageName to patcher.get() } - // region Save. - - apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply { - patcherResult.applyTo(this) - }.let { patchedApkFile -> - if (!mount && !unsigned) { - ApkUtils.signApk( - patchedApkFile, - outputFilePath, - signer, - ApkUtils.KeyStoreDetails( - keystoreFilePath, - keyStorePassword, - keyStoreEntryAlias, - keyStoreEntryPassword, - ), - ) - } else { - patchedApkFile.copyTo(outputFilePath, overwrite = true) - } - } - - logger.info("Saved to $outputFilePath") - - // endregion - - // region Install. - - deviceSerial?.let { - runBlocking { - when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) { - RootInstallerResult.FAILURE -> logger.severe("Failed to mount the patched APK file") - is AdbInstallerResult.Failure -> logger.severe(result.exception.toString()) - else -> logger.info("Installed the patched APK file") - } - } - } - - // endregion - if (purge) { logger.info("Purging temporary files") purge(temporaryFilesPath) diff --git a/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt index 98c5966..4b6221a 100644 --- a/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt +++ b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt @@ -7,7 +7,38 @@ import kotlinx.serialization.Serializable @ExperimentalSerializationApi @Serializable data class PatchingResult( + var packageName: String? = null, + var packageVersion: String? = null, + var success: Boolean = true, + @EncodeDefault val patchingSteps: MutableList = mutableListOf(), @EncodeDefault val appliedPatches: MutableList = mutableListOf(), @EncodeDefault val failedPatches: MutableList = mutableListOf() - // Maybe add results for compilation, APK aligning, signing? ) + +@ExperimentalSerializationApi +fun PatchingResult.addStepResult( + step: PatchingStep, + context: T, + block: T.() -> R, +): R { + try { + val result = block(context) + this.patchingSteps.add( + PatchingStepResult( + step = step, + success = true + ) + ) + return result + } catch (e: Exception) { + this.success = false + this.patchingSteps.add( + PatchingStepResult( + step = step, + success = false, + message = e.message + ) + ) + throw e + } +} diff --git a/src/main/kotlin/app/morphe/cli/command/model/PatchingStep.kt b/src/main/kotlin/app/morphe/cli/command/model/PatchingStep.kt new file mode 100644 index 0000000..8f521da --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/model/PatchingStep.kt @@ -0,0 +1,8 @@ +package app.morphe.cli.command.model + +enum class PatchingStep { + PATCHING, + REBUILDING, + SIGNING, + INSTALLING +} \ No newline at end of file diff --git a/src/main/kotlin/app/morphe/cli/command/model/PatchingStepResult.kt b/src/main/kotlin/app/morphe/cli/command/model/PatchingStepResult.kt new file mode 100644 index 0000000..419ba2f --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/model/PatchingStepResult.kt @@ -0,0 +1,12 @@ +package app.morphe.cli.command.model + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable + +@ExperimentalSerializationApi +@Serializable +data class PatchingStepResult( + val step: PatchingStep, + val success: Boolean, + val message: String? = null +) From 4f7f5e5214118d8885dafb1bafc707d52e90d102 Mon Sep 17 00:00:00 2001 From: Eric Ahn Date: Sun, 11 Jan 2026 13:54:38 -0800 Subject: [PATCH 4/5] chore: Simplify addStepResult --- src/main/kotlin/app/morphe/cli/command/PatchCommand.kt | 4 ---- .../kotlin/app/morphe/cli/command/model/PatchingResult.kt | 7 +++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 5174e6c..1a60baa 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -346,7 +346,6 @@ internal object PatchCommand : Runnable { // Execute patches. patchingResult.addStepResult( PatchingStep.PATCHING, - this, { runBlocking { patcher().collect { patchResult -> @@ -381,7 +380,6 @@ internal object PatchCommand : Runnable { apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply { patchingResult.addStepResult( PatchingStep.REBUILDING, - this, { patcherResult.applyTo(this) } @@ -390,7 +388,6 @@ internal object PatchCommand : Runnable { if (!mount && !unsigned) { patchingResult.addStepResult( PatchingStep.SIGNING, - this, { ApkUtils.signApk( patchedApkFile, @@ -418,7 +415,6 @@ internal object PatchCommand : Runnable { deviceSerial?.let { patchingResult.addStepResult( PatchingStep.INSTALLING, - this, { runBlocking { when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) { diff --git a/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt index 4b6221a..e9dabd1 100644 --- a/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt +++ b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt @@ -16,13 +16,12 @@ data class PatchingResult( ) @ExperimentalSerializationApi -fun PatchingResult.addStepResult( +fun PatchingResult.addStepResult( step: PatchingStep, - context: T, - block: T.() -> R, + block: () -> R, ): R { try { - val result = block(context) + val result = block() this.patchingSteps.add( PatchingStepResult( step = step, From ff80fe8a6e91a65af9d2ac362a5768e9aad16227 Mon Sep 17 00:00:00 2001 From: Eric Ahn Date: Sun, 11 Jan 2026 13:57:26 -0800 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../app/morphe/cli/command/PatchCommand.kt | 19 ++++++++++++++----- .../cli/command/model/PatchingResult.kt | 2 +- .../cli/command/model/SerializablePatch.kt | 6 +++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 1a60baa..c730844 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -312,7 +312,7 @@ internal object PatchCommand : Runnable { val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") - var patchingResult = PatchingResult() + val patchingResult = PatchingResult() try { val (packageName, patcherResult) = Patcher( @@ -417,9 +417,16 @@ internal object PatchCommand : Runnable { PatchingStep.INSTALLING, { runBlocking { - when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) { - RootInstallerResult.FAILURE -> logger.severe("Failed to mount the patched APK file") - is AdbInstallerResult.Failure -> logger.severe(result.exception.toString()) + val result = installer!!.install(Installer.Apk(outputFilePath, packageName)) + when (result) { + RootInstallerResult.FAILURE -> { + logger.severe("Failed to mount the patched APK file") + throw IllegalStateException("Failed to mount the patched APK file") + } + is AdbInstallerResult.Failure -> { + logger.severe(result.exception.toString()) + throw result.exception + } else -> logger.info("Installed the patched APK file") } } @@ -430,7 +437,9 @@ internal object PatchCommand : Runnable { // endregion } finally { patchingResultOutputFilePath?.let { outputFile -> - Json.encodeToStream(patchingResult, outputFile.outputStream()) + outputFile.outputStream().use { outputStream -> + Json.encodeToStream(patchingResult, outputStream) + } logger.info("Patching result saved to $outputFile") } } diff --git a/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt index e9dabd1..2ffaaf2 100644 --- a/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt +++ b/src/main/kotlin/app/morphe/cli/command/model/PatchingResult.kt @@ -35,7 +35,7 @@ fun PatchingResult.addStepResult( PatchingStepResult( step = step, success = false, - message = e.message + message = e.toString() ) ) throw e diff --git a/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt b/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt index 88cd340..fb484cd 100644 --- a/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt +++ b/src/main/kotlin/app/morphe/cli/command/model/SerializablePatch.kt @@ -21,7 +21,7 @@ import kotlinx.serialization.json.buildJsonObject data class SerializablePatch( val name: String? = null, val index: Int? = null, - val options: Map = mutableMapOf() + val options: Map = emptyMap() ) @ExperimentalSerializationApi @@ -81,7 +81,7 @@ object PatchSerializer : KSerializer { value.options.forEach { (key, optionValue) -> add(buildJsonObject { put("key", JsonPrimitive(key)) - put("value", serializeValue(optionValue)) + put("value", optionValue) }) } }) @@ -93,7 +93,7 @@ object PatchSerializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Patch") { element("name") element("index") - element>("options") + element>("options") } override fun deserialize(decoder: Decoder): SerializablePatch {