diff --git a/.editorconfig b/.editorconfig index ad28fa9..7224619 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,23 @@ -root = true - -[{*.kt,*.kts}] +[*.{kt,kts}] +end_of_line = lf ij_kotlin_allow_trailing_comma = true +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_indent_before_arrow_on_new_line = false +ij_kotlin_line_break_after_multiline_when_entry = true ij_kotlin_allow_trailing_comma_on_call_site = true +indent_size = 4 +indent_style = space +insert_final_newline = true +ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = 8 +ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4 +ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset +ktlint_code_style = intellij_idea +ktlint_enum_entry_name_casing = upper_or_camel_cases +ktlint_function_signature_body_expression_wrapping = default +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset +ktlint_ignore_back_ticked_identifier = false +ktlint_property_naming_constant_naming = screaming_snake_case +max_line_length = off +ktlint_standard_property-naming = disabled +ktlint_standard_discouraged-comment-location = disabled +ktlint_standard_function-expression-body = disabled \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d5a025c..1fc7082 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,11 +9,11 @@ jobs: with: submodules: 'true' - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: 17 + java-version: 21 cache: 'gradle' - name: Gradle build diff --git a/README.md b/README.md index 202a9c1..0215abe 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,20 @@ Self-describing values for Future-proofing [![ci](https://github.com/erwin-kok/multiformat/actions/workflows/ci.yaml/badge.svg)](https://github.com/erwin-kok/multiformat/actions/workflows/ci.yaml) [![Maven Central](https://img.shields.io/maven-central/v/org.erwinkok.multiformat/multiformat)](https://central.sonatype.com/artifact/org.erwinkok.result/result-monad) -[![Kotlin](https://img.shields.io/badge/kotlin-1.9.0-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Kotlin](https://img.shields.io/badge/kotlin-2.3.0-blue.svg?logo=kotlin)](http://kotlinlang.org) [![License](https://img.shields.io/github/license/erwin-kok/multiformat.svg)](https://github.com/erwin-kok/multiformat/blob/master/LICENSE) -## Usage +## Introduction + +This project provides **Kotlin implementations of the Multiformats protocols**. + +Multiformats define self-describing data formats designed to remain interoperable across systems, languages, and decades. They are a foundational building block in systems such as IPFS and libp2p. -Kotlin DSL: +The goal of this project is to offer **clear, explicit, and specification-faithful implementations** of the core Multiformats standards, optimized for correctness and readability rather than convenience abstractions. + +Specifications are defined at https://multiformats.io. + +## Installation ```kotlin repositories { @@ -20,28 +28,30 @@ dependencies { } ``` -## Introduction +## Implemented Protocols -This project implements various protocols defined at: https://multiformats.io/ +- **[multiaddr](https://github.com/multiformats/multiaddr)** — Self-describing network addresses +- **[multibase](https://github.com/multiformats/multibase)** — Base encoding descriptors +- **[multicodec](https://github.com/multiformats/multicodec)** — Self-describing serialization codes +- **[multihash](https://github.com/multiformats/multihash)** — Cryptographic hash identifiers +- **[multistream-select](https://github.com/multiformats/multistream-select)** — Protocol negotiation -Notably, the following protocols are implemented: +Additionally, this project implements: +- **[CID](https://github.com/multiformats/cid)** - Content Identifier -- [multiaddr](https://github.com/multiformats/multiaddr): network addresses -- [multibase](https://github.com/multiformats/multibase): base encodings -- [multicodec](https://github.com/multiformats/multicodec): serialization codes -- [multihash](https://github.com/multiformats/multihash): cryptographic hashes -- [multistream-select](https://github.com/multiformats/multistream-select): Friendly protocol multiplexing. +## Error Handling Model -Next to this, it also implements Cid: https://github.com/multiformats/cid +This project uses an explicit Result monad for error handling. +With few exceptions, public APIs return Result instead of throwing exceptions. This makes error propagation explicit, composable, and visible in the type system. -## Using the Result Monad +This is a deliberate design choice. -This project is using the [result-monad](https://github.com/erwin-kok/result-monad) +Exceptions are implicit and non-local: once execution enters a try block, it is no longer clear which operation caused control flow to jump to a catch clause. This complicates reasoning about resource ownership and cleanup. -This means that (almost) all methods of this project return a `Result<...>`. The caller can check whether an error was generated, -or it can use the value. For example: +By contrast, Result values make success and failure part of normal control flow. +Example: ```kotlin val connection = createConnection() .getOrElse { @@ -50,16 +60,8 @@ val connection = createConnection() } connection.write(...) - - -fun createConnection(): Result { - ... -} ``` - -The advantage is that it is easier (at least for me) to track the flow of the code and to handle correct/error cases. -The disadvantage of exceptions is, is that you do not know which statement in a try-block generated the exception. For -example: +Compared to exception-based control flow: ```kotlin var connection: Connection? = null @@ -76,10 +78,9 @@ try { } ``` -In the catch-block you do not know where the exception was thrown: before or after creating the connection. This means -that you do not know if the connection should be closed, or not. Of course there are many ways to solve this. +In the catch block, it is unclear whether the connection was successfully created or not. -With the result-monad you can do something like: +Using Result makes this explicit: ```kotlin val connection = createConnection() .getOrElse { @@ -94,12 +95,11 @@ methodGeneratingError() } ... ``` +The control flow and resource lifetime are explicit and locally reasoned about. ## Usage -A (very) brief description on how to use multiformats: - -...but please also look at the various tests. +This section provides a brief overview. For more comprehensive examples, see the test suite. ### multiaddr @@ -146,7 +146,6 @@ val selected = MultistreamMuxer.selectOneOf(setOf("/a", "/b", "/c"), connection) } ``` - ## Sub-modules This project has three submodules: @@ -157,16 +156,11 @@ git submodule add https://github.com/multiformats/multibase src/main/kotlin/org/ git submodule add https://github.com/multiformats/multihash src/main/kotlin/org/erwinkok/multiformat/spec/multihash ``` -These are the official specifications repositories, which are used here for auto-generation code and or verifying the -test results are according to spec. - -## Contributing - -Bug reports and pull requests are welcome on [GitHub](https://github.com/erwin-kok/multiformat). - -## Contact +They are used for: -If you want to contact me, please write an e-mail to: [erwin.kok(at)protonmail.com](mailto:erwin.kok@protonmail.com) +- Code generation +- Conformance testing +- Verifying behavior against the specifications ## License diff --git a/build.gradle.kts b/build.gradle.kts index cf89a61..9caadbd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,8 @@ -// Copyright (c) 2022 Erwin Kok. BSD-3-Clause license. See LICENSE file for more details. -@file:Suppress("UnstableApiUsage") - import com.adarshr.gradle.testlogger.theme.ThemeType +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask -@Suppress("DSL_SCOPE_VIOLATION") plugins { - kotlin("jvm") version "2.0.20" + kotlin("jvm") version "2.3.0" `java-library` `java-test-fixtures` @@ -30,7 +27,7 @@ repositories { } group = "org.erwinkok.multiformat" -version = "1.1.0" +version = "1.2.0" java { withSourcesJar() @@ -58,33 +55,28 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.debug) + testRuntimeOnly(libs.logback.classic) testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } testlogger { theme = ThemeType.MOCHA } -tasks { - withType().configureEach { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - } - } - - withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_17.toString() - targetCompatibility = JavaVersion.VERSION_17.toString() +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) } +} - withType { - rejectVersionIf { - isNonStable(candidate.version) - } - } +kotlin { + jvmToolchain(21) +} - test { - useJUnitPlatform() +tasks.withType { + rejectVersionIf { + isNonStable(candidate.version) } } @@ -95,6 +87,10 @@ fun isNonStable(version: String): Boolean { return isStable.not() } +tasks.test { + useJUnitPlatform() +} + sourceSets { main { java { @@ -103,31 +99,21 @@ sourceSets { } } -koverReport { - filters { - excludes { - classes( - "org.erwinkok.multiformat.multicodec.Codec", - "org.erwinkok.multiformat.multicodec.GenerateKt*", - ) - } - } - - defaults { - html { - onCheck = true +kover { + reports { + filters { + excludes { + classes( + "org.erwinkok.multiformat.multicodec.Codec", + "org.erwinkok.multiformat.multicodec.GenerateKt*", + ) + } } verify { - onCheck = true rule { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.APPLICATION bound { - minValue = 0 - maxValue = 99 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.LINE - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + minValue.set(0) } } } @@ -153,7 +139,7 @@ publishing { developer { id.set("erwin-kok") name.set("Erwin Kok") - email.set("erwin-kok@gmx.com") + email.set("erwin.kok@protonmail.com") url.set("https://github.com/erwin-kok/") roles.set(listOf("owner", "developer")) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42d17e1..130d1ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,22 @@ [versions] -kotlinx-coroutines = "1.9.0" -kotlinx-atomicfu = "0.25.0" -kotlin-logging = "3.0.5" -kotlinx-serialization = "1.7.3" -junit-jupiter = "5.11.1" -slf4j-api = "2.0.16" - -kotlin = "2.0.20" -kover-plugin = "0.7.2" -ktlint-plugin = "11.5.0" -nexus-plugin = "1.3.0" -versions-plugin = "0.47.0" +kotlinx-coroutines = "1.10.2" +kotlinx-atomicfu = "0.29.0" +kotlin-logging = "7.0.14" +kotlinx-serialization = "1.9.0" +junit-jupiter = "6.0.2" +slf4j-api = "2.0.17" + +kotlin = "2.3.0" +kover-plugin = "0.9.4" +ktlint-plugin = "14.0.1" +nexus-plugin = "2.0.0" +versions-plugin = "0.53.0" testlogger-plugin = "4.0.0" -protobuf-plugin = "0.9.3" +protobuf-plugin = "0.9.6" ipaddress = "5.5.1" -ktor = "2.3.12" +ktor = "3.3.3" +logback-classic = "1.5.18" result-monad = "1.4.0" @@ -24,7 +25,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kotlinx-coroutines" } kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinx-atomicfu" } -kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" } +kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlin-logging" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } ipaddress = { module = "com.github.seancfoley:ipaddress", version.ref = "ipaddress" } @@ -36,6 +37,8 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-jupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-classic" } + [plugins] build-kotlin = { id = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } build-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover-plugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7..f8e1ee3 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17a8ddc..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d4..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -133,10 +132,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -169,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -197,16 +198,19 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 6689b85..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/main/kotlin/org/erwinkok/multiformat/multiaddress/Multiaddress.kt b/src/main/kotlin/org/erwinkok/multiformat/multiaddress/Multiaddress.kt index ef72286..35d69d9 100644 --- a/src/main/kotlin/org/erwinkok/multiformat/multiaddress/Multiaddress.kt +++ b/src/main/kotlin/org/erwinkok/multiformat/multiaddress/Multiaddress.kt @@ -14,7 +14,7 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream class Multiaddress private constructor(val components: List) { - private val _string: String by lazy { constructString() } + private val string: String by lazy { constructString() } val bytes: ByteArray by lazy { constructBytes() } fun encapsulate(address: String): Result { @@ -58,7 +58,7 @@ class Multiaddress private constructor(val components: List) { } override fun toString(): String { - return _string + return string } override fun equals(other: Any?): Boolean { diff --git a/src/main/kotlin/org/erwinkok/multiformat/multicodec/Generate.kt b/src/main/kotlin/org/erwinkok/multiformat/multicodec/Generate.kt index c8650b6..a3be7fd 100644 --- a/src/main/kotlin/org/erwinkok/multiformat/multicodec/Generate.kt +++ b/src/main/kotlin/org/erwinkok/multiformat/multicodec/Generate.kt @@ -1,5 +1,4 @@ // Copyright (c) 2022 Erwin Kok. BSD-3-Clause license. See LICENSE file for more details. -// ktlint-disable filename package org.erwinkok.multiformat.multicodec import java.io.BufferedReader diff --git a/src/main/kotlin/org/erwinkok/multiformat/multihash/MultihashRegistry.kt b/src/main/kotlin/org/erwinkok/multiformat/multihash/MultihashRegistry.kt index 6871540..1053674 100644 --- a/src/main/kotlin/org/erwinkok/multiformat/multihash/MultihashRegistry.kt +++ b/src/main/kotlin/org/erwinkok/multiformat/multihash/MultihashRegistry.kt @@ -1,7 +1,7 @@ // Copyright (c) 2022 Erwin Kok. BSD-3-Clause license. See LICENSE file for more details. package org.erwinkok.multiformat.multihash -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import org.erwinkok.multiformat.multicodec.Multicodec import org.erwinkok.multiformat.multihash.hashers.Blake2b import org.erwinkok.multiformat.multihash.hashers.Blake2s @@ -71,7 +71,7 @@ object MultihashRegistry { }, { val message = "Could not register hasher for $type: ${errorMessage(it)}" - logger.warn(message) + logger.warn { message } Err(message) }, ) @@ -87,7 +87,7 @@ object MultihashRegistry { }, { val message = "Could not register hasher for $type: ${errorMessage(it)}" - logger.warn(message) + logger.warn { message } Err(message) }, ) diff --git a/src/main/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxer.kt b/src/main/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxer.kt index 3135d11..ff840a3 100644 --- a/src/main/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxer.kt +++ b/src/main/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxer.kt @@ -1,11 +1,11 @@ // Copyright (c) 2022 Erwin Kok. BSD-3-Clause license. See LICENSE file for more details. package org.erwinkok.multiformat.multistream +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.util.collections.ConcurrentSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import mu.KotlinLogging import org.erwinkok.result.Err import org.erwinkok.result.Error import org.erwinkok.result.Errors.EndOfStream diff --git a/src/test/kotlin/org/erwinkok/multiformat/multibase/MultibaseTest.kt b/src/test/kotlin/org/erwinkok/multiformat/multibase/MultibaseTest.kt index 75a842b..43e8cb0 100644 --- a/src/test/kotlin/org/erwinkok/multiformat/multibase/MultibaseTest.kt +++ b/src/test/kotlin/org/erwinkok/multiformat/multibase/MultibaseTest.kt @@ -1,7 +1,7 @@ // Copyright (c) 2022 Erwin Kok. BSD-3-Clause license. See LICENSE file for more details. package org.erwinkok.multiformat.multibase -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import org.erwinkok.result.expectNoErrors import org.erwinkok.result.onFailure import org.erwinkok.result.onSuccess diff --git a/src/test/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxerTest.kt b/src/test/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxerTest.kt index f42344f..bd96c4c 100644 --- a/src/test/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxerTest.kt +++ b/src/test/kotlin/org/erwinkok/multiformat/multistream/MultistreamMuxerTest.kt @@ -3,19 +3,14 @@ package org.erwinkok.multiformat.multistream -import io.ktor.utils.io.core.BytePacketBuilder -import io.ktor.utils.io.core.buildPacket -import io.ktor.utils.io.core.readBytes import io.ktor.utils.io.core.writeFully import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest -import org.erwinkok.multiformat.util.UVarInt -import org.erwinkok.result.Err +import kotlinx.io.Buffer +import kotlinx.io.readByteArray import org.erwinkok.result.Error -import org.erwinkok.result.Errors import org.erwinkok.result.Ok -import org.erwinkok.result.Result import org.erwinkok.result.coAssertErrorResult import org.erwinkok.result.expectNoErrors import org.erwinkok.result.getError @@ -47,6 +42,8 @@ internal class MultistreamMuxerTest { ) val selectOneResult = MultistreamMuxer.selectOneOf(setOf(ProtocolId.of("/proto1")), localConnection).expectNoErrors() + localConnection.close() + assertEquals("/proto1", selectOneResult.id) remoteReceived( @@ -74,6 +71,8 @@ internal class MultistreamMuxerTest { ), localConnection, ).expectNoErrors() + localConnection.close() + assertEquals("/proto3", selectOneResult.id) remoteReceived( @@ -92,6 +91,8 @@ internal class MultistreamMuxerTest { ) val selectOneResult = MultistreamMuxer.selectOneOf(setOf(ProtocolId.of("/proto2")), localConnection) + localConnection.close() + assertEquals(Error("Peer does not support any of the given protocols"), selectOneResult.getError()) remoteReceived( @@ -109,6 +110,8 @@ internal class MultistreamMuxerTest { muxer.addHandler(ProtocolId.of("/proto3")) val negotiateResult = muxer.negotiate(localConnection).expectNoErrors() + localConnection.close() + assertEquals("/proto3", negotiateResult.protocol.id) assertEquals(null, negotiateResult.handler) @@ -134,6 +137,8 @@ internal class MultistreamMuxerTest { Ok(Unit) } val negotiateResult = muxer.negotiate(localConnection).expectNoErrors() + localConnection.close() + assertEquals("/proto4", negotiateResult.protocol.id) assertNotNull(negotiateResult.handler) negotiateResult.handler?.invoke(ProtocolId.of("AProtocol"), remoteConnection) @@ -155,6 +160,8 @@ internal class MultistreamMuxerTest { ) val listResult = muxer.list(localConnection).expectNoErrors() + localConnection.close() + assertEquals("/proto1, /proto2, /proto3/sub-proto", listResult.joinToString(", ")) remoteReceived( @@ -267,7 +274,7 @@ internal class MultistreamMuxerTest { var simOpenInfo: SimOpenInfo? = null val job = launch { simOpenInfo = MultistreamMuxer.selectWithSimopenOrFail(setOf(ProtocolId.of("/a")), remoteConnection).expectNoErrors() - assertEquals("/a", simOpenInfo!!.protocol.id, "incorrect protocol selected") + assertEquals("/a", simOpenInfo.protocol.id, "incorrect protocol selected") } val simOpenInfo2 = MultistreamMuxer.selectWithSimopenOrFail(setOf(ProtocolId.of("/a")), localConnection).expectNoErrors() assertEquals("/a", simOpenInfo2.protocol.id, "incorrect protocol selected") @@ -280,7 +287,7 @@ internal class MultistreamMuxerTest { var simOpenInfo: SimOpenInfo? = null val job = launch { simOpenInfo = MultistreamMuxer.selectWithSimopenOrFail(setOf(ProtocolId.of("/a"), ProtocolId.of("/b")), remoteConnection).expectNoErrors() - assertEquals("/b", simOpenInfo!!.protocol.id, "incorrect protocol selected") + assertEquals("/b", simOpenInfo.protocol.id, "incorrect protocol selected") } val simOpenInfo2 = MultistreamMuxer.selectWithSimopenOrFail(setOf(ProtocolId.of("/b")), localConnection).expectNoErrors() assertEquals("/b", simOpenInfo2.protocol.id, "incorrect protocol selected") @@ -300,38 +307,24 @@ internal class MultistreamMuxerTest { } private suspend fun remoteSends(vararg messages: String) { - val packet = buildPacket { - for (message in messages) { - val messageNewline = message + '\n' - writeUnsignedVarInt(messageNewline.length.toULong()) - writeFully(messageNewline.toByteArray()) - } + val buffer = Buffer() + for (message in messages) { + val messageNewline = message + '\n' + buffer.writeUnsignedVarInt(messageNewline.length.toULong()) + buffer.writeFully(messageNewline.toByteArray()) } - remoteConnection.output.writePacket(packet) - remoteConnection.output.flush() + remoteConnection.writeRawBuffer(buffer) } private suspend fun remoteReceived(vararg messages: String) { - val packet = buildPacket { - for (message in messages) { - val messageNewline = message + '\n' - writeUnsignedVarInt(messageNewline.length.toULong()) - writeFully(messageNewline.toByteArray()) - } + val buffer = Buffer() + for (message in messages) { + val messageNewline = message + '\n' + buffer.writeUnsignedVarInt(messageNewline.length.toULong()) + buffer.writeFully(messageNewline.toByteArray()) } - val expected = String(packet.readBytes()) - val actual = String(remoteConnection.readAll().readBytes()) + val expected = String(buffer.readByteArray()) + val actual = String(remoteConnection.readRawBuffer().readByteArray()) assertEquals(expected, actual) } } - -fun BytePacketBuilder.writeUnsignedVarInt(x: ULong): Result { - return UVarInt.writeUnsignedVarInt(x) { - try { - this.writeByte(it) - Ok(Unit) - } catch (e: Exception) { - Err(Errors.EndOfStream) - } - } -} diff --git a/src/test/kotlin/org/erwinkok/multiformat/multistream/TestUtf8Connection.kt b/src/test/kotlin/org/erwinkok/multiformat/multistream/TestUtf8Connection.kt index efb87ab..f15cda1 100644 --- a/src/test/kotlin/org/erwinkok/multiformat/multistream/TestUtf8Connection.kt +++ b/src/test/kotlin/org/erwinkok/multiformat/multistream/TestUtf8Connection.kt @@ -3,12 +3,13 @@ package org.erwinkok.multiformat.multistream import io.ktor.utils.io.ByteChannel import io.ktor.utils.io.cancel -import io.ktor.utils.io.close -import io.ktor.utils.io.core.ByteReadPacket -import io.ktor.utils.io.core.buildPacket -import io.ktor.utils.io.core.readBytes -import io.ktor.utils.io.core.writeFully +import io.ktor.utils.io.readBuffer +import io.ktor.utils.io.readByte +import io.ktor.utils.io.writeBuffer import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.io.Buffer +import kotlinx.io.EOFException +import kotlinx.io.readByteArray import org.erwinkok.multiformat.util.UVarInt import org.erwinkok.result.Err import org.erwinkok.result.Errors @@ -23,57 +24,56 @@ class TestUtf8Connection { val local = Inner(input, output) val remote = Inner(output, input) - inner class Inner( - val input: ByteChannel, - val output: ByteChannel, + class Inner( + private val input: ByteChannel, + private val output: ByteChannel, ) : Utf8Connection { override suspend fun readUtf8(): Result { return readUnsignedVarInt() .map { it.toInt() } .map { wanted -> try { - val packet = input.readPacket(wanted) - if (packet.remaining.toInt() != wanted) { - val result = Err("Required $wanted bytes, but received ${packet.remaining}") - packet.close() - return result + val source = input.readBuffer(wanted) + if (!source.request(wanted.toLong())) { + return Err("Required $wanted bytes, but stream closed") } - val bytes = packet.readBytes() + val bytes = source.readByteArray(wanted) if (bytes.isEmpty() || Char(bytes[bytes.size - 1].toInt()) != '\n') { return Err("message did not have trailing newline") } - packet.close() return Ok(String(bytes).trim { it <= ' ' }) - } catch (e: ClosedReceiveChannelException) { + } catch (_: ClosedReceiveChannelException) { return Err(Errors.EndOfStream) } } } override suspend fun writeUtf8(vararg messages: String): Result { - val packet = buildPacket { - for (message in messages) { - val messageNewline = message + '\n' - writeUnsignedVarInt(messageNewline.length.toULong()) - writeFully(messageNewline.toByteArray()) - } + val buffer = Buffer() + for (message in messages) { + val messageNewline = message + '\n' + buffer.writeUnsignedVarInt(messageNewline.length.toULong()) + buffer.write(messageNewline.toByteArray()) } - output.writePacket(packet) + output.writeBuffer(buffer) + output.flush() return Ok(Unit) } + suspend fun readRawBuffer(): Buffer { + return input.readBuffer() + } + + suspend fun writeRawBuffer(buffer: Buffer) { + output.writeBuffer(buffer) + output.flush() + } + override fun close() { input.cancel() output.close() } - suspend fun readAll(): ByteReadPacket { - return buildPacket { - val count = input.availableForRead - writePacket(input.readPacket(count)) - } - } - private suspend fun readUnsignedVarInt(): Result { return UVarInt.coReadUnsignedVarInt { readByte() } } @@ -81,9 +81,22 @@ class TestUtf8Connection { private suspend fun readByte(): Result { return try { Ok(input.readByte().toUByte()) - } catch (e: ClosedReceiveChannelException) { + } catch (_: EOFException) { + Err(Errors.EndOfStream) + } catch (_: ClosedReceiveChannelException) { Err(Errors.EndOfStream) } } } } + +fun Buffer.writeUnsignedVarInt(x: ULong): Result { + return UVarInt.writeUnsignedVarInt(x) { + try { + this.writeByte(it) + Ok(Unit) + } catch (_: Exception) { + Err(Errors.EndOfStream) + } + } +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..86912a2 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + %black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): %msg%n%throwable + + + + + + ${LOGS}/application.log + + %d %p %C{1} [%t] %m%n + + + + + ${LOGS}/archived/application-%d{yyyy-MM-dd}.%i.log + + 10MB + 30 + 10MB + + + + + + + + + + + + + +