Skip to content

Commit

Permalink
Implemented Json to Tlv and Tlv to Json Converter in Kotlin (#26458)
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 authored and pull[bot] committed Oct 19, 2023
1 parent 568fea4 commit c333bde
Show file tree
Hide file tree
Showing 8 changed files with 2,093 additions and 6 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
35 changes: 34 additions & 1 deletion 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 @@ -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"

Expand Down
201 changes: 201 additions & 0 deletions src/controller/java/src/chip/json/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.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<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()
}
Loading

0 comments on commit c333bde

Please sign in to comment.