From e31e3829a64b08eede744affa39104f396f2acf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 20:31:28 +0000 Subject: [PATCH] test: add regression tests for PR #1907 URL crash fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests cover the StringIndexOutOfBoundsException in Url.getPart() that occurred when readEnd() trimmed trailing characters (e.g. ':') from a detected URL but urlMarker indices remained pointing past the trimmed string's end. - UrlMarkerTest: three cases testing PORT/QUERY index at or beyond string length (the boundary conditions fixed by minOf clamping and the startIndex >= length guard) - UrlsDetectorTest: regression for "今北産業" (no crash, 0 URLs) and for a bare "example.com:" host - UrlParserTest: end-to-end regression for "今北産業" and "example.com:" https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2 --- .../commons/richtext/UrlParserTest.kt | 24 +++++++ .../nip10Notes/urls/UrlsDetectorTest.kt | 21 ++++++ .../quartz/utils/urldetector/UrlMarkerTest.kt | 64 +++++++++++++++++++ 3 files changed, 109 insertions(+) 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 b9613c5cd..8777f5329 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 @@ -281,6 +281,30 @@ class UrlParserTest { Urls(withScheme = emptySet()), ) + /** + * Regression test for PR #1907: parsing a note whose content is only the Japanese phrase + * "今北産業" (a common internet abbreviation) must not throw a StringIndexOutOfBoundsException + * from Url.getPart() and must produce no detected URLs. + */ + @Test + fun testImakitaSangyo() = + test( + "今北産業", + Urls(), + ) + + /** + * Regression test for PR #1907: a URL that ends with ':' causes readEnd() to trim it, + * but the PORT marker index was already set to buffer.length (one past the trimmed URL). + * Accessing host/port must not throw StringIndexOutOfBoundsException. + */ + @Test + fun testUrlEndingWithColon() = + test( + "example.com:", + Urls(), + ) + @Test fun testHour() = test( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt index 0d526ab17..7e7f66d24 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt @@ -41,4 +41,25 @@ class UrlsDetectorTest { assertContains(detectedLinks, "https://mysite.xyz") assertContains(detectedLinks, "https://myblog.xyz") } + + /** + * Regression test for PR #1907: the Japanese phrase "今北産業" must not crash the URL + * detector with a StringIndexOutOfBoundsException and must return no URLs. + */ + @Test + fun doesNotCrashOnJapaneseText() { + val detectedLinks = fastFindURLs("今北産業") + assertEquals(0, detectedLinks.size) + } + + /** + * Regression test for PR #1907: a bare host ending with ':' triggers PORT marker placement + * at buffer.length, then readEnd() trims the ':', leaving PORT beyond the URL string. + * Url.getPart() must not throw StringIndexOutOfBoundsException. + */ + @Test + fun doesNotCrashOnUrlEndingWithColon() { + // Just verifying no exception is thrown; a bare "example.com:" is not a valid URL. + fastFindURLs("example.com:") + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlMarkerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlMarkerTest.kt index 660759b16..2c9bd35cb 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlMarkerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlMarkerTest.kt @@ -318,4 +318,68 @@ class UrlMarkerTest { "#?@Sdsf", intArrayOf(0, 8, 18, 33, 35, 36, 53), ) + + /** + * Regression test for PR #1907: StringIndexOutOfBoundsException in Url.getPart() when a URL + * ending with a character in CANNOT_END_URLS_WITH (e.g. ':') is trimmed by readEnd(), but the + * urlMarker PORT index was set to buffer.length (one past the last valid index). + * + * After trimming, PORT index == originalUrl.length, so getPart(PORT) must return null + * and getPart(HOST) must clamp endIndex to originalUrl.length via minOf. + * + * Simulates what happens when "example.com:" is detected: the ':' is trimmed to produce + * "example.com" but PORT marker remains at index 11 (= trimmed string length). + */ + @Test + fun testPortIndexAtStringLength() = + testUrlMarker( + "example.com", // 11 chars – simulates "example.com:" after trailing ':' is trimmed + "https", + "", + "", + "example.com", // host must not throw; endIndex clamped by minOf + -1, // PORT at index 11 == length → getPart returns null → falls back to scheme default → 443, but no scheme → -1 + "/", + "", + "", + intArrayOf(-1, -1, 0, 11, -1, -1, -1), // HOST=0, PORT=11 (= string length) + ) + + /** + * Regression test for PR #1907: PORT index beyond the end of the trimmed URL. + * Simulates a marker that points one position past the string length. + */ + @Test + fun testPortIndexBeyondStringLength() = + testUrlMarker( + "example.com", // 11 chars + "https", + "", + "", + "example.com", // host must not throw + -1, // PORT at 12 > length → getPart(PORT) returns null → -1 + "/", + "", + "", + intArrayOf(-1, -1, 0, 12, -1, -1, -1), // PORT=12 > string length 11 + ) + + /** + * Regression test for PR #1907: QUERY index at string length (simulates "example.com/path?" + * after trailing '?' is trimmed, leaving urlMarker QUERY at the trimmed string's length). + */ + @Test + fun testQueryIndexAtStringLength() = + testUrlMarker( + "example.com/path", // 16 chars – simulates "example.com/path?" after '?' trimmed + "https", + "", + "", + "example.com", + 443, + "/path", // path must not throw; endIndex clamped by minOf + "", // QUERY at 16 == length → getPart(QUERY) returns null + "", + intArrayOf(-1, -1, 0, -1, 11, 16, -1), // HOST=0, PATH=11, QUERY=16 (= string length) + ) }