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
11 changes: 5 additions & 6 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Idna.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ internal object Idna {
beStrict: Boolean = false,
): ParseResult<String> {
val mapped = mapAll(domain) ?: return idnaError(domain)
val labels = splitLabels(normalizeNfc(mapped))
// UTS-46 step 3: canonical NFC normalization (UAX #15), applied post-mapping, pre-label-split.
val labels = splitLabels(Normalizer.nfc(mapped))
return processLabels(labels, domain).map { it.joinToString(LABEL_SEPARATOR) }
}

Expand Down Expand Up @@ -103,13 +104,11 @@ internal object Idna {
*/
internal fun domainToUnicode(domain: String): String {
val mapped = mapAll(domain) ?: domain
val labels = splitLabels(normalizeNfc(mapped))
// UTS-46 step 3: canonical NFC normalization (UAX #15), applied post-mapping, pre-label-split.
val labels = splitLabels(Normalizer.nfc(mapped))
return labels.joinToString(LABEL_SEPARATOR) { decodeLabelForDisplay(it) }
}

/** UTS-46 step 3: canonical NFC normalization (UAX #15), applied post-mapping, pre-label-split. */
private fun normalizeNfc(s: String): String = Normalizer.nfc(s)

/**
* Applies the UTS-46 mapping step to every code point of [domain], returning the mapped text or
* `null` on the first disallowed code point (the caller turns `null` into a fatal error).
Expand Down Expand Up @@ -200,7 +199,7 @@ internal object Idna {
* Re-validates a freshly Punycode-decoded A-label (UTS-46 V1 / P4): the decoded U-label must be
* non-empty, must carry a non-ASCII code point (an all-ASCII result should never have been
* ACE-encoded, whatwg/url#760), must already be in NFC (V1), and must not itself begin with the
* ACE prefix (a double-encoded label, whatwg/url#803). The top-level NFC pass ([normalizeNfc])
* ACE prefix (a double-encoded label, whatwg/url#803). The top-level NFC pass ([Normalizer.nfc])
* runs before Punycode decoding, so this is the only point the decoded label's NFC form is checked.
*/
internal fun isValidDecodedALabel(decoded: String): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ import org.dexpace.kuri.percent.PercentEncodeSets
import org.dexpace.kuri.scheme.Scheme
import org.dexpace.kuri.text.isAsciiDigit

/** The canonical `file` scheme, special-cased by the file sub-machine (§8.3). */
private const val FILE_SCHEME: String = "file"

/** WHATWG `localhost`, mapped to the empty host in the FILE_HOST state ([PARSE-37]). */
private const val LOCALHOST: String = "localhost"

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@
* Copyright (c) 2026 dexpace and Omar Aljarrah
* SPDX-License-Identifier: MIT
*/

// The flat bag of profile-agnostic §8.3 path/authority/drive predicates the Url state machine keys
// off; grouping them beside their consumers keeps one definition per rule (hence the per-file
// function count legitimately exceeds detekt's heuristic).
@file:Suppress("TooManyFunctions")

package org.dexpace.kuri.parser

import org.dexpace.kuri.percent.PercentCodec
import org.dexpace.kuri.percent.PercentEncodeSets
import org.dexpace.kuri.text.isAsciiAlpha

/** The canonical scheme name whose state machine owns the Windows-drive-letter quirks. */
private const val FILE_SCHEME: String = "file"
/** The canonical `file` scheme, special-cased throughout the §8.3 `Url` state machine. */
internal const val FILE_SCHEME: String = "file"

/** The code points that may terminate a Windows drive letter prefix (`/`, `\`, `?`, `#`). */
private const val WINDOWS_DRIVE_TERMINATORS: String = "/\\?#"

/**
* True when [ch] is one of the three universal authority/path terminators `/`, `?`, `#`
Expand Down Expand Up @@ -105,3 +115,26 @@ internal fun cloneBasePath(base: ParsedComponents?): MutableList<String> {
val segments = (base?.path as? UrlPath.Segments)?.segments ?: emptyList()
return segments.toMutableList()
}

/**
* True when [value] *is* a Windows drive letter: exactly an ASCII alpha followed by `:`
* or `|` (SPEC §9.4 [PATH-14]).
*/
internal fun isWindowsDriveLetter(value: String): Boolean =
value.length == 2 && value[0].isAsciiAlpha() && (value[1] == ':' || value[1] == '|')

/**
* True when [value] is a *normalized* Windows drive letter: an ASCII alpha followed by `:`
* (SPEC §9.4 [PATH-14]).
*/
internal fun isNormalizedWindowsDrive(value: String): Boolean =
value.length == 2 && value[0].isAsciiAlpha() && value[1] == ':'

/**
* True when [value] *starts with* a Windows drive letter (SPEC §9.4 [PATH-14]): a drive
* letter that is either the whole string or immediately followed by `/`, `\`, `?`, or `#`.
*/
internal fun startsWithWindowsDrive(value: String): Boolean {
val hasDrive = value.length >= 2 && value[0].isAsciiAlpha() && (value[1] == ':' || value[1] == '|')
return hasDrive && (value.length == 2 || value[2] in WINDOWS_DRIVE_TERMINATORS)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import org.dexpace.kuri.scheme.isSchemeContinuationChar
import org.dexpace.kuri.text.asciiLowercased
import org.dexpace.kuri.text.isAsciiAlpha

/** The canonical `file` scheme, special-cased throughout the §8 state machine. */
private const val FILE_SCHEME: String = "file"

/**
* The routing, scheme, relative-resolution, path, query, and opaque-path states of the §8.3
* `Url`-profile state machine (WHATWG basic URL parser).
Expand Down
Loading