From 46395b7b3686c07717652d86a24b07d65da0f053 Mon Sep 17 00:00:00 2001 From: Evgeni Margolis Date: Thu, 4 May 2023 23:14:35 -0700 Subject: [PATCH] Implemented Json to Tlv and Tlv to Json Converter in Kotlin Note that NOT all TLV configurations are supported by the current implementation. Here is the list of limitations: - TLV Lists are not supported - Multi-Dimensional TLV Arrays are not supported - All elements of an array MUST be of the same type - The top level TLV element MUST be a single structure with AnonymousTag - The following tags are supported: - AnonymousTag are used only with TLV Arrays elements or a top-level structure - ContextSpecificTag are used only with TLV Structure elements - CommonProfileTag are used only with TLV Structure elements - Infinity Float/Double values are not supported --- examples/java-matter-controller/BUILD.gn | 3 +- src/controller/java/BUILD.gn | 35 +- .../java/src/chip/json/JsonToTlv.kt | 201 ++ .../java/src/chip/json/TlvToJson.kt | 205 +++ src/controller/java/src/chip/tlv/utils.kt | 16 +- .../tests/chip/json/JsonToTlvToJsonTest.kt | 1630 +++++++++++++++++ third_party/java_deps/BUILD.gn | 6 +- third_party/java_deps/set_up_java_deps.sh | 3 +- 8 files changed, 2093 insertions(+), 6 deletions(-) create mode 100644 src/controller/java/src/chip/json/JsonToTlv.kt create mode 100644 src/controller/java/src/chip/json/TlvToJson.kt create mode 100644 src/controller/java/tests/chip/json/JsonToTlvToJsonTest.kt diff --git a/examples/java-matter-controller/BUILD.gn b/examples/java-matter-controller/BUILD.gn index cdb67f1b8ed41d..92ae40df938568 100644 --- a/examples/java-matter-controller/BUILD.gn +++ b/examples/java-matter-controller/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Project CHIP Authors +# Copyright (c) 2022-2023 Project CHIP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ kotlin_binary("java-matter-controller") { output_name = "java-matter-controller" deps = [ ":java", + "${chip_root}/src/controller/java:json_to_tlv_to_json_test", "${chip_root}/src/controller/java:tlv_read_write_test", "${chip_root}/src/controller/java:tlv_reader_test", "${chip_root}/src/controller/java:tlv_writer_test", diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 84565a2bf18e08..a92ec4164aae43 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2021 Project CHIP Authors +# Copyright (c) 2020-2023 Project CHIP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -204,6 +204,39 @@ kotlin_library("tlv_read_write_test") { kotlinc_flags = [ "-Xlint:deprecation" ] } +kotlin_library("json") { + output_name = "libCHIPJson.jar" + + deps = [ + ":tlv", + "${chip_root}/third_party/java_deps:gson", + ] + + sources = [ + "src/chip/json/JsonToTlv.kt", + "src/chip/json/TlvToJson.kt", + ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + +kotlin_library("json_to_tlv_to_json_test") { + output_name = "JsonToTlvToJsonTest.jar" + + deps = [ + ":json", + ":tlv", + "${chip_root}/third_party/java_deps:gson", + "${chip_root}/third_party/java_deps:junit-4", + "${chip_root}/third_party/java_deps:kotlin-test", + "${chip_root}/third_party/java_deps:truth", + ] + + sources = [ "tests/chip/json/JsonToTlvToJsonTest.kt" ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + android_library("java") { output_name = "CHIPController.jar" diff --git a/src/controller/java/src/chip/json/JsonToTlv.kt b/src/controller/java/src/chip/json/JsonToTlv.kt new file mode 100644 index 00000000000000..4ef5889ecf2c5b --- /dev/null +++ b/src/controller/java/src/chip/json/JsonToTlv.kt @@ -0,0 +1,201 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2023 Google LLC. + * + * 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 chip.json + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.protobuf.ByteString +import java.util.Base64 + +/** + * Implements Matter JSON to TLV converter. + * + * Note that NOT all TLV configurations are supported by the current implementation. Below is the + * list of limitations: + * - TLV Lists are not supported + * - Multi-Dimensional TLV Arrays are not supported + * - All elements of an array MUST be of the same type + * - The top level TLV element MUST be a single structure with AnonymousTag + * - The following tags are supported: + * - AnonymousTag are used only with TLV Arrays elements or a top-level structure + * - ContextSpecificTag are used only with TLV Structure elements + * - CommonProfileTag are used only with TLV Structure elements + * - Infinity Float/Double values are not supported + * + * @param json string representing Json encoded data to be converted into TLV format + * @throws IllegalArgumentException if the data was invalid + */ +fun TlvWriter.fromJsonString(json: String): ByteArray { + validateIsJsonObjectAndConvert(JsonParser.parseString(json), AnonymousTag) + return validateTlv().getEncoded() +} + +/** + * Converts Json Object into TLV Structure or TLV top level elements. + * + * @param json object to be converted to TLV. + * @throws IllegalArgumentException if the data was invalid + */ +private fun TlvWriter.fromJson(json: JsonObject): TlvWriter { + json.keySet().forEach { key -> + val (tag, type, subType) = extractTagAndTypeFromJsonKey(key) + fromJson(json.get(key), tag, type, subType) + } + return this +} + +/** + * Converts Json Array into TLV Array. + * + * @param json object to be converted to TLV. + * @param type Type of array elements. + * @throws IllegalArgumentException if the data was invalid + */ +private fun TlvWriter.fromJson(json: JsonArray, type: String): TlvWriter { + json.iterator().forEach { element -> fromJson(element, AnonymousTag, type) } + return this +} + +/** + * Converts Json Element into TLV Array. + * + * @param element element to be converted to TLV. + * @param tag element tag. + * @param type element type. + * @param subType array elements type. Only relevant when type is an Array. Should be empty string + * in all other cases. + * @throws IllegalArgumentException if the data was invalid + */ +private fun TlvWriter.fromJson(element: JsonElement, tag: Tag, type: String, subType: String = "") { + when (type) { + JSON_VALUE_TYPE_INT -> put(tag, validateIsNumber(element).toLong()) + JSON_VALUE_TYPE_UINT -> put(tag, validateIsNumber(element).toLong().toULong()) + JSON_VALUE_TYPE_BOOL -> put(tag, validateIsBoolean(element)) + JSON_VALUE_TYPE_FLOAT -> put(tag, validateIsDouble(element).toFloat()) + JSON_VALUE_TYPE_DOUBLE -> put(tag, validateIsDouble(element)) + JSON_VALUE_TYPE_BYTES -> put(tag, validateIsString(element).base64Encode()) + JSON_VALUE_TYPE_STRING -> put(tag, validateIsString(element)) + JSON_VALUE_TYPE_NULL -> validateIsNullAndPut(element, tag) + JSON_VALUE_TYPE_STRUCT -> validateIsJsonObjectAndConvert(element, tag) + JSON_VALUE_TYPE_ARRAY -> { + if (subType.isEmpty()) { + throw IllegalArgumentException("Multi-Dimensional JSON Array is Invalid") + } else { + require(element.isJsonArray()) { "Expected Array; the actual element is: $element" } + startArray(tag).fromJson(element.getAsJsonArray(), subType).endArray() + } + } + JSON_VALUE_TYPE_EMPTY -> + throw IllegalArgumentException("Empty array was expected but there is value: $element}") + else -> throw IllegalArgumentException("Invalid type was specified: $type") + } +} + +/** + * Extracts tag and type fields from Json key. Valid JSON key SHOULD have 1, 2, or 3 fields + * constracted as [name:][tag:]type[-subtype] + * + * @param key Json element key value. + * @throws IllegalArgumentException if the key format was invalid + */ +private fun extractTagAndTypeFromJsonKey(key: String): Triple { + val keyFields = key.split(":") + var type = keyFields.last() + val typeFields = type.split("-") + var subType = "" + + val tagNumber = + when (keyFields.size) { + 2 -> keyFields.first().toUIntOrNull() + 3 -> keyFields[1].toUIntOrNull() + else -> throw IllegalArgumentException("Invalid JSON key value: $key") + } + + val tag = + when { + tagNumber == null -> throw IllegalArgumentException("Invalid JSON key value: $key") + tagNumber <= UByte.MAX_VALUE.toUInt() -> ContextSpecificTag(tagNumber.toInt()) + tagNumber <= UShort.MAX_VALUE.toUInt() -> CommonProfileTag(2, tagNumber) + else -> CommonProfileTag(4, tagNumber) + } + + // Valid type field of the JSON key SHOULD have type and optional subtype component + require(typeFields.size in (1..2)) { "Invalid JSON key value: $key" } + + if (typeFields.size == 2) { + require(typeFields[0] == JSON_VALUE_TYPE_ARRAY) { "Invalid JSON key value: $key" } + type = JSON_VALUE_TYPE_ARRAY + subType = typeFields[1] + } + + return Triple(tag, type, subType) +} + +private fun String.base64Encode(): ByteString { + return ByteString.copyFrom(Base64.getDecoder().decode(this)) +} + +/** Verifies JsonElement is Number. If yes, returns the value. */ +private fun validateIsNumber(element: JsonElement): Number { + require( + element.isJsonPrimitive() && + (element.getAsJsonPrimitive().isNumber() || element.getAsJsonPrimitive().isString()) + ) { + "Expected Integer represented as a Number or as a String; the actual element is: $element" + } + return element.getAsJsonPrimitive().getAsNumber() +} + +/** Verifies JsonElement is Boolean. If yes, returns the value. */ +private fun validateIsBoolean(element: JsonElement): Boolean { + require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean()) { + "Expected Boolean; the actual element is: $element" + } + return element.getAsJsonPrimitive().getAsBoolean() +} + +/** Verifies JsonElement is Double. If yes, returns the value. */ +private fun validateIsDouble(element: JsonElement): Double { + require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) { + "Expected Double; the actual element is: $element" + } + return element.getAsJsonPrimitive().getAsDouble() +} + +/** Verifies JsonElement is String. If yes, returns the value. */ +private fun validateIsString(element: JsonElement): String { + require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + "Expected String; the actual element is: $element" + } + return element.getAsJsonPrimitive().getAsString() +} + +/** Verifies JsonElement is Null. If yes, puts it into TLV. */ +private fun TlvWriter.validateIsNullAndPut(element: JsonElement, tag: Tag) { + require(element.isJsonNull()) { "Expected Null; the actual element is: $element" } + putNull(tag) +} + +/** Verifies JsonElement is JsonObject. If yes, converts it into TLV Structure. */ +private fun TlvWriter.validateIsJsonObjectAndConvert(element: JsonElement, tag: Tag) { + require(element.isJsonObject()) { "Expected JsonObject; the actual element is: $element" } + startStructure(tag).fromJson(element.getAsJsonObject()).endStructure() +} diff --git a/src/controller/java/src/chip/json/TlvToJson.kt b/src/controller/java/src/chip/json/TlvToJson.kt new file mode 100644 index 00000000000000..cd1e6c30245400 --- /dev/null +++ b/src/controller/java/src/chip/json/TlvToJson.kt @@ -0,0 +1,205 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2023 Google LLC. + * + * 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 chip.json + +import com.google.gson.JsonArray +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import java.util.Base64 + +/** + * Implements Matter TLV to JSON converter. + * + * Note that NOT all TLV configurations are supported by the current implementation. Below is the + * list of limitations: + * - TLV Lists are not supported + * - Multi-Dimensional TLV Arrays are not supported + * - All elements of an array MUST be of the same type + * - The top level TLV element MUST be a single structure with AnonymousTag + * - The following tags are supported: + * - AnonymousTag are used only with TLV Arrays elements or a top-level structure + * - ContextSpecificTag are used only with TLV Structure elements + * - CommonProfileTag are used only with TLV Structure elements + * - Infinity Float/Double values are not supported + * + * Rules for representing integers in the Json format: + * - If the size of the integer is less or equal to 32-bits then it is represetned as a Number. + * - If the size of the integer is larger than 32 bits then it will be represented as a String. + * + * @throws IllegalArgumentException if the data was invalid + */ +fun TlvReader.toJsonString(): String { + val element = nextElement() + require(element.value is StructureValue) { + "The top level element must be a structure. The actual value is ${element.value}" + } + require(element.tag is AnonymousTag) { + "The top level TLV Structure MUST have anonymous tag. The actual tag is ${element.tag}" + } + return getStructJson().toString() +} +/** + * Encodes TLV Structure into Json Object. The TLV reader should be positioned at the start of a TLV + * Structure (StructureValue element). After this call the TLV reader is positioned at the end of + * the a TLV Structure (EndOfContainerValue element). + */ +private fun TlvReader.getStructJson(): JsonObject { + var json = JsonObject() + while (!isEndOfTlv()) { + val element = nextElement() + val tag = element.tag + val value = element.value + + val key = + when (tag) { + is AnonymousTag -> "" + is ContextSpecificTag -> tag.tagNumber.toString() + ":" + is CommonProfileTag -> tag.tagNumber.toString() + ":" + else -> throw IllegalArgumentException("Unsupported TLV tag format: $tag") + } + getJsonValueTypeField(value) + + when (value) { + is IntValue -> { + if (value.value >= Int.MIN_VALUE && value.value <= Int.MAX_VALUE) { + json.addProperty(key, value.value) + } else { + json.addProperty(key, value.value.toString()) + } + } + is UnsignedIntValue -> { + if (value.value.toULong() <= UInt.MAX_VALUE.toULong()) { + json.addProperty(key, value.value) + } else { + json.addProperty(key, value.value.toULong().toString()) + } + } + is Utf8StringValue -> json.addProperty(key, value.value) + is ByteStringValue -> + json.addProperty(key, Base64.getEncoder().encodeToString(value.value.toByteArray())) + is BooleanValue -> json.addProperty(key, value.value) + is FloatValue -> json.addProperty(key, validateFloat(value.value)) + is DoubleValue -> json.addProperty(key, validateDouble(value.value)) + is StructureValue -> json.add(key, getStructJson()) + is ArrayValue -> { + val (array, type) = getArrayJsonWithElementsType() + json.add("$key-$type", array) + } + is ListValue -> + throw IllegalArgumentException("Invalid TLV element: TLV List is not supported") + is NullValue -> json.add(key, JsonNull.INSTANCE) + is EndOfContainerValue -> return json + } + } + return json +} + +/** + * Encodes TLV Array data into Json Array. The TLV reader should be positioned at the start of a TLV + * Array (ArrayValue element). After this call the TLV reader is positioned at the end of the a TLV + * Structure (EndOfContainerValue element). This method returns Json encoded array and a String + * specifying types of the elements in the array. + */ +private fun TlvReader.getArrayJsonWithElementsType(): Pair { + var json = JsonArray() + var lastValue: Value = ArrayValue + + while (!isEndOfTlv()) { + val value = nextElement().value + if (lastValue !is ArrayValue && value !is EndOfContainerValue) { + require(value::class == lastValue::class) { + "Invalid TLV element: all elements in array MUST be of the same type. Value ($value) is different from previous value ($lastValue)." + } + } + + when (value) { + is IntValue -> { + if (value.value >= Int.MIN_VALUE && value.value <= Int.MAX_VALUE) { + json.add(value.value) + } else { + json.add(value.value.toString()) + } + } + is UnsignedIntValue -> { + if (value.value.toULong() <= UInt.MAX_VALUE) { + json.add(value.value) + } else { + json.add(value.value.toULong().toString()) + } + } + is Utf8StringValue -> json.add(value.value) + is ByteStringValue -> json.add(Base64.getEncoder().encodeToString(value.value.toByteArray())) + is BooleanValue -> json.add(value.value) + is FloatValue -> json.add(validateFloat(value.value)) + is DoubleValue -> json.add(validateDouble(value.value)) + is StructureValue -> json.add(getStructJson()) + is ArrayValue -> + throw IllegalArgumentException( + "Invalid TLV element: multi-dimensional TLV Array not supported" + ) + is ListValue -> throw IllegalArgumentException("Invalid TLV Element: TLV List not supported") + is NullValue -> json.add(JsonNull.INSTANCE) + is EndOfContainerValue -> { + var subType = getJsonValueTypeField(lastValue) + if (subType == JSON_VALUE_TYPE_ARRAY) { + subType = JSON_VALUE_TYPE_EMPTY + } + return Pair(json, subType) + } + } + + lastValue = value + } + + throw IllegalArgumentException( + "Invalid TLV structure: TLV Array with last value ($lastValue) is not closed" + ) +} + +/** Returns type string that should be encoded in the Json key string for the specified value. */ +private fun getJsonValueTypeField(value: Value): String { + return when (value) { + is IntValue -> JSON_VALUE_TYPE_INT + is UnsignedIntValue -> JSON_VALUE_TYPE_UINT + is BooleanValue -> JSON_VALUE_TYPE_BOOL + is FloatValue -> JSON_VALUE_TYPE_FLOAT + is DoubleValue -> JSON_VALUE_TYPE_DOUBLE + is ByteStringValue -> JSON_VALUE_TYPE_BYTES + is Utf8StringValue -> JSON_VALUE_TYPE_STRING + is NullValue -> JSON_VALUE_TYPE_NULL + is StructureValue -> JSON_VALUE_TYPE_STRUCT + is ArrayValue -> JSON_VALUE_TYPE_ARRAY + else -> JSON_VALUE_TYPE_EMPTY + } +} + +/** Verifies that Float value is valid supported value. */ +private fun validateFloat(value: Float): Float { + require(value != Float.NEGATIVE_INFINITY && value != Float.POSITIVE_INFINITY) { + "Unsupported Float Infinity value" + } + return value +} + +/** Verifies that Double value is valid supported value. */ +private fun validateDouble(value: Double): Double { + require(value != Double.NEGATIVE_INFINITY && value != Double.POSITIVE_INFINITY) { + "Unsupported Double Infinity value" + } + return value +} diff --git a/src/controller/java/src/chip/tlv/utils.kt b/src/controller/java/src/chip/tlv/utils.kt index e68e7482045495..3aadf79bdf621e 100644 --- a/src/controller/java/src/chip/tlv/utils.kt +++ b/src/controller/java/src/chip/tlv/utils.kt @@ -20,8 +20,8 @@ package chip.tlv /** Converts bytes in a Little Endian format into Long integer. */ internal fun ByteArray.fromLittleEndianToLong(isSigned: Boolean = false): Long = - foldRightIndexed(0) { i, it, acc -> - (acc shl 8) or (if (i == lastIndex && isSigned) it.toLong() else (it.toLong() and 0xFF)) + foldRightIndexed(0) { i, value, acc -> + (acc shl 8) or (if (i == lastIndex && isSigned) value.toLong() else (value.toLong() and 0xFF)) } /** Converts Number into a byte array in a Little Endian format. */ @@ -62,3 +62,15 @@ internal fun unsignedIntSize(value: ULong): Short { } internal fun Byte.toBinary(): String = Integer.toBinaryString(toInt() and 0xFF) + +internal const val JSON_VALUE_TYPE_INT = "INT" +internal const val JSON_VALUE_TYPE_UINT = "UINT" +internal const val JSON_VALUE_TYPE_BOOL = "BOOL" +internal const val JSON_VALUE_TYPE_FLOAT = "FLOAT" +internal const val JSON_VALUE_TYPE_DOUBLE = "DOUBLE" +internal const val JSON_VALUE_TYPE_BYTES = "BYTES" +internal const val JSON_VALUE_TYPE_STRING = "STRING" +internal const val JSON_VALUE_TYPE_NULL = "NULL" +internal const val JSON_VALUE_TYPE_STRUCT = "STRUCT" +internal const val JSON_VALUE_TYPE_ARRAY = "ARRAY" +internal const val JSON_VALUE_TYPE_EMPTY = "?" diff --git a/src/controller/java/tests/chip/json/JsonToTlvToJsonTest.kt b/src/controller/java/tests/chip/json/JsonToTlvToJsonTest.kt new file mode 100644 index 00000000000000..04bfb69eb809fa --- /dev/null +++ b/src/controller/java/tests/chip/json/JsonToTlvToJsonTest.kt @@ -0,0 +1,1630 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2023 Google LLC. + * + * 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 chip.json + +import com.google.common.truth.Truth.assertThat +import com.google.gson.JsonParser +import com.google.protobuf.ByteString +import kotlin.test.assertFailsWith +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class JsonToTlvToJsonTest { + + private fun String.octetsToByteArray(): ByteArray = + replace(" ", "").chunked(2).map { it.toInt(16) and 0xFF }.map { it.toByte() }.toByteArray() + + private fun String.toByteString(): ByteString = ByteString.copyFrom(this.toByteArray()) + + private fun checkValidConversion( + jsonOriginal: String, + tlvEncoding: ByteArray, + jsonExpected: String = jsonOriginal + ) { + assertThat(TlvWriter().fromJsonString(jsonOriginal)).isEqualTo(tlvEncoding) + assertThat(TlvReader(tlvEncoding).toJsonString()) + .isEqualTo(JsonParser.parseString(jsonExpected).asJsonObject.toString()) + if (jsonOriginal != jsonExpected) { + assertThat(TlvWriter().fromJsonString(jsonExpected)).isEqualTo(tlvEncoding) + } + } + + @Test + fun convertBoolean_false() { + // Boolean false + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(0), false) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "0:BOOL" : false + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertBoolean_true() { + // Boolean true + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), true) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:BOOL" : true + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertSignedInt_1BytePositive() { + // Signed Integer 42, 1-octet + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(2), 42) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "2:INT" : 42 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertSignedInt_1ByteNegative() { + // Signed Integer -17, 1-octet + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(3), -17) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "3:INT" : -17 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertUnsignedInt_1Byte() { + // Unsigned Integer 42, 1-octet + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(4), 42U) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "value:4:UINT" : 42 + } + """ + val expectedJson = """ + { + "4:UINT" : 42 + } + """ + + checkValidConversion(json, encoding, expectedJson) + } + + @Test + fun convertSignedInt_1Byte2octet() { + // Signed Integer 42, 1-byte encoded as 2-octet + val encoding = "15 21 06 2a 00 18".octetsToByteArray() + val expectedJson = """ + { + "6:INT" : 42 + } + """ + + // Note: the current implementation follows the minimum encoding policy, which encodes this + // value as 1-octet. Testing only decoding. + assertThat(TlvReader(encoding).toJsonString()) + .isEqualTo(JsonParser.parseString(expectedJson).asJsonObject.toString()) + } + + @Test + fun convertSignedInt_2Bytes() { + // Signed Integer 4242, 2-octet + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(7), 4242) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "7:INT" : 4242 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertSignedInt_4Bytes() { + // Signed Integer -170000, 4-octet + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(80), -170000) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "80:INT" : -170000 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertSignedInt_8Bytes() { + // Signed Integer (Long) 40000000000, 8-octet + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(202), 40000000000) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "202:INT" : "40000000000" + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertUnsignedInt_8Bytes() { + // Unsigned Integer (Long) 40000000000, 8-octet + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(222), 40000000000U) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "222:UINT" : "40000000000" + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertUtf8String_hello() { + // UTF-8 String, 1-octet length, "Hello!" + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(0), "Hello!") + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "0:STRING" : "Hello!" + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertUtf8String_tschuh() { + // UTF-8 String, 1-octet length, "Tschüs" + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(250), "Tschüs") + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "250:STRING" : "Tschüs" + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertOctetString() { + // Octet String, 1-octet length, octets 00 01 02 03 04 + val value = ByteString.fromHex("0001020304") + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), value) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:BYTES" : "AAECAwQ=" + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertNull() { + // Null + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .putNull(ContextSpecificTag(1)) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:NULL" : null + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertFloat_0() { + // Single precision floating point 0.0 + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), 0.0f) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:FLOAT" : 0.0 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertFloat_1third() { + // Single precision floating point (1.0 / 3.0) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(100), 1.0f / 3.0f) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "100:FLOAT" : 0.33333334 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertFloat_17_9() { + // Single precision floating point 17.9 + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(101), 17.9f) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "101:FLOAT" : 17.9 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertFloat_positiveInfinity_throwsIllegalArgumentException() { + // Single precision floating point infinity (∞) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), Float.POSITIVE_INFINITY) + .endStructure() + .validateTlv() + .getEncoded() + + // Throws exception because the encoded value is unsupported Infinity value + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertFloat_negativeInfinity_throwsIllegalArgumentException() { + // Single precision floating point negative infinity (-∞) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), Float.NEGATIVE_INFINITY) + .endStructure() + .validateTlv() + .getEncoded() + + // Throws exception because the encoded value is unsupported Infinity value + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertDouble_0() { + // Double precision floating point 0.0 + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), 0.0) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:DOUBLE" : 0.0 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertDouble_1third() { + // Double precision floating point (1.0 / 3.0) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), 1.0 / 3.0) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:DOUBLE" : 0.3333333333333333 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertDouble_17_9() { + // Double precision floating point 17.9 + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), 17.9) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:DOUBLE" : 17.9 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertDouble_positiveInfinity_throwsIllegalArgumentException() { + // Double precision floating point infinity (∞) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), Double.POSITIVE_INFINITY) + .endStructure() + .validateTlv() + .getEncoded() + + // Throws exception because the encoded value is unsupported Infinity value + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertDouble_negativeInfinity_throwsIllegalArgumentException() { + // Double precision floating point negative infinity (-∞) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), Double.NEGATIVE_INFINITY) + .endStructure() + .validateTlv() + .getEncoded() + + // Throws exception because the encoded value is unsupported Infinity value + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertEmptyStructure() { + // Empty Structure, {} + val encoding = + TlvWriter().startStructure(AnonymousTag).endStructure().validateTlv().getEncoded() + val json = """ + { + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertEmptyStructureWithinStructure() { + // Empty Structure, {} + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startStructure(ContextSpecificTag(1)) + .endStructure() + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:STRUCT" : {} + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertEmptyStructureWithCommonProfileTag_throwsIllegalArgumentException() { + // Empty Structure, {} + val encoding = + TlvWriter() + .startStructure(CommonProfileTag(2, 1000u)) + .endStructure() + .validateTlv() + .getEncoded() + + // Throws exception because top level element must be anonymous Structure + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertTopLevelArray_throwsIllegalArgumentException() { + // Empty Array, [] + val tag = AnonymousTag + val encoding = TlvWriter().startArray(tag).endArray().validateTlv().getEncoded() + + // Throws exception because top level element must be anonymous Structure + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertTopLevelInt_throwsIllegalArgumentException() { + // Empty Array, [] + val value = 42 + val tag = CommonProfileTag(2, 1000u) + val encoding = TlvWriter().put(tag, value).validateTlv().getEncoded() + + // Throws exception because top level element must be anonymous Structure + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertArray_empty() { + // Empty Array, [] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(1)) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1:ARRAY-?" : [] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArrayWithCommonProfileTag2_empty() { + // Empty Array, [] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(CommonProfileTag(2, 10000u)) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "value:10000:ARRAY-?" : [] + } + """ + val expectedJson = """ + { + "10000:ARRAY-?" : [] + } + """ + + checkValidConversion(json, encoding, expectedJson) + } + + @Test + fun convertArrayWithCommonProfileTag4_empty() { + // Empty Array, [] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(CommonProfileTag(4, 1000000u)) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1000000:ARRAY-?" : [] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertList_empty_throwsIllegalArgumentException() { + // Empty List, [] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startList(ContextSpecificTag(1)) + .endList() + .endStructure() + .validateTlv() + .getEncoded() + + // Throws exception because TLV Lists are not supported + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertIntegersWithContextTags() { + // Structure, two context specific tags, Signed Integer, 1 octet values, {0 = 42, 1 = -17} + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(0), 42) + .put(ContextSpecificTag(1), -17) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "0:INT" : 42, + "1:INT" : -17 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertStructure_intsWithContextTags() { + // Structure, two context specific tags, Signed Integer, 1 octet values, {0 = 42, 1 = -17} + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startStructure(ContextSpecificTag(0)) + .put(ContextSpecificTag(0), 42) + .put(ContextSpecificTag(1), -17) + .endStructure() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:STRUCT" : { + "0:INT" : 42, + "1:INT" : -17 + } + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_ints() { + // Array of Signed Integers (1-octet values): [0, 1, 2, 3, 4] + val values = longArrayOf(0, 1, 2, 3, 4) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .putSignedLongArray(ContextSpecificTag(0), values) + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-INT" : [ + 0, + 1, + 2, + 3, + 4 + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_double() { + // Array of Doubles: [1.1, 134.2763, -12345.87] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(0)) + .put(AnonymousTag, 1.1) + .put(AnonymousTag, 134.2763) + .put(AnonymousTag, -12345.87) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-DOUBLE" : [ + 1.1, + 134.2763, + -12345.87 + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_float() { + // Array of Floats: [1.1, 134.2763, -12345.87] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(CommonProfileTag(2, 1000u)) + .put(AnonymousTag, 1.1f) + .put(AnonymousTag, 134.2763f) + .put(AnonymousTag, -12345.87f) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "1000:ARRAY-FLOAT" : [ + 1.1, + 134.2763, + -12345.87 + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_string() { + // Array of Strings: ["ABC", "Options", "more"] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(CommonProfileTag(4, 100000u)) + .put(AnonymousTag, "ABC") + .put(AnonymousTag, "Options") + .put(AnonymousTag, "more") + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "100000:ARRAY-STRING" : [ + "ABC", + "Options", + "more" + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_boolean() { + // Array of Booleans: [true, false, false] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(255)) + .put(AnonymousTag, true) + .put(AnonymousTag, false) + .put(AnonymousTag, false) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "255:ARRAY-BOOL" : [ + true, + false, + false + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_null() { + // Array of Nulls: [null, null] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(1)) + .putNull(AnonymousTag) + .putNull(AnonymousTag) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "1:ARRAY-NULL" : [ + null, + null + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_boolean_throwsIllegalArgumentException() { + // Array of bools: error type doesn't match + val json = + """ + { + "value:1:ARRAY-BOOL" : [ + "yes", + "no" + ] + } + """ + + // Throws exception because subtype encoded in the Json key (Boolean) doesn't match the String + // type of the elements in the array + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertArray_uint_throwsIllegalArgumentException() { + // Array of unsigned integers: error type doesn't match + val json = + """ + { + "value:1:ARRAY-UINT" : [ + "yes", + "no" + ] + } + """ + + // Throws exception because subtype encoded in the Json key (Boolean) doesn't match the String + // type of the elements in the array + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertArray_float_throwsIllegalArgumentException() { + // Array of floats: error type doesn't match + val json = + """ + { + "1:ARRAY-FLOAT" : [ + { + "1" : 22, + "2" : 23 + } + ] + } + """ + + // Throws exception because subtype encoded in the Json key (Float) doesn't match the Structure + // type of the elements in the array + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertArray_uint2_throwsIllegalArgumentException() { + // Array of unsigned integer: error type doesn't match + val json = """ + { + "2:ARRAY-UINT" : [ + null + ] + } + """ + + // Throws exception because subtype encoded in the Json key (UInt) doesn't match the Null + // type of the elements in the array + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertByteStringArray_throwsIllegalArgumentException() { + // Anonymous Array of ByteString, [{00 01 02 03 04}, {FF}, {4A EF 88}] + val json = + """ + { + "value:ARRAY-BYTES": [ + "AA45ECAwQ=" + ] + } + """ + + // Throws exception because string is invalid base64 encoded value + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertArray_mixedValues_throwsIllegalArgumentException() { + // Array of mixed elements: [42, -170000, {}, 17.9, "Hello!"] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(0)) + .put(AnonymousTag, 42) + .put(AnonymousTag, -170000) + .startStructure(AnonymousTag) + .endStructure() + .put(AnonymousTag, 17.9) + .put(AnonymousTag, "Hello!") + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + + // Throws exception because TLV Array Must have same type elements + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertAnonymousTag_throwsIllegalArgumentException() { + // Anonymous tag, Unsigned Integer, 1-octet value, 42U + val json = """ + { + "value:UINT" : 42 + } + """ + + // Throws exception because element within structure cannot have anonymous tag + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertKeyWithoutTag_throwsIllegalArgumentException2() { + // Anonymous tag, Unsigned Integer, 1-octet value, 42U + val json = """ + { + "UINT" : 42 + } + """ + + // Throws exception because Json key must have valid tag field + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertContextTag_withinStructure() { + // Context tag 255 (max), Unsigned Integer, 1-octet value: {255 = 42U} + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startStructure(ContextSpecificTag(0)) + .put(ContextSpecificTag(255), 42U) + .endStructure() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "value:0:STRUCT": { + "name:255:UINT" : 42 + } + } + """ + val expectedJson = + """ + { + "0:STRUCT": { + "255:UINT" : 42 + } + } + """ + + checkValidConversion(json, encoding, expectedJson) + } + + @Test + fun convertStructWithMixedTags() { + // Context and Common Profile tags, Unsigned Integer structure: {255 = 42, 256 = 17000, 65535 = + // 1, 65536 = 345678, 4294967295 = 500000000000} + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startStructure(ContextSpecificTag(0)) + .put(ContextSpecificTag(UByte.MAX_VALUE.toInt()), 42U) + .put(CommonProfileTag(2, UByte.MAX_VALUE + 1U), 17000U) + .put(CommonProfileTag(2, UShort.MAX_VALUE.toUInt()), 1U) + .put(CommonProfileTag(4, UShort.MAX_VALUE + 1U), 345678U) + .put(CommonProfileTag(4, UInt.MAX_VALUE), 500000000000U) + .endStructure() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:STRUCT": { + "255:UINT" : 42, + "256:UINT" : 17000, + "65535:UINT" : 1, + "65536:UINT" : 345678, + "4294967295:UINT" : "500000000000" + } + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertContextTag_invalidContextTag_throwsIllegalArgumentException() { + // Context and Common Profile tags, Unsigned Integer structure: {255 = 42, 256 = 17000, 65535 = + // 1, 65536 = 345678, 4294967295 = 500000000000, , 4294967296 = 34} + val json = + """ + { + "0:STRUCT": { + "255:UINT" : 42, + "256:UINT" : 17000, + "65535:UINT" : 1, + "65536:UINT" : 345678, + "4294967295:UINT" : "500000000000", + "invalid:4294967296:UINT" : 34 + } + } + """ + + // 4294967296 exceeds valid context specific or common profile tag value of 32-bits + assertFailsWith { TlvWriter().fromJsonString(json) } + } + + @Test + fun convertCommonProfileTag2() { + // Common profile tag 1, Unsigned Integer, 1-octet value, Matter::1 = 42U + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(CommonProfileTag(2, 1000u), 42U) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "1000:UINT" : 42 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun encodeCommonProfileTag4() { + // Common profile tag 100000, Unsigned Integer, 1-octet value, Matter::100000 = 42U + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(CommonProfileTag(4, 100000u), 42U) + .endStructure() + .validateTlv() + .getEncoded() + val json = """ + { + "100000:UINT" : 42 + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertFullyQualifiedTag8_throwsIllegalArgumentException() { + // Fully qualified tag, Vendor ID 0xFFF1/65521, profile number 0xDEED/57069, 4-octet tag + // 0xAA55FEED/2857762541, Unsigned Integer, 1-octet value 42, 65521::57069:2857762541 = 42U + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(FullyQualifiedTag(8, 0xFFF1u, 0xDEEDu, 0xAA55FEEDu), 42U) + .endStructure() + .validateTlv() + .getEncoded() + + // Fully qualified tags are not supported + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertFullyQualifiedTags_throwsIllegalArgumentException() { + // Structure with the fully qualified tag, Vendor ID 0xFFF1/65521, profile number 0xDEED/57069, + // 2-octet tag 1. The structure contains a single element labeled using a fully qualified tag + // under the same profile, with 2-octet tag 0xAA55/43605.65521::57069:1 = {65521::57069:43605 = + // 42U} + val value = 42U + val structTag = FullyQualifiedTag(6, 0xFFF1u, 0xDEEDu, 1u) + val valueTag = FullyQualifiedTag(6, 0xFFF1u, 57069u, 43605u) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startStructure(structTag) + .put(valueTag, value) + .endStructure() + .endStructure() + .validateTlv() + .getEncoded() + + // Fully qualified tags are not supported + assertFailsWith { TlvReader(encoding).toJsonString() } + } + + @Test + fun convertSignedLongArray() { + // Anonymous Array of Signed Integers: [42, -17, -170000, 40000000000] + val values = longArrayOf(42, -17, -170000, 40000000000) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .putSignedLongArray(ContextSpecificTag(0), values) + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-INT": [ + 42, + -17, + -170000, + "40000000000" + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertUnsignedLongArray() { + // Anonymous Array of Unigned Integers: [42, 170000, 40000000000] + val values = longArrayOf(42, 170000, 40000000000) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .putUnsignedLongArray(ContextSpecificTag(0), values) + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-UINT": [ + 42, + 170000, + "40000000000" + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertByteStringArray() { + // Anonymous Array of ByteString, [{00 01 02 03 04}, {FF}, {4A EF 88}] + val values = + listOf( + ByteString.fromHex("0001020304"), + ByteString.fromHex("FF"), + ByteString.fromHex("4AEF88") + ) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .putByteStringArray(ContextSpecificTag(0), values) + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-BYTES": [ + "AAECAwQ=", + "/w==", + "Su+I" + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertStructure_Mixed() { + // Structure with mixed elements + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startStructure(ContextSpecificTag(0)) + .put(ContextSpecificTag(0), 20.toLong()) + .put(ContextSpecificTag(1), true) + .put(ContextSpecificTag(2), 0.toULong()) + .put(ContextSpecificTag(3), "Test ByteString Value".toByteString()) + .put(ContextSpecificTag(4), "hello") + .put(ContextSpecificTag(5), -500000) + .put(ContextSpecificTag(6), 17.9) + .put(ContextSpecificTag(7), 17.9f) + .endStructure() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:STRUCT": { + "0:INT": 20, + "1:BOOL": true, + "2:UINT": 0, + "3:BYTES": "VGVzdCBCeXRlU3RyaW5nIFZhbHVl", + "4:STRING": "hello", + "5:INT": -500000, + "6:DOUBLE": 17.9, + "7:FLOAT": 17.9 + } + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArrayofStructureWithMixedElements() { + // Array of structures with mixed elements + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(CommonProfileTag(2, 1000U)) + .startStructure(AnonymousTag) + .put(ContextSpecificTag(0), 20) + .put(ContextSpecificTag(1), true) + .put(ContextSpecificTag(2), 0.toULong()) + .put(ContextSpecificTag(3), "Test ByteString Value 1".toByteString()) + .put(ContextSpecificTag(4), "hello1") + .put(ContextSpecificTag(5), -500000) + .put(ContextSpecificTag(6), 17.9) + .put(ContextSpecificTag(7), 17.9f) + .endStructure() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(0), -10) + .put(ContextSpecificTag(1), false) + .put(ContextSpecificTag(2), 128.toULong()) + .put(ContextSpecificTag(3), "Test ByteString Value 2".toByteString()) + .put(ContextSpecificTag(4), "hello2") + .put(ContextSpecificTag(5), 40000000000) + .put(ContextSpecificTag(6), -1754.923) + .put(ContextSpecificTag(7), 97.945f) + .endStructure() + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "1000:ARRAY-STRUCT": [ + { + "0:INT": 20, + "1:BOOL": true, + "2:UINT": 0, + "3:BYTES": "VGVzdCBCeXRlU3RyaW5nIFZhbHVlIDE=", + "4:STRING": "hello1", + "5:INT": -500000, + "6:DOUBLE": 17.9, + "7:FLOAT": 17.9 + }, + { + "0:INT": -10, + "1:BOOL": false, + "2:UINT": 128, + "3:BYTES": "VGVzdCBCeXRlU3RyaW5nIFZhbHVlIDI=", + "4:STRING": "hello2", + "5:INT": "40000000000", + "6:DOUBLE": -1754.923, + "7:FLOAT": 97.945 + } + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertingStructureWithMixedElementsAndNames() { + // Array of structures with mixed elements + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startStructure(CommonProfileTag(4, 444444U)) + .startStructure(ContextSpecificTag(0)) + .put(ContextSpecificTag(0), "Test String Element #0") + .put(ContextSpecificTag(1), 1) + .put(ContextSpecificTag(2), true) + .endStructure() + .put(ContextSpecificTag(1), 17.4f) + .startStructure(ContextSpecificTag(2)) + .put(ContextSpecificTag(0), "Test String Element #2") + .put(ContextSpecificTag(3), "Test ByteString Element #3".toByteString()) + .endStructure() + .endStructure() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "value_name:444444:STRUCT": { + "0:STRUCT": { + "name0:0:STRING": "Test String Element #0", + "name1:1:INT": 1, + "name2:2:BOOL": true + }, + "name2:1:FLOAT": 17.4, + "2:STRUCT": { + "name0:0:STRING": "Test String Element #2", + "3:BYTES": "VGVzdCBCeXRlU3RyaW5nIEVsZW1lbnQgIzM=" + } + } + } + """ + val expectedJson = + """ + { + "444444:STRUCT": { + "0:STRUCT": { + "0:STRING": "Test String Element #0", + "1:INT": 1, + "2:BOOL": true + }, + "1:FLOAT": 17.4, + "2:STRUCT": { + "0:STRING": "Test String Element #2", + "3:BYTES": "VGVzdCBCeXRlU3RyaW5nIEVsZW1lbnQgIzM=" + } + } + } + """ + + checkValidConversion(json, encoding, expectedJson) + } + + @Test + fun convertArrayOfStrings() { + // Array of String elements + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(0)) + .put(AnonymousTag, "Test array member 0") + .put(AnonymousTag, "Test array member 1") + .put(AnonymousTag, "Test array member 2") + .put(AnonymousTag, "Test array member 3") + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-STRING": [ + "Test array member 0", + "Test array member 1", + "Test array member 2", + "Test array member 3" + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArrayOfByteStrings() { + // Array of ByteString elements + val values = + listOf( + "Test array member 0".toByteString(), + "Test array member 1".toByteString(), + "Test array member 2".toByteString(), + "Test array member 3".toByteString(), + ) + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .putByteStringArray(ContextSpecificTag(0), values) + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-BYTES": [ + "VGVzdCBhcnJheSBtZW1iZXIgMA==", + "VGVzdCBhcnJheSBtZW1iZXIgMQ==", + "VGVzdCBhcnJheSBtZW1iZXIgMg==", + "VGVzdCBhcnJheSBtZW1iZXIgMw==" + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertTopLevelMixedValues() { + // Top level elements with mixed values + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .put(ContextSpecificTag(0), 42) + .put(ContextSpecificTag(1), "Test array member 0".toByteString()) + .put(ContextSpecificTag(2), 156.398) + .put(ContextSpecificTag(3), 73709551615U) + .put(ContextSpecificTag(4), true) + .putNull(ContextSpecificTag(5)) + .startStructure(ContextSpecificTag(6)) + .put(ContextSpecificTag(1), "John") + .put(ContextSpecificTag(2), 34U) + .put(ContextSpecificTag(3), true) + .startArray(ContextSpecificTag(4)) + .put(AnonymousTag, 5) + .put(AnonymousTag, 9) + .put(AnonymousTag, 10) + .endArray() + .startArray(ContextSpecificTag(5)) + .put(AnonymousTag, "Ammy") + .put(AnonymousTag, "David") + .put(AnonymousTag, "Larry") + .endArray() + .startArray(ContextSpecificTag(6)) + .put(AnonymousTag, true) + .put(AnonymousTag, false) + .put(AnonymousTag, true) + .endArray() + .endStructure() + .put(ContextSpecificTag(7), 0.0f) + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "value:0:INT": 42, + "value:1:BYTES": "VGVzdCBhcnJheSBtZW1iZXIgMA==", + "value:2:DOUBLE": 156.398, + "value:3:UINT": "73709551615", + "value:4:BOOL": true, + "value:5:NULL": null, + "value:6:STRUCT": { + "name:1:STRING": "John", + "age:2:UINT": 34, + "approved:3:BOOL": true, + "kids:4:ARRAY-INT": [ + 5, + 9, + 10 + ], + "names:5:ARRAY-STRING": [ + "Ammy", + "David", + "Larry" + ], + "6:ARRAY-BOOL": [ + true, + false, + true + ] + }, + "value:7:FLOAT": 0.0 + } + """ + val expectedJson = + """ + { + "0:INT": 42, + "1:BYTES": "VGVzdCBhcnJheSBtZW1iZXIgMA==", + "2:DOUBLE": 156.398, + "3:UINT": "73709551615", + "4:BOOL": true, + "5:NULL": null, + "6:STRUCT": { + "1:STRING": "John", + "2:UINT": 34, + "3:BOOL": true, + "4:ARRAY-INT": [ + 5, + 9, + 10 + ], + "5:ARRAY-STRING": [ + "Ammy", + "David", + "Larry" + ], + "6:ARRAY-BOOL": [ + true, + false, + true + ] + }, + "7:FLOAT": 0.0 + } + """ + + checkValidConversion(json, encoding, expectedJson) + } + + @Test + fun convertArray_UIntMinMax() { + // Array of Unsigned Integers, where each element represents MAX possible value for unsigned + // integere + // types UByte, UShort, UInt, and ULong: [0xFF, 0xFFFF, 0xFFFFFFFF, 0xFFFFFFFF_FFFFFFFF] + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(0)) + .put(AnonymousTag, UByte.MAX_VALUE) + .put(AnonymousTag, UShort.MAX_VALUE) + .put(AnonymousTag, UInt.MAX_VALUE) + .put(AnonymousTag, ULong.MAX_VALUE) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-UINT": [ + 255, + 65535, + 4294967295, + "18446744073709551615" + ] + } + """ + + checkValidConversion(json, encoding) + } + + @Test + fun convertArray_IntMinMax() { + // Array of Integers, where each element represents MIN or MAX possible value for integere + // types Byte, Short, Int, and Long + val encoding = + TlvWriter() + .startStructure(AnonymousTag) + .startArray(ContextSpecificTag(0)) + .put(AnonymousTag, Byte.MIN_VALUE) + .put(AnonymousTag, Byte.MAX_VALUE) + .put(AnonymousTag, Short.MIN_VALUE) + .put(AnonymousTag, Short.MAX_VALUE) + .put(AnonymousTag, Int.MIN_VALUE) + .put(AnonymousTag, Int.MAX_VALUE) + .put(AnonymousTag, Long.MIN_VALUE) + .put(AnonymousTag, Long.MAX_VALUE) + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + val json = + """ + { + "0:ARRAY-INT": [ + -128, + 127, + -32768, + 32767, + -2147483648, + 2147483647, + "-9223372036854775808", + "9223372036854775807" + ] + } + """ + + checkValidConversion(json, encoding) + } +} diff --git a/third_party/java_deps/BUILD.gn b/third_party/java_deps/BUILD.gn index a5e7c0629d94be..58260d6d84ceb5 100644 --- a/third_party/java_deps/BUILD.gn +++ b/third_party/java_deps/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Project CHIP Authors +# Copyright (c) 2022-2023 Project CHIP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -44,3 +44,7 @@ java_prebuilt("truth") { java_prebuilt("junit-4") { jar_path = "artifacts/junit-4.13.2.jar" } + +java_prebuilt("gson") { + jar_path = "artifacts/gson-2.9.1.jar" +} diff --git a/third_party/java_deps/set_up_java_deps.sh b/third_party/java_deps/set_up_java_deps.sh index 911a53a638708d..d49ec170129494 100755 --- a/third_party/java_deps/set_up_java_deps.sh +++ b/third_party/java_deps/set_up_java_deps.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # -# Copyright (c) 2022 Project CHIP Authors +# Copyright (c) 2022-2023 Project CHIP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,3 +24,4 @@ curl --fail --location --silent --show-error https://repo1.maven.org/maven2/org/ curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/protobuf/protobuf-java/3.22.0/protobuf-java-3.22.0.jar -o third_party/java_deps/artifacts/protobuf-java-3.22.0.jar curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/truth/truth/1.1.3/truth-1.1.3.jar -o third_party/java_deps/artifacts/truth-1.1.3.jar curl --fail --location --silent --show-error https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar -o third_party/java_deps/artifacts/junit-4.13.2.jar +curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/code/gson/gson/2.9.1/gson-2.9.1.jar -o third_party/java_deps/artifacts/gson-2.9.1.jar