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
16 changes: 11 additions & 5 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,11 @@ public class Uri internal constructor(
* The contract is enforced, not merely intended: the candidate is resolved back against this URI
* and compared to [target], and returned only when it round-trips. The result is `null` when no
* relative form resolves back to [target] — the two differ in [scheme] or [authority], either side
* has an opaque path (see [isOpaquePath]), [target] is not under this URI's directory, or the only
* has an opaque path (see [isOpaquePath]), [target] is not under this URI's directory, the only
* candidate is a case RFC 3986 resolution would re-read (an empty reference re-inherits the base
* query; a `..`-bearing suffix), or this URI has no [scheme] (resolution requires an absolute base).
* query; a `..`-bearing suffix), the candidate's resolution does not produce a valid URI (dot-segment
* removal can yield a `//`-leading path that re-reads as an invalid authority), or this URI has no
* [scheme] (resolution requires an absolute base).
* This mirrors [Url.relativize], which likewise returns `null` when
* no relative form exists. This is total: it never throws.
*
Expand All @@ -312,9 +314,13 @@ public class Uri internal constructor(
// relativize inverts resolve, which requires an absolute base; a scheme-less base has no relative
// form and would trip the structured resolver's absolute-base precondition, so reject it up front.
if (scheme == null) return null
// Round-trip the already-parsed candidate through the structured resolver instead of re-parsing
// its serialized string, keeping it only when the resolved value equals the target.
return relativeReference(target)?.takeIf { Uri(Resolver.resolve(components, it.components)) == target }
val candidate = relativeReference(target)
// Resolve the already-parsed candidate through the structured resolver; a candidate whose
// resolution does not yield a valid URI (dot-segment removal can produce a //-leading path that
// re-reads as an invalid authority) means no relative form exists, so fold that failure to null
// rather than letting the resolver's internal parse error surface — relativize stays total.
val resolved = candidate?.let { Resolver.resolve(components, it.components).getOrNull() }
return candidate?.takeIf { resolved != null && Uri(resolved) == target }
}

/**
Expand Down
68 changes: 3 additions & 65 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Idna.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import org.dexpace.kuri.error.HostError
import org.dexpace.kuri.error.ParseResult
import org.dexpace.kuri.error.UriParseError
import org.dexpace.kuri.error.map
import org.dexpace.kuri.text.MAX_CODE_POINT
import org.dexpace.kuri.text.appendCodePoint
import org.dexpace.kuri.text.codePointsOf

/** ACE prefix marking a Punycode-encoded (`xn--`) label (RFC 5890 §2.3.2.1). */
private const val ACE_PREFIX: String = "xn--"
Expand All @@ -24,27 +27,6 @@ private const val NON_ASCII_MIN: Int = 0x80
/** Distance from an uppercase ASCII letter to its lowercase counterpart (`'A'` -> `'a'`). */
private const val ASCII_CASE_OFFSET: Int = 0x20

/** Largest Unicode scalar value (U+10FFFF). */
private const val MAX_CODE_POINT: Int = 0x10FFFF

/** Largest code point that fits in a single UTF-16 code unit. */
private const val MAX_BMP_CODE_POINT: Int = 0xFFFF

/** First code point of the supplementary planes (needs a surrogate pair). */
private const val SUPPLEMENTARY_BASE: Int = 0x10000

/** Bit shift separating the high- and low-surrogate halves of a code point. */
private const val SURROGATE_SHIFT: Int = 10

/** Mask isolating the low-surrogate payload bits of a supplementary code point. */
private const val LOW_SURROGATE_MASK: Int = 0x3FF

/** First UTF-16 high-surrogate code unit (`U+D800`). */
private const val HIGH_SURROGATE_START: Int = 0xD800

/** First UTF-16 low-surrogate code unit (`U+DC00`). */
private const val LOW_SURROGATE_START: Int = 0xDC00

/**
* UTS-46 ToASCII / ToUnicode for IDNA domains (SPEC §7.4, [HOST-26]) under the `Url`-profile
* parameter set ([HOST-28]): `CheckHyphens = false`, `CheckBidi = true`, `CheckJoiners = true`,
Expand Down Expand Up @@ -290,48 +272,4 @@ internal object Idna {
buildString(s.length) {
for (c in s) append(if (c in 'A'..'Z') c + ASCII_CASE_OFFSET else c)
}

/** Splits [input] into Unicode code points, combining well-formed surrogate pairs. */
private fun codePointsOf(input: String): List<Int> {
val result = ArrayList<Int>(input.length)
var index = 0
while (index < input.length) {
val high = input[index]
val low = if (index + 1 < input.length) input[index + 1] else null
if (high.isHighSurrogate() && low != null && low.isLowSurrogate()) {
result.add(toCodePoint(high, low))
index += 2
} else {
result.add(high.code)
index++
}
}
return result
}

