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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,9 @@ kuri implements the standards below; per-standard conformance is measured in [Co
| [RFC 3987][rfc3987] | IRIs — one-way `Iri` mapping, not a validating parser* | Supported | Default |
| [WHATWG URL Standard][whatwg-url] | the `Url` model — parser, special schemes, canonical serialization | Conformant | Default |

\* kuri maps IRIs to URIs one-way (RFC 3987 §3.1/§3.2); it does not implement §4 validation (bidi, repertoire checks).
\* kuri maps IRIs to URIs one-way (RFC 3987 §3.1/§3.2), rejecting a §2.2 `ucschar`/`iprivate`
repertoire violation and a §4.1 bidi formatting character; it does not enforce §4.2's per-component
directionality restriction, which the RFC itself states as a SHOULD, not a MUST.

**Hosts, internationalization, and IP addresses**

Expand All @@ -491,7 +493,7 @@ kuri implements the standards below; per-standard conformance is measured in [Co

| Standard | Governs | Compliance | Support |
|------------------------------------------------|----------------------------------------------|------------|---------|
| [`application/x-www-form-urlencoded`][formenc] | Form-encoded query parsing and serialization | Supported | Default |
| [`application/x-www-form-urlencoded`][formenc] | Form-encoded query parsing and serialization | Conformant | Default |

**Notation and requirement levels**

Expand Down Expand Up @@ -548,6 +550,7 @@ Behavior is checked against the conformance corpora the standards ship with:
| IDNA `IdnaTestV2` + `toascii` | 2756 / 2760 |
| Unicode `NormalizationTest.txt` (NFC) | 20 034 / 20 034 |
| RFC 3986 §5.4 reference resolution | all rows |
| WHATWG `urlencoded-parser.any.js` — form parsing | 35 / 35 |

Any case that does not yet pass is pinned in a checked-in known-failures baseline; the build fails if a passing case
later regresses.
Expand Down
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ val generators: List<Triple<String, String, String>> =
"percent-encoding",
"Regenerate the WPT percent-encoding conformance fixture from the vendored ada WPT corpus.",
),
Triple(
"generateUrlEncodedTestData",
"urlencoded",
"Regenerate the WPT urlencoded-parser conformance fixture from tools/urlencoded/urlencoded-parser.json.",
),
)

val codegenTasks: List<TaskProvider<Exec>> =
Expand Down
4 changes: 3 additions & 1 deletion docs/idna-unicode-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ The generators are Go programs under `tools/` (module
`github.com/dexpace/kuri/tools`), driven by `tools/internal/codegen`. That
package reads raw UCD via `tools/internal/ucd` and derives the conformance
baseline via `tools/internal/idnaref`, a faithful Go port of kuri's runtime IDNA
engine.
engine — a second implementation of kuri's own algorithm, not an independent
oracle. WPT (below) is the independent oracle; idnaref only narrows the
known-failures residual within whatever WPT doesn't already exercise.

## Source of truth

Expand Down
13 changes: 13 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@ kover = "0.9.8"
binary-compat = "0.18.1"
dokka = "2.0.0"
maven-publish = "0.30.0"
icu4j = "77.1"
# Pinned to match the ASM version binary-compatibility-validator 0.18.1 wires into its own
# jvm/native apiBuild classpath internally, so the hand-wired android apiBuild leg (build.gradle.kts)
# parses class files with the same ASM release the rest of the apiCheck gate uses.
asm = "9.6"

