Skip to content

Commit 3c95aac

Browse files
authored
feat: Add Hex patch (#3034)
1 parent b1c19a7 commit 3c95aac

File tree

3 files changed

+194
-0
lines changed

3 files changed

+194
-0
lines changed

api/revanced-patches.api

+19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public final class app/revanced/patches/all/misc/debugging/EnableAndroidDebuggin
2828
public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V
2929
}
3030

31+
public final class app/revanced/patches/all/misc/hex/HexPatch : app/revanced/patches/shared/misc/hex/BaseHexPatch {
32+
public fun <init> ()V
33+
}
34+
3135
public final class app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch : app/revanced/patcher/patch/ResourcePatch {
3236
public static final field INSTANCE Lapp/revanced/patches/all/misc/network/OverrideCertificatePinningPatch;
3337
public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V
@@ -669,6 +673,21 @@ public abstract class app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportRes
669673
protected final fun getGmsCoreVendorGroupId ()Ljava/lang/String;
670674
}
671675

676+
public abstract class app/revanced/patches/shared/misc/hex/BaseHexPatch : app/revanced/patcher/patch/RawResourcePatch {
677+
public fun <init> ()V
678+
public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V
679+
public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V
680+
}
681+
682+
public final class app/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement {
683+
public static final field Companion Lapp/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement$Companion;
684+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
685+
public final fun replacePattern ([B)V
686+
}
687+
688+
public final class app/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement$Companion {
689+
}
690+
672691
public abstract class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch : app/revanced/patcher/patch/BytecodePatch {
673692
public fun <init> (Ljava/lang/String;Ljava/util/Set;)V
674693
public fun <init> (Ljava/util/Set;)V
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package app.revanced.patches.all.misc.hex
2+
3+
import app.revanced.patcher.patch.PatchException
4+
import app.revanced.patcher.patch.annotation.Patch
5+
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.registerNewPatchOption
6+
import app.revanced.patches.shared.misc.hex.BaseHexPatch
7+
import app.revanced.util.Utils.trimIndentMultiline
8+
import app.revanced.patcher.patch.Patch as PatchClass
9+
10+
@Patch(
11+
name = "Hex",
12+
description = "Replaces a hexadecimal patterns of bytes of files in an APK.",
13+
use = false,
14+
)
15+
@Suppress("unused")
16+
class HexPatch : BaseHexPatch() {
17+
// TODO: Instead of stringArrayOption, use a custom option type to work around
18+
// https://github.com/ReVanced/revanced-library/issues/48.
19+
// Replace the custom option type with a stringArrayOption once the issue is resolved.
20+
private val replacementsOption by registerNewPatchOption<PatchClass<*>, List<String>>(
21+
key = "replacements",
22+
title = "replacements",
23+
description = """
24+
Hexadecimal patterns to search for and replace with another in a target file.
25+
26+
A pattern is a sequence of case insensitive strings, each representing hexadecimal bytes, separated by spaces.
27+
An example pattern is 'aa 01 02 FF'.
28+
29+
Every pattern must be followed by a pipe ('|'), the replacement pattern,
30+
another pipe ('|'), and the path to the file to make the changes in relative to the APK root.
31+
The replacement pattern must have the same length as the original pattern.
32+
33+
Full example of a valid input:
34+
'aa 01 02 FF|00 00 00 00|path/to/file'
35+
""".trimIndentMultiline(),
36+
required = true,
37+
valueType = "StringArray",
38+
)
39+
40+
override val replacements
41+
get() = replacementsOption!!.map { from ->
42+
val (pattern, replacementPattern, targetFilePath) = try {
43+
from.split("|", limit = 3)
44+
} catch (e: Exception) {
45+
throw PatchException(
46+
"Invalid input: $from.\n" +
47+
"Every pattern must be followed by a pipe ('|'), " +
48+
"the replacement pattern, another pipe ('|'), " +
49+
"and the path to the file to make the changes in relative to the APK root. ",
50+
)
51+
}
52+
53+
Replacement(pattern, replacementPattern, targetFilePath)
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package app.revanced.patches.shared.misc.hex
2+
3+
import app.revanced.patcher.data.ResourceContext
4+
import app.revanced.patcher.patch.PatchException
5+
import app.revanced.patcher.patch.RawResourcePatch
6+
import kotlin.math.max
7+
8+
abstract class BaseHexPatch : RawResourcePatch() {
9+
internal abstract val replacements: List<Replacement>
10+
11+
override fun execute(context: ResourceContext) {
12+
replacements.groupBy { it.targetFilePath }.forEach { (targetFilePath, replacements) ->
13+
val targetFile = try {
14+
context[targetFilePath, true]
15+
} catch (e: Exception) {
16+
throw PatchException("Could not find target file: $targetFilePath")
17+
}
18+
19+
// TODO: Use a file channel to read and write the file instead of reading the whole file into memory,
20+
// in order to reduce memory usage.
21+
val targetFileBytes = targetFile.readBytes()
22+
23+
replacements.forEach { replacement ->
24+
replacement.replacePattern(targetFileBytes)
25+
}
26+
27+
targetFile.writeBytes(targetFileBytes)
28+
}
29+
}
30+
31+
/**
32+
* Represents a pattern to search for and its replacement pattern.
33+
*
34+
* @property pattern The pattern to search for.
35+
* @property replacementPattern The pattern to replace the [pattern] with.
36+
* @property targetFilePath The path to the file to make the changes in relative to the APK root.
37+
*/
38+
class Replacement(
39+
private val pattern: String,
40+
replacementPattern: String,
41+
internal val targetFilePath: String,
42+
) {
43+
private val patternBytes = pattern.toByteArrayPattern()
44+
private val replacementPattern = replacementPattern.toByteArrayPattern()
45+
46+
init {
47+
if (this.patternBytes.size != this.replacementPattern.size) {
48+
throw PatchException("Pattern and replacement pattern must have the same length: $pattern")
49+
}
50+
}
51+
52+
/**
53+
* Replaces the [patternBytes] with the [replacementPattern] in the [targetFileBytes].
54+
*
55+
* @param targetFileBytes The bytes of the file to make the changes in.
56+
*/
57+
fun replacePattern(targetFileBytes: ByteArray) {
58+
val startIndex = indexOfPatternIn(targetFileBytes)
59+
60+
if (startIndex == -1) {
61+
throw PatchException("Pattern not found in target file: $pattern")
62+
}
63+
64+
replacementPattern.copyInto(targetFileBytes, startIndex)
65+
}
66+
67+
// TODO: Allow searching in a file channel instead of a byte array to reduce memory usage.
68+
/**
69+
* Returns the index of the first occurrence of [patternBytes] in the haystack
70+
* using the Boyer-Moore algorithm.
71+
*
72+
* @param haystack The array to search in.
73+
*
74+
* @return The index of the first occurrence of the [patternBytes] in the haystack or -1
75+
* if the [patternBytes] is not found.
76+
*/
77+
private fun indexOfPatternIn(haystack: ByteArray): Int {
78+
val needle = patternBytes
79+
80+
val haystackLength = haystack.size - 1
81+
val needleLength = needle.size - 1
82+
val right = IntArray(256) { -1 }
83+
84+
for (i in 0 until needleLength) right[needle[i].toInt().and(0xFF)] = i
85+
86+
var skip: Int
87+
for (i in 0..haystackLength - needleLength) {
88+
skip = 0
89+
90+
for (j in needleLength - 1 downTo 0)
91+
if (needle[j] != haystack[i + j]) {
92+
skip = max(1, j - right[haystack[i + j].toInt().and(0xFF)])
93+
94+
break
95+
}
96+
97+
if (skip == 0) return i
98+
}
99+
return -1
100+
}
101+
102+
companion object {
103+
/**
104+
* Convert a string representing a pattern of hexadecimal bytes to a byte array.
105+
*
106+
* @return The byte array representing the pattern.
107+
* @throws PatchException If the pattern is invalid.
108+
*/
109+
private fun String.toByteArrayPattern() = try {
110+
split(" ").map { it.toInt(16).toByte() }.toByteArray()
111+
} catch (e: NumberFormatException) {
112+
throw PatchException(
113+
"Could not parse pattern: $this. A pattern is a sequence of case insensitive strings " +
114+
"representing hexadecimal bytes separated by spaces",
115+
e,
116+
)
117+
}
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)