diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/IndexParser.kt b/core/data/src/main/java/com/looker/core/data/fdroid/IndexParser.kt new file mode 100644 index 000000000..2e908fe4d --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/IndexParser.kt @@ -0,0 +1,43 @@ +package com.looker.core.data.fdroid + +import com.looker.core.data.fdroid.model.v1.IndexV1 +import com.looker.core.data.fdroid.model.v2.Entry +import com.looker.core.data.fdroid.model.v2.IndexV2 +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +object IndexParser { + + @Volatile + private var JSON: Json? = null + + /** + * Initializing [Json] is expensive, so using this method is preferable as it keeps returning + * a single instance with the recommended settings. + */ + val json: Json + @JvmStatic + get() { + return JSON ?: synchronized(this) { + Json { + ignoreUnknownKeys = true + } + } + } + + @JvmStatic + fun parseV1(str: String): IndexV1 { + return json.decodeFromString(str) + } + + @JvmStatic + fun parseV2(str: String): IndexV2 { + return json.decodeFromString(str) + } + + @JvmStatic + fun parseEntry(str: String): Entry { + return json.decodeFromString(str) + } + +} diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/IndexFile.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/IndexFile.kt new file mode 100644 index 000000000..b4e0f76ab --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/IndexFile.kt @@ -0,0 +1,10 @@ +package com.looker.core.data.fdroid.model + +interface IndexFile { + val name: String + val sha256: String? + val size: Long? + val ipfsCidV1: String? + + suspend fun serialize(): String +} diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v1/AppDto.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v1/AppV1.kt similarity index 100% rename from core/data/src/main/java/com/looker/core/data/fdroid/model/v1/AppDto.kt rename to core/data/src/main/java/com/looker/core/data/fdroid/model/v1/AppV1.kt diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v1/PackageDto.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v1/PackageV1.kt similarity index 100% rename from core/data/src/main/java/com/looker/core/data/fdroid/model/v1/PackageDto.kt rename to core/data/src/main/java/com/looker/core/data/fdroid/model/v1/PackageV1.kt diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v1/RepoDto.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v1/RepoV1.kt similarity index 100% rename from core/data/src/main/java/com/looker/core/data/fdroid/model/v1/RepoDto.kt rename to core/data/src/main/java/com/looker/core/data/fdroid/model/v1/RepoV1.kt diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/Entry.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/Entry.kt new file mode 100644 index 000000000..34f86234e --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/Entry.kt @@ -0,0 +1,47 @@ +package com.looker.core.data.fdroid.model.v2 + +import com.looker.core.data.fdroid.IndexParser.json +import com.looker.core.data.fdroid.model.IndexFile +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString + +@Serializable +data class Entry( + val timestamp: Long, + val version: Long, + val maxAge: Int? = null, + val index: EntryFileV2, + val diffs: Map = emptyMap(), +) { + /** + * @return the diff for the given [timestamp] or null if none exists + * in which case the full [index] should be used. + */ + fun getDiff(timestamp: Long): EntryFileV2? { + return diffs[timestamp.toString()] + } +} + + +@Serializable +data class EntryFileV2( + override val name: String, + override val sha256: String, + override val size: Long, + @SerialName("ipfsCIDv1") + override val ipfsCidV1: String? = null, + val numPackages: Int, +) : IndexFile { + companion object { + fun deserialize(string: String): EntryFileV2 { + return json.decodeFromString(string) + } + } + + override suspend fun serialize(): String { + return json.encodeToString(this) + } +} + diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/FileV2.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/FileV2.kt new file mode 100644 index 000000000..bbe44a9bd --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/FileV2.kt @@ -0,0 +1,63 @@ +package com.looker.core.data.fdroid.model.v2 + +import com.looker.core.data.fdroid.IndexParser.json +import com.looker.core.data.fdroid.model.IndexFile +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString + + +@Serializable +data class FileV2( + override val name: String, + override val sha256: String? = null, + override val size: Long? = null, + @SerialName("ipfsCIDv1") + override val ipfsCidV1: String? = null, +) : IndexFile { + companion object { + @JvmStatic + fun deserialize(string: String?): FileV2? { + if (string == null) return null + return json.decodeFromString(string) + } + + @JvmStatic + fun fromPath(path: String): FileV2 = FileV2(path) + } + + override suspend fun serialize(): String { + return json.encodeToString(this) + } +} + +typealias LocalizedTextV2 = Map +typealias LocalizedFileV2 = Map +typealias LocalizedFileListV2 = Map> + +@Serializable +data class MirrorV2( + val url: String, + val location: String? = null, +) + +@Serializable +data class AntiFeatureV2( + val icon: LocalizedFileV2 = emptyMap(), + val name: LocalizedTextV2, + val description: LocalizedTextV2 = emptyMap(), +) + +@Serializable +data class CategoryV2( + val icon: LocalizedFileV2 = emptyMap(), + val name: LocalizedTextV2, + val description: LocalizedTextV2 = emptyMap(), +) + +@Serializable +data class ReleaseChannelV2( + val name: LocalizedTextV2, + val description: LocalizedTextV2 = emptyMap(), +) diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/IndexV2.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/IndexV2.kt new file mode 100644 index 000000000..eef542f46 --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/IndexV2.kt @@ -0,0 +1,14 @@ +package com.looker.core.data.fdroid.model.v2 + +import kotlinx.serialization.Serializable + +@Serializable +public data class IndexV2( + val repo: RepoV2, + val packages: Map = emptyMap(), +) { + public fun walkFiles(fileConsumer: (FileV2?) -> Unit) { + repo.walkFiles(fileConsumer) + packages.values.forEach { it.walkFiles(fileConsumer) } + } +} diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/PackageV2.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/PackageV2.kt new file mode 100644 index 000000000..cd05e0d96 --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/PackageV2.kt @@ -0,0 +1,177 @@ +package com.looker.core.data.fdroid.model.v2 + +import com.looker.core.data.fdroid.IndexParser +import com.looker.core.data.fdroid.model.IndexFile +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString + +@Serializable +data class PackageV2( + val metadata: MetadataV2, + val versions: Map = emptyMap(), +) { + fun walkFiles(fileConsumer: (FileV2?) -> Unit) { + metadata.walkFiles(fileConsumer) + versions.values.forEach { it.walkFiles(fileConsumer) } + } +} + +@Serializable +data class MetadataV2( + val name: LocalizedTextV2? = null, + val summary: LocalizedTextV2? = null, + val description: LocalizedTextV2? = null, + val added: Long, + val lastUpdated: Long, + val webSite: String? = null, + val changelog: String? = null, + val license: String? = null, + val sourceCode: String? = null, + val issueTracker: String? = null, + val translation: String? = null, + val preferredSigner: String? = null, + val categories: List = emptyList(), + val authorName: String? = null, + val authorEmail: String? = null, + val authorWebSite: String? = null, + val authorPhone: String? = null, + val donate: List = emptyList(), + val liberapayID: String? = null, + val liberapay: String? = null, + val openCollective: String? = null, + val bitcoin: String? = null, + val litecoin: String? = null, + val flattrID: String? = null, + val icon: LocalizedFileV2? = null, + val featureGraphic: LocalizedFileV2? = null, + val promoGraphic: LocalizedFileV2? = null, + val tvBanner: LocalizedFileV2? = null, + val video: LocalizedTextV2? = null, + val screenshots: Screenshots? = null, +) { + fun walkFiles(fileConsumer: (FileV2?) -> Unit) { + icon?.values?.forEach { fileConsumer(it) } + featureGraphic?.values?.forEach { fileConsumer(it) } + promoGraphic?.values?.forEach { fileConsumer(it) } + tvBanner?.values?.forEach { fileConsumer(it) } + screenshots?.phone?.values?.forEach { it.forEach(fileConsumer) } + screenshots?.sevenInch?.values?.forEach { it.forEach(fileConsumer) } + screenshots?.tenInch?.values?.forEach { it.forEach(fileConsumer) } + screenshots?.wear?.values?.forEach { it.forEach(fileConsumer) } + screenshots?.tv?.values?.forEach { it.forEach(fileConsumer) } + } +} + + +@Serializable +data class Screenshots( + val phone: LocalizedFileListV2? = null, + val sevenInch: LocalizedFileListV2? = null, + val tenInch: LocalizedFileListV2? = null, + val wear: LocalizedFileListV2? = null, + val tv: LocalizedFileListV2? = null, +) { + val isNull: Boolean + get() = phone == null && sevenInch == null && tenInch == null && wear == null && tv == null +} + +interface PackageVersion { + val versionCode: Long + val signer: SignerV2? + val releaseChannels: List? + val packageManifest: PackageManifest + val hasKnownVulnerability: Boolean +} + +const val ANTI_FEATURE_KNOWN_VULNERABILITY: String = "KnownVuln" + +@Serializable +data class PackageVersionV2( + val added: Long, + val file: FileV1, + val src: FileV2? = null, + val manifest: ManifestV2, + 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) + + fun walkFiles(fileConsumer: (FileV2?) -> Unit) { + fileConsumer(src) + } +} + +@Serializable +data class FileV1( + override val name: String, + override val sha256: String, + override val size: Long? = null, + @SerialName("ipfsCIDv1") + override val ipfsCidV1: String? = null, +) : IndexFile { + companion object { + @JvmStatic + fun deserialize(string: String?): FileV1? { + if (string == null) return null + return IndexParser.json.decodeFromString(string) + } + } + + override suspend fun serialize(): String { + return IndexParser.json.encodeToString(this) + } +} + + +interface PackageManifest { + val minSdkVersion: Int? + val maxSdkVersion: Int? + val featureNames: List? + val nativecode: List? +} + +@Serializable +data class ManifestV2( + val versionName: String, + val versionCode: Long, + val usesSdk: UsesSdkV2? = 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(), + 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 +data class UsesSdkV2( + val minSdkVersion: Int, + val targetSdkVersion: Int, +) + +@Serializable +data class SignerV2( + val sha256: List, + val hasMultipleSigners: Boolean = false, +) + +@Serializable +data class PermissionV2( + val name: String, + val maxSdkVersion: Int? = null, +) + +@Serializable +data class FeatureV2( + val name: String, +) diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/RepoV2.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/RepoV2.kt new file mode 100644 index 000000000..2d95e78fd --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/RepoV2.kt @@ -0,0 +1,23 @@ +package com.looker.core.data.fdroid.model.v2 + +import kotlinx.serialization.Serializable + +@Serializable +public data class RepoV2( + val name: LocalizedTextV2 = emptyMap(), + val icon: LocalizedFileV2 = emptyMap(), + val address: String, + val webBaseUrl: String? = null, + val description: LocalizedTextV2 = emptyMap(), + val mirrors: List = emptyList(), + val timestamp: Long, + val antiFeatures: Map = emptyMap(), + val categories: Map = emptyMap(), + val releaseChannels: Map = emptyMap(), +) { + public fun walkFiles(fileConsumer: (FileV2?) -> Unit) { + icon.values.forEach { fileConsumer(it) } + antiFeatures.values.forEach { it.icon.values.forEach { icon -> fileConsumer(icon) } } + categories.values.forEach { it.icon.values.forEach { icon -> fileConsumer(icon) } } + } +} diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/stream/IndexV2DiffStreamReceiver.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/stream/IndexV2DiffStreamReceiver.kt new file mode 100644 index 000000000..a97acf980 --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/stream/IndexV2DiffStreamReceiver.kt @@ -0,0 +1,35 @@ +package com.looker.core.data.fdroid.model.v2.stream + +import kotlinx.serialization.json.JsonObject + +interface IndexV2DiffStreamReceiver { + + /** + * Receives the diff for the [RepoV2] from the index stream. + */ + fun receiveRepoDiff(version: Long, repoJsonObject: JsonObject) + + /** + * Receives one diff for a [MetadataV2] from the index stream. + * This is called once for each package in the index diff. + * + * If the given [packageJsonObject] is null, the package should be removed. + */ + fun receivePackageMetadataDiff(packageName: String, packageJsonObject: JsonObject?) + + /** + * Receives the diff for all versions of the give n [packageName] + * as a map of versions IDs to the diff [JsonObject]. + * This is called once for each package in the index diff (if versions have changed). + * + * If an entry in the given [versionsDiffMap] is null, + * the version with that ID should be removed. + */ + fun receiveVersionsDiff(packageName: String, versionsDiffMap: Map?) + + /** + * Called when the stream has been processed to its end. + */ + fun onStreamEnded() + +} diff --git a/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/stream/IndexV2StreamReceiver.kt b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/stream/IndexV2StreamReceiver.kt new file mode 100644 index 000000000..323dfaec9 --- /dev/null +++ b/core/data/src/main/java/com/looker/core/data/fdroid/model/v2/stream/IndexV2StreamReceiver.kt @@ -0,0 +1,25 @@ +package com.looker.core.data.fdroid.model.v2.stream + +import com.looker.core.data.fdroid.model.v2.PackageV2 +import com.looker.core.data.fdroid.model.v2.RepoV2 + +interface IndexV2StreamReceiver { + + /** + * Receives the [RepoV2] from the index stream. + * Attention: This might get called after receiving packages. + */ + fun receive(repo: RepoV2, version: Long, certificate: String) + + /** + * Receives one [PackageV2] from the index stream. + * This is called once for each package in the index. + */ + fun receive(packageName: String, p: PackageV2) + + /** + * Called when the stream has been processed to its end. + */ + fun onStreamEnded() + +}