[libraries]
# kotlin-test is pulled in via the kotlin("test") MPP helper; listed for reference.
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
# jvmTest-only: an independent third-party UTS-46 implementation to cross-check kuri's IDNA
# engine against (see IdnaIcu4jDifferentialTest). No multiplatform artifact exists, so this
# must never be referenced from commonMain/commonTest.
icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" }
# Runtime classpath for the manually wired android apiBuild/apiCheck tasks (build.gradle.kts).
asm-core = { module = "org.ow2.asm:asm", version.ref = "asm" }
asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" }
kotlin-metadata-jvm = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" }

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Expand Down
30 changes: 30 additions & 0 deletions kuri/api/android/kuri.api
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,34 @@ public final class org/dexpace/kuri/error/UriParseError$InvalidScheme : org/dexp
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter : org/dexpace/kuri/error/UriParseError {
public fun <init> (II)V
public final fun component1 ()I
public final fun component2 ()I
public final fun copy (II)Lorg/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter;
public static synthetic fun copy$default (Lorg/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter;IIILjava/lang/Object;)Lorg/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter;
public fun equals (Ljava/lang/Object;)Z
public final fun getAt ()I
public final fun getCodePoint ()I
public fun getMessage ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/kuri/error/UriParseError$IriInvalidCodePoint : org/dexpace/kuri/error/UriParseError {
public fun <init> (II)V
public final fun component1 ()I
public final fun component2 ()I
public final fun copy (II)Lorg/dexpace/kuri/error/UriParseError$IriInvalidCodePoint;
public static synthetic fun copy$default (Lorg/dexpace/kuri/error/UriParseError$IriInvalidCodePoint;IIILjava/lang/Object;)Lorg/dexpace/kuri/error/UriParseError$IriInvalidCodePoint;
public fun equals (Ljava/lang/Object;)Z
public final fun getAt ()I
public final fun getCodePoint ()I
public fun getMessage ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/kuri/error/UriParseError$MissingScheme : org/dexpace/kuri/error/UriParseError {
public static final field INSTANCE Lorg/dexpace/kuri/error/UriParseError$MissingScheme;
public fun equals (Ljava/lang/Object;)Z
Expand All @@ -376,6 +404,7 @@ public final class org/dexpace/kuri/error/UriSyntaxException : java/lang/Illegal

public final class org/dexpace/kuri/error/ValidationError : java/lang/Enum {
public static final field BACKSLASH_AS_SOLIDUS Lorg/dexpace/kuri/error/ValidationError;
public static final field INVALID_CREDENTIALS Lorg/dexpace/kuri/error/ValidationError;
public static final field INVALID_URL_UNIT Lorg/dexpace/kuri/error/ValidationError;
public static final field LEADING_OR_TRAILING_C0_CONTROL_OR_SPACE Lorg/dexpace/kuri/error/ValidationError;
public static final field MISSING_AUTHORITY_SLASHES Lorg/dexpace/kuri/error/ValidationError;
Expand Down Expand Up @@ -519,6 +548,7 @@ public final class org/dexpace/kuri/query/QueryParameters : java/lang/Iterable,
public static final fun parse (Ljava/lang/String;)Lorg/dexpace/kuri/query/QueryParameters;
public static final fun parseForm (Ljava/lang/String;)Lorg/dexpace/kuri/query/QueryParameters;
public final fun size ()I
public final fun split (Ljava/lang/String;C)Ljava/util/List;
public final fun toFormUrlEncoded ()Ljava/lang/String;
public final fun toMap ()Ljava/util/Map;
public final fun toQueryString ()Ljava/lang/String;
Expand Down
28 changes: 28 additions & 0 deletions kuri/api/jvm/kuri.api
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,34 @@ public final class org/dexpace/kuri/error/UriParseError$InvalidScheme : org/dexp
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter : org/dexpace/kuri/error/UriParseError {
public fun <init> (II)V
public final fun component1 ()I
public final fun component2 ()I
public final fun copy (II)Lorg/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter;
public static synthetic fun copy$default (Lorg/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter;IIILjava/lang/Object;)Lorg/dexpace/kuri/error/UriParseError$IriBidiFormattingCharacter;
public fun equals (Ljava/lang/Object;)Z
public final fun getAt ()I
public final fun getCodePoint ()I
public fun getMessage ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/kuri/error/UriParseError$IriInvalidCodePoint : org/dexpace/kuri/error/UriParseError {
public fun <init> (II)V
public final fun component1 ()I
public final fun component2 ()I
public final fun copy (II)Lorg/dexpace/kuri/error/UriParseError$IriInvalidCodePoint;
public static synthetic fun copy$default (Lorg/dexpace/kuri/error/UriParseError$IriInvalidCodePoint;IIILjava/lang/Object;)Lorg/dexpace/kuri/error/UriParseError$IriInvalidCodePoint;
public fun equals (Ljava/lang/Object;)Z
public final fun getAt ()I
public final fun getCodePoint ()I
public fun getMessage ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/kuri/error/UriParseError$MissingScheme : org/dexpace/kuri/error/UriParseError {
public static final field INSTANCE Lorg/dexpace/kuri/error/UriParseError$MissingScheme;
public fun equals (Ljava/lang/Object;)Z
Expand Down
32 changes: 32 additions & 0 deletions kuri/api/kuri.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,38 @@ sealed interface org.dexpace.kuri.error/UriParseError { // org.dexpace.kuri.erro
final fun toString(): kotlin/String // org.dexpace.kuri.error/UriParseError.InvalidScheme.toString|toString(){}[0]
}

final class IriBidiFormattingCharacter : org.dexpace.kuri.error/UriParseError { // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter|null[0]
constructor <init>(kotlin/Int, kotlin/Int) // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.<init>|<init>(kotlin.Int;kotlin.Int){}[0]

final val at // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.at|{}at[0]
final fun <get-at>(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.at.<get-at>|<get-at>(){}[0]
final val codePoint // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.codePoint|{}codePoint[0]
final fun <get-codePoint>(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.codePoint.<get-codePoint>|<get-codePoint>(){}[0]

final fun component1(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.component1|component1(){}[0]
final fun component2(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.component2|component2(){}[0]
final fun copy(kotlin/Int = ..., kotlin/Int = ...): org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.copy|copy(kotlin.Int;kotlin.Int){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // org.dexpace.kuri.error/UriParseError.IriBidiFormattingCharacter.toString|toString(){}[0]
}

final class IriInvalidCodePoint : org.dexpace.kuri.error/UriParseError { // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint|null[0]
constructor <init>(kotlin/Int, kotlin/Int) // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.<init>|<init>(kotlin.Int;kotlin.Int){}[0]

final val at // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.at|{}at[0]
final fun <get-at>(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.at.<get-at>|<get-at>(){}[0]
final val codePoint // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.codePoint|{}codePoint[0]
final fun <get-codePoint>(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.codePoint.<get-codePoint>|<get-codePoint>(){}[0]

final fun component1(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.component1|component1(){}[0]
final fun component2(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.component2|component2(){}[0]
final fun copy(kotlin/Int = ..., kotlin/Int = ...): org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.copy|copy(kotlin.Int;kotlin.Int){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // org.dexpace.kuri.error/UriParseError.IriInvalidCodePoint.toString|toString(){}[0]
}

final object EmptyHost : org.dexpace.kuri.error/UriParseError { // org.dexpace.kuri.error/UriParseError.EmptyHost|null[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.dexpace.kuri.error/UriParseError.EmptyHost.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.dexpace.kuri.error/UriParseError.EmptyHost.hashCode|hashCode(){}[0]
Expand Down
72 changes: 72 additions & 0 deletions kuri/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.KotlinMultiplatform
import com.vanniktech.maven.publish.SonatypeHost
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
import kotlinx.validation.KotlinApiBuildTask
import kotlinx.validation.KotlinApiCompareTask
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion

Expand Down Expand Up @@ -160,6 +162,10 @@ kotlin {
// on the Java test's compile classpath, at the version the Kotlin release was built for.
jvmTest.dependencies {
implementation(kotlin("test-junit"))
// Independent third-party UTS-46 oracle for IdnaIcu4jDifferentialTest (issue #66):
// icu4j has no multiplatform artifact, so it is scoped to the JVM test classpath only
// and must never be referenced from commonMain/commonTest.
implementation(libs.icu4j)
}
// Instrumented tests run under AndroidJUnitRunner (JUnit4), so bind kotlin.test to JUnit4
// and pull in the AndroidX test runtime that provides the runner.
Expand Down Expand Up @@ -242,6 +248,72 @@ apiValidation {
}
}

// binary-compatibility-validator's built-in multiplatform auto-configuration only recognizes an
// Android target compilation literally named "release" (the classic AGP `com.android.library` +
// `androidTarget()` build-variant naming). The AGP KMP library plugin used here
// (`com.android.kotlin.multiplatform.library`) is single-variant and names its production
// compilation "main" instead, so that auto-configuration never matches it, `androidApiBuild`/
// `androidApiCheck`/`androidApiDump` tasks never get created, and `apiCheck`/`apiDump` silently
// skip the android surface entirely (see the upstream gap tracked as KT-71172; verified locally
// by inspecting the plugin's target-matching source — it hasn't changed as of BCV 0.18.1). That
// silent skip is exactly how `kuri/api/android/kuri.api` went stale: nothing ever regenerated or
// checked it. Wire an equivalent android leg by hand using BCV's own public task classes,
// `KotlinApiBuildTask`/`KotlinApiCompareTask`, mirroring the classpath and directory layout
// (`api/android/kuri.api`) its Kotlin-target legs already use, and hook the result into the same
// `apiDump`/`apiCheck` entry points so android gets no special-cased invocation.
val androidAbiRuntimeClasspath: NamedDomainObjectProvider<Configuration> =
configurations.register("androidAbiRuntimeClasspath") {
description = "Runtime classpath for the manually wired android apiBuild/apiCheck tasks."
isCanBeConsumed = false
isCanBeResolved = true
isVisible = false
}

dependencies.apply {
add(androidAbiRuntimeClasspath.name, libs.asm.core.get())
add(androidAbiRuntimeClasspath.name, libs.asm.tree.get())
add(
androidAbiRuntimeClasspath.name,
libs.kotlin.metadata.jvm
.get(),
)
}

val androidApiBuild: TaskProvider<KotlinApiBuildTask> =
tasks.register<KotlinApiBuildTask>("androidApiBuild") {
group = "verification"
description = "Builds the Kotlin API dump for the android target's 'main' compilation of kuri."
inputClassesDirs.from(tasks.named("compileAndroidMain").map { it.outputs.files })
inputDependencies.from(
kotlin.targets
.getByName("android")
.compilations
.getByName("main")
.compileDependencyFiles,
)
outputApiFile.set(layout.buildDirectory.file("kotlin/abi-android/kuri.api"))
runtimeClasspath.from(androidAbiRuntimeClasspath)
}

val androidApiCheck: TaskProvider<KotlinApiCompareTask> =
tasks.register<KotlinApiCompareTask>("androidApiCheck") {
group = "verification"
description = "Checks the android target's public API against api/android/kuri.api."
projectApiFile.set(layout.projectDirectory.file("api/android/kuri.api"))
generatedApiFile.set(androidApiBuild.flatMap { it.outputApiFile })
}

val androidApiDump: TaskProvider<Copy> =
tasks.register<Copy>("androidApiDump") {
group = "other"
description = "Copies the android target's generated API dump into api/android/kuri.api."
from(androidApiBuild.flatMap { it.outputApiFile })
into(layout.projectDirectory.dir("api/android"))
}

tasks.named("apiCheck") { dependsOn(androidApiCheck) }
tasks.named("apiDump") { dependsOn(androidApiDump) }

// Publish every Kotlin Multiplatform target (plus the root `kotlinMultiplatform` publication) to Maven
// Central through the Central Portal. The vanniktech plugin wires the per-target publications, a
// Dokka-generated Javadoc jar and GPG signing; coordinates derive from the module `group` and the
Expand Down
Loading
Loading