Compare commits

..

26 Commits

Author SHA1 Message Date
semantic-release-bot
4bbe2fa150 chore(release): 1.5.0 [skip ci]
# [1.5.0](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0) (2023-12-28)

### Features

* Add JSON de- and serialization of patches ([ecff6fe](ecff6fe0d3))
* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](9fda407441))
2023-12-28 21:35:25 +00:00
oSumAtrIX
938eac53b1 chore: Merge branch dev to main (#20) 2023-12-28 22:34:05 +01:00
Pun Butrach
ab004b91f0 docs: Update to latest branding changes (#23) 2023-12-10 13:31:20 +01:00
semantic-release-bot
b637e0d7d7 chore(release): 1.5.0-dev.2 [skip ci]
# [1.5.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.5.0-dev.1...v1.5.0-dev.2) (2023-12-07)

### Features

* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](9fda407441))
2023-12-07 15:41:07 +00:00
oSumAtrIX
9fda407441 feat: Improve mount reliability by unmounting existing mounts and killing running apps 2023-12-07 16:39:51 +01:00
semantic-release-bot
44f7c13a77 chore(release): 1.5.0-dev.1 [skip ci]
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0-dev.1) (2023-12-07)

### Features

* Add JSON de- and serialization of patches ([ecff6fe](ecff6fe0d3))
2023-12-07 14:16:31 +00:00
oSumAtrIX
ecff6fe0d3 feat: Add JSON de- and serialization of patches 2023-12-07 15:14:59 +01:00
oSumAtrIX
3515e331ac refactor: Simplify code 2023-12-07 02:45:49 +01:00
oSumAtrIX
a921c40306 build: Bump dependencies 2023-12-07 02:44:36 +01:00
oSumAtrIX
b6e1de5eaf chore: Use correct variable keyword 2023-12-07 02:38:52 +01:00
oSumAtrIX
bf5780e2f7 chore: Improve wording 2023-12-07 02:29:46 +01:00
oSumAtrIX
99e90c02ed refactor: Remove unused typealias 2023-12-01 23:06:29 +01:00
semantic-release-bot
ac3917fecc chore(release): 1.4.0 [skip ci]
# [1.4.0](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0) (2023-11-27)

### Bug Fixes

* Differentiate no package compatibility to any version compatibility ([762b7e3](762b7e3bc0))
* Sort the version maps by the most common version ([e4be6db](e4be6dbccd))

### Features

* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](c5f3536cbb))
* Allow getting most common compatible versions for all packages ([96845ba](96845ba265))
2023-11-27 21:50:58 +00:00
oSumAtrIX
98b7b347cb chore: Merge branch dev to main (#19) 2023-11-27 22:49:52 +01:00
semantic-release-bot
cdf94ffeca chore(release): 1.4.0-dev.2 [skip ci]
# [1.4.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2023-11-27)

### Bug Fixes

* Differentiate no package compatibility to any version compatibility ([762b7e3](762b7e3bc0))
* Sort the version maps by the most common version ([e4be6db](e4be6dbccd))

### Features

* Allow getting most common compatible versions for all packages ([96845ba](96845ba265))
2023-11-27 21:43:22 +00:00
oSumAtrIX
37434cf4a4 build: Bump dependencies 2023-11-27 22:40:07 +01:00
oSumAtrIX
73c97abedd chore: Lint code 2023-11-27 22:40:00 +01:00
oSumAtrIX
762b7e3bc0 fix: Differentiate no package compatibility to any version compatibility 2023-11-27 22:30:59 +01:00
oSumAtrIX
e4be6dbccd fix: Sort the version maps by the most common version 2023-11-27 21:21:25 +01:00
oSumAtrIX
96845ba265 feat: Allow getting most common compatible versions for all packages 2023-11-27 21:03:23 +01:00
semantic-release-bot
34171b534b chore(release): 1.4.0-dev.1 [skip ci]
# [1.4.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0-dev.1) (2023-11-27)

### Features

* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](c5f3536cbb))
2023-11-27 01:26:17 +00:00
oSumAtrIX
c5f3536cbb feat: Add PatchUtils#getMostCommonCompatibleVersions utility function 2023-11-27 02:24:55 +01:00
oSumAtrIX
893274074b refactor: Do not escape unnecessary 2023-11-27 02:24:55 +01:00
semantic-release-bot
b6c09d42ae chore(release): 1.3.0 [skip ci]
# [1.3.0](https://github.com/ReVanced/revanced-library/compare/v1.2.0...v1.3.0) (2023-11-26)

### Bug Fixes

* Add missing log when calling `UserAdbManager#install` ([90b612b](90b612bee8))
* Delete mount script ([4fe0fb0](4fe0fb0a61))

### Features

