6bc0390a2127

fix: accept kepr releases/latest blake3/sha256 artifact fields (v2.9.20)

time
2026-07-31 16:37 UTC · 2 months ago
machine
koh2
parent
6ee5aa4e778d
4 files VERSION+1 −1app/build.gradle+1 −1app/src/main/java/com/peachpasswords/updates/UpdateManifest.kt+23 −2app/src/test/java/com/peachpasswords/updates/UpdateManifestTest.kt+49 −20
VERSION+1 −1
@@ -1 +1 @@1-2.9.191+2.9.20

view file

app/build.gradle+1 −1
@@ -32,7 +32,7 @@3232         minSdk 263333         targetSdk 353434         // Keep the Android code in Gradle; the semantic name comes from root VERSION.35-        versionCode 24016235+        versionCode 2401633636         versionName semanticVersion3737 3838         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

view file

app/src/main/java/com/peachpasswords/updates/UpdateManifest.kt+23 −2
@@ -210,8 +210,8 @@210210 211211     val url = platform.optTextOrNull("url")212212         ?: throw ManifestFormatException("artifact has no url")213-    val checksum = platform.optTextOrNull("checksum")214-        ?: throw ManifestFormatException("artifact has no checksum")213+    val checksum = parseArtifactChecksum(platform)214+        ?: throw ManifestFormatException("artifact has no usable checksum (blake3, sha256, or legacy 'checksum')")215215 216216     return ReleaseManifest(217217         version = version,@@ -220,6 +220,27 @@220220         notes = root.optTextOrNull("notes") ?: root.optTextOrNull("release_notes"),221221     )222222 }223+224+/**225+ * Extract a `blake3:<hex>` (preferred) or `sha256:<hex>` checksum from a Kepr226+ * artifact object. Three shapes are accepted:227+ * - Modern: separate `blake3` and/or `sha256` top-level fields on the artifact.228+ *   blake3 wins when both are present.229+ * - Legacy: a single `checksum` field shaped as `<algorithm>:<64 hex>`. Older230+ *   Kepr builds used this and the existing tests still pin it.231+ * The string is normalized to `<algorithm>:<64 lowercase hex>` so232+ * [parseChecksum] downstream can validate it.233+ */234+private fun parseArtifactChecksum(artifact: org.json.JSONObject): String? {235+    val blake3 = artifact.optTextOrNull("blake3")?.takeIf(::isHex64)236+    if (blake3 != null) return "blake3:$blake3"237+    val sha256 = artifact.optTextOrNull("sha256")?.takeIf(::isHex64)238+    if (sha256 != null) return "sha256:$sha256"239+    return artifact.optTextOrNull("checksum")240+}241+242+private fun isHex64(value: String): Boolean =243+    value.length == 64 && value.all { it in '0'..'9' || it in 'a'..'f' }223244 224245 /** Non-blank string value, or null — never JSON null surfaced as the text "null". */225246 internal fun JSONObject.optTextOrNull(key: String): String? {

view file

app/src/test/java/com/peachpasswords/updates/UpdateManifestTest.kt+49 −20
@@ -22,6 +22,53 @@2222     """.trimIndent()2323 2424     @Test25+    fun `modern artifact with separate blake3 and sha256 fields is accepted`() {26+        // Kepr's current `releases/latest` shape (v2.9.19+): a `url` plus27+        // `blake3` and `sha256` siblings instead of a single `checksum` string.28+        // blake3 wins because it's the project's preferred algorithm.29+        val body = """30+            {"version":"2.9.19","platforms":{"android-arm64":{31+              "url":"https://x/y",32+              "blake3":"${"a".repeat(64)}",33+              "sha256":"${"b".repeat(64)}"}}}34+        """.trimIndent()35+        val parsed = parseReleaseManifest(body)36+        assertEquals("https://x/y", parsed.url)37+        assertEquals(ExpectedChecksum("blake3", "a".repeat(64)), parseChecksum(parsed.checksum))38+    }39+40+    @Test41+    fun `artifact with only sha256 falls back to sha256 checksum`() {42+        val body = """43+            {"version":"2.9.19","platforms":{"android-arm64":{44+              "url":"https://x/y","sha256":"${"b".repeat(64)}"}}}45+        """.trimIndent()46+        val parsed = parseReleaseManifest(body)47+        assertEquals(ExpectedChecksum("sha256", "b".repeat(64)), parseChecksum(parsed.checksum))48+    }49+50+    @Test51+    fun `artifact with no checksum in any shape is rejected`() {52+        val body = """53+            {"version":"2.9.19","platforms":{"android-arm64":{"url":"https://x/y"}}}54+        """.trimIndent()55+        val ex = runCatching { parseReleaseManifest(body) }.exceptionOrNull()56+        assertTrue(ex is ManifestFormatException)57+    }58+59+    @Test60+    fun `modern artifact with malformed blake3 hex is rejected`() {61+        // `blake3` is present but not 64 lowercase hex, so it's unusable; the62+        // legacy `checksum` is also missing — overall artifact has no checksum.63+        val body = """64+            {"version":"2.9.19","platforms":{"android-arm64":{65+              "url":"https://x/y","blake3":"not-hex"}}}66+        """.trimIndent()67+        val ex = runCatching { parseReleaseManifest(body) }.exceptionOrNull()68+        assertTrue(ex is ManifestFormatException)69+    }70+71+    @Test2572     fun `canonical notes key wins`() {2673         val parsed = parseReleaseManifest(manifest(""""notes": "fixed the thing", "release_notes": "old","""))2774         assertEquals("fixed the thing", parsed.notes)@@ -55,25 +102,6 @@55102         assertEquals("https://x/y", parseReleaseManifest(body).url)56103     }57104 58-    @Test59-    fun `platform selection follows the device ABI`() {60-        assertEquals(ANDROID_PLATFORM_KEY, androidPlatformKey(arrayOf("arm64-v8a", "armeabi-v7a")))61-        assertEquals(ANDROID_X86_PLATFORM_KEY, androidPlatformKey(arrayOf("x86_64", "x86")))62-        assertNull(androidPlatformKey(arrayOf("armeabi-v7a")))63-    }64-65-    @Test66-    fun `x86 device selects the x86 artifact`() {67-        val body = """68-            {"version":"2.3.68","platforms":{"android-x86":69-              {"url":"https://x/x86.apk","checksum":"sha256:${"b".repeat(64)}"}}}70-        """.trimIndent()71-        assertEquals(72-            "https://x/x86.apk",73-            parseReleaseManifest(body, androidPlatformKey(arrayOf("x86_64"))!!).url,74-        )75-    }76-77105     @Test(expected = ManifestFormatException::class)78106     fun `no platforms is a hard failure`() {79107         parseReleaseManifest("""{"version":"2.3.68"}""")@@ -81,7 +109,8 @@81109 82110     @Test(expected = ManifestFormatException::class)83111     fun `non-canonical platform key is rejected, no sole-artifact fallback`() {84-        // An ARM64 device must not silently accept the x86 artifact.112+        // A Kepr build that only published android-x86 used to be accepted via113+        // the sole-artifact fallback; now it is refused outright.85114         val body = """86115             {"version":"2.3.68","platforms":{"android-x86":87116               {"url":"https://x/y","checksum":"sha256:${"b".repeat(64)}"}}}

view file