[index] Add UpdateChecker with tests

Also introduce interfaces for important classes, so they can be implemented by stuff like database classes as well. This makes UpdateChecker and CompatibilityChecker more generic.

Also add tests for CompatibilityChecker.
This commit is contained in:
Torsten Grote
2022-06-02 13:51:57 -03:00
parent 17acd0d56f
commit 632833a3e7
11 changed files with 572 additions and 35 deletions

View File

@@ -3774,6 +3774,11 @@
<sha256 value="dd39be18d38ba8c157d752f8e26aeb05ef80354f73f05faaac9f98c2f3c35496" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="io.mockk" name="mockk" version="1.12.4">
<artifact name="mockk-1.12.4.jar">
<sha256 value="2c34a3690b958a3cf38b82d0f4910dc9992fb078dce6f56d71498293557bf805" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-agent-android" version="1.12.3">
<artifact name="mockk-agent-android-1.12.3.aar">
<sha256 value="3a14b27b3888370a8e6e8af62979efdd87f31451c798a41eca741f68ce7772ab" origin="Generated by Gradle because a key couldn't be downloaded"/>
@@ -3784,31 +3789,66 @@
<sha256 value="c9de731e90bbd7a27ed13e5370e7a10ab44b4fe1e85137bc15568494213aade3" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-agent-api" version="1.12.4">
<artifact name="mockk-agent-api-1.12.4.jar">
<sha256 value="6eb3407b1f88c0c0ced9636f82874100786b74b3b06c4354c4d85229779fdec8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-agent-common" version="1.12.3">
<artifact name="mockk-agent-common-1.12.3.jar">
<sha256 value="201f62541a73bed02ec346e413e7f52f70d300cf8c3ead3d37fca8dfc42fc97c" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-agent-common" version="1.12.4">
<artifact name="mockk-agent-common-1.12.4.jar">
<sha256 value="427d071ec7a85f105c152a51a89738d8ee52954130e5c09500837dfbe3549329" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-agent-jvm" version="1.12.4">
<artifact name="mockk-agent-jvm-1.12.4.jar">
<sha256 value="840c11f2e0a14d35e229c2b6018273f4623c7f619ebf9701164bb9c2db99c098" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-android" version="1.12.3">
<artifact name="mockk-android-1.12.3.aar">
<sha256 value="3ad29a64b6bf8a1ce150f6cda36fdd56dec273a2fbcb169e84ccda6126aa3497" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-android" version="1.12.4">
<artifact name="mockk-android-1.12.4.aar">
<sha256 value="3ad29a64b6bf8a1ce150f6cda36fdd56dec273a2fbcb169e84ccda6126aa3497" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-common" version="1.12.3">
<artifact name="mockk-common-1.12.3.jar">
<sha256 value="2fb0d511ab5ec1fa31981d1dba94a9f8da603787a81e59d36395c695b661a2ab" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-common" version="1.12.4">
<artifact name="mockk-common-1.12.4.jar">
<sha256 value="16f1ba4738535458cb91fa1a759794f6618a6f9f1ae1d149e79b48cc06ea5e7b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-dsl" version="1.12.3">
<artifact name="mockk-dsl-1.12.3.jar">
<sha256 value="05ceb1530ec4e6f9c7a1fe9c5b194a97e48023a95aaa4b91c96364f70c020f93" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-dsl" version="1.12.4">
<artifact name="mockk-dsl-1.12.4.jar">
<sha256 value="7fc96f9ed5118c915a3890ba2e4090c9b283ae7bdc37ab83885415bdf77650e4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-dsl-jvm" version="1.12.3">
<artifact name="mockk-dsl-jvm-1.12.3.jar">
<sha256 value="038d1c794cb26e3f56d217fa31ccf9b9f08bdec18c50b078705ed8f2fe1006b9" origin="Generated by Gradle because a key couldn't be downloaded"/>
</artifact>
</component>
<component group="io.mockk" name="mockk-dsl-jvm" version="1.12.4">
<artifact name="mockk-dsl-jvm-1.12.4.jar">
<sha256 value="faee4b52def68fa182f89d23c2a45f2246ef88b6b1ba98346aa85f57e5ed630f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.netty" name="netty-buffer" version="4.1.34.Final">
<artifact name="netty-buffer-4.1.34.Final.jar">
<sha256 value="39dfe88df8505fd01fbf9c1dbb6b6fa9b0297e453c3dc4ce039ea578aea2eaa3" origin="Generated by Gradle"/>
@@ -4075,6 +4115,16 @@
<sha256 value="4b41509cc861e7d9c0bc6d7fd19fd885bf1a43f0357808f8073e5b2029d09d82" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy" version="1.12.6">
<artifact name="byte-buddy-1.12.6.jar">
<sha256 value="211918dc24f0fdef4335ce8af40ef5616e15e818b962a21146397c7701eb75a7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.10.20">
<artifact name="byte-buddy-agent-1.10.20.jar">
<sha256 value="b592a6c43e752bf41659717956c57fbb790394d2ee5f8941876659f9c5c0e7e8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.10.5">
<artifact name="byte-buddy-agent-1.10.5.jar">
<sha256 value="290c9930965ef5810ddb15baf3b3647ce952f40fa2f0af82d5f669e04ba87e5b" origin="Generated by Gradle"/>
@@ -4083,6 +4133,11 @@
<sha256 value="1e132221d225384596ef5e3fa4808763410981a0ef036f0c25f99c0a73667017" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.12.6">
<artifact name="byte-buddy-agent-1.12.6.jar">
<sha256 value="9b29421fe4650b75fc3ed53590f914c54f932e334b3506cc00296dff73024183" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-parent" version="1.10.5">
<artifact name="byte-buddy-parent-1.10.5.pom">
<sha256 value="d09a551832612058930d5f5a0473c4628be5af8a4961d12b7d5dd6b30160c61a" origin="Generated by Gradle"/>
@@ -5367,6 +5422,11 @@
<sha256 value="6e0f5490e6b9649ddd2670534e4d3a03bd283c3358b8eef5d1304fd5f8a5a4fb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.0">
<artifact name="kotlin-reflect-1.6.0.jar">
<sha256 value="c6161884209221db7f5ddb031bb480a3c46bb90d5b65d7cc0167b149aaa9c494" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.10">
<artifact name="kotlin-reflect-1.6.10.jar">
<sha256 value="3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203" origin="Generated by Gradle"/>

