diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 67c21b387..387a79787 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3774,6 +3774,11 @@ + + + + + @@ -3784,31 +3789,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4075,6 +4115,16 @@ + + + + + + + + + + @@ -4083,6 +4133,11 @@ + + + + + @@ -5367,6 +5422,11 @@ + + + + + diff --git a/index/build.gradle b/index/build.gradle index ab946f853..1afc4b850 100644 --- a/index/build.gradle +++ b/index/build.gradle @@ -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 diff --git a/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt b/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt index 09c708cbe..5d4ee694c 100644 --- a/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt +++ b/index/src/androidMain/kotlin/org/fdroid/CompatibilityChecker.kt @@ -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 = SUPPORTED_ABIS, ) : CompatibilityChecker { private val features = HashMap().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 } diff --git a/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt b/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt new file mode 100644 index 000000000..85799b9c9 --- /dev/null +++ b/index/src/androidMain/kotlin/org/fdroid/UpdateChecker.kt @@ -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 +} + +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 getUpdate( + versions: List, + packageInfo: PackageInfo, + releaseChannels: List? = 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 getSuggestedVersion( + versions: List, + preferredSigner: String?, + releaseChannels: List? = 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 getUpdate( + versions: List, + allowedSignersGetter: (() -> Set?)? = null, + installedVersionCode: Long = 0, + allowedReleaseChannels: List? = 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, + versionReleaseChannels: List?, + 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 + } + +} diff --git a/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt b/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt index 68df42d82..c2a670c70 100644 --- a/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt +++ b/index/src/androidMain/kotlin/org/fdroid/index/IndexUtils.kt @@ -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 { diff --git a/index/src/androidTest/kotlin/org/fdroid/CompatibilityCheckerTest.kt b/index/src/androidTest/kotlin/org/fdroid/CompatibilityCheckerTest.kt new file mode 100644 index 000000000..9174f4c97 --- /dev/null +++ b/index/src/androidTest/kotlin/org/fdroid/CompatibilityCheckerTest.kt @@ -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? = null, + override val nativecode: List? = null, + ) : PackageManifest + +} diff --git a/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt b/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt new file mode 100644 index 000000000..55a38a1b1 --- /dev/null +++ b/index/src/androidTest/kotlin/org/fdroid/UpdateCheckerTest.kt @@ -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, + releaseChannels: List?, + ): 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? = 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? = null + override val nativecode: List? = null + }, + override val hasKnownVulnerability: Boolean = false, + ) : PackageVersion + + private data class AppPreferences( + override val ignoreVersionCodeUpdate: Long = 0, + override val releaseChannels: List = emptyList(), + ) : PackagePreference + +} diff --git a/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt b/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt index 1677b6625..573701409 100644 --- a/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt +++ b/index/src/commonMain/kotlin/org/fdroid/index/v1/PackageV1.kt @@ -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(), diff --git a/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt b/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt index 5dacdc4a2..5b196f10d 100644 --- a/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt +++ b/index/src/commonMain/kotlin/org/fdroid/index/v2/PackageV2.kt @@ -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? + 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 = emptyList(), + override val releaseChannels: List = emptyList(), val antiFeatures: Map = 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? + public val nativecode: List? +} + @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 = emptyList(), val usesPermissionSdk23: List = emptyList(), - val nativecode: List = emptyList(), + override val nativecode: List = emptyList(), val features: List = emptyList(), -) +) : PackageManifest { + override val minSdkVersion: Int? = usesSdk?.minSdkVersion + override val featureNames: List = 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, val hasMultipleSigners: Boolean = false, ) diff --git a/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt b/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt index 6e9587486..a7d6f3495 100644 --- a/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt +++ b/index/src/sharedTest/kotlin/org/fdroid/test/TestDataV2.kt @@ -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", diff --git a/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt b/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt index 7227567ea..a83a3ba5d 100644 --- a/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt +++ b/index/src/sharedTest/kotlin/org/fdroid/test/TestVersionUtils.kt @@ -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 {