diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/HostParser.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/HostParser.kt index b8d0b88..9210166 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/HostParser.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/HostParser.kt @@ -133,17 +133,8 @@ internal object HostParser { } } - /** Index of the first forbidden-domain code point in [domain] (§7.6 [HOST-37]), or [NOT_FOUND]. */ - private fun firstForbiddenDomainIndex(domain: String): Int { - var index = 0 - var found = NOT_FOUND - while (index < domain.length && found == NOT_FOUND) { - if (isForbiddenDomainCodePoint(domain[index])) found = index - index++ - } - check(found == NOT_FOUND || found in domain.indices) { "found out of bounds: $found" } - return found - } + /** Index of the first forbidden-domain code point in [domain] (§7.6 [HOST-37]), or [NOT_FOUND] (`-1`). */ + private fun firstForbiddenDomainIndex(domain: String): Int = domain.indexOfFirst { isForbiddenDomainCodePoint(it) } // --- Uri profile (RFC 3986 §3.2.2 host grammar, §7.2/§7.5/§7.8) ------------------- diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/Ipv4.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/Ipv4.kt index a8b2e8f..dcb244f 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/Ipv4.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/Ipv4.kt @@ -48,6 +48,12 @@ private const val RADIX_OCTAL: Int = 8 /** Hexadecimal radix selected by a `0x`/`0X` part prefix. */ private const val RADIX_HEX: Int = 16 +/** Length of the `0x`/`0X` prefix stripped from a hexadecimal part before its digits ([HOST-21] step 3). */ +private const val HEX_PREFIX_LENGTH: Int = 2 + +/** Length of the single leading `0` stripped from an octal part before its digits ([HOST-21] step 3). */ +private const val OCTAL_PREFIX_LENGTH: Int = 1 + /** Sentinel returned by [parsePart]/[decOctetOrNull] for an unparsable part. */ private const val INVALID_PART: Long = -1L @@ -164,22 +170,25 @@ internal object Ipv4 { require(part.isNotEmpty()) { "empty part reached parsePart" } val hex = isHexPrefixed(part) val octal = !hex && part.length > 1 && part[0] == '0' - val radix = - if (hex) { - RADIX_HEX - } else if (octal) { - RADIX_OCTAL - } else { - RADIX_DECIMAL + // One decision selects both the radix and the prefix length to strip; a decimal part carries + // offset 0 so it reuses `part` verbatim, materializing no substring on the common path. + val radix: Int + val prefixLength: Int + when { + hex -> { + radix = RADIX_HEX + prefixLength = HEX_PREFIX_LENGTH } - val digits = - if (hex) { - part.substring(2) - } else if (octal) { - part.substring(1) - } else { - part + octal -> { + radix = RADIX_OCTAL + prefixLength = OCTAL_PREFIX_LENGTH } + else -> { + radix = RADIX_DECIMAL + prefixLength = 0 + } + } + val digits = if (prefixLength == 0) part else part.substring(prefixLength) return parseInRadix(digits, radix) } diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/OpaqueHost.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/OpaqueHost.kt index 86225be..00bc207 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/OpaqueHost.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/host/OpaqueHost.kt @@ -60,18 +60,6 @@ internal object OpaqueHost { * `%` is excluded from the scan because it is permitted in an opaque host ([HOST-33]); all * forbidden host code points are in the Basic Multilingual Plane, so a per-`Char` scan is exact. */ - private fun firstForbiddenHostIndex(input: String): Int { - var index = 0 - var found = NOT_FOUND - while (index < input.length && found == NOT_FOUND) { - val cp = input[index] - if (cp != '%' && isForbiddenHostCodePoint(cp)) { - found = index - } - index++ - } - check(found == NOT_FOUND || found in input.indices) { "found index out of bounds: $found" } - check(found == NOT_FOUND || input[found] != '%') { "'%' must not be flagged forbidden" } - return found - } + private fun firstForbiddenHostIndex(input: String): Int = + input.indexOfFirst { it != '%' && isForbiddenHostCodePoint(it) } } diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/percent/PercentCodec.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/percent/PercentCodec.kt index b9dfd37..73aaa48 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/percent/PercentCodec.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/percent/PercentCodec.kt @@ -154,16 +154,7 @@ internal object PercentCodec { out.appendRange(input, start, start + TRIPLET_LENGTH) return start + TRIPLET_LENGTH } - val count = (end - start) / TRIPLET_LENGTH - check(count >= 1) { "a decode run must contain at least one non-ASCII triplet" } - val bytes = ByteArray(count) - var source = start - var target = 0 - while (target < count) { - bytes[target] = percentByteAt(input, source).toByte() - source += TRIPLET_LENGTH - target++ - } + val bytes = tripletRunBytes(input, start, end) val decoded = bytes.decodeToString() // A run decodes only when it is well-formed UTF-8: re-encoding the decoded text must // reproduce the exact octets. This preserves a genuinely-encoded U+FFFD (which round-trips) @@ -254,8 +245,22 @@ internal object PercentCodec { while (end < input.length && input[end] == '%' && hasPercentHexPairAt(input, end)) { end += TRIPLET_LENGTH } + out.append(tripletRunBytes(input, start, end).decodeToString()) + return end + } + + /** + * Materializes the triplet run `input[start, end)` into one decoded octet per `%HH` triplet + * ([PCT-25] gathering); [start]..[end] MUST span a whole, non-empty number of triplets. Shared by + * the lenient decode and RFC 3987 display-decode run appenders, whose surrounding logic differs. + */ + private fun tripletRunBytes( + input: String, + start: Int, + end: Int, + ): ByteArray { val count = (end - start) / TRIPLET_LENGTH - check(count >= 1) { "triplet run must contain at least one triplet" } + check(count >= 1) { "a triplet run must contain at least one triplet" } val bytes = ByteArray(count) var source = start var target = 0 @@ -264,7 +269,6 @@ internal object PercentCodec { source += TRIPLET_LENGTH target++ } - out.append(bytes.decodeToString()) - return end + return bytes } } diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/query/QueryParametersBuilder.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/query/QueryParametersBuilder.kt index b95c011..664c1c7 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/query/QueryParametersBuilder.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/query/QueryParametersBuilder.kt @@ -135,7 +135,7 @@ public class QueryParametersBuilder internal constructor( * * @return a snapshot of the accumulated pairs; later mutation of this builder does not affect it. */ - public fun build(): QueryParameters = QueryParameters(pairs.toList()) + public fun build(): QueryParameters = QueryParameters(pairs) /** True when no pairs are accumulated; the O(1) check the `Uri`/`Url` builders use to collapse an emptied query. */ internal fun isEmpty(): Boolean = pairs.isEmpty()