/** Combines a UTF-16 surrogate pair into a single supplementary-plane code point. */
private fun toCodePoint(
high: Char,
low: Char,
): Int {
require(high.isHighSurrogate()) { "expected high surrogate: $high" }
require(low.isLowSurrogate()) { "expected low surrogate: $low" }
val highBits = (high.code - HIGH_SURROGATE_START) shl SURROGATE_SHIFT
return SUPPLEMENTARY_BASE + highBits + (low.code - LOW_SURROGATE_START)
}

/** Appends [codePoint] to [out], emitting a surrogate pair for supplementary values. */
private fun appendCodePoint(
out: StringBuilder,
codePoint: Int,
) {
require(codePoint in 0..MAX_CODE_POINT) { "code point out of range: $codePoint" }
if (codePoint <= MAX_BMP_CODE_POINT) {
out.append(codePoint.toChar())
} else {
val offset = codePoint - SUPPLEMENTARY_BASE
out.append((HIGH_SURROGATE_START + (offset ushr SURROGATE_SHIFT)).toChar())
out.append((LOW_SURROGATE_START + (offset and LOW_SURROGATE_MASK)).toChar())
}
}
}
25 changes: 2 additions & 23 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/IdnaValidity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/
package org.dexpace.kuri.idna

import org.dexpace.kuri.text.toCodePoint

/** Record separator inside a decoded range blob (never appears inside a record). */
private const val RECORD_SEPARATOR: Char = '\n'

Expand Down Expand Up @@ -57,18 +59,6 @@ private const val BIDI_ON: Char = 'O'
/** Sentinel for a code point with no Bidi_Class the rule consults; it satisfies no condition. */
private const val BIDI_NONE: Char = '\u0000'

/** First UTF-16 high-surrogate code unit (`U+D800`). */
private const val HIGH_SURROGATE_START: Int = 0xD800

/** First UTF-16 low-surrogate code unit (`U+DC00`). */
private const val LOW_SURROGATE_START: Int = 0xDC00

/** Bit shift separating the high- and low-surrogate halves of a code point. */
private const val SURROGATE_SHIFT: Int = 10

/** First code point of the supplementary planes (needs a surrogate pair). */
private const val SUPPLEMENTARY_BASE: Int = 0x10000

/**
* UTS-46 label-validity rules beyond the mapping table (SPEC §7.4, [HOST-28] `CheckJoiners = true`,
* `CheckBidi = true`):
Expand Down Expand Up @@ -419,15 +409,4 @@ internal object IdnaValidity {
val paired = high.isHighSurrogate() && low != null && low.isLowSurrogate()
return if (paired) toCodePoint(high, requireNotNull(low)) else high.code
}

/** Combines a UTF-16 surrogate pair into a single supplementary-plane code point. */
private fun toCodePoint(
high: Char,
low: Char,
): Int {
require(high.isHighSurrogate()) { "expected high surrogate: $high" }
require(low.isLowSurrogate()) { "expected low surrogate: $low" }
val highBits = (high.code - HIGH_SURROGATE_START) shl SURROGATE_SHIFT
return SUPPLEMENTARY_BASE + highBits + (low.code - LOW_SURROGATE_START)
}
}
65 changes: 3 additions & 62 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Normalizer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

package org.dexpace.kuri.idna

import org.dexpace.kuri.text.appendCodePoint
import org.dexpace.kuri.text.codePointsOf

/** Combining class of a starter (and of all non-combining characters). */
private const val CCC_STARTER: Int = 0

Expand All @@ -29,27 +32,6 @@ private const val DECOMP_SEPARATOR: Char = ' '
/** Bit width reserved for a code point when packing a composition key (covers U+10FFFF). */
private const val CODE_POINT_BITS: Int = 21

/** Inclusive upper bound of the Unicode code-point space (U+10FFFF). */
private const val MAX_CODE_POINT: Int = 0x10FFFF

/** Highest code point representable in a single UTF-16 unit. */
private const val MAX_BMP_CODE_POINT: Int = 0xFFFF

/** First supplementary-plane code point (the surrogate-pair base). */
private const val SUPPLEMENTARY_BASE: Int = 0x10000

