blob: 91b9f080a9031197759c5c1a8f807213e248fe50 [file] [log] [blame]
/*
*
* 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.jsontlv
import com.google.gson.JsonArray
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import java.util.Base64
import chip.tlv.*
/**
* 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<JsonArray, String> {
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
}