diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptor.kt index 1f6afef7e..3dc9b7229 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptor.kt @@ -72,8 +72,13 @@ class LocalBlossomCacheRedirectInterceptor( } private fun findSha256AndExtensionInPath(url: HttpUrl): Triple? { - url.pathSegments.forEachIndexed { index, segment -> - val match = SHA256_SEGMENT_REGEX.find(segment) ?: return@forEachIndexed + // Walk segments right-to-left so CDNs that put a cache prefix (itself a + // 64-char hex segment) ahead of the blob hash — e.g. + // `https://share.yabu.me//.webp` — still resolve to the + // blob and leave the prefix in `xs`. + for (index in url.pathSegments.indices.reversed()) { + val segment = url.pathSegments[index] + val match = SHA256_SEGMENT_REGEX.find(segment) ?: continue val sha = match.value.lowercase() val ext = guessExtensionFrom(segment, sha) ?: "bin" return Triple(index, sha, ext) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt index afaff2dcf..489b1b241 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt @@ -132,6 +132,21 @@ class LocalBlossomCacheRedirectInterceptorTest { response.close() } + @Test + fun bridgeOnPicksRightmostShaWhenPathHasTwoHashes() { + // share.yabu.me layout: /. + val prefix = "84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5" + val blob = "28fa4d999af6ae3e4e11bfc2727130ef1b3a13cc0f981e5a93c3996cb2f524e5" + val interceptor = LocalBlossomCacheRedirectInterceptor { true } + val captured = mutableListOf() + val response = interceptor.intercept(fakeChain("https://share.yabu.me/$prefix/$blob.webp", captured)) + assertEquals( + "http://127.0.0.1:24242/$blob.webp?xs=https%3A%2F%2Fshare.yabu.me%2F$prefix", + captured.single(), + ) + response.close() + } + private fun fakeChain( url: String, captured: MutableList, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt index 78d121b6f..6f8d6ce74 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt @@ -145,7 +145,10 @@ private fun percentEncode(input: String): String { private fun extractSha256FromUrlPath(url: String): String? { val pathPart = url.substringBefore('?').substringBefore('#') - val match = sha256InPathRegex.find(pathPart) ?: return null + // Prefer the rightmost 64-char hex segment so CDNs that put a cache prefix + // (itself a 64-char hex) ahead of the blob hash — e.g. + // `https://share.yabu.me//.webp` — still resolve to the blob. + val match = sha256InPathRegex.findAll(pathPart).lastOrNull() ?: return null return match.value.lowercase() } @@ -189,7 +192,7 @@ private fun extractServerBase( val hostStart = schemeEnd + 3 if (hostStart >= pathPart.length) return null - val shaIndex = pathPart.indexOf(sha, ignoreCase = true) + val shaIndex = pathPart.lastIndexOf(sha, ignoreCase = true) if (shaIndex >= 0) { // Anchor on the slash immediately preceding the sha so the cache // can append "/" verbatim per the local-blossom-cache spec. diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt index db8240220..853dad9b5 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt @@ -173,4 +173,27 @@ class MediaUrlContentExtTest { val uri = "blossom:$sha.jpg?xs=https://nostr.build" assertEquals(uri, bridgeProfilePictureUrl(uri, useBridge = true)) } + + @Test + fun bridgeOnPicksRightmostShaWhenPathHasTwoHashes() { + // share.yabu.me layout: /. + val prefix = "84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5" + val blob = "28fa4d999af6ae3e4e11bfc2727130ef1b3a13cc0f981e5a93c3996cb2f524e5" + val image = MediaUrlImage(url = "https://share.yabu.me/$prefix/$blob.webp", hash = null) + assertEquals( + "blossom:$blob.webp?xs=https://share.yabu.me/$prefix", + image.toCoilModel(useLocalBlossomBridge = true), + ) + } + + @Test + fun bridgeProfilePictureUrlPicksRightmostShaWhenPathHasTwoHashes() { + val prefix = "84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5" + val blob = "28fa4d999af6ae3e4e11bfc2727130ef1b3a13cc0f981e5a93c3996cb2f524e5" + val url = "https://share.yabu.me/$prefix/$blob.webp" + assertEquals( + "http://127.0.0.1:24242/$blob.webp?xs=https%3A%2F%2Fshare.yabu.me%2F$prefix", + bridgeProfilePictureUrl(url, useBridge = true), + ) + } }