diff --git a/build.gradle.kts b/build.gradle.kts index 98e4e4443..18e27d042 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,9 @@ import java.net.URL import kotlinx.validation.ApiValidationExtension import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest +import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import ru.vyarus.gradle.plugin.animalsniffer.AnimalSnifferExtension @@ -13,6 +16,7 @@ buildscript { dependencies { classpath(libs.gradlePlugin.dokka) classpath(libs.gradlePlugin.kotlin) + classpath(libs.gradlePlugin.kotlinSerialization) classpath(libs.gradlePlugin.androidJunit5) classpath(libs.gradlePlugin.android) classpath(libs.gradlePlugin.graal) @@ -172,6 +176,18 @@ subprojects { systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") } + // https://publicobject.com/2023/04/16/read-a-project-file-in-a-kotlin-multiplatform-test/ + tasks.withType().configureEach { + environment("OKHTTP_ROOT", rootDir) + } + tasks.withType().configureEach { + environment("SIMCTL_CHILD_OKHTTP_ROOT", rootDir) + environment("OKHTTP_ROOT", rootDir) + } + tasks.withType().configureEach { + environment("OKHTTP_ROOT", rootDir.toString()) + } + if (platform == "jdk8alpn") { // Add alpn-boot on Java 8 so we can use HTTP/2 without a stable API. val alpnBootVersion = alpnBootVersion() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3348b69c6..2061a60f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ com-squareup-moshi = "1.14.0" com-squareup-okio = "3.3.0" de-mannodermaus-junit5 = "1.3.0" graalvm = "22.3.2" +kotlinx-serialization = "1.5.0" org-bouncycastle = "1.72" org-conscrypt = "2.5.2" org-jetbrains-coroutines = "1.6.4" @@ -41,6 +42,7 @@ gradlePlugin-dokka = "org.jetbrains.dokka:dokka-gradle-plugin:1.8.10" gradlePlugin-errorprone = "net.ltgt.gradle:gradle-errorprone-plugin:3.0.1" gradlePlugin-graal = "com.palantir.graal:gradle-graal:0.12.0" gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "org-jetbrains-kotlin" } +gradlePlugin-kotlinSerialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "org-jetbrains-kotlin" } gradlePlugin-mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.24.0" gradlePlugin-shadow = "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" gradlePlugin-spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.18.0" @@ -69,6 +71,8 @@ kotlin-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "org-jetbrains-kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "org-jetbrains-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "org-jetbrains-coroutines" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } nativeImageSvm = { module = "org.graalvm.nativeimage:svm", version.ref = "graalvm" } openjsse = "org.openjsse:openjsse:1.1.12" playservices-safetynet = "com.google.android.gms:play-services-safetynet:18.0.1" @@ -82,3 +86,4 @@ squareup-okhttp-icu = "com.squareup.okhttpicu:okhttp-icu:0.2.0" squareup-kotlinPoet = "com.squareup:kotlinpoet:1.13.1" squareup-okio = { module = "com.squareup.okio:okio", version.ref = "com-squareup-okio" } squareup-okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "com-squareup-okio" } +squareup-okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "com-squareup-okio" } diff --git a/okhttp-testing-support/build.gradle.kts b/okhttp-testing-support/build.gradle.kts index aee44af4e..fe8915e5e 100644 --- a/okhttp-testing-support/build.gradle.kts +++ b/okhttp-testing-support/build.gradle.kts @@ -7,8 +7,23 @@ kotlin { jvm { withJava() } + if (kmpJsEnabled) { + js(IR) { + nodejs() + } + } sourceSets { + val commonMain by getting { + dependencies { + api(libs.squareup.okio) + } + } + val jsMain by getting { + dependencies { + implementation(libs.squareup.okio.nodefilesystem) + } + } val jvmMain by getting { dependencies { api(projects.okhttp) diff --git a/okhttp-testing-support/src/commonMain/kotlin/okhttp3/TestUtilCommon.kt b/okhttp-testing-support/src/commonMain/kotlin/okhttp3/TestUtilCommon.kt new file mode 100644 index 000000000..678f31e2b --- /dev/null +++ b/okhttp-testing-support/src/commonMain/kotlin/okhttp3/TestUtilCommon.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3 + +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toPath + +val okHttpRoot: Path + get() = getEnv("OKHTTP_ROOT")!!.toPath() + +expect val SYSTEM_FILE_SYSTEM: FileSystem + +expect fun getEnv(name: String): String? + +expect val isJvm: Boolean diff --git a/okhttp-testing-support/src/jsMain/kotlin/okhttp3/TestUtilJs.kt b/okhttp-testing-support/src/jsMain/kotlin/okhttp3/TestUtilJs.kt new file mode 100644 index 000000000..f77acbd22 --- /dev/null +++ b/okhttp-testing-support/src/jsMain/kotlin/okhttp3/TestUtilJs.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3 + +import okio.FileSystem +import okio.NodeJsFileSystem + +actual fun getEnv(name: String) = js("globalThis.process.env[name]") as String? + +actual val SYSTEM_FILE_SYSTEM: FileSystem = NodeJsFileSystem + +actual val isJvm = false diff --git a/okhttp-testing-support/src/jvmMain/kotlin/okhttp3/TestUtil.kt b/okhttp-testing-support/src/jvmMain/kotlin/okhttp3/TestUtilJvm.kt similarity index 96% rename from okhttp-testing-support/src/jvmMain/kotlin/okhttp3/TestUtil.kt rename to okhttp-testing-support/src/jvmMain/kotlin/okhttp3/TestUtilJvm.kt index 6b2a8f116..47d58233e 100644 --- a/okhttp-testing-support/src/jvmMain/kotlin/okhttp3/TestUtil.kt +++ b/okhttp-testing-support/src/jvmMain/kotlin/okhttp3/TestUtilJvm.kt @@ -22,6 +22,7 @@ import java.net.UnknownHostException import java.util.Arrays import okhttp3.internal.http2.Header import okio.Buffer +import okio.FileSystem import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.Assumptions.assumeTrue @@ -122,3 +123,9 @@ object TestUtil { block(suppressed.toList()) } } + +actual fun getEnv(name: String) = System.getenv(name) + +actual val SYSTEM_FILE_SYSTEM = FileSystem.SYSTEM + +actual val isJvm = true diff --git a/okhttp/build.gradle.kts b/okhttp/build.gradle.kts index 26e8a479c..fe04b8a70 100644 --- a/okhttp/build.gradle.kts +++ b/okhttp/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinCompile plugins { kotlin("multiplatform") + kotlin("plugin.serialization") id("org.jetbrains.dokka") id("com.vanniktech.maven.publish.base") id("binary-compatibility-validator") @@ -29,8 +30,6 @@ kotlin { } } } - browser { - } } } @@ -43,9 +42,12 @@ kotlin { } val commonTest by getting { dependencies { - implementation(libs.kotlin.test.common) + implementation(projects.okhttpTestingSupport) + implementation(libs.assertk) implementation(libs.kotlin.test.annotations) - api(libs.assertk) + implementation(libs.kotlin.test.common) + implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) } } val nonJvmMain = create("nonJvmMain") { @@ -83,7 +85,6 @@ kotlin { kotlin.srcDir("$buildDir/generated/sources/idnaMappingTable") dependencies { dependsOn(commonTest) - implementation(projects.okhttpTestingSupport) implementation(projects.okhttpTls) implementation(projects.okhttpUrlconnection) implementation(projects.mockwebserver3) @@ -112,6 +113,7 @@ kotlin { } getByName("jsMain") { + kotlin.srcDir("$buildDir/generated/sources/idnaMappingTable") dependencies { dependsOn(nonJvmMain) api(libs.squareup.okio) diff --git a/okhttp/src/jvmTest/java/okhttp3/WebPlatformToAsciiData.kt b/okhttp/src/commonTest/kotlin/okhttp3/WebPlatformToAsciiData.kt similarity index 67% rename from okhttp/src/jvmTest/java/okhttp3/WebPlatformToAsciiData.kt rename to okhttp/src/commonTest/kotlin/okhttp3/WebPlatformToAsciiData.kt index 0d7e13461..ac1f3019e 100644 --- a/okhttp/src/jvmTest/java/okhttp3/WebPlatformToAsciiData.kt +++ b/okhttp/src/commonTest/kotlin/okhttp3/WebPlatformToAsciiData.kt @@ -15,17 +15,16 @@ */ package okhttp3 -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import okio.FileSystem -import okio.Path.Companion.toPath +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json /** * A test from the [Web Platform To ASCII](https://github.com/web-platform-tests/wpt/blob/master/url/resources/toascii.json). * * Each test is a line of the file `toascii.json`. */ +@Serializable class WebPlatformToAsciiData { var input: String? = null var output: String? = null @@ -35,15 +34,9 @@ class WebPlatformToAsciiData { companion object { fun load(): List { - val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - - @OptIn(ExperimentalStdlibApi::class) - val adapter = moshi.adapter>() - - return FileSystem.RESOURCES.read("/web-platform-test-toascii.json".toPath()) { - adapter.fromJson(this)!! + val path = okHttpRoot / "okhttp/src/commonTest/resources/web-platform-test-toascii.json" + return SYSTEM_FILE_SYSTEM.read(path) { + Json.decodeFromString>(readUtf8()) } } } diff --git a/okhttp/src/jvmTest/java/okhttp3/WebPlatformToAsciiTest.kt b/okhttp/src/commonTest/kotlin/okhttp3/WebPlatformToAsciiTest.kt similarity index 75% rename from okhttp/src/jvmTest/java/okhttp3/WebPlatformToAsciiTest.kt rename to okhttp/src/commonTest/kotlin/okhttp3/WebPlatformToAsciiTest.kt index 620651135..20be68549 100644 --- a/okhttp/src/jvmTest/java/okhttp3/WebPlatformToAsciiTest.kt +++ b/okhttp/src/commonTest/kotlin/okhttp3/WebPlatformToAsciiTest.kt @@ -17,14 +17,12 @@ package okhttp3 import assertk.assertThat import assertk.assertions.isEqualTo -import assertk.assertions.isNotNull +import kotlin.test.Test import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.TestFactory /** Runs the web platform ToAscii tests. */ class WebPlatformToAsciiTest { - val knownFailures = setOf( + val knownFailuresJvm = setOf( // OkHttp rejects empty labels. "x..xn--zca", "x..ß", @@ -65,24 +63,47 @@ class WebPlatformToAsciiTest { "نامه‌ای", ) - @TestFactory - fun testFactory(): List { - val list = WebPlatformToAsciiData.load() - return list.map { entry -> - DynamicTest.dynamicTest(entry.input!!) { - var failure: AssertionError? = null - try { - testToAscii(entry.input!!, entry.output, entry.comment) - } catch (e: AssertionError) { - failure = e - } + val knownFailuresNonJvm = knownFailuresJvm + setOf( + // Punycode is not implemented. + "a†--", + "-†", + "≠", + "≮", + "≯", + ) - setOf( + // Non-Transitional is fixed. + "xn--a.ß", + ) - if (entry.input in knownFailures) { - assertThat(failure).isNotNull() - } else { - if (failure != null) throw failure - } + @Test + fun test() { + val knownFailures = when { + isJvm -> knownFailuresJvm + else -> knownFailuresNonJvm + } + + val list = WebPlatformToAsciiData.load() + val failures = mutableListOf() + for (entry in list) { + var failure: Throwable? = null + try { + testToAscii(entry.input!!, entry.output, entry.comment) + } catch (e: Throwable) { + failure = e } + + if (entry.input in knownFailures) { + if (failure == null) failures += AssertionError("known failure didn't fail: $entry") + } else { + if (failure != null) failures += failure + } + } + + if (failures.isNotEmpty()) { + for (failure in failures) { + println(failure) + } + throw failures.first() } } diff --git a/okhttp/src/jvmTest/resources/web-platform-test-toascii.json b/okhttp/src/commonTest/resources/web-platform-test-toascii.json similarity index 100% rename from okhttp/src/jvmTest/resources/web-platform-test-toascii.json rename to okhttp/src/commonTest/resources/web-platform-test-toascii.json diff --git a/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HostnamesNonJvm.kt b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HostnamesNonJvm.kt index 8d011b2dc..10abc50cb 100644 --- a/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HostnamesNonJvm.kt +++ b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HostnamesNonJvm.kt @@ -16,8 +16,27 @@ package okhttp3.internal import com.squareup.okhttpicu.SYSTEM_NORMALIZER +import okhttp3.internal.idn.IDNA_MAPPING_TABLE +import okio.Buffer internal actual fun idnToAscii(host: String): String { - // TODO implement properly - return SYSTEM_NORMALIZER.normalizeNfc(host).lowercase() + val bufferA = Buffer().writeUtf8(host) + val bufferB = Buffer() + + // 1. Map, from bufferA to bufferB. + while (!bufferA.exhausted()) { + val codePoint = bufferA.readUtf8CodePoint() + require(IDNA_MAPPING_TABLE.map(codePoint, bufferB)) { + "disallowed code point: $codePoint" + } + } + + // 2. Normalize, from bufferB to bufferA. + val normalized = SYSTEM_NORMALIZER.normalizeNfc(bufferB.readUtf8()) + bufferA.writeUtf8(normalized) + + // 3. For each label, convert/validate Punycode. + // TODO. + + return bufferA.readUtf8() }