From 951610d9caeb444b3c4f944cda69bf95ef90a75f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 17:10:56 +0000 Subject: [PATCH 1/7] fix: prevent StringIndexOutOfBoundsException in Url.getPart() When UrlDetector.readEnd() trims the last character from URLs ending with characters like '.', ',', etc., the urlMarker indices remain based on the original buffer length. This caused getPart() to call substring() with an end index beyond the trimmed URL's length. Fix by clamping the end index to originalUrl.length and guarding against an out-of-bounds start index. https://claude.ai/code/session_014Q2SvTE5vKgUm1w8dPVjYo --- .../com/vitorpamplona/quartz/utils/urldetector/Url.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/Url.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/Url.kt index 538d9ea323..46927f771d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/Url.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/Url.kt @@ -228,11 +228,17 @@ class Url( return null } + val startIndex = urlMarker.indexOf(part) + if (startIndex < 0 || startIndex >= originalUrl.length) { + return null + } + val nextPart = nextExistingPart(part) return if (nextPart == null) { - originalUrl.substring(urlMarker.indexOf(part)) + originalUrl.substring(startIndex) } else { - originalUrl.substring(urlMarker.indexOf(part), urlMarker.indexOf(nextPart)) + val endIndex = urlMarker.indexOf(nextPart) + originalUrl.substring(startIndex, minOf(endIndex, originalUrl.length)) } } From e31e3829a64b08eede744affa39104f396f2acf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 20:31:28 +0000 Subject: [PATCH 2/7] 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 b9613c5cd7..8777f5329c 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 0d526ab172..7e7f66d246 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 660759b160..2c9bd35cb2 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 3/7] =?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 8777f5329c..600c0f27f0 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 7e7f66d246..aae685bf2b 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 2c9bd35cb2..b13e3b6585 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 4/7] 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 b13e3b6585..660759b160 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 5/7] 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 0000000000..10bd863162 --- /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 6/7] =?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 10bd863162..a29c5e99e3 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 } } From 3d488165c3cadb6a5f1475551d2daa3bafb2e120 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 23 Mar 2026 21:40:20 +0000 Subject: [PATCH 7/7] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 5f2c6efd61..d956737d3e 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -71,7 +71,7 @@ Zacytuj Sklonuj Zaproponuj zmianę - Nowa kwota w Satsach + Nowa kwota w satoszach Dodaj "odpowiadając do " " i " @@ -92,7 +92,7 @@ Lightning transfer Wiadomość dla odbiorcy Dziękuję bardzo! - Kwota w Satsach + Kwota w satoszach Wyślij Kreator tajnych emoji Dodaj emoji z ukrytą wiadomością do wpisu @@ -1101,6 +1101,13 @@ Nowy wpis w społeczności Nowy produkt Nowy GEO-ekskluzywny Wpis + Nowy artykuł + Tytuł + Podsumowanie (opcjonalnie) + Adres URL miniaturki (opcjonalnie) + Napisz artykuł w formacie markdown… + Podgląd + Edytuj Otwórz wszystkie odzewy na ten post Zamknij wszystkie odzewy na ten post Odpowiedź @@ -1238,6 +1245,7 @@ OTS: %1$s Potwierdzenie znacznika czasu Istnieje dowód na to, że ten post został podpisany przed %1$s. Dowód został opatrzony pieczęcią w łańcuchu bloków Bitcoin w tym dniu i czasie. + Redaguj artykuł Edytuj wpis Propozycja ulepszenia wpisu Podsumowanie zmian @@ -1560,6 +1568,7 @@ wyszukaj, npub1…, alicja@domena.pl Obsługuje npub, nprofile, NIP-05, hex, i namecoin (.bit, d/, id/) Sprawdź listę obserwowanych + Porada Znaleziono %1$d kont(a) Wybrano: %1$d Rozwiązane przez Namecoin @@ -1659,4 +1668,11 @@ Biegłość w sprawdzaniu typów: %1$s Certyfikat dla Żądanie certyfikatu do + Przedział czasu + Od + Do + Teraz + Cały czas + Ostatnia synchronizacja %1$s + Od ostatniej synchronizacji