/** First UTF-16 high-surrogate unit. */
private const val HIGH_SURROGATE_START: Int = 0xD800

/** First UTF-16 low-surrogate unit. */
private const val LOW_SURROGATE_START: Int = 0xDC00

/** Bit shift separating the high- and low-surrogate halves of a supplementary code point. */
private const val SURROGATE_SHIFT: Int = 10

/** Mask isolating the low-surrogate bits of a supplementary code point. */
private const val LOW_SURROGATE_MASK: Int = 0x3FF

// Algorithmic Hangul syllable composition/decomposition parameters (UAX #15, §3.12 of the core spec).
private const val S_BASE: Int = 0xAC00
private const val L_BASE: Int = 0x1100
Expand Down Expand Up @@ -278,32 +260,6 @@ private fun packPair(
combining: Int,
): Long = (starter.toLong() shl CODE_POINT_BITS) or combining.toLong()

/** Splits [input] into Unicode scalar values, folding well-formed surrogate pairs. */
private fun codePointsOf(input: String): List<Int> {
require(input.isNotEmpty()) { "codePointsOf requires non-empty input" }
val result = ArrayList<Int>(input.length)
var index = 0
while (index < input.length) {
val high = input[index]
val low = if (index + 1 < input.length) input[index + 1] else null
val paired = high.isHighSurrogate() && low != null && low.isLowSurrogate()
result.add(if (paired) toCodePoint(high, low) else high.code)
index += if (paired) 2 else 1
}
return result
}

/** Combines a UTF-16 surrogate pair into a single supplementary-plane code point. */
private fun toCodePoint(
high: Char,
low: Char,
): Int {
require(high.isHighSurrogate()) { "expected high surrogate: $high" }
require(low.isLowSurrogate()) { "expected low surrogate: $low" }
val highBits = (high.code - HIGH_SURROGATE_START) shl SURROGATE_SHIFT
return SUPPLEMENTARY_BASE + highBits + (low.code - LOW_SURROGATE_START)
}

/** Builds a string from a list of scalar [codePoints], emitting surrogate pairs as needed. */
private fun buildScalarString(codePoints: List<Int>): String {
require(codePoints.isNotEmpty()) { "cannot build a string from no code points" }
Expand All @@ -313,18 +269,3 @@ private fun buildScalarString(codePoints: List<Int>): String {
}
return builder.toString()
}

/** Appends [codePoint] to [out], emitting a surrogate pair for supplementary values. */
private fun appendCodePoint(
out: StringBuilder,
codePoint: Int,
) {
require(codePoint in 0..MAX_CODE_POINT) { "code point out of range: $codePoint" }
if (codePoint <= MAX_BMP_CODE_POINT) {
out.append(codePoint.toChar())
return
}
val offset = codePoint - SUPPLEMENTARY_BASE
out.append((HIGH_SURROGATE_START + (offset ushr SURROGATE_SHIFT)).toChar())
out.append((LOW_SURROGATE_START + (offset and LOW_SURROGATE_MASK)).toChar())
}
69 changes: 4 additions & 65 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Punycode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
*/
package org.dexpace.kuri.idna

import org.dexpace.kuri.text.HIGH_SURROGATE_START
import org.dexpace.kuri.text.MAX_CODE_POINT
import org.dexpace.kuri.text.appendCodePoint
import org.dexpace.kuri.text.codePointsOf
import org.dexpace.kuri.text.isAsciiAlphanumeric

/** Radix of the Bootstring generalized variable-length integers (RFC 3492 §5). */
Expand Down Expand Up @@ -33,27 +37,6 @@ private const val LETTER_DIGIT_COUNT: Int = 26
/** Delimiter separating the basic-code-point prefix from the encoded suffix. */
private const val DELIMITER: Char = '-'

/** Largest Unicode scalar value; decoding past it is malformed input. */
private const val MAX_CODE_POINT: Int = 0x10FFFF

/** Largest code point that fits in a single UTF-16 code unit. */
private const val MAX_BMP_CODE_POINT: Int = 0xFFFF

/** First code point of the supplementary planes (needs a surrogate pair). */
private const val SUPPLEMENTARY_BASE: Int = 0x10000

/** Bit shift separating the high- and low-surrogate halves of a code point. */
private const val SURROGATE_SHIFT: Int = 10

/** Mask isolating the low-surrogate payload bits of a supplementary code point. */
private const val LOW_SURROGATE_MASK: Int = 0x3FF

