mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix: resolve relative URLs against a pathless base on Android
Android's java.net.URI resolves a relative reference against a base with an empty path by gluing it onto the host: `y18.svg` against `https://news.ycombinator.com` became `https://news.ycombinator.comy18.svg`, so Hacker News' compact link preview never loaded its icon (seen on an SM-T220). The JDK follows RFC 3986 and treats the empty path as `/`, so normalize the base to that before resolving. Rebuilt from the raw parts so an encoded query is not encoded twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ff15246c7b
commit
3d9c5bf0c9
+21
-1
@@ -29,7 +29,7 @@ actual fun resolveHttpUrl(
|
||||
reference: String,
|
||||
): String? =
|
||||
runCatching {
|
||||
val baseUri = base?.let { runCatching { URI(it).toURL().toURI() }.getOrNull() }
|
||||
val baseUri = base?.let { runCatching { URI(it).toURL().toURI().withRootPath() }.getOrNull() }
|
||||
val resolved = if (baseUri != null) baseUri.resolve(reference) else URI(reference)
|
||||
if (!resolved.scheme.equals("http", ignoreCase = true) &&
|
||||
!resolved.scheme.equals("https", ignoreCase = true)
|
||||
@@ -39,3 +39,23 @@ actual fun resolveHttpUrl(
|
||||
resolved.toURL().toString()
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
/**
|
||||
* `https://host` as `https://host/`. RFC 3986 resolves a relative reference against an empty base path
|
||||
* as if the path were `/`, and the JDK does, but Android's `java.net.URI` does not: it glued
|
||||
* `y18.svg` onto `https://news.ycombinator.com` as `https://news.ycombinator.comy18.svg`, so the
|
||||
* page's icon (or a relative og:image) pointed at a host that does not exist.
|
||||
*/
|
||||
private fun URI.withRootPath(): URI =
|
||||
if (isOpaque || !rawPath.isNullOrEmpty() || rawAuthority == null) {
|
||||
this
|
||||
} else {
|
||||
// From the raw (still-encoded) parts: the multi-argument constructor would encode them again.
|
||||
URI(
|
||||
buildString {
|
||||
append(scheme).append("://").append(rawAuthority).append('/')
|
||||
rawQuery?.let { append('?').append(it) }
|
||||
rawFragment?.let { append('#').append(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.util
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* A relative reference against a base with no path resolves as if the path were `/` (RFC 3986).
|
||||
* Android's `java.net.URI` got this wrong — `y18.svg` against `https://news.ycombinator.com` became
|
||||
* `https://news.ycombinator.comy18.svg` — so the base is normalized before resolving.
|
||||
*/
|
||||
class HttpUrlResolutionTest {
|
||||
@Test
|
||||
fun relativeReferenceAgainstAPathlessBase() {
|
||||
assertEquals("https://news.ycombinator.com/y18.svg", resolveHttpUrl("https://news.ycombinator.com", "y18.svg"))
|
||||
assertEquals("https://news.ycombinator.com/favicon.ico", resolveHttpUrl("https://news.ycombinator.com", "/favicon.ico"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pathlessBaseKeepsItsQueryAndEncoding() {
|
||||
assertEquals("https://a.example/icon.png", resolveHttpUrl("https://a.example?q=a%20b", "icon.png"))
|
||||
// Encoded characters are carried over as they are, not encoded a second time.
|
||||
assertEquals("https://a.example/icons/a%20b.png", resolveHttpUrl("https://a.example?q=a%20b", "icons/a%20b.png"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun basesWithAPathAreUnchanged() {
|
||||
assertEquals("https://a.example/dir/icon.png", resolveHttpUrl("https://a.example/dir/page.html", "icon.png"))
|
||||
assertEquals("https://cdn.example/x.png", resolveHttpUrl("https://a.example/dir/", "//cdn.example/x.png"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonHttpStaysRefused() {
|
||||
assertNull(resolveHttpUrl("https://a.example", "file:///etc/passwd"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user