Skip to content

Commit

Permalink
Implemented Json to Tlv and Tlv to Json Converter in Kotlin
Browse files Browse the repository at this point in the history
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
  • Loading branch information
emargolis committed May 11, 2023
1 parent 023b023 commit f701f75
Show file tree
Hide file tree
Showing 8 changed files with 2,082 additions and 7 deletions.
3 changes: 2 additions & 1 deletion examples/java-matter-controller/BUILD.gn
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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",
Expand Down
25 changes: 23 additions & 2 deletions src/controller/java/BUILD.gn
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -146,11 +146,16 @@ if (chip_link_tests) {
kotlin_library("tlv") {
output_name = "libCHIPTlv.jar"

deps = [ "${chip_root}/third_party/java_deps:protobuf-java" ]
deps = [
"${chip_root}/third_party/java_deps:protobuf-java",
"${chip_root}/third_party/java_deps:gson",
]

sources = [
"src/chip/tlv/Element.kt",
"src/chip/tlv/JsonToTlv.kt",
"src/chip/tlv/TlvReader.kt",
"src/chip/tlv/TlvToJson.kt",
"src/chip/tlv/TlvWriter.kt",
"src/chip/tlv/tags.kt",
"src/chip/tlv/types.kt",
Expand Down Expand Up @@ -204,6 +209,22 @@ kotlin_library("tlv_read_write_test") {
kotlinc_flags = [ "-Xlint:deprecation" ]
}

kotlin_library("json_to_tlv_to_json_test") {
output_name = "JsonToTlvToJsonTest.jar"

deps = [
":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/tlv/JsonToTlvToJsonTest.kt" ]

kotlinc_flags = [ "-Xlint:deprecation" ]
}

android_library("java") {
output_name = "CHIPController.jar"

Expand Down
201 changes: 201 additions & 0 deletions src/controller/java/src/chip/tlv/JsonToTlv.kt
Original file line number Diff line number Diff line change
@@ -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.tlv

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<Tag, String, String> {
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()
}

0 comments on commit f701f75

Please sign in to comment.