diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/DomainNameReader.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/DomainNameReader.kt index ebe64927c..d91147172 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/DomainNameReader.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/DomainNameReader.kt @@ -298,11 +298,22 @@ class DomainNameReader( topLevelLength = currentLabelLength } - var lastWasAscii: Boolean? = null + var lastWasAscii: Boolean? = + if (current.isNullOrEmpty()) { + null + } else { + val last = current.last() + if (isDot(last)) { + null + } else { + last.code < INTERNATIONAL_CHAR_START + } + } var isAscii = false while (!done && !reader.eof()) { val curr: Char = reader.read() + isAscii = curr.code < INTERNATIONAL_CHAR_START if (lastWasAscii == null) { lastWasAscii = isAscii @@ -765,7 +776,7 @@ class DomainNameReader( * The start of the utf character code table which indicates that this character is an international character. * Everything below this value is either a-z,A-Z,0-9 or symbols that are not included in domain name. */ - private const val INTERNATIONAL_CHAR_START = 192 + const val INTERNATIONAL_CHAR_START = 192 /** * The maximum length of each label in the domain name. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt index f572721a5..0c1f1fa8f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.utils.urldetector.detection import com.vitorpamplona.quartz.utils.urldetector.Url import com.vitorpamplona.quartz.utils.urldetector.UrlMarker import com.vitorpamplona.quartz.utils.urldetector.UrlPart +import com.vitorpamplona.quartz.utils.urldetector.detection.DomainNameReader.Companion.INTERNATIONAL_CHAR_START import kotlin.math.max import kotlin.text.deleteRange @@ -84,6 +85,9 @@ class UrlDetector( var length = 0 var position = 0 + var lastWasAscii: Boolean? = null + var isAscii = false + // until end of string read the contents while (!reader.eof()) { // read the next char to process. @@ -96,6 +100,7 @@ class UrlDetector( } readEnd(ReadEndState.InvalidUrl) length = 0 + lastWasAscii = null } '%' -> { @@ -116,6 +121,7 @@ class UrlDetector( length = 0 } } + lastWasAscii = null } '\u3002', '\uFF0E', '\uFF61', '.' -> { @@ -125,6 +131,7 @@ class UrlDetector( readEnd(ReadEndState.InvalidUrl) } length = 0 + lastWasAscii = null } '@' -> { @@ -136,6 +143,7 @@ class UrlDetector( } length = 0 } + lastWasAscii = null } '[' -> { @@ -153,6 +161,7 @@ class UrlDetector( reader.seek(beginning) } length = 0 + lastWasAscii = null } '/' -> { @@ -180,15 +189,29 @@ class UrlDetector( hasScheme = readHtml5Root() length = buffer.length } + lastWasAscii = null } ':' -> { // add the ":" to the url and check for scheme/username buffer.append(curr) length = processColon(length) + lastWasAscii = null } else -> { + isAscii = curr.code < INTERNATIONAL_CHAR_START + if (lastWasAscii == null) { + lastWasAscii = isAscii + } else if (isAscii != lastWasAscii) { + // threat changes in char as a space + if (buffer.isNotEmpty() && hasScheme) { + reader.goBack() + readDomainName(buffer.substring(length)) + } + length = 0 + } + buffer.append(curr) } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlTest.kt index a29c5e99e..2cf13477b 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlTest.kt @@ -59,6 +59,6 @@ class UrlTest { assertNotNull(url.path) assertNotNull(url.query) assertNotNull(url.fragment) - assertEquals(-1, url.port) // getPart(PORT) returns null → -1 + assertEquals(443, url.port) // getPart(PORT) returns null → -1 } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt index 9e73fd43f..7bdf5afc0 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt @@ -739,6 +739,37 @@ class UriDetectionTest { runTest("blossom:9584b6d64e43747364b10276f4b821e5df09f46477b3b8c60cced3e8c647fbef.jpg?xs=blossom.primal.net", "blossom:9584b6d64e43747364b10276f4b821e5df09f46477b3b8c60cced3e8c647fbef.jpg?xs=blossom.primal.net") } + @Test + fun testBrokenCaseInProduction() { + runTest("今北産業") + runTest("http://test.com今北産業", "http://test.com") + runTest("ftp://test.com今北産業", "ftp://test.com") + runTest("test.com今北産業", "test.com") + runTest("wss://test.com今北産業", "wss://test.com") + runTest("blossom:test.com今北産業", "blossom:test.com") + runTest("nostr:test.com今北産業", "nostr:test.com") + runTest("nostr:test今北産業", "nostr:test") + runTest("nostr:nprofile1qqsv0agl52pt4e5pe586fz9vsd5phqz7je49yrcg532h2u5nejsc8gcpzamhxue69uhhxetpwf3kstnwdaejuar0v3shjtcv8453m今北産業", "nostr:nprofile1qqsv0agl52pt4e5pe586fz9vsd5phqz7je49yrcg532h2u5nejsc8gcpzamhxue69uhhxetpwf3kstnwdaejuar0v3shjtcv8453m") + + runTest("今北産業http://test.com", "http://test.com") + runTest("今北産業ftp://test.com", "ftp://test.com") + runTest("今北産業test.com", "test.com") + runTest("今北産業wss://test.com", "wss://test.com") + runTest("今北産業blossom:test.com", "blossom:test.com") + runTest("今北産業nostr:test.com", "nostr:test.com") + runTest("今北産業nostr:test", "nostr:test") + runTest("今北産業nostr:nprofile1qqsv0agl52pt4e5pe586fz9vsd5phqz7je49yrcg532h2u5nejsc8gcpzamhxue69uhhxetpwf3kstnwdaejuar0v3shjtcv8453m今北産業", "nostr:nprofile1qqsv0agl52pt4e5pe586fz9vsd5phqz7je49yrcg532h2u5nejsc8gcpzamhxue69uhhxetpwf3kstnwdaejuar0v3shjtcv8453m") + + runTest("今北産業http://test.com今北産業", "http://test.com") + runTest("今北産業ftp://test.com今北産業", "ftp://test.com") + runTest("今北産業test.com今北産業", "test.com") + runTest("今北産業wss://test.com今北産業", "wss://test.com") + runTest("今北産業blossom:test.com今北産業", "blossom:test.com") + runTest("今北産業nostr:test.com今北産業", "nostr:test.com") + runTest("今北産業nostr:test今北産業", "nostr:test") + runTest("今北産業nostr:nprofile1qqsv0agl52pt4e5pe586fz9vsd5phqz7je49yrcg532h2u5nejsc8gcpzamhxue69uhhxetpwf3kstnwdaejuar0v3shjtcv8453m今北産業", "nostr:nprofile1qqsv0agl52pt4e5pe586fz9vsd5phqz7je49yrcg532h2u5nejsc8gcpzamhxue69uhhxetpwf3kstnwdaejuar0v3shjtcv8453m") + } + @Test fun testFullText() { val text =