View File

@@ -70,6 +70,7 @@ kotlin {
androidTest {
dependencies {
implementation 'junit:junit:4.13.2'
implementation 'io.mockk:mockk:1.12.4'
}
}
androidAndroidTest {
@@ -105,6 +106,9 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions {
unitTests.returnDefaultValues = true
}
}
import org.jetbrains.dokka.gradle.DokkaTask

View File

@@ -3,18 +3,20 @@ package org.fdroid
import android.content.pm.PackageManager
import android.os.Build.SUPPORTED_ABIS
import android.os.Build.VERSION.SDK_INT
import org.fdroid.index.v2.ManifestV2
import org.fdroid.index.v2.PackageManifest
public fun interface CompatibilityChecker {
public fun isCompatible(manifest: ManifestV2): Boolean
public fun isCompatible(manifest: PackageManifest): Boolean
}
/**
* This class checks if an APK is compatible with the user's device.
*/
public class CompatibilityCheckerImpl(
public class CompatibilityCheckerImpl @JvmOverloads constructor(
packageManager: PackageManager,
private val forceTouchApps: Boolean = false,
private val sdkInt: Int = SDK_INT,
private val supportedAbis: Array<String> = SUPPORTED_ABIS,
) : CompatibilityChecker {
private val features = HashMap<String, Int>().apply {
@@ -25,21 +27,22 @@ public class CompatibilityCheckerImpl(
}
}
public override fun isCompatible(manifest: ManifestV2): Boolean {
if (SDK_INT < manifest.usesSdk?.minSdkVersion ?: 0) return false
if (SDK_INT > manifest.maxSdkVersion ?: Int.MAX_VALUE) return false
public override fun isCompatible(manifest: PackageManifest): Boolean {
if (sdkInt < manifest.minSdkVersion ?: 0) return false
if (sdkInt > manifest.maxSdkVersion ?: Int.MAX_VALUE) return false
if (!isNativeCodeCompatible(manifest)) return false
manifest.features.iterator().forEach { feature ->
if (forceTouchApps && feature.name == "android.hardware.touchscreen") return@forEach
if (!features.containsKey(feature.name)) return false
manifest.featureNames?.iterator()?.forEach { feature ->
if (forceTouchApps && feature == "android.hardware.touchscreen") return@forEach
if (!features.containsKey(feature)) return false
}
return true
}
private fun isNativeCodeCompatible(manifest: ManifestV2): Boolean {
if (manifest.nativecode.isNullOrEmpty()) return true
SUPPORTED_ABIS.forEach { supportedAbi ->
if (manifest.nativecode.contains(supportedAbi)) return true
private fun isNativeCodeCompatible(manifest: PackageManifest): Boolean {
val nativeCode = manifest.nativecode
if (nativeCode.isNullOrEmpty()) return true
supportedAbis.forEach { supportedAbi ->
if (nativeCode.contains(supportedAbi)) return true
}
return false
}

View File

@@ -0,0 +1,152 @@
package org.fdroid
import android.content.pm.PackageInfo
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import org.fdroid.index.IndexUtils.getPackageSignature
import org.fdroid.index.IndexUtils.getVersionCode
import org.fdroid.index.v2.PackageVersion
public interface PackagePreference {
public val ignoreVersionCodeUpdate: Long
public val releaseChannels: List<String>
}
public class UpdateChecker(
private val compatibilityChecker: CompatibilityChecker,
) {
/**
* Returns a [PackageVersion] for the given [packageInfo] that is the suggested update
* or null if there is no suitable update in [versions].
*
* Special case: A version with the [PackageInfo.getLongVersionCode] will be returned
* if [PackageVersion.hasKnownVulnerability] is true, even if there is no update.
*
* @param versions a **sorted** list of [PackageVersion] with highest version code first.
* @param packageInfo needs to be retrieved with [GET_SIGNING_CERTIFICATES]
* @param releaseChannels optional list of release channels to consider on top of stable.
* If this is null or empty, only versions without channel (stable) will be considered.
* @param preferencesGetter an optional way to consider additional per app preferences
*/
public fun <T : PackageVersion> getUpdate(
versions: List<T>,
packageInfo: PackageInfo,
releaseChannels: List<String>? = null,
preferencesGetter: (() -> PackagePreference?)? = null,
): T? = getUpdate(
versions = versions,
allowedSignersGetter = {
// always gives us the oldest signer, even if they rotated certs by now
@Suppress("DEPRECATION")
packageInfo.signatures.map { getPackageSignature(it.toByteArray()) }.toSet()
},
installedVersionCode = packageInfo.getVersionCode(),
allowedReleaseChannels = releaseChannels,
preferencesGetter = preferencesGetter,
)
/**
* Returns the [PackageVersion] that is suggested for a new installation
* or null if there is no suitable candidate in [versions].
*
* @param versions a **sorted** list of [PackageVersion] with highest version code first.
* @param preferredSigner The SHA-256 hash of the signing certificate in lower-case hex.
* Only versions from this signer will be considered for installation.
* @param releaseChannels optional list of release channels to consider on top of stable.
* If this is null or empty, only versions without channel (stable) will be considered.
* @param preferencesGetter an optional way to consider additional per app preferences
*/
public fun <T : PackageVersion> getSuggestedVersion(
versions: List<T>,
preferredSigner: String?,
releaseChannels: List<String>? = null,
preferencesGetter: (() -> PackagePreference?)? = null,
): T? = getUpdate(
versions = versions,
allowedSignersGetter = preferredSigner?.let { { setOf(it) } },
allowedReleaseChannels = releaseChannels,
preferencesGetter = preferencesGetter,
)
/**
* Returns the [PackageVersion] that is the suggested update
* for the given [installedVersionCode] or suggested for new installed if the given code is 0,
* or null if there is no suitable candidate in [versions].
*
* Special case: A version with the [installedVersionCode] will be returned
* if [PackageVersion.hasKnownVulnerability] is true, even if there is no update.
*
* @param versions a **sorted** list of [PackageVersion] with highest version code first.
* @param allowedSignersGetter should return set of SHA-256 hashes of the signing certificates
* in lower-case hex. Only versions from these signers will be considered for installation.
* This is is null or returns null, all signers will be allowed.
* If the set of signers is empty, no signers will be allowed, i.e. only apps without signer.
* @param allowedReleaseChannels optional list of release channels to consider on top of stable.
* If this is null or empty, only versions without channel (stable) will be considered.
* @param preferencesGetter an optional way to consider additional per app preferences
*/
public fun <T : PackageVersion> getUpdate(
versions: List<T>,
allowedSignersGetter: (() -> Set<String>?)? = null,
installedVersionCode: Long = 0,
allowedReleaseChannels: List<String>? = null,
preferencesGetter: (() -> PackagePreference?)? = null,
): T? {
// getting signatures is rather expensive, so we only do that when there's update candidates
val allowedSigners by lazy { allowedSignersGetter?.let { it() } }
versions.iterator().forEach versions@{ version ->
// if the installed version has a known vulnerability, we return it as well
if (version.versionCode == installedVersionCode && version.hasKnownVulnerability) {
return version
}
// if version code is not higher than installed skip package as list is sorted
if (version.versionCode <= installedVersionCode) return null
// we don't support versions that have multiple signers
if (version.signer?.hasMultipleSigners == true) return@versions
// skip incompatible versions
if (!compatibilityChecker.isCompatible(version.packageManifest)) return@versions
// check if we should ignore this version code
val packagePreference = preferencesGetter?.let { it() }
val ignoreVersionCode = packagePreference?.ignoreVersionCodeUpdate ?: 0
if (ignoreVersionCode >= version.versionCode) return@versions
// check if release channel of version is allowed
val hasAllowedReleaseChannel = hasAllowedReleaseChannel(
allowedReleaseChannels = allowedReleaseChannels?.toMutableSet() ?: LinkedHashSet(),
versionReleaseChannels = version.releaseChannels,
packagePreference = packagePreference,
)
if (!hasAllowedReleaseChannel) return@versions
// check if this version's signer is allowed
val versionSigners = version.signer?.sha256?.toSet()
// F-Droid allows versions without signature 🤦, allow those and if no allowed signers
if (versionSigners != null && allowedSigners != null) {
if (versionSigners.intersect(allowedSigners!!).isEmpty()) return@versions
}
// no need to see other versions, we got the highest version code per sorting
return version
}
return null
}
private fun hasAllowedReleaseChannel(
allowedReleaseChannels: MutableSet<String>,
versionReleaseChannels: List<String>?,
packagePreference: PackagePreference?,
): Boolean {
// no channels (aka stable version) is always allowed
if (versionReleaseChannels.isNullOrEmpty()) return true
// add release channels from package preferences into the ones we allow
val extraChannels = packagePreference?.releaseChannels
if (!extraChannels.isNullOrEmpty()) {
allowedReleaseChannels.addAll(extraChannels)
}
// if allowed releases channels are empty (only stable) don't consider this version
if (allowedReleaseChannels.isEmpty()) return false
// don't consider version with non-matching release channel
if (allowedReleaseChannels.intersect(versionReleaseChannels).isEmpty()) return false
// one of the allowed channels is present in this version
return true
}
}

View File

@@ -36,7 +36,7 @@ public object IndexUtils {
return messageDigest.digest()
}
internal fun PackageInfo.getVersionCode(): Long {
public fun PackageInfo.getVersionCode(): Long {
return if (Build.VERSION.SDK_INT >= 28) {
longVersionCode
} else {

View File

@@ -0,0 +1,113 @@
package org.fdroid
import android.content.pm.FeatureInfo
import android.content.pm.PackageManager
import io.mockk.every
import io.mockk.mockk
import org.fdroid.index.v2.PackageManifest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
internal class CompatibilityCheckerTest {
private val sdkInt: Int = 30
private val supportedAbis = arrayOf("x86")
private val packageManager: PackageManager = mockk()
init {
every { packageManager.systemAvailableFeatures } returns arrayOf(
FeatureInfo().apply { name = "foo bar" },
FeatureInfo().apply { name = "1337" },
)
}
private val checker = CompatibilityCheckerImpl(
packageManager = packageManager,
forceTouchApps = false,
sdkInt = sdkInt,
supportedAbis = supportedAbis,
)
@Test
fun emptyManifestIsCompatible() {
val manifest = Manifest()
assertTrue(checker.isCompatible(manifest))
}
@Test
fun minSdkIsRespected() {
// smaller or equal minSdks are compatible
val manifest1 = Manifest(minSdkVersion = 1)
assertTrue(checker.isCompatible(manifest1))
val manifest2 = Manifest(minSdkVersion = sdkInt)
assertTrue(checker.isCompatible(manifest2))
// a minSdk higher than the system is not compatible
val manifest3 = Manifest(minSdkVersion = sdkInt + 1)
assertFalse(checker.isCompatible(manifest3))
}
@Test
fun maxSdkIsRespected() {
// smaller maxSdks are not compatible
val manifest1 = Manifest(maxSdkVersion = sdkInt - 1)
assertFalse(checker.isCompatible(manifest1))
// higher or equal are compatible
val manifest2 = Manifest(maxSdkVersion = sdkInt)
assertTrue(checker.isCompatible(manifest2))
val manifest3 = Manifest(maxSdkVersion = sdkInt + 1)
assertTrue(checker.isCompatible(manifest3))
}
@Test
fun emptyNativeCodeIsCompatible() {
val manifest = Manifest(nativecode = emptyList())
assertTrue(checker.isCompatible(manifest))
}
@Test
fun nativeCodeMustBeAvailable() {
val manifest1 = Manifest(nativecode = listOf("x86"))
assertTrue(checker.isCompatible(manifest1))
val manifest2 = Manifest(nativecode = listOf("x86", "armeabi-v7a"))
assertTrue(checker.isCompatible(manifest2))
val manifest3 = Manifest(nativecode = listOf("arm64-v8a", "armeabi-v7a"))
assertFalse(checker.isCompatible(manifest3))
}
@Test
fun featuresMustBeAvailable() {
val manifest1 = Manifest(featureNames = listOf("foo bar"))
assertTrue(checker.isCompatible(manifest1))
val manifest2 = Manifest(featureNames = listOf("1337", "foo bar"))
assertTrue(checker.isCompatible(manifest2))
val manifest3 = Manifest(featureNames = listOf("1337", "foo bar", "42"))
assertFalse(checker.isCompatible(manifest3))
val manifest4 = Manifest(featureNames = listOf("foo", "bar"))
assertFalse(checker.isCompatible(manifest4))
}
@Test
fun forceTouchScreenIsRespected() {
val checkerForce = CompatibilityCheckerImpl(packageManager, true, sdkInt, supportedAbis)
// when forced, apps that need touchscreen on non-touchscreen device are compatible
val manifest1 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
assertTrue(checkerForce.isCompatible(manifest1))
val manifest2 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
assertTrue(checkerForce.isCompatible(manifest2))
// when not forced, apps that need touchscreen on non-touchscreen device are not compatible
val manifest3 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
assertFalse(checker.isCompatible(manifest3))
val manifest4 = Manifest(featureNames = listOf("android.hardware.touchscreen"))
assertFalse(checker.isCompatible(manifest4))
}
private data class Manifest(
override val minSdkVersion: Int? = null,
override val maxSdkVersion: Int? = null,
override val featureNames: List<String>? = null,
override val nativecode: List<String>? = null,
) : PackageManifest
}

View File

@@ -0,0 +1,179 @@
package org.fdroid
import org.fdroid.index.RELEASE_CHANNEL_BETA
import org.fdroid.index.v2.PackageManifest
import org.fdroid.index.v2.PackageVersion
import org.fdroid.index.v2.SignerV2
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
internal class UpdateCheckerTest {
private val updateChecker = UpdateChecker { true }
private val signer = "9f9261f0b911c60f8db722f5d430a9e9d557a3f8078ce43e1c07522ef41efedb"
private val signatureV2 = SignerV2(listOf(signer))
private val betaChannels = listOf(RELEASE_CHANNEL_BETA)
private val version1 = Version(1)
private val version2 = Version(2)
private val version3 = Version(3)
private val versions = listOf(version3, version2, version1)
@Test
fun highestVersionCode() {
assertEquals(version3, updateChecker.getUpdate(versions))
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 2))
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 1))
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 0))
}
@Test
fun noUpdateIfSameOrHigherVersionInstalled() {
assertNull(updateChecker.getUpdate(versions, installedVersionCode = 3))
assertNull(updateChecker.getUpdate(versions, installedVersionCode = 4))
}
@Test
fun testIncompatibleVersionNotConsidered() {
var versionNum = 0
val updateChecker = UpdateChecker {
versionNum++
versionNum != 1 // highest version is incompatible
}
val v = updateChecker.getUpdate(versions)
assertEquals(version2, v)
}
@Test
fun ignoredVersionNotConsidered() {
val not4 = { AppPreferences(ignoreVersionCodeUpdate = 4) }
val not3 = { AppPreferences(ignoreVersionCodeUpdate = 3) }
val not2 = { AppPreferences(ignoreVersionCodeUpdate = 2) }
assertNull(updateChecker.getUpdate(versions, preferencesGetter = not4))
assertNull(updateChecker.getUpdate(versions, preferencesGetter = not3))
assertEquals(version3, updateChecker.getUpdate(versions, preferencesGetter = not2))
}
@Test
fun betaVersionOnlyReturnedWhenAllowed() {
val version3 = version3.copy(releaseChannels = betaChannels)
val versions = listOf(version3, version2, version1)
// beta not allowed, so 2 returned
assertEquals(version2, updateChecker.getUpdate(versions))
// now beta is allowed, so 3 returned
assertEquals(version3, getWithAllowReleaseChannels(versions, betaChannels))
}
@Test
fun emptyReleaseChannelsAlwaysIncluded() {
val version3 = version3.copy(releaseChannels = emptyList())
val versions = listOf(version3, version2, version1)
// version with empty release channels gets returned
assertEquals(version3, updateChecker.getUpdate(versions))
// version with empty release channels gets returned when allowing also beta
assertEquals(version3,
updateChecker.getUpdate(versions, allowedReleaseChannels = betaChannels)
)
// version with empty release channels gets returned when allow list is empty
assertEquals(version3, getWithAllowReleaseChannels(versions, emptyList()))
assertEquals(this.version3, getWithAllowReleaseChannels(this.versions, emptyList()))
}
@Test
fun onlyAllowedReleaseChannelsGetIncluded() {
val version3 = version3.copy(releaseChannels = listOf("a"))
val version2 = version2.copy(releaseChannels = listOf("a", "b", "c"))
val versions = listOf(version3, version2, version1)
// only stable version gets returned
assertEquals(version1, getWithAllowReleaseChannels(versions, null))
// as long as "a" is included, 3 gets returned
assertEquals(version3, getWithAllowReleaseChannels(versions, listOf("a")))
assertEquals(version3, getWithAllowReleaseChannels(versions, listOf("a", "b")))
assertEquals(version3, getWithAllowReleaseChannels(versions, listOf("a", "b", "z")))
// as long as "b" or "c" is included, 2 gets returned
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("b")))
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("b", "z")))
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("c")))
assertEquals(version2, getWithAllowReleaseChannels(versions, listOf("c", "z")))
// if neither "a", "b" or "c" is included, 1 gets returned
assertEquals(version1, getWithAllowReleaseChannels(versions, listOf("x", "y", "z")))
}
@Test
fun multipleSignersNotSupported() {
val version = version3.copy(signer = signatureV2.copy(hasMultipleSigners = true))
val versions = listOf(version)
assertNull(updateChecker.getUpdate(versions))
}
@Test
fun onlyAllowedSignersGetIncluded() {
val version3 = version3.copy(signer = SignerV2(listOf("foo", "bar")))
val version2 = version2.copy(signer = signatureV2)
val versions = listOf(version3, version2, version1)
val v2Set = signatureV2.sha256.toMutableSet()
// 3 gets returned if at least one of its signers are allowed, or all are allowed
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("foo") }))
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("bar") }))
assertEquals(version3, updateChecker.getUpdate(versions, { v2Set + "bar" }))
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("foo", "bar") }))
assertEquals(version3, updateChecker.getUpdate(versions, { setOf("foo", "bar", "42") }))
assertEquals(version3, updateChecker.getUpdate(versions, { null }))
// 2 gets returned if at least one of its signers are allowed
assertEquals(version2, updateChecker.getUpdate(versions, { v2Set }))
assertEquals(version2, updateChecker.getUpdate(versions, { v2Set + "foo bar" }))
// empty set means no signatures are allowed, only works for apps without signature
assertEquals(version1, updateChecker.getUpdate(versions, { emptySet() }))
// apps without signature get through everything
assertEquals(version1, updateChecker.getUpdate(versions, { setOf("no version") }))
// if no matching sig can be found, no version gets returned
assertNull(updateChecker.getUpdate(listOf(version3, version2), { setOf("no version") }))
}
@Test
fun installedVulnerableVersionAlwaysReturned() {
val version3 = version3.copy(hasKnownVulnerability = true)
val versions = listOf(version3, version2, version1)
assertEquals(version3, updateChecker.getUpdate(versions))
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 3))
assertEquals(version3, updateChecker.getUpdate(versions, installedVersionCode = 2))
}
private fun getWithAllowReleaseChannels(
versions: List<Version>,
releaseChannels: List<String>?,
): Version? {
val v1 = updateChecker.getUpdate(versions, allowedReleaseChannels = releaseChannels)
assertEquals(v1,
updateChecker.getUpdate(versions) {
AppPreferences(releaseChannels = releaseChannels ?: emptyList())
}
)
assertEquals(v1,
updateChecker.getUpdate(versions, allowedReleaseChannels = releaseChannels) {
AppPreferences(releaseChannels = releaseChannels ?: emptyList())
}
)
return v1
}
private data class Version(
override val versionCode: Long,
override val signer: SignerV2? = null,
override val releaseChannels: List<String>? = null,
// the manifest is only needed for compatibility checking which we can test differently
override val packageManifest: PackageManifest = object : PackageManifest {
override val minSdkVersion: Int? = null
override val maxSdkVersion: Int? = null
override val featureNames: List<String>? = null
override val nativecode: List<String>? = null
},
override val hasKnownVulnerability: Boolean = false,
) : PackageVersion
private data class AppPreferences(
override val ignoreVersionCodeUpdate: Long = 0,
override val releaseChannels: List<String> = emptyList(),
) : PackagePreference
}

