From b565c37277697cd55825c559956e4588a925a76d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 15:59:32 +0000 Subject: [PATCH] fix: parse IPv6-literal URLs in post content UrlParser.parseValidUrls filtered every detected URL through isValidTopLevelDomain(), which requires the TLD's first character to be an ASCII letter. IPv6 literal hosts are bracketed (e.g. [2001:db8::1]) and have no dotted TLD, so the whole bracketed host became the candidate "TLD", starting with '[' and failing the check. As a result, valid IPv6 URLs like http://[302:68d0:f0d5:b88d::bdb]/ were dropped and rendered as plain text instead of links. The UrlDetector already validates the bracketed address as syntactically correct IPv6, so accept bracketed hosts directly in isValidTopLevelDomain. --- .../amethyst/commons/richtext/UrlParser.kt | 5 +++++ .../commons/richtext/UrlParserTest.kt | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt index 6356b2e372..04078271c9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt @@ -47,6 +47,11 @@ class UrlParser { fun Char.isAsciiLetter(): Boolean = (this in 'a'..'z' || this in 'A'..'Z') fun Url.isValidTopLevelDomain(): Boolean { + // IPv6 literal hosts are bracketed (e.g. [2001:db8::1]) and have no dotted TLD, so the + // letter-first TLD rule below would wrongly reject them. The detector already validated + // the bracketed address as a syntactically correct IPv6 literal, so accept it directly. + if (host.startsWith('[')) return true + /* According to the TLD Applicant Guidebook published June 2012, ICANN does not allow numbers in TLDs. */ diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt index 600c0f27f0..7ce6c472d3 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt @@ -300,6 +300,27 @@ class UrlParserTest { Urls(withScheme = emptySet()), ) + @Test + fun testIPv6UrlWithScheme() = + test( + "http://[302:68d0:f0d5:b88d::bdb]/08e31992c51b06d8f8ee4e40207ede11d4c69b0db2d268fb2783d45d69094c2b", + Urls(withScheme = setOf("http://[302:68d0:f0d5:b88d::bdb]/08e31992c51b06d8f8ee4e40207ede11d4c69b0db2d268fb2783d45d69094c2b")), + ) + + @Test + fun testIPv6UrlWithPort() = + test( + "http://[2001:db8::1]:8080/path", + Urls(withScheme = setOf("http://[2001:db8::1]:8080/path")), + ) + + @Test + fun testIPv6UrlInSentence() = + test( + "check this http://[2a01:5cc0:1:2::4] out", + Urls(withScheme = setOf("http://[2a01:5cc0:1:2::4]")), + ) + @Test fun testBlossom() { val blossom = "blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.satellite.earth"