feat: disable outdated version screen
Release / Release (push) Failing after 2m55s

This commit is contained in:
2026-08-21 16:28:11 +02:00
parent a7d1cc987a
commit c5d43f5b51
5 changed files with 274 additions and 20 deletions
@@ -0,0 +1,152 @@
package app.twitter.patches
import com.google.gson.JsonParser
import java.net.HttpURLConnection
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.security.MessageDigest
import java.util.HexFormat
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
import kotlin.io.path.absolutePathString
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteIfExists
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import kotlin.io.path.inputStream
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class MorphePatchIntegrationTest {
@Test
fun `applies outdated version patch to target APKM`() {
val inputApkm = requiredPathProperty("integrationTest.inputApkm")
val patchBundle = requiredPathProperty("integrationTest.patchBundle")
val morpheCache = requiredPathProperty("integrationTest.morpheCache")
val javaExecutable = requiredPathProperty("integrationTest.javaExecutable")
assertTrue(inputApkm.exists(), "Missing integration-test input: $inputApkm")
assertTrue(patchBundle.exists(), "Missing patch bundle: $patchBundle")
val morpheJar = downloadMorphe(morpheCache)
val workDirectory = Files.createTempDirectory("twitter-patch-integration-")
try {
val patchedApk = workDirectory.resolve("twitter-patched.apk")
val resultFile = workDirectory.resolve("patching-result.json")
val processLog = workDirectory.resolve("morphe.log")
val process = ProcessBuilder(
javaExecutable.absolutePathString(),
"-jar",
morpheJar.absolutePathString(),
"patch",
"--patches=${patchBundle.absolutePathString()}",
"--exclusive",
"--enable=$PATCH_NAME",
"--unsigned",
"--out=${patchedApk.absolutePathString()}",
"--result-file=${resultFile.absolutePathString()}",
inputApkm.absolutePathString()
)
.directory(workDirectory.toFile())
.redirectErrorStream(true)
.redirectOutput(processLog.toFile())
.start()
val completed = process.waitFor(10, TimeUnit.MINUTES)
if (!completed) process.destroyForcibly().waitFor()
val log = Files.readString(processLog)
assertTrue(completed, "Morphe timed out after 10 minutes:\n$log")
assertEquals(0, process.exitValue(), "Morphe failed:\n$log")
assertTrue(patchedApk.exists() && patchedApk.fileSize() > 0, "Patched APK was not created")
assertTrue(resultFile.exists() && resultFile.fileSize() > 0, "Patching report was not created")
ZipFile(patchedApk.toFile()).use { apk ->
assertTrue(apk.getEntry("AndroidManifest.xml") != null, "Morphe output is not an APK")
}
val result = JsonParser.parseString(Files.readString(resultFile)).asJsonObject
if (result.has("success")) {
assertTrue(result.get("success").asBoolean, "Morphe reported an unsuccessful run")
}
assertEquals("com.twitter.android", result.get("packageName").asString)
assertEquals("12.7.1-release.0", result.get("packageVersion").asString)
val steps = result.getAsJsonArray("patchingSteps").map { it.asJsonObject }
assertTrue(steps.all { it.get("success").asBoolean }, "A Morphe patching step failed")
assertTrue(
steps.map { it.get("step").asString }.containsAll(listOf("PATCHING", "REBUILDING")),
"Morphe did not complete patching and rebuilding"
)
assertFalse(
steps.any { it.get("step").asString == "SIGNING" },
"Unsigned integration test unexpectedly signed the APK"
)
val appliedPatchNames = result.getAsJsonArray("appliedPatches")
.map { it.asJsonObject.get("name").asString }
assertTrue(PATCH_NAME in appliedPatchNames, "Expected patch was not applied: $appliedPatchNames")
assertTrue(result.getAsJsonArray("failedPatches").isEmpty, "Morphe reported failed patches")
} finally {
workDirectory.toFile().deleteRecursively()
}
}
private fun downloadMorphe(cacheDirectory: Path): Path {
cacheDirectory.createDirectories()
val target = cacheDirectory.resolve("morphe-desktop-$MORPHE_VERSION-all.jar")
if (target.exists() && sha256(target) == MORPHE_SHA256) return target
val partial = Files.createTempFile(cacheDirectory, "morphe-desktop-", ".part")
try {
val connection = URI(MORPHE_URL).toURL().openConnection() as HttpURLConnection
connection.instanceFollowRedirects = true
connection.connectTimeout = 30_000
connection.readTimeout = 120_000
connection.setRequestProperty("User-Agent", "twitter-patch-integration-test")
connection.inputStream.use { input ->
Files.copy(input, partial, REPLACE_EXISTING)
}
assertEquals(MORPHE_SHA256, sha256(partial), "Downloaded Morphe checksum mismatch")
Files.move(partial, target, REPLACE_EXISTING)
return target
} finally {
partial.deleteIfExists()
}
}
private fun sha256(path: Path): String {
val digest = MessageDigest.getInstance("SHA-256")
path.inputStream().buffered().use { input ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (true) {
val count = input.read(buffer)
if (count < 0) break
digest.update(buffer, 0, count)
}
}
return HexFormat.of().formatHex(digest.digest())
}
private fun requiredPathProperty(name: String): Path {
val value = System.getProperty(name)
assertFalse(value.isNullOrBlank(), "Missing system property $name")
return Path.of(value)
}
private companion object {
const val PATCH_NAME = "Disable outdated version screen"
const val MORPHE_VERSION = "1.13.1"
const val MORPHE_SHA256 = "a212531f68d3ada4b504bf9dac3e02460589f3bf8868ee66e1b4780152d3466b"
const val MORPHE_URL =
"https://github.com/MorpheApp/morphe-desktop/releases/download/" +
"v$MORPHE_VERSION/morphe-desktop-$MORPHE_VERSION-all.jar"
}
}
@@ -0,0 +1,48 @@
package app.twitter.patches
import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
import app.morphe.patcher.Fingerprint
import app.morphe.patcher.patch.bytecodePatch
import app.twitter.patches.shared.Constants.COMPATIBILITY_X
private val outdatedVersionEnforcementFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/o;",
name = "invoke",
returnType = "Ljava/lang/Object;",
parameters = listOf("Ljava/lang/Object;"),
strings = listOf("Target version is ", "Detected shutdown")
)
private val shutdownStateFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/q;",
name = "isShutdown",
returnType = "Z",
parameters = emptyList()
)
@Suppress("unused")
val disableOutdatedVersionScreenPatch = bytecodePatch(
name = "Disable outdated version screen",
description = "Prevents X from blocking app access when it marks the client version as outdated.",
default = true
) {
compatibleWith(COMPATIBILITY_X)
execute {
outdatedVersionEnforcementFingerprint.method.addInstructions(
0,
"""
sget-object v0, Lkotlin/Unit;->a:Lkotlin/Unit;
return-object v0
"""
)
shutdownStateFingerprint.method.addInstructions(
0,
"""
const/4 v0, 0x0
return v0
"""
)
}
}
@@ -1,13 +0,0 @@
package app.twitter.patches
import app.morphe.patcher.patch.bytecodePatch
import app.twitter.patches.shared.Constants.COMPATIBILITY_X
@Suppress("unused")
val noopPatch = bytecodePatch(
name = "No-op",
description = "Makes no changes; validates the X patching pipeline.",
default = true
) {
compatibleWith(COMPATIBILITY_X)
}