1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-13 18:30:49 +02:00

Compare commits

...

8 Commits

Author SHA1 Message Date
semantic-release-bot
062ae14936 chore: Release v21.1.0-dev.2 [skip ci]
# [21.1.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.1...v21.1.0-dev.2) (2025-06-20)

### Bug Fixes

* Add back missing log by naming logger correctly ([#332](https://github.com/ReVanced/revanced-patcher/issues/332)) ([e4e66b0](e4e66b0d8b))
* Support UTF-8 chars when compiling instructions in Smali in non UTF-8 environments ([#331](https://github.com/ReVanced/revanced-patcher/issues/331)) ([bb8771b](bb8771bb8b))

### Features

* Use option name as key for simplicity and consistency ([754b02e](754b02e4ca))

### Performance Improvements

* Use a buffered writer to reduce IO overhead ([#347](https://github.com/ReVanced/revanced-patcher/issues/347)) ([99f4318](99f431897e))
2025-06-20 13:28:31 +00:00
Pg
99f431897e perf: Use a buffered writer to reduce IO overhead (#347) 2025-06-20 15:26:10 +02:00
oSumAtrIX
d80abbcd17 docs: Correct API usage of fingerprints 2025-03-10 13:52:09 +01:00
oSumAtrIX
509ecc81e1 docs: Correct API usage of fingerprints 2025-03-10 13:47:55 +01:00
kitadai31
e4e66b0d8b fix: Add back missing log by naming logger correctly (#332) 2025-01-20 00:40:26 +01:00
Vologhat
bb8771bb8b fix: Support UTF-8 chars when compiling instructions in Smali in non UTF-8 environments (#331) 2025-01-07 01:30:21 +01:00
oSumAtrIX
754b02e4ca feat: Use option name as key for simplicity and consistency 2024-12-24 16:47:48 +01:00
oSumAtrIX
fe5fb736cb build: Bump dependencies 2024-12-17 04:20:28 +01:00
11 changed files with 163 additions and 89 deletions

View File

@@ -1,3 +1,21 @@
# [21.1.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.1...v21.1.0-dev.2) (2025-06-20)
### Bug Fixes
* Add back missing log by naming logger correctly ([#332](https://github.com/ReVanced/revanced-patcher/issues/332)) ([e4e66b0](https://github.com/ReVanced/revanced-patcher/commit/e4e66b0d8bb0986b79fb150b9c15da35b8e11561))
* Support UTF-8 chars when compiling instructions in Smali in non UTF-8 environments ([#331](https://github.com/ReVanced/revanced-patcher/issues/331)) ([bb8771b](https://github.com/ReVanced/revanced-patcher/commit/bb8771bb8b8ab1724d957e56f4de88c02684d87b))
### Features
* Use option name as key for simplicity and consistency ([754b02e](https://github.com/ReVanced/revanced-patcher/commit/754b02e4ca66ec10764d5205c6643f2d86d0c6a2))
### Performance Improvements
* Use a buffered writer to reduce IO overhead ([#347](https://github.com/ReVanced/revanced-patcher/issues/347)) ([99f4318](https://github.com/ReVanced/revanced-patcher/commit/99f431897eb9e607987fd5d09b879d7eda442f3e))
# [21.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0...v21.1.0-dev.1) (2024-12-07) # [21.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0...v21.1.0-dev.1) (2024-12-07)

View File

@@ -73,6 +73,8 @@ public final class app/revanced/patcher/Patcher : java/io/Closeable {
} }
public final class app/revanced/patcher/PatcherConfig { public final class app/revanced/patcher/PatcherConfig {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
} }
@@ -169,9 +171,12 @@ public final class app/revanced/patcher/patch/BytecodePatchContext : app/revance
public final class app/revanced/patcher/patch/Option { public final class app/revanced/patcher/patch/Option {
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDefault ()Ljava/lang/Object; public final fun getDefault ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String; public final fun getDescription ()Ljava/lang/String;
public final fun getKey ()Ljava/lang/String; public final fun getKey ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getRequired ()Z public final fun getRequired ()Z
public final fun getTitle ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String;
public final fun getType ()Lkotlin/reflect/KType; public final fun getType ()Lkotlin/reflect/KType;

View File

@@ -85,14 +85,14 @@ val disableAdsPatch = bytecodePatch(
// Business logic of the patch to disable ads in the app. // Business logic of the patch to disable ads in the app.
execute { execute {
// Fingerprint to find the method to patch. // Fingerprint to find the method to patch.
val showAdsMatch by showAdsFingerprint { val showAdsFingerprint = fingerprint {
// More about fingerprints on the next page of the documentation. // More about fingerprints on the next page of the documentation.
} }
// In the method that shows ads, // In the method that shows ads,
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file) // call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
// to enable or disable ads. // to enable or disable ads.
showAdsMatch.method.addInstructions( showAdsFingerprint.method.addInstructions(
0, 0,
""" """
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
@@ -122,10 +122,10 @@ To define an option, use the available `option` functions:
```kt ```kt
val patch = bytecodePatch(name = "Patch") { val patch = bytecodePatch(name = "Patch") {
// Add an inbuilt option and delegate it to a property. // Add an inbuilt option and delegate it to a property.
val value by stringOption(key = "option") val value by stringOption(name = "Inbuilt option")
// Add an option with a custom type and delegate it to a property. // Add an option with a custom type and delegate it to a property.
val string by option<String>(key = "string") val string by option<String>(name = "String option")
execute { execute {
println(value) println(value)
@@ -139,7 +139,7 @@ Options of a patch can be set after loading the patches with `PatchLoader` by ob
```kt ```kt
loadPatchesJar(patches).apply { loadPatchesJar(patches).apply {
// Type is checked at runtime. // Type is checked at runtime.
first { it.name == "Patch" }.options["option"] = "Value" first { it.name == "Patch" }.options["Option"] = "Value"
} }
``` ```
@@ -152,7 +152,7 @@ option.type // The KType of the option. Captures the full type information of th
Options can be declared outside a patch and added to a patch manually: Options can be declared outside a patch and added to a patch manually:
```kt ```kt
val option = stringOption(key = "option") val option = stringOption(name = "Option")
bytecodePatch(name = "Patch") { bytecodePatch(name = "Patch") {
val value by option() val value by option()
@@ -185,7 +185,7 @@ val patch = bytecodePatch(name = "Complex patch") {
extendWith("complex-patch.rve") extendWith("complex-patch.rve")
execute { execute {
fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V") fingerprint.method.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
} }
} }
``` ```
@@ -249,10 +249,10 @@ The same order is followed for multiple patches depending on the patch.
- A patch can declare compatibility with specific packages and versions, - A patch can declare compatibility with specific packages and versions,
but patches can still be executed on any package or version. but patches can still be executed on any package or version.
It is recommended that compatibility is specified to present known compatible packages and versions. It is recommended that compatibility is specified to present known compatible packages and versions.
- If `compatibleWith` is not used, the patch is treated as compatible with any package - If `compatibleWith` is not used, the patch is treated as compatible with any package
- If a package is specified with no versions, the patch is compatible with any version of the package - If a package is specified with no versions, the patch is compatible with any version of the package
- If an empty array of versions is specified, the patch is not compatible with any version of the package. - If an empty array of versions is specified, the patch is not compatible with any version of the package.
This is useful for declaring incompatibility with a specific package. This is useful for declaring incompatibility with a specific package.
- A patch can raise a `PatchException` at any time of execution to indicate that the patch failed to execute. - A patch can raise a `PatchException` at any time of execution to indicate that the patch failed to execute.
## ⏭️ What's next ## ⏭️ What's next

View File

@@ -1,3 +1,3 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
version = 21.1.0-dev.1 version = 21.1.0-dev.2

View File

@@ -1,14 +1,14 @@
[versions] [versions]
android = "4.1.1.4" android = "4.1.1.4"
apktool-lib = "2.9.3" apktool-lib = "2.10.1.1"
binary-compatibility-validator = "0.15.1" binary-compatibility-validator = "0.15.1"
kotlin = "2.0.0" kotlin = "2.0.20"
kotlinx-coroutines-core = "1.8.1" kotlinx-coroutines-core = "1.8.1"
mockk = "1.13.10" mockk = "1.13.10"
multidexlib2 = "3.0.3.r3" multidexlib2 = "3.0.3.r3"
# Tracking https://github.com/google/smali/issues/64. # Tracking https://github.com/google/smali/issues/64.
#noinspection GradleDependency #noinspection GradleDependency
smali = "3.0.5" smali = "3.0.8"
xpp3 = "1.1.4c" xpp3 = "1.1.4c"
[libraries] [libraries]

View File

@@ -16,9 +16,28 @@ import java.util.logging.Logger
class PatcherConfig( class PatcherConfig(
internal val apkFile: File, internal val apkFile: File,
private val temporaryFilesPath: File = File("revanced-temporary-files"), private val temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: String? = null, aaptBinaryPath: File? = null,
frameworkFileDirectory: String? = null, frameworkFileDirectory: String? = null,
) { ) {
/**
* The configuration for the patcher.
*
* @param apkFile The apk file to patch.
* @param temporaryFilesPath A path to a folder to store temporary files in.
* @param aaptBinaryPath A path to a custom aapt binary.
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
*/
@Deprecated(
"Use the constructor with a File for aaptBinaryPath instead.",
ReplaceWith("PatcherConfig(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)"),
)
constructor(
apkFile: File,
temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: String? = null,
frameworkFileDirectory: String? = null,
) : this(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)
private val logger = Logger.getLogger(PatcherConfig::class.java.name) private val logger = Logger.getLogger(PatcherConfig::class.java.name)
/** /**
@@ -33,8 +52,7 @@ class PatcherConfig(
*/ */
internal val resourceConfig = internal val resourceConfig =
Config.getDefaultConfig().apply { Config.getDefaultConfig().apply {
useAapt2 = true aaptBinary = aaptBinaryPath
aaptPath = aaptBinaryPath ?: ""
frameworkDirectory = frameworkFileDirectory frameworkDirectory = frameworkFileDirectory
} }

View File

@@ -34,7 +34,7 @@ import java.util.logging.Logger
class BytecodePatchContext internal constructor(private val config: PatcherConfig) : class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>, PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable { Closeable {
private val logger = Logger.getLogger(this::javaClass.name) private val logger = Logger.getLogger(this::class.java.name)
/** /**
* [Opcodes] of the supplied [PatcherConfig.apkFile]. * [Opcodes] of the supplied [PatcherConfig.apkFile].

View File

@@ -20,16 +20,51 @@ import kotlin.reflect.typeOf
* @constructor Create a new [Option]. * @constructor Create a new [Option].
*/ */
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
class Option<T> @PublishedApi internal constructor( class Option<T>
@PublishedApi
@Deprecated("Use the constructor with the name instead of a key instead.")
internal constructor(
@Deprecated("Use the name property instead.")
val key: String, val key: String,
val default: T? = null, val default: T? = null,
val values: Map<String, T?>? = null, val values: Map<String, T?>? = null,
@Deprecated("Use the name property instead.")
val title: String? = null, val title: String? = null,
val description: String? = null, val description: String? = null,
val required: Boolean = false, val required: Boolean = false,
val type: KType, val type: KType,
val validator: Option<T>.(T?) -> Boolean = { true }, val validator: Option<T>.(T?) -> Boolean = { true },
) { ) {
/**
* The name.
*/
val name = key
/**
* An option.
*
* @param T The value type of the option.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param description A description.
* @param required Whether the option is required.
* @param type The type of the option value (to handle type erasure).
* @param validator The function to validate the option value.
*
* @constructor Create a new [Option].
*/
@PublishedApi
internal constructor(
name: String,
default: T? = null,
values: Map<String, T?>? = null,
description: String? = null,
required: Boolean = false,
type: KType,
validator: Option<T>.(T?) -> Boolean = { true },
) : this(name, default, values, name, description, required, type, validator)
/** /**
* The value of the [Option]. * The value of the [Option].
*/ */
@@ -109,7 +144,7 @@ class Option<T> @PublishedApi internal constructor(
class Options internal constructor( class Options internal constructor(
private val options: Map<String, Option<*>>, private val options: Map<String, Option<*>>,
) : Map<String, Option<*>> by options { ) : Map<String, Option<*>> by options {
internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.key }) internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.name })
/** /**
* Set an option's value. * Set an option's value.
@@ -856,14 +891,14 @@ sealed class OptionException(errorMessage: String) : Exception(errorMessage, nul
* *
* @param value The value that failed validation. * @param value The value that failed validation.
*/ */
class ValueValidationException(value: Any?, option: Option<*>) : OptionException("The option value \"$value\" failed validation for ${option.key}") class ValueValidationException(value: Any?, option: Option<*>) : OptionException("The option value \"$value\" failed validation for ${option.name}")
/** /**
* An exception thrown when a value is required but null was passed. * An exception thrown when a value is required but null was passed.
* *
* @param option The [Option] that requires a value. * @param option The [Option] that requires a value.
*/ */
class ValueRequiredException(option: Option<*>) : OptionException("The option ${option.key} requires a value, but the value was null") class ValueRequiredException(option: Option<*>) : OptionException("The option ${option.name} requires a value, but the value was null")
/** /**
* An exception thrown when a [Option] is not found. * An exception thrown when a [Option] is not found.

View File

@@ -10,9 +10,9 @@ import brut.androlib.ApkDecoder
import brut.androlib.apk.UsesFramework import brut.androlib.apk.UsesFramework
import brut.androlib.res.Framework import brut.androlib.res.Framework
import brut.androlib.res.ResourcesDecoder import brut.androlib.res.ResourcesDecoder
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
import brut.androlib.res.decoder.AndroidManifestResourceParser import brut.androlib.res.decoder.AndroidManifestResourceParser
import brut.androlib.res.decoder.XmlPullStreamDecoder import brut.androlib.res.xml.ResXmlUtils
import brut.androlib.res.xml.ResXmlPatcher
import brut.directory.ExtFile import brut.directory.ExtFile
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@@ -51,64 +51,62 @@ class ResourcePatchContext internal constructor(
* *
* @param mode The [ResourceMode] to use. * @param mode The [ResourceMode] to use.
*/ */
internal fun decodeResources(mode: ResourceMode) = internal fun decodeResources(mode: ResourceMode) = with(packageMetadata.apkInfo) {
with(packageMetadata.apkInfo) { config.initializeTemporaryFilesDirectories()
config.initializeTemporaryFilesDirectories()
// Needed to decode resources. // Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this) val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this)
if (mode == ResourceMode.FULL) { if (mode == ResourceMode.FULL) {
logger.info("Decoding resources") logger.info("Decoding resources")
resourcesDecoder.decodeResources(config.apkFiles) resourcesDecoder.decodeResources(config.apkFiles)
resourcesDecoder.decodeManifest(config.apkFiles) resourcesDecoder.decodeManifest(config.apkFiles)
// Needed to record uncompressed files. // Needed to record uncompressed files.
val apkDecoder = ApkDecoder(config.resourceConfig, this) ApkDecoder(this, config.resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework = usesFramework =
UsesFramework().apply { UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id } ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
} else {
logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
XmlPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer,
).decodeManifest(
apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { // Do nothing.
}
},
)
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode
}
/*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
because ResourcesDecoder.decodeResources loads the main package
and not XmlPullStreamDecoder.decodeManifest.
See ARSCDecoder.readTableType for more info.
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
*/
metadata.apkInfo.sparseResources = false
} }
} else {
logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
AndroidManifestPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.newXmlSerializer(),
).decode(
apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { // Do nothing.
}
},
)
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
// AndroidManifestPullStreamDecoder.decode() sets metadata.apkInfo.
packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode
}
/*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
because ResourcesDecoder.decodeResources loads the main package
and not AndroidManifestPullStreamDecoder.decode.
See ARSCDecoder.readTableType for more info.
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
*/
metadata.apkInfo.sparseResources = false
} }
} }
}
/** /**
* Compile resources in [PatcherConfig.apkFiles]. * Compile resources in [PatcherConfig.apkFiles].
@@ -130,10 +128,10 @@ class ResourcePatchContext internal constructor(
AaptInvoker( AaptInvoker(
config.resourceConfig, config.resourceConfig,
packageMetadata.apkInfo, packageMetadata.apkInfo,
).invokeAapt( ).invoke(
resources.resolve("resources.apk"), resources.resolve("resources.apk"),
config.apkFiles.resolve("AndroidManifest.xml").also { config.apkFiles.resolve("AndroidManifest.xml").also {
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it) ResXmlUtils.fixingPublicAttrsInProviderAttributes(it)
}, },
config.apkFiles.resolve("res"), config.apkFiles.resolve("res"),
null, null,

View File

@@ -34,7 +34,7 @@ class Document internal constructor(
readerCount.remove(it) readerCount.remove(it)
} }
it.outputStream().use { stream -> it.outputStream().buffered().use { stream ->
TransformerFactory.newInstance() TransformerFactory.newInstance()
.newTransformer() .newTransformer()
.transform(DOMSource(this), StreamResult(stream)) .transform(DOMSource(this), StreamResult(stream))

View File

@@ -50,7 +50,7 @@ class InlineSmaliCompiler {
registers, registers,
instructions, instructions,
) )
val reader = InputStreamReader(input.byteInputStream()) val reader = InputStreamReader(input.byteInputStream(), Charsets.UTF_8)
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15) val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource) val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens) val parser = smaliParser(tokens)