From 51e388d2a1ccb09051b2983a2c625fbc4c15e3b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 14:54:13 +0000 Subject: [PATCH] fix: bounds-check ServiceType parsing of NIP-85 service tags ServiceType.parse destructured the result of split(":", limit = 2) into two components, so any value without a colon (e.g. the "client" of a ["client", "nostria"] tag) threw IndexOutOfBoundsException instead of returning null. It also accepted "30382:" as an empty service type. ServiceType.isOfKind had the same class of bug: it read serviceType[kind.length] after startsWith, which is out of bounds when the value equals the kind exactly ("30382" vs "30382"). Parse the separator by index and check the length before indexing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0112ysMEzSaezH9mXFteNt13 --- .../list/tags/ServiceType.kt | 9 ++- .../ServiceTypeParserTest.kt | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ServiceTypeParserTest.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/list/tags/ServiceType.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/list/tags/ServiceType.kt index dafea6d8cc..a894cb1c75 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/list/tags/ServiceType.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/list/tags/ServiceType.kt @@ -33,15 +33,16 @@ data class ServiceType( ) = "$kind:$type" fun parse(serviceType: String): ServiceType? { - val (kindStr, type) = serviceType.split(":", limit = 2) - val kind = kindStr.toIntOrNull() ?: return null - return ServiceType(kind, type) + val divider = serviceType.indexOf(':') + if (divider < 0 || divider == serviceType.length - 1) return null + val kind = serviceType.substring(0, divider).toIntOrNull() ?: return null + return ServiceType(kind, serviceType.substring(divider + 1)) } fun isOfKind( serviceType: String, kind: String, - ) = serviceType.startsWith(kind) && serviceType[kind.length] == ':' + ) = serviceType.length > kind.length && serviceType.startsWith(kind) && serviceType[kind.length] == ':' } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ServiceTypeParserTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ServiceTypeParserTest.kt new file mode 100644 index 0000000000..ced9e6b6a9 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ServiceTypeParserTest.kt @@ -0,0 +1,80 @@ +/* + * 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.experimental.nip85TrustedAssertions + +import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceType +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ServiceTypeParserTest { + @Test + fun parseValid() { + assertEquals(ServiceType(30382, "rank"), ServiceType.parse("30382:rank")) + } + + @Test + fun parseKeepsColonsInTheType() { + assertEquals(ServiceType(30382, "rank:extra"), ServiceType.parse("30382:rank:extra")) + } + + @Test + fun parseWithoutSeparator() { + // a `["client", "nostria"]` tag ends up here. Must not crash. + assertNull(ServiceType.parse("client")) + } + + @Test + fun parseEmpty() { + assertNull(ServiceType.parse("")) + } + + @Test + fun parseWithoutKind() { + assertNull(ServiceType.parse(":rank")) + } + + @Test + fun parseWithoutType() { + assertNull(ServiceType.parse("30382:")) + } + + @Test + fun parseNonNumericKind() { + assertNull(ServiceType.parse("client:nostria")) + } + + @Test + fun isOfKindMatches() { + assertTrue(ServiceType.isOfKind("30382:rank", "30382")) + } + + @Test + fun isOfKindOnPrefixWithoutSeparator() { + // "30382" starts with "30382" but has no `:` at that position. Must not crash. + assertFalse(ServiceType.isOfKind("30382", "30382")) + assertFalse(ServiceType.isOfKind("", "")) + assertFalse(ServiceType.isOfKind("303820:rank", "30382")) + assertFalse(ServiceType.isOfKind("30383:rank", "30382")) + } +}