| /* |
| * |
| * 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 |
| } |