* Increase certainty of the possibility to mount ([10f8cd1](10f8cd1470))
* Select first Adb device, if none supplied automatically ([1a5f868](1a5f868ecd))
2023-11-26 04:35:19 +00:00
oSumAtrIX
fe8a2334e6 chore: Merge branch dev to main (#18) 2023-11-26 05:33:52 +01:00
oSumAtrIX
a9e5966145 chore: Lint code 2023-11-26 05:27:29 +01:00
20 changed files with 780 additions and 249 deletions

View File

@@ -1,3 +1,73 @@
# [1.5.0](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0) (2023-12-28)
### Features
* Add JSON de- and serialization of patches ([ecff6fe](https://github.com/ReVanced/revanced-library/commit/ecff6fe0d3889d729a0badcfa28b89610bd27d48))
* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](https://github.com/ReVanced/revanced-library/commit/9fda40744173669c84b0c2599ae5ac5d39591798))
# [1.5.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.5.0-dev.1...v1.5.0-dev.2) (2023-12-07)
### Features
* Improve mount reliability by unmounting existing mounts and killing running apps ([9fda407](https://github.com/ReVanced/revanced-library/commit/9fda40744173669c84b0c2599ae5ac5d39591798))
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.4.0...v1.5.0-dev.1) (2023-12-07)
### Features
* Add JSON de- and serialization of patches ([ecff6fe](https://github.com/ReVanced/revanced-library/commit/ecff6fe0d3889d729a0badcfa28b89610bd27d48))
# [1.4.0](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0) (2023-11-27)
### Bug Fixes
* Differentiate no package compatibility to any version compatibility ([762b7e3](https://github.com/ReVanced/revanced-library/commit/762b7e3bc01e2ca33dfcdbb1b5028d60ef6e0a48))
* Sort the version maps by the most common version ([e4be6db](https://github.com/ReVanced/revanced-library/commit/e4be6dbccd86700ffafe7cd8395e845bbd3d5138))
### Features
* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](https://github.com/ReVanced/revanced-library/commit/c5f3536cbb6997766076595dc0b2b5d2e861ca73))
* Allow getting most common compatible versions for all packages ([96845ba](https://github.com/ReVanced/revanced-library/commit/96845ba265e6dc208c7ac96f5e58734209cd1720))
# [1.4.0-dev.2](https://github.com/ReVanced/revanced-library/compare/v1.4.0-dev.1...v1.4.0-dev.2) (2023-11-27)
### Bug Fixes
* Differentiate no package compatibility to any version compatibility ([762b7e3](https://github.com/ReVanced/revanced-library/commit/762b7e3bc01e2ca33dfcdbb1b5028d60ef6e0a48))
* Sort the version maps by the most common version ([e4be6db](https://github.com/ReVanced/revanced-library/commit/e4be6dbccd86700ffafe7cd8395e845bbd3d5138))
### Features
* Allow getting most common compatible versions for all packages ([96845ba](https://github.com/ReVanced/revanced-library/commit/96845ba265e6dc208c7ac96f5e58734209cd1720))
# [1.4.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.3.0...v1.4.0-dev.1) (2023-11-27)
### Features
* Add `PatchUtils#getMostCommonCompatibleVersions` utility function ([c5f3536](https://github.com/ReVanced/revanced-library/commit/c5f3536cbb6997766076595dc0b2b5d2e861ca73))
# [1.3.0](https://github.com/ReVanced/revanced-library/compare/v1.2.0...v1.3.0) (2023-11-26)
### Bug Fixes
* Add missing log when calling `UserAdbManager#install` ([90b612b](https://github.com/ReVanced/revanced-library/commit/90b612bee8591c01b8befabde4147c7de7a2a09f))
* Delete mount script ([4fe0fb0](https://github.com/ReVanced/revanced-library/commit/4fe0fb0a617082b24199331671193e4fa7f485e2))
### Features
* Increase certainty of the possibility to mount ([10f8cd1](https://github.com/ReVanced/revanced-library/commit/10f8cd1470fd29cfefe53bf00a4a014f71a3f706))
* Select first Adb device, if none supplied automatically ([1a5f868](https://github.com/ReVanced/revanced-library/commit/1a5f868ecd0d278d574c12664ee95139c2423c17))
# [1.3.0-dev.1](https://github.com/ReVanced/revanced-library/compare/v1.2.1-dev.1...v1.3.0-dev.1) (2023-11-26)

View File

@@ -6,34 +6,53 @@
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/revanced">
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://twitter.com/revancedapp">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032018-6da37214-7474-4641-a1da-7af7db3a31cd.png" />
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</a>&nbsp;&nbsp;&nbsp;
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
@@ -54,7 +73,7 @@ This document describes how to contribute to ReVanced Library.
Features can be requested by opening an issue using the
[Feature request issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+).
> **Note**
> [!NOTE]
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Library.
> Good motivation has to be provided for a request to be accepted.
@@ -76,4 +95,4 @@ If you encounter a bug while using ReVanced Library, open an issue using the
it will be merged into the `dev` branch and will be included in the next release of ReVanced Library
❤️ Thank you for considering contributing to ReVanced Library,
ReVanced
ReVanced

View File

@@ -63,6 +63,49 @@ public final class app/revanced/library/Options$Patch$Option {
public final class app/revanced/library/PatchUtils {
public static final field INSTANCE Lapp/revanced/library/PatchUtils;
public final fun getMostCommonCompatibleVersion (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String;
public final fun getMostCommonCompatibleVersions (Ljava/util/Set;Ljava/util/Set;Z)Ljava/util/Map;
public static synthetic fun getMostCommonCompatibleVersions$default (Lapp/revanced/library/PatchUtils;Ljava/util/Set;Ljava/util/Set;ZILjava/lang/Object;)Ljava/util/Map;
}
public final class app/revanced/library/PatchUtils$Json {
public static final field INSTANCE Lapp/revanced/library/PatchUtils$Json;
public final fun deserialize (Ljava/io/InputStream;Ljava/lang/Class;)Ljava/util/Set;
public final fun serialize (Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;)V
public static synthetic fun serialize$default (Lapp/revanced/library/PatchUtils$Json;Ljava/util/Set;Lkotlin/jvm/functions/Function1;ZLjava/io/OutputStream;ILjava/lang/Object;)V
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch : app/revanced/library/PatchUtils$Json$JsonPatch {
public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$Companion;
public final fun getCompatiblePackages ()Ljava/util/Set;
public final fun getDependencies ()Ljava/util/Set;
public final fun getDescription ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getOptions ()Ljava/util/Map;
public final fun getRequiresIntegrations ()Z
public final fun getUse ()Z
public final fun setRequiresIntegrations (Z)V
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$Companion {
public final fun fromPatch (Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch;
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption {
public static final field Companion Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion;
public final fun getDefault ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String;
public final fun getKey ()Ljava/lang/String;
public final fun getRequired ()Z
public final fun getTitle ()Ljava/lang/String;
public final fun getValueType ()Ljava/lang/String;
public final fun getValues ()Ljava/util/Map;
}
public final class app/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption$Companion {
public final fun fromPatchOption (Lapp/revanced/patcher/patch/options/PatchOption;)Lapp/revanced/library/PatchUtils$Json$FullJsonPatch$FullJsonPatchOption;
}
public abstract interface class app/revanced/library/PatchUtils$Json$JsonPatch {
}
public abstract class app/revanced/library/adb/AdbManager {
@@ -87,6 +130,7 @@ public final class app/revanced/library/adb/AdbManager$Companion {
}
public final class app/revanced/library/adb/AdbManager$DeviceNotFoundException : java/lang/Exception {
public fun <init> ()V
}
public final class app/revanced/library/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception {

View File

@@ -75,4 +75,4 @@ publishing {
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
kotlin.code.style = official
version = 1.3.0-dev.1
version = 1.5.0

View File

@@ -3,9 +3,9 @@ apksig = "8.1.4"
bcpkix-jdk18on = "1.76"
jackson-module-kotlin = "2.14.3"
jadb = "1.2.1"
kotlin-reflect = "1.9.10"
kotlin-test = "1.9.10"
revanced-patcher = "19.0.0"
kotlin-reflect = "1.9.20"
kotlin-test = "1.9.20"
revanced-patcher = "19.1.0"
binary-compatibility-validator = "0.13.2"
[libraries]

View File

@@ -26,8 +26,9 @@ object ApkSigner {
private val logger = Logger.getLogger(app.revanced.library.ApkSigner::class.java.name)
init {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(BouncyCastleProvider())
}
}
/**
@@ -39,14 +40,15 @@ object ApkSigner {
*/
fun newPrivateKeyCertificatePair(
commonName: String = "ReVanced",
validUntil: Date = Date(System.currentTimeMillis() + 356.days.inWholeMilliseconds * 24)
validUntil: Date = Date(System.currentTimeMillis() + 356.days.inWholeMilliseconds * 24),
): PrivateKeyCertificatePair {
logger.fine("Creating certificate for $commonName")
// Generate a new key pair.
val keyPair = KeyPairGenerator.getInstance("RSA").apply {
initialize(4096)
}.generateKeyPair()
val keyPair =
KeyPairGenerator.getInstance("RSA").apply {
initialize(4096)
}.generateKeyPair()
var serialNumber: BigInteger
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong())
@@ -55,22 +57,22 @@ object ApkSigner {
val name = X500Name("CN=$commonName")
// Create a new certificate.
val certificate = JcaX509CertificateConverter().getCertificate(
X509v3CertificateBuilder(
name,
serialNumber,
Date(System.currentTimeMillis()),
validUntil,
Locale.ENGLISH,
name,
SubjectPublicKeyInfo.getInstance(keyPair.public.encoded)
).build(JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private))
)
val certificate =
JcaX509CertificateConverter().getCertificate(
X509v3CertificateBuilder(
name,
serialNumber,
Date(System.currentTimeMillis()),
validUntil,
Locale.ENGLISH,
name,
SubjectPublicKeyInfo.getInstance(keyPair.public.encoded),
).build(JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private)),
)
return PrivateKeyCertificatePair(keyPair.private, certificate)
}
/**
* Read a [PrivateKeyCertificatePair] from a keystore entry.
*
@@ -87,16 +89,18 @@ object ApkSigner {
): PrivateKeyCertificatePair {
logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias")
if (!keyStore.containsAlias(keyStoreEntryAlias))
if (!keyStore.containsAlias(keyStoreEntryAlias)) {
throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias")
}
// Read the private key and certificate from the keystore.
val privateKey = try {
keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey
} catch (exception: UnrecoverableKeyException) {
throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias")
}
val privateKey =
try {
keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey
} catch (exception: UnrecoverableKeyException) {
throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias")
}
val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate
@@ -110,9 +114,7 @@ object ApkSigner {
* @return The created keystore.
* @see KeyStoreEntry
*/
fun newKeyStore(
entries: List<KeyStoreEntry>
): KeyStore {
fun newKeyStore(entries: List<KeyStoreEntry>): KeyStore {
logger.fine("Creating keystore")
return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply {
@@ -124,7 +126,7 @@ object ApkSigner {
entry.alias,
entry.privateKeyCertificatePair.privateKey,
entry.password.toCharArray(),
arrayOf(entry.privateKeyCertificatePair.certificate)
arrayOf(entry.privateKeyCertificatePair.certificate),
)
}
}
@@ -140,10 +142,10 @@ object ApkSigner {
fun newKeyStore(
keyStoreOutputStream: OutputStream,
keyStorePassword: String,
entries: List<KeyStoreEntry>
entries: List<KeyStoreEntry>,
) = newKeyStore(entries).store(
keyStoreOutputStream,
keyStorePassword.toCharArray()
keyStorePassword.toCharArray(),
) // Save the keystore.
/**
@@ -156,7 +158,7 @@ object ApkSigner {
*/
fun readKeyStore(
keyStoreInputStream: InputStream,
keyStorePassword: String?
keyStorePassword: String?,
): KeyStore {
logger.fine("Reading keystore")
@@ -164,10 +166,11 @@ object ApkSigner {
try {
load(keyStoreInputStream, keyStorePassword?.toCharArray())
} catch (exception: IOException) {
if (exception.cause is UnrecoverableKeyException)
if (exception.cause is UnrecoverableKeyException) {
throw IllegalArgumentException("Invalid keystore password")
else
} else {
throw exception
}
}
}
}
@@ -183,20 +186,21 @@ object ApkSigner {
fun newApkSignerBuilder(
privateKeyCertificatePair: PrivateKeyCertificatePair,
signer: String,
createdBy: String
createdBy: String,
): ApkSigner.Builder {
logger.fine(
"Creating new ApkSigner " +
"with $signer as signer and " +
"$createdBy as Created-By attribute in the APK's manifest"
"with $signer as signer and " +
"$createdBy as Created-By attribute in the APK's manifest",
)
// Create the signer config.
val signerConfig = ApkSigner.SignerConfig.Builder(
signer,
privateKeyCertificatePair.privateKey,
listOf(privateKeyCertificatePair.certificate)
).build()
val signerConfig =
ApkSigner.SignerConfig.Builder(
signer,
privateKeyCertificatePair.privateKey,
listOf(privateKeyCertificatePair.certificate),
).build()
// Create the signer.
return ApkSigner.Builder(listOf(signerConfig)).apply {
@@ -227,10 +231,13 @@ object ApkSigner {
) = newApkSignerBuilder(
readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword),
signer,
createdBy
createdBy,
)
fun ApkSigner.Builder.signApk(input: File, output: File) {
fun ApkSigner.Builder.signApk(
input: File,
output: File,
) {
logger.info("Signing ${input.name}")
setInputApk(input)
@@ -250,7 +257,7 @@ object ApkSigner {
class KeyStoreEntry(
val alias: String,
val password: String,
val privateKeyCertificatePair: PrivateKeyCertificatePair = newPrivateKeyCertificatePair()
val privateKeyCertificatePair: PrivateKeyCertificatePair = newPrivateKeyCertificatePair(),
)
/**
@@ -263,4 +270,4 @@ object ApkSigner {
val privateKey: PrivateKey,
val certificate: X509Certificate,
)
}
}

View File

@@ -22,7 +22,11 @@ object ApkUtils {
* @param outputFile The apk to write the new entries to.
* @param patchedEntriesSource The result of the patcher to add the patched dex files and resources.
*/
fun copyAligned(apkFile: File, outputFile: File, patchedEntriesSource: PatcherResult) {
fun copyAligned(
apkFile: File,
outputFile: File,
patchedEntriesSource: PatcherResult,
) {
logger.info("Aligning ${apkFile.name}")
outputFile.toPath().deleteIfExists()
@@ -30,13 +34,15 @@ object ApkUtils {
ZipFile(outputFile).use { file ->
patchedEntriesSource.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry(it.name), it.stream.readBytes()
ZipEntry(it.name),
it.stream.readBytes(),
)
}
patchedEntriesSource.resourceFile?.let {
file.copyEntriesFromFileAligned(
ZipFile(it), ZipFile.apkZipEntryAlignment
ZipFile(it),
ZipFile.apkZipEntryAlignment,
)
}
@@ -44,7 +50,8 @@ object ApkUtils {
// TODO: Fix copying resources that are not needed anymore.
file.copyEntriesFromFileAligned(
ZipFile(apkFile), ZipFile.apkZipEntryAlignment
ZipFile(apkFile),
ZipFile.apkZipEntryAlignment,
)
}
}
@@ -62,26 +69,27 @@ object ApkUtils {
signingOptions: SigningOptions,
) {
// Get the keystore from the file or create a new one.
val keyStore = if (signingOptions.keyStore.exists()) {
ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword)
} else {
val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password)
val keyStore =
if (signingOptions.keyStore.exists()) {
ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword)
} else {
val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password)
// Create a new keystore with a new keypair and saves it.
ApkSigner.newKeyStore(listOf(entry)).also { keyStore ->
keyStore.store(
signingOptions.keyStore.outputStream(),
signingOptions.keyStorePassword?.toCharArray()
)
// Create a new keystore with a new keypair and saves it.
ApkSigner.newKeyStore(listOf(entry)).also { keyStore ->
keyStore.store(
signingOptions.keyStore.outputStream(),
signingOptions.keyStorePassword?.toCharArray(),
)
}
}
}
ApkSigner.newApkSignerBuilder(
keyStore,
signingOptions.alias,
signingOptions.password,
signingOptions.signer,
signingOptions.signer
signingOptions.signer,
).signApk(apk, output)
}
@@ -101,4 +109,4 @@ object ApkUtils {
val password: String = "",
val signer: String = "ReVanced",
)
}
}

View File

@@ -2,67 +2,69 @@
package app.revanced.library
import app.revanced.library.Options.Patch.Option
import app.revanced.patcher.PatchClass
import app.revanced.patcher.PatchSet
import app.revanced.patcher.patch.options.PatchOptionException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File
import java.util.logging.Logger
private typealias PatchList = List<PatchClass>
@Suppress("unused")
object Options {
private val logger = Logger.getLogger(Options::class.java.name)
private var mapper = jacksonObjectMapper()
private val mapper = jacksonObjectMapper()
/**
* Serializes the options for the patches in the list.
* Serializes the options for a set of patches.
*
* @param patches The list of patches to serialize.
* @param patches The set of patches to serialize.
* @param prettyPrint Whether to pretty print the JSON.
* @return The JSON string containing the options.
*/
fun serialize(patches: PatchSet, prettyPrint: Boolean = false): String = patches
.filter { it.options.any() }
.map { patch ->
Patch(
patch.name!!,
patch.options.values.map { option ->
val optionValue = try {
option.value
} catch (e: PatchOptionException) {
logger.warning("Using default option value for the ${patch.name} patch: ${e.message}")
option.default
}
fun serialize(
patches: PatchSet,
prettyPrint: Boolean = false,
): String =
patches
.filter { it.options.any() }
.map { patch ->
Patch(
patch.name!!,
patch.options.values.map { option ->
val optionValue =
try {
option.value
} catch (e: PatchOptionException) {
logger.warning("Using default option value for the ${patch.name} patch: ${e.message}")
option.default
}
Option(option.key, optionValue)
Option(option.key, optionValue)
},
)
}
// See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7.
.distinctBy { it.patchName }
.let {
if (prettyPrint) {
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it)
} else {
mapper.writeValueAsString(it)
}
)
}
// See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7.
.distinctBy { it.patchName }
.let {
if (prettyPrint)
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it)
else
mapper.writeValueAsString(it)
}
}
/**
* Deserializes the options for the patches in the list.
* Deserializes the options to a set of patches.
*
* @param json The JSON string containing the options.
* @return The list of [Patch]s.
* @return A set of [Patch]s.
* @see Patch
* @see PatchList
*/
fun deserialize(json: String): Array<Patch> = mapper.readValue(json, Array<Patch>::class.java)
/**
* Sets the options for the patches in the list.
* Sets the options for a set of patches.
*
* @param json The JSON string containing the options.
*/
@@ -70,9 +72,10 @@ object Options {
filter { it.options.any() }.let { patches ->
if (patches.isEmpty()) return
val jsonPatches = deserialize(json).associate {
it.patchName to it.options.associate { option -> option.key to option.value }
}
val jsonPatches =
deserialize(json).associate {
it.patchName to it.options.associate { option -> option.key to option.value }
}
patches.forEach { patch ->
jsonPatches[patch.name]?.let { jsonPatchOptions ->
@@ -89,7 +92,7 @@ object Options {
}
/**
* Sets the options for the patches in the list.
* Sets the options for a set of patches.
*
* @param file The file containing the JSON string containing the options.
* @see setOptions
@@ -104,9 +107,8 @@ object Options {
*/
class Patch internal constructor(
val patchName: String,
val options: List<Option>
val options: List<Option>,
) {
/**
* Data class for patch option.
*
@@ -115,4 +117,4 @@ object Options {
*/
class Option internal constructor(val key: String, val value: Any?)
}
}
}

View File

@@ -1,6 +1,19 @@
package app.revanced.library
import app.revanced.patcher.PatchClass
import app.revanced.patcher.PatchSet
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.options.PatchOption
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.InputStream
import java.io.OutputStream
typealias PackageName = String
typealias Version = String
typealias Count = Int
typealias VersionMap = LinkedHashMap<Version, Count>
typealias PackageNameMap = Map<PackageName, VersionMap>
/**
* Utility functions for working with patches.
@@ -14,7 +27,17 @@ object PatchUtils {
* @param packageName The name of the compatible package.
* @return The most common version of.
*/
fun getMostCommonCompatibleVersion(patches: PatchSet, packageName: String) = patches
@Deprecated(
"Use getMostCommonCompatibleVersions instead.",
ReplaceWith(
"getMostCommonCompatibleVersions(patches, setOf(packageName))" +
".entries.firstOrNull()?.value?.keys?.firstOrNull()",
),
)
fun getMostCommonCompatibleVersion(
patches: PatchSet,
packageName: String,
) = patches
.mapNotNull {
// Map all patches to their compatible packages with version constraints.
it.compatiblePackages?.firstOrNull { compatiblePackage ->
@@ -25,4 +48,151 @@ object PatchUtils {
.groupingBy { it }
.eachCount()
.maxByOrNull { it.value }?.key
}
/**
* Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version.
*
* @param patches The set of patches to check.
* @param packageNames The names of the compatible packages to include. If null, all packages will be included.
* @param countUnusedPatches Whether to count patches that are not used.
* @return A map of package names to a map of versions to their count.
*/
fun getMostCommonCompatibleVersions(
patches: PatchSet,
packageNames: Set<String>? = null,
countUnusedPatches: Boolean = false,
): PackageNameMap =
buildMap {
fun filterWantedPackages(compatiblePackages: Iterable<Patch.CompatiblePackage>): Iterable<Patch.CompatiblePackage> {
val wantedPackages = packageNames?.toHashSet() ?: return compatiblePackages
return compatiblePackages.filter { it.name in wantedPackages }
}
patches
.filter { it.use || countUnusedPatches }
.flatMap { it.compatiblePackages ?: emptyList() }
.let(::filterWantedPackages)
.forEach { compatiblePackage ->
if (compatiblePackage.versions?.isEmpty() == true) {
return@forEach
}
val versionMap = getOrPut(compatiblePackage.name) { linkedMapOf() }
compatiblePackage.versions?.let { versions ->
versions.forEach { version ->
versionMap[version] = versionMap.getOrDefault(version, 0) + 1
}
}
}
// Sort the version maps by the most common version.
forEach { (packageName, versionMap) ->
this[packageName] =
versionMap
.asIterable()
.sortedWith(compareByDescending { it.value })
.associate { it.key to it.value } as VersionMap
}
}
object Json {
private val mapper = jacksonObjectMapper()
/**
* Serializes a set of [Patch]es to a JSON string and writes it to an output stream.
*
* @param patches The set of [Patch]es to serialize.
* @param transform A function to transform the [Patch]es to [JsonPatch]es.
* @param prettyPrint Whether to pretty print the JSON.
* @param outputStream The output stream to write the JSON to.
*/
fun serialize(
patches: PatchSet,
transform: (Patch<*>) -> JsonPatch = { patch -> FullJsonPatch.fromPatch(patch) },
prettyPrint: Boolean = false,
outputStream: OutputStream,
) {
patches.map(transform).let { transformed ->
if (prettyPrint) {
mapper.writerWithDefaultPrettyPrinter().writeValue(outputStream, transformed)
} else {
mapper.writeValue(outputStream, transformed)
}
}
}
/**
* Deserializes a JSON string to a set of [FullJsonPatch]es from an input stream.
*
* @param inputStream The input stream to read the JSON from.
* @param jsonPatchElementClass The class of the [JsonPatch]es to deserialize.
* @return A set of [JsonPatch]es.
* @see FullJsonPatch
*/
fun <T : JsonPatch> deserialize(
inputStream: InputStream,
jsonPatchElementClass: Class<T>,
): Set<T> =
mapper.readValue(
inputStream,
mapper.typeFactory.constructCollectionType(Set::class.java, jsonPatchElementClass),
)
interface JsonPatch
/**
* A JSON representation of a [Patch].
* @see Patch
*/
class FullJsonPatch internal constructor(
val name: String?,
val description: String?,
val compatiblePackages: Set<Patch.CompatiblePackage>?,
val dependencies: Set<PatchClass>?,
val use: Boolean,
var requiresIntegrations: Boolean,
val options: Map<String, FullJsonPatchOption<*>>,
) : JsonPatch {
companion object {
fun fromPatch(patch: Patch<*>) =
FullJsonPatch(
patch.name,
patch.description,
patch.compatiblePackages,
patch.dependencies,
patch.use,
patch.requiresIntegrations,
patch.options.mapValues { FullJsonPatchOption.fromPatchOption(it.value) },
)
}
/**
* A JSON representation of a [PatchOption].
* @see PatchOption
*/
class FullJsonPatchOption<T> internal constructor(
val key: String,
val default: T?,
val values: Map<String, T?>?,
val title: String?,
val description: String?,
val required: Boolean,
val valueType: String,
) {
companion object {
fun fromPatchOption(option: PatchOption<*>) =
FullJsonPatchOption(
option.key,
option.default,
option.values,
option.title,
option.description,
option.required,
option.valueType,
)
}
}
}
}
}

View File

@@ -5,10 +5,11 @@ import app.revanced.library.adb.Constants.CREATE_DIR
import app.revanced.library.adb.Constants.DELETE
import app.revanced.library.adb.Constants.GET_INSTALLED_PATH
import app.revanced.library.adb.Constants.INSTALLATION_PATH
import app.revanced.library.adb.Constants.INSTALL_MOUNT
import app.revanced.library.adb.Constants.INSTALL_MOUNT_SCRIPT
import app.revanced.library.adb.Constants.INSTALL_PATCHED_APK
import app.revanced.library.adb.Constants.MOUNT_PATH
import app.revanced.library.adb.Constants.KILL
import app.revanced.library.adb.Constants.MOUNT_SCRIPT
import app.revanced.library.adb.Constants.MOUNT_SCRIPT_PATH
import app.revanced.library.adb.Constants.PATCHED_APK_PATH
import app.revanced.library.adb.Constants.PLACEHOLDER
import app.revanced.library.adb.Constants.RESTART
@@ -29,15 +30,16 @@ import java.util.logging.Logger
sealed class AdbManager private constructor(deviceSerial: String?) {
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
protected val device = with(JadbConnection().devices) {
if (isEmpty()) throw DeviceNotFoundException()
protected val device =
with(JadbConnection().devices) {
if (isEmpty()) throw DeviceNotFoundException()
deviceSerial?.let {
firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException(deviceSerial)
} ?: first().also {
logger.warning("No device serial supplied. Using device with serial ${it.serial}")
}
}!!
deviceSerial?.let {
firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException(deviceSerial)
} ?: first().also {
logger.warning("No device serial supplied. Using device with serial ${it.serial}")
}
}!!
init {
logger.fine("Connected to ${device.serial}")
@@ -70,8 +72,10 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
* @return The [AdbManager].
* @throws DeviceNotFoundException If the device can not be found.
*/
fun getAdbManager(deviceSerial: String? = null, root: Boolean = false): AdbManager =
if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial)
fun getAdbManager(
deviceSerial: String? = null,
root: Boolean = false,
): AdbManager = if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial)
}
/**
@@ -101,9 +105,8 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement(packageName))
device.run(INSTALL_MOUNT, packageName).waitFor()
device.run(UMOUNT, packageName).waitFor() // Sanity check.
device.run(MOUNT_PATH, packageName).waitFor()
device.run(INSTALL_MOUNT_SCRIPT, packageName).waitFor()
device.run(MOUNT_SCRIPT_PATH, packageName).waitFor()
device.run(RESTART, packageName)
device.run(DELETE, TMP_PATH)
@@ -115,15 +118,19 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
device.run(UMOUNT, packageName)
device.run(DELETE.applyReplacement(PATCHED_APK_PATH), packageName)
device.run(DELETE, MOUNT_PATH.applyReplacement(packageName))
device.run(DELETE, MOUNT_SCRIPT_PATH.applyReplacement(packageName))
device.run(DELETE, TMP_PATH)
device.run(RESTART, packageName)
device.run(KILL, packageName)
super.uninstall(packageName)
}
companion object Utils {
private fun JadbDevice.run(command: String, with: String) = run(command.applyReplacement(with))
private fun JadbDevice.run(
command: String,
with: String,
) = run(command.applyReplacement(with))
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
}
}
@@ -162,13 +169,15 @@ sealed class AdbManager private constructor(deviceSerial: String?) {
class Apk(val file: File, val packageName: String? = null)
class DeviceNotFoundException internal constructor(deviceSerial: String? = null) :
Exception(deviceSerial?.let {
"The device with the ADB device serial \"$deviceSerial\" can not be found"
} ?: "No ADB device found")
Exception(
deviceSerial?.let {
"The device with the ADB device serial \"$deviceSerial\" can not be found"
} ?: "No ADB device found",
)
class FailedToFindInstalledPackageException internal constructor(packageName: String) :
Exception("Failed to find installed package \"$packageName\" because no activity was found")
class PackageNameRequiredException internal constructor() :
Exception("Package name is required")
}
}

View File

@@ -5,8 +5,10 @@ import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
internal fun JadbDevice.buildCommand(
command: String,
su: Boolean = true,
): ShellProcessBuilder {
if (su) return shellProcessBuilder("su -c \'$command\'")
val args = command.split(" ") as ArrayList<String>
@@ -15,14 +17,19 @@ internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): Shell
return shellProcessBuilder(cmd, *args.toTypedArray())
}
internal fun JadbDevice.run(command: String, su: Boolean = true) =
this.buildCommand(command, su).start()
internal fun JadbDevice.run(
command: String,
su: Boolean = true,
) = this.buildCommand(command, su).start()
internal fun JadbDevice.hasSu() =
this.run("whoami", true).waitFor() == 0
internal fun JadbDevice.hasSu() = this.run("whoami", true).waitFor() == 0
internal fun JadbDevice.push(file: File, targetFilePath: String) =
push(file, RemoteFile(targetFilePath))
internal fun JadbDevice.push(
file: File,
targetFilePath: String,
) = push(file, RemoteFile(targetFilePath))
internal fun JadbDevice.createFile(targetFile: String, content: String) =
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
internal fun JadbDevice.createFile(
targetFile: String,
content: String,
) = push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))

View File

@@ -6,37 +6,45 @@ internal object Constants {
internal const val TMP_PATH = "/data/local/tmp/revanced.tmp"
internal const val INSTALLATION_PATH = "/data/adb/revanced/"
internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk"
internal const val MOUNT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
internal const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
internal const val DELETE = "rm -rf $PLACEHOLDER"
internal const val CREATE_DIR = "mkdir -p"
internal const val RESTART = "am start -S $PLACEHOLDER"
internal const val KILL = "am force-stop $PLACEHOLDER"
internal const val GET_INSTALLED_PATH = "pm path $PLACEHOLDER"
internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " +
internal const val INSTALL_PATCHED_APK =
"base_path=\"$PATCHED_APK_PATH\" && " +
"mv $TMP_PATH ${'$'}base_path && " +
"chmod 644 ${'$'}base_path && " +
"chown system:system ${'$'}base_path && " +
"chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
internal const val UMOUNT =
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d ' ' -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH"
internal const val INSTALL_MOUNT_SCRIPT = "mv $TMP_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH"
internal val MOUNT_SCRIPT =
"""
#!/system/bin/sh
MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin
MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
#!/system/bin/sh
MAGISKTMP="$( magisk --path )" || MAGISKTMP=/sbin
MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
until [ "${'$'}(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
until [ -d "/sdcard/Android" ]; do sleep 1; done
base_path="$PATCHED_APK_PATH"
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done
until [ -d "/sdcard/Android" ]; do sleep 1; done
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
# Unmount any existing mount as a safety measure
$UMOUNT
base_path="$PATCHED_APK_PATH"
stock_path=$( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
# Kill the app to force it to restart the mounted APK in case it's already running
$KILL
""".trimIndent()
}

View File

@@ -10,10 +10,11 @@ object Logger {
/**
* Rules for allowed loggers.
*/
private val allowedLoggersRules = arrayOf<String.() -> Boolean>(
{ startsWith("app.revanced") }, // ReVanced loggers.
{ this == "" } // Logs warnings when compiling resources (Logger in class brut.util.OS).
)
private val allowedLoggersRules =
arrayOf<String.() -> Boolean>(
{ startsWith("app.revanced") }, // ReVanced loggers.
{ this == "" }, // Logs warnings when compiling resources (Logger in class brut.util.OS).
)
private val rootLogger = java.util.logging.Logger.getLogger("")
@@ -48,13 +49,14 @@ object Logger {
fun addHandler(
publishHandler: (log: String, level: Level, loggerName: String?) -> Unit,
flushHandler: () -> Unit,
closeHandler: () -> Unit
closeHandler: () -> Unit,
) = object : Handler() {
override fun publish(record: LogRecord) = publishHandler(
formatter.format(record),
record.level,
record.loggerName
)
override fun publish(record: LogRecord) =
publishHandler(
formatter.format(record),
record.level,
record.loggerName,
)
override fun flush() = flushHandler()
@@ -77,10 +79,11 @@ object Logger {
}
log.toByteArray().let {
if (level.intValue() > Level.WARNING.intValue())
if (level.intValue() > Level.WARNING.intValue()) {
System.err.write(it)
else
} else {
System.out.write(it)
}
}
}
@@ -91,4 +94,4 @@ object Logger {
addHandler(publishHandler, flushHandler, flushHandler)
}
}
}

View File

@@ -9,25 +9,34 @@ internal fun UInt.toLittleEndian() =
internal fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
internal fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt()
internal fun UInt.toBigEndian() =
(
((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)
).toUInt()
internal fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
internal fun ByteBuffer.getUShort() = this.getShort().toUShort()
internal fun ByteBuffer.getUInt() = this.getInt().toUInt()
internal fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
internal fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
internal fun DataInput.readUShort() = this.readShort().toUShort()
internal fun DataInput.readUInt() = this.readInt().toUInt()
internal fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
internal fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
internal fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
internal fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
internal fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
internal fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())

View File

@@ -14,10 +14,11 @@ class ZipFile(file: File) : Closeable {
private var entries: MutableList<ZipEntry> = mutableListOf()
// Open file for writing if it doesn't exist (because the intention is to write) or is writable.
private val filePointer: RandomAccessFile = RandomAccessFile(
file,
if (!file.exists() || file.canWrite()) "rw" else "r"
)
private val filePointer: RandomAccessFile =
RandomAccessFile(
file,
if (!file.exists() || file.canWrite()) "rw" else "r",
)
private var centralDirectoryNeedsRewrite = false
@@ -28,8 +29,9 @@ class ZipFile(file: File) : Closeable {
if (file.length() > 0) {
val endRecord = findEndRecord()
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries)
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries) {
throw IllegalArgumentException("Multi-file archives are not supported")
}
entries = readEntries(endRecord).toMutableList()
}
@@ -66,16 +68,17 @@ class ZipFile(file: File) : Closeable {
for (i in 1..numberOfEntries) {
add(
ZipEntry.fromCDE(filePointer).also
{
//for some reason the local extra field can be different from the central one
it.readLocalExtra(
filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
it.localHeaderOffset.toLong() + 28,
2
{
// for some reason the local extra field can be different from the central one
it.readLocalExtra(
filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
it.localHeaderOffset.toLong() + 28,
2,
),
)
)
})
},
)
}
}
}
@@ -89,20 +92,24 @@ class ZipFile(file: File) : Closeable {
val entriesCount = entries.size.toUShort()
val endRecord = ZipEndRecord(
0u,
0u,
entriesCount,
entriesCount,
filePointer.channel.position().toUInt() - centralDirectoryStartOffset,
centralDirectoryStartOffset,
""
)
val endRecord =
ZipEndRecord(
0u,
0u,
entriesCount,
entriesCount,
filePointer.channel.position().toUInt() - centralDirectoryStartOffset,
centralDirectoryStartOffset,
"",
)
filePointer.channel.write(endRecord.toECD())
}
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
private fun addEntry(
entry: ZipEntry,
data: ByteBuffer,
) {
centralDirectoryNeedsRewrite = true
entry.localHeaderOffset = filePointer.channel.position().toUInt()
@@ -113,7 +120,10 @@ class ZipFile(file: File) : Closeable {
entries.add(entry)
}
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
fun addEntryCompressData(
entry: ZipEntry,
data: ByteArray,
) {
val compressor = Deflater(compressionLevel, true)
compressor.setInput(data)
compressor.finish()
@@ -138,7 +148,11 @@ class ZipFile(file: File) : Closeable {
addEntry(entry, compressedBuffer)
}
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
private fun addEntryCopyData(
entry: ZipEntry,
data: ByteBuffer,
alignment: Int? = null,
) {
alignment?.let {
// Calculate where data would end up.
val dataOffset = filePointer.filePointer + entry.LFHSize
@@ -160,7 +174,7 @@ class ZipFile(file: File) : Closeable {
return filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
entry.dataOffset.toLong(),
entry.compressedSize.toLong()
entry.compressedSize.toLong(),
)
}
@@ -170,7 +184,10 @@ class ZipFile(file: File) : Closeable {
* @param file The file to copy entries from.
* @param entryAlignment A function that returns the alignment for a given entry.
*/
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
fun copyEntriesFromFileAligned(
file: ZipFile,
entryAlignment: (entry: ZipEntry) -> Int?,
) {
for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates
@@ -189,9 +206,13 @@ class ZipFile(file: File) : Closeable {
private const val LIBRARY_ALIGNMENT = 4096
val apkZipEntryAlignment = { entry: ZipEntry ->
if (entry.compression.toUInt() != 0u) null
else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT
else DEFAULT_ALIGNMENT
if (entry.compression.toUInt() != 0u) {
null
} else if (entry.fileName.endsWith(".so")) {
LIBRARY_ALIGNMENT
} else {
DEFAULT_ALIGNMENT
}
}
}
}
}

View File

@@ -17,7 +17,6 @@ internal class ZipEndRecord(
val centralDirectoryStartOffset: UInt,
val fileComment: String,
) {
companion object {
const val ECD_HEADER_SIZE = 22
const val ECD_SIGNATURE = 0x06054b50u
@@ -25,8 +24,9 @@ internal class ZipEndRecord(
fun fromECD(input: DataInput): ZipEndRecord {
val signature = input.readUIntLE()
if (signature != ECD_SIGNATURE)
if (signature != ECD_SIGNATURE) {
throw IllegalArgumentException("Input doesn't start with end record signature")
}
val diskNumber = input.readUShortLE()
val startingDiskNumber = input.readUShortLE()
@@ -50,7 +50,7 @@ internal class ZipEndRecord(
totalEntries,
centralDirectorySize,
centralDirectoryStartOffset,
fileComment
fileComment,
)
}
}

View File

@@ -22,7 +22,7 @@ class ZipEntry private constructor(
internal val fileName: String,
internal val extraField: ByteArray,
internal val fileComment: String,
internal var localExtraField: ByteArray = ByteArray(0), //separate for alignment
internal var localExtraField: ByteArray = ByteArray(0), // separate for alignment
) {
internal val LFHSize: Int
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
@@ -31,12 +31,12 @@ class ZipEntry private constructor(
get() = localHeaderOffset + LFHSize.toUInt()
constructor(fileName: String) : this(
0x1403u, //made by unix, version 20
0x1403u, // made by unix, version 20
0u,
0u,
0u,
0x0821u, //seems to be static time google uses, no idea
0x0221u, //same as above
0x0821u, // seems to be static time google uses, no idea
0x0221u, // same as above
0u,
0u,
0u,
@@ -46,21 +46,22 @@ class ZipEntry private constructor(
0u,
fileName,
ByteArray(0),
""
"",
)
companion object {
internal const val CDE_HEADER_SIZE = 46
internal const val CDE_SIGNATURE = 0x02014b50u
internal const val LFH_HEADER_SIZE = 30
internal const val LFH_HEADER_SIZE = 30
internal const val LFH_SIGNATURE = 0x04034b50u
internal fun fromCDE(input: DataInput): ZipEntry {
val signature = input.readUIntLE()
if (signature != CDE_SIGNATURE)
if (signature != CDE_SIGNATURE) {
throw IllegalArgumentException("Input doesn't start with central directory entry signature")
}
val version = input.readUShortLE()
val versionNeeded = input.readUShortLE()
@@ -97,8 +98,11 @@ class ZipEntry private constructor(
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
}
flags = (flags and 0b1000u.inv()
.toUShort()) //disable data descriptor flag as they are not used
flags = (
flags and
0b1000u.inv()
.toUShort()
) // disable data descriptor flag as they are not used
return ZipEntry(
version,
@@ -121,7 +125,7 @@ class ZipEntry private constructor(
}
}
internal fun readLocalExtra(buffer: ByteBuffer) {
internal fun readLocalExtra(buffer: ByteBuffer) {
buffer.order(ByteOrder.LITTLE_ENDIAN)
localExtraField = ByteArray(buffer.getUShort().toInt())
}
@@ -129,8 +133,9 @@ class ZipEntry private constructor(
internal fun toLFH(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
val buffer =
ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(LFH_SIGNATURE)
buffer.putUShort(versionNeeded)
@@ -184,4 +189,4 @@ class ZipEntry private constructor(
buffer.flip()
return buffer
}
}
}

View File

@@ -45,4 +45,4 @@ internal object PatchOptionsTest {
// Do nothing
}
}
}
}

View File

@@ -4,15 +4,116 @@ import app.revanced.patcher.PatchSet
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.intArrayPatchOption
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import kotlin.test.assertEquals
internal object PatchUtilsTest {
private val patches =
arrayOf(
newPatch("some.package", setOf("a")) { stringPatchOption("string", "value") },
newPatch("some.package", setOf("a", "b"), use = false),
newPatch("some.package", setOf("a", "b", "c"), use = false),
newPatch("some.other.package", setOf("b"), use = false),
newPatch("some.other.package", setOf("b", "c")) { booleanPatchOption("bool", true) },
newPatch("some.other.package", setOf("b", "c", "d")),
newPatch("some.other.other.package") { intArrayPatchOption("intArray", arrayOf(1, 2, 3)) },
newPatch("some.other.other.package", setOf("a")),
newPatch("some.other.other.package", setOf("b")),
newPatch("some.other.other.other.package", use = false),
newPatch("some.other.other.other.package", use = false),
).toSet()
@Test
fun `empty because package is incompatible with any version`() {
assertEqualsVersions(
expected = emptyMap(),
patches = setOf(newPatch("some.package", emptySet(), use = true)),
compatiblePackageNames = setOf("some.package"),
)
}
@Test
fun `empty list of versions because package is unconstrained to any version`() {
assertEqualsVersions(
expected = mapOf("some.package" to linkedMapOf()),
patches = setOf(newPatch("some.package")),
compatiblePackageNames = setOf("some.package"),
countUnusedPatches = true,
)
}
@Test
fun `empty because no known package was supplied`() {
assertEqualsVersions(
expected = emptyMap(),
patches,
compatiblePackageNames = setOf("unknown.package"),
)
}
@Test
fun `common versions correctly ordered for each package`() {
fun assertEqualsExpected(compatiblePackageNames: Set<String>?) =
assertEqualsVersions(
expected =
mapOf(
"some.package" to linkedMapOf("a" to 3, "b" to 2, "c" to 1),
"some.other.package" to linkedMapOf("b" to 3, "c" to 2, "d" to 1),
"some.other.other.package" to linkedMapOf("a" to 1, "b" to 1),
"some.other.other.other.package" to linkedMapOf(),
),
patches,
compatiblePackageNames,
countUnusedPatches = true,
)
assertEqualsExpected(
compatiblePackageNames =
setOf(
"some.package",
"some.other.package",
"some.other.other.package",
"some.other.other.other.package",
),
)
assertEqualsExpected(
compatiblePackageNames = null,
)
}
@Test
fun `common versions correctly ordered for each package without counting unused patches`() {
assertEqualsVersions(
expected =
mapOf(
"some.package" to linkedMapOf("a" to 1),
"some.other.package" to linkedMapOf("b" to 2, "c" to 2, "d" to 1),
"some.other.other.package" to linkedMapOf("a" to 1, "b" to 1),
),
patches,
compatiblePackageNames =
setOf(
"some.package",
"some.other.package",
"some.other.other.package",
"some.other.other.other.package",
),
countUnusedPatches = false,
)
}
@Test
fun `return 'a' because it is the most common version`() {
val patches = arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d")
.map { version -> newPatch("some.package", version) }
.toSet()
val patches =
arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d")
.map { version -> newPatch("some.package", setOf(version)) }
.toSet()
assertEqualsVersion("a", patches, "some.package")
}
@@ -24,35 +125,83 @@ internal object PatchUtilsTest {
@Test
fun `return null because no patch is compatible with the supplied package name`() {
val patches = setOf(newPatch("some.package", "a"))
val patches = setOf(newPatch("some.package", setOf("a")))
assertEqualsVersion(null, patches, "other.package")
}
@Test
fun `return null because no patch compatible package is constrained to a version`() {
val patches = setOf(
newPatch("other.package"),
newPatch("other.package"),
)
fun `return null because no compatible package is constrained to a version`() {
val patches =
setOf(
newPatch("other.package"),
newPatch("other.package"),
)
assertEqualsVersion(null, patches, "other.package")
}
@Test
fun `serializes to and deserializes from JSON string correctly`() {
val out = ByteArrayOutputStream()
PatchUtils.Json.serialize(patches, outputStream = out)
val deserialized =
PatchUtils.Json.deserialize(
ByteArrayInputStream(out.toByteArray()),
PatchUtils.Json.FullJsonPatch::class.java,
)
assert(patches.size == deserialized.size)
}
private fun assertEqualsVersions(
expected: PackageNameMap,
patches: PatchSet,
compatiblePackageNames: Set<String>?,
countUnusedPatches: Boolean = false,
) = assertEquals(
expected,
PatchUtils.getMostCommonCompatibleVersions(patches, compatiblePackageNames, countUnusedPatches),
)
private fun assertEqualsVersion(
expected: String?, patches: PatchSet, compatiblePackageName: String
) = assertEquals(expected, PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName))
expected: String?,
patches: PatchSet,
compatiblePackageName: String,
) {
// Test both the deprecated and the new method.
private fun newPatch(packageName: String, vararg versions: String) = object : BytecodePatch() {
@Suppress("DEPRECATION")
assertEquals(
expected,
PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName),
)
assertEquals(
expected,
PatchUtils.getMostCommonCompatibleVersions(patches, setOf(compatiblePackageName))
.entries.firstOrNull()?.value?.keys?.firstOrNull(),
)
}
private fun newPatch(
packageName: String,
versions: Set<String>? = null,
use: Boolean = true,
options: Patch<*>.() -> Unit = {},
) = object : BytecodePatch(
name = "test",
compatiblePackages = setOf(CompatiblePackage(packageName, versions?.toSet())),
use = use,
) {
init {
// Set the compatible packages field to the supplied package name and versions reflectively,
// because the setter is private but needed for testing.
val compatiblePackagesField = Patch::class.java.getDeclaredField("compatiblePackages")
compatiblePackagesField.isAccessible = true
compatiblePackagesField.set(this, setOf(CompatiblePackage(packageName, versions.toSet())))
options()
}
override fun execute(context: BytecodeContext) {}
// Needed to make the patches unique.
override fun equals(other: Any?) = false
}
}
}