diff --git a/benchmarks/src/kotlinMain/kotlin/io/rsocket/kotlin/benchmarks/RSocketKotlinBenchmark.kt b/benchmarks/src/kotlinMain/kotlin/io/rsocket/kotlin/benchmarks/RSocketKotlinBenchmark.kt index 08f143352..57ea066b5 100644 --- a/benchmarks/src/kotlinMain/kotlin/io/rsocket/kotlin/benchmarks/RSocketKotlinBenchmark.kt +++ b/benchmarks/src/kotlinMain/kotlin/io/rsocket/kotlin/benchmarks/RSocketKotlinBenchmark.kt @@ -16,6 +16,7 @@ package io.rsocket.kotlin.benchmarks +import io.ktor.utils.io.core.* import io.rsocket.kotlin.* import io.rsocket.kotlin.core.* import io.rsocket.kotlin.payload.* @@ -62,9 +63,9 @@ class RSocketKotlinBenchmark : RSocketBenchmark() { } } - override fun createPayload(size: Int): Payload = if (size == 0) Payload.Empty else Payload.wrap( - data = ByteArray(size / 2).also { Random.nextBytes(it) }, - metadata = ByteArray(size / 2).also { Random.nextBytes(it) } + override fun createPayload(size: Int): Payload = if (size == 0) Payload.Empty else Payload( + data = ByteReadPacket(ByteArray(size / 2).also { Random.nextBytes(it) }), + metadata = ByteReadPacket(ByteArray(size / 2).also { Random.nextBytes(it) }) ) override fun releasePayload(payload: Payload) { diff --git a/build.gradle.kts b/build.gradle.kts index 28f4a1da8..32d764063 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -206,13 +206,18 @@ subprojects { if (name.contains("test", ignoreCase = true) || project.name == "rsocket-test") { useExperimentalAnnotation("kotlin.time.ExperimentalTime") useExperimentalAnnotation("kotlin.ExperimentalStdlibApi") + useExperimentalAnnotation("kotlinx.coroutines.ExperimentalCoroutinesApi") useExperimentalAnnotation("kotlinx.coroutines.InternalCoroutinesApi") useExperimentalAnnotation("kotlinx.coroutines.ObsoleteCoroutinesApi") useExperimentalAnnotation("kotlinx.coroutines.FlowPreview") + useExperimentalAnnotation("io.ktor.util.KtorExperimentalAPI") useExperimentalAnnotation("io.ktor.util.InternalAPI") useExperimentalAnnotation("io.ktor.utils.io.core.internal.DangerousInternalIoApi") + + useExperimentalAnnotation("io.rsocket.kotlin.TransportApi") + useExperimentalAnnotation("io.rsocket.kotlin.ExperimentalMetadataApi") } } } diff --git a/examples/interactions/src/jvmMain/kotlin/MetadataExample.kt b/examples/interactions/src/jvmMain/kotlin/MetadataExample.kt new file mode 100644 index 000000000..29a04bf97 --- /dev/null +++ b/examples/interactions/src/jvmMain/kotlin/MetadataExample.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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. + */ + +import io.ktor.utils.io.core.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.metadata.* +import io.rsocket.kotlin.payload.* + +@OptIn(ExperimentalMetadataApi::class) +public fun main() { + val p1 = buildPayload { + data("Hello") + + metadata("some text") + } + + println("Case: 1") + println(p1.data.readText()) + println(p1.metadata?.readText()) + + val p2 = buildPayload { + data("Hello") + + metadata(RoutingMetadata("tag1", "tag2")) + } + + val tags = p2.metadata?.read(RoutingMetadata)?.tags.orEmpty() + println("Case: 2") + println(tags) + + + val p3 = buildPayload { + data("hello") + + compositeMetadata { + add(RoutingMetadata("tag3", "t4")) + add(RawMetadata(WellKnownMimeType.ApplicationJson, buildPacket { writeText("{s: 2}") })) + } + } + + val cm = p3.metadata!!.read(CompositeMetadata) + println("Case: 3") + println(cm[RoutingMetadata].tags) + println(cm[WellKnownMimeType.ApplicationJson].readText()) +} diff --git a/examples/interactions/src/jvmMain/kotlin/shared.kt b/examples/interactions/src/jvmMain/kotlin/shared.kt new file mode 100644 index 000000000..11bd4fdd4 --- /dev/null +++ b/examples/interactions/src/jvmMain/kotlin/shared.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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. + */ + +import io.ktor.utils.io.core.* +import io.rsocket.kotlin.payload.* + +/** + * Simple custom [Payload] factory function with string data and metadata. + * Has almost no overhead over call to [Payload] constructor with data and metadata as [ByteReadPacket]. + * Similar functions can be created for all needed use cases + */ +fun Payload(data: String, metadata: String? = null): Payload = buildPayload { + data(data) + if (metadata != null) metadata(metadata) +} diff --git a/examples/multiplatform-chat/src/clientMain/kotlin/Api.kt b/examples/multiplatform-chat/src/clientMain/kotlin/Api.kt index 610233831..e55ae2123 100644 --- a/examples/multiplatform-chat/src/clientMain/kotlin/Api.kt +++ b/examples/multiplatform-chat/src/clientMain/kotlin/Api.kt @@ -51,6 +51,6 @@ suspend fun connectToApiUsingTCP(name: String): Api { private fun connector(name: String): RSocketConnector = RSocketConnector { connectionConfig { - setupPayload { Payload(name) } + setupPayload { buildPayload { data(name) } } } } diff --git a/examples/multiplatform-chat/src/clientMain/kotlin/PayloadWithRoute.kt b/examples/multiplatform-chat/src/clientMain/kotlin/PayloadWithRoute.kt index 35dd6d19f..75597e68a 100644 --- a/examples/multiplatform-chat/src/clientMain/kotlin/PayloadWithRoute.kt +++ b/examples/multiplatform-chat/src/clientMain/kotlin/PayloadWithRoute.kt @@ -15,9 +15,10 @@ */ import io.ktor.utils.io.core.* +import io.rsocket.kotlin.metadata.* import io.rsocket.kotlin.payload.* -fun Payload(route: String, packet: ByteReadPacket): Payload = Payload { +fun Payload(route: String, packet: ByteReadPacket): Payload = buildPayload { data(packet) - metadata(route) + metadata(RoutingMetadata(route)) } diff --git a/examples/multiplatform-chat/src/commonMain/kotlin/Serialization.kt b/examples/multiplatform-chat/src/commonMain/kotlin/Serialization.kt index e273a6711..88aae2f1f 100644 --- a/examples/multiplatform-chat/src/commonMain/kotlin/Serialization.kt +++ b/examples/multiplatform-chat/src/commonMain/kotlin/Serialization.kt @@ -15,6 +15,7 @@ */ import io.ktor.utils.io.core.* +import io.rsocket.kotlin.metadata.* import io.rsocket.kotlin.payload.* import kotlinx.serialization.* import kotlinx.serialization.protobuf.* @@ -27,20 +28,15 @@ val ConfiguredProtoBuf = ProtoBuf inline fun ProtoBuf.decodeFromPayload(payload: Payload): T = decodeFromByteArray(payload.data.readBytes()) @ExperimentalSerializationApi -inline fun ProtoBuf.encodeToPayload(route: String, value: T): Payload { - return kotlin.runCatching { //TODO some ktor issue... - Payload { - data(encodeToByteArray(value)) - metadata(route) - } - }.getOrNull() ?: Payload { - data(encodeToByteArray(value)) - metadata(route) - } +inline fun ProtoBuf.encodeToPayload(route: String, value: T): Payload = buildPayload { + data(encodeToByteArray(value)) + metadata(RoutingMetadata(route)) } @ExperimentalSerializationApi -inline fun ProtoBuf.encodeToPayload(value: T): Payload = Payload(encodeToByteArray(value)) +inline fun ProtoBuf.encodeToPayload(value: T): Payload = buildPayload { + data(encodeToByteArray(value)) +} @ExperimentalSerializationApi inline fun ProtoBuf.decoding(payload: Payload, block: (I) -> O): Payload = diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/App.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/App.kt index 59685953f..816bb582b 100644 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/App.kt +++ b/examples/multiplatform-chat/src/serverJvmMain/kotlin/App.kt @@ -24,13 +24,15 @@ import io.ktor.util.* import io.ktor.websocket.* import io.rsocket.kotlin.* import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.metadata.* +import io.rsocket.kotlin.payload.* import io.rsocket.kotlin.transport.ktor.* import io.rsocket.kotlin.transport.ktor.server.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.serialization.* -@OptIn(KtorExperimentalAPI::class, ExperimentalSerializationApi::class) +@OptIn(KtorExperimentalAPI::class, ExperimentalSerializationApi::class, ExperimentalMetadataApi::class) fun main() { val proto = ConfiguredProtoBuf val users = Users() @@ -43,6 +45,8 @@ fun main() { val rSocketServer = RSocketServer() + fun Payload.route(): String = metadata?.read(RoutingMetadata)?.tags?.first() ?: error("No route provided") + //create acceptor val acceptor = ConnectionAcceptor { val userName = config.setupPayload.data.readText() @@ -52,9 +56,7 @@ fun main() { RSocketRequestHandler { fireAndForget { withContext(session) { - when (val route = it.metadata?.readText()) { - null -> error("No route provided") - + when (val route = it.route()) { "users.deleteMe" -> userApi.deleteMe() else -> error("Wrong route: $route") @@ -63,17 +65,15 @@ fun main() { } requestResponse { withContext(session) { - when (val route = it.metadata?.readText()) { - null -> error("No route provided") + when (val route = it.route()) { + "users.getMe" -> proto.encodeToPayload(userApi.getMe()) + "users.all" -> proto.encodeToPayload(userApi.all()) - "users.getMe" -> proto.encodeToPayload(userApi.getMe()) - "users.all" -> proto.encodeToPayload(userApi.all()) + "chats.all" -> proto.encodeToPayload(chatsApi.all()) + "chats.new" -> proto.decoding(it) { (name) -> chatsApi.new(name) } + "chats.delete" -> proto.decoding(it) { (id) -> chatsApi.delete(id) } - "chats.all" -> proto.encodeToPayload(chatsApi.all()) - "chats.new" -> proto.decoding(it) { (name) -> chatsApi.new(name) } - "chats.delete" -> proto.decoding(it) { (id) -> chatsApi.delete(id) } - - "messages.send" -> proto.decoding(it) { (chatId, content) -> + "messages.send" -> proto.decoding(it) { (chatId, content) -> messagesApi.send(chatId, content) } "messages.history" -> proto.decoding>(it) { (chatId, limit) -> @@ -85,9 +85,7 @@ fun main() { } } requestStream { - when (val route = it.metadata?.readText()) { - null -> error("No route provided") - + when (val route = it.route()) { "messages.stream" -> { val (chatId, fromMessageId) = proto.decodeFromPayload(it) messagesApi.messages(chatId, fromMessageId).map { m -> proto.encodeToPayload(m) } diff --git a/examples/nodejs-tcp-transport/src/jsMain/kotlin/Server.kt b/examples/nodejs-tcp-transport/src/jsMain/kotlin/Server.kt index 5804ee319..03b47c09a 100644 --- a/examples/nodejs-tcp-transport/src/jsMain/kotlin/Server.kt +++ b/examples/nodejs-tcp-transport/src/jsMain/kotlin/Server.kt @@ -37,7 +37,7 @@ fun main() { RSocketRequestHandler { requestResponse { println("Received: ${it.data.readText()}") - Payload("Hello from nodejs") + buildPayload { data("Hello from nodejs") } } } } diff --git a/playground/src/commonMain/kotlin/Metadata.kt b/playground/src/commonMain/kotlin/Metadata.kt new file mode 100644 index 000000000..1b39431a5 --- /dev/null +++ b/playground/src/commonMain/kotlin/Metadata.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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. + */ + +import io.ktor.utils.io.core.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.metadata.* +import io.rsocket.kotlin.payload.* + +@OptIn(ExperimentalMetadataApi::class) +public fun metadata() { + + //writing + + val routing = RoutingMetadata("tag1", "route1", "something") + val tracing = ZipkinTracingMetadata( + kind = ZipkinTracingMetadata.Kind.Sample, + traceId = 123L, + spanId = 123L + ) + val cm1 = buildCompositeMetadata { + add(routing) + add(tracing) + } + // or + val cm2 = CompositeMetadata(routing, tracing) + + //all lambdas are inline, and don't create another objects for builders + //so no overhead comparing to using plain Payload(data, metadata) function + val payload = buildPayload { + data("Some data") + metadata(cm2) + // or + metadata(cm1) + // or + compositeMetadata { + add(routing) + add(tracing) + + add(WellKnownMimeType.ApplicationAvro, buildPacket { writeText("some custom metadata building") }) + //or + val raw = RawMetadata(WellKnownMimeType.ApplicationAvro, ByteReadPacket.Empty) + + add(raw) + } + } + + //reading + //property types are not needed, just for better online readability + val cm: CompositeMetadata = payload.metadata?.read(CompositeMetadata) ?: return + + val rmr = RawMetadata.reader(WellKnownMimeType.ApplicationAvro) + + + val rm: RoutingMetadata = cm.get(RoutingMetadata) + val tm: ZipkinTracingMetadata = cm.get(ZipkinTracingMetadata) + + //or + val rm1: RoutingMetadata = cm[RoutingMetadata] + val tm1: ZipkinTracingMetadata = cm[ZipkinTracingMetadata] + + //or + val rmNull: RoutingMetadata? = cm.getOrNull(RoutingMetadata) + val tmNull: ZipkinTracingMetadata? = cm.getOrNull(ZipkinTracingMetadata) + + //or + val rmList: List = cm.list(RoutingMetadata) //spec allow this + + //or + val c: Boolean = cm.contains(RoutingMetadata) + val c2: Boolean = RoutingMetadata in cm + + //or + + cm.entries.forEach { + if (it.hasMimeTypeOf(RoutingMetadata)) { + val rm2: RoutingMetadata = it.read(RoutingMetadata) + } + //or + val tm2: ZipkinTracingMetadata? = it.readOrNull(ZipkinTracingMetadata) + + + } + + //usage + + rm.tags.forEach { + println("tag: $it") + } + + tm.traceId + tm.traceIdHigh + + val span = "${tm.parentSpanId}-${tm.spanId}" + +} diff --git a/playground/src/commonMain/kotlin/Stub.kt b/playground/src/commonMain/kotlin/Stub.kt index 339ed4a2d..330333032 100644 --- a/playground/src/commonMain/kotlin/Stub.kt +++ b/playground/src/commonMain/kotlin/Stub.kt @@ -44,7 +44,10 @@ suspend fun RSocket.doSomething() { // launch { rSocket.fireAndForget(Payload(byteArrayOf(1, 1, 1), byteArrayOf(2, 2, 2))) } // launch { rSocket.metadataPush(byteArrayOf(1, 2, 3)) } var i = 0 - requestStream(Payload(byteArrayOf(1, 1, 1), byteArrayOf(2, 2, 2))).buffer(10000).collect { + requestStream(buildPayload { + data(byteArrayOf(1, 1, 1)) + metadata(byteArrayOf(2, 2, 2)) + }).buffer(10000).collect { println(it.data.readBytes().contentToString()) if (++i == 10000) error("") } diff --git a/playground/src/commonMain/kotlin/TCP.kt b/playground/src/commonMain/kotlin/TCP.kt index b6859ece9..dd7e6aecc 100644 --- a/playground/src/commonMain/kotlin/TCP.kt +++ b/playground/src/commonMain/kotlin/TCP.kt @@ -35,6 +35,6 @@ suspend fun testNodeJsServer(dispatcher: CoroutineContext) { val transport = aSocket(SelectorManager(dispatcher)).tcp().clientTransport("127.0.0.1", 9000) val client = RSocketConnector().connect(transport) - val response = client.requestResponse(Payload("Hello from JVM")) + val response = client.requestResponse(buildPayload { data("Hello from JVM") }) println(response.data.readText()) } diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/Annotations.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/Annotations.kt index 052daceb0..3214dde03 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/Annotations.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/Annotations.kt @@ -23,3 +23,10 @@ package io.rsocket.kotlin "This API can change in future in non backwards-incompatible manner." ) public annotation class TransportApi + +@Retention(value = AnnotationRetention.BINARY) +@RequiresOptIn( + level = RequiresOptIn.Level.WARNING, + message = "This is an API to work with metadata. This API can change in future in non backwards-incompatible manner." +) +public annotation class ExperimentalMetadataApi diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/RSocketRequestHandler.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/RSocketRequestHandler.kt index 97a3c5dff..d7f19a31d 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/RSocketRequestHandler.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/RSocketRequestHandler.kt @@ -29,27 +29,27 @@ class RSocketRequestHandlerBuilder internal constructor() { private var requestChannel: (RSocket.(payloads: Flow) -> Flow)? = null public fun metadataPush(block: (suspend RSocket.(metadata: ByteReadPacket) -> Unit)) { - require(metadataPush == null) { "Metadata Push handler already configured" } + check(metadataPush == null) { "Metadata Push handler already configured" } metadataPush = block } public fun fireAndForget(block: (suspend RSocket.(payload: Payload) -> Unit)) { - require(metadataPush == null) { "Fire and Forget handler already configured" } + check(metadataPush == null) { "Fire and Forget handler already configured" } fireAndForget = block } public fun requestResponse(block: (suspend RSocket.(payload: Payload) -> Payload)) { - require(metadataPush == null) { "Request Response Push handler already configured" } + check(metadataPush == null) { "Request Response Push handler already configured" } requestResponse = block } public fun requestStream(block: (RSocket.(payload: Payload) -> Flow)) { - require(metadataPush == null) { "Request Stream handler already configured" } + check(metadataPush == null) { "Request Stream handler already configured" } requestStream = block } public fun requestChannel(block: (RSocket.(payloads: Flow) -> Flow)) { - require(metadataPush == null) { "Request Channel handler already configured" } + check(metadataPush == null) { "Request Channel handler already configured" } requestChannel = block } diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/core/MimeType.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/core/MimeType.kt new file mode 100644 index 000000000..cf30ef1d4 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/core/MimeType.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.core + +import io.rsocket.kotlin.frame.io.* + +public interface MimeType + +public interface MimeTypeWithName : MimeType { + public val text: String +} + +public interface MimeTypeWithId : MimeType { + public val identifier: Byte +} + +public data class CustomMimeType(override val text: String) : MimeTypeWithName { + init { + text.requireAscii() + require(text.length in 1..128) { "Mime-type text length must be in range 1..128 but was '${text.length}'" } + } + + override fun toString(): String = text +} + +public data class ReservedMimeType(override val identifier: Byte) : MimeTypeWithId { + init { + require(identifier in 1..128) { "Mime-type identifier must be in range 1..128 but was '${identifier}'" } + } + + override fun toString(): String = "ID: $identifier" +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/core/WellKnownMimeType.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/core/WellKnownMimeType.kt new file mode 100644 index 000000000..be6aade89 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/core/WellKnownMimeType.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.core + +public enum class WellKnownMimeType( + public override val text: String, + public override val identifier: Byte, +) : MimeTypeWithName, MimeTypeWithId { + ApplicationAvro("application/avro", 0x00), + ApplicationCbor("application/cbor", 0x01), + ApplicationGraphql("application/graphql", 0x02), + ApplicationGzip("application/gzip", 0x03), + ApplicationJavascript("application/javascript", 0x04), + ApplicationJson("application/json", 0x05), + ApplicationOctetStream("application/octet-stream", 0x06), + ApplicationPdf("application/pdf", 0x07), + ApplicationThrift("application/vnd.apache.thrift.binary", 0x08), + ApplicationProtoBuf("application/vnd.google.protobuf", 0x09), + ApplicationXml("application/xml", 0x0A), + ApplicationZip("application/zip", 0x0B), + AudioAac("audio/aac", 0x0C), + AudioMp3("audio/mp3", 0x0D), + AudioMp4("audio/mp4", 0x0E), + AudioMpeg3("audio/mpeg3", 0x0F), + AudioMpeg("audio/mpeg", 0x10), + AudioOgg("audio/ogg", 0x11), + AudioOpus("audio/opus", 0x12), + AudioVorbis("audio/vorbis", 0x13), + ImageBmp("image/bmp", 0x14), + ImageGif("image/gif", 0x15), + ImageHeicSequence("image/heic-sequence", 0x16), + ImageHeic("image/heic", 0x17), + ImageHeifSequence("image/heif-sequence", 0x18), + ImageHeif("image/heif", 0x19), + ImageJpeg("image/jpeg", 0x1A), + ImagePng("image/png", 0x1B), + ImageTiff("image/tiff", 0x1C), + MultipartMixed("multipart/mixed", 0x1D), + TextCss("text/css", 0x1E), + TextCsv("text/csv", 0x1F), + TextHtml("text/html", 0x20), + TextPlain("text/plain", 0x21), + TextXml("text/xml", 0x22), + VideoH264("video/H264", 0x23), + VideoH265("video/H265", 0x24), + VideoVp8("video/VP8", 0x25), + ApplicationHessian("application/x-hessian", 0x26), + ApplicationJavaObject("application/x-java-object", 0x27), + ApplicationCloudeventsJson("application/cloudevents+json", 0x28), + ApplicationCapnProto("application/x-capnp", 0x29), + ApplicationFlatBuffers("application/x-flatbuffers", 0x2A), + + MessageRSocketMimeType("message/x.rsocket.mime-type.v0", 0x7A), + MessageRSocketAcceptMimeTypes("message/x.rsocket.accept-mime-types.v0", 0x7b), + MessageRSocketAuthentication("message/x.rsocket.authentication.v0", 0x7C), + MessageRSocketTracingZipkin("message/x.rsocket.tracing-zipkin.v0", 0x7D), + MessageRSocketRouting("message/x.rsocket.routing.v0", 0x7E), + MessageRSocketCompositeMetadata("message/x.rsocket.composite-metadata.v0", 0x7F); + + override fun toString(): String = text + + public companion object { + private val byIdentifier: Array = arrayOfNulls(128) + private val byName: MutableMap = HashMap(128) + + init { + values().forEach { + byIdentifier[it.identifier.toInt()] = it + byName[it.text] = it + } + } + + public operator fun invoke(identifier: Byte): WellKnownMimeType? = byIdentifier[identifier.toInt()] + + public operator fun invoke(text: String): WellKnownMimeType? = byName[text] + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/Frame.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/Frame.kt index baeada3c0..73f247df8 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/Frame.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/Frame.kt @@ -18,6 +18,7 @@ package io.rsocket.kotlin.frame import io.ktor.utils.io.core.* import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* import io.rsocket.kotlin.frame.io.* private const val FlagsMask: Int = 1023 @@ -34,7 +35,7 @@ abstract class Frame internal constructor(open val type: FrameType) : Closeable protected abstract fun StringBuilder.appendSelf() @DangerousInternalIoApi - fun toPacket(pool: BufferPool): ByteReadPacket { + fun toPacket(pool: ObjectPool): ByteReadPacket { check(type.canHaveMetadata || !(flags check Flags.Metadata)) { "bad value for metadata flag" } return buildPacket(pool) { writeInt(streamId) @@ -60,7 +61,7 @@ abstract class Frame internal constructor(open val type: FrameType) : Closeable } @DangerousInternalIoApi -fun ByteReadPacket.readFrame(pool: BufferPool): Frame = use { +fun ByteReadPacket.readFrame(pool: ObjectPool): Frame = use { val streamId = readInt() val typeAndFlags = readShort().toInt() and 0xFFFF val flags = typeAndFlags and FlagsMask diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/SetupFrame.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/SetupFrame.kt index 54f551c6e..a6d22ae05 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/SetupFrame.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/SetupFrame.kt @@ -20,6 +20,7 @@ import io.ktor.utils.io.core.* import io.rsocket.kotlin.frame.io.* import io.rsocket.kotlin.keepalive.* import io.rsocket.kotlin.payload.* +import kotlin.time.* private const val HonorLeaseFlag = 64 private const val ResumeEnabledFlag = 128 @@ -85,3 +86,44 @@ internal fun ByteReadPacket.readSetup(pool: BufferPool, flags: Int): SetupFrame payload = payload ) } + +private fun ByteReadPacket.readMimeType(): String { + val length = readByte().toInt() + return readTextExactBytes(length) +} + +private fun BytePacketBuilder.writeMimeType(mimeType: String) { + val bytes = mimeType.encodeToByteArray() //TODO check + writeByte(bytes.size.toByte()) + writeFully(bytes) +} + +private fun ByteReadPacket.readPayloadMimeType(): PayloadMimeType { + val metadata = readMimeType() + val data = readMimeType() + return PayloadMimeType(data = data, metadata = metadata) +} + +private fun BytePacketBuilder.writePayloadMimeType(payloadMimeType: PayloadMimeType) { + writeMimeType(payloadMimeType.metadata) + writeMimeType(payloadMimeType.data) +} + +@OptIn(ExperimentalTime::class) +private fun ByteReadPacket.readMillis(): Duration = readInt().milliseconds + +@OptIn(ExperimentalTime::class) +private fun BytePacketBuilder.writeMillis(duration: Duration) { + writeInt(duration.toInt(DurationUnit.MILLISECONDS)) +} + +private fun ByteReadPacket.readKeepAlive(): KeepAlive { + val interval = readMillis() + val maxLifetime = readMillis() + return KeepAlive(interval = interval, maxLifetime = maxLifetime) +} + +private fun BytePacketBuilder.writeKeepAlive(keepAlive: KeepAlive) { + writeMillis(keepAlive.interval) + writeMillis(keepAlive.maxLifetime) +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Dump.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Dump.kt index ab0ca082c..2682eb34a 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Dump.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Dump.kt @@ -103,7 +103,8 @@ internal fun StringBuilder.appendPacket(tag: String, packet: ByteReadPacket) { } internal fun StringBuilder.appendPayload(payload: Payload) { - if (payload.metadata != null) appendPacket("Metadata", payload.metadata) + val metadata = payload.metadata + if (metadata != null) appendPacket("Metadata", metadata) appendPacket("Data", payload.data) } diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Flags.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Flags.kt index 00932e1fc..20511db98 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Flags.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/Flags.kt @@ -16,6 +16,8 @@ package io.rsocket.kotlin.frame.io +import kotlin.experimental.* + internal object Flags { const val Ignore = 512 const val Metadata = 256 @@ -25,3 +27,5 @@ internal object Flags { } internal infix fun Int.check(flag: Int): Boolean = this and flag == flag + +internal infix fun Byte.check(flag: Byte): Boolean = this and flag == flag diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/mimeType.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/mimeType.kt new file mode 100644 index 000000000..cc13f2d5e --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/mimeType.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.frame.io + +import io.ktor.utils.io.core.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.metadata.security.* +import kotlin.experimental.* + +internal fun BytePacketBuilder.writeMimeType(type: MimeType) { + when (type) { + is MimeTypeWithId -> writeIdentifier(type.identifier) + is MimeTypeWithName -> writeTextWithLength(type.text) + else -> error("Unknown mime type") + } +} + +internal fun ByteReadPacket.readMimeType(): MimeType = readType( + { WellKnownMimeType(it) ?: ReservedMimeType(it) }, + { WellKnownMimeType(it) ?: CustomMimeType(it) } +) + +internal fun BytePacketBuilder.writeAuthType(type: AuthType) { + when (type) { + is AuthTypeWithId -> writeIdentifier(type.identifier) + is AuthTypeWithName -> writeTextWithLength(type.text) + else -> error("Unknown mime type") + } +} + +internal fun ByteReadPacket.readAuthType(): AuthType = readType( + { WellKnowAuthType(it) ?: ReservedAuthType(it) }, + { WellKnowAuthType(it) ?: CustomAuthType(it) } +) + +private fun BytePacketBuilder.writeTextWithLength(text: String) { + val typeBytes = text.encodeToByteArray() + writeByte(typeBytes.size.toByte()) //write length + writeFully(typeBytes) //write mime type +} + +private const val KnownTypeFlag: Byte = Byte.MIN_VALUE + +private fun BytePacketBuilder.writeIdentifier(identifier: Byte) { + writeByte(identifier or KnownTypeFlag) +} + +private inline fun ByteReadPacket.readType( + fromIdentifier: (Byte) -> T, + fromText: (String) -> T, +): T { + val byte = readByte() + return if (byte check KnownTypeFlag) { + val identifier = byte xor KnownTypeFlag + fromIdentifier(identifier) + } else { + val stringType = readTextExactBytes(byte.toInt()) + fromText(stringType) + } +} + +internal fun String.requireAscii() { + require(all { it.toInt() <= 0x7f }) { "String should be an ASCII encodded string" } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/util.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/util.kt index 8b806cb7b..24f98ee9a 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/util.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/frame/io/util.kt @@ -34,47 +34,6 @@ internal fun BytePacketBuilder.writeResumeToken(resumeToken: ByteReadPacket?) { } } -internal fun ByteReadPacket.readMimeType(): String { - val length = readByte().toInt() - return readText(max = length) -} - -internal fun BytePacketBuilder.writeMimeType(mimeType: String) { - val bytes = mimeType.encodeToByteArray() //TODO check - writeByte(bytes.size.toByte()) - writeFully(bytes) -} - -internal fun ByteReadPacket.readPayloadMimeType(): PayloadMimeType { - val metadata = readMimeType() - val data = readMimeType() - return PayloadMimeType(data = data, metadata = metadata) -} - -internal fun BytePacketBuilder.writePayloadMimeType(payloadMimeType: PayloadMimeType) { - writeMimeType(payloadMimeType.metadata) - writeMimeType(payloadMimeType.data) -} - -@OptIn(ExperimentalTime::class) -internal fun ByteReadPacket.readMillis(): Duration = readInt().milliseconds - -@OptIn(ExperimentalTime::class) -internal fun BytePacketBuilder.writeMillis(duration: Duration) { - writeInt(duration.toInt(DurationUnit.MILLISECONDS)) -} - -internal fun ByteReadPacket.readKeepAlive(): KeepAlive { - val interval = readMillis() - val maxLifetime = readMillis() - return KeepAlive(interval = interval, maxLifetime = maxLifetime) -} - -internal fun BytePacketBuilder.writeKeepAlive(keepAlive: KeepAlive) { - writeMillis(keepAlive.interval) - writeMillis(keepAlive.maxLifetime) -} - internal fun ByteReadPacket.readPacket(pool: BufferPool): ByteReadPacket { if (isEmpty) return ByteReadPacket.Empty return buildPacket(pool) { diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadata.kt new file mode 100644 index 000000000..19ae7ce89 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadata.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* + +@ExperimentalMetadataApi +public fun CompositeMetadata(vararg entries: Metadata): CompositeMetadata = + DefaultCompositeMetadata(entries.map(CompositeMetadata::Entry)) + +@ExperimentalMetadataApi +public fun CompositeMetadata(entries: List): CompositeMetadata = + DefaultCompositeMetadata(entries.map(CompositeMetadata::Entry)) + +@ExperimentalMetadataApi +public interface CompositeMetadata : Metadata { + public val entries: List + override val mimeType: MimeType get() = Reader.mimeType + + override fun BytePacketBuilder.writeSelf() { + entries.forEach { + writeMimeType(it.mimeType) + writeLength(it.content.remaining.toInt()) //write metadata length + writePacket(it.content) //write metadata content + } + } + + public class Entry(public val mimeType: MimeType, public val content: ByteReadPacket) { + public constructor(metadata: Metadata) : this(metadata.mimeType, metadata.toPacket()) + } + + public companion object Reader : MetadataReader { + override val mimeType: MimeType get() = WellKnownMimeType.MessageRSocketCompositeMetadata + + @DangerousInternalIoApi + @OptIn(ExperimentalStdlibApi::class) + override fun ByteReadPacket.read(pool: ObjectPool): CompositeMetadata = DefaultCompositeMetadata(buildList { + while (isNotEmpty) { + val type = readMimeType() + val length = readLength() + val packet = readPacket(pool, length) + add(Entry(type, packet)) + } + }) + } +} + +@ExperimentalMetadataApi +private class DefaultCompositeMetadata(override val entries: List) : CompositeMetadata diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataBuilder.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataBuilder.kt new file mode 100644 index 000000000..f83bc04fd --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataBuilder.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.payload.* + +@ExperimentalMetadataApi +public interface CompositeMetadataBuilder { + public fun add(mimeType: MimeType, metadata: ByteReadPacket) + public fun add(metadata: Metadata) + + public fun clean() + public fun build(): CompositeMetadata +} + +@ExperimentalMetadataApi +public inline fun buildCompositeMetadata(block: CompositeMetadataBuilder.() -> Unit): CompositeMetadata { + val builder = createCompositeMetadataBuilder() + try { + builder.block() + return builder.build() + } catch (t: Throwable) { + builder.clean() + throw t + } +} + +@ExperimentalMetadataApi +public inline fun PayloadBuilder.compositeMetadata(block: CompositeMetadataBuilder.() -> Unit): Unit = + metadata(buildCompositeMetadata(block)) + +@PublishedApi +@ExperimentalMetadataApi +internal fun createCompositeMetadataBuilder(): CompositeMetadataBuilder = CompositeMetadataFromBuilder() + +@ExperimentalMetadataApi +private class CompositeMetadataFromBuilder : CompositeMetadataBuilder, CompositeMetadata { + private val _entries = mutableListOf() + + override val entries: List get() = _entries + + override fun add(mimeType: MimeType, metadata: ByteReadPacket) { + _entries += CompositeMetadata.Entry(mimeType, metadata) + } + + override fun add(metadata: Metadata) { + _entries += CompositeMetadata.Entry(metadata) + } + + override fun clean() { + _entries.forEach { it.content.release() } + } + + override fun build(): CompositeMetadata = this +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataExtensions.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataExtensions.kt new file mode 100644 index 000000000..034de0f5c --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataExtensions.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* + +@ExperimentalMetadataApi +public fun CompositeMetadata.Entry.hasMimeTypeOf(reader: MetadataReader<*>): Boolean = mimeType == reader.mimeType + +@ExperimentalMetadataApi +@OptIn(DangerousInternalIoApi::class) +public fun CompositeMetadata.Entry.read(reader: MetadataReader): M = read(ChunkBuffer.Pool, reader) + +@ExperimentalMetadataApi +@OptIn(DangerousInternalIoApi::class) +public fun CompositeMetadata.Entry.readOrNull(reader: MetadataReader): M? = readOrNull(ChunkBuffer.Pool, reader) + +@ExperimentalMetadataApi +@DangerousInternalIoApi +public fun CompositeMetadata.Entry.read(pool: ObjectPool, reader: MetadataReader): M { + if (mimeType == reader.mimeType) return content.read(pool, reader) + + content.release() + error("Expected mimeType '${reader.mimeType}' but was '$mimeType'") +} + +@ExperimentalMetadataApi +@DangerousInternalIoApi +public fun CompositeMetadata.Entry.readOrNull(pool: ObjectPool, reader: MetadataReader): M? { + return if (mimeType == reader.mimeType) content.read(pool, reader) else null +} + + +@ExperimentalMetadataApi +public operator fun CompositeMetadata.contains(mimeType: MimeType): Boolean { + return entries.any { it.mimeType == mimeType } +} + +@ExperimentalMetadataApi +public operator fun CompositeMetadata.get(mimeType: MimeType): ByteReadPacket { + return entries.first { it.mimeType == mimeType }.content +} + +@ExperimentalMetadataApi +public fun CompositeMetadata.getOrNull(mimeType: MimeType): ByteReadPacket? { + return entries.find { it.mimeType == mimeType }?.content +} + +@ExperimentalMetadataApi +public fun CompositeMetadata.list(mimeType: MimeType): List { + return entries.mapNotNull { if (it.mimeType == mimeType) it.content else null } +} + + +@ExperimentalMetadataApi +public operator fun CompositeMetadata.contains(reader: MetadataReader<*>): Boolean { + return contains(reader.mimeType) +} + +@ExperimentalMetadataApi +public operator fun CompositeMetadata.get(reader: MetadataReader): M { + return get(reader.mimeType).read(reader) +} + +@ExperimentalMetadataApi +public fun CompositeMetadata.getOrNull(reader: MetadataReader): M? { + return getOrNull(reader.mimeType)?.read(reader) +} + +@ExperimentalMetadataApi +public fun CompositeMetadata.list(reader: MetadataReader): List { + return entries.mapNotNull { it.readOrNull(reader) } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/Metadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/Metadata.kt new file mode 100644 index 000000000..a94d1ad70 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/Metadata.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* +import io.rsocket.kotlin.payload.* + +@ExperimentalMetadataApi +public interface Metadata { + public val mimeType: MimeType + public fun BytePacketBuilder.writeSelf() +} + +@ExperimentalMetadataApi +public interface MetadataReader { + public val mimeType: MimeType + + @DangerousInternalIoApi + public fun ByteReadPacket.read(pool: ObjectPool): M +} + + +@ExperimentalMetadataApi +public fun PayloadBuilder.metadata(metadata: Metadata): Unit = metadata(metadata.toPacket()) + +@ExperimentalMetadataApi +@OptIn(DangerousInternalIoApi::class) +public fun ByteReadPacket.read(reader: MetadataReader): M = read(ChunkBuffer.Pool, reader) + +@ExperimentalMetadataApi +public fun Metadata.toPacket(): ByteReadPacket = buildPacket { writeSelf() } + + +@ExperimentalMetadataApi +@DangerousInternalIoApi +public fun ByteReadPacket.read(pool: ObjectPool, reader: MetadataReader): M = use { + with(reader) { read(pool) } +} + +@ExperimentalMetadataApi +@DangerousInternalIoApi +public fun Metadata.toPacket(pool: ObjectPool): ByteReadPacket = buildPacket(pool) { writeSelf() } diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/PerStreamAcceptableDataMimeTypesMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/PerStreamAcceptableDataMimeTypesMetadata.kt new file mode 100644 index 000000000..532edabcf --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/PerStreamAcceptableDataMimeTypesMetadata.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* + +@ExperimentalMetadataApi +public fun PerStreamAcceptableDataMimeTypesMetadata(vararg tags: MimeType): PerStreamAcceptableDataMimeTypesMetadata = + PerStreamAcceptableDataMimeTypesMetadata(tags.toList()) + +@ExperimentalMetadataApi +public class PerStreamAcceptableDataMimeTypesMetadata(public val types: List) : Metadata { + override val mimeType: MimeType get() = Reader.mimeType + + override fun BytePacketBuilder.writeSelf() { + types.forEach { + writeMimeType(it) + } + } + + public companion object Reader : MetadataReader { + override val mimeType: MimeType get() = WellKnownMimeType.MessageRSocketAcceptMimeTypes + + @DangerousInternalIoApi + @OptIn(ExperimentalStdlibApi::class) + override fun ByteReadPacket.read(pool: ObjectPool): PerStreamAcceptableDataMimeTypesMetadata = + PerStreamAcceptableDataMimeTypesMetadata(buildList { + while (isNotEmpty) add(readMimeType()) + }) + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/PerStreamDataMimeTypeMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/PerStreamDataMimeTypeMetadata.kt new file mode 100644 index 000000000..77a72b951 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/PerStreamDataMimeTypeMetadata.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* + +@ExperimentalMetadataApi +public class PerStreamDataMimeTypeMetadata(public val type: MimeType) : Metadata { + override val mimeType: MimeType get() = Reader.mimeType + + override fun BytePacketBuilder.writeSelf() { + writeMimeType(type) + } + + public companion object Reader : MetadataReader { + override val mimeType: MimeType get() = WellKnownMimeType.MessageRSocketMimeType + + @DangerousInternalIoApi + override fun ByteReadPacket.read(pool: ObjectPool): PerStreamDataMimeTypeMetadata = + PerStreamDataMimeTypeMetadata(readMimeType()) + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/RawMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/RawMetadata.kt new file mode 100644 index 000000000..f18d3abb5 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/RawMetadata.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* + +@ExperimentalMetadataApi +public class RawMetadata( + override val mimeType: MimeType, + public val content: ByteReadPacket, +) : Metadata { + override fun BytePacketBuilder.writeSelf() { + writePacket(content) + } + + private class Reader(override val mimeType: MimeType) : MetadataReader { + @DangerousInternalIoApi + override fun ByteReadPacket.read(pool: ObjectPool): RawMetadata = RawMetadata(mimeType, readPacket(pool)) + } + + public companion object { + public fun reader(mimeType: MimeType): MetadataReader = Reader(mimeType) + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/RoutingMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/RoutingMetadata.kt new file mode 100644 index 000000000..23738d748 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/RoutingMetadata.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* + +@ExperimentalMetadataApi +public fun RoutingMetadata(vararg tags: String): RoutingMetadata = RoutingMetadata(tags.toList()) + +@ExperimentalMetadataApi +public class RoutingMetadata(public val tags: List) : Metadata { + init { + tags.forEach { + require(it.length in 1..255) { "Tag length must be in range 1..255 but was '${it.length}'" } + } + } + + override val mimeType: MimeType get() = Reader.mimeType + + override fun BytePacketBuilder.writeSelf() { + tags.forEach { + val bytes = it.encodeToByteArray() + writeByte(bytes.size.toByte()) + writeFully(bytes) + } + } + + public companion object Reader : MetadataReader { + override val mimeType: MimeType get() = WellKnownMimeType.MessageRSocketRouting + + @DangerousInternalIoApi + @OptIn(ExperimentalStdlibApi::class, ExperimentalUnsignedTypes::class) + override fun ByteReadPacket.read(pool: ObjectPool): RoutingMetadata = RoutingMetadata(buildList { + while (isNotEmpty) { + val length = readUByte().toInt() + add(readTextExactBytes(length)) + } + }) + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/ZipkinTracingMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/ZipkinTracingMetadata.kt new file mode 100644 index 000000000..5f50c8e24 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/ZipkinTracingMetadata.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* +import kotlin.experimental.* + +@ExperimentalMetadataApi +public class ZipkinTracingMetadata internal constructor( + public val kind: Kind, + public val hasIds: Boolean, //trace and span ids + public val hasParentSpanId: Boolean, + public val extendedTraceId: Boolean, + public val traceId: Long, + public val traceIdHigh: Long, + public val spanId: Long, + public val parentSpanId: Long, +) : Metadata { + override val mimeType: MimeType get() = Reader.mimeType + + public enum class Kind { + Debug, + Sample, + NotSampled, + Unspecified + } + + override fun BytePacketBuilder.writeSelf() { + var flags = when (kind) { + Kind.Debug -> DebugFlag + Kind.Sample -> SampleFlag + Kind.NotSampled -> NotSampledFlag + Kind.Unspecified -> 0 + } + + if (!hasIds) return writeByte(flags) + + flags = flags or HasIdsFlag + + if (extendedTraceId) flags = flags or ExtendedTraceFlag + if (hasParentSpanId) flags = flags or HasParentSpanFlag + + writeByte(flags) + + if (extendedTraceId) writeLong(traceIdHigh) + writeLong(traceId) + writeLong(spanId) + if (hasParentSpanId) writeLong(parentSpanId) + } + + public companion object Reader : MetadataReader { + override val mimeType: MimeType get() = WellKnownMimeType.MessageRSocketTracingZipkin + + @DangerousInternalIoApi + override fun ByteReadPacket.read(pool: ObjectPool): ZipkinTracingMetadata { + val flags = readByte() + + val kind = when { + flags check DebugFlag -> Kind.Debug + flags check SampleFlag -> Kind.Sample + flags check NotSampledFlag -> Kind.NotSampled + else -> Kind.Unspecified + } + + if (!(flags check HasIdsFlag)) return ZipkinTracingMetadata(kind) + + val extendedTraceId = flags check ExtendedTraceFlag + val hasParentSpanId = flags check HasParentSpanFlag + + val traceIdHigh = if (extendedTraceId) readLong() else 0 + val traceId = readLong() + val spanId = readLong() + val parentSpanId = if (hasParentSpanId) readLong() else 0 + + return ZipkinTracingMetadata(kind, true, hasParentSpanId, extendedTraceId, traceId, traceIdHigh, spanId, parentSpanId) + } + } +} + +@ExperimentalMetadataApi +public fun ZipkinTracingMetadata(kind: ZipkinTracingMetadata.Kind): ZipkinTracingMetadata = + ZipkinTracingMetadata(kind, false, false, false, 0, 0, 0, 0) + +@ExperimentalMetadataApi +public fun ZipkinTracingMetadata( + kind: ZipkinTracingMetadata.Kind, + traceId: Long, + spanId: Long, +): ZipkinTracingMetadata = ZipkinTracingMetadata(kind, true, false, false, traceId, 0, spanId, 0) + +@ExperimentalMetadataApi +public fun ZipkinTracingMetadata( + kind: ZipkinTracingMetadata.Kind, + traceId: Long, + spanId: Long, + parentSpanId: Long, +): ZipkinTracingMetadata = ZipkinTracingMetadata(kind, true, true, false, traceId, 0, spanId, parentSpanId) + +@Suppress("FunctionName") +@ExperimentalMetadataApi +public fun ZipkinTracingMetadata128( + kind: ZipkinTracingMetadata.Kind, + traceId: Long, + traceIdHigh: Long, + spanId: Long, +): ZipkinTracingMetadata = ZipkinTracingMetadata(kind, true, false, true, traceId, traceIdHigh, spanId, 0) + +@Suppress("FunctionName") +@ExperimentalMetadataApi +public fun ZipkinTracingMetadata128( + kind: ZipkinTracingMetadata.Kind, + traceId: Long, + traceIdHigh: Long, + spanId: Long, + parentSpanId: Long, +): ZipkinTracingMetadata = ZipkinTracingMetadata(kind, true, true, true, traceId, traceIdHigh, spanId, parentSpanId) + + +private const val HasIdsFlag: Byte = Byte.MIN_VALUE +private const val DebugFlag: Byte = 0b01000000 +private const val SampleFlag: Byte = 0b00100000 +private const val NotSampledFlag: Byte = 0b00010000 +private const val ExtendedTraceFlag: Byte = 0b00001000 +private const val HasParentSpanFlag: Byte = 0b00000100 diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/AuthMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/AuthMetadata.kt new file mode 100644 index 000000000..bd1e584bc --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/AuthMetadata.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata.security + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* +import io.rsocket.kotlin.metadata.* + +@ExperimentalMetadataApi +public interface AuthMetadata : Metadata { + public val type: AuthType + public fun BytePacketBuilder.writeContent() + + override val mimeType: MimeType get() = WellKnownMimeType.MessageRSocketAuthentication + + override fun BytePacketBuilder.writeSelf() { + writeAuthType(type) + writeContent() + } +} + +@ExperimentalMetadataApi +public interface AuthMetadataReader : MetadataReader { + @DangerousInternalIoApi + public fun ByteReadPacket.readContent(type: AuthType, pool: ObjectPool): AM + + override val mimeType: MimeType get() = WellKnownMimeType.MessageRSocketAuthentication + + @DangerousInternalIoApi + override fun ByteReadPacket.read(pool: ObjectPool): AM { + val type = readAuthType() + return readContent(type, pool) + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/AuthType.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/AuthType.kt new file mode 100644 index 000000000..d6d2bc101 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/AuthType.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata.security + +import io.rsocket.kotlin.frame.io.* + +public interface AuthType + +public interface AuthTypeWithName : AuthType { + public val text: String +} + +public interface AuthTypeWithId : AuthType { + public val identifier: Byte +} + +public data class CustomAuthType(override val text: String) : AuthTypeWithName { + init { + text.requireAscii() + require(text.length in 1..128) { "Mime-type length must be in range 1..128 but was '${text.length}'" } + } + + override fun toString(): String = text +} + +public data class ReservedAuthType(override val identifier: Byte) : AuthTypeWithId { + init { + require(identifier in 1..128) { "Mime-type identifier must be in range 1..128 but was '${identifier}'" } + } + + override fun toString(): String = "ID: $identifier" +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/BearerAuthMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/BearerAuthMetadata.kt new file mode 100644 index 000000000..a22f69472 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/BearerAuthMetadata.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata.security + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* + +@ExperimentalMetadataApi +public class BearerAuthMetadata( + public val token: String, +) : AuthMetadata { + override val type: AuthType get() = WellKnowAuthType.Bearer + override fun BytePacketBuilder.writeContent() { + writeText(token) + } + + public companion object Reader : AuthMetadataReader { + @DangerousInternalIoApi + override fun ByteReadPacket.readContent(type: AuthType, pool: ObjectPool): BearerAuthMetadata { + require(type == WellKnowAuthType.Bearer) { "Metadata auth type should be 'bearer'" } + val token = readText() + return BearerAuthMetadata(token) + } + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/RawAuthMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/RawAuthMetadata.kt new file mode 100644 index 000000000..fba48b397 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/RawAuthMetadata.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata.security + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* +import io.rsocket.kotlin.frame.io.* + +@ExperimentalMetadataApi +public class RawAuthMetadata( + public override val type: AuthType, + public val content: ByteReadPacket, +) : AuthMetadata { + + override fun BytePacketBuilder.writeContent() { + writePacket(content) + } + + public companion object Reader : AuthMetadataReader { + @DangerousInternalIoApi + override fun ByteReadPacket.readContent(type: AuthType, pool: ObjectPool): RawAuthMetadata { + val content = readPacket(pool) + return RawAuthMetadata(type, content) + } + } +} + +@ExperimentalMetadataApi +public fun RawAuthMetadata.hasAuthTypeOf(reader: AuthMetadataReader<*>): Boolean = type == reader.mimeType + +@ExperimentalMetadataApi +@OptIn(DangerousInternalIoApi::class) +public fun RawAuthMetadata.read(reader: AuthMetadataReader): AM = read(ChunkBuffer.Pool, reader) + +@ExperimentalMetadataApi +@OptIn(DangerousInternalIoApi::class) +public fun RawAuthMetadata.readOrNull(reader: AuthMetadataReader): AM? = readOrNull(ChunkBuffer.Pool, reader) + + +@ExperimentalMetadataApi +@DangerousInternalIoApi +public fun RawAuthMetadata.read(pool: ObjectPool, reader: AuthMetadataReader): AM { + return readOrNull(pool, reader) ?: run { + content.release() + error("Expected auth type '${reader.mimeType}' but was '$mimeType'") + } +} + +@ExperimentalMetadataApi +@DangerousInternalIoApi +public fun RawAuthMetadata.readOrNull(pool: ObjectPool, reader: AuthMetadataReader): AM? { + if (type != reader.mimeType) return null + + with(reader) { + return content.use { it.readContent(type, pool) } + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/SimpleAuthMetadata.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/SimpleAuthMetadata.kt new file mode 100644 index 000000000..089eca748 --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/SimpleAuthMetadata.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata.security + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.core.internal.* +import io.ktor.utils.io.pool.* +import io.rsocket.kotlin.* + +@ExperimentalMetadataApi +public class SimpleAuthMetadata( + public val username: String, + public val password: String, +) : AuthMetadata { + + init { + require(username.length < 65535) { "Username length must be in range 1..65535 but was '${username.length}'" } + } + + override val type: AuthType get() = WellKnowAuthType.Simple + override fun BytePacketBuilder.writeContent() { + val length = username.encodeToByteArray() + writeShort(length.size.toShort()) + writeText(username) + writeText(password) + } + + public companion object Reader : AuthMetadataReader { + @DangerousInternalIoApi + override fun ByteReadPacket.readContent(type: AuthType, pool: ObjectPool): SimpleAuthMetadata { + require(type == WellKnowAuthType.Simple) { "Metadata auth type should be 'simple'" } + val length = readShort().toInt() + val username = readTextExactBytes(length) + val password = readText() + return SimpleAuthMetadata(username, password) + } + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/WellKnowAuthType.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/WellKnowAuthType.kt new file mode 100644 index 000000000..b1704660b --- /dev/null +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/metadata/security/WellKnowAuthType.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata.security + +public enum class WellKnowAuthType( + public override val text: String, + public override val identifier: Byte, +) : AuthTypeWithName, AuthTypeWithId { + Simple("simple", 0x00), + Bearer("bearer", 0x01); + + override fun toString(): String = text + + public companion object { + private val byIdentifier: Array = arrayOfNulls(128) + private val byName: MutableMap = HashMap(128) + + init { + values().forEach { + byIdentifier[it.identifier.toInt()] = it + byName[it.text] = it + } + } + + public operator fun invoke(identifier: Byte): WellKnowAuthType? = byIdentifier[identifier.toInt()] + + public operator fun invoke(text: String): WellKnowAuthType? = byName[text] + } +} diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/Payload.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/Payload.kt index 8b263233f..91afce1f3 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/Payload.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/Payload.kt @@ -18,14 +18,15 @@ package io.rsocket.kotlin.payload import io.ktor.utils.io.core.* -class Payload( - val data: ByteReadPacket, - val metadata: ByteReadPacket? = null, -) : Closeable { +public fun Payload(data: ByteReadPacket, metadata: ByteReadPacket? = null): Payload = DefaultPayload(data, metadata) - fun copy(): Payload = Payload(data.copy(), metadata?.copy()) +public interface Payload : Closeable { + public val data: ByteReadPacket + public val metadata: ByteReadPacket? - fun release() { + public fun copy(): Payload = DefaultPayload(data.copy(), metadata?.copy()) + + public fun release() { data.release() metadata?.release() } @@ -34,26 +35,12 @@ class Payload( release() } - companion object { - val Empty = Payload(ByteReadPacket.Empty) + public companion object { + public val Empty: Payload = Payload(ByteReadPacket.Empty) } } -fun Payload(data: String, metadata: String? = null): Payload = Payload( - data = buildPacket { writeText(data) }, - metadata = metadata?.let { buildPacket { writeText(it) } } -) - -fun Payload(data: ByteArray, metadata: ByteArray? = null): Payload = Payload( - data = buildPacket { writeFully(data) }, - metadata = metadata?.let { buildPacket { writeFully(it) } } -) - -/** - * Wrap data and metadata arrays without copying them. - * Changes in input arrays will change payload data, same as reading from payload will change input arrays. - */ -fun Payload.Companion.wrap(data: ByteArray, metadata: ByteArray? = null): Payload = Payload( - data = ByteReadPacket(data), - metadata = metadata?.let { ByteReadPacket(it) } -) +private class DefaultPayload( + override val data: ByteReadPacket, + override val metadata: ByteReadPacket?, +) : Payload diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadBuilder.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadBuilder.kt index 8e5e3e5c0..5935a655b 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadBuilder.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadBuilder.kt @@ -18,65 +18,67 @@ package io.rsocket.kotlin.payload import io.ktor.utils.io.core.* -class PayloadBuilder -@PublishedApi -internal constructor() { - private var data: BytePacketBuilder? = null - private var metadata: BytePacketBuilder? = null - - @PublishedApi - internal fun data(): BytePacketBuilder = (data ?: BytePacketBuilder().also { data = it }) - - @PublishedApi - internal fun metadata(): BytePacketBuilder = (metadata ?: BytePacketBuilder().also { metadata = it }) - - inline fun data(block: BytePacketBuilder.() -> Unit): Unit = data().block() - inline fun metadata(block: BytePacketBuilder.() -> Unit): Unit = metadata().block() - - @PublishedApi - internal fun build(): Payload = Payload( - data = data?.build() ?: ByteReadPacket.Empty, - metadata = metadata?.build() - ) +public interface PayloadBuilder { + public fun data(value: ByteReadPacket) + public fun metadata(value: ByteReadPacket) - @PublishedApi - internal fun release() { - data?.release() - metadata?.release() - } + public fun clean() + public fun build(): Payload } -inline fun Payload(config: PayloadBuilder.() -> Unit): Payload { - val builder = PayloadBuilder() +public inline fun buildPayload(block: PayloadBuilder.() -> Unit): Payload { + val builder = createPayloadBuilder() try { - builder.config() + builder.block() return builder.build() - } catch (e: Throwable) { - builder.release() - throw e + } catch (t: Throwable) { + builder.clean() + throw t } } -fun PayloadBuilder.data(text: String): Unit = data { - writeText(text) -} +public inline fun PayloadBuilder.data(block: BytePacketBuilder.() -> Unit): Unit = data(buildPacket(block = block)) +public fun PayloadBuilder.data(value: String): Unit = data { writeText(value) } +public fun PayloadBuilder.data(value: ByteArray): Unit = data { writeFully(value) } -fun PayloadBuilder.data(bytes: ByteArray): Unit = data { - writeFully(bytes) -} +public inline fun PayloadBuilder.metadata(block: BytePacketBuilder.() -> Unit): Unit = metadata(buildPacket(block = block)) +public fun PayloadBuilder.metadata(value: String): Unit = metadata { writeText(value) } +public fun PayloadBuilder.metadata(value: ByteArray): Unit = metadata { writeFully(value) } -fun PayloadBuilder.data(packet: ByteReadPacket): Unit = data { - writePacket(packet) -} -fun PayloadBuilder.metadata(text: String): Unit = metadata { - writeText(text) -} +@PublishedApi +internal fun createPayloadBuilder(): PayloadBuilder = PayloadFromBuilder() -fun PayloadBuilder.metadata(bytes: ByteArray): Unit = metadata { - writeFully(bytes) -} +private class PayloadFromBuilder : PayloadBuilder, Payload { + private var hasData = false + private var hasMetadata = false -fun PayloadBuilder.metadata(packet: ByteReadPacket): Unit = metadata { - writePacket(packet) + override var data: ByteReadPacket = ByteReadPacket.Empty + private set + override var metadata: ByteReadPacket? = null + private set + + override fun data(value: ByteReadPacket) { + if (hasData) { + value.release() + error("Data already provided") + } + data = value + hasData = true + } + + override fun metadata(value: ByteReadPacket) { + if (hasMetadata) { + value.release() + error("Metadata already provided") + } + metadata = value + hasMetadata = true + } + + override fun clean(): Unit = release() + override fun build(): Payload { + check(hasData) { "Data is required" } + return this + } } diff --git a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadMimeType.kt b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadMimeType.kt index 634d39023..42fa98a7a 100644 --- a/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadMimeType.kt +++ b/rsocket-core/src/commonMain/kotlin/io/rsocket/kotlin/payload/PayloadMimeType.kt @@ -16,7 +16,20 @@ package io.rsocket.kotlin.payload +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.frame.io.* + data class PayloadMimeType( val data: String = "application/binary", - val metadata: String = "application/binary" -) + val metadata: String = "application/binary", +) { + init { + data.requireAscii() + metadata.requireAscii() + } +} + +public fun PayloadMimeType( + data: MimeTypeWithName, + metadata: MimeTypeWithName, +): PayloadMimeType = PayloadMimeType(data.text, metadata.text) diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataTest.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataTest.kt new file mode 100644 index 000000000..2d1476f3a --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/CompositeMetadataTest.kt @@ -0,0 +1,216 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.ktor.utils.io.core.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.test.* +import kotlin.test.* + +class CompositeMetadataTest : TestWithLeakCheck { + + @Test + fun decodeEntryHasNoContent() { + val cm = buildCompositeMetadata { + add(CustomMimeType("w"), ByteReadPacket.Empty) + } + + val decoded = cm.toPacket(InUseTrackingPool).read(InUseTrackingPool, CompositeMetadata) + + assertEquals(1, decoded.entries.size) + val entry = decoded.entries.first() + assertEquals(CustomMimeType("w"), entry.mimeType) + assertEquals(0, entry.content.remaining) + } + + @Test + fun decodeMultiple() { + val cm = buildCompositeMetadata { + add(CustomMimeType("custom"), packet("custom metadata")) + add(ReservedMimeType(120), packet("reserved metadata")) + add(WellKnownMimeType.ApplicationAvro, packet("avro metadata")) + } + val decoded = cm.toPacket(InUseTrackingPool).read(InUseTrackingPool, CompositeMetadata) + + assertEquals(3, decoded.entries.size) + decoded.entries[0].let { custom -> + assertEquals(CustomMimeType("custom"), custom.mimeType) + assertEquals("custom metadata", custom.content.readText()) + } + decoded.entries[1].let { reserved -> + assertEquals(ReservedMimeType(120), reserved.mimeType) + assertEquals("reserved metadata", reserved.content.readText()) + } + decoded.entries[2].let { known -> + assertEquals(WellKnownMimeType.ApplicationAvro, known.mimeType) + assertEquals("avro metadata", known.content.readText()) + } + } + + @Test + fun failOnMimeTypeWithNoMimeLength() { + val packet = packet { + writeByte(120) + } + assertFails { + packet.read(InUseTrackingPool, CompositeMetadata) + } + } + + @Test + fun testContains() { + val cm = buildCompositeMetadata { + add(CustomMimeType("custom"), packet("custom metadata")) + add(ReservedMimeType(120), packet("reserved metadata")) + add(WellKnownMimeType.ApplicationAvro, packet("avro metadata")) + } + val decoded = cm.toPacket(InUseTrackingPool).read(InUseTrackingPool, CompositeMetadata) + + assertTrue(CustomMimeType("custom") in decoded) + assertTrue(CustomMimeType("custom2") !in decoded) + + assertTrue(ReservedMimeType(120) in decoded) + assertTrue(ReservedMimeType(110) !in decoded) + + assertTrue(WellKnownMimeType.ApplicationAvro in decoded) + assertTrue(WellKnownMimeType.MessageRSocketRouting !in decoded) + + decoded.entries.forEach { it.content.release() } + } + + @Test + fun testGet() { + val cm = buildCompositeMetadata { + add(CustomMimeType("custom"), packet("custom metadata")) + add(ReservedMimeType(120), packet("reserved metadata")) + add(WellKnownMimeType.ApplicationAvro, packet("avro metadata")) + } + val decoded = cm.toPacket(InUseTrackingPool).read(InUseTrackingPool, CompositeMetadata) + + assertEquals("custom metadata", decoded[CustomMimeType("custom")].readText()) + assertEquals("reserved metadata", decoded[ReservedMimeType(120)].readText()) + assertEquals("avro metadata", decoded[WellKnownMimeType.ApplicationAvro].readText()) + } + + @Test + fun testGetOrNull() { + val cm = buildCompositeMetadata { + add(CustomMimeType("custom"), packet("custom metadata")) + add(ReservedMimeType(120), packet("reserved metadata")) + add(WellKnownMimeType.ApplicationAvro, packet("avro metadata")) + } + val decoded = cm.toPacket(InUseTrackingPool).read(InUseTrackingPool, CompositeMetadata) + + assertNull(decoded.getOrNull(ReservedMimeType(121))) + assertNull(decoded.getOrNull(CustomMimeType("custom2"))) + assertNull(decoded.getOrNull(WellKnownMimeType.MessageRSocketRouting)) + + assertEquals("custom metadata", decoded.getOrNull(CustomMimeType("custom"))?.readText()) + assertEquals("reserved metadata", decoded.getOrNull(ReservedMimeType(120))?.readText()) + assertEquals("avro metadata", decoded.getOrNull(WellKnownMimeType.ApplicationAvro)?.readText()) + } + + @Test + fun testList() { + val cm = buildCompositeMetadata { + add(CustomMimeType("custom"), packet("custom metadata - 1")) + add(ReservedMimeType(120), packet("reserved metadata - 1")) + add(ReservedMimeType(120), packet("reserved metadata - 2")) + add(WellKnownMimeType.MessageRSocketRouting, packet("routing metadata")) + add(CustomMimeType("custom"), packet("custom metadata - 2")) + } + val decoded = cm.toPacket(InUseTrackingPool).read(InUseTrackingPool, CompositeMetadata) + + assertEquals(5, decoded.entries.size) + + decoded.list(WellKnownMimeType.ApplicationAvro).let { + assertEquals(0, it.size) + } + + decoded.list(CustomMimeType("custom2")).let { + assertEquals(0, it.size) + } + + decoded.list(ReservedMimeType(110)).let { + assertEquals(0, it.size) + } + + decoded.list(WellKnownMimeType.MessageRSocketRouting).let { + assertEquals(1, it.size) + assertEquals("routing metadata", it[0].readText()) + } + + decoded.list(CustomMimeType("custom")).let { + assertEquals(2, it.size) + assertEquals("custom metadata - 1", it[0].readText()) + assertEquals("custom metadata - 2", it[1].readText()) + } + + decoded.list(ReservedMimeType(120)).let { + assertEquals(2, it.size) + assertEquals("reserved metadata - 1", it[0].readText()) + assertEquals("reserved metadata - 2", it[1].readText()) + } + } + + @Test + fun testBuilderReleaseOnError() { + val packet = packet("string") + assertFails { + buildCompositeMetadata { + add(WellKnownMimeType.ApplicationAvro, packet) + throw error("") + } + } + assertTrue(packet.isEmpty) + } + + @Test + fun testCombine() { + val cm = buildCompositeMetadata { + add(RoutingMetadata("tag1", "tag2")) + add(PerStreamAcceptableDataMimeTypesMetadata( + WellKnownMimeType.ApplicationAvro, + CustomMimeType("application/custom"), + ReservedMimeType(120) + )) + add(WellKnownMimeType.ApplicationJson, packet("{}")) + } + + val decoded = cm.toPacket(InUseTrackingPool).read(InUseTrackingPool, CompositeMetadata) + assertEquals(3, decoded.entries.size) + + assertEquals(listOf("tag1", "tag2"), decoded[RoutingMetadata].tags) + assertEquals( + listOf( + WellKnownMimeType.ApplicationAvro, + CustomMimeType("application/custom"), + ReservedMimeType(120) + ), + decoded[PerStreamAcceptableDataMimeTypesMetadata].types + ) + assertEquals("{}", decoded[WellKnownMimeType.ApplicationJson].readText()) + } + + @Test + fun failOnNonAscii() { + assertFailsWith(IllegalArgumentException::class) { + CustomMimeType("1234567#4? 𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎") + } + } + +} diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/PerStreamAcceptableDataMimeTypesMetadataTest.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/PerStreamAcceptableDataMimeTypesMetadataTest.kt new file mode 100644 index 000000000..20378d42a --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/PerStreamAcceptableDataMimeTypesMetadataTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.test.* +import kotlin.test.* + +class PerStreamAcceptableDataMimeTypesMetadataTest : TestWithLeakCheck { + @Test + fun encodeMetadata() { + val metadata = PerStreamAcceptableDataMimeTypesMetadata( + ReservedMimeType(110), + WellKnownMimeType.ApplicationAvro, + CustomMimeType("custom"), + WellKnownMimeType.ApplicationCbor, + ReservedMimeType(120), + CustomMimeType("custom2"), + ) + val decoded = metadata.toPacket(InUseTrackingPool).read(InUseTrackingPool, PerStreamAcceptableDataMimeTypesMetadata) + assertEquals(WellKnownMimeType.MessageRSocketAcceptMimeTypes, decoded.mimeType) + assertEquals(6, decoded.types.size) + assertEquals( + listOf( + ReservedMimeType(110), + WellKnownMimeType.ApplicationAvro, + CustomMimeType("custom"), + WellKnownMimeType.ApplicationCbor, + ReservedMimeType(120), + CustomMimeType("custom2"), + ), + decoded.types + ) + } +} diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/PerStreamDataMimeTypeMetadataTest.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/PerStreamDataMimeTypeMetadataTest.kt new file mode 100644 index 000000000..7734aba76 --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/PerStreamDataMimeTypeMetadataTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.test.* +import kotlin.test.* + +class PerStreamDataMimeTypeMetadataTest : TestWithLeakCheck { + @Test + fun encodeReserved() { + val metadata = PerStreamDataMimeTypeMetadata(ReservedMimeType(110)) + val decoded = metadata.toPacket(InUseTrackingPool).read(InUseTrackingPool, PerStreamDataMimeTypeMetadata) + assertEquals(WellKnownMimeType.MessageRSocketMimeType, decoded.mimeType) + assertEquals(ReservedMimeType(110), decoded.type) + } + + @Test + fun encodeCustom() { + val metadata = PerStreamDataMimeTypeMetadata(CustomMimeType("custom-2")) + val decoded = metadata.toPacket(InUseTrackingPool).read(InUseTrackingPool, PerStreamDataMimeTypeMetadata) + assertEquals(WellKnownMimeType.MessageRSocketMimeType, decoded.mimeType) + assertEquals(CustomMimeType("custom-2"), decoded.type) + } + + @Test + fun encodeWellKnown() { + val metadata = PerStreamDataMimeTypeMetadata(WellKnownMimeType.ApplicationGraphql) + val decoded = metadata.toPacket(InUseTrackingPool).read(InUseTrackingPool, PerStreamDataMimeTypeMetadata) + assertEquals(WellKnownMimeType.MessageRSocketMimeType, decoded.mimeType) + assertEquals(WellKnownMimeType.ApplicationGraphql, decoded.type) + } +} diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/RoutingMetadataTest.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/RoutingMetadataTest.kt new file mode 100644 index 000000000..2ee2a4876 --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/RoutingMetadataTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.rsocket.kotlin.test.* +import kotlin.test.* + +class RoutingMetadataTest : TestWithLeakCheck { + @Test + fun encodeMetadata() { + val tags = listOf("ws://localhost:8080/rsocket", "x".repeat(200)) + val metadata = RoutingMetadata(tags) + val decodedMetadata = metadata.toPacket(InUseTrackingPool).read(InUseTrackingPool, RoutingMetadata) + assertEquals(tags, decodedMetadata.tags) + } + + @Test + fun failOnEmptyTag() { + assertFailsWith(IllegalArgumentException::class) { + RoutingMetadata(listOf("", "tag")) + } + } + + @Test + fun failOnLongTag() { + assertFailsWith(IllegalArgumentException::class) { + RoutingMetadata(listOf("tag", "t".repeat(256))) + } + } +} diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/Util.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/Util.kt new file mode 100644 index 000000000..03a72aeaf --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/Util.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.rsocket.kotlin.test.* + +fun Metadata.readLoop(reader: MetadataReader): M = toPacket(InUseTrackingPool).read(InUseTrackingPool, reader) diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/ZipkinTracingMetadataTest.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/ZipkinTracingMetadataTest.kt new file mode 100644 index 000000000..2b67d123c --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/ZipkinTracingMetadataTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata + +import io.rsocket.kotlin.test.* +import kotlin.random.* +import kotlin.test.* + +class ZipkinTracingMetadataTest : TestWithLeakCheck { + + @Test + fun encodeEmptyTrace() = forAllKinds { kind -> + val metadata = ZipkinTracingMetadata(kind) + + val decoded = metadata.readLoop(ZipkinTracingMetadata) + assertEquals(kind, decoded.kind) + assertFalse(decoded.hasIds) + assertFalse(decoded.hasParentSpanId) + assertFalse(decoded.extendedTraceId) + assertEquals(0, decoded.traceId) + assertEquals(0, decoded.traceIdHigh) + assertEquals(0, decoded.spanId) + assertEquals(0, decoded.parentSpanId) + } + + @Test + fun encodeTraceWithTraceAndSpan() = forAllKinds { kind -> + val traceId = Random.nextLong() + val spanId = Random.nextLong() + val metadata = ZipkinTracingMetadata(kind, traceId, spanId) + + val decoded = metadata.readLoop(ZipkinTracingMetadata) + assertEquals(kind, decoded.kind) + assertTrue(decoded.hasIds) + assertFalse(decoded.hasParentSpanId) + assertFalse(decoded.extendedTraceId) + assertEquals(traceId, decoded.traceId) + assertEquals(0, decoded.traceIdHigh) + assertEquals(spanId, decoded.spanId) + assertEquals(0, decoded.parentSpanId) + } + + @Test + fun encodeTraceWithTraceAndParentSpan() = forAllKinds { kind -> + val traceId = Random.nextLong() + val spanId = Random.nextLong() + val parentSpanId = Random.nextLong() + val metadata = ZipkinTracingMetadata(kind, traceId, spanId, parentSpanId) + + val decoded = metadata.readLoop(ZipkinTracingMetadata) + assertEquals(kind, decoded.kind) + assertTrue(decoded.hasIds) + assertTrue(decoded.hasParentSpanId) + assertFalse(decoded.extendedTraceId) + assertEquals(traceId, decoded.traceId) + assertEquals(0, decoded.traceIdHigh) + assertEquals(spanId, decoded.spanId) + assertEquals(parentSpanId, decoded.parentSpanId) + } + + @Test + fun encodeTraceWithExtendedTraceAndSpan() = forAllKinds { kind -> + val traceId = Random.nextLong() + val traceIdHigh = Random.nextLong() + val spanId = Random.nextLong() + val metadata = ZipkinTracingMetadata128(kind, traceId, traceIdHigh, spanId) + + val decoded = metadata.readLoop(ZipkinTracingMetadata) + assertEquals(kind, decoded.kind) + assertTrue(decoded.hasIds) + assertFalse(decoded.hasParentSpanId) + assertTrue(decoded.extendedTraceId) + assertEquals(traceId, decoded.traceId) + assertEquals(traceIdHigh, decoded.traceIdHigh) + assertEquals(spanId, decoded.spanId) + assertEquals(0, decoded.parentSpanId) + } + + @Test + fun encodeTraceWithExtendedTraceAndParentSpan() = forAllKinds { kind -> + val traceId = Random.nextLong() + val traceIdHigh = Random.nextLong() + val spanId = Random.nextLong() + val parentSpanId = Random.nextLong() + val metadata = ZipkinTracingMetadata128(kind, traceId, traceIdHigh, spanId, parentSpanId) + + val decoded = metadata.readLoop(ZipkinTracingMetadata) + assertEquals(kind, decoded.kind) + assertTrue(decoded.hasIds) + assertTrue(decoded.hasParentSpanId) + assertTrue(decoded.extendedTraceId) + assertEquals(traceId, decoded.traceId) + assertEquals(traceIdHigh, decoded.traceIdHigh) + assertEquals(spanId, decoded.spanId) + assertEquals(parentSpanId, decoded.parentSpanId) + } + + private fun forAllKinds(block: (kind: ZipkinTracingMetadata.Kind) -> Unit) { + ZipkinTracingMetadata.Kind.values().forEach(block) + } +} diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/security/AuthMetadataTest.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/security/AuthMetadataTest.kt new file mode 100644 index 000000000..cb086a580 --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/metadata/security/AuthMetadataTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.metadata.security + +import io.rsocket.kotlin.metadata.* +import io.rsocket.kotlin.test.* +import kotlin.test.* + +class AuthMetadataTest : TestWithLeakCheck { + @Test + fun encodeSimple() { + val metadata = SimpleAuthMetadata("user", "password1234") + val decoded = metadata.readLoop(SimpleAuthMetadata) + + assertEquals("user", decoded.username) + assertEquals("password1234", decoded.password) + } + + @Test + fun encodeSimpleComplex() { + val name = "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎" + val metadata = SimpleAuthMetadata(name, "password1234") + val decoded = metadata.readLoop(SimpleAuthMetadata) + + assertEquals(name, decoded.username) + assertEquals("password1234", decoded.password) + } + + @Test + fun encodeSimpleVeryComplex() { + val name = "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎1234567#4? " + val metadata = SimpleAuthMetadata(name, "password1234") + val decoded = metadata.readLoop(SimpleAuthMetadata) + + assertEquals(name, decoded.username) + assertEquals("password1234", decoded.password) + } + + @Test + fun encodeBearer() { + val token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpYXQxIjoxNTE2MjM5MDIyLCJpYXQyIjoxNTE2MjM5MDIyLCJpYXQzIjoxNTE2MjM5MDIyLCJpYXQ0IjoxNTE2MjM5MDIyfQ.ljYuH-GNyyhhLcx-rHMchRkGbNsR2_4aSxo8XjrYrSM"; + val metadata = BearerAuthMetadata(token) + val decoded = metadata.readLoop(BearerAuthMetadata) + + assertEquals(token, decoded.token) + } + + @Test + fun failOnLongUsername() { + assertFailsWith(IllegalArgumentException::class) { + SimpleAuthMetadata("x".repeat(66000), "") + } + } + + @Test + fun encodeCustomAuth() { + val metadata = RawAuthMetadata(CustomAuthType("custom/auth"), packet("hello world auth data")) + val decoded = metadata.readLoop(RawAuthMetadata) + + assertEquals(CustomAuthType("custom/auth"), decoded.type) + assertEquals("hello world auth data", decoded.content.readText()) + } + + @Test + fun failOnNonAscii() { + assertFailsWith(IllegalArgumentException::class) { + CustomAuthType("1234567#4? 𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎") + } + } + + @Test + fun failOnLongMimeType() { + assertFailsWith(IllegalArgumentException::class) { + CustomAuthType("1234567890".repeat(13)) + } + } + + @Test + fun failOnEmptyMimeType() { + assertFailsWith(IllegalArgumentException::class) { + CustomAuthType("") + } + } + +} diff --git a/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/payload/PayloadBuilderTest.kt b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/payload/PayloadBuilderTest.kt new file mode 100644 index 000000000..ea8194a7e --- /dev/null +++ b/rsocket-core/src/commonTest/kotlin/io/rsocket/kotlin/payload/PayloadBuilderTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * 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 io.rsocket.kotlin.payload + +import io.ktor.utils.io.core.* +import io.rsocket.kotlin.test.* +import kotlin.test.* + +class PayloadBuilderTest : TestWithLeakCheck { + + @Test + fun payloadCopy() { + val payload = Payload(packet("data"), packet("metadata")) + val copy = payload.copy() + + assertTrue(payload.data.isNotEmpty) + assertTrue(payload.metadata?.isNotEmpty == true) + assertTrue(copy.data.isNotEmpty) + assertTrue(copy.metadata?.isNotEmpty == true) + + assertBytesEquals(payload.data.readBytes(), copy.data.readBytes()) + assertBytesEquals(payload.metadata?.readBytes(), copy.metadata?.readBytes()) + } + + @Test + fun payloadRelease() { + Payload(packet("data"), packet("metadata")).release() + } + + @Test + fun failOnBuilderWithNoData() { + assertFailsWith(IllegalStateException::class) { + buildPayload { + metadata(packet("metadata")) + } + } + } + + @Test + fun failOnBuilderDataReassignment() { + assertFailsWith(IllegalStateException::class) { + buildPayload { + data(packet("data")) + data(packet("data2")) + } + } + } + + @Test + fun failOnBuilderMetadataReassignment() { + assertFailsWith(IllegalStateException::class) { + buildPayload { + metadata(packet("data")) + metadata(packet("data2")) + } + } + } +} diff --git a/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/Packets.kt b/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/Packets.kt index ab89d0d97..09467f6ec 100644 --- a/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/Packets.kt +++ b/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/Packets.kt @@ -23,6 +23,8 @@ import io.ktor.utils.io.pool.* import io.rsocket.kotlin.payload.* import kotlin.test.* +fun packet(block: BytePacketBuilder.() -> Unit): ByteReadPacket = buildPacket(InUseTrackingPool, block) + fun packet(text: String): ByteReadPacket = buildPacket(InUseTrackingPool) { writeText(text) } fun packet(array: ByteArray): ByteReadPacket = buildPacket(InUseTrackingPool) { writeFully(array) } diff --git a/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/TestRSocket.kt b/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/TestRSocket.kt index 97a9f1d11..189755a97 100644 --- a/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/TestRSocket.kt +++ b/rsocket-test/src/commonMain/kotlin/io/rsocket/kotlin/test/TestRSocket.kt @@ -31,7 +31,7 @@ class TestRSocket : RSocket { override suspend fun requestResponse(payload: Payload): Payload { payload.release() - return Payload(data, metadata) + return Payload(packet(data), packet(metadata)) } override fun requestStream(payload: Payload): Flow = flow {