From 29fb632452ba31a4ec25a005e8385b87982d3c8d Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 7 Jul 2026 16:49:37 +0300 Subject: [PATCH] refactor: tidy parser file layout and inline a redundant NFC wrapper Housekeeping in the parser and idna packages: - Collapse three per-file copies of `private const val FILE_SCHEME = "file"` in the parser package into one internal const in UrlParserHelpers. (Url.kt's own copy stays: it is in a different package, and importing a parser-internal const there for one literal would read worse than the local one.) - Fold UrlParserDrive's three Windows-drive predicates into UrlParserHelpers, which already consumes them, and delete the file. One flat bag of same-package helper predicates does not need two files. - Inline Idna's one-line normalizeNfc pass-through to Normalizer.nfc at both call sites, keeping the "UTS-46 step 3" rationale as a comment. Pure refactor: no behavior change, public API unchanged. --- .../kotlin/org/dexpace/kuri/idna/Idna.kt | 11 +++--- .../dexpace/kuri/parser/UrlParserAuthority.kt | 3 -- .../org/dexpace/kuri/parser/UrlParserDrive.kt | 33 ----------------- .../dexpace/kuri/parser/UrlParserHelpers.kt | 37 ++++++++++++++++++- .../dexpace/kuri/parser/UrlParserStates.kt | 3 -- 5 files changed, 40 insertions(+), 47 deletions(-) delete mode 100644 kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserDrive.kt diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Idna.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Idna.kt index ae1174b..e793884 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Idna.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/idna/Idna.kt @@ -63,7 +63,8 @@ internal object Idna { beStrict: Boolean = false, ): ParseResult { 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) } } @@ -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). @@ -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 = diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserAuthority.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserAuthority.kt index ffdfbf7..0647241 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserAuthority.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserAuthority.kt @@ -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" diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserDrive.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserDrive.kt deleted file mode 100644 index d5d042c..0000000 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserDrive.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2026 dexpace and Omar Aljarrah - * SPDX-License-Identifier: MIT - */ -package org.dexpace.kuri.parser - -import org.dexpace.kuri.text.isAsciiAlpha - -/** The code points that may terminate a Windows drive letter prefix (`/`, `\`, `?`, `#`). */ -private const val WINDOWS_DRIVE_TERMINATORS: String = "/\\?#" - -/** - * 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) -} diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserHelpers.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserHelpers.kt index 5eb0ec3..c6bc3b6 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserHelpers.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserHelpers.kt @@ -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 `/`, `?`, `#` @@ -105,3 +115,26 @@ internal fun cloneBasePath(base: ParsedComponents?): MutableList { 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) +} diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserStates.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserStates.kt index 8145017..596f5f8 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserStates.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/parser/UrlParserStates.kt @@ -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).