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
↓ peach-android-6bc0390a.tar.gzatlas at this save
files changed4 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 −20app/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"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? {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)}"}}}