From 1027722047556230ab219f234d935407b3f92d3c Mon Sep 17 00:00:00 2001 From: Yufeng Wang Date: Thu, 2 Mar 2023 11:15:29 -0800 Subject: [PATCH] [Java] Add the TLVReader/TLVWriter lib for generic IM write/invoke APIs (#25411) * [java] Add tlv writer and reader lib * Address review comments --- .github/workflows/smoketest-android.yaml | 2 +- .github/workflows/tests.yaml | 2 +- src/controller/java/BUILD.gn | 23 +- src/controller/java/src/chip/tlv/Element.kt | 27 ++ src/controller/java/src/chip/tlv/TlvReader.kt | 385 ++++++++++++++++++ src/controller/java/src/chip/tlv/TlvWriter.kt | 359 ++++++++++++++++ src/controller/java/src/chip/tlv/tags.kt | 179 ++++++++ src/controller/java/src/chip/tlv/types.kt | 167 ++++++++ src/controller/java/src/chip/tlv/utils.kt | 64 +++ src/controller/java/src/chip/tlv/values.kt | 103 +++++ third_party/java_deps/BUILD.gn | 4 + third_party/java_deps/set_up_java_deps.sh | 1 + 12 files changed, 1313 insertions(+), 3 deletions(-) create mode 100644 src/controller/java/src/chip/tlv/Element.kt create mode 100644 src/controller/java/src/chip/tlv/TlvReader.kt create mode 100644 src/controller/java/src/chip/tlv/TlvWriter.kt create mode 100644 src/controller/java/src/chip/tlv/tags.kt create mode 100644 src/controller/java/src/chip/tlv/types.kt create mode 100644 src/controller/java/src/chip/tlv/utils.kt create mode 100644 src/controller/java/src/chip/tlv/values.kt diff --git a/.github/workflows/smoketest-android.yaml b/.github/workflows/smoketest-android.yaml index bf6b2d995d55d0..22c41997ed36d4 100644 --- a/.github/workflows/smoketest-android.yaml +++ b/.github/workflows/smoketest-android.yaml @@ -38,7 +38,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: connectedhomeip/chip-build-android:0.6.44 + image: connectedhomeip/chip-build-android:0.6.46 volumes: - "/tmp/log_output:/tmp/test_logs" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 95d5a4193cd9a7..07c9f8447177fa 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -531,7 +531,7 @@ jobs: runs-on: ubuntu-latest container: - image: connectedhomeip/chip-build:0.6.44 + image: connectedhomeip/chip-build-java:0.6.46 options: --privileged --sysctl "net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=0 net.ipv6.conf.all.forwarding=0" diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index add8ee2f9630f9..840d468ba7a597 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -138,10 +138,31 @@ if (chip_link_tests) { } } +kotlin_library("tlv") { + output_name = "libCHIPTlv.jar" + + deps = [ "${chip_root}/third_party/java_deps:protobuf-java" ] + + sources = [ + "src/chip/tlv/Element.kt", + "src/chip/tlv/TlvReader.kt", + "src/chip/tlv/TlvWriter.kt", + "src/chip/tlv/tags.kt", + "src/chip/tlv/types.kt", + "src/chip/tlv/utils.kt", + "src/chip/tlv/values.kt", + ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + android_library("java") { output_name = "CHIPController.jar" - deps = [ "${chip_root}/third_party/java_deps:annotation" ] + deps = [ + ":tlv", + "${chip_root}/third_party/java_deps:annotation", + ] data_deps = [ ":jni" ] diff --git a/src/controller/java/src/chip/tlv/Element.kt b/src/controller/java/src/chip/tlv/Element.kt new file mode 100644 index 00000000000000..baee623058582d --- /dev/null +++ b/src/controller/java/src/chip/tlv/Element.kt @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-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 + +/** + * Represents a single TLV element. + * + * @property tag the tag of the element and its associated data + * @property value the value of the element + */ +data class Element(val tag: Tag, val value: Value) diff --git a/src/controller/java/src/chip/tlv/TlvReader.kt b/src/controller/java/src/chip/tlv/TlvReader.kt new file mode 100644 index 00000000000000..e281d0ff3d4a7b --- /dev/null +++ b/src/controller/java/src/chip/tlv/TlvReader.kt @@ -0,0 +1,385 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-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.protobuf.ByteString +import java.lang.Double.longBitsToDouble +import java.lang.Float.intBitsToFloat + +/** + * Implements Matter TLV reader that supports all values and tags as defined in the Spec. + * + * @param bytes the bytes to interpret + */ +class TlvReader(bytes: ByteArray) : Iterable { + private val bytes = bytes.copyOf() + private var index = 0 + + /** + * Reads the next element from the TLV. + * + * @throws TlvParsingException if the TLV data was invalid + */ + fun nextElement(): Element { + // Ensure that at least one byte for control data is available for reading. + checkSize("controlByte", 1) + val controlByte = bytes[index] + val elementType = + runCatching { Type.from(controlByte) } + .onFailure { + throw TlvParsingException("Type error at $index for ${controlByte.toBinary()}", it) + } + .getOrThrow() + + index++ + + // Read tag, and advance index past tag bytes + val tag = + runCatching { Tag.from(controlByte, index, bytes) } + .onFailure { + throw TlvParsingException("Tag error at $index for ${controlByte.toBinary()}", it) + } + .getOrThrow() + + index += tag.size + + // Element has either a length section or a fixed number of bytes for the value section. If + // present, length is encoded as 1 or 2 bytes indicating the number of bytes in the value. + val lengthSize = elementType.lengthSize + val valueSize: Int + if (lengthSize > 0) { + checkSize("length", lengthSize) + if (lengthSize > Int.SIZE_BYTES) { + throw TlvParsingException("Length $lengthSize at $index too long") + } + valueSize = bytes.sliceArray(index until index + lengthSize).fromLittleEndianToLong().toInt() + index += lengthSize + } else { + valueSize = elementType.valueSize.toInt() + } + + // Ensure that the encoded length fits in the range of the array, and advance index to the + // next control byte. + checkSize("value", valueSize) + val valueBytes = bytes.sliceArray(index until index + valueSize) + index += valueSize + + // Only supporting a small subset of value types currently. Others will just be interpreted + // as a null value. + val value: Value = + when (elementType) { + is SignedIntType -> IntValue(valueBytes.fromLittleEndianToLong(isSigned = true)) + is UnsignedIntType -> UnsignedIntValue(valueBytes.fromLittleEndianToLong()) + is Utf8StringType -> Utf8StringValue(String(valueBytes, Charsets.UTF_8)) + is ByteStringType -> ByteStringValue(ByteString.copyFrom(valueBytes)) + is BooleanType -> BooleanValue(elementType.value) + is FloatType -> FloatValue(intBitsToFloat(valueBytes.fromLittleEndianToLong().toInt())) + is DoubleType -> DoubleValue(longBitsToDouble(valueBytes.fromLittleEndianToLong())) + is StructureType -> StructureValue + is ArrayType -> ArrayValue + is ListType -> ListValue + is EndOfContainerType -> EndOfContainerValue + else -> NullValue + } + + return Element(tag, value) + } + + /** + * Reads the encoded Long value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getLong(tag: Tag): Long { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is IntValue) { "Unexpected value $value at index $index (expected IntValue)" } + return value.value + } + + /** + * Reads the encoded ULong value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getULong(tag: Tag): ULong { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is UnsignedIntValue) { + "Unexpected value $value at index $index (expected UnsignedIntValue)" + } + return value.value.toULong() + } + + /** + * Reads the encoded Int value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getInt(tag: Tag): Int { + return checkRange(getLong(tag), Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()).toInt() + } + + /** + * Reads the encoded UInt value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getUInt(tag: Tag): UInt { + return checkRange(getULong(tag), UInt.MIN_VALUE.toULong()..UInt.MAX_VALUE.toULong()).toUInt() + } + + /** + * Reads the encoded Short value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getShort(tag: Tag): Short { + return checkRange(getLong(tag), Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong()).toShort() + } + + /** + * Reads the encoded UShort value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getUShort(tag: Tag): UShort { + return checkRange(getULong(tag), UShort.MIN_VALUE.toULong()..UShort.MAX_VALUE.toULong()) + .toUShort() + } + + /** + * Reads the encoded Byte value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getByte(tag: Tag): Byte { + return checkRange(getLong(tag), Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong()).toByte() + } + + /** + * Reads the encoded UByte value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getUByte(tag: Tag): UByte { + return checkRange(getULong(tag), UByte.MIN_VALUE.toULong()..UByte.MAX_VALUE.toULong()).toUByte() + } + + /** + * Reads the encoded Boolean value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getBool(tag: Tag): Boolean { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is BooleanValue) { + "Unexpected value $value at index $index (expected BooleanValue)" + } + return value.value + } + + /** + * Reads the encoded Float value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getFloat(tag: Tag): Float { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is FloatValue) { "Unexpected value $value at index $index (expected FloatValue)" } + return value.value + } + + /** + * Reads the encoded Double value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getDouble(tag: Tag): Double { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is DoubleValue) { + "Unexpected value $value at index $index (expected DoubleValue)" + } + return value.value + } + + /** + * Reads the encoded UTF8 String value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getUtf8String(tag: Tag): String { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is Utf8StringValue) { + "Unexpected value $value at index $index (expected Utf8StringValue)" + } + return value.value + } + + /** + * Reads the encoded Octet String value and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getByteString(tag: Tag): ByteString { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is ByteStringValue) { + "Unexpected value $value at index $index (expected ByteStringValue)" + } + return value.value + } + + /** + * Verifies that the current element is Null with expected tag and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun getNull(tag: Tag) { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is NullValue) { "Unexpected value $value at index $index (expected NullValue)" } + } + + /** + * Verifies that the current element is a start of a Structure and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun enterStructure(tag: Tag) { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is StructureValue) { + "Unexpected value $value at index $index (expected StructureValue)" + } + } + + /** + * Verifies that the current element is a start of an Array and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun enterArray(tag: Tag) { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is ArrayValue) { "Unexpected value $value at index $index (expected ArrayValue)" } + } + + /** + * Verifies that the current element is a start of a List and advances to the next element. + * + * @throws TlvParsingException if the element is not of the expected type or tag + */ + fun enterList(tag: Tag) { + val value = nextElement().verifyTagAndGetValue(tag) + require(value is ListValue) { "Unexpected value $value at index $index (expected ListValue)" } + } + + /** + * Completes the reading of a Tlv container and prepares to read elements after the container. + * + * Note that if a TlvReader is not currently positioned at the EndOfContainerValue then function + * skips all unread element in container until it finds the end. + * + * @throws TlvParsingException if the end of the container element is not found + */ + fun exitContainer() { + var relevantDepth = 1 + while (relevantDepth > 0) { + val value = nextElement().value + if (value is EndOfContainerValue) { + relevantDepth-- + } else if (value is StructureValue || value is ArrayValue || value is ListValue) { + relevantDepth++ + } + } + } + + private fun Element.verifyTagAndGetValue(expectedTag: Tag): Value { + require(tag == expectedTag) { + "Unexpected value tag $tag at index $index (expected $expectedTag)" + } + return value + } + + /** + * Skips the current element and advances to the next element. + * + * @throws TlvParsingException if the TLV data was invalid + */ + fun skipElement() { + nextElement() + } + + /** Returns the total number of bytes read since the TlvReader was initialized. */ + fun getLengthRead(): Int { + return index + } + + /** Returns the total number of bytes that can be read until the end of TLV data is reached. */ + fun getRemainingLength(): Int { + return bytes.size - index + } + + /** Returns true if TlvReader is positioned at the end of container. */ + fun isEndOfContainer(): Boolean { + // Ensure that at least one byte for control data is available for reading. + checkSize("controlByte", 1) + return bytes[index] == EndOfContainerType.encode() + } + + /** Returns true if TlvReader reached the end of Tlv data. Returns false otherwise. */ + fun isEndOfTlv(): Boolean { + return bytes.size == index + } + + /** Resets the reader to the start of the provided byte array. */ + fun reset() { + index = 0 + } + + override fun iterator(): Iterator { + return object : AbstractIterator() { + override fun computeNext() { + if (index < bytes.size) { + setNext(nextElement()) + } else { + done() + } + } + } + } + + private fun checkSize(propertyName: String, size: Number) { + if (index + size.toInt() > bytes.size) { + throw TlvParsingException( + "Invalid $propertyName length $size at index $index with ${bytes.size - index} available." + ) + } + } + + private fun > checkRange( + value: T, + range: ClosedRange, + message: String = "Value $value at index $index is out of range $range" + ): T { + if (value !in range) { + throw TlvParsingException(message) + } + return value + } +} + +/** Exception thrown if there was an issue decoding the Matter TLV data. */ +class TlvParsingException internal constructor(msg: String, cause: Throwable? = null) : + RuntimeException(msg, cause) diff --git a/src/controller/java/src/chip/tlv/TlvWriter.kt b/src/controller/java/src/chip/tlv/TlvWriter.kt new file mode 100644 index 00000000000000..ace70593d6cfb8 --- /dev/null +++ b/src/controller/java/src/chip/tlv/TlvWriter.kt @@ -0,0 +1,359 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-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.protobuf.ByteString +import java.io.ByteArrayOutputStream + +/** + * Implements Matter TLV writer that supports all values and tags as defined in the Spec. + * + * @param bytes the bytes to interpret + */ +class TlvWriter(initialCapacity: Int = 32) { + private val bytes = ByteArrayOutputStream(/* size= */ initialCapacity) + private var containerDepth: Int = 0 + private var containerType = Array(4) { NullType() } + + /** + * Writes the next element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + private fun put(element: Element): TlvWriter { + val value = element.value + val tag = element.tag + val type = value.toType() + val encodedType = type.encode() + + if (containerDepth == 0) { + require(tag !is ContextSpecificTag) { + "Invalid use of context tag at index ${bytes.size()}: can only be used within a " + + "structure or a list" + } + } + + if (containerDepth > 0 && containerType[containerDepth - 1] is ArrayType) { + require(tag is AnonymousTag) { + "Invalid element tag at index ${bytes.size()}: elements of an array SHALL be anonymous" + } + } + + if (tag is ContextSpecificTag) { + require(tag.tagNumber.toUInt() <= UByte.MAX_VALUE) { + "Invalid context specific tag " + "value ${tag.tagNumber} at index ${bytes.size()}" + } + } + + // Update depth if the element is an end of container + if (value is EndOfContainerValue) { + require(containerDepth > 0) { + "Cannot close container at index ${bytes.size()}, which is not in the open container." + } + containerDepth-- + } + + // Encode control byte and tag + val encodedControlAndTag: ByteArray = + runCatching { Tag.encode(encodedType, tag) } + .onFailure { + throw TlvEncodingException( + "Type error at ${bytes.size()} for ${encodedType.toBinary()}", + it + ) + } + .getOrThrow() + bytes.write(encodedControlAndTag) + + // Encode length and the value + bytes.write(value.encode()) + + // Update depth if the element is a start of container + if (value is StructureValue || value is ArrayValue || value is ListValue) { + if (containerType.size == containerDepth) { + containerType = containerType.plus(type) + } else { + containerType[containerDepth] = type + } + containerDepth++ + } + + return this + } + + /** + * Writes the next Long element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: Long): TlvWriter { + return put(Element(tag, IntValue(value))) + } + + /** + * Writes the next ULong element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: ULong): TlvWriter { + return put(Element(tag, UnsignedIntValue(value.toLong()))) + } + + /** + * Writes the next ULong, UInt, UShort, or UByte element to the TLV. + * + * This method is functionally equivalent to fun put(tag: Tag, value: ULong): TlvWriter is + * required when called from Java, which doesn't support Unsigned integer types. + * + * @throws TlvEncodingException if the data was invalid + */ + fun putUnsigned(tag: Tag, value: Number): TlvWriter { + return put(Element(tag, UnsignedIntValue(value.toLong()))) + } + + /** + * Writes the next Int element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: Int): TlvWriter { + return put(Element(tag, IntValue(value.toLong()))) + } + + /** + * Writes the next UInt element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: UInt): TlvWriter { + return put(Element(tag, UnsignedIntValue(value.toLong()))) + } + + /** + * Writes the next Short element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: Short): TlvWriter { + return put(Element(tag, IntValue(value.toLong()))) + } + + /** + * Writes the next UShort element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: UShort): TlvWriter { + return put(Element(tag, UnsignedIntValue(value.toLong()))) + } + + /** + * Writes the next Byte element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: Byte): TlvWriter { + return put(Element(tag, IntValue(value.toLong()))) + } + + /** + * Writes the next UByte element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: UByte): TlvWriter { + return put(Element(tag, UnsignedIntValue(value.toLong()))) + } + + /** + * Writes the next Boolean element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: Boolean): TlvWriter { + return put(Element(tag, BooleanValue(value))) + } + + /** + * Writes the next Float element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: Float): TlvWriter { + return put(Element(tag, FloatValue(value))) + } + + /** + * Writes the next Double element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: Double): TlvWriter { + return put(Element(tag, DoubleValue(value))) + } + + /** + * Writes the next UTF8 String element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: String): TlvWriter { + return put(Element(tag, Utf8StringValue(value))) + } + + /** + * Writes the next Octet String element to the TLV. + * + * @throws TlvEncodingException if the data was invalid + */ + fun put(tag: Tag, value: ByteString): TlvWriter { + return put(Element(tag, ByteStringValue(value))) + } + + /** + * Writes an array of Signed Long elements to the TLV. + * + * Note that this method can only be used when all elements of an array are of Signed Long type. + * + * @throws TlvEncodingException if the data was invalid + */ + fun putSignedLongArray(tag: Tag, array: LongArray): TlvWriter { + startArray(tag) + array.forEach { put(AnonymousTag, it) } + return endArray() + } + + /** + * Writes an array of Unsigned Long elements to the TLV. + * + * Note that this method can only be used when all elements of an array are of ULong type. + * + * @throws TlvEncodingException if the data was invalid + */ + fun putUnsignedLongArray(tag: Tag, array: LongArray): TlvWriter { + startArray(tag) + array.forEach { put(AnonymousTag, it.toULong()) } + return endArray() + } + + /** + * Writes an array of Octet String elements to the TLV. + * + * Note that this method can only be used when all elements of an array are of Octet String type. + * + * @throws TlvEncodingException if the data was invalid + */ + fun putByteStringArray(tag: Tag, array: List): TlvWriter { + startArray(tag) + array.forEach { put(AnonymousTag, it) } + return endArray() + } + + /** + * Writes the next Null element to the TLV. + * + * @throws TlvEncodingException if the tag was invalid + */ + fun putNull(tag: Tag): TlvWriter { + return put(Element(tag, NullValue)) + } + + /** + * Writes the next element identifying the start of a Structure to the TLV. + * + * @throws TlvEncodingException if the tag was invalid + */ + fun startStructure(tag: Tag): TlvWriter { + return put(Element(tag, StructureValue)) + } + + /** + * Writes the next element identifying the start of an Array to the TLV. + * + * @throws TlvEncodingException if the tag was invalid + */ + fun startArray(tag: Tag): TlvWriter { + return put(Element(tag, ArrayValue)) + } + + /** + * Writes the next element identifying the start of a List to the TLV. + * + * @throws TlvEncodingException if the tag was invalid + */ + fun startList(tag: Tag): TlvWriter { + return put(Element(tag, ListValue)) + } + + /** Writes the End of Container element to the TLV. */ + fun endStructure(): TlvWriter { + require((containerDepth > 0) && (containerType[containerDepth - 1] is StructureType)) { + "Error closing structure at index ${bytes.size()} as currently opened container is not " + + "a structure" + } + return put(Element(AnonymousTag, EndOfContainerValue)) + } + + /** Writes the End of Container element to the TLV. */ + fun endArray(): TlvWriter { + require(containerDepth > 0 && containerType[containerDepth - 1] is ArrayType) { + "Error closing array at index ${bytes.size()} as currently opened container is not an array" + } + return put(Element(AnonymousTag, EndOfContainerValue)) + } + + /** Writes the End of Container element to the TLV. */ + fun endList(): TlvWriter { + require(containerDepth > 0 && containerType[containerDepth - 1] is ListType) { + "Error closing list at index ${bytes.size()} as currently opened container is not a list" + } + return put(Element(AnonymousTag, EndOfContainerValue)) + } + + /** Returns the total number of bytes written since the writer was initialized. */ + fun getLengthWritten(): Int { + return bytes.size() + } + + /** Verifies that all open containers are closed. */ + fun validateTlv(): TlvWriter { + if (containerDepth > 0) { + throw TlvEncodingException( + "Invalid Tlv data at index ${bytes.size()}: $containerDepth containers are not closed" + ) + } + return this + } + + /** Returns the TLV encoded data written since the writer was initialized. */ + fun getEncoded(): ByteArray { + return bytes.toByteArray() + } + + /** Resets the writer state to empty byte array. */ + fun reset() { + bytes.reset() + containerDepth = 0 + containerType = Array(4) { NullType() } + } +} + +/** Exception thrown if there was an issue encoding the TLV data. */ +class TlvEncodingException internal constructor(msg: String, cause: Throwable? = null) : + RuntimeException(msg, cause) diff --git a/src/controller/java/src/chip/tlv/tags.kt b/src/controller/java/src/chip/tlv/tags.kt new file mode 100644 index 00000000000000..bd6128b1baf913 --- /dev/null +++ b/src/controller/java/src/chip/tlv/tags.kt @@ -0,0 +1,179 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-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 kotlin.experimental.and +import kotlin.experimental.or + +private const val TAG_MASK = 0b11100000.toByte() +private const val ANONYMOUS = 0b0.toByte() +private const val CONTEXT_SPECIFIC = 0b00100000.toByte() +private const val COMMON_PROFILE_2 = 0b01000000.toByte() +private const val COMMON_PROFILE_4 = 0b01100000.toByte() +private const val IMPLICIT_PROFILE_2 = 0b10000000.toByte() +private const val IMPLICIT_PROFILE_4 = 0b10100000.toByte() +private const val FULLY_QUALIFIED_6 = 0b11000000.toByte() +private const val FULLY_QUALIFIED_8 = 0b11100000.toByte() + +/** Represents a tag within a TLV element. */ +sealed class Tag { + /** The number of bytes included in this tag. */ + abstract val size: Int + + internal companion object { + /** + * Parses a [Tag] from the given byte array using the control byte to determine the size of the + * tag. + * + * @param controlByte the control byte for the element whose tag is being parsed + * @param startIndex the index within [bytes] at which the tag data starts + * @param bytes the bytes of the TLV element + * @throws IllegalStateException if the byte array is too short to include the required tag data + */ + fun from(controlByte: Byte, startIndex: Int, bytes: ByteArray): Tag { + return when (controlByte and TAG_MASK) { + ANONYMOUS -> AnonymousTag + CONTEXT_SPECIFIC -> { + ContextSpecificTag(checkBytes(startIndex, 1, bytes).first().toUByte().toInt()) + } + COMMON_PROFILE_2 -> { + CommonProfileTag( + size = 2, + tagNumber = checkBytes(startIndex, 2, bytes).fromLittleEndianToLong().toUInt() + ) + } + COMMON_PROFILE_4 -> { + CommonProfileTag( + size = 4, + tagNumber = checkBytes(startIndex, 4, bytes).fromLittleEndianToLong().toUInt() + ) + } + IMPLICIT_PROFILE_2 -> { + ImplicitProfileTag( + size = 2, + tagNumber = checkBytes(startIndex, 2, bytes).fromLittleEndianToLong().toUInt() + ) + } + IMPLICIT_PROFILE_4 -> { + ImplicitProfileTag( + size = 4, + tagNumber = checkBytes(startIndex, 4, bytes).fromLittleEndianToLong().toUInt() + ) + } + FULLY_QUALIFIED_6 -> { + FullyQualifiedTag( + size = 6, + vendorId = checkBytes(startIndex, 2, bytes).fromLittleEndianToLong().toUShort(), + profileNumber = + checkBytes(startIndex + 2, 2, bytes).fromLittleEndianToLong().toUShort(), + tagNumber = checkBytes(startIndex + 4, 2, bytes).fromLittleEndianToLong().toUInt() + ) + } + FULLY_QUALIFIED_8 -> { + FullyQualifiedTag( + size = 8, + vendorId = checkBytes(startIndex, 2, bytes).fromLittleEndianToLong().toUShort(), + profileNumber = + checkBytes(startIndex + 2, 2, bytes).fromLittleEndianToLong().toUShort(), + tagNumber = checkBytes(startIndex + 4, 4, bytes).fromLittleEndianToLong().toUInt() + ) + } + else -> throw IllegalArgumentException("Invalid control byte $controlByte") + } + } + + /** + * Encode control byte and a tag as a TLV byte array from a [Tag] object. + * + * @param encodedType the partially encoded control byte with element type information + * @param tag the tag of the encoded element + * @throws IllegalStateException if the byte array is too short to include the required tag data + */ + fun encode(encodedType: Byte, tag: Tag): ByteArray { + // Encode control byte + val controlByte = + encodedType or + when (tag) { + is AnonymousTag -> ANONYMOUS + is ContextSpecificTag -> CONTEXT_SPECIFIC + is CommonProfileTag -> if (tag.size == 2) COMMON_PROFILE_2 else COMMON_PROFILE_4 + is ImplicitProfileTag -> if (tag.size == 2) IMPLICIT_PROFILE_2 else IMPLICIT_PROFILE_4 + is FullyQualifiedTag -> if (tag.size == 6) FULLY_QUALIFIED_6 else FULLY_QUALIFIED_8 + } + + // Encode tag + val encodedTag = + when (tag) { + is AnonymousTag -> byteArrayOf() + is ContextSpecificTag -> { + require(tag.tagNumber.toUInt() <= UByte.MAX_VALUE) { + "Invalid tag value ${tag.tagNumber} for context specific tag" + } + byteArrayOf(tag.tagNumber.toByte()) + } + is CommonProfileTag -> tag.tagNumber.toByteArrayLittleEndian(tag.size.toShort()) + is ImplicitProfileTag -> tag.tagNumber.toByteArrayLittleEndian(tag.size.toShort()) + is FullyQualifiedTag -> { + tag.vendorId.toByteArrayLittleEndian(2) + + tag.profileNumber.toByteArrayLittleEndian(2) + + tag.tagNumber.toByteArrayLittleEndian((tag.size - 4).toShort()) + } + } + + return byteArrayOf(controlByte) + encodedTag + } + + private fun checkBytes(startIndex: Int, expectedBytes: Int, actualBytes: ByteArray): ByteArray { + val remaining = actualBytes.size - startIndex + if (expectedBytes > remaining) { + throw IllegalStateException( + "Invalid tag: Expected $expectedBytes but only $remaining bytes available at $startIndex" + ) + } + return actualBytes.sliceArray(startIndex until startIndex + expectedBytes) + } + } +} + +/** An anonymous tag encoding no data. */ +object AnonymousTag : Tag() { + override val size: Int = 0 +} + +/** A context-specific tag including a tag number within a structure. */ +data class ContextSpecificTag(val tagNumber: Int) : Tag() { + override val size: Int = 1 +} + +/** A common-profile tag including a tag number within a structure. */ +data class CommonProfileTag(override val size: Int, val tagNumber: UInt) : Tag() + +/** An implicit-profile tag including a tag number within a structure. */ +data class ImplicitProfileTag(override val size: Int, val tagNumber: UInt) : Tag() + +/** + * A fully-qualified tag including a vendor identifier, a profile number and a tag number within a + * structure. + */ +data class FullyQualifiedTag( + override val size: Int, + val vendorId: UShort, + val profileNumber: UShort, + val tagNumber: UInt +) : Tag() diff --git a/src/controller/java/src/chip/tlv/types.kt b/src/controller/java/src/chip/tlv/types.kt new file mode 100644 index 00000000000000..d4949877b783f3 --- /dev/null +++ b/src/controller/java/src/chip/tlv/types.kt @@ -0,0 +1,167 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-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 kotlin.experimental.and +import kotlin.experimental.or + +private const val MODIFIED_TYPE_MASK = 0b00011100.toByte() +private const val ELEMENT_TYPE_MASK = 0b00011111.toByte() +private const val SIGNED_INT_TYPE = 0b00000000.toByte() +private const val UNSIGNED_INT_TYPE = 0b00000100.toByte() +private const val UTF8_STRING_TYPE = 0b00001100.toByte() +private const val BYTE_STRING_TYPE = 0b00010000.toByte() +private const val BOOLEAN_FALSE = 0b01000.toByte() +private const val BOOLEAN_TRUE = 0b01001.toByte() +private const val FLOATING_POINT_4 = 0b01010.toByte() +private const val FLOATING_POINT_8 = 0b01011.toByte() +private const val NULL = 0b10100.toByte() +private const val STRUCTURE = 0b10101.toByte() +private const val ARRAY = 0b10110.toByte() +private const val LIST = 0b10111.toByte() +private const val END_OF_CONTAINER = 0b11000.toByte() + +/** + * Represents the type of element for a TLV element. + * + * @property lengthSize the size, in bytes, of the length section of this element + * @property valueSize the size, in bytes, of this element's value section + */ +internal sealed class Type(val lengthSize: Short, val valueSize: Short) { + abstract fun encode(): Byte + + internal companion object { + /** Returns the element type encoded by the given control byte. */ + fun from(controlByte: Byte): Type { + // Integer and string types encode the length in the lower two bits. For these types, + // ignore the lower 2 bits for matching, and extract that size later. + val modifiedControlByte = + when (val byte = controlByte and MODIFIED_TYPE_MASK) { + SIGNED_INT_TYPE, + UNSIGNED_INT_TYPE, + UTF8_STRING_TYPE, + BYTE_STRING_TYPE -> byte + else -> controlByte and ELEMENT_TYPE_MASK + } + + return when (modifiedControlByte) { + SIGNED_INT_TYPE -> SignedIntType(extractSize(controlByte)) + UNSIGNED_INT_TYPE -> UnsignedIntType(extractSize(controlByte)) + UTF8_STRING_TYPE -> Utf8StringType(extractSize(controlByte)) + BYTE_STRING_TYPE -> ByteStringType(extractSize(controlByte)) + BOOLEAN_FALSE -> BooleanType(false) + BOOLEAN_TRUE -> BooleanType(true) + FLOATING_POINT_4 -> FloatType() + FLOATING_POINT_8 -> DoubleType() + NULL -> NullType() + STRUCTURE -> StructureType() + ARRAY -> ArrayType() + LIST -> ListType() + END_OF_CONTAINER -> EndOfContainerType + else -> + throw IllegalStateException( + "Unexpected control byte ${modifiedControlByte.toBinaryString()}" + ) + } + } + + private fun Byte.toBinaryString() = Integer.toBinaryString(0xFF and this.toInt()) + + private fun extractSize(byte: Byte): Short { + // Variably-sized element types encode their length in the lower 2 bits. + return when (byte and 0b011) { + 0b000.toByte() -> 1 + 0b001.toByte() -> 2 + 0b010.toByte() -> 4 + else -> 8 + } + } + } +} + +private fun encodeSize(size: Short): Byte { + // Variably-sized element types encode their length in the lower 2 bits. + return when (size.toInt()) { + 1 -> 0b000.toByte() + 2 -> 0b001.toByte() + 4 -> 0b010.toByte() + 8 -> 0b011.toByte() + else -> throw IllegalStateException("Unexpected size ${size}") + } +} + +/** Represents a signed integer value. */ +internal class SignedIntType(valueSize: Short) : Type(0, valueSize) { + override fun encode() = SIGNED_INT_TYPE or encodeSize(valueSize) +} + +/** Represents an unsigned integer value as a Long. */ +internal class UnsignedIntType(valueSize: Short) : Type(0, valueSize) { + override fun encode() = UNSIGNED_INT_TYPE or encodeSize(valueSize) +} + +/** Represents a boolean value. */ +internal class BooleanType(val value: Boolean) : Type(0, 0) { + override fun encode() = if (value) BOOLEAN_TRUE else BOOLEAN_FALSE +} + +/** Represents a floating-point float value. */ +internal class FloatType : Type(0, 4) { + override fun encode() = FLOATING_POINT_4 +} + +/** Represents a floating-point double value. */ +internal class DoubleType : Type(0, 8) { + override fun encode() = FLOATING_POINT_8 +} + +/** Represents a UTF-8 string value. */ +internal class Utf8StringType(lengthSize: Short) : Type(lengthSize, 0) { + override fun encode() = UTF8_STRING_TYPE or encodeSize(lengthSize) +} + +/** Represents a byte string value. */ +internal class ByteStringType(lengthSize: Short) : Type(lengthSize, 0) { + override fun encode() = BYTE_STRING_TYPE or encodeSize(lengthSize) +} + +/** Represents a null value. */ +internal class NullType : Type(0, 0) { + override fun encode() = NULL +} + +/** Represents a structure container type. */ +internal class StructureType : Type(0, 0) { + override fun encode() = STRUCTURE +} + +/** Represents an array container type. */ +internal class ArrayType : Type(0, 0) { + override fun encode() = ARRAY +} + +/** Represents a list container type. */ +internal class ListType : Type(0, 0) { + override fun encode() = LIST +} + +/** Represents the end of a container type. */ +internal object EndOfContainerType : Type(0, 0) { + override fun encode() = END_OF_CONTAINER +} diff --git a/src/controller/java/src/chip/tlv/utils.kt b/src/controller/java/src/chip/tlv/utils.kt new file mode 100644 index 00000000000000..e68e7482045495 --- /dev/null +++ b/src/controller/java/src/chip/tlv/utils.kt @@ -0,0 +1,64 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-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 + +/** Converts bytes in a Little Endian format into Long integer. */ +internal fun ByteArray.fromLittleEndianToLong(isSigned: Boolean = false): Long = + foldRightIndexed(0) { i, it, acc -> + (acc shl 8) or (if (i == lastIndex && isSigned) it.toLong() else (it.toLong() and 0xFF)) + } + +/** Converts Number into a byte array in a Little Endian format. */ +internal fun Number.toByteArrayLittleEndian(numBytes: Short): ByteArray = + toLong().toLittleEndianBytes(numBytes) + +internal fun UByte.toByteArrayLittleEndian(numBytes: Short): ByteArray = + toLong().toLittleEndianBytes(numBytes) + +internal fun UShort.toByteArrayLittleEndian(numBytes: Short): ByteArray = + toLong().toLittleEndianBytes(numBytes) + +internal fun UInt.toByteArrayLittleEndian(numBytes: Short): ByteArray = + toLong().toLittleEndianBytes(numBytes) + +internal fun ULong.toByteArrayLittleEndian(numBytes: Short): ByteArray = + toLong().toLittleEndianBytes(numBytes) + +private fun Long.toLittleEndianBytes(numBytes: Short) = + ByteArray(numBytes.toInt()) { i -> (this shr (8 * i)).toByte() } + +internal fun signedIntSize(value: Long): Short { + return when (value) { + in Byte.MIN_VALUE..Byte.MAX_VALUE -> 1 + in Short.MIN_VALUE..Short.MAX_VALUE -> 2 + in Int.MIN_VALUE..Int.MAX_VALUE -> 4 + else -> 8 + } +} + +internal fun unsignedIntSize(value: ULong): Short { + return when { + value <= UByte.MAX_VALUE -> 1 + value <= UShort.MAX_VALUE -> 2 + value <= UInt.MAX_VALUE -> 4 + else -> 8 + } +} + +internal fun Byte.toBinary(): String = Integer.toBinaryString(toInt() and 0xFF) diff --git a/src/controller/java/src/chip/tlv/values.kt b/src/controller/java/src/chip/tlv/values.kt new file mode 100644 index 00000000000000..b28eabf3bcd9ca --- /dev/null +++ b/src/controller/java/src/chip/tlv/values.kt @@ -0,0 +1,103 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-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.protobuf.ByteString +import java.lang.Double.doubleToLongBits +import java.lang.Float.floatToIntBits + +/** Represents the value of a TLV element. */ +sealed class Value { + internal abstract fun toType(): Type + internal abstract fun encode(): ByteArray +} + +/** Represents a signed integer value of a TLV element. */ +data class IntValue(val value: Long) : Value() { + override fun toType() = SignedIntType(signedIntSize(value)) + override fun encode() = value.toByteArrayLittleEndian(toType().valueSize) +} + +/** Represents an unsigned integer value of a TLV element. */ +data class UnsignedIntValue(val value: Long) : Value() { + override fun toType() = UnsignedIntType(unsignedIntSize(value.toULong())) + override fun encode() = value.toByteArrayLittleEndian(toType().valueSize) +} + +/** Represents a boolean value of a TLV element. */ +data class BooleanValue(val value: Boolean) : Value() { + override fun toType() = BooleanType(value) + override fun encode() = ByteArray(0) +} + +/** Represents a floating-point Float value of a TLV element. */ +data class FloatValue(val value: Float) : Value() { + override fun toType() = FloatType() + override fun encode() = floatToIntBits(value).toByteArrayLittleEndian(4) +} + +/** Represents a floating-point DoubleFloat value of a TLV element. */ +data class DoubleValue(val value: Double) : Value() { + override fun toType() = DoubleType() + override fun encode() = doubleToLongBits(value).toByteArrayLittleEndian(8) +} + +/** Represents a UTF8 string value of a TLV element. */ +data class Utf8StringValue(val value: String) : Value() { + override fun toType() = Utf8StringType(unsignedIntSize(value.toByteArray().size.toULong())) + override fun encode() = + value.toByteArray().size.toByteArrayLittleEndian(toType().lengthSize) + value.toByteArray() +} + +/** Represents a byte string value of a TLV element. */ +data class ByteStringValue(val value: ByteString) : Value() { + override fun toType() = ByteStringType(unsignedIntSize(value.size().toULong())) + override fun encode() = + value.toByteArray().size.toByteArrayLittleEndian(toType().lengthSize) + value.toByteArray() +} + +/** Represents a null value in a TLV element. */ +object NullValue : Value() { + override fun toType() = NullType() + override fun encode() = ByteArray(0) +} + +/** Represents an empty value for a structure container element. */ +object StructureValue : Value() { + override fun toType() = StructureType() + override fun encode() = ByteArray(0) +} + +/** Represents an array value of a TLV element. */ +object ArrayValue : Value() { + override fun toType() = ArrayType() + override fun encode() = ByteArray(0) +} + +/** Represents a list value of a TLV element. */ +object ListValue : Value() { + override fun toType() = ListType() + override fun encode() = ByteArray(0) +} + +/** Represents an empty value for an end-of-container element. */ +object EndOfContainerValue : Value() { + override fun toType() = EndOfContainerType + override fun encode() = ByteArray(0) +} diff --git a/third_party/java_deps/BUILD.gn b/third_party/java_deps/BUILD.gn index d265a43b33a6fe..0a821fad9bba90 100644 --- a/third_party/java_deps/BUILD.gn +++ b/third_party/java_deps/BUILD.gn @@ -28,3 +28,7 @@ java_prebuilt("json") { java_prebuilt("kotlin-stdlib") { jar_path = "artifacts/kotlin-stdlib-1.8.10.jar" } + +java_prebuilt("protobuf-java") { + jar_path = "artifacts/protobuf-java-3.22.0.jar" +} diff --git a/third_party/java_deps/set_up_java_deps.sh b/third_party/java_deps/set_up_java_deps.sh index 6098e956718055..de2d2c848f2b37 100755 --- a/third_party/java_deps/set_up_java_deps.sh +++ b/third_party/java_deps/set_up_java_deps.sh @@ -20,3 +20,4 @@ mkdir -p third_party/java_deps/artifacts curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar -o third_party/java_deps/artifacts/jsr305-3.0.2.jar curl --fail --location --silent --show-error https://repo1.maven.org/maven2/org/json/json/20220924/json-20220924.jar -o third_party/java_deps/artifacts/json-20220924.jar curl --fail --location --silent --show-error https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.8.10/kotlin-stdlib-1.8.10.jar -o third_party/java_deps/artifacts/kotlin-stdlib-1.8.10.jar +curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/protobuf/protobuf-java/3.22.0/protobuf-java-3.22.0.jar -o third_party/java_deps/artifacts/protobuf-java-3.22.0.jar