fix(blossom-bridge): pick rightmost sha256 segment in URL path

CDNs like share.yabu.me serve blobs under `<cache-prefix-sha>/<blob-sha>.<ext>`
where both path segments are 64-char hex. The bridge previously locked onto the
first match (the cache prefix), dropping the blob segment from `xs` and asking
the local cache for a non-existent blob. Walking the path right-to-left makes
the rightmost sha — the one that carries the file extension — win, while the
prefix segments stay in `xs` so the cache can fetch upstream on miss.

Behaviour is unchanged when the path has a single sha; tests cover the new
two-hash layout in both the Coil-model path and the OkHttp interceptor path.
This commit is contained in:
Claude
2026-05-13 10:11:32 +00:00
parent f288f2d921
commit 1b287baaf2
4 changed files with 50 additions and 4 deletions
@@ -72,8 +72,13 @@ class LocalBlossomCacheRedirectInterceptor(
} }
private fun findSha256AndExtensionInPath(url: HttpUrl): Triple<Int, String, String>? { private fun findSha256AndExtensionInPath(url: HttpUrl): Triple<Int, String, String>? {
url.pathSegments.forEachIndexed { index, segment -> // Walk segments right-to-left so CDNs that put a cache prefix (itself a
val match = SHA256_SEGMENT_REGEX.find(segment) ?: return@forEachIndexed // 64-char hex segment) ahead of the blob hash — e.g.
// `https://share.yabu.me/<prefix>/<sha>.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 sha = match.value.lowercase()
val ext = guessExtensionFrom(segment, sha) ?: "bin" val ext = guessExtensionFrom(segment, sha) ?: "bin"
return Triple(index, sha, ext) return Triple(index, sha, ext)
@@ -132,6 +132,21 @@ class LocalBlossomCacheRedirectInterceptorTest {
response.close() response.close()
} }
@Test
fun bridgeOnPicksRightmostShaWhenPathHasTwoHashes() {
// share.yabu.me layout: <cache-prefix-sha>/<blob-sha>.<ext>
val prefix = "84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5"
val blob = "28fa4d999af6ae3e4e11bfc2727130ef1b3a13cc0f981e5a93c3996cb2f524e5"
val interceptor = LocalBlossomCacheRedirectInterceptor { true }
val captured = mutableListOf<String>()
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( private fun fakeChain(
url: String, url: String,
captured: MutableList<String>, captured: MutableList<String>,
@@ -145,7 +145,10 @@ private fun percentEncode(input: String): String {
private fun extractSha256FromUrlPath(url: String): String? { private fun extractSha256FromUrlPath(url: String): String? {
val pathPart = url.substringBefore('?').substringBefore('#') 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/<prefix>/<sha>.webp` — still resolve to the blob.
val match = sha256InPathRegex.findAll(pathPart).lastOrNull() ?: return null
return match.value.lowercase() return match.value.lowercase()
} }
@@ -189,7 +192,7 @@ private fun extractServerBase(
val hostStart = schemeEnd + 3 val hostStart = schemeEnd + 3
if (hostStart >= pathPart.length) return null if (hostStart >= pathPart.length) return null
val shaIndex = pathPart.indexOf(sha, ignoreCase = true) val shaIndex = pathPart.lastIndexOf(sha, ignoreCase = true)
if (shaIndex >= 0) { if (shaIndex >= 0) {
// Anchor on the slash immediately preceding the sha so the cache // Anchor on the slash immediately preceding the sha so the cache
// can append "/<sha>" verbatim per the local-blossom-cache spec. // can append "/<sha>" verbatim per the local-blossom-cache spec.
@@ -173,4 +173,27 @@ class MediaUrlContentExtTest {
val uri = "blossom:$sha.jpg?xs=https://nostr.build" val uri = "blossom:$sha.jpg?xs=https://nostr.build"
assertEquals(uri, bridgeProfilePictureUrl(uri, useBridge = true)) assertEquals(uri, bridgeProfilePictureUrl(uri, useBridge = true))
} }
@Test
fun bridgeOnPicksRightmostShaWhenPathHasTwoHashes() {
// share.yabu.me layout: <cache-prefix-sha>/<blob-sha>.<ext>
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),
)
}
} }