diff --git a/koap-core/api/koap-core.api b/koap-core/api/koap-core.api index c44cec48..7c839964 100644 --- a/koap-core/api/koap-core.api +++ b/koap-core/api/koap-core.api @@ -314,6 +314,19 @@ public final class com/juul/koap/Message$Option$Edhoc : com/juul/koap/Message$Op public fun toString ()Ljava/lang/String; } +public final class com/juul/koap/Message$Option$ExperimentalUse : com/juul/koap/Message$Option { + public fun (I[B)V + public final fun component1 ()I + public final fun component2 ()[B + public final fun copy (I[B)Lcom/juul/koap/Message$Option$ExperimentalUse; + public static synthetic fun copy$default (Lcom/juul/koap/Message$Option$ExperimentalUse;I[BILjava/lang/Object;)Lcom/juul/koap/Message$Option$ExperimentalUse; + public fun equals (Ljava/lang/Object;)Z + public final fun getNumber ()I + public final fun getValue ()[B + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract class com/juul/koap/Message$Option$Format : com/juul/koap/Message$Option { public abstract fun getNumber ()I } @@ -554,6 +567,18 @@ public final class com/juul/koap/Message$Option$RequestTag : com/juul/koap/Messa public fun toString ()Ljava/lang/String; } +public final class com/juul/koap/Message$Option$Reserved : com/juul/koap/Message$Option { + public final fun component1 ()I + public final fun component2 ()[B + public final fun copy (I[B)Lcom/juul/koap/Message$Option$Reserved; + public static synthetic fun copy$default (Lcom/juul/koap/Message$Option$Reserved;I[BILjava/lang/Object;)Lcom/juul/koap/Message$Option$Reserved; + public fun equals (Ljava/lang/Object;)Z + public final fun getNumber ()I + public final fun getValue ()[B + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/juul/koap/Message$Option$Size1 : com/juul/koap/Message$Option { public fun (J)V public final fun component1 ()J @@ -576,6 +601,18 @@ public final class com/juul/koap/Message$Option$Size2 : com/juul/koap/Message$Op public fun toString ()Ljava/lang/String; } +public final class com/juul/koap/Message$Option$Unassigned : com/juul/koap/Message$Option { + public final fun component1 ()I + public final fun component2 ()[B + public final fun copy (I[B)Lcom/juul/koap/Message$Option$Unassigned; + public static synthetic fun copy$default (Lcom/juul/koap/Message$Option$Unassigned;I[BILjava/lang/Object;)Lcom/juul/koap/Message$Option$Unassigned; + public fun equals (Ljava/lang/Object;)Z + public final fun getNumber ()I + public final fun getValue ()[B + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/juul/koap/Message$Option$UriHost : com/juul/koap/Message$Option { public fun (Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; diff --git a/koap-core/api/koap-core.klib.api b/koap-core/api/koap-core.klib.api index c2764019..593ab65e 100644 --- a/koap-core/api/koap-core.klib.api +++ b/koap-core/api/koap-core.klib.api @@ -361,6 +361,22 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0] final fun toString(): kotlin/String // com.juul.koap/Message.Option.Echo.toString|toString(){}[0] } + final class ExperimentalUse : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.ExperimentalUse|null[0] + constructor (kotlin/Int, kotlin/ByteArray) // com.juul.koap/Message.Option.ExperimentalUse.|(kotlin.Int;kotlin.ByteArray){}[0] + + final val number // com.juul.koap/Message.Option.ExperimentalUse.number|{}number[0] + final fun (): kotlin/Int // com.juul.koap/Message.Option.ExperimentalUse.number.|(){}[0] + final val value // com.juul.koap/Message.Option.ExperimentalUse.value|{}value[0] + final fun (): kotlin/ByteArray // com.juul.koap/Message.Option.ExperimentalUse.value.|(){}[0] + + final fun component1(): kotlin/Int // com.juul.koap/Message.Option.ExperimentalUse.component1|component1(){}[0] + final fun component2(): kotlin/ByteArray // com.juul.koap/Message.Option.ExperimentalUse.component2|component2(){}[0] + final fun copy(kotlin/Int = ..., kotlin/ByteArray = ...): com.juul.koap/Message.Option.ExperimentalUse // com.juul.koap/Message.Option.ExperimentalUse.copy|copy(kotlin.Int;kotlin.ByteArray){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.juul.koap/Message.Option.ExperimentalUse.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.juul.koap/Message.Option.ExperimentalUse.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.juul.koap/Message.Option.ExperimentalUse.toString|toString(){}[0] + } + final class HopLimit : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.HopLimit|null[0] constructor (kotlin/Long) // com.juul.koap/Message.Option.HopLimit.|(kotlin.Long){}[0] @@ -573,6 +589,20 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0] final fun toString(): kotlin/String // com.juul.koap/Message.Option.RequestTag.toString|toString(){}[0] } + final class Reserved : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Reserved|null[0] + final val number // com.juul.koap/Message.Option.Reserved.number|{}number[0] + final fun (): kotlin/Int // com.juul.koap/Message.Option.Reserved.number.|(){}[0] + final val value // com.juul.koap/Message.Option.Reserved.value|{}value[0] + final fun (): kotlin/ByteArray // com.juul.koap/Message.Option.Reserved.value.|(){}[0] + + final fun component1(): kotlin/Int // com.juul.koap/Message.Option.Reserved.component1|component1(){}[0] + final fun component2(): kotlin/ByteArray // com.juul.koap/Message.Option.Reserved.component2|component2(){}[0] + final fun copy(kotlin/Int = ..., kotlin/ByteArray = ...): com.juul.koap/Message.Option.Reserved // com.juul.koap/Message.Option.Reserved.copy|copy(kotlin.Int;kotlin.ByteArray){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.juul.koap/Message.Option.Reserved.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.juul.koap/Message.Option.Reserved.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.juul.koap/Message.Option.Reserved.toString|toString(){}[0] + } + final class Size1 : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Size1|null[0] constructor (kotlin/Long) // com.juul.koap/Message.Option.Size1.|(kotlin.Long){}[0] @@ -599,6 +629,20 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0] final fun toString(): kotlin/String // com.juul.koap/Message.Option.Size2.toString|toString(){}[0] } + final class Unassigned : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Unassigned|null[0] + final val number // com.juul.koap/Message.Option.Unassigned.number|{}number[0] + final fun (): kotlin/Int // com.juul.koap/Message.Option.Unassigned.number.|(){}[0] + final val value // com.juul.koap/Message.Option.Unassigned.value|{}value[0] + final fun (): kotlin/ByteArray // com.juul.koap/Message.Option.Unassigned.value.|(){}[0] + + final fun component1(): kotlin/Int // com.juul.koap/Message.Option.Unassigned.component1|component1(){}[0] + final fun component2(): kotlin/ByteArray // com.juul.koap/Message.Option.Unassigned.component2|component2(){}[0] + final fun copy(kotlin/Int = ..., kotlin/ByteArray = ...): com.juul.koap/Message.Option.Unassigned // com.juul.koap/Message.Option.Unassigned.copy|copy(kotlin.Int;kotlin.ByteArray){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.juul.koap/Message.Option.Unassigned.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.juul.koap/Message.Option.Unassigned.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.juul.koap/Message.Option.Unassigned.toString|toString(){}[0] + } + final class UriHost : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.UriHost|null[0] constructor (kotlin/String) // com.juul.koap/Message.Option.UriHost.|(kotlin.String){}[0] diff --git a/koap-core/src/commonMain/kotlin/Decoder.kt b/koap-core/src/commonMain/kotlin/Decoder.kt index 8aa21f10..00d50ba6 100644 --- a/koap-core/src/commonMain/kotlin/Decoder.kt +++ b/koap-core/src/commonMain/kotlin/Decoder.kt @@ -33,6 +33,7 @@ import com.juul.koap.Message.Option.ContentFormat import com.juul.koap.Message.Option.ETag import com.juul.koap.Message.Option.Echo import com.juul.koap.Message.Option.Edhoc +import com.juul.koap.Message.Option.ExperimentalUse import com.juul.koap.Message.Option.Format import com.juul.koap.Message.Option.HopLimit import com.juul.koap.Message.Option.IfMatch @@ -48,8 +49,10 @@ import com.juul.koap.Message.Option.ProxyUri import com.juul.koap.Message.Option.QBlock1 import com.juul.koap.Message.Option.QBlock2 import com.juul.koap.Message.Option.RequestTag +import com.juul.koap.Message.Option.Reserved import com.juul.koap.Message.Option.Size1 import com.juul.koap.Message.Option.Size2 +import com.juul.koap.Message.Option.Unassigned import com.juul.koap.Message.Option.UriHost import com.juul.koap.Message.Option.UriPath import com.juul.koap.Message.Option.UriPort @@ -428,7 +431,9 @@ internal fun ByteArrayReader.readOption(preceding: Format?): Option? { 252 -> Echo(readByteArray(length)) 258 -> NoResponse(readNumberOfLength(length)) 292 -> RequestTag(readByteArray(length)) - else -> error("Unsupported option number $number") + in RESERVED_OPTION_NUMBERS -> Reserved(number, readByteArray(length)) + in EXPERIMENTAL_USE_OPTION_RANGE -> ExperimentalUse(number, readByteArray(length)) + else -> Unassigned(number, readByteArray(length)) } } diff --git a/koap-core/src/commonMain/kotlin/Message.kt b/koap-core/src/commonMain/kotlin/Message.kt index 4b3595a7..ee94cf94 100644 --- a/koap-core/src/commonMain/kotlin/Message.kt +++ b/koap-core/src/commonMain/kotlin/Message.kt @@ -49,6 +49,8 @@ private val HOP_LIMIT_RANGE = 1..255 private val ECHO_SIZE_RANGE = 1..40 private val REQUEST_TAG_SIZE_RANGE = 0..8 private val NO_RESPONSE_RANGE = 0..127 +internal val RESERVED_OPTION_NUMBERS = setOf(0, 128, 132, 136, 140) +internal val EXPERIMENTAL_USE_OPTION_RANGE = 65000..65535 sealed class Message { @@ -149,6 +151,67 @@ sealed class Message { ) : Format() } + data class Unassigned internal constructor( + val number: Int, + val value: ByteArray, + ) : Option() { + + override fun equals(other: Any?): Boolean = + this === other || + (other is Unassigned && number == other.number && value.contentEquals(other.value)) + + override fun hashCode(): Int { + var result = number + result = 31 * result + value.contentHashCode() + return result + } + + override fun toString(): String = "Unassigned(number=$number, value=${value.toHexString()})" + } + + /** RFC 7252 5.10.7. Location-Path and Location-Query reserved option numbers, and zero */ + data class Reserved internal constructor( + val number: Int, + val value: ByteArray, + ) : Option() { + + override fun equals(other: Any?): Boolean = + this === other || + (other is Reserved && number == other.number && value.contentEquals(other.value)) + + override fun hashCode(): Int { + var result = number + result = 31 * result + value.contentHashCode() + return result + } + + override fun toString(): String = "Reserved(number=$number, value=${value.toHexString()})" + } + + /** RFC 7252 12.2. CoAP Option Numbers Registry, Table 8, Experimental use */ + data class ExperimentalUse( + val number: Int, + val value: ByteArray, + ) : Option() { + init { + require(number in EXPERIMENTAL_USE_OPTION_RANGE) { + "Option number $number is outside experimental use range of $EXPERIMENTAL_USE_OPTION_RANGE" + } + } + + override fun equals(other: Any?): Boolean = + this === other || + (other is ExperimentalUse && number == other.number && value.contentEquals(other.value)) + + override fun hashCode(): Int { + var result = number + result = 31 * result + value.contentHashCode() + return result + } + + override fun toString(): String = "ExperimentalUse(number=$number, value=${value.toHexString()})" + } + /** RFC 7252 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query */ data class UriHost( val uri: String, diff --git a/koap-core/src/commonMain/kotlin/ToFormat.kt b/koap-core/src/commonMain/kotlin/ToFormat.kt index 2b863d82..06f0481e 100644 --- a/koap-core/src/commonMain/kotlin/ToFormat.kt +++ b/koap-core/src/commonMain/kotlin/ToFormat.kt @@ -8,6 +8,7 @@ import com.juul.koap.Message.Option.ContentFormat import com.juul.koap.Message.Option.ETag import com.juul.koap.Message.Option.Echo import com.juul.koap.Message.Option.Edhoc +import com.juul.koap.Message.Option.ExperimentalUse import com.juul.koap.Message.Option.Format import com.juul.koap.Message.Option.Format.empty import com.juul.koap.Message.Option.Format.opaque @@ -27,8 +28,10 @@ import com.juul.koap.Message.Option.ProxyUri import com.juul.koap.Message.Option.QBlock1 import com.juul.koap.Message.Option.QBlock2 import com.juul.koap.Message.Option.RequestTag +import com.juul.koap.Message.Option.Reserved import com.juul.koap.Message.Option.Size1 import com.juul.koap.Message.Option.Size2 +import com.juul.koap.Message.Option.Unassigned import com.juul.koap.Message.Option.UriHost import com.juul.koap.Message.Option.UriPath import com.juul.koap.Message.Option.UriPort @@ -65,4 +68,7 @@ internal fun Option.toFormat(): Format = is Echo -> opaque(252, option.value) is NoResponse -> uint(258, option.value) is RequestTag -> opaque(292, option.tag) + is Reserved -> opaque(option.number, option.value) + is ExperimentalUse -> opaque(option.number, option.value) + is Unassigned -> opaque(option.number, option.value) } diff --git a/koap-core/src/commonTest/kotlin/DecoderTest.kt b/koap-core/src/commonTest/kotlin/DecoderTest.kt index a8c6f5a5..66848c1f 100644 --- a/koap-core/src/commonTest/kotlin/DecoderTest.kt +++ b/koap-core/src/commonTest/kotlin/DecoderTest.kt @@ -9,6 +9,7 @@ import com.juul.koap.Message.Option.Block1 import com.juul.koap.Message.Option.Block2 import com.juul.koap.Message.Option.Echo import com.juul.koap.Message.Option.Edhoc +import com.juul.koap.Message.Option.ExperimentalUse import com.juul.koap.Message.Option.HopLimit import com.juul.koap.Message.Option.NoResponse import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response4xx @@ -20,8 +21,10 @@ import com.juul.koap.Message.Option.Oscore import com.juul.koap.Message.Option.QBlock1 import com.juul.koap.Message.Option.QBlock2 import com.juul.koap.Message.Option.RequestTag +import com.juul.koap.Message.Option.Reserved import com.juul.koap.Message.Option.Size1 import com.juul.koap.Message.Option.Size2 +import com.juul.koap.Message.Option.Unassigned import com.juul.koap.Message.Option.UriHost import com.juul.koap.Message.Option.UriPath import com.juul.koap.Message.Option.UriPort @@ -459,6 +462,39 @@ class DecoderTest { ) } + @Test + fun decodeUnassignedOption() { + testReadOption( + encoded = """ + E3 11 27 # Option Delta: 0x1234, Option Length: 3 + 01 02 03 # Option Value: 0x01, 0x02, 0x03 + """, + expected = Unassigned(0x1234, byteArrayOf(0x01, 0x02, 0x03)), + ) + } + + @Test + fun decodeReservedOption() { + testReadOption( + encoded = """ + D3 7F # Option Delta: 140, Option Length: 3 + 61 62 63 # Option Value: 0x61, 0x62, 0x63 + """, + expected = Reserved(140, byteArrayOf(0x61, 0x62, 0x63)), + ) + } + + @Test + fun decodeExperimentalUseOption() { + testReadOption( + encoded = """ + E3 FC DE # Option Delta: 65003, Option Length: 3 + 41 42 43 # Option Value: 0x41, 0x42, 0x43 + """, + expected = ExperimentalUse(65003, byteArrayOf(0x41, 0x42, 0x43)), + ) + } + @Test fun decodingTcpMessageDoesNotReadBeyondLengthSpecifiedInHeader() { val message = Message.Tcp( diff --git a/koap-core/src/commonTest/kotlin/EncoderTest.kt b/koap-core/src/commonTest/kotlin/EncoderTest.kt index 5be1f907..b28140ba 100644 --- a/koap-core/src/commonTest/kotlin/EncoderTest.kt +++ b/koap-core/src/commonTest/kotlin/EncoderTest.kt @@ -8,6 +8,7 @@ import com.juul.koap.Message.Option.Block1 import com.juul.koap.Message.Option.Block2 import com.juul.koap.Message.Option.Echo import com.juul.koap.Message.Option.Edhoc +import com.juul.koap.Message.Option.ExperimentalUse import com.juul.koap.Message.Option.HopLimit import com.juul.koap.Message.Option.NoResponse import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response2xx @@ -20,8 +21,10 @@ import com.juul.koap.Message.Option.Oscore import com.juul.koap.Message.Option.QBlock1 import com.juul.koap.Message.Option.QBlock2 import com.juul.koap.Message.Option.RequestTag +import com.juul.koap.Message.Option.Reserved import com.juul.koap.Message.Option.Size1 import com.juul.koap.Message.Option.Size2 +import com.juul.koap.Message.Option.Unassigned import com.juul.koap.Message.Option.UriPath import com.juul.koap.Message.Option.UriPort import com.juul.koap.Message.Udp.Type.Acknowledgement @@ -523,6 +526,46 @@ class EncoderTest { """, ) } + + @Test + fun writeUnassignedOption() { + testWriteOption( + option = Unassigned(0x4321, byteArrayOf(0x04, 0x03, 0x02, 0x01)), + expected = """ + E4 42 14 # Option Delta: 0x4321, Option Length: 4 + 04 03 02 01 # Option Value: 0x04, 0x03, 0x02, 0x01 + """, + ) + } + + @Test + fun writeReservedOption() { + testWriteOption( + option = Reserved(136, byteArrayOf(0x34, 0x33, 0x32, 0x31)), + expected = """ + D4 7B # Option Delta: 136, Option Length: 4 + 34 33 32 31 # Option Value: 0x34, 0x33, 0x32, 0x31 + """, + ) + } + + @Test + fun writeExperimentalUseOption() { + testWriteOption( + option = ExperimentalUse(65007, byteArrayOf(0x24, 0x23, 0x22, 0x21)), + expected = """ + E4 FC E2 # Option Delta: 65007, Option Length: 4 + 24 23 22 21 # Option Value: 0x24, 0x23, 0x22, 0x21 + """, + ) + } + + @Test + fun experimentalUseOptionWithNumberOutsideExperimentalRangeThrowsIllegalArgumentException() { + assertFailsWith { + ExperimentalUse(64999, byteArrayOf()) + } + } } private fun testWriteToken( diff --git a/koap-core/src/commonTest/kotlin/MessageTest.kt b/koap-core/src/commonTest/kotlin/MessageTest.kt index 2d9aa4c0..ca56e306 100644 --- a/koap-core/src/commonTest/kotlin/MessageTest.kt +++ b/koap-core/src/commonTest/kotlin/MessageTest.kt @@ -32,11 +32,14 @@ import com.juul.koap.Message.Option.ContentFormat import com.juul.koap.Message.Option.ETag import com.juul.koap.Message.Option.Echo import com.juul.koap.Message.Option.Edhoc +import com.juul.koap.Message.Option.ExperimentalUse import com.juul.koap.Message.Option.IfMatch import com.juul.koap.Message.Option.IfNoneMatch import com.juul.koap.Message.Option.Oscore import com.juul.koap.Message.Option.QBlock2 import com.juul.koap.Message.Option.RequestTag +import com.juul.koap.Message.Option.Reserved +import com.juul.koap.Message.Option.Unassigned import com.juul.koap.Message.Option.UriHost import com.juul.koap.Message.Option.UriPort import kotlin.test.Test @@ -63,6 +66,9 @@ class MessageTest { assertToString(UriPort(1234), "UriPort(port=1234)") assertToString(Block1(100, true, Block.Size.`64`), "Block1(number=100, more=true, size=64)") assertToString(QBlock2(101, false, Block.Size.Bert), "QBlock2(number=101, more=false, size=Bert)") + assertToString(Unassigned(5674, "abc".encodeToByteArray()), "Unassigned(number=5674, value=61 62 63)") + assertToString(Reserved(136, "abc".encodeToByteArray()), "Reserved(number=136, value=61 62 63)") + assertToString(ExperimentalUse(65123, "abc".encodeToByteArray()), "ExperimentalUse(number=65123, value=61 62 63)") } @Test