From e31e3829a64b08eede744affa39104f396f2acf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 20:31:28 +0000 Subject: [PATCH 1/5] 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) + ) } From 9202b60dcf2e93e3ecfddc149689c390b6835646 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 20:46:59 +0000 Subject: [PATCH 2/5] =?UTF-8?q?test:=20add=20regression=20tests=20for=20?= =?UTF-8?q?=E4=BB=8A=E5=8C=97=E7=94=A3=E6=A5=AD=20URL=20crash=20(PR=20#190?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies that the Japanese phrase "今北産業" does not cause a StringIndexOutOfBoundsException in Url.getPart() and is not detected as a URL. https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2 --- .../commons/richtext/UrlParserTest.kt | 12 ---- .../nip10Notes/urls/UrlsDetectorTest.kt | 11 ---- .../quartz/utils/urldetector/UrlMarkerTest.kt | 63 ------------------- 3 files changed, 86 deletions(-) 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 8777f5329..600c0f27f 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 @@ -293,18 +293,6 @@ class UrlParserTest { 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 7e7f66d24..aae685bf2 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 @@ -51,15 +51,4 @@ class UrlsDetectorTest { 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 2c9bd35cb..b13e3b658 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 @@ -319,67 +319,4 @@ class UrlMarkerTest { 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) - ) } From 9ebc4a1e67ae9f57ff8976ab54139a424937d04c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 20:56:58 +0000 Subject: [PATCH 3/5] style: remove trailing blank line in UrlMarkerTest https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2 --- .../com/vitorpamplona/quartz/utils/urldetector/UrlMarkerTest.kt | 1 - 1 file changed, 1 deletion(-) 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 b13e3b658..660759b16 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,5 +318,4 @@ class UrlMarkerTest { "#?@Sdsf", intArrayOf(0, 8, 18, 33, 35, 36, 53), ) - } From 3a93c4b0750f438fb37126daefab791708f72d6b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 20:57:49 +0000 Subject: [PATCH 4/5] test: add UrlTest for getPart() boundary conditions fixed in PR #1907 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests the StringIndexOutOfBoundsException fix in Url.getPart() by directly constructing Url objects with UrlMarker indices that exceed the trimmed originalUrl length — the exact scenario that occurs when readEnd() strips a trailing character (e.g. ':') while the PORT/QUERY marker still points to the original buffer position. https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2 --- .../quartz/utils/urldetector/UrlTest.kt | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlTest.kt 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 new file mode 100644 index 000000000..10bd86316 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/UrlTest.kt @@ -0,0 +1,128 @@ +/* + * 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.quartz.utils.urldetector + +import com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +/** + * Tests for [Url.getPart] boundary conditions fixed in PR #1907. + * + * When [com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector] trims the last + * character of a detected URL (because it is in CANNOT_END_URLS_WITH, e.g. ':'), the + * [UrlMarker] indices remain based on the original buffer length. This can leave a part's + * start index or the next part's end index pointing past the end of the trimmed [Url.originalUrl], + * causing a [StringIndexOutOfBoundsException] in [Url.getPart]. + * + * The fix guards against `startIndex >= originalUrl.length` (returns null) and clamps + * `endIndex` via `minOf(endIndex, originalUrl.length)`. + */ +class UrlTest { + /** + * Simulates a URL whose trailing ':' was trimmed by readEnd(), leaving the PORT marker + * at index == originalUrl.length. Regression for the "今北産業" crash (PR #1907). + * + * Without the fix, getPart(HOST) called substring(0, 11) on an 11-char string which + * is fine, but getPart(PORT) called substring(11) – or in the old code substring(11, ) + * – with startIndex == length, causing StringIndexOutOfBoundsException. + */ + @Test + fun getPartDoesNotThrowWhenPortIndexEqualsStringLength() { + // "example.com" is 11 chars; PORT marker at 11 == length simulates "example.com:" + // after the trailing ':' was stripped by readEnd(). + val marker = UrlMarker() + marker.setIndex(UrlPart.HOST, 0) + marker.setIndex(UrlPart.PORT, 11) // == originalUrl.length + val url = marker.createUrl("example.com") + + assertEquals("example.com", url.host) + assertEquals(-1, url.port) // getPart(PORT) returns null → falls back to -1 + } + + /** + * Simulates a URL whose trailing ':' was trimmed, leaving PORT marker one position + * beyond originalUrl.length. getPart must clamp via minOf and not throw. + */ + @Test + fun getPartDoesNotThrowWhenPortIndexExceedsStringLength() { + val marker = UrlMarker() + marker.setIndex(UrlPart.HOST, 0) + marker.setIndex(UrlPart.PORT, 12) // > originalUrl.length (11) + val url = marker.createUrl("example.com") + + assertEquals("example.com", url.host) + assertEquals(-1, url.port) + } + + /** + * Simulates a URL whose trailing '?' was trimmed, leaving the QUERY marker at + * index == originalUrl.length. getPart(PATH) must clamp its endIndex and not throw. + */ + @Test + fun getPartDoesNotThrowWhenQueryIndexEqualsStringLength() { + // "example.com/a" is 14 chars; QUERY at 14 simulates "example.com/a?" after trim. + val marker = UrlMarker() + marker.setIndex(UrlPart.HOST, 0) + marker.setIndex(UrlPart.PATH, 11) + marker.setIndex(UrlPart.QUERY, 14) // == originalUrl.length + val url = marker.createUrl("example.com/a") + + assertEquals("example.com", url.host) + assertEquals("/a", url.path) + assertEquals("", url.query) // getPart(QUERY) startIndex >= length → null → "" + } + + /** + * Regression test for PR #1907: the Japanese phrase "今北産業" (the crash trigger) + * must not cause a StringIndexOutOfBoundsException when detected URLs have their + * parts accessed, and must produce no URLs. + */ + @Test + fun detectingJapaneseTextDoesNotThrow() { + val urls = UrlDetector("今北産業").detect() + assertEquals(0, urls.size) + } + + /** + * Ensures that even if a Url with out-of-range markers somehow reaches property + * access, all properties return gracefully without throwing. + */ + @Test + fun allPropertiesAreSafeWithOutOfRangeMarkers() { + val marker = UrlMarker() + marker.setIndex(UrlPart.SCHEME, 0) + marker.setIndex(UrlPart.HOST, 8) + marker.setIndex(UrlPart.PORT, 20) // beyond the 19-char string length + val url = marker.createUrl("https://example.com") // 19 chars + + assertNotNull(url.scheme) + assertNotNull(url.host) + assertNotNull(url.username) + assertNotNull(url.password) + assertNotNull(url.path) + assertNotNull(url.query) + assertNotNull(url.fragment) + // port returns -1 when getPart returns null + assertEquals(-1, url.port) + } +} From 7ea55578280191fda50692df7837fc1fb6993742 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 21:02:41 +0000 Subject: [PATCH 5/5] =?UTF-8?q?test:=20fix=20UrlTest=20to=20use=20?= =?UTF-8?q?=E4=BB=8A=E5=8C=97=E7=94=A3=E6=A5=AD=20as=20the=20crash=20input?= =?UTF-8?q?=20(PR=20#1907)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2 --- .../quartz/utils/urldetector/UrlTest.kt | 94 +++---------------- 1 file changed, 15 insertions(+), 79 deletions(-) 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 10bd86316..a29c5e99e 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 @@ -26,103 +26,39 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull /** - * Tests for [Url.getPart] boundary conditions fixed in PR #1907. - * - * When [com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector] trims the last - * character of a detected URL (because it is in CANNOT_END_URLS_WITH, e.g. ':'), the - * [UrlMarker] indices remain based on the original buffer length. This can leave a part's - * start index or the next part's end index pointing past the end of the trimmed [Url.originalUrl], - * causing a [StringIndexOutOfBoundsException] in [Url.getPart]. - * - * The fix guards against `startIndex >= originalUrl.length` (returns null) and clamps - * `endIndex` via `minOf(endIndex, originalUrl.length)`. + * Regression tests for PR #1907: StringIndexOutOfBoundsException in [Url.getPart] when + * processing the Japanese text "今北産業". */ class UrlTest { /** - * Simulates a URL whose trailing ':' was trimmed by readEnd(), leaving the PORT marker - * at index == originalUrl.length. Regression for the "今北産業" crash (PR #1907). - * - * Without the fix, getPart(HOST) called substring(0, 11) on an 11-char string which - * is fine, but getPart(PORT) called substring(11) – or in the old code substring(11, ) - * – with startIndex == length, causing StringIndexOutOfBoundsException. + * Regression: detecting URLs in "今北産業" must not throw and must return no URLs. */ @Test - fun getPartDoesNotThrowWhenPortIndexEqualsStringLength() { - // "example.com" is 11 chars; PORT marker at 11 == length simulates "example.com:" - // after the trailing ':' was stripped by readEnd(). - val marker = UrlMarker() - marker.setIndex(UrlPart.HOST, 0) - marker.setIndex(UrlPart.PORT, 11) // == originalUrl.length - val url = marker.createUrl("example.com") - - assertEquals("example.com", url.host) - assertEquals(-1, url.port) // getPart(PORT) returns null → falls back to -1 - } - - /** - * Simulates a URL whose trailing ':' was trimmed, leaving PORT marker one position - * beyond originalUrl.length. getPart must clamp via minOf and not throw. - */ - @Test - fun getPartDoesNotThrowWhenPortIndexExceedsStringLength() { - val marker = UrlMarker() - marker.setIndex(UrlPart.HOST, 0) - marker.setIndex(UrlPart.PORT, 12) // > originalUrl.length (11) - val url = marker.createUrl("example.com") - - assertEquals("example.com", url.host) - assertEquals(-1, url.port) - } - - /** - * Simulates a URL whose trailing '?' was trimmed, leaving the QUERY marker at - * index == originalUrl.length. getPart(PATH) must clamp its endIndex and not throw. - */ - @Test - fun getPartDoesNotThrowWhenQueryIndexEqualsStringLength() { - // "example.com/a" is 14 chars; QUERY at 14 simulates "example.com/a?" after trim. - val marker = UrlMarker() - marker.setIndex(UrlPart.HOST, 0) - marker.setIndex(UrlPart.PATH, 11) - marker.setIndex(UrlPart.QUERY, 14) // == originalUrl.length - val url = marker.createUrl("example.com/a") - - assertEquals("example.com", url.host) - assertEquals("/a", url.path) - assertEquals("", url.query) // getPart(QUERY) startIndex >= length → null → "" - } - - /** - * Regression test for PR #1907: the Japanese phrase "今北産業" (the crash trigger) - * must not cause a StringIndexOutOfBoundsException when detected URLs have their - * parts accessed, and must produce no URLs. - */ - @Test - fun detectingJapaneseTextDoesNotThrow() { + fun detectingImakitaSangyoDoesNotThrow() { val urls = UrlDetector("今北産業").detect() assertEquals(0, urls.size) } /** - * Ensures that even if a Url with out-of-range markers somehow reaches property - * access, all properties return gracefully without throwing. + * Regression: constructing a Url with "今北産業" as the original string and a HOST + * marker at 0 with PORT at the string length (simulating a trimmed trailing character) + * must not throw StringIndexOutOfBoundsException when accessing any property. + * + * "今北産業" has length 4. PORT at 4 == length triggers the startIndex >= length guard + * added to getPart() in PR #1907. */ @Test - fun allPropertiesAreSafeWithOutOfRangeMarkers() { + fun urlPropertiesDoNotThrowForImakitaSangyoWithOutOfRangeMarker() { val marker = UrlMarker() - marker.setIndex(UrlPart.SCHEME, 0) - marker.setIndex(UrlPart.HOST, 8) - marker.setIndex(UrlPart.PORT, 20) // beyond the 19-char string length - val url = marker.createUrl("https://example.com") // 19 chars + marker.setIndex(UrlPart.HOST, 0) + marker.setIndex(UrlPart.PORT, 4) // == "今北産業".length + val url = marker.createUrl("今北産業") assertNotNull(url.scheme) assertNotNull(url.host) - assertNotNull(url.username) - assertNotNull(url.password) assertNotNull(url.path) assertNotNull(url.query) assertNotNull(url.fragment) - // port returns -1 when getPart returns null - assertEquals(-1, url.port) + assertEquals(-1, url.port) // getPart(PORT) returns null → -1 } }