Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/host/HostParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) -------------------

Expand Down
37 changes: 23 additions & 14 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/host/Ipv4.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}

Expand Down
16 changes: 2 additions & 14 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/host/OpaqueHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
30 changes: 17 additions & 13 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/percent/PercentCodec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -264,7 +269,6 @@ internal object PercentCodec {
source += TRIPLET_LENGTH
target++
}
out.append(bytes.decodeToString())
return end
return bytes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading