diff --git a/ktor-http/api/ktor-http.api b/ktor-http/api/ktor-http.api index f369eca9d6a..3b3da432dff 100644 --- a/ktor-http/api/ktor-http.api +++ b/ktor-http/api/ktor-http.api @@ -262,7 +262,7 @@ public final class io/ktor/http/ContentTypesKt { public static final fun withCharsetIfNeeded (Lio/ktor/http/ContentType;Ljava/nio/charset/Charset;)Lio/ktor/http/ContentType; } -public final class io/ktor/http/Cookie { +public final class io/ktor/http/Cookie : java/io/Serializable { public static final field Companion Lio/ktor/http/Cookie$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Lio/ktor/http/CookieEncoding;Ljava/lang/Integer;Lio/ktor/util/date/GMTDate;Ljava/lang/String;Ljava/lang/String;ZZLjava/util/Map;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/ktor/http/CookieEncoding;Ljava/lang/Integer;Lio/ktor/util/date/GMTDate;Ljava/lang/String;Ljava/lang/String;ZZLjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -966,7 +966,7 @@ public final class io/ktor/http/URLParserKt { public static final fun takeFrom (Lio/ktor/http/URLBuilder;Ljava/lang/String;)Lio/ktor/http/URLBuilder; } -public final class io/ktor/http/URLProtocol { +public final class io/ktor/http/URLProtocol : java/io/Serializable { public static final field Companion Lio/ktor/http/URLProtocol$Companion; public fun (Ljava/lang/String;I)V public final fun component1 ()Ljava/lang/String; @@ -1026,7 +1026,7 @@ public final class io/ktor/http/UnsafeHeaderException : java/lang/IllegalArgumen public fun (Ljava/lang/String;)V } -public final class io/ktor/http/Url { +public final class io/ktor/http/Url : java/io/Serializable { public static final field Companion Lio/ktor/http/Url$Companion; public fun equals (Ljava/lang/Object;)Z public final fun getEncodedFragment ()Ljava/lang/String; @@ -1053,6 +1053,7 @@ public final class io/ktor/http/Url { } public final class io/ktor/http/Url$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class io/ktor/http/UrlKt { @@ -1060,6 +1061,15 @@ public final class io/ktor/http/UrlKt { public static final fun getProtocolWithAuthority (Lio/ktor/http/Url;)Ljava/lang/String; } +public final class io/ktor/http/UrlSerializer : kotlinx/serialization/KSerializer { + public fun ()V + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/ktor/http/Url; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/ktor/http/Url;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +} + public final class io/ktor/http/auth/AuthScheme { public static final field Basic Ljava/lang/String; public static final field Bearer Ljava/lang/String; diff --git a/ktor-http/api/ktor-http.klib.api b/ktor-http/api/ktor-http.klib.api index 7bd6b70711b..7ab441b73c7 100644 --- a/ktor-http/api/ktor-http.klib.api +++ b/ktor-http/api/ktor-http.klib.api @@ -573,7 +573,7 @@ final class io.ktor.http/ContentType : io.ktor.http/HeaderValueWithParameters { } } -final class io.ktor.http/Cookie { // io.ktor.http/Cookie|null[0] +final class io.ktor.http/Cookie : io.ktor.utils.io/JvmSerializable { // io.ktor.http/Cookie|null[0] constructor (kotlin/String, kotlin/String, io.ktor.http/CookieEncoding = ..., kotlin/Int? = ..., io.ktor.util.date/GMTDate? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin.collections/Map = ...) // io.ktor.http/Cookie.|(kotlin.String;kotlin.String;io.ktor.http.CookieEncoding;kotlin.Int?;io.ktor.util.date.GMTDate?;kotlin.String?;kotlin.String?;kotlin.Boolean;kotlin.Boolean;kotlin.collections.Map){}[0] final val domain // io.ktor.http/Cookie.domain|{}domain[0] @@ -1048,7 +1048,7 @@ final class io.ktor.http/URLParserException : kotlin/IllegalStateException { // constructor (kotlin/String, kotlin/Throwable) // io.ktor.http/URLParserException.|(kotlin.String;kotlin.Throwable){}[0] } -final class io.ktor.http/URLProtocol { // io.ktor.http/URLProtocol|null[0] +final class io.ktor.http/URLProtocol : io.ktor.utils.io/JvmSerializable { // io.ktor.http/URLProtocol|null[0] constructor (kotlin/String, kotlin/Int) // io.ktor.http/URLProtocol.|(kotlin.String;kotlin.Int){}[0] final val defaultPort // io.ktor.http/URLProtocol.defaultPort|{}defaultPort[0] @@ -1085,7 +1085,7 @@ final class io.ktor.http/UnsafeHeaderException : kotlin/IllegalArgumentException constructor (kotlin/String) // io.ktor.http/UnsafeHeaderException.|(kotlin.String){}[0] } -final class io.ktor.http/Url { // io.ktor.http/Url|null[0] +final class io.ktor.http/Url : io.ktor.utils.io/JvmSerializable { // io.ktor.http/Url|null[0] final val encodedFragment // io.ktor.http/Url.encodedFragment|{}encodedFragment[0] final fun (): kotlin/String // io.ktor.http/Url.encodedFragment.|(){}[0] final val encodedPassword // io.ktor.http/Url.encodedPassword|{}encodedPassword[0] @@ -1129,7 +1129,19 @@ final class io.ktor.http/Url { // io.ktor.http/Url|null[0] final fun hashCode(): kotlin/Int // io.ktor.http/Url.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // io.ktor.http/Url.toString|toString(){}[0] - final object Companion // io.ktor.http/Url.Companion|null[0] + final object Companion { // io.ktor.http/Url.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // io.ktor.http/Url.Companion.serializer|serializer(){}[0] + } +} + +final class io.ktor.http/UrlSerializer : kotlinx.serialization/KSerializer { // io.ktor.http/UrlSerializer|null[0] + constructor () // io.ktor.http/UrlSerializer.|(){}[0] + + final val descriptor // io.ktor.http/UrlSerializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // io.ktor.http/UrlSerializer.descriptor.|(){}[0] + + final fun deserialize(kotlinx.serialization.encoding/Decoder): io.ktor.http/Url // io.ktor.http/UrlSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, io.ktor.http/Url) // io.ktor.http/UrlSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;io.ktor.http.Url){}[0] } sealed class io.ktor.http.auth/HttpAuthHeader { // io.ktor.http.auth/HttpAuthHeader|null[0] diff --git a/ktor-http/build.gradle.kts b/ktor-http/build.gradle.kts index 0ec58b576f7..bb98a0ec6c1 100644 --- a/ktor-http/build.gradle.kts +++ b/ktor-http/build.gradle.kts @@ -14,5 +14,12 @@ kotlin { api(libs.kotlinx.serialization.core) } } + jvmTest { + dependencies { + implementation(project(":ktor-shared:ktor-junit")) + implementation(project(":ktor-shared:ktor-serialization:ktor-serialization-kotlinx")) + implementation(project(":ktor-shared:ktor-serialization:ktor-serialization-kotlinx:ktor-serialization-kotlinx-json")) + } + } } } diff --git a/ktor-http/common/src/io/ktor/http/Cookie.kt b/ktor-http/common/src/io/ktor/http/Cookie.kt index 4db6b279f8e..8470f6565df 100644 --- a/ktor-http/common/src/io/ktor/http/Cookie.kt +++ b/ktor-http/common/src/io/ktor/http/Cookie.kt @@ -6,6 +6,7 @@ package io.ktor.http import io.ktor.util.* import io.ktor.util.date.* +import io.ktor.utils.io.* import kotlinx.serialization.* import kotlin.jvm.* @@ -37,7 +38,17 @@ public data class Cookie( val secure: Boolean = false, val httpOnly: Boolean = false, val extensions: Map = emptyMap() -) +) : JvmSerializable { + private fun writeReplace(): Any = JvmSerializerReplacement(CookieJvmSerializer, this) +} + +internal object CookieJvmSerializer : JvmSerializer { + override fun jvmSerialize(value: Cookie): ByteArray = + renderSetCookieHeader(value).encodeToByteArray() + + override fun jvmDeserialize(value: ByteArray): Cookie = + parseServerSetCookieHeader(value.decodeToString()) +} /** * Cooke encoding strategy diff --git a/ktor-http/common/src/io/ktor/http/URLProtocol.kt b/ktor-http/common/src/io/ktor/http/URLProtocol.kt index 5095eb6fd4c..513e6287014 100644 --- a/ktor-http/common/src/io/ktor/http/URLProtocol.kt +++ b/ktor-http/common/src/io/ktor/http/URLProtocol.kt @@ -5,13 +5,14 @@ package io.ktor.http import io.ktor.util.* +import io.ktor.utils.io.* /** * Represents URL protocol * @property name of protocol (schema) * @property defaultPort default port for protocol or `-1` if not known */ -public data class URLProtocol(val name: String, val defaultPort: Int) { +public data class URLProtocol(val name: String, val defaultPort: Int) : JvmSerializable { init { require(name.all { it.isLowerCase() }) { "All characters should be lower case" } } diff --git a/ktor-http/common/src/io/ktor/http/Url.kt b/ktor-http/common/src/io/ktor/http/Url.kt index d50c96d8c4a..60c94e0aeec 100644 --- a/ktor-http/common/src/io/ktor/http/Url.kt +++ b/ktor-http/common/src/io/ktor/http/Url.kt @@ -4,6 +4,11 @@ package io.ktor.http +import io.ktor.utils.io.* +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + /** * Represents an immutable URL * @@ -18,6 +23,7 @@ package io.ktor.http * @property password password part of URL * @property trailingQuery keep trailing question character even if there are no query parameters */ +@Serializable(with = UrlSerializer::class) public class Url internal constructor( protocol: URLProtocol?, public val host: String, @@ -29,7 +35,7 @@ public class Url internal constructor( public val password: String?, public val trailingQuery: Boolean, private val urlString: String -) { +) : JvmSerializable { init { require(specifiedPort in 0..65535) { "Port must be between 0 and 65535, or $DEFAULT_PORT if not set. Provided: $specifiedPort" @@ -222,6 +228,8 @@ public class Url internal constructor( return urlString.hashCode() } + private fun writeReplace(): Any = JvmSerializerReplacement(UrlJvmSerializer, this) + public companion object } @@ -254,3 +262,22 @@ internal val Url.encodedUserAndPassword: String get() = buildString { appendUserAndPassword(encodedUser, encodedPassword) } + +public class UrlSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Url", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Url = + Url(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: Url) { + encoder.encodeString(value.toString()) + } +} + +internal object UrlJvmSerializer : JvmSerializer { + override fun jvmSerialize(value: Url): ByteArray = + value.toString().encodeToByteArray() + + override fun jvmDeserialize(value: ByteArray): Url = + Url(value.decodeToString()) +} diff --git a/ktor-http/jvm/test/io/ktor/tests/http/SerializableTest.kt b/ktor-http/jvm/test/io/ktor/tests/http/SerializableTest.kt new file mode 100644 index 00000000000..bc2e21fc599 --- /dev/null +++ b/ktor-http/jvm/test/io/ktor/tests/http/SerializableTest.kt @@ -0,0 +1,24 @@ +// ktlint-disable filename +/* + * Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.tests.http + +import io.ktor.http.* +import io.ktor.junit.* +import kotlin.test.* + +class SerializableTest { + @Test + fun urlTest() { + val url = Url("https://localhost/path?key=value#fragment") + assertEquals(url, assertSerializable(url)) + } + + @Test + fun cookieTest() { + val cookie = Cookie("key", "value") + assertEquals(cookie, assertSerializable(cookie)) + } +} diff --git a/ktor-io/api/ktor-io.api b/ktor-io/api/ktor-io.api index 8cb92a34138..147672b699b 100644 --- a/ktor-io/api/ktor-io.api +++ b/ktor-io/api/ktor-io.api @@ -209,6 +209,17 @@ public final class io/ktor/utils/io/CountedByteWriteChannelKt { public static final fun counted (Lio/ktor/utils/io/ByteWriteChannel;)Lio/ktor/utils/io/CountedByteWriteChannel; } +public final class io/ktor/utils/io/DefaultJvmSerializerReplacement : java/io/Externalizable { + public static final field Companion Lio/ktor/utils/io/DefaultJvmSerializerReplacement$Companion; + public fun ()V + public fun (Lio/ktor/utils/io/JvmSerializer;Ljava/lang/Object;)V + public fun readExternal (Ljava/io/ObjectInput;)V + public fun writeExternal (Ljava/io/ObjectOutput;)V +} + +public final class io/ktor/utils/io/DefaultJvmSerializerReplacement$Companion { +} + public final class io/ktor/utils/io/DeprecationKt { public static final fun readText (Lkotlinx/io/Source;)Ljava/lang/String; public static final fun release (Lkotlinx/io/Sink;)V @@ -217,6 +228,15 @@ public final class io/ktor/utils/io/DeprecationKt { public abstract interface annotation class io/ktor/utils/io/InternalAPI : java/lang/annotation/Annotation { } +public final class io/ktor/utils/io/JvmSerializable_jvmKt { + public static final fun JvmSerializerReplacement (Lio/ktor/utils/io/JvmSerializer;Ljava/lang/Object;)Ljava/lang/Object; +} + +public abstract interface class io/ktor/utils/io/JvmSerializer : java/io/Serializable { + public abstract fun jvmDeserialize ([B)Ljava/lang/Object; + public abstract fun jvmSerialize (Ljava/lang/Object;)[B +} + public abstract interface annotation class io/ktor/utils/io/KtorDsl : java/lang/annotation/Annotation { } diff --git a/ktor-io/api/ktor-io.klib.api b/ktor-io/api/ktor-io.klib.api index cba7dc34d45..b56b03ef723 100644 --- a/ktor-io/api/ktor-io.klib.api +++ b/ktor-io/api/ktor-io.klib.api @@ -53,6 +53,11 @@ abstract interface <#A: kotlin/Any> io.ktor.utils.io.pool/ObjectPool : kotlin/Au open fun close() // io.ktor.utils.io.pool/ObjectPool.close|close(){}[0] } +abstract interface <#A: kotlin/Any?> io.ktor.utils.io/JvmSerializer : io.ktor.utils.io/JvmSerializable { // io.ktor.utils.io/JvmSerializer|null[0] + abstract fun jvmDeserialize(kotlin/ByteArray): #A // io.ktor.utils.io/JvmSerializer.jvmDeserialize|jvmDeserialize(kotlin.ByteArray){}[0] + abstract fun jvmSerialize(#A): kotlin/ByteArray // io.ktor.utils.io/JvmSerializer.jvmSerialize|jvmSerialize(1:0){}[0] +} + abstract interface io.ktor.utils.io.core/Closeable : kotlin/AutoCloseable { // io.ktor.utils.io.core/Closeable|null[0] abstract fun close() // io.ktor.utils.io.core/Closeable.close|close(){}[0] } @@ -97,6 +102,8 @@ abstract interface io.ktor.utils.io/ChannelJob { // io.ktor.utils.io/ChannelJob| abstract fun (): kotlinx.coroutines/Job // io.ktor.utils.io/ChannelJob.job.|(){}[0] } +abstract interface io.ktor.utils.io/JvmSerializable // io.ktor.utils.io/JvmSerializable|null[0] + abstract class <#A: kotlin/Any> io.ktor.utils.io.pool/DefaultPool : io.ktor.utils.io.pool/ObjectPool<#A> { // io.ktor.utils.io.pool/DefaultPool|null[0] constructor (kotlin/Int) // io.ktor.utils.io.pool/DefaultPool.|(kotlin.Int){}[0] @@ -399,6 +406,7 @@ final fun (kotlinx.io/Source).io.ktor.utils.io.core/readTextExactCharacters(kotl final fun (kotlinx.io/Source).io.ktor.utils.io.core/release() // io.ktor.utils.io.core/release|release@kotlinx.io.Source(){}[0] final fun (kotlinx.io/Source).io.ktor.utils.io.core/takeWhile(kotlin/Function1) // io.ktor.utils.io.core/takeWhile|takeWhile@kotlinx.io.Source(kotlin.Function1){}[0] final fun (kotlinx.io/Source).io.ktor.utils.io/readText(): kotlin/String // io.ktor.utils.io/readText|readText@kotlinx.io.Source(){}[0] +final fun <#A: kotlin/Any> io.ktor.utils.io/JvmSerializerReplacement(io.ktor.utils.io/JvmSerializer<#A>, #A): kotlin/Any // io.ktor.utils.io/JvmSerializerReplacement|JvmSerializerReplacement(io.ktor.utils.io.JvmSerializer<0:0>;0:0){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.io/Sink).io.ktor.utils.io.core/preview(kotlin/Function1): #A // io.ktor.utils.io.core/preview|preview@kotlinx.io.Sink(kotlin.Function1){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.io/Source).io.ktor.utils.io.core/preview(kotlin/Function1): #A // io.ktor.utils.io.core/preview|preview@kotlinx.io.Source(kotlin.Function1){0§}[0] final fun <#A: kotlin/Any?> io.ktor.utils.io.core/withMemory(kotlin/Int, kotlin/Function1): #A // io.ktor.utils.io.core/withMemory|withMemory(kotlin.Int;kotlin.Function1){0§}[0] diff --git a/ktor-io/common/src/io/ktor/utils/io/JvmSerializable.kt b/ktor-io/common/src/io/ktor/utils/io/JvmSerializable.kt new file mode 100644 index 00000000000..490fdef56c8 --- /dev/null +++ b/ktor-io/common/src/io/ktor/utils/io/JvmSerializable.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.utils.io + +public expect interface JvmSerializable + +public interface JvmSerializer : JvmSerializable { + public fun jvmSerialize(value: T): ByteArray + public fun jvmDeserialize(value: ByteArray): T +} + +public expect fun JvmSerializerReplacement(serializer: JvmSerializer, value: T): Any + +internal object DummyJvmSimpleSerializerReplacement diff --git a/ktor-io/jsAndWasmShared/src/io/ktor/utils/io/JvmSerializable.jsAndWasmShared.kt b/ktor-io/jsAndWasmShared/src/io/ktor/utils/io/JvmSerializable.jsAndWasmShared.kt new file mode 100644 index 00000000000..5fcedcdd9b3 --- /dev/null +++ b/ktor-io/jsAndWasmShared/src/io/ktor/utils/io/JvmSerializable.jsAndWasmShared.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.utils.io + +/** Alias for `java.io.Serializable` on JVM. Empty interface otherwise. */ +public actual interface JvmSerializable + +public actual fun JvmSerializerReplacement(serializer: JvmSerializer, value: T): Any = + DummyJvmSimpleSerializerReplacement diff --git a/ktor-io/jvm/src/io/ktor/utils/io/JvmSerializable.jvm.kt b/ktor-io/jvm/src/io/ktor/utils/io/JvmSerializable.jvm.kt new file mode 100644 index 00000000000..875bf5b8edb --- /dev/null +++ b/ktor-io/jvm/src/io/ktor/utils/io/JvmSerializable.jvm.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.utils.io + +import java.io.* + +public actual typealias JvmSerializable = Serializable + +@Suppress("UNCHECKED_CAST") +public actual fun JvmSerializerReplacement(serializer: JvmSerializer, value: T): Any = + DefaultJvmSerializerReplacement(serializer, value) + +@PublishedApi // IMPORTANT: changing the class name would result in serialization incompatibility +internal class DefaultJvmSerializerReplacement( + private var serializer: JvmSerializer?, + private var value: T? +) : Externalizable { + constructor() : this(null, null) + + override fun writeExternal(out: ObjectOutput) { + out.writeObject(serializer) + out.writeObject(serializer!!.jvmSerialize(value!!)) + } + + @Suppress("UNCHECKED_CAST") + override fun readExternal(`in`: ObjectInput) { + serializer = `in`.readObject() as JvmSerializer + value = serializer!!.jvmDeserialize(`in`.readObject() as ByteArray) + } + + private fun readResolve(): Any = + value!! + + companion object { + private const val serialVersionUID: Long = 0L + } +} diff --git a/ktor-io/posix/src/io/ktor/utils/io/JvmSerializable.posix.kt b/ktor-io/posix/src/io/ktor/utils/io/JvmSerializable.posix.kt new file mode 100644 index 00000000000..5fcedcdd9b3 --- /dev/null +++ b/ktor-io/posix/src/io/ktor/utils/io/JvmSerializable.posix.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.utils.io + +/** Alias for `java.io.Serializable` on JVM. Empty interface otherwise. */ +public actual interface JvmSerializable + +public actual fun JvmSerializerReplacement(serializer: JvmSerializer, value: T): Any = + DummyJvmSimpleSerializerReplacement diff --git a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/Assertions.kt b/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/Assertions.kt index f5e0d07b1e8..f3857c1e453 100644 --- a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/Assertions.kt +++ b/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/Assertions.kt @@ -4,6 +4,8 @@ package io.ktor.junit +import java.io.* + /** * Convenience function for asserting on all elements of a collection. */ @@ -29,3 +31,10 @@ fun assertAll(collection: Iterable, assertion: (T) -> Unit) { } ) } + +inline fun assertSerializable(obj: T): T { + val encoded = ByteArrayOutputStream().also { + ObjectOutputStream(it).writeObject(obj) + }.toByteArray() + return ObjectInputStream(encoded.inputStream()).readObject() as T +}