1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-06 16:38:50 +02:00

Compare commits

...

48 Commits

Author SHA1 Message Date
semantic-release-bot
f1775f83d0 chore(release): 4.1.3 [skip ci]
## [4.1.3](https://github.com/revanced/revanced-patcher/compare/v4.1.2...v4.1.3) (2022-09-07)

### Bug Fixes

* only run list option check if not null ([4055939](4055939c08))
2022-09-07 21:48:24 +00:00
Sculas
4055939c08 fix: only run list option check if not null 2022-09-07 23:46:46 +02:00
semantic-release-bot
85120374d6 chore(release): 4.1.2 [skip ci]
## [4.1.2](https://github.com/revanced/revanced-patcher/compare/v4.1.1...v4.1.2) (2022-09-07)

### Bug Fixes

* invalid types for example options ([79f91e0](79f91e0e5a))
2022-09-07 21:24:50 +00:00
Sculas
8b4819faa1 Merge remote-tracking branch 'origin/main' into main 2022-09-07 23:23:29 +02:00
semantic-release-bot
d219276298 chore(release): 4.1.1 [skip ci]
## [4.1.1](https://github.com/revanced/revanced-patcher/compare/v4.1.0...v4.1.1) (2022-09-07)

### Bug Fixes

* handle private companion objects ([ad3d332](ad3d332e27))
2022-09-07 21:23:12 +00:00
Sculas
79f91e0e5a fix: invalid types for example options 2022-09-07 23:22:34 +02:00
Sculas
fadf62f594 Merge remote-tracking branch 'origin/main' into main 2022-09-07 23:21:41 +02:00
Sculas
ad3d332e27 fix: handle private companion objects 2022-09-07 23:21:32 +02:00
semantic-release-bot
8f66df7666 chore(release): 4.1.0 [skip ci]
# [4.1.0](https://github.com/revanced/revanced-patcher/compare/v4.0.0...v4.1.0) (2022-09-07)

### Features

* deprecation for patches ([80c2e80](80c2e80925))
2022-09-07 20:32:51 +00:00
Sculas
80c2e80925 feat: deprecation for patches 2022-09-07 22:31:15 +02:00
semantic-release-bot
c3db23d3c7 chore(release): 4.0.0 [skip ci]
# [4.0.0](https://github.com/revanced/revanced-patcher/compare/v3.5.1...v4.0.0) (2022-09-07)

### Code Refactoring

* Improve Patch Options ([6b909c1](6b909c1ee6))

### BREAKING CHANGES

* Options has been moved from Patch to a new interface called OptionsContainer and are now handled entirely different. Make sure to check the examples to understand how it works.
2022-09-07 18:57:04 +00:00
Sculas
c28584736e Merge remote-tracking branch 'origin/main' into main 2022-09-07 20:55:45 +02:00
Sculas
6b909c1ee6 refactor: Improve Patch Options
It's so much better now. Really happy with the current system.

BREAKING CHANGE: Options has been moved from Patch to a new interface called OptionsContainer and are now handled entirely different. Make sure to check the examples to understand how it works.
2022-09-07 20:55:35 +02:00
Sculas
0e8446516e build: add Kotlin Reflect 2022-09-07 20:52:05 +02:00
semantic-release-bot
aa46b953db chore(release): 3.5.1 [skip ci]
## [3.5.1](https://github.com/revanced/revanced-patcher/compare/v3.5.0...v3.5.1) (2022-09-06)

### Bug Fixes

* add tests for PathOption ([d6308e1](d6308e126c))
* PathOption should be open, not sealed ([a562e47](a562e476c0))
* typo in ListOption ([3921648](392164862c))

### Performance Improvements

* make exception an object ([75d2be8](75d2be8803))
2022-09-06 20:38:24 +00:00
Sculas
a562e476c0 fix: PathOption should be open, not sealed 2022-09-06 22:36:20 +02:00
Sculas
75d2be8803 perf: make exception an object 2022-09-06 22:35:33 +02:00
Sculas
d6308e126c fix: add tests for PathOption 2022-09-06 22:35:00 +02:00
Sculas
bb97af4d86 refactor: add FileOption alias for PathOption 2022-09-06 22:34:46 +02:00
Sculas
392164862c fix: typo in ListOption 2022-09-06 21:44:05 +02:00
Sculas
53e807dec1 refactor: add PatchOption.PathOption 2022-09-06 21:43:45 +02:00
semantic-release-bot
288d50a8b4 chore(release): 3.5.0 [skip ci]
# [3.5.0](https://github.com/revanced/revanced-patcher/compare/v3.4.1...v3.5.0) (2022-09-05)

### Features

* default value for `Package.versions` annotation parameter ([131dedd](131dedd4b0))
2022-09-05 14:45:56 +00:00
oSumAtrIX
131dedd4b0 feat: default value for Package.versions annotation parameter
Reverts 4b81318710
2022-09-05 16:44:35 +02:00
semantic-release-bot
5a92d5c29d chore(release): 3.4.1 [skip ci]
## [3.4.1](https://github.com/revanced/revanced-patcher/compare/v3.4.0...v3.4.1) (2022-09-03)

### Bug Fixes

* remove default param from Package.versions ([4b81318](4b81318710))
2022-09-03 20:54:06 +00:00
Sculas
4b81318710 fix: remove default param from Package.versions
Kotlin compiler bug produces invalid bytecode, resulting in an IncompleteAnnotationException at runtime.
2022-09-03 22:52:49 +02:00
semantic-release-bot
44f6a3ebc5 chore(release): 3.4.0 [skip ci]
# [3.4.0](https://github.com/revanced/revanced-patcher/compare/v3.3.3...v3.4.0) (2022-08-31)

### Features

