diff --git a/api/revanced-patches.api b/api/revanced-patches.api index c17ab404a3..d61454ba1d 100644 --- a/api/revanced-patches.api +++ b/api/revanced-patches.api @@ -28,6 +28,10 @@ public final class app/revanced/patches/all/misc/debugging/EnableAndroidDebuggin public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V } +public final class app/revanced/patches/all/misc/hex/HexPatch : app/revanced/patches/shared/misc/hex/BaseHexPatch { + public fun ()V +} + public final class app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch : app/revanced/patcher/patch/ResourcePatch { public static final field INSTANCE Lapp/revanced/patches/all/misc/network/OverrideCertificatePinningPatch; public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V @@ -669,6 +673,21 @@ public abstract class app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportRes protected final fun getGmsCoreVendorGroupId ()Ljava/lang/String; } +public abstract class app/revanced/patches/shared/misc/hex/BaseHexPatch : app/revanced/patcher/patch/RawResourcePatch { + public fun ()V + public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V + public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V +} + +public final class app/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement { + public static final field Companion Lapp/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun replacePattern ([B)V +} + +public final class app/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement$Companion { +} + public abstract class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch : app/revanced/patcher/patch/BytecodePatch { public fun (Ljava/lang/String;Ljava/util/Set;)V public fun (Ljava/util/Set;)V diff --git a/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt b/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt new file mode 100644 index 0000000000..164eaa13a7 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt @@ -0,0 +1,55 @@ +package app.revanced.patches.all.misc.hex + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.registerNewPatchOption +import app.revanced.patches.shared.misc.hex.BaseHexPatch +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.patcher.patch.Patch as PatchClass + +@Patch( + name = "Hex", + description = "Replaces a hexadecimal patterns of bytes of files in an APK.", + use = false, +) +@Suppress("unused") +class HexPatch : BaseHexPatch() { + // TODO: Instead of stringArrayOption, use a custom option type to work around + // https://github.com/ReVanced/revanced-library/issues/48. + // Replace the custom option type with a stringArrayOption once the issue is resolved. + private val replacementsOption by registerNewPatchOption, List>( + key = "replacements", + title = "replacements", + description = """ + Hexadecimal patterns to search for and replace with another in a target file. + + A pattern is a sequence of case insensitive strings, each representing hexadecimal bytes, separated by spaces. + An example pattern is 'aa 01 02 FF'. + + Every pattern must be followed by a pipe ('|'), the replacement pattern, + another pipe ('|'), and the path to the file to make the changes in relative to the APK root. + The replacement pattern must have the same length as the original pattern. + + Full example of a valid input: + 'aa 01 02 FF|00 00 00 00|path/to/file' + """.trimIndentMultiline(), + required = true, + valueType = "StringArray", + ) + + override val replacements + get() = replacementsOption!!.map { from -> + val (pattern, replacementPattern, targetFilePath) = try { + from.split("|", limit = 3) + } catch (e: Exception) { + throw PatchException( + "Invalid input: $from.\n" + + "Every pattern must be followed by a pipe ('|'), " + + "the replacement pattern, another pipe ('|'), " + + "and the path to the file to make the changes in relative to the APK root. ", + ) + } + + Replacement(pattern, replacementPattern, targetFilePath) + } +} diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/hex/BaseHexPatch.kt b/src/main/kotlin/app/revanced/patches/shared/misc/hex/BaseHexPatch.kt new file mode 100644 index 0000000000..781444d688 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/shared/misc/hex/BaseHexPatch.kt @@ -0,0 +1,120 @@ +package app.revanced.patches.shared.misc.hex + +import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.RawResourcePatch +import kotlin.math.max + +abstract class BaseHexPatch : RawResourcePatch() { + internal abstract val replacements: List + + override fun execute(context: ResourceContext) { + replacements.groupBy { it.targetFilePath }.forEach { (targetFilePath, replacements) -> + val targetFile = try { + context[targetFilePath, true] + } catch (e: Exception) { + throw PatchException("Could not find target file: $targetFilePath") + } + + // TODO: Use a file channel to read and write the file instead of reading the whole file into memory, + // in order to reduce memory usage. + val targetFileBytes = targetFile.readBytes() + + replacements.forEach { replacement -> + replacement.replacePattern(targetFileBytes) + } + + targetFile.writeBytes(targetFileBytes) + } + } + + /** + * Represents a pattern to search for and its replacement pattern. + * + * @property pattern The pattern to search for. + * @property replacementPattern The pattern to replace the [pattern] with. + * @property targetFilePath The path to the file to make the changes in relative to the APK root. + */ + class Replacement( + private val pattern: String, + replacementPattern: String, + internal val targetFilePath: String, + ) { + private val patternBytes = pattern.toByteArrayPattern() + private val replacementPattern = replacementPattern.toByteArrayPattern() + + init { + if (this.patternBytes.size != this.replacementPattern.size) { + throw PatchException("Pattern and replacement pattern must have the same length: $pattern") + } + } + + /** + * Replaces the [patternBytes] with the [replacementPattern] in the [targetFileBytes]. + * + * @param targetFileBytes The bytes of the file to make the changes in. + */ + fun replacePattern(targetFileBytes: ByteArray) { + val startIndex = indexOfPatternIn(targetFileBytes) + + if (startIndex == -1) { + throw PatchException("Pattern not found in target file: $pattern") + } + + replacementPattern.copyInto(targetFileBytes, startIndex) + } + + // TODO: Allow searching in a file channel instead of a byte array to reduce memory usage. + /** + * Returns the index of the first occurrence of [patternBytes] in the haystack + * using the Boyer-Moore algorithm. + * + * @param haystack The array to search in. + * + * @return The index of the first occurrence of the [patternBytes] in the haystack or -1 + * if the [patternBytes] is not found. + */ + private fun indexOfPatternIn(haystack: ByteArray): Int { + val needle = patternBytes + + val haystackLength = haystack.size - 1 + val needleLength = needle.size - 1 + val right = IntArray(256) { -1 } + + for (i in 0 until needleLength) right[needle[i].toInt().and(0xFF)] = i + + var skip: Int + for (i in 0..haystackLength - needleLength) { + skip = 0 + + for (j in needleLength - 1 downTo 0) + if (needle[j] != haystack[i + j]) { + skip = max(1, j - right[haystack[i + j].toInt().and(0xFF)]) + + break + } + + if (skip == 0) return i + } + return -1 + } + + companion object { + /** + * Convert a string representing a pattern of hexadecimal bytes to a byte array. + * + * @return The byte array representing the pattern. + * @throws PatchException If the pattern is invalid. + */ + private fun String.toByteArrayPattern() = try { + split(" ").map { it.toInt(16).toByte() }.toByteArray() + } catch (e: NumberFormatException) { + throw PatchException( + "Could not parse pattern: $this. A pattern is a sequence of case insensitive strings " + + "representing hexadecimal bytes separated by spaces", + e, + ) + } + } + } +}