blob: 80909d4d5f999f1c9a315c3614c6f3edc412538c [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.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import java.util.Base64
import chip.tlv.*
/**
* 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(): ByteArray {
return 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()
}