* nullable parameters ([7882a8d](7882a8d928))
2022-08-31 18:32:43 +00:00
oSumAtrIX
7882a8d928 feat: nullable parameters
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-08-31 20:30:31 +02:00
semantic-release-bot
cc3d32748b chore(release): 3.3.3 [skip ci]
## [3.3.3](https://github.com/revanced/revanced-patcher/compare/v3.3.2...v3.3.3) (2022-08-14)

### Bug Fixes

* show error message if cause is null ([f9da2ad](f9da2ad531))
2022-08-14 15:25:16 +00:00
oSumAtrIX
f9da2ad531 fix: show error message if cause is null 2022-08-14 17:22:43 +02:00
semantic-release-bot
b19e1131e8 chore(release): 3.3.2 [skip ci]
## [3.3.2](https://github.com/revanced/revanced-patcher/compare/v3.3.1...v3.3.2) (2022-08-06)

### Bug Fixes

* close open files ([#75](https://github.com/revanced/revanced-patcher/issues/75)) ([123ad54](123ad54c15))
2022-08-06 22:17:39 +00:00
dan1st
123ad54c15 fix: close open files (#75) 2022-08-07 00:16:23 +02:00
Sculas
09f6ab4155 Merge remote-tracking branch 'origin/main' into main 2022-08-03 18:32:34 +02:00
Sculas
01cf3fb50f refactor: util package structure 2022-08-03 18:31:31 +02:00
semantic-release-bot
6c5f9d4198 chore(release): 3.3.1 [skip ci]
## [3.3.1](https://github.com/revanced/revanced-patcher/compare/v3.3.0...v3.3.1) (2022-08-03)

### Bug Fixes

* revert soft dependencies ([7b2d058](7b2d058144))
2022-08-03 01:48:56 +00:00
oSumAtrIX
7b2d058144 fix: revert soft dependencies 2022-08-03 03:45:34 +02:00
semantic-release-bot
db2804270e chore(release): 3.3.0 [skip ci]
# [3.3.0](https://github.com/revanced/revanced-patcher/compare/v3.2.1...v3.3.0) (2022-08-02)

### Features

* add getValue & setValue for PatchOption ([2572cd0](2572cd04b5))
2022-08-02 21:31:59 +00:00
Sculas
2572cd04b5 feat: add getValue & setValue for PatchOption 2022-08-02 23:30:38 +02:00
semantic-release-bot
5eb8b428b9 chore(release): 3.2.1 [skip ci]
## [3.2.1](https://github.com/revanced/revanced-patcher/compare/v3.2.0...v3.2.1) (2022-08-02)

### Bug Fixes

* check if patch option requirement is met ([14a73bf](14a73bfcaf))
2022-08-02 20:43:26 +00:00
Sculas
3a118d9b9d Merge remote-tracking branch 'origin/main' into main 2022-08-02 22:41:41 +02:00
Sculas
14a73bfcaf fix: check if patch option requirement is met 2022-08-02 22:41:34 +02:00
semantic-release-bot
567bf52e16 chore(release): 3.2.0 [skip ci]
# [3.2.0](https://github.com/revanced/revanced-patcher/compare/v3.1.0...v3.2.0) (2022-08-02)

### Features

* PatchOptions#nullify to nullify an option ([371f0c4](371f0c4d0b))
2022-08-02 20:36:21 +00:00
Sculas
35c6489dba Merge remote-tracking branch 'origin/main' into main 2022-08-02 22:34:44 +02:00
Sculas
371f0c4d0b feat: PatchOptions#nullify to nullify an option 2022-08-02 22:32:55 +02:00
Sculas
1b42f65d95 refactor: migrate to custom exceptions for patch options 2022-08-02 22:16:37 +02:00
semantic-release-bot
2aee0cbd0f chore(release): 3.1.0 [skip ci]
# [3.1.0](https://github.com/revanced/revanced-patcher/compare/v3.0.0...v3.1.0) (2022-08-02)

### Features

* validator for patch options ([4e2e772](4e2e772389))
2022-08-02 20:02:18 +00:00
Sculas
19256b5437 Merge remote-tracking branch 'origin/main' into main 2022-08-02 22:00:40 +02:00
Sculas
67a5237541 test: refactor & add more tests 2022-08-02 22:00:32 +02:00
Sculas
4e2e772389 feat: validator for patch options 2022-08-02 22:00:10 +02:00
19 changed files with 544 additions and 232 deletions

View File

@@ -1,3 +1,127 @@
## [4.1.3](https://github.com/revanced/revanced-patcher/compare/v4.1.2...v4.1.3) (2022-09-07)
### Bug Fixes
* only run list option check if not null ([4055939](https://github.com/revanced/revanced-patcher/commit/4055939c089e3c396c308c980215d93a1dea5954))
## [4.1.2](https://github.com/revanced/revanced-patcher/compare/v4.1.1...v4.1.2) (2022-09-07)
### Bug Fixes
* invalid types for example options ([79f91e0](https://github.com/revanced/revanced-patcher/commit/79f91e0e5a6d99828f30aae55339ce0d897394c7))
## [4.1.1](https://github.com/revanced/revanced-patcher/compare/v4.1.0...v4.1.1) (2022-09-07)
### Bug Fixes
* handle private companion objects ([ad3d332](https://github.com/revanced/revanced-patcher/commit/ad3d332e27d07e9d074bbaaf51af7eb2f9bfc7d5))
# [4.1.0](https://github.com/revanced/revanced-patcher/compare/v4.0.0...v4.1.0) (2022-09-07)
### Features
* deprecation for patches ([80c2e80](https://github.com/revanced/revanced-patcher/commit/80c2e809251cdb04d2dd3b3bfdbb8844bdfa31fa))
# [4.0.0](https://github.com/revanced/revanced-patcher/compare/v3.5.1...v4.0.0) (2022-09-07)
### Code Refactoring
* Improve Patch Options ([6b909c1](https://github.com/revanced/revanced-patcher/commit/6b909c1ee6b8c2ea08bbca059df755e2e5f31656))
### BREAKING CHANGES
* Options has been moved from Patch to a new interface called OptionsContainer and are now handled entirely different. Make sure to check the examples to understand how it works.
## [3.5.1](https://github.com/revanced/revanced-patcher/compare/v3.5.0...v3.5.1) (2022-09-06)
### Bug Fixes
* add tests for PathOption ([d6308e1](https://github.com/revanced/revanced-patcher/commit/d6308e126c6217b098192c51b6e98bc85a8656bd))
* PathOption should be open, not sealed ([a562e47](https://github.com/revanced/revanced-patcher/commit/a562e476c085841efbc7ee98b01d8e6bb18ed757))
* typo in ListOption ([3921648](https://github.com/revanced/revanced-patcher/commit/392164862c83d6e76b2a2113d6f6d59fef0020d1))
### Performance Improvements
* make exception an object ([75d2be8](https://github.com/revanced/revanced-patcher/commit/75d2be88037c9cf5436ab69d92abea575409a865))
# [3.5.0](https://github.com/revanced/revanced-patcher/compare/v3.4.1...v3.5.0) (2022-09-05)
### Features
* default value for `Package.versions` annotation parameter ([131dedd](https://github.com/revanced/revanced-patcher/commit/131dedd4b021fe1c3b0be49ccba4764b325770ea))
## [3.4.1](https://github.com/revanced/revanced-patcher/compare/v3.4.0...v3.4.1) (2022-09-03)
### Bug Fixes
* remove default param from Package.versions ([4b81318](https://github.com/revanced/revanced-patcher/commit/4b813187107e85dc267dbc2d353884b2cc671cc4))
# [3.4.0](https://github.com/revanced/revanced-patcher/compare/v3.3.3...v3.4.0) (2022-08-31)
### Features
* nullable parameters ([7882a8d](https://github.com/revanced/revanced-patcher/commit/7882a8d928cad8de8cfea711947fc02659549d20))
## [3.3.3](https://github.com/revanced/revanced-patcher/compare/v3.3.2...v3.3.3) (2022-08-14)
### Bug Fixes
* show error message if cause is null ([f9da2ad](https://github.com/revanced/revanced-patcher/commit/f9da2ad531644617ad5a2cc6a1819d530e18ba22))
## [3.3.2](https://github.com/revanced/revanced-patcher/compare/v3.3.1...v3.3.2) (2022-08-06)
### Bug Fixes
* close open files ([#75](https://github.com/revanced/revanced-patcher/issues/75)) ([123ad54](https://github.com/revanced/revanced-patcher/commit/123ad54c150bd04f4b8ef5c65334ea468ceb99cc))
## [3.3.1](https://github.com/revanced/revanced-patcher/compare/v3.3.0...v3.3.1) (2022-08-03)
### Bug Fixes
* revert soft dependencies ([7b2d058](https://github.com/revanced/revanced-patcher/commit/7b2d058144b0718992d329731e2af7cc704e4370))
# [3.3.0](https://github.com/revanced/revanced-patcher/compare/v3.2.1...v3.3.0) (2022-08-02)
### Features
* add getValue & setValue for PatchOption ([2572cd0](https://github.com/revanced/revanced-patcher/commit/2572cd04b5da4eeae738c8dde31493177edf0bf8))
## [3.2.1](https://github.com/revanced/revanced-patcher/compare/v3.2.0...v3.2.1) (2022-08-02)
### Bug Fixes
* check if patch option requirement is met ([14a73bf](https://github.com/revanced/revanced-patcher/commit/14a73bfcafac36bce2b8466788d460edde7a14fd))
# [3.2.0](https://github.com/revanced/revanced-patcher/compare/v3.1.0...v3.2.0) (2022-08-02)
### Features
* PatchOptions#nullify to nullify an option ([371f0c4](https://github.com/revanced/revanced-patcher/commit/371f0c4d0bf96e7f6db35085efccaed3000a096c))
# [3.1.0](https://github.com/revanced/revanced-patcher/compare/v3.0.0...v3.1.0) (2022-08-02)
### Features
* validator for patch options ([4e2e772](https://github.com/revanced/revanced-patcher/commit/4e2e77238957d7732326cfe5e05145bf7dab5bfb))
# [3.0.0](https://github.com/revanced/revanced-patcher/compare/v2.9.0...v3.0.0) (2022-08-02) # [3.0.0](https://github.com/revanced/revanced-patcher/compare/v2.9.0...v3.0.0) (2022-08-02)

View File

@@ -26,6 +26,7 @@ dependencies {
implementation("app.revanced:multidexlib2:2.5.2.r2") implementation("app.revanced:multidexlib2:2.5.2.r2")
implementation("org.apktool:apktool-lib:2.7.0-SNAPSHOT") implementation("org.apktool:apktool-lib:2.7.0-SNAPSHOT")
implementation(kotlin("reflect"))
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
} }

View File

@@ -1,2 +1,2 @@
kotlin.code.style = official kotlin.code.style = official
version = 3.0.0 version = 4.1.3

View File

@@ -4,6 +4,7 @@ import app.revanced.patcher.data.Data
import app.revanced.patcher.data.PackageMetadata import app.revanced.patcher.data.PackageMetadata
import app.revanced.patcher.data.impl.findIndexed import app.revanced.patcher.data.impl.findIndexed
import app.revanced.patcher.extensions.PatchExtensions.dependencies import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.deprecated
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.nullOutputStream import app.revanced.patcher.extensions.nullOutputStream
import app.revanced.patcher.fingerprint.method.utils.MethodFingerprintUtils.resolve import app.revanced.patcher.fingerprint.method.utils.MethodFingerprintUtils.resolve
@@ -49,70 +50,74 @@ class Patcher(private val options: PatcherOptions) {
init { init {
val extInputFile = ExtFile(options.inputFile) val extInputFile = ExtFile(options.inputFile)
val outDir = File(options.resourceCacheDirectory) try {
if (outDir.exists()) { val outDir = File(options.resourceCacheDirectory)
logger.info("Deleting existing resource cache directory") if (outDir.exists()) {
outDir.deleteRecursively() logger.info("Deleting existing resource cache directory")
} outDir.deleteRecursively()
outDir.mkdirs() }
outDir.mkdirs()
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) }) val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
val resourceTable = androlib.getResTable(extInputFile, true) val resourceTable = androlib.getResTable(extInputFile, true)
val packageMetadata = PackageMetadata() val packageMetadata = PackageMetadata()
if (options.patchResources) { if (options.patchResources) {
logger.info("Decoding resources") logger.info("Decoding resources")
// decode resources to cache directory // decode resources to cache directory
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable) androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable) androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
// read additional metadata from the resource table // read additional metadata from the resource table
packageMetadata.metaInfo.usesFramework = UsesFramework().also { framework -> packageMetadata.metaInfo.usesFramework = UsesFramework().also { framework ->
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted() framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
}
packageMetadata.metaInfo.doNotCompress = buildList {
androlib.recordUncompressedFiles(extInputFile, this)
}
} else {
logger.info("Only decoding AndroidManifest.xml because resource patching is disabled")
// create decoder for the resource table
val decoder = ResAttrDecoder()
decoder.currentPackage = ResPackage(resourceTable, 0, null)
// create xml parser with the decoder
val axmlParser = AXmlResourceParser()
axmlParser.attrDecoder = decoder
// parse package information with the decoder and parser which will set required values in the resource table
// instead of decodeManifest another more low level solution can be created to make it faster/better
XmlPullStreamDecoder(
axmlParser, AndrolibResources().resXmlSerializer
).decodeManifest(
extInputFile.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
)
} }
packageMetadata.metaInfo.doNotCompress = buildList { packageMetadata.packageName = resourceTable.currentResPackage.name
androlib.recordUncompressedFiles(extInputFile, this) packageMetadata.packageVersion = resourceTable.versionInfo.versionName
} packageMetadata.metaInfo.versionInfo = resourceTable.versionInfo
packageMetadata.metaInfo.sdkInfo = resourceTable.sdkInfo
} else { logger.info("Reading dex files")
logger.info("Only decoding AndroidManifest.xml because resource patching is disabled")
// create decoder for the resource table // read dex files
val decoder = ResAttrDecoder() val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
decoder.currentPackage = ResPackage(resourceTable, 0, null) // get the opcodes
opcodes = dexFile.opcodes
// create xml parser with the decoder // finally create patcher data
val axmlParser = AXmlResourceParser() data = PatcherData(
axmlParser.attrDecoder = decoder dexFile.classes.toMutableList(), options.resourceCacheDirectory, packageMetadata
// parse package information with the decoder and parser which will set required values in the resource table
// instead of decodeManifest another more low level solution can be created to make it faster/better
XmlPullStreamDecoder(
axmlParser, AndrolibResources().resXmlSerializer
).decodeManifest(
extInputFile.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
) )
} finally {
extInputFile.close()
} }
packageMetadata.packageName = resourceTable.currentResPackage.name
packageMetadata.packageVersion = resourceTable.versionInfo.versionName
packageMetadata.metaInfo.versionInfo = resourceTable.versionInfo
packageMetadata.metaInfo.sdkInfo = resourceTable.sdkInfo
logger.info("Reading dex files")
// read dex files
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
// get the opcodes
opcodes = dexFile.opcodes
// finally create patcher data
data = PatcherData(
dexFile.classes.toMutableList(), options.resourceCacheDirectory, packageMetadata
)
} }
/** /**
@@ -165,43 +170,46 @@ class Patcher(private val options: PatcherOptions) {
if (options.patchResources) { if (options.patchResources) {
val cacheDirectory = ExtFile(options.resourceCacheDirectory) val cacheDirectory = ExtFile(options.resourceCacheDirectory)
try {
val androlibResources = AndrolibResources().also { resources ->
resources.buildOptions = BuildOptions().also { buildOptions ->
buildOptions.setBuildOptions(options)
buildOptions.isFramework = metaInfo.isFrameworkApk
buildOptions.resourcesAreCompressed = metaInfo.compressionType
buildOptions.doNotCompress = metaInfo.doNotCompress
}
val androlibResources = AndrolibResources().also { resources -> resources.setSdkInfo(metaInfo.sdkInfo)
resources.buildOptions = BuildOptions().also { buildOptions -> resources.setVersionInfo(metaInfo.versionInfo)
buildOptions.setBuildOptions(options) resources.setSharedLibrary(metaInfo.sharedLibrary)
buildOptions.isFramework = metaInfo.isFrameworkApk resources.setSparseResources(metaInfo.sparseResources)
buildOptions.resourcesAreCompressed = metaInfo.compressionType
buildOptions.doNotCompress = metaInfo.doNotCompress
} }
resources.setSdkInfo(metaInfo.sdkInfo) val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
}
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml") ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile) val aaptFile = cacheDirectory.resolve("aapt_temp_file")
val aaptFile = cacheDirectory.resolve("aapt_temp_file") // delete if it exists
Files.deleteIfExists(aaptFile.toPath())
// delete if it exists val resDirectory = cacheDirectory.resolve("res")
Files.deleteIfExists(aaptFile.toPath()) val includedFiles = metaInfo.usesFramework.ids.map { id ->
androlibResources.getFrameworkApk(
id, metaInfo.usesFramework.tag
)
}.toTypedArray()
val resDirectory = cacheDirectory.resolve("res") logger.info("Compiling resources")
val includedFiles = metaInfo.usesFramework.ids.map { id -> androlibResources.aaptPackage(
androlibResources.getFrameworkApk( aaptFile, manifestFile, resDirectory, null, null, includedFiles
id, metaInfo.usesFramework.tag
) )
}.toTypedArray()
logger.info("Compiling resources") resourceFile = aaptFile
androlibResources.aaptPackage( } finally {
aaptFile, manifestFile, resDirectory, null, null, includedFiles cacheDirectory.close()
) }
resourceFile = aaptFile
} }
logger.trace("Creating new dex file") logger.trace("Creating new dex file")
@@ -262,16 +270,16 @@ class Patcher(private val options: PatcherOptions) {
} }
// recursively apply all dependency patches // recursively apply all dependency patches
patch.dependencies.forEach { patch.dependencies?.forEach {
val dependency = it.patch.java val patchDependency = it.java
val result = applyPatch(patchDependency, appliedPatches)
val result = applyPatch(dependency, appliedPatches)
if (result.isSuccess()) return@forEach if (result.isSuccess()) return@forEach
val error = result.error()!! val error = result.error()!!
val errorMessage = error.cause ?: error.message val errorMessage = error.cause ?: error.message
return PatchResultError("'$patchName' depends on '${patchDependency.patchName}' but the following error was raised: $errorMessage")
return PatchResultError("'$patchName' depends on '${dependency.patchName}' but the following error was raised: $errorMessage")
} }
val patchInstance = patch.getDeclaredConstructor().newInstance() val patchInstance = patch.getDeclaredConstructor().newInstance()
@@ -282,6 +290,10 @@ class Patcher(private val options: PatcherOptions) {
return PatchResultError("'$patchName' is a resource patch, but resource patching is disabled") return PatchResultError("'$patchName' is a resource patch, but resource patching is disabled")
} }
patch.deprecated?.let { (reason, replacement) ->
logger.warn("'$patchName' is deprecated: '$reason'" + if (replacement != null) ". Use '$replacement' instead." else "")
}
// TODO: find a solution for this // TODO: find a solution for this
val data = if (isResourcePatch) { val data = if (isResourcePatch) {
data.resourceData data.resourceData

View File

@@ -24,5 +24,5 @@ annotation class Compatibility(
@MustBeDocumented @MustBeDocumented
annotation class Package( annotation class Package(
val name: String, val name: String,
val versions: Array<String> val versions: Array<String> = [],
) )

View File

@@ -0,0 +1,20 @@
package app.revanced.patcher.annotation
import app.revanced.patcher.data.Data
import app.revanced.patcher.patch.Patch
import kotlin.reflect.KClass
/**
* Declares a [Patch] deprecated for removal.
* @param reason The reason why the patch is deprecated.
* @param replacement The replacement for the deprecated patch, if any.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Repeatable
annotation class PatchDeprecated(
val reason: String,
val replacement: KClass<out Patch<Data>> = Patch::class
// Values cannot be nullable in annotations, so this will have to do.
)

View File

@@ -1,16 +1,15 @@
package app.revanced.patcher.extensions package app.revanced.patcher.extensions
import app.revanced.patcher.annotation.Compatibility import app.revanced.patcher.annotation.*
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Data
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.annotations.Dependencies import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.annotations.DependencyType
import app.revanced.patcher.patch.annotations.DependsOn
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KVisibility
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.companionObjectInstance
/** /**
* Recursively find a given annotation on a class. * Recursively find a given annotation on a class.
@@ -38,26 +37,33 @@ private fun <T : Annotation> Class<*>.findAnnotationRecursively(
return null return null
} }
private typealias PatchClass = Class<out Patch<Data>>
object PatchExtensions { object PatchExtensions {
val PatchClass.patchName: String get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName val Class<out Patch<Data>>.patchName: String
val PatchClass.version get() = recursiveAnnotation(Version::class)?.version get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
val PatchClass.include get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Patch::class)!!.include val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version
val PatchClass.description get() = recursiveAnnotation(Description::class)?.description val Class<out Patch<Data>>.include get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Patch::class)!!.include
val PatchClass.dependencies get() = buildList { val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description
recursiveAnnotation(DependsOn::class)?.let { add(PatchDependency(it.value, it.type)) } val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.DependsOn::class)?.dependencies
recursiveAnnotation(Dependencies::class)?.dependencies?.forEach { add(PatchDependency(it, DependencyType.HARD)) } val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages
}.toTypedArray() val Class<out Patch<Data>>.options: PatchOptions?
val PatchClass.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages get() = kotlin.companionObject?.let { cl ->
if (cl.visibility != KVisibility.PUBLIC) return null
kotlin.companionObjectInstance?.let {
(it as? OptionsContainer)?.options
}
}
val Class<out Patch<Data>>.deprecated: Pair<String, KClass<out Patch<Data>>?>?
get() = recursiveAnnotation(PatchDeprecated::class)?.let {
it.reason to it.replacement.let { cl ->
if (cl == Patch::class) null else cl
}
}
@JvmStatic @JvmStatic
fun PatchClass.dependsOn(patch: PatchClass): Boolean { fun Class<out Patch<Data>>.dependsOn(patch: Class<out Patch<Data>>): Boolean {
if (this.patchName == patch.patchName) throw IllegalArgumentException("thisval and patch may not be the same") if (this.patchName == patch.patchName) throw IllegalArgumentException("thisval and patch may not be the same")
return this.dependencies.any { it.patch.java.patchName == this@dependsOn.patchName } return this.dependencies?.any { it.java.patchName == this@dependsOn.patchName } == true
} }
class PatchDependency internal constructor(val patch: KClass<out Patch<Data>>, val type: DependencyType = DependencyType.HARD)
} }
object MethodFingerprintExtensions { object MethodFingerprintExtensions {

View File

@@ -22,10 +22,10 @@ import org.jf.dexlib2.iface.Method
* A `null` opcode is equals to an unknown opcode. * A `null` opcode is equals to an unknown opcode.
*/ */
abstract class MethodFingerprint( abstract class MethodFingerprint(
internal val returnType: String?, internal val returnType: String? = null,
internal val access: Int?, internal val access: Int? = null,
internal val parameters: Iterable<String>?, internal val parameters: Iterable<String>? = null,
internal val opcodes: Iterable<Opcode?>?, internal val opcodes: Iterable<Opcode?>? = null,
internal val strings: Iterable<String>? = null, internal val strings: Iterable<String>? = null,
internal val customFingerprint: ((methodDef: Method) -> Boolean)? = null internal val customFingerprint: ((methodDef: Method) -> Boolean)? = null
) : Fingerprint { ) : Fingerprint {

View File

@@ -17,9 +17,18 @@ abstract class Patch<out T : Data> {
* The main function of the [Patch] which the patcher will call. * The main function of the [Patch] which the patcher will call.
*/ */
abstract fun execute(data: @UnsafeVariance T): PatchResult abstract fun execute(data: @UnsafeVariance T): PatchResult
}
abstract class OptionsContainer {
/** /**
* A list of [PatchOption]s. * A list of [PatchOption]s.
* @see PatchOptions
*/ */
open val options = PatchOptions() @Suppress("MemberVisibilityCanBePrivate")
val options = PatchOptions()
protected fun option(opt: PatchOption<*>): PatchOption<*> {
options.register(opt)
return opt
}
} }

View File

@@ -1,21 +1,34 @@
@file:Suppress("CanBeParameter", "MemberVisibilityCanBePrivate", "UNCHECKED_CAST")
package app.revanced.patcher.patch package app.revanced.patcher.patch
@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") import java.io.File
import java.nio.file.Path
import kotlin.reflect.KProperty
class NoSuchOptionException(val option: String) : Exception("No such option: $option") class NoSuchOptionException(val option: String) : Exception("No such option: $option")
class IllegalValueException(val value: Any?) : Exception("Illegal value: $value")
class InvalidTypeException(val got: String, val expected: String) :
Exception("Invalid option value type: $got, expected $expected")
object RequirementNotMetException : Exception("null was passed into an option that requires a value")
/** /**
* A registry for an array of [PatchOption]s. * A registry for an array of [PatchOption]s.
* @param options An array of [PatchOption]s. * @param options An array of [PatchOption]s.
*/ */
@Suppress("MemberVisibilityCanBePrivate")
class PatchOptions(vararg val options: PatchOption<*>) : Iterable<PatchOption<*>> { class PatchOptions(vararg val options: PatchOption<*>) : Iterable<PatchOption<*>> {
private val register = buildMap { private val register = mutableMapOf<String, PatchOption<*>>()
for (option in options) {
if (containsKey(option.key)) { init {
throw IllegalStateException("Multiple options found with the same key") options.forEach { register(it) }
} }
put(option.key, option)
internal fun register(option: PatchOption<*>) {
if (register.containsKey(option.key)) {
throw IllegalStateException("Multiple options found with the same key")
} }
register[option.key] = option
} }
/** /**
@@ -31,13 +44,22 @@ class PatchOptions(vararg val options: PatchOption<*>) : Iterable<PatchOption<*>
* Please note that using the wrong value type results in a runtime error. * Please note that using the wrong value type results in a runtime error.
*/ */
inline operator fun <reified T> set(key: String, value: T) { inline operator fun <reified T> set(key: String, value: T) {
@Suppress("UNCHECKED_CAST") val opt = get(key) as? PatchOption<T> @Suppress("UNCHECKED_CAST") val opt = get(key) as PatchOption<T>
if (opt == null || opt.value !is T) throw IllegalArgumentException( if (opt.value !is T) throw InvalidTypeException(
"The type of the option value is not the same as the type value provided" T::class.java.canonicalName,
opt.value?.let { it::class.java.canonicalName } ?: "null"
) )
opt.value = value opt.value = value
} }
/**
* Sets the value of a [PatchOption] to `null`.
* @param key The key of the [PatchOption].
*/
fun nullify(key: String) {
get(key).value = null
}
override fun iterator() = options.iterator() override fun iterator() = options.iterator()
} }
@@ -55,9 +77,37 @@ sealed class PatchOption<T>(
default: T?, default: T?,
val title: String, val title: String,
val description: String, val description: String,
val required: Boolean val required: Boolean,
val validator: (T?) -> Boolean
) { ) {
var value: T? = default var value: T? = default
set(value) {
if (value == null && required) {
throw RequirementNotMetException
}
if (!validator(value)) {
throw IllegalValueException(value)
}
field = value
}
/**
* Gets the value of the option.
* Please note that using the wrong value type results in a runtime error.
*/
operator fun <T> getValue(thisRef: Any?, property: KProperty<*>) = value as T
/**
* Gets the value of the option.
* Please note that using the wrong value type results in a runtime error.
*/
inline operator fun <reified V> setValue(thisRef: Any?, property: KProperty<*>, new: V) {
if (value !is V) throw InvalidTypeException(
V::class.java.canonicalName,
value?.let { it::class.java.canonicalName } ?: "null"
)
value = new as T
}
/** /**
* A [PatchOption] representing a [String]. * A [PatchOption] representing a [String].
@@ -68,9 +118,10 @@ sealed class PatchOption<T>(
default: String?, default: String?,
title: String, title: String,
description: String, description: String,
required: Boolean = false required: Boolean = false,
validator: (String?) -> Boolean = { true }
) : PatchOption<String>( ) : PatchOption<String>(
key, default, title, description, required key, default, title, description, required, validator
) )
/** /**
@@ -82,9 +133,10 @@ sealed class PatchOption<T>(
default: Boolean?, default: Boolean?,
title: String, title: String,
description: String, description: String,
required: Boolean = false required: Boolean = false,
validator: (Boolean?) -> Boolean = { true }
) : PatchOption<Boolean>( ) : PatchOption<Boolean>(
key, default, title, description, required key, default, title, description, required, validator
) )
/** /**
@@ -98,13 +150,16 @@ sealed class PatchOption<T>(
val options: Iterable<E>, val options: Iterable<E>,
title: String, title: String,
description: String, description: String,
required: Boolean = false required: Boolean = false,
validator: (E?) -> Boolean = { true }
) : PatchOption<E>( ) : PatchOption<E>(
key, default, title, description, required key, default, title, description, required, {
(it?.let { it in options } ?: true) && validator(it)
}
) { ) {
init { init {
if (default !in options) { if (default != null && default !in options) {
throw IllegalStateException("Default option must be an allowed options") throw IllegalStateException("Default option must be an allowed option")
} }
} }
} }
@@ -119,9 +174,10 @@ sealed class PatchOption<T>(
options: Iterable<String>, options: Iterable<String>,
title: String, title: String,
description: String, description: String,
required: Boolean = false required: Boolean = false,
validator: (String?) -> Boolean = { true }
) : ListOption<String>( ) : ListOption<String>(
key, default, options, title, description, required key, default, options, title, description, required, validator
) )
/** /**
@@ -134,8 +190,41 @@ sealed class PatchOption<T>(
options: Iterable<Int>, options: Iterable<Int>,
title: String, title: String,
description: String, description: String,
required: Boolean = false required: Boolean = false,
validator: (Int?) -> Boolean = { true }
) : ListOption<Int>( ) : ListOption<Int>(
key, default, options, title, description, required key, default, options, title, description, required, validator
) )
}
/**
* A [PatchOption] representing a [Path].
* @see PatchOption
*/
open class PathOption(
key: String,
default: Path?,
title: String,
description: String,
required: Boolean = false,
validator: (Path?) -> Boolean = { true }
) : PatchOption<Path>(
key, default, title, description, required, validator
)
/**
* A [PathOption] of type [File].
* @see PathOption
*/
class FileOption(
key: String,
default: File?,
title: String,
description: String,
required: Boolean = false,
validator: (File?) -> Boolean = { true }
) : PathOption(
key, default?.toPath(), title, description, required, {
validator(it?.toFile())
}
)
}

View File

@@ -19,29 +19,6 @@ annotation class Patch(val include: Boolean = true)
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented @MustBeDocumented
@Deprecated(
"Does not support new parameter 'type'",
ReplaceWith("DependsOn")
)
annotation class Dependencies(
val dependencies: Array<KClass<out Patch<Data>>> = []
)
/**
* Annotation for dependencies of [Patch]es .
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Repeatable
annotation class DependsOn( annotation class DependsOn(
val value: KClass<out Patch<Data>>, val dependencies: Array<KClass<out Patch<Data>>> = []
val type: DependencyType = DependencyType.HARD )
)
enum class DependencyType {
/**
* Enforces that the dependency is applied, even if it was not selected.
*/
HARD
}

View File

@@ -5,6 +5,6 @@ import java.io.InputStream
/** /**
* Wrapper for dex files. * Wrapper for dex files.
* @param name The original name of the dex file. * @param name The original name of the dex file.
* @param dexFileInputStream The dex file as [InputStream]. * @param stream The dex file as [InputStream].
*/ */
data class DexFile(val name: String, val dexFileInputStream: InputStream) data class DexFile(val name: String, val stream: InputStream)

View File

@@ -1,17 +1,17 @@
package app.revanced.patcher.util.patch.base package app.revanced.patcher.util.patch
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Data
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import java.io.File import java.io.File
/** /**
* @param patchBundlePath The path to the patch bundle. * @param path The path to the patch bundle.
*/ */
abstract class PatchBundle(patchBundlePath: String) : File(patchBundlePath) { abstract class PatchBundle(path: String) : File(path) {
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList { internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
classNames.forEach { className -> for (className in classNames) {
val clazz = classLoader.loadClass(className) val clazz = classLoader.loadClass(className)
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) return@forEach if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) continue
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Data>>) @Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Data>>)
} }
} }

View File

@@ -1,4 +1,4 @@
package app.revanced.patcher.util.patch.util package app.revanced.patcher.util.patch
internal class StringIterator<T, I : Iterator<T>>( internal class StringIterator<T, I : Iterator<T>>(
private val iterator: I, private val iterator: I,

View File

@@ -1,7 +1,7 @@
package app.revanced.patcher.util.patch.implementation package app.revanced.patcher.util.patch.impl
import app.revanced.patcher.util.patch.base.PatchBundle import app.revanced.patcher.util.patch.PatchBundle
import app.revanced.patcher.util.patch.util.StringIterator import app.revanced.patcher.util.patch.StringIterator
import org.jf.dexlib2.DexFileFactory import org.jf.dexlib2.DexFileFactory
/** /**

View File

@@ -1,7 +1,7 @@
package app.revanced.patcher.util.patch.implementation package app.revanced.patcher.util.patch.impl
import app.revanced.patcher.util.patch.base.PatchBundle import app.revanced.patcher.util.patch.PatchBundle
import app.revanced.patcher.util.patch.util.StringIterator import app.revanced.patcher.util.patch.StringIterator
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.jar.JarFile import java.util.jar.JarFile

View File

@@ -0,0 +1,93 @@
package app.revanced.patcher.patch
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.io.File
import kotlin.test.assertNotEquals
internal class PatchOptionsTest {
private val options = ExampleBytecodePatch.options
@Test
fun `should not throw an exception`() {
for (option in options) {
when (option) {
is PatchOption.StringOption -> {
option.value = "Hello World"
}
is PatchOption.BooleanOption -> {
option.value = false
}
is PatchOption.StringListOption -> {
option.value = option.options.first()
for (choice in option.options) {
println(choice)
}
}
is PatchOption.IntListOption -> {
option.value = option.options.first()
for (choice in option.options) {
println(choice)
}
}
is PatchOption.PathOption -> {
option.value = File("test.txt").toPath()
}
}
}
val option = options["key1"]
println(option.value)
options["key1"] = "Hello, world!"
println(option.value)
}
@Test
fun `should return a different value when changed`() {
var value: String by options["key1"]
val current = value + "" // force a copy
value = "Hello, world!"
assertNotEquals(current, value)
}
@Test
fun `should be able to set value to null`() {
// Sadly, doing:
// > options["key2"] = null
// is not possible because Kotlin
// cannot reify the type "Nothing?".
options.nullify("key2")
}
@Test
fun `should fail because the option does not exist`() {
assertThrows<NoSuchOptionException> {
options["this option does not exist"] = 123
}
}
@Test
fun `should fail because of invalid value type`() {
assertThrows<InvalidTypeException> {
options["key1"] = 123
}
}
@Test
fun `should fail because of an illegal value`() {
assertThrows<IllegalValueException> {
options["key3"] = "this value is not an allowed option"
}
}
@Test
fun `should fail because of the requirement is not met`() {
assertThrows<RequirementNotMetException> {
options.nullify("key1")
}
}
}

View File

@@ -1,37 +0,0 @@
package app.revanced.patcher.usage
import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
import kotlin.test.Test
internal class PatchOptionsUsage {
@Test
fun patchOptionsUsage() {
val options = ExampleBytecodePatch().options
for (option in options) {
when (option) {
is PatchOption.StringOption -> {
option.value = "Hello World"
}
is PatchOption.BooleanOption -> {
option.value = false
}
is PatchOption.StringListOption -> {
option.value = option.options.first()
for (choice in option.options) {
println(choice)
}
}
is PatchOption.IntListOption -> {
option.value = option.options.first()
for (choice in option.options) {
println(choice)
}
}
}
}
println(options["key1"].value)
options["key1"] = "Hello, world!"
println(options["key1"].value)
}
}

View File

@@ -7,11 +7,10 @@ import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.PatchOption import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.PatchResult import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.DependencyType
import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.patch.impl.BytecodePatch import app.revanced.patcher.patch.impl.BytecodePatch
@@ -33,13 +32,15 @@ import org.jf.dexlib2.immutable.reference.ImmutableFieldReference
import org.jf.dexlib2.immutable.reference.ImmutableStringReference import org.jf.dexlib2.immutable.reference.ImmutableStringReference
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
import org.jf.dexlib2.util.Preconditions import org.jf.dexlib2.util.Preconditions
import java.io.File
import java.nio.file.Path
@Patch @Patch
@Name("example-bytecode-patch") @Name("example-bytecode-patch")
@Description("Example demonstration of a bytecode patch.") @Description("Example demonstration of a bytecode patch.")
@ExampleResourceCompatibility @ExampleResourceCompatibility
@Version("0.0.1") @Version("0.0.1")
@DependsOn(ExampleBytecodePatch::class, DependencyType.HARD) @DependsOn([ExampleBytecodePatch::class])
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// This function will be executed by the patcher. // This function will be executed by the patcher.
// You can treat it as a constructor // You can treat it as a constructor
@@ -47,6 +48,10 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// Get the resolved method by its fingerprint from the resolver cache // Get the resolved method by its fingerprint from the resolver cache
val result = ExampleFingerprint.result!! val result = ExampleFingerprint.result!!
// Patch options
println(key1)
key2 = false
// Get the implementation for the resolved method // Get the implementation for the resolved method
val method = result.mutableMethod val method = result.mutableMethod
val implementation = method.implementation!! val implementation = method.implementation!!
@@ -165,18 +170,31 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
) )
} }
override val options = PatchOptions( companion object : OptionsContainer() {
PatchOption.StringOption( private var key1: String by option(
"key1", "default", "title", "description", true PatchOption.StringOption(
), "key1", "default", "title", "description", true
PatchOption.BooleanOption( )
"key2", true, "title", "description" // required defaults to false )
), private var key2: Boolean by option(
PatchOption.StringListOption( PatchOption.BooleanOption(
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description" "key2", true, "title", "description" // required defaults to false
), )
PatchOption.IntListOption( )
"key4", 1, listOf(1, 2, 3), "title", "description" private var key3: String by option(
), PatchOption.StringListOption(
) "key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description"
)
)
private var key4: Int by option(
PatchOption.IntListOption(
"key4", 1, listOf(1, 2, 3), "title", "description"
)
)
private var key5: Path by option(
PatchOption.PathOption(
"key5", File("test.txt").toPath(), "title", "description"
)
)
}
} }