/** First UTF-16 high-surrogate code unit (`U+D800`). */
private const val HIGH_SURROGATE_START: Int = 0xD800

/** First UTF-16 low-surrogate code unit (`U+DC00`). */
private const val LOW_SURROGATE_START: Int = 0xDC00

/** Last UTF-16 surrogate code unit (`U+DFFF`); the surrogate block is `U+D800..U+DFFF`. */
private const val LAST_SURROGATE: Int = 0xDFFF

Expand Down Expand Up @@ -349,50 +332,6 @@ internal object Punycode {
else -> -1
}

/** Splits [input] into Unicode code points, combining well-formed surrogate pairs. */
private fun codePointsOf(input: String): List<Int> {
val result = ArrayList<Int>(input.length)
var index = 0
while (index < input.length) {
val high = input[index]
val low = if (index + 1 < input.length) input[index + 1] else null
if (high.isHighSurrogate() && low != null && low.isLowSurrogate()) {
result.add(toCodePoint(high, low))
index += 2
} else {
result.add(high.code)
index++
}
}
return result
}

/** Combines a UTF-16 surrogate pair into a single supplementary-plane code point. */
private fun toCodePoint(
high: Char,
low: Char,
): Int {
require(high.isHighSurrogate()) { "expected high surrogate: $high" }
require(low.isLowSurrogate()) { "expected low surrogate: $low" }
val highBits = (high.code - HIGH_SURROGATE_START) shl SURROGATE_SHIFT
return SUPPLEMENTARY_BASE + highBits + (low.code - LOW_SURROGATE_START)
}

/** Appends [codePoint] to [output], emitting a surrogate pair for supplementary values. */
private fun appendCodePoint(
output: StringBuilder,
codePoint: Int,
) {
require(codePoint in 0..MAX_CODE_POINT) { "code point out of range: $codePoint" }
if (codePoint <= MAX_BMP_CODE_POINT) {
output.append(codePoint.toChar())
} else {
val offset = codePoint - SUPPLEMENTARY_BASE
output.append((HIGH_SURROGATE_START + (offset ushr SURROGATE_SHIFT)).toChar())
output.append((LOW_SURROGATE_START + (offset and LOW_SURROGATE_MASK)).toChar())
}
}

/**
* Immutable carrier for the encoder's per-`n`-pass state (RFC 3492 §6.3). [basicCount]
* is threaded through so the `first` flag of [adapt] (`handled == basicCount`) survives
Expand Down
11 changes: 5 additions & 6 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/Resolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,17 @@ internal object Resolver {
*
* @param base the absolute base components; its scheme MUST be present.
* @param reference the reference components to resolve.
* @return the resolved target components.
* @return [ParseResult.Ok] with the resolved target components, or [ParseResult.Err] when the
* recomposed target does not parse — e.g. dot-segment removal produced a `//`-leading, authority-less
* path that re-reads as an invalid authority.
*/
internal fun resolve(
base: ParsedComponents,
reference: ParsedComponents,
): ParsedComponents {
): ParseResult<ParsedComponents> {
require(base.scheme != null) { "structured resolution requires an absolute base scheme" }
val target = transformReferences(partsOf(base), partsOf(reference))
return when (val parsed = UriParser.parse(recompose(target), structuredOptions(base, reference))) {
is ParseResult.Ok -> parsed.value
is ParseResult.Err -> error("resolved reference is not a valid URI: ${parsed.error}")
}
return UriParser.parse(recompose(target), structuredOptions(base, reference))
}

/** Derives the round-trip [ParseOptions] for a structured resolve: a zone id on either input opts in. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package org.dexpace.kuri.percent

import org.dexpace.kuri.text.hexDigitToInt
import org.dexpace.kuri.text.isAsciiHexDigit
import org.dexpace.kuri.text.isSurrogatePairAt
import org.dexpace.kuri.text.toPercentEncodedByte

/** Mask isolating the low eight bits of an `Int`, turning a signed `Byte` into an octet `0..255`. */
Expand Down Expand Up @@ -206,15 +207,6 @@ internal object PercentCodec {
return found
}

/** True when a high surrogate at [i] is immediately followed by a low surrogate. */
private fun isSurrogatePairAt(
input: String,
i: Int,
): Boolean {
val next = i + 1
return input[i].isHighSurrogate() && next < input.length && input[next].isLowSurrogate()
}

/** Appends the [step]-unit code point at [i] as `+` (form space) or as UTF-8 triplets. */
private fun appendEncoded(
out: StringBuilder,
Expand Down
Loading
Loading