View File

@@ -22,7 +22,7 @@ import org.fdroid.index.v2.LocalizedTextV2
import org.fdroid.index.v2.ManifestV2
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.index.v2.PermissionV2
import org.fdroid.index.v2.SignatureV2
import org.fdroid.index.v2.SignerV2
import org.fdroid.index.v2.UsesSdkV2
@Serializable
@@ -71,7 +71,7 @@ public data class PackageV1(
targetSdkVersion = targetSdkVersion ?: minSdkVersion ?: 1,
),
maxSdkVersion = maxSdkVersion,
signer = signer?.let { SignatureV2(listOf(it)) },
signer = signer?.let { SignerV2(listOf(it)) },
usesPermission = usesPermission.map { PermissionV2(it.name, it.maxSdk) },
usesPermissionSdk23 = usesPermission23.map { PermissionV2(it.name, it.maxSdk) },
nativecode = nativeCode ?: emptyList(),

View File

@@ -71,16 +71,32 @@ public data class Screenshots(
get() = phone == null && sevenInch == null && tenInch == null && wear == null && tv == null
}
public interface PackageVersion {
public val versionCode: Long
public val signer: SignerV2?
public val releaseChannels: List<String>?
public val packageManifest: PackageManifest
public val hasKnownVulnerability: Boolean
}
public const val ANTI_FEATURE_KNOWN_VULNERABILITY: String = "KnownVuln"
@Serializable
public data class PackageVersionV2(
val added: Long,
val file: FileV1,
val src: FileV2? = null,
val manifest: ManifestV2,
val releaseChannels: List<String> = emptyList(),
override val releaseChannels: List<String> = emptyList(),
val antiFeatures: Map<String, LocalizedTextV2> = emptyMap(),
val whatsNew: LocalizedTextV2 = emptyMap(),
) {
) : PackageVersion {
override val versionCode: Long = manifest.versionCode
override val signer: SignerV2? = manifest.signer
override val packageManifest: PackageManifest = manifest
override val hasKnownVulnerability: Boolean
get() = antiFeatures.contains(ANTI_FEATURE_KNOWN_VULNERABILITY)
public fun walkFiles(fileConsumer: (FileV2?) -> Unit) {
fileConsumer(src)
}
@@ -97,18 +113,28 @@ public data class FileV1(
val size: Long? = null,
)
public interface PackageManifest {
public val minSdkVersion: Int?
public val maxSdkVersion: Int?
public val featureNames: List<String>?
public val nativecode: List<String>?
}
@Serializable
public data class ManifestV2(
val versionName: String,
val versionCode: Long,
val usesSdk: UsesSdkV2? = null,
val maxSdkVersion: Int? = null,
val signer: SignatureV2? = null, // TODO really null?
override val maxSdkVersion: Int? = null,
val signer: SignerV2? = null, // yes this can be null for stuff like non-apps
val usesPermission: List<PermissionV2> = emptyList(),
val usesPermissionSdk23: List<PermissionV2> = emptyList(),
val nativecode: List<String> = emptyList(),
override val nativecode: List<String> = emptyList(),
val features: List<FeatureV2> = emptyList(),
)
) : PackageManifest {
override val minSdkVersion: Int? = usesSdk?.minSdkVersion
override val featureNames: List<String> = features.map { it.name }
}
@Serializable
public data class UsesSdkV2(
@@ -117,7 +143,7 @@ public data class UsesSdkV2(
)
@Serializable
public data class SignatureV2(
public data class SignerV2(
val sha256: List<String>,
val hasMultipleSigners: Boolean = false,
)

View File

@@ -17,7 +17,7 @@ import org.fdroid.index.v2.PermissionV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
import org.fdroid.index.v2.Screenshots
import org.fdroid.index.v2.SignatureV2
import org.fdroid.index.v2.SignerV2
import org.fdroid.index.v2.UsesSdkV2
internal const val LOCALE = "en-US"
@@ -72,7 +72,7 @@ internal fun PackageVersionV2.v1compat() = copy(
src = src?.v1compat(),
manifest = manifest.copy(
signer = if (manifest.signer?.sha256?.size ?: 0 <= 1) manifest.signer else {
SignatureV2(manifest.signer?.sha256?.subList(0, 1) ?: error(""))
SignerV2(manifest.signer?.sha256?.subList(0, 1) ?: error(""))
}
),
releaseChannels = releaseChannels.filter { it == RELEASE_CHANNEL_BETA },
@@ -245,7 +245,7 @@ internal object TestDataMidV2 {
targetSdkVersion = 32,
),
maxSdkVersion = 4568,
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf("824a109b2352138c3699760e1683385d0ed50ce526fc7982f8d65757743374bf"),
),
),
@@ -315,7 +315,7 @@ internal object TestDataMidV2 {
minSdkVersion = 22,
targetSdkVersion = 25,
),
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
),
usesPermission = listOf(
@@ -366,7 +366,7 @@ internal object TestDataMidV2 {
minSdkVersion = 22,
targetSdkVersion = 25,
),
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
),
usesPermission = listOf(
@@ -416,7 +416,7 @@ internal object TestDataMidV2 {
minSdkVersion = 22,
targetSdkVersion = 25,
),
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
),
usesPermission = listOf(
@@ -463,7 +463,7 @@ internal object TestDataMidV2 {
minSdkVersion = 22,
targetSdkVersion = 25,
),
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
),
usesPermission = listOf(
@@ -510,7 +510,7 @@ internal object TestDataMidV2 {
minSdkVersion = 22,
targetSdkVersion = 25,
),
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
),
usesPermission = listOf(
@@ -839,7 +839,7 @@ internal object TestDataMaxV2 {
minSdkVersion = 22,
targetSdkVersion = 25,
),
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf("43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"),
),
usesPermission = listOf(
@@ -999,7 +999,7 @@ internal object TestDataMaxV2 {
targetSdkVersion = 25,
),
maxSdkVersion = Int.MAX_VALUE,
signer = SignatureV2(
signer = SignerV2(
sha256 = listOf(
"43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab",
"33238d512c1e3eb2d6569f4a3bfbf5523418b22e0a3ed1552770abb9a9c9ccvb",

View File

@@ -5,7 +5,7 @@ import org.fdroid.index.v2.FileV1
import org.fdroid.index.v2.ManifestV2
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.index.v2.PermissionV2
import org.fdroid.index.v2.SignatureV2
import org.fdroid.index.v2.SignerV2
import org.fdroid.index.v2.UsesSdkV2
import org.fdroid.test.TestRepoUtils.getRandomFileV2
import org.fdroid.test.TestRepoUtils.getRandomLocalizedTextV2
@@ -37,7 +37,7 @@ internal object TestVersionUtils {
targetSdkVersion = Random.nextInt(),
),
maxSdkVersion = Random.nextInt().orNull(),
signer = SignatureV2(getRandomList(Random.nextInt(1, 3)) {
signer = SignerV2(getRandomList(Random.nextInt(1, 3)) {
getRandomString(64)
}).orNull(),
usesPermission = getRandomList {