forked from ReVanced/revanced-patches
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<PatchClass<*>, List<String>>( | ||
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) | ||
} | ||
} |
120 changes: 120 additions & 0 deletions
120
src/main/kotlin/app/revanced/patches/shared/misc/hex/BaseHexPatch.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Replacement> | ||
|
||
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, | ||
) | ||
} | ||
} | ||